clitutor
Version:
Interactive CLI learning tool for beginners
1,311 lines (1,197 loc) โข 40.5 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 cyan = chalk.cyan;
const magenta = chalk.magenta;
const blue = chalk.blue;
const red = chalk.red;
const lessons = [
{
id: "intro",
title: "Git Goes Social!",
content: async () => {
clearScreen();
console.log(
boxen(green("Chapter 5: Git Collaboration - Working as a Team"), {
padding: 1,
borderStyle: "double",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nYou've mastered personal Git. Now let's learn to " + bold("collaborate") + " with others!"
);
await sleep(500);
await printWithTypingEffect(
"\nImagine Git as " + bold("Google Docs for code") + "... ๐"
);
await sleep(500);
await printWithTypingEffect(
"\nMultiple people can work on the same project without stepping on each other's toes!"
);
await sleep(1000);
console.log(
boxen(
bold("What You'll Learn:\n\n") +
"๐ " + bold("Clone") + " projects from the internet\n" +
"๐ณ " + bold("Create branches") + " for safe experiments\n" +
"๐ค " + bold("Merge") + " your work with others\n" +
"โ๏ธ " + bold("Resolve conflicts") + " when changes clash\n" +
"๐ค " + bold("Share") + " your code with the world\n" +
"๐ " + bold("Pull requests") + " for code reviews",
{
padding: 1,
margin: { top: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("From Solo to Team:\n\n") +
"Chapter 4: " + dim("You โ Your Computer\n") +
"Chapter 5: " + green("You โ GitHub/GitLab โ Team\n\n") +
"Git becomes a " + bold("communication tool") + "!\n" +
"โข Share your work\n" +
"โข Get others' updates\n" +
"โข Work on the same files safely\n\n" +
dim("It's like multiplayer mode for coding! ๐ฎ"),
{
padding: 1,
margin: { top: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
"๐ป " + bold("Terminal says:") + "\n\n" +
"\"Ready to join the global coding community? ๐\n\n" +
"Git collaboration might seem complex, but it's just\n" +
"sharing your time machine with friends!\n\n" +
"Think of it like this:\n" +
"โข Your local Git = Your personal diary\n" +
"โข GitHub/GitLab = Shared library\n" +
"โข Collaboration = Borrowing and contributing books\n\n" +
"Let's learn to play nicely with others!\"",
{
padding: 1,
margin: { top: 1 },
borderColor: "cyan",
borderStyle: "round",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\nFirst, let's understand where code lives on the internet..."
);
},
},
{
id: "remotes_clone",
title: "Cloning: Downloading Projects",
content: async () => {
clearScreen();
console.log(
boxen(green("git clone - Copying Projects ๐ฅ"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nBefore we share code, let's learn to " + bold("download") + " existing projects!"
);
await sleep(700);
console.log(
boxen(
bold("What is GitHub/GitLab/Bitbucket?\n\n") +
"Think of them as " + bold("libraries for code") + ":\n" +
"โข Store Git repositories online\n" +
"โข Share with the world (or just your team)\n" +
"โข Browse others' code\n" +
"โข Collaborate on projects\n\n" +
dim("GitHub is like Instagram for developers! ๐ธ"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Cloning a Repository:\n\n") +
cyan("git clone") + " " + yellow("https://github.com/user/project.git") + "\n\n" +
"What happens:\n" +
"1. Downloads entire project history\n" +
"2. Sets up connection to origin\n" +
"3. Checks out default branch\n" +
"4. Ready to work!\n\n" +
dim("It's like downloading a complete time machine!"),
{
padding: 1,
borderColor: "cyan",
}
)
);
await sleep(1000);
// Visual representation
console.log(
boxen(
bold("Visual: Clone Process\n\n") +
" " + blue("GitHub") + " " + green("Your Computer") + "\n" +
" โโโโโโโโโโ " + cyan("clone") + " โโโโโโโโโโ\n" +
" โ Project โ โโโโโโโโโโ โ Project โ\n" +
" โ History โ โ History โ\n" +
" โโโโโโโโโโ โโโโโโโโโโ\n" +
" (origin) (local)\n\n" +
dim("You get the entire history, not just current files!"),
{
padding: 1,
margin: { top: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("After Cloning:\n\n") +
"$ " + cyan("cd project") + dim(" # Enter the folder\n") +
"$ " + cyan("git remote -v") + dim(" # See connection\n\n") +
"origin https://github.com/user/project.git (fetch)\n" +
"origin https://github.com/user/project.git (push)\n\n" +
yellow("origin") + " = Nickname for the online repository\n" +
dim("(You can have multiple remotes!)"),
{
padding: 1,
borderColor: "yellow",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n๐ก Remember: Clone " + bold("once") + ", then pull updates (we'll learn that soon!)"
);
},
},
{
id: "branches",
title: "Branches: Parallel Universes",
content: async () => {
clearScreen();
console.log(
boxen(green("Branches - Your Experiment Playground ๐ณ"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nBranches are Git's " + bold("killer feature") + " for collaboration!"
);
await sleep(700);
await printWithTypingEffect(
"\n\nThink of branches as " + bold("parallel universes") + " for your code... ๐"
);
await sleep(1000);
console.log(
boxen(
bold("What are Branches?\n\n") +
"Imagine writing a book:\n" +
"โข " + green("main") + " branch = Published book\n" +
"โข " + yellow("feature") + " branch = Draft chapter\n" +
"โข " + yellow("experiment") + " branch = Crazy idea\n\n" +
"You can:\n" +
"โข Work on drafts without affecting the book\n" +
"โข Try multiple versions\n" +
"โข Merge good ideas back\n" +
"โข Abandon bad ideas\n\n" +
dim("Branches = Safe experimentation!"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Branch Commands:\n\n") +
cyan("git branch") + " โ List all branches\n" +
cyan("git branch feature") + " โ Create 'feature' branch\n" +
cyan("git checkout feature") + " โ Switch to 'feature'\n" +
cyan("git checkout -b feature") + " โ Create AND switch\n\n" +
"The " + yellow("*") + " shows current branch:\n" +
"$ " + cyan("git branch") + "\n" +
yellow("* main") + "\n" +
" feature\n" +
" bugfix",
{
padding: 1,
borderColor: "cyan",
}
)
);
await sleep(1000);
// Visual branch diagram
console.log(
boxen(
bold("Visual: Branch Timeline\n\n") +
"main โโโโโโโโโ\n" +
" \\\n" +
"feature โโโโโโโโโ\n" +
" \\\n" +
"bugfix โโโโโ\n\n" +
"Each โ is a commit\n" +
"Branches " + bold("diverge") + " from main\n" +
"Work happens " + bold("independently") + "\n\n" +
dim("Like alternate timelines in movies! ๐ฌ"),
{
padding: 1,
margin: { top: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Branch Naming Conventions:\n\n") +
green("Good names:") + "\n" +
"โข feature/add-login\n" +
"โข bugfix/fix-crash\n" +
"โข feature/dark-mode\n" +
"โข hotfix/security-patch\n\n" +
red("Bad names:") + "\n" +
"โข stuff\n" +
"โข asdf\n" +
"โข my-branch\n" +
"โข test123\n\n" +
dim("Be descriptive - future you will thank you!"),
{
padding: 1,
borderColor: "green",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n๐ฏ Rule: One branch = One feature/fix. Keep it focused!"
);
},
},
{
id: "merging",
title: "Merging: Combining Work",
content: async () => {
clearScreen();
console.log(
boxen(green("git merge - Bringing Changes Together ๐ค"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nAfter working on a branch, you'll want to " + bold("merge") + " changes back!"
);
await sleep(700);
console.log(
boxen(
bold("What is Merging?\n\n") +
"Remember our book analogy:\n" +
"โข You finished your draft chapter\n" +
"โข Time to add it to the main book\n" +
"โข " + cyan("merge") + " combines the changes\n\n" +
"Git is smart:\n" +
"โข Automatically combines changes\n" +
"โข Preserves everyone's work\n" +
"โข Creates a merge commit\n\n" +
dim("Like weaving threads together! ๐งต"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("How to Merge:\n\n") +
"1. Switch to target branch:\n" +
" $ " + cyan("git checkout main") + "\n\n" +
"2. Merge feature branch:\n" +
" $ " + cyan("git merge feature/add-login") + "\n\n" +
"3. Git combines the changes:\n" +
" " + green("Updating 3a4f5b6..7c8d9e0") + "\n" +
" " + green("Fast-forward") + "\n" +
" index.html | 25 " + green("+++") + red("---") + "\n" +
" 1 file changed, 20 insertions(+), 5 deletions(-)",
{
padding: 1,
borderColor: "cyan",
}
)
);
await sleep(1000);
// Visual merge
console.log(
boxen(
bold("Visual: Before and After Merge\n\n") +
"BEFORE:\n" +
"main โโโโโโโโโ\n" +
" \\\n" +
"feature โโโโโโโโโ\n\n" +
"AFTER " + cyan("git merge feature") + ":\n" +
"main โโโโโโโโโโโโโโโโโ\n" +
" \\ /\n" +
"feature โโโโโโโโโ\n\n" +
dim("The branches reunite! ๐"),
{
padding: 1,
margin: { top: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
"๐ป " + bold("Terminal says:") + "\n\n" +
"\"Merging is like a zipper! ๐ค\n\n" +
"I take changes from both branches and zip them\n" +
"together intelligently.\n\n" +
"Most of the time, I can figure it out myself.\n" +
"But sometimes... conflicts happen!\n\n" +
"Don't worry - we'll handle those next!\"",
{
padding: 1,
margin: { top: 1 },
borderColor: "cyan",
borderStyle: "round",
}
)
);
},
},
{
id: "conflicts",
title: "Merge Conflicts: When Changes Clash",
content: async () => {
clearScreen();
console.log(
boxen(green("Merge Conflicts - Don't Panic! โ๏ธ"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nSometimes two people change the " + bold("same lines") + "... Git needs your help!"
);
await sleep(700);
console.log(
boxen(
bold("Why Conflicts Happen:\n\n") +
"Person A changes line 10 to: " + green('"Hello World!"') + "\n" +
"Person B changes line 10 to: " + green('"Hi Everyone!"') + "\n\n" +
"Git: " + dim('"Which one do you want? ๐คท"') + "\n\n" +
"Conflicts are " + bold("normal") + "!\n" +
"โข Not errors\n" +
"โข Not anyone's fault\n" +
"โข Just need human decision\n\n" +
dim("Think: Two chefs seasoning the same dish!"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("What Conflicts Look Like:\n\n") +
"<<<<<<< HEAD\n" +
red("Hello World!") + dim(" (current branch)\n") +
"=======\n" +
green("Hi Everyone!") + dim(" (incoming branch)\n") +
">>>>>>> feature/new-greeting\n\n" +
"Git shows:\n" +
"โข " + red("Your version") + " (HEAD)\n" +
"โข " + dim("=======") + " separator\n" +
"โข " + green("Their version") + " (feature)\n\n" +
dim("You decide what to keep!"),
{
padding: 1,
borderColor: "red",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Resolving Conflicts:\n\n") +
"1. " + bold("Open") + " the conflicted file\n" +
"2. " + bold("Find") + " the <<<<<<< markers\n" +
"3. " + bold("Decide") + " what to keep:\n" +
" โข Keep yours\n" +
" โข Keep theirs\n" +
" โข Keep both\n" +
" โข Write something new\n" +
"4. " + bold("Remove") + " the markers\n" +
"5. " + cyan("git add") + " the file\n" +
"6. " + cyan("git commit") + " to finish\n\n" +
dim("You're the peacemaker! ๐๏ธ"),
{
padding: 1,
margin: { top: 1 },
borderColor: "green",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Example Resolution:\n\n") +
"BEFORE (conflicted):\n" +
"<<<<<<< HEAD\n" +
"Welcome to our site!\n" +
"=======\n" +
"Welcome to our awesome site!\n" +
">>>>>>> feature\n\n" +
"AFTER (resolved):\n" +
green("Welcome to our awesome site!") + "\n\n" +
dim("Picked the better version and removed markers!"),
{
padding: 1,
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n๐ก Pro tip: Communicate with your team to avoid conflicts!"
);
},
},
{
id: "push_pull",
title: "Push & Pull: Sharing Your Work",
content: async () => {
clearScreen();
console.log(
boxen(green("Sharing with the World ๐"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nTime to learn the dance of " + bold("push") + " and " + bold("pull") + "!"
);
await sleep(700);
console.log(
boxen(
bold("The Collaboration Dance:\n\n") +
cyan("git pull") + " โ Download others' changes\n" +
cyan("git push") + " โ Upload your changes\n\n" +
"Think of it like:\n" +
"โข " + cyan("pull") + " = Refresh your copy\n" +
"โข " + cyan("push") + " = Share your work\n\n" +
dim("Like syncing your phone with the cloud! โ๏ธ"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "cyan",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Pulling Updates:\n\n") +
"$ " + cyan("git pull") + "\n" +
"remote: Counting objects: 5, done.\n" +
"remote: Compressing objects: 100% (3/3), done.\n" +
green("Updating 7a3f9c2..5b4d8e1") + "\n" +
green("Fast-forward") + "\n" +
" README.md | 10 " + green("+++") + red("---") + "\n" +
" 1 file changed, 7 insertions(+), 3 deletions(-)\n\n" +
"What happened:\n" +
"โข Fetched changes from origin\n" +
"โข Merged into your branch\n" +
"โข Updated your files\n\n" +
dim("Always pull before starting work!"),
{
padding: 1,
borderColor: "gray",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Pushing Your Work:\n\n") +
"$ " + cyan("git push") + "\n" +
"Counting objects: 3, done.\n" +
"Delta compression using up to 8 threads.\n" +
"Compressing objects: 100% (3/3), done.\n" +
"Writing objects: 100% (3/3), 328 bytes | 328.00 KiB/s, done.\n" +
"To https://github.com/user/project.git\n" +
" 5b4d8e1..9c7f3a2 main -> main\n\n" +
green("Success!") + " Your commits are now online!\n\n" +
dim("Others can now pull your changes"),
{
padding: 1,
margin: { top: 1 },
borderColor: "green",
}
)
);
await sleep(1000);
// Visual push/pull
console.log(
boxen(
bold("Visual: Push and Pull Flow\n\n") +
" " + green("Local") + " " + blue("GitHub") + "\n" +
" โ " + cyan("push") + " โ\n" +
" โโโโโโโโโ โโโโโโโโโ โโโโโโโโโ\n" +
" โโโโโโโโโ\n" +
" " + cyan("pull") + "\n\n" +
dim("Keep both in sync!"),
{
padding: 1,
borderColor: "yellow",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n๐ฏ Golden rule: " + cyan("pull") + " before you work, " + cyan("push") + " when you're done!"
);
},
},
{
id: "pull_requests",
title: "Pull Requests: Code Reviews",
content: async () => {
clearScreen();
console.log(
boxen(green("Pull Requests - Teamwork at its Best ๐ค"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nPull Requests (PRs) are how teams " + bold("review") + " and " + bold("discuss") + " code!"
);
await sleep(700);
console.log(
boxen(
bold("What's a Pull Request?\n\n") +
"It's like saying:\n" +
dim('"Hey team, I finished this feature!') + "\n" +
dim('Can you review it before we add it?"') + "\n\n" +
"PRs include:\n" +
"โข Your branch's changes\n" +
"โข Description of what you did\n" +
"โข Discussion thread\n" +
"โข Review feedback\n" +
"โข Approval process\n\n" +
dim("Like submitting homework for review! ๐"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("PR Workflow:\n\n") +
"1. " + cyan("Create") + " feature branch\n" +
"2. " + cyan("Make") + " commits\n" +
"3. " + cyan("Push") + " to GitHub\n" +
"4. " + cyan("Open") + " Pull Request\n" +
"5. Team " + cyan("reviews") + " code\n" +
"6. " + cyan("Address") + " feedback\n" +
"7. " + cyan("Merge") + " when approved\n\n" +
dim("Collaboration at its finest! ๐"),
{
padding: 1,
borderColor: "cyan",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Good PR Description:\n\n") +
green("Title:") + " Add dark mode toggle to settings\n\n" +
green("Description:") + "\n" +
"## What changed?\n" +
"- Added toggle switch in settings menu\n" +
"- Implemented dark theme CSS\n" +
"- Saved preference to localStorage\n\n" +
"## Why?\n" +
"Users requested dark mode for late-night coding\n\n" +
"## Screenshots\n" +
"[Light mode] [Dark mode]\n\n" +
"## Testing\n" +
"- Tested on Chrome, Firefox, Safari\n" +
"- Toggle persists after refresh",
{
padding: 1,
margin: { top: 1 },
borderColor: "green",
}
)
);
await sleep(1000);
console.log(
boxen(
"๐ป " + bold("Terminal says:") + "\n\n" +
"\"Pull Requests are where the magic happens! โจ\n\n" +
"They're not just about code - they're about:\n" +
"โข Learning from others\n" +
"โข Sharing knowledge\n" +
"โข Improving quality\n" +
"โข Building better software together\n\n" +
"Don't be shy - every PR is a chance to learn!\"",
{
padding: 1,
margin: { top: 1 },
borderColor: "cyan",
borderStyle: "round",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n๐ก Remember: PRs are conversations, not judgments!"
);
},
},
{
id: "summary",
title: "Your Collaboration Toolkit",
content: async () => {
clearScreen();
console.log(
boxen(green("Git Collaboration Reference ๐ค"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nHere's your complete collaboration toolkit:"
);
await sleep(700);
console.log(
boxen(
bold("๐ฅ Getting Code\n") +
cyan("git clone [url]") + " โ Copy entire project\n" +
cyan("git pull") + " โ Get latest changes\n\n" +
bold("๐ณ Branching\n") +
cyan("git branch") + " โ List branches\n" +
cyan("git checkout -b name") + " โ Create & switch\n" +
cyan("git checkout name") + " โ Switch branch\n\n" +
bold("๐ค Merging\n") +
cyan("git merge branch") + " โ Combine branches\n" +
dim("(Resolve conflicts if needed)\n\n") +
bold("๐ค Sharing\n") +
cyan("git push") + " โ Upload changes\n" +
cyan("git push -u origin branch") + " โ First push\n\n" +
bold("๐ Pull Requests\n") +
dim("Create on GitHub/GitLab for review"),
{
padding: 1,
margin: { top: 1 },
borderColor: "yellow",
}
)
);
await sleep(1000);
console.log(
boxen(
bold("Complete Collaboration Flow:\n\n") +
dim("# Clone a project\n") +
"$ " + cyan("git clone https://github.com/team/project.git") + "\n" +
"$ " + cyan("cd project") + "\n\n" +
dim("# Create feature branch\n") +
"$ " + cyan("git checkout -b feature/add-search") + "\n\n" +
dim("# Make changes and commit\n") +
"$ " + dim("# edit files...") + "\n" +
"$ " + cyan("git add .") + "\n" +
"$ " + cyan('git commit -m "Add search functionality"') + "\n\n" +
dim("# Push and create PR\n") +
"$ " + cyan("git push -u origin feature/add-search") + "\n" +
dim("# Open PR on GitHub") + "\n\n" +
dim("Team collaboration achieved! ๐"),
{
padding: 1,
margin: { top: 1 },
borderColor: "gray",
}
)
);
await sleep(1000);
await printWithTypingEffect(
"\n๐ฏ Next: Join an open source project and make your first PR!"
);
},
},
];
// Exercises for Chapter 5
const exercises = [
{
id: "remote_understanding",
type: "multiple_choice",
title: "๐ Understanding Remotes",
scenario:
"After cloning a repository, you run " + cyan("git remote -v") + " and see:\n\n" +
"origin https://github.com/coolproject/app.git (fetch)\n" +
"origin https://github.com/coolproject/app.git (push)\n",
question: "What does 'origin' represent?",
choices: [
"Your local repository",
"The default name for the remote repository you cloned from",
"The original author of the code",
"The main branch name",
],
answer: 1,
explanation:
"'origin' is Git's default nickname for the remote repository you cloned from. It's like a bookmark to GitHub/GitLab. You could rename it or add more remotes if needed!",
wrongAnswerHints: {
0: "That's your local repo - origin points to the remote!",
2: "Origin refers to the remote location, not a person!",
3: "That would be 'main' or 'master' - origin is the remote!",
},
},
{
id: "branch_status",
type: "multiple_choice",
title: "๐ณ Branch Detective",
scenario:
"You need to check which branch you're currently on before making changes.",
question: "Which command shows your current branch?",
choices: [
cyan("git status"),
cyan("git branch --current"),
cyan("git checkout"),
cyan("git show-branch"),
],
answer: 0,
explanation:
cyan("git status") + " shows your current branch at the top: 'On branch main'. You can also use " + cyan("git branch") + " which marks the current branch with an asterisk (*). Both work!",
wrongAnswerHints: {
1: "Close! Try 'git branch' without --current, or something even simpler!",
2: "That's for switching branches, not checking!",
3: "That's more complex - try the simplest status command!",
},
},
{
id: "merge_direction",
type: "multiple_choice",
title: "๐ค Merge Direction",
scenario:
"You've finished work on 'feature/login' branch and want to merge it into 'main'.",
question: "What's the correct sequence of commands?",
choices: [
cyan("git checkout feature/login") + " then " + cyan("git merge main"),
cyan("git checkout main") + " then " + cyan("git merge feature/login"),
cyan("git merge main feature/login"),
cyan("git push origin feature/login"),
],
answer: 1,
explanation:
"First " + cyan("checkout main") + " (the target branch), then " + cyan("merge feature/login") + " into it. Think: 'I want to update main, so I go there first, then bring in the feature changes.'",
wrongAnswerHints: {
0: "This would merge main INTO feature - backwards!",
2: "Merge doesn't work with two branch names like this!",
3: "This pushes but doesn't merge branches locally!",
},
},
{
id: "conflict_resolution",
type: "multiple_choice",
title: "โ๏ธ Conflict Resolution",
scenario:
"During a merge, you see this in a file:\n\n" +
"<<<<<<< HEAD\n" +
"const title = " + red('"My App"') + "\n" +
"=======\n" +
"const title = " + green('"Our App"') + "\n" +
">>>>>>> feature/rebrand\n",
question: "After deciding to keep 'Our App', what's the next step?",
choices: [
"Just save the file and push",
"Delete the file and start over",
"Remove all conflict markers, save, then " + cyan("git add") + " and " + cyan("git commit"),
"Run " + cyan("git merge --abort") + " to cancel",
],
answer: 2,
explanation:
"After resolving conflicts: 1) Remove ALL the conflict markers (<<<, ===, >>>), 2) Keep the code you want, 3) Save the file, 4) " + cyan("git add") + " the resolved file, 5) " + cyan("git commit") + " to complete the merge!",
wrongAnswerHints: {
0: "You need to stage and commit after resolving!",
1: "Don't delete! Just clean up the markers!",
3: "That cancels the merge - but you can resolve it!",
},
},
{
id: "collaboration_flow",
type: "type_command",
title: "๐ Complete Collaboration",
scenario:
"You've cloned a repo and want to create a new feature branch called 'feature/dark-mode' and switch to it immediately.\n\n" +
"What single command creates AND switches to this new branch?",
question: "Type the complete command:",
answer: "git checkout -b feature/dark-mode",
alternates: [
"git checkout -b 'feature/dark-mode'",
'git checkout -b "feature/dark-mode"',
],
hints: [
"Use git checkout with a special flag",
"The -b flag means 'create branch'",
"git checkout -b [branch-name]",
],
explanation:
"Perfect! " + cyan("git checkout -b") + " is the shortcut that both creates a new branch AND switches to it. This is much faster than doing " + cyan("git branch") + " followed by " + cyan("git checkout") + ". Now you're ready to start working on your feature!",
showBuilder: true,
},
];
// Exercise runner functions (similar to previous chapters)
async function runExercise(exercise, index, total) {
clearScreen();
const progress = `[${index + 1}/${total}]`;
console.log(
boxen(green(progress + " " + exercise.title), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
// Show collaboration workflow for the last exercise
if (exercise.showBuilder) {
console.log("\n" + bold("Let's visualize the collaboration flow:"));
await sleep(1000);
console.log(
boxen(
green("COLLABORATION WORKFLOW ๐ค") + "\n\n" +
dim("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ") + "\n\n" +
bold("Current state:") + "\n" +
"โ You're on 'main' branch\n" +
"โ Need feature/dark-mode branch\n\n" +
bold("Option 1 (Two commands):") + "\n" +
dim("git branch feature/dark-mode") + "\n" +
dim("git checkout feature/dark-mode") + "\n\n" +
bold("Option 2 (One command - better!):") + "\n" +
cyan("git checkout -b feature/dark-mode") + "\n\n" +
dim("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ") + "\n\n" +
green("Result:") + "\n" +
"โ New branch created\n" +
"โ Switched to new branch\n" +
"โ Ready to code!",
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "bold",
borderColor: "yellow",
title: "๐ค Interactive Builder",
titleAlignment: "center",
}
)
);
await sleep(2000);
await printWithTypingEffect("\nNow type the efficient command!");
await sleep(1000);
}
if (exercise.scenario) {
console.log("\n" + bold("Scenario:"));
await printWithTypingEffect(exercise.scenario);
await sleep(1000);
}
if (exercise.hint) {
console.log("\n" + yellow("๐ก Hint: ") + dim(exercise.hint));
await sleep(1000);
}
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."));
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 normalizedAnswer = answer.trim();
const isCorrect =
normalizedAnswer === exercise.answer ||
(exercise.alternates && exercise.alternates.includes(normalizedAnswer));
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! Collaboration commands take practice. ๐ช"));
}
await sleep(2000);
return correct;
}
async function start() {
clearScreen();
// Run through lessons
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("Collaboration Practice! ๐ค") +
"\n\n" +
"Let's test your teamwork skills with " +
bold("5 exercises") +
"\n\n" +
dim("Remember: Git is all about working together!"),
{
padding: 1,
borderStyle: "double",
borderColor: "green",
}
)
);
await sleep(2000);
// Quick command review
clearScreen();
console.log(
boxen(green("๐ฏ Collaboration Cheat Sheet"), {
padding: 1,
borderStyle: "round",
borderColor: "green",
})
);
await printWithTypingEffect(
"\nYour essential collaboration commands:"
);
await sleep(700);
console.log(
boxen(
bold("The Collaboration Dance:\n\n") +
"1๏ธโฃ " + cyan("git clone") + " โ Copy project\n" +
"2๏ธโฃ " + cyan("git checkout -b") + " โ Create branch\n" +
"3๏ธโฃ " + cyan("git add & commit") + " โ Make changes\n" +
"4๏ธโฃ " + cyan("git push") + " โ Share branch\n" +
"5๏ธโฃ " + cyan("Create PR") + " โ Request review\n" +
"6๏ธโฃ " + cyan("git pull") + " โ Get updates\n" +
"7๏ธโฃ " + cyan("git merge") + " โ Combine work\n\n" +
"And remember:\n" +
"โข Conflicts are normal - don't panic!\n" +
"โข Communication prevents conflicts\n" +
"โข Every PR is a learning opportunity\n\n" +
dim("Let's practice! ๐"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: "yellow",
}
)
);
await sleep(1500);
const { ready } = await inquirer.prompt([
{
type: "confirm",
name: "ready",
message: "Ready to test your collaboration skills?",
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 one?",
default: true,
},
]);
if (!ready) break;
}
}
// Chapter completion
clearScreen();
const percentage = Math.round((score / exercises.length) * 100);
let message = "";
if (percentage === 100) {
message = "๐ PERFECT! You're a collaboration champion!";
} else if (percentage >= 80) {
message = "๐ Excellent! You're ready for team projects!";
} else if (percentage >= 60) {
message = "๐ Good work! Your collaboration skills are growing!";
} else {
message = "๐ช Keep practicing! Teamwork takes time to master!";
}
console.log(
boxen(
green("๐ Chapter 5 Complete! ๐") +
"\n\n" +
bold(`Your Score: ${score}/${exercises.length} (${percentage}%)`) +
"\n\n" +
message +
"\n\n" +
"Collaboration skills unlocked:\n" +
"โ
Clone repositories from GitHub/GitLab\n" +
"โ
Create and switch between branches\n" +
"โ
Merge branches together\n" +
"โ
Resolve merge conflicts\n" +
"โ
Push and pull changes\n" +
"โ
Understand pull requests\n" +
"โ
Work effectively in teams\n\n" +
yellow("๐ฏ Next Challenge:") + "\n" +
"Find an open source project you like and:\n" +
"1. Fork and clone it\n" +
"2. Create a feature branch\n" +
"3. Make a small improvement\n" +
"4. Submit your first pull request!\n\n" +
dim("Ready for Chapter 6: Git Mastery? 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 };