- Detailed stack breakdown: runtime, framework, test, bundler, database - Generic integration detection (25+ packages: Clerk, Stripe, OpenAI, Sentry...) - jq fallback: grep-based JSON parsing when jq not installed - Stack recap at top of human output + full stack object in JSON - README prompt updated: Stack Recap first, CLAUDE.md template ~100 lines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
187 lines
5.1 KiB
JavaScript
187 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Claude Code Knowledge Quiz
|
|
* Test your understanding of Claude Code with interactive MCQs
|
|
*
|
|
* Part of: claude-code-ultimate-guide
|
|
* Author: Florian BRUNIAUX
|
|
*/
|
|
|
|
import { createRequire } from 'module';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join } from 'path';
|
|
import { parseArgs } from 'util';
|
|
|
|
import { displayHeader, displayHelp } from './ui.js';
|
|
import { selectProfile, selectTopics } from './prompts.js';
|
|
import { loadQuestions, filterQuestions, shuffleArray } from './questions.js';
|
|
import { runQuiz } from './quiz.js';
|
|
import { displayFinalScore, offerRetry } from './score.js';
|
|
import { loadSession, saveSession } from './session.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// CLI argument parsing
|
|
const { values: args } = parseArgs({
|
|
options: {
|
|
profile: { type: 'string', short: 'p' },
|
|
topics: { type: 'string', short: 't' },
|
|
count: { type: 'string', short: 'c' },
|
|
dynamic: { type: 'boolean', short: 'd', default: false },
|
|
help: { type: 'boolean', short: 'h', default: false },
|
|
version: { type: 'boolean', short: 'v', default: false }
|
|
},
|
|
strict: false
|
|
});
|
|
|
|
// Profile configurations
|
|
const PROFILES = {
|
|
junior: {
|
|
name: 'Junior Developer',
|
|
description: '45 min to productivity',
|
|
questionCount: 15,
|
|
focusTopics: [1, 2, 3, 6],
|
|
difficultyWeight: { junior: 0.8, senior: 0.2, power: 0 }
|
|
},
|
|
senior: {
|
|
name: 'Senior Developer',
|
|
description: '40 min to mastery',
|
|
questionCount: 20,
|
|
focusTopics: [2, 3, 4, 7, 9],
|
|
difficultyWeight: { junior: 0.3, senior: 0.5, power: 0.2 }
|
|
},
|
|
power: {
|
|
name: 'Power User',
|
|
description: '2 hours for full mastery',
|
|
questionCount: 25,
|
|
focusTopics: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
difficultyWeight: { junior: 0.2, senior: 0.4, power: 0.4 }
|
|
},
|
|
pm: {
|
|
name: 'Product Manager',
|
|
description: '20 min overview',
|
|
questionCount: 10,
|
|
focusTopics: [1, 2, 3],
|
|
difficultyWeight: { junior: 1, senior: 0, power: 0 }
|
|
}
|
|
};
|
|
|
|
// Topic names
|
|
const TOPICS = {
|
|
1: 'Quick Start & Installation',
|
|
2: 'Core Concepts',
|
|
3: 'Memory & Settings',
|
|
4: 'Agents',
|
|
5: 'Skills',
|
|
6: 'Commands',
|
|
7: 'Hooks',
|
|
8: 'MCP Servers',
|
|
9: 'Advanced Patterns',
|
|
10: 'Reference & Troubleshooting'
|
|
};
|
|
|
|
async function main() {
|
|
// Handle help/version
|
|
if (args.help) {
|
|
displayHelp();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (args.version) {
|
|
console.log('claude-code-quiz v1.0.0');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Display header
|
|
displayHeader();
|
|
|
|
// Profile selection
|
|
let profile;
|
|
if (args.profile && PROFILES[args.profile]) {
|
|
profile = args.profile;
|
|
} else {
|
|
profile = await selectProfile(PROFILES);
|
|
}
|
|
|
|
// Topic selection
|
|
let selectedTopics;
|
|
if (args.topics) {
|
|
selectedTopics = args.topics.split(',').map(t => parseInt(t.trim())).filter(t => t >= 1 && t <= 10);
|
|
if (selectedTopics.length === 0) selectedTopics = PROFILES[profile].focusTopics;
|
|
} else {
|
|
selectedTopics = await selectTopics(TOPICS, PROFILES[profile].focusTopics);
|
|
}
|
|
|
|
// Question count
|
|
let questionCount = PROFILES[profile].questionCount;
|
|
if (args.count) {
|
|
const parsed = parseInt(args.count);
|
|
if (parsed > 0 && parsed <= 50) questionCount = parsed;
|
|
}
|
|
|
|
// Load and filter questions
|
|
const questionsDir = join(__dirname, '..', 'questions');
|
|
const allQuestions = await loadQuestions(questionsDir);
|
|
const filteredQuestions = filterQuestions(allQuestions, {
|
|
profile,
|
|
topics: selectedTopics,
|
|
count: questionCount,
|
|
difficultyWeight: PROFILES[profile].difficultyWeight
|
|
});
|
|
|
|
if (filteredQuestions.length === 0) {
|
|
console.log('\nNo questions available for selected criteria. Try different topics.');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Shuffle questions
|
|
const quizQuestions = shuffleArray(filteredQuestions).slice(0, questionCount);
|
|
|
|
// Run quiz
|
|
const results = await runQuiz(quizQuestions, {
|
|
profile,
|
|
topics: selectedTopics,
|
|
dynamicEnabled: args.dynamic
|
|
});
|
|
|
|
// Display final score
|
|
displayFinalScore(results, TOPICS);
|
|
|
|
// Save session
|
|
await saveSession(results);
|
|
|
|
// Offer retry
|
|
const retryChoice = await offerRetry(results);
|
|
|
|
if (retryChoice === 'retry-wrong') {
|
|
// Filter to only wrong questions and run again
|
|
const wrongQuestions = results.questions.filter(q => !q.correct);
|
|
if (wrongQuestions.length > 0) {
|
|
const retryResults = await runQuiz(wrongQuestions.map(q => q.question), {
|
|
profile,
|
|
topics: selectedTopics,
|
|
dynamicEnabled: args.dynamic,
|
|
isRetry: true
|
|
});
|
|
displayFinalScore(retryResults, TOPICS);
|
|
await saveSession(retryResults);
|
|
}
|
|
} else if (retryChoice === 'new-quiz') {
|
|
// Restart with different questions
|
|
const newQuestions = shuffleArray(filteredQuestions).slice(0, questionCount);
|
|
const newResults = await runQuiz(newQuestions, {
|
|
profile,
|
|
topics: selectedTopics,
|
|
dynamicEnabled: args.dynamic
|
|
});
|
|
displayFinalScore(newResults, TOPICS);
|
|
await saveSession(newResults);
|
|
}
|
|
// else 'exit' - do nothing
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Error:', err.message);
|
|
process.exit(1);
|
|
});
|