clitutor
Version:
Interactive CLI learning tool for beginners
1,379 lines (1,270 loc) • 38.3 kB
JavaScript
const inquirer = require("inquirer");
const chalk = require("chalk");
const boxen = require("boxen");
const ora = require("ora");
const {
clearScreen,
sleep,
printWithTypingEffect,
validateCommand,
showHint,
} = require("../utils/helpers");
const green = chalk.hex("#00FF00");
const dim = chalk.dim;
const bold = chalk.bold;
const yellow = chalk.yellow;
const lessons = [
{
id: "intro",
title: "Welcome to the Command Line!",
content: async () => {
clearScreen();
console.log(
boxen(green("Chapter 1: Understanding Command Structure"), {
padding: 1,
borderStyle: "double",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nThe command line (CLI) is a text-based way to interact with your computer."
);
await sleep(500);
await printWithTypingEffect(
"Instead of clicking buttons, you type commands to tell your computer what to do."
);
await sleep(500);
await printWithTypingEffect(
"\nThink of it as having a conversation with your computer!"
);
await sleep(1000);
await printWithTypingEffect(
"\nThe objective of this first chapter is to learn the basic structures you need to start talking through your terminal!"
);
await sleep(500);
},
},
{
id: "commands_intro",
title: "Commands: The Building Blocks",
content: async () => {
clearScreen();
console.log(
boxen(green("What Are Commands?"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nCommands are like " +
bold("verbs") +
" in a sentence - they tell the computer what action to take."
);
await sleep(700);
console.log(
boxen(
"Examples of commands:\n\n" +
green("pwd") +
dim(" # Print working directory\n") +
green("ls") +
dim(" # List files\n") +
green("clear") +
dim(" # Clear the screen\n") +
green("echo") +
dim(" # Display text\n") +
green("git") +
dim(" # Version control tool"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect("\nSome commands work by themselves:");
console.log(
boxen(
green("pwd") + "\n" + dim("└─ Complete command, shows your location"),
{
padding: 1,
margin: { top: 1 },
borderColor: "gray",
}
)
);
await sleep(700);
await printWithTypingEffect("\nOthers need more information...");
await sleep(700);
},
},
{
id: "learning_objectives",
title: "What We'll Learn",
content: async () => {
clearScreen();
console.log(
boxen(green("Building Your Command Knowledge"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect("\nImagine you want to tell someone:");
await sleep(700);
await printWithTypingEffect(
"\n" + bold('"Please add this file to my project carefully"')
);
await sleep(1000);
console.log(
boxen(
"In command line, this becomes:\n\n" +
green("git") +
" " +
yellow("add") +
" " +
bold("file.txt") +
" " +
yellow("--verbose") +
"\n\n" +
"Each part has a purpose:\n" +
"• " +
green("git") +
" = The tool (command)\n" +
"• " +
yellow("add") +
" = The action (subcommand)\n" +
"• " +
bold("file.txt") +
" = What to add (argument)\n" +
"• " +
yellow("--verbose") +
" = How to do it (flag)",
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\nLet's break down each part so you can build any command!"
);
},
},
{
id: "subcommands",
title: "Subcommands: Commands Within Commands",
content: async () => {
clearScreen();
console.log(
boxen(green("Commands Can Have Subcommands"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nSome commands are like " +
bold("toolboxes") +
" - they contain multiple tools inside."
);
await sleep(700);
await printWithTypingEffect(
"\n\nLet's use " + green("git") + " as an example."
);
await sleep(700);
console.log(
boxen(
bold("What is Git?") +
"\n\n" +
"Git helps you save different versions of your work,\n" +
"like a time machine for your files!\n\n" +
"You can:\n" +
"• Save snapshots of your work\n" +
"• Go back to earlier versions\n" +
"• Share your work with others\n\n" +
dim(
"(You might have heard of GitHub - that's\n" +
'the "hub" where people share their git projects!)'
),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1500);
await printWithTypingEffect(
"\nBut " +
green("git") +
" alone doesn't do anything - you need to tell it what to do:"
);
await sleep(700);
console.log(
boxen(
green("git") +
dim(' # Just saying "git" - but what action?\n\n') +
green("git") +
" " +
yellow("status") +
dim(" # Check what's changed\n") +
green("git") +
" " +
yellow("add") +
dim(" # Prepare files to save\n") +
green("git") +
" " +
yellow("commit") +
dim(" # Save a snapshot\n") +
green("git") +
" " +
yellow("push") +
dim(" # Upload to the internet"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
"Structure: " +
green("command") +
" " +
yellow("subcommand") +
"\n\n" +
"The subcommand tells the command " +
bold("what specific action") +
" to take.",
{
padding: 1,
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
'\nFor simplicity, we will use the term "command + subcommand", but it can also be called ' +
bold("program + command") +
"."
);
await sleep(1000);
await printWithTypingEffect("\nLet's see how this works in practice!");
await sleep(1000);
},
},
{
id: "arguments",
title: "Arguments: The Target",
content: async () => {
clearScreen();
console.log(
boxen(green("Arguments: What to Act On"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nArguments are like " +
bold("objects") +
" in a sentence - they tell commands what to work with."
);
await sleep(700);
await printWithTypingEffect("\n\nLet's see what you're really saying:");
await sleep(700);
console.log(
boxen(
"Commands without arguments often feel incomplete:\n\n" +
green("echo") +
dim(' # "Computer, echo!" ...echo what?\n') +
green("echo") +
" " +
bold('"Hello"') +
dim(' # "Computer, echo Hello!"\n\n') +
green("git add") +
dim(' # "Git, add!" ...add what?\n') +
green("git add") +
" " +
bold("file.txt") +
dim(' # "Git, add file.txt!"'),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("What you're saying with git commands:") +
"\n\n" +
green("git") +
" " +
yellow("add") +
" " +
bold(".") +
"\n" +
dim('→ "Git, add everything in this folder"\n\n') +
green("git") +
" " +
yellow("add") +
" " +
bold("*.js") +
"\n" +
dim('→ "Git, add all JavaScript files"\n\n') +
green("git") +
" " +
yellow("add") +
" " +
bold("README.md") +
"\n" +
dim('→ "Git, add the README.md file"'),
{
padding: 1,
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\nArguments complete your command's meaning!"
);
await sleep(1000);
// Terminal says box
console.log(
boxen(
"💻 " + bold("Terminal says:") + "\n\n" +
"\"When you give me a command without arguments,\n" +
"I often feel confused! It's like saying:\n\n" +
"'Hey, add!' ...add what? 🤷\n" +
"'Hey, delete!' ...delete what? 😰\n\n" +
"Arguments help me understand what you want!\"",
{
padding: 1,
margin: { top: 1 },
borderColor: "cyan",
borderStyle: "round",
}
)
);
},
},
{
id: "flags",
title: "Flags: Modifying Behavior",
content: async () => {
clearScreen();
console.log(
boxen(green("Flags: How to Do It"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nFlags are like " +
bold("adverbs") +
" - they modify HOW a command works."
);
await sleep(700);
await printWithTypingEffect(
"\n\nThink of them as switches or settings you can turn on."
);
await sleep(700);
console.log(
boxen(
bold("Two styles of flags:\n\n") +
yellow("Short flags: ") +
green("-") +
" followed by a letter\n" +
" Examples: -m, -a, -v\n\n" +
yellow("Long flags: ") +
green("--") +
" followed by a word\n" +
" Examples: --message, --all, --verbose",
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
"Real examples:\n\n" +
green("ls") +
" " +
yellow("-l") +
dim(" # List with details\n") +
green("git status") +
" " +
yellow("--short") +
dim(" # Shorter output\n") +
green("git commit") +
" " +
yellow("-m") +
' "Fix"' +
dim(" # Flag with value"),
{
padding: 1,
borderColor: "gray",
}
)
);
await sleep(700);
await printWithTypingEffect(
"\nFlags answer the question: " + bold('"How?"')
);
await sleep(1000);
console.log(
boxen(green("Where Can Flags Go?"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
margin: { top: 1 },
})
);
await printWithTypingEffect(
"\nFor beginners, we show flags in a standard order:"
);
await sleep(700);
console.log(
boxen(
bold("Standard order (easiest to read):\n") +
green("command") +
" " +
yellow("[subcommand]") +
" " +
yellow("[flags]") +
" " +
bold("[arguments]") +
"\n\n" +
green("git") +
" " +
yellow("commit") +
" " +
yellow("-m") +
" " +
bold('"Message"') +
"\n" +
green("ls") +
" " +
yellow("-la") +
" " +
bold("/home"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n" + bold("But here's the secret:") + " Flags are flexible!"
);
await sleep(700);
console.log(
boxen(
bold("These all work the same:\n\n") +
green("ls") +
" " +
yellow("-l") +
" " +
bold("/home") +
dim(" # Flag before argument\n") +
green("ls") +
" " +
bold("/home") +
" " +
yellow("-l") +
dim(" # Flag after argument\n") +
green("git") +
" " +
yellow("-c color.ui=true") +
" status" +
dim(" # Flag before subcommand!"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n📚 For now, stick to the standard order. As you advance, you'll use this flexibility!"
);
await sleep(1000);
// Terminal says box
console.log(
boxen(
"💻 " + bold("Terminal says:") + "\n\n" +
"\"Flags are like my preference settings!\n\n" +
"Without flags: I do things my default way 🤖\n" +
"With flags: I follow your special instructions! ✨\n\n" +
"For example:\n" +
green("ls") + " → 'Show files' (basic)\n" +
green("ls") + " " + yellow("-la") + " → 'Show files with ALL details!' (fancy)\"",
{
padding: 1,
margin: { top: 1 },
borderColor: "cyan",
borderStyle: "round",
}
)
);
},
},
{
id: "argument_types",
title: "Arguments: For Commands or Flags",
content: async () => {
clearScreen();
console.log(
boxen(green("Where Arguments Belong"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nNow that you know commands, arguments, and flags..."
);
await sleep(700);
await printWithTypingEffect(
"\nHere's something important: Arguments can work with " +
bold("different parts") +
" of your command!"
);
await sleep(1000);
console.log(
boxen(
bold("Arguments answer 'what?' for different parts:\n\n") +
"1️⃣ " + bold("Arguments for Commands/Subcommands") + "\n" +
"These tell the command WHAT to work with:\n\n" +
green("echo") +
" " +
bold('"Hello World"') +
"\n" +
dim(" └─ Tells echo WHAT to display\n\n") +
green("git add") +
" " +
bold("README.md") +
"\n" +
dim(" └─ Tells git add WHAT file to add"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
"2️⃣ " + bold("Arguments for Flags") + "\n" +
"Some flags need their own arguments:\n\n" +
green("git commit") +
" " +
yellow("-m") +
" " +
bold('"Fix bug"') +
"\n" +
dim(" └─ The -m flag needs a message\n\n") +
green("head") +
" " +
yellow("-n") +
" " +
bold("20") +
" file.txt\n" +
dim(" └─ The -n flag needs a number\n") +
dim(" └─ Command argument"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n🎯 The key insight: Arguments always answer 'what?' - but they might be answering that question for different parts of your command!"
);
await sleep(1000);
console.log(
boxen(
bold("Real example with both types:\n\n") +
green("git") + " " + yellow("log") + " " + yellow("--since") + " " + bold('"2 weeks ago"') + " " + bold("main") + "\n\n" +
"Breaking it down:\n" +
"• " + green("git") + " = Command\n" +
"• " + yellow("log") + " = Subcommand\n" +
"• " + yellow("--since") + " = Flag that needs a date\n" +
"• " + bold('"2 weeks ago"') + " = Argument for the flag\n" +
"• " + bold("main") + " = Argument for git log\n\n" +
dim("Translation: 'Git, show me the log of main branch since 2 weeks ago'"),
{
padding: 1,
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n💡 Pro tip: Flag arguments usually come right after their flag!"
);
},
},
{
id: "handling_spaces",
title: "Handling Spaces in Commands",
content: async () => {
clearScreen();
console.log(
boxen(green("The Space Challenge"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nThe command line has one quirk you need to know..."
);
await sleep(700);
await printWithTypingEffect(
"\n" + bold("Spaces") + " are special - they separate the parts of your command!"
);
await sleep(1000);
console.log(
boxen(
"Think of spaces like commas in a list:\n\n" +
green("echo") + " Hello World\n" +
dim("└─ The computer sees: echo, Hello, World\n") +
" (3 separate things!)\n\n" +
"That's why only 'Hello' gets printed!",
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
// Command flow visualization
console.log(
boxen(
bold("Visual: How the computer reads your command\n\n") +
"YOU TYPE: COMPUTER SEES:\n" +
green("echo") + " Hello World ┌──────┐ ┌───────┐ ┌───────┐\n" +
" ↓ │ echo │→│ Hello │→│ World │\n" +
dim(" \"one thing\" ") + "└──────┘ └───────┘ └───────┘\n" +
" " + dim("\"three separate things\""),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\nSo how do we tell the computer that words belong together?"
);
await sleep(700);
await printWithTypingEffect(
"\n" + bold("Use quotes!") + " They're like a box that keeps words together."
);
await sleep(1000);
console.log(
boxen(
bold("Without quotes = Multiple pieces:\n") +
green("echo") +
" Hello World" +
dim(" → echo gets 'Hello' and 'World' separately\n\n") +
bold("With quotes = One piece:\n") +
green("echo") +
' "Hello World"' +
dim(' → echo gets "Hello World" as one unit'),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Real-world examples:\n\n") +
"Creating a folder with spaces:\n" +
green("mkdir") + " " + bold('"My Projects"') + "\n\n" +
"Git commit with a sentence:\n" +
green("git commit -m") + " " + bold('"Fix login bug"') + "\n\n" +
"Searching for a phrase:\n" +
green("grep") + " " + bold('"error occurred"') + " log.txt",
{
padding: 1,
borderColor: "yellow",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n🎯 Remember: Quotes are your friends when dealing with spaces!"
);
await sleep(700);
console.log(
boxen(
yellow("Quick tip:") + " Both work the same:\n" +
'• Double quotes: "Hello World"\n' +
"• Single quotes: 'Hello World'\n\n" +
dim("(We'll learn the subtle differences later!)"),
{
padding: 1,
margin: { top: 1 },
borderColor: "gray",
}
)
);
},
},
{
id: "complete_picture",
title: "The Complete Picture",
content: async () => {
clearScreen();
console.log(
boxen(green("Putting It All Together"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect("\nCommands can combine all these parts:");
await sleep(700);
console.log(
boxen(
green("command") +
" " +
yellow("[subcommand]") +
" " +
yellow("[flags]") +
" " +
bold("[arguments]") +
"\n\n" +
"Like a sentence:\n" +
bold("VERB") +
" " +
bold("ACTION") +
" " +
bold("HOW") +
" " +
bold("WHAT") +
"\n\n" +
dim(
"(This is the standard order for clarity, but flags can be flexible!)"
),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
"Let's analyze:\n\n" +
green("git") +
" " +
yellow("commit") +
" " +
yellow("-m") +
" " +
bold('"Initial commit"') +
"\n\n" +
"• " +
green("git") +
" = Command (the tool)\n" +
"• " +
yellow("commit") +
" = Subcommand (save changes)\n" +
"• " +
yellow("-m") +
" = Flag (message flag)\n" +
"• " +
bold('"Initial commit"') +
" = Argument (the message)",
{
padding: 1,
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\nNot all commands use every part - use only what's needed!"
);
},
},
];
// Simplified exercises focused on core concepts
const exercises = [
{
id: "what_are_commands",
type: "multiple_choice",
title: "📚 Recognizing Commands",
question:
"You want to check what's changed in your project. Which part is the COMMAND in: " +
green("git") +
" " +
yellow("status") +
"?",
choices: [
green("git") + " - the main program you're talking to",
yellow("status") + " - the action to perform",
"Both are commands",
"Neither is a command",
],
answer: 0,
explanation:
green("git") +
" is the command - it's like saying 'Hey Git!' to get its attention. The " +
yellow("status") +
" part tells Git what you want it to do.",
wrongAnswerHints: {
1: "That's the subcommand - it tells the command what to do!",
2: "Only one is the main command - the other is a subcommand.",
3: "Look again - one of these is definitely a command!",
},
},
{
id: "what_are_arguments",
type: "multiple_choice",
title: "🎯 Understanding Arguments",
question:
"You're telling Git to add a file. In: " +
green("git add") +
" " +
bold("README.md") +
"\nWhat role does " +
bold("README.md") +
" play?",
choices: [
"It's another command",
"It's a flag that modifies behavior",
"It's the target - what Git should add",
"It's a subcommand",
],
answer: 2,
explanation:
bold("README.md") +
" is an argument - it answers the question 'add WHAT?' It's like telling your computer: 'Hey Git, add README.md to my project!'",
wrongAnswerHints: {
0: "Commands are the main programs like 'git' - this is something else!",
1: "Flags start with - or -- and modify HOW things work.",
3: "Subcommands are actions like 'add' or 'commit' - this is a file name!",
},
},
{
id: "flag_types",
type: "multiple_choice",
title: "🚩 Recognizing Flags",
question:
"You see this command: " +
green("ls") +
" " +
yellow("-la") +
" " +
bold("/home") +
"\nWhich part is the flag that changes HOW ls works?",
choices: [
green("ls") + " - the command",
yellow("-la") + " - it starts with a dash",
bold("/home") + " - the directory",
"There is no flag in this command",
],
answer: 1,
explanation:
yellow("-la") +
" is the flag! The dash (-) is the giveaway. It's like saying: 'Hey ls, list the files, but do it with -l (long format) and -a (show all files)!'",
wrongAnswerHints: {
0: "That's the command itself - flags modify how commands work!",
2: "That's an argument telling ls WHERE to look.",
3: "Look for something starting with - or -- !",
},
},
{
id: "handling_spaces",
type: "multiple_choice",
title: "💬 Talking with Spaces",
question:
"You type: " +
green("echo") +
" Hello World" +
"\nBut it only prints 'Hello'. What happened to 'World'?",
choices: [
"The echo command is broken",
"The computer thinks you gave it TWO things to echo",
"World is not a valid word",
"Echo can only handle one word at a time",
],
answer: 1,
explanation:
"The computer uses spaces to separate your instructions! It thought you said: 'Echo this: Hello' and 'Also this: World'. To keep them together, use quotes: " +
green("echo") +
' "Hello World" - like putting your words in a box!',
wrongAnswerHints: {
0: "Echo works fine - it's about how the computer reads your message!",
2: "All words are valid - it's about how they're grouped!",
3: "Echo can handle many words - if you tell it they belong together!",
},
},
{
id: "git_commit_practice",
type: "type_command",
title: "✍️ Practice: Git Commit",
scenario:
'You\'ve fixed a login bug and want to save your work. You need to tell Git to commit with the message "Fix login bug".\n\n' +
dim("Remember: You're having a conversation with Git!"),
question: "Type the complete command to save your changes:",
answer: 'git commit -m "Fix login bug"',
alternates: ["git commit -m 'Fix login bug'"],
hints: [
"Start by calling git: git commit",
"Add the message flag: -m",
'Tell it WHAT message: "Fix login bug" (with quotes!)',
],
explanation:
"Perfect! You just had a complete conversation with Git:\n" +
green("git") +
" → 'Hey Git!'\n" +
yellow("commit") +
" → 'Save my changes'\n" +
yellow("-m") +
" → 'with this message:'\n" +
bold('"Fix login bug"') +
" → 'the actual message'\n\n" +
"It's like saying: 'Hey Git, commit my changes with the message Fix login bug!'",
// Add command builder before this exercise
showBuilder: true,
},
];
// Simplified exercise runner
async function runExercise(exercise, index, total) {
clearScreen();
// Progress indicator
const progress = `[${index + 1}/${total}]`;
console.log(
boxen(green(progress + " " + exercise.title), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
// Show command builder for the last exercise
if (exercise.showBuilder) {
console.log("\n" + bold("Let's build this command step by step:"));
await sleep(1000);
console.log(
boxen(
green("BUILD YOUR COMMAND! 🔧") + "\n\n" +
dim("─────────────────────────────────") + "\n\n" +
"Pick a tool: " + green("[ git ]") + "\n" +
"Pick an action: " + yellow("[ commit ]") + "\n" +
"Add a flag: " + yellow("[ -m ]") + "\n" +
"Add details: " + bold('[ "Fix login bug" ]') + "\n\n" +
dim("─────────────────────────────────") + "\n\n" +
"Result: " + green("git") + " " + yellow("commit") + " " + yellow("-m") + " " + bold('"Fix login bug"'),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "bold",
borderColor: "yellow",
title: "🔧 Interactive Builder",
titleAlignment: "center",
}
)
);
await sleep(2000);
await printWithTypingEffect("\nNow you try building it yourself!");
await sleep(1000);
}
// Show scenario if present
if (exercise.scenario) {
console.log("\n" + bold("Scenario:"));
await printWithTypingEffect(exercise.scenario);
await sleep(1000);
}
// Show hint if present
if (exercise.hint) {
console.log("\n" + yellow("💡 Hint: ") + dim(exercise.hint));
await sleep(1000);
}
// Handle exercise types
if (exercise.type === "multiple_choice") {
return await runMultipleChoice(exercise);
} else if (exercise.type === "type_command") {
return await runTypeCommand(exercise);
}
}
async function runMultipleChoice(exercise) {
if (exercise.command) {
console.log("\n" + exercise.command);
}
console.log("\n" + exercise.question);
const { choice } = await inquirer.prompt([
{
type: "list",
name: "choice",
message: green(">"),
prefix: "",
choices: exercise.choices,
},
]);
const choiceIndex = exercise.choices.indexOf(choice);
const isCorrect = choiceIndex === exercise.answer;
if (isCorrect) {
const spinner = ora({
text: "Checking...",
color: "green",
}).start();
await sleep(800);
spinner.succeed(green("Correct! ✨"));
} else {
console.log(chalk.red("\nNot quite right."));
// Show specific wrong answer hint if available
if (exercise.wrongAnswerHints && exercise.wrongAnswerHints[choiceIndex]) {
console.log(yellow("\n💡 Hint: ") + dim(exercise.wrongAnswerHints[choiceIndex]));
}
}
if (exercise.explanation) {
console.log(dim("\n" + exercise.explanation));
}
await sleep(2000);
return isCorrect;
}
async function runTypeCommand(exercise) {
console.log("\n" + exercise.question);
let attempts = 0;
let correct = false;
while (!correct && attempts < 3) {
const { answer } = await inquirer.prompt([
{
type: "input",
name: "answer",
message: green(">"),
prefix: "",
validate: (input) => input.trim().length > 0 || "Please type a command",
},
]);
const isCorrect =
answer.trim() === exercise.answer ||
(exercise.alternates && exercise.alternates.includes(answer.trim()));
if (isCorrect) {
correct = true;
const spinner = ora({
text: "Checking...",
color: "green",
}).start();
await sleep(800);
spinner.succeed(green("Perfect! ✨"));
if (exercise.explanation) {
console.log(dim("\n" + exercise.explanation));
}
} else {
attempts++;
if (attempts < 3) {
console.log(chalk.red("Not quite. Try again!"));
if (exercise.hints && exercise.hints[attempts - 1]) {
showHint(exercise.hints[attempts - 1]);
}
}
}
}
if (!correct) {
console.log(
chalk.yellow(`\nThe correct answer was: ${green(exercise.answer)}`)
);
console.log(dim("Don't worry! Practice makes perfect. 💪"));
}
await sleep(2000);
return correct;
}
async function start() {
clearScreen();
// Introduction
for (const lesson of lessons) {
await lesson.content();
console.log(); // Add spacing before the prompt
const { ready } = await inquirer.prompt([
{
type: "confirm",
name: "ready",
message: "Ready to continue?",
default: true,
},
]);
if (!ready) {
return;
}
}
// Practice exercises intro
clearScreen();
console.log(
boxen(
green("Quick Practice! 🗝️") +
"\n\n" +
"Let's test what you've learned with " +
bold("5 quick exercises") +
"\n\n" +
dim("Don't worry about mistakes - that's how we learn!"),
{
padding: 1,
borderStyle: "double",
borderColor: "green",
}
)
);
await sleep(2000);
// Quick review before exercises
clearScreen();
console.log(
boxen(green("🎯 Quick Review"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nRemember: Using the command line is like having a conversation!"
);
await sleep(700);
console.log(
boxen(
bold("The conversation pattern:\n\n") +
green("command") +
" → Who you're talking to\n" +
yellow("subcommand") +
" → What action to take\n" +
yellow("flags") +
" → How to do it\n" +
bold("arguments") +
" → What to work with\n\n" +
"Example: " +
green("git") +
" " +
yellow("add") +
" " +
yellow("-v") +
" " +
bold("file.txt") +
"\n" +
dim("'Hey Git, add file.txt verbosely!'"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1500);
await printWithTypingEffect("\n💡 Tip: Look for patterns in the exercises!");
await sleep(1000);
const { ready } = await inquirer.prompt([
{
type: "confirm",
name: "ready",
message: "Ready to practice?",
default: true,
},
]);
if (!ready) {
return;
}
// Run exercises
let score = 0;
const results = [];
for (let i = 0; i < exercises.length; i++) {
const result = await runExercise(exercises[i], i, exercises.length);
results.push({
exercise: exercises[i].title,
passed: result,
});
if (result) score++;
if (i < exercises.length - 1) {
const { ready } = await inquirer.prompt([
{
type: "confirm",
name: "ready",
message: "Ready for the next exercise?",
default: true,
},
]);
if (!ready) break;
}
}
// Completion with score
clearScreen();
const percentage = Math.round((score / exercises.length) * 100);
let message = "";
if (percentage === 100) {
message = "🏆 PERFECT SCORE! You're a command line natural!";
} else if (percentage >= 80) {
message = "🌟 Excellent work! You've mastered the basics!";
} else if (percentage >= 60) {
message = "👍 Good job! Keep practicing to improve!";
} else {
message = "💪 Nice effort! Review the lessons and try again!";
}
console.log(
boxen(
green("🎉 Chapter 1 Complete! 🎉") +
"\n\n" +
bold(`Your Score: ${score}/${exercises.length} (${percentage}%)`) +
"\n\n" +
message +
"\n\n" +
"You learned:\n" +
"✅ Commands are the building blocks\n" +
"✅ Some commands have subcommands\n" +
"✅ Arguments tell commands what to work with\n" +
"✅ Flags modify how commands behave\n" +
"✅ Arguments can work with commands OR flags\n" +
"✅ Spaces separate command parts\n" +
"✅ Quotes group words together\n" +
"✅ Flags can be flexible in position\n\n" +
dim("Ready for Chapter 2: Essential Commands? Coming soon..."),
{
padding: 1,
borderStyle: "double",
borderColor: "green",
margin: 1,
}
)
);
// Show exercise summary
console.log("\n" + bold("Exercise Summary:"));
results.forEach((result) => {
const icon = result.passed ? green("✓") : chalk.red("✗");
console.log(`${icon} ${result.exercise}`);
});
await inquirer.prompt([
{
type: "input",
name: "continue",
message: "\nPress Enter to return to the main menu...",
prefix: "",
},
]);
}
module.exports = { start };