From e244c710a4455101cee6b7bee675577980c8aaf1 Mon Sep 17 00:00:00 2001 From: "Thierry V." <46031203+thierryvm@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:19:08 +0200 Subject: [PATCH] feat(reference): explained examples that run in the practice terminal Replayed through the engine, 224 of the 464 /app/reference examples printed an error: made-up file names, comments typed as part of the command, bash shown to Windows learners. - New CommandExample {command, explanation, environments?}; examples live in commandExamples.ts and are merged into the catalogue (single source). Every example is explained for a beginner and uses the practice terminal's files; Windows learners get the PowerShell form. - NOT_SIMULATED marks commands the engine does not run yet (tree, find, less, alias, tar, zip...); the reference tells the learner so. - commandReference.test.ts replays every shown example (git ones inside a prepared repository) and ratchets the 32 remaining gaps, which are engine gaps fixed next. - Content fixes: umask is not a Windows command, find gets its PowerShell form, a cherry-pick example used an impossible commit id, a crontab line and .gitignore patterns were shown as commands, the curl alias note only applies to Windows PowerShell 5.1. - UI: the duplicated "Description" is gone; only the card header toggles (a real Button with aria-controls), so selecting an example or following a doc link no longer collapses the card; examples wrap instead of scrolling; filter pills are one scrollable row until the content area is 48rem wide (container query: the sidebar squeezes the width), reported by @thierry. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 18 + STORY.md | 10 + src/app/components/CommandReference.tsx | 131 +++---- src/app/data/commandCatalogue.ts | 93 +---- src/app/data/commandExamples.ts | 464 ++++++++++++++++++++++++ src/app/types/curriculum.ts | 15 +- src/test/commandReference.test.ts | 86 +++++ src/test/commandReferenceGaps.ts | 50 +++ src/test/commandReferenceReplay.ts | 60 +++ 9 files changed, 781 insertions(+), 146 deletions(-) create mode 100644 src/app/data/commandExamples.ts create mode 100644 src/test/commandReference.test.ts create mode 100644 src/test/commandReferenceGaps.ts create mode 100644 src/test/commandReferenceReplay.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a697e33..06a5134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ --- +## 📖 26 septembre 2026 — Une page Référence qui explique, et dont chaque exemple fonctionne + +*Page `/app/reference` · 76 commandes · nouveau test permanent `commandReference` · exemples en erreur dans le terminal : 224 → 32* + +Thierry a demandé que la page Référence, qui contient aussi de la théorie, soit vérifiée comme les leçons et rendue plus pédagogique. Rejoués dans le terminal du site, **224 de ses 464 exemples affichaient une erreur**. + +- **Chaque exemple est expliqué.** « cd .. » n'était suivi d'aucun mot. Chaque exemple dit maintenant ce qu'il fait, et pourquoi on l'utiliserait : `-p` crée les dossiers intermédiaires, `grep` distingue les majuscules, l'ordre compte dans `> fichier 2>&1`. +- **Les exemples utilisent les fichiers du terminal d'entraînement.** `cat notes.txt` ou `cp a.txt b.txt` visaient des fichiers qui n'existent pas. Les exemples s'appuient sur `documents/notes.txt`, `projets/script.sh` et les autres fichiers présents : on peut les essayer tels quels dans une leçon. +- **Un élève Windows voit du PowerShell.** Les exemples bash (`2>/dev/null`, `ls -la`) ne s'affichent plus sous Windows : ils sont remplacés par leur forme PowerShell (`2>$null`, `Get-ChildItem -Force`). +- **Les commandes non simulées le disent.** `tree`, `find`, `less`, `alias`, `tar` ou `zip` affichent : « pas encore simulée dans le terminal des leçons, essaie-la dans le terminal de ton ordinateur ». +- **Des erreurs de fond corrigées.** `umask` était annoncée sous Windows, où elle n'existe pas. Un exemple `git cherry-pick` citait un identifiant de commit impossible. Une ligne de fichier crontab et des motifs `.gitignore` étaient présentés comme des commandes à taper. Enfin, l'avertissement sur `curl` ne vaut que pour Windows PowerShell 5.1. +- **Les filtres ne prennent plus tout l'écran.** Signalé par Thierry : sur un téléphone ou dans une fenêtre étroite, les 14 filtres occupaient sept lignes. Ils tiennent maintenant sur une ligne qu'on fait défiler, et passent à la ligne dès que la place le permet. +- **Une carte ouverte reste ouverte.** Sélectionner un exemple pour le copier, ou cliquer sur un lien de documentation, refermait la carte. Seule la ligne de titre ouvre et ferme désormais la carte, et c'est un vrai bouton pour les lecteurs d'écran. + +Ce qui reste, et c'est dit : **32 exemples** échouent encore, parce que le simulateur ne connaît pas certaines commandes ou options : `cat -n`, `grep -r` sur un dossier, `Get-Date`, `Get-Help`, `tasklist`, `Set-Content`. Il y a aussi un vrai bogue : `git commit -am "message"` perd son message. Le test les liste, et la liste ne peut que rétrécir. Leur correction est la prochaine livraison. + +--- + ## 🗺️ 26 septembre 2026 — La roadmap publique dit ce qui existe vraiment *Page d'accueil · `llms.txt` · `docs/ROADMAP.md`* diff --git a/STORY.md b/STORY.md index fca37b3..fe6ddf3 100644 --- a/STORY.md +++ b/STORY.md @@ -21,6 +21,16 @@ Ce projet a été construit avec l'aide de Claude — l'IA d'Anthropic, des mod --- +## Une référence qu'on peut recopier (26 septembre 2026) + +La page Référence ressemblait à un aide-mémoire sérieux : syntaxe, exemples, erreurs courantes, liens officiels. Je l'ai traitée comme les leçons, en rejouant chaque exemple dans notre terminal. Près de la moitié échouait. Personne n'avait menti : `cat notes.txt` est un bon exemple dans l'absolu. Mais l'élève qui le recopie dans une leçon n'a pas de `notes.txt` à cet endroit, et il reçoit une erreur rouge sur la page même censée l'aider. + +Une référence pour débutants n'est pas une référence pour experts. L'expert lit `cp -r dossier backup/` et comprend. Le débutant a besoin qu'on lui dise ce que fait `-r`, sur des fichiers qu'il a sous la main, dans le shell qu'il utilise vraiment. Chaque exemple est donc devenu une phrase, et chaque phrase est rejouée par un test. + +Thierry a ajouté son propre regard pendant que je travaillais : « ce filtre prend énormément de place ». Il avait raison. Quatorze étiquettes empilées sur sept lignes repoussaient la première commande hors de l'écran. Elles tiennent maintenant sur une ligne qu'on fait défiler. + +--- + ## Qui vérifie les vérificateurs ? (26 septembre 2026) Thierry a posé une question qu'on ne se posait plus depuis des mois : « les agents qu'on a créés, ils sont encore bons ? » Une vingtaine d'agents relisent chaque changement du projet. Personne ne les relisait, eux. diff --git a/src/app/components/CommandReference.tsx b/src/app/components/CommandReference.tsx index 9fab26e..d08fec1 100644 --- a/src/app/components/CommandReference.tsx +++ b/src/app/components/CommandReference.tsx @@ -1,9 +1,9 @@ import { useMemo, useState } from 'react'; -import { Search, Terminal, ChevronDown, ChevronRight, ExternalLink, BookOpen } from 'lucide-react'; +import { Search, Terminal, ChevronDown, ChevronRight, ExternalLink, BookOpen, Info } from 'lucide-react'; import { useEnvironment } from '../context/EnvironmentContext'; import { commandCatalogue } from '../data/commandCatalogue'; import { TOTAL_COMMANDS } from '../data/landingContent'; -import type { EnrichedCommand, EnvironmentId } from '../types/curriculum'; +import type { CommandExample, EnrichedCommand, EnvironmentId } from '../types/curriculum'; import { usePageSEO } from '../hooks/useLessonSEO'; import { Button } from './ui/button'; import { Input } from './ui/input'; @@ -69,35 +69,9 @@ function commandForEnv(cmd: EnrichedCommand, env: SelectedEnv): { command: strin return { command: cmd.name.split(' / ')[0] }; } -function highlightExampleLine(line: string, i: number) { - if (line.startsWith('$')) { - return ( -
- $ - {line.slice(1)} -
- ); - } - if (line.startsWith('PS>')) { - return ( -
- PS> - {line.slice(3)} -
- ); - } - if (line.startsWith('#')) { - return ( -
- {line} -
- ); - } - return ( -
- {line} -
- ); +/** Examples written for the learner's OS: PowerShell forms on Windows, bash / zsh ones elsewhere. */ +function examplesForEnv(cmd: EnrichedCommand, env: SelectedEnv): CommandExample[] { + return cmd.examples.filter((ex) => !ex.environments || ex.environments.includes(env)); } export function CommandReference() { @@ -145,7 +119,7 @@ export function CommandReference() { return ( // md:pr-32 reserves the FAB clear zone (cf. Dashboard). -
+
{/* AI tutor panel — surfaced on the command reference too because it's a natural place to ask "how does X compare to Y?" follow-up questions. */} @@ -179,14 +153,19 @@ export function CommandReference() {
- {/* Category filters */} -
+ {/* Category filters — one scrollable row while the content area is narrow + (14 wrapped pills used to fill the first screen: a phone, or a desktop + window squeezed by the sidebar), wrapped rows once it is 48rem wide. A + container query, not a viewport breakpoint: the sidebar eats the width. + The thin scrollbar tells mouse users the row scrolls. */} +
{categories.map((cat) => (
+ {isOpen && ( -
+
{/* The command as it's written on the user's current OS */}

@@ -318,17 +302,34 @@ export function CommandReference() { {cmd.syntax}

-
-

Description

-

{cmd.summary}

-
+ {notSimulated && ( +

+

+ )} - {cmd.examples.length > 0 && ( + {examples.length > 0 && (

Exemples

-
-                            {cmd.examples.map((ex, i) => highlightExampleLine(ex, i))}
-                          
+
    + {examples.map((ex, i) => ( +
  • + {/* Wrap rather than scroll: on a phone the end of a long command + (often the option the explanation is about) must stay visible. */} +
    +                                  
    +                                  {ex.command}
    +                                
    +

    {ex.explanation}

    +
  • + ))} +
)} diff --git a/src/app/data/commandCatalogue.ts b/src/app/data/commandCatalogue.ts index 30b1029..1059b3f 100644 --- a/src/app/data/commandCatalogue.ts +++ b/src/app/data/commandCatalogue.ts @@ -1,4 +1,9 @@ import type { CategoryMeta, EnrichedCommand, EnvironmentId, OfficialDoc } from '../types/curriculum'; +import { COMMAND_EXAMPLES, NOT_SIMULATED } from './commandExamples'; + +/** A command as written below; examples, simulation status and docs are merged in afterwards. */ +type BaseCommand = Omit; +type BaseCategory = Omit & { commands: BaseCommand[] }; /** * Structured command catalogue for Terminal Learning. @@ -9,7 +14,7 @@ import type { CategoryMeta, EnrichedCommand, EnvironmentId, OfficialDoc } from ' * * Categories map 1:1 to curriculum modules via their `id`. */ -const baseCatalogue: CategoryMeta[] = [ +const baseCatalogue: BaseCategory[] = [ // ─── LEVEL 1 — FUNDAMENTALS ─────────────────────────────── { @@ -38,7 +43,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'pwd', summary: 'Afficher le dossier courant', - examples: ['pwd'], commonErrors: [], }, { @@ -54,7 +58,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'ls [options] [chemin]', summary: "Lister le contenu d'un dossier", - examples: ['ls', 'ls -la'], commonErrors: ["Confondre ls et dir selon l'environnement"], }, { @@ -67,7 +70,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'cd chemin', summary: 'Changer de dossier', - examples: ['cd Documents', 'cd ..', 'cd ~'], commonErrors: ['Chemin relatif/absolu incorrect'], }, { @@ -84,7 +86,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'clear | cls', summary: 'Nettoyer le terminal', - examples: ['clear', 'cls'], commonErrors: [], }, { @@ -99,7 +100,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'tree [chemin]', summary: "Afficher l'arborescence des répertoires", - examples: ['tree', 'tree /F (Windows : afficher aussi les fichiers)'], commonErrors: [ 'tree non préinstallé sur certaines distributions Linux (apt install tree) ni sur macOS (brew install tree)', ], @@ -130,7 +130,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'mkdir dossier', summary: 'Créer un dossier', - examples: ['mkdir projet', 'mkdir -p projets/web/css'], commonErrors: [], }, { @@ -145,7 +144,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'touch fichier.txt', summary: 'Créer un fichier vide', - examples: ['touch notes.txt'], commonErrors: [], }, { @@ -163,7 +161,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'cp source destination', summary: 'Copier un fichier ou dossier', - examples: ['cp a.txt b.txt', 'cp -r dossier backup/'], commonErrors: ['Oublier -r pour un dossier sous Unix'], }, { @@ -182,7 +179,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'mv source destination', summary: 'Déplacer ou renommer', - examples: ['mv notes.txt archive/notes.txt'], commonErrors: [], }, { @@ -200,7 +196,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'rm fichier.txt', summary: 'Supprimer un fichier', - examples: ['rm notes.txt', 'rm -r dossier/'], commonErrors: [ 'Confondre suppression fichier et dossier', 'Utiliser rm -rf sans comprendre', @@ -236,7 +231,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'cat fichier.txt', summary: "Afficher le contenu d'un fichier", - examples: ['cat notes.txt'], commonErrors: [], }, { @@ -253,7 +247,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'less fichier.txt', summary: 'Lire un fichier page par page', - examples: ['less log.txt'], commonErrors: [], }, { @@ -269,7 +262,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'head -n 10 fichier.txt | tail -n 10 fichier.txt', summary: "Voir le début ou la fin d'un fichier", - examples: ['head -n 5 notes.txt', 'tail -f app.log'], commonErrors: [], }, ], @@ -302,7 +294,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'grep motif fichier.txt', summary: 'Chercher un motif dans un fichier', - examples: ['grep error app.log', 'grep -rn "TODO" src/'], commonErrors: ['Motif non cité', 'Mauvais chemin'], }, { @@ -314,12 +305,12 @@ const baseCatalogue: CategoryMeta[] = [ variants: [ { environment: 'linux', command: "find . -name '*.txt'" }, { environment: 'macos', command: "find . -name '*.txt'" }, + { environment: 'windows', command: 'Get-ChildItem -Recurse -Filter *.txt', shell: 'PowerShell' }, { environment: 'windows', command: 'where nomcommande', shell: 'CMD' }, ], compatibility: ['linux', 'macos', 'windows'], syntax: 'find . -name motif', summary: 'Trouver des fichiers ou commandes', - examples: ['find . -name notes.txt'], commonErrors: [], }, { @@ -338,7 +329,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'wc fichier.txt', summary: 'Compter lignes, mots et caractères', - examples: ['wc notes.txt', 'wc -l *.ts'], commonErrors: [], }, ], @@ -371,7 +361,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'echo [texte]', summary: "Afficher du texte dans le terminal (supporte l'interpolation $VAR)", - examples: ['echo "Bonjour"', 'echo $HOME', 'echo "User: $USER"'], commonErrors: [ 'Windows : echo fonctionne aussi, mais $HOME devient $env:USERPROFILE', 'Write-Output écrit dans le pipeline ; Write-Host écrit directement à la console (sans pipeline)', @@ -389,7 +378,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'date [+format]', summary: "Afficher la date et l'heure courantes", - examples: ['date', 'date +"%Y-%m-%d %H:%M"'], commonErrors: [ 'Windows : Get-Date -Format "yyyy-MM-dd HH:mm" pour le formatage', ], @@ -407,7 +395,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos'], syntax: 'uname [-a]', summary: 'Afficher des informations sur le système (noyau, architecture)', - examples: ['uname', 'uname -a'], commonErrors: [ 'Windows : pas de uname natif — utiliser $PSVersionTable.OS ou (Get-ComputerInfo)', ], @@ -424,7 +411,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['windows'], syntax: 'Get-ComputerInfo', summary: 'Afficher des informations détaillées sur le système Windows', - examples: ['Get-ComputerInfo', '(Get-ComputerInfo).WindowsProductName', '$PSVersionTable.PSVersion'], commonErrors: [], }, { @@ -439,7 +425,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'history [n]', summary: "Afficher l'historique des commandes", - examples: ['history', 'history | grep git'], commonErrors: [], }, { @@ -454,7 +439,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'man commande', summary: "Afficher le manuel détaillé d'une commande", - examples: ['man ls', 'man grep'], commonErrors: [ 'Windows : Get-Help (et Get-Help -Examples pour des exemples)', ], @@ -471,7 +455,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: "alias nom='commande'", summary: 'Créer un raccourci pour une commande', - examples: ["alias ll='ls -la'", "alias gs='git status'", 'alias'], commonErrors: [ 'Temporaire : à mettre dans ~/.bashrc / ~/.zshrc pour la rendre permanente', ], @@ -488,7 +471,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['macos'], syntax: 'open fichier|dossier|URL', summary: "Ouvrir un fichier, dossier ou URL avec l'application par défaut (macOS)", - examples: ['open notes.txt', 'open .', 'open https://example.com'], commonErrors: ['Linux : équivalent xdg-open · Windows : start'], }, { @@ -503,7 +485,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['macos'], syntax: 'commande | pbcopy / pbpaste', summary: 'Copier vers / coller depuis le presse-papiers système (macOS)', - examples: ['cat fichier.txt | pbcopy', 'pbpaste > nouveau.txt'], commonErrors: ['Linux : xclip ou xsel · Windows : Set-Clipboard / Get-Clipboard'], }, { @@ -518,7 +499,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['macos'], syntax: 'brew install|update|list [paquet]', summary: 'Homebrew — gestionnaire de paquets macOS', - examples: ['brew install htop', 'brew update', 'brew list'], commonErrors: [ 'Linux : brew fonctionne aussi (brew.sh), mais apt / dnf / pacman sont les gestionnaires natifs', ], @@ -535,7 +515,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['windows'], syntax: 'winget install|list [paquet]', summary: 'Windows Package Manager — gestionnaire de paquets Windows', - examples: ['winget install Git.Git', 'winget install Microsoft.VisualStudioCode', 'winget list'], commonErrors: [], }, ], @@ -569,7 +548,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'chmod mode fichier', summary: "Modifier les permissions d'un fichier", - examples: ['chmod 755 script.sh', 'chmod +x script.sh'], commonErrors: ['Confondre octal et symbolique', 'Oublier -R pour un dossier'], }, { @@ -584,7 +562,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'chown utilisateur:groupe fichier', summary: "Changer le propriétaire d'un fichier", - examples: ['chown user:staff fichier.txt', 'chown -R www-data:www-data /var/www'], commonErrors: ['Oublier sudo', 'Mauvais format utilisateur:groupe'], }, { @@ -597,7 +574,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'whoami', summary: "Afficher l'utilisateur courant", - examples: ['whoami'], commonErrors: [], }, { @@ -612,7 +588,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'id [utilisateur]', summary: "Afficher l'identité et les groupes", - examples: ['id', 'id root'], commonErrors: [], }, { @@ -629,7 +604,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'sudo commande [args]', summary: 'Exécuter une commande avec les droits administrateur (root)', - examples: ['sudo apt update', 'sudo chmod 600 ~/.ssh/id_rsa', 'sudo -i'], commonErrors: [ 'Windows : pas de sudo natif — relancer le terminal en Administrateur (Start-Process -Verb RunAs)', ], @@ -641,10 +615,9 @@ const baseCatalogue: CategoryMeta[] = [ level: 2, recommendedFor: ['linux', 'macos'], variants: [], - compatibility: ['linux', 'macos', 'windows'], + compatibility: ['linux', 'macos'], syntax: 'umask [mode]', summary: 'Afficher ou définir le masque de permissions par défaut des nouveaux fichiers', - examples: ['umask', 'umask 027'], commonErrors: [ "Pas d'équivalent direct sur Windows — les permissions des nouveaux fichiers sont héritées du dossier parent", ], @@ -661,7 +634,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['windows'], syntax: 'Get-Acl fichier', summary: "Afficher les permissions (ACL) d'un fichier ou répertoire sous Windows", - examples: ['Get-Acl notes.txt', 'Get-Acl C:\\path | Format-List'], commonErrors: [], }, ], @@ -695,7 +667,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'ps [options]', summary: 'Lister les processus en cours', - examples: ['ps', 'ps aux'], commonErrors: [], }, { @@ -713,7 +684,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'kill [signal] PID', summary: 'Arrêter un processus', - examples: ['kill 1234', 'kill -9 1234'], commonErrors: ['Mauvais PID', 'Oublier sudo pour un processus système'], }, { @@ -728,7 +698,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'top | htop', summary: "Monitorer l'activité système en temps réel", - examples: ['top', 'htop'], commonErrors: [], }, { @@ -743,7 +712,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'jobs', summary: 'Afficher les tâches en arrière-plan du shell courant', - examples: ['sleep 100 &', 'jobs'], commonErrors: [], }, { @@ -758,7 +726,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'bg [%n]', summary: 'Reprendre une tâche suspendue en arrière-plan (après Ctrl+Z)', - examples: ['bg %1', 'jobs'], commonErrors: [], }, { @@ -773,7 +740,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'fg [%n]', summary: 'Ramener une tâche en avant-plan', - examples: ['fg %1', 'fg'], commonErrors: [], }, ], @@ -804,7 +770,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'commande > fichier | commande >> fichier', summary: 'Rediriger la sortie vers un fichier', - examples: ['echo "hello" > output.txt', 'ls >> listing.txt'], commonErrors: ['Confondre > (écrase) et >> (ajoute)'], }, { @@ -817,7 +782,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'commande1 | commande2', summary: 'Chaîner les commandes entre elles', - examples: ['ls | wc -l', 'cat log.txt | grep error', 'ps aux | grep node'], commonErrors: ['Confondre | et >'], }, { @@ -832,7 +796,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'commande | tee fichier', summary: 'Écrire dans un fichier ET afficher à l\'écran', - examples: ['ls | tee listing.txt'], commonErrors: [], }, { @@ -847,7 +810,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'commande 2> fichier', summary: "Rediriger la sortie d'erreur (stderr) vers un fichier", - examples: ['ls inexistant 2> erreurs.log', 'ls inexistant 2>/dev/null # ignorer'], commonErrors: ['Windows : $null remplace /dev/null (commande 2>$null)'], }, { @@ -862,7 +824,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'commande > fichier 2>&1', summary: 'Fusionner stderr dans stdout — capturer tout dans un seul fichier', - examples: ['npm run build > build.log 2>&1', 'command 2>&1 | grep "Error"'], commonErrors: ['PowerShell : *>&1 redirige tous les streams vers stdout'], }, ], @@ -892,7 +853,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'tar [options] archive.tar.gz fichiers', summary: 'Créer ou extraire une archive tar', - examples: ['tar -czf backup.tar.gz dossier/', 'tar -xzf backup.tar.gz'], commonErrors: ['Confondre -c (créer) et -x (extraire)', 'Oublier -z pour gzip'], }, { @@ -908,7 +868,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'zip archive.zip fichiers | unzip archive.zip', summary: 'Créer ou extraire une archive zip', - examples: ['zip -r projet.zip dossier/', 'unzip projet.zip'], commonErrors: ['Oublier -r pour un dossier'], }, ], @@ -943,7 +902,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'export VAR=valeur', summary: "Définir une variable d'environnement (paire clé/valeur lue par les programmes)", - examples: ['export EDITOR=nano', 'export PATH=$PATH:/opt/myapp/bin', 'echo $PATH'], commonErrors: [ 'Temporaire : perdue à la fermeture du terminal — la rendre permanente dans ~/.bashrc / ~/.zshrc', 'Pas d\'espaces autour du = en bash (VAR=valeur, pas VAR = valeur)', @@ -963,7 +921,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'env', summary: "Lister toutes les variables d'environnement actives", - examples: ['env', 'env | grep PATH', 'echo $HOME'], commonErrors: [], }, { @@ -980,7 +937,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'source fichier-config', summary: 'Recharger un fichier de configuration shell sans relancer le terminal', - examples: ['source ~/.bashrc', '. ~/.zshrc'], commonErrors: [ 'Fichier de config selon le shell : ~/.bashrc (bash), ~/.zshrc (zsh), $PROFILE (PowerShell)', ], @@ -999,7 +955,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'chmod +x script.sh && ./script.sh', summary: 'Rendre un script bash exécutable et le lancer (shebang #!/bin/bash en 1re ligne)', - examples: ['chmod +x deploy.sh', './deploy.sh', 'bash script.sh'], commonErrors: [ 'Oublier chmod +x : "Permission denied"', 'Oublier le ./ : le shell ne cherche pas dans le répertoire courant par sécurité', @@ -1019,7 +974,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'crontab -l | crontab -e', summary: 'Planifier des tâches récurrentes (équivalent Windows : Planificateur de tâches)', - examples: ['crontab -l', 'crontab -e', '0 9 * * 1-5 /home/user/backup.sh'], commonErrors: [ 'crontab -r supprime TOUTES les tâches (pas de confirmation)', 'Vérifier la syntaxe sur crontab.guru avant la prod', @@ -1035,7 +989,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos'], syntax: 'printenv [NOM]', summary: "Afficher la valeur d'une ou de toutes les variables d'environnement", - examples: ['printenv PATH', 'printenv USER', 'printenv'], commonErrors: [ "Windows : utiliser $env:NOM ou [Environment]::GetEnvironmentVariable('NOM')", ], @@ -1070,7 +1023,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'ping hote', summary: "Tester l'accessibilité d'un hôte et mesurer la latence (paquets ICMP)", - examples: ['ping google.com', 'ping -c 4 8.8.8.8'], commonErrors: [ 'Linux/macOS pinguent en continu (Ctrl+C pour arrêter) — utiliser -c N pour limiter', 'Windows : -n N (pas -c). Windows envoie 4 paquets par défaut', @@ -1090,9 +1042,8 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'curl [options] url', summary: 'Envoyer des requêtes HTTP/HTTPS (tester des APIs, déboguer des endpoints)', - examples: ['curl -I https://example.com', "curl -X POST url -H \"Content-Type: application/json\" -d '{\"k\":\"v\"}'", 'curl -s url | jq .'], commonErrors: [ - 'En PowerShell, curl est un alias de Invoke-WebRequest — utiliser curl.exe pour le vrai curl', + 'Windows PowerShell 5.1 : curl y est un alias d\'Invoke-WebRequest (taper curl.exe pour le vrai curl) ; PowerShell 7 lance directement curl.exe', 'Oublier -L pour suivre les redirections', ], }, @@ -1110,7 +1061,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'wget [options] url', summary: 'Télécharger des fichiers depuis le web (reprend les téléchargements interrompus)', - examples: ['wget -O nom.zip url', 'wget -c url', 'iwr url -OutFile fichier.zip'], commonErrors: [ 'wget non préinstallé sur macOS (brew install wget) ni Windows (utiliser Invoke-WebRequest / iwr)', ], @@ -1129,7 +1079,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'nslookup domaine | dig domaine', summary: 'Résoudre un nom de domaine en adresse IP (diagnostic DNS)', - examples: ['nslookup google.com', 'dig +short google.com', 'dig @1.1.1.1 google.com'], commonErrors: [ 'nslookup partout ; dig sur Linux/macOS (Chocolatey sur Windows) ; Resolve-DnsName natif PowerShell', ], @@ -1148,7 +1097,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'ssh user@hote', summary: 'Se connecter à un serveur distant de manière chiffrée (OpenSSH natif Win10+)', - examples: ['ssh -p 2222 user@serveur', 'ssh-keygen -t ed25519 -C "mon@email.com"', 'ssh-copy-id user@serveur'], commonErrors: [ 'Préférer ed25519 à rsa pour les nouvelles clés', 'Ne jamais partager la clé privée (~/.ssh/id_ed25519) — seule la .pub se dépose sur les serveurs', @@ -1168,7 +1116,6 @@ const baseCatalogue: CategoryMeta[] = [ compatibility: ['linux', 'macos', 'windows'], syntax: 'scp source user@hote:destination', summary: 'Copier des fichiers entre machines via SSH (scp natif Win10+)', - examples: ['scp fichier.txt user@serveur:/home/user/', 'scp -r dossier/ user@serveur:/var/www/', 'scp -P 2222 fichier.txt user@serveur:/home/'], commonErrors: [ '-P majuscule pour scp (port), -p minuscule pour ssh — asymétrie historique', 'Pour gros volumes ou transferts récurrents, préférer rsync -avz', @@ -1197,70 +1144,60 @@ const baseCatalogue: CategoryMeta[] = [ id: 'git_init', name: 'git init', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git init [chemin]', summary: 'Initialiser un nouveau dépôt Git', - examples: ['git init', 'git init mon-projet'], commonErrors: ['Lancer git init dans le mauvais dossier (vérifier avec pwd)'], }, { id: 'git_config', name: 'git config', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git config [--global] clé valeur', summary: 'Configurer Git (identité, préférences)', - examples: ['git config --global user.name "Alice"', 'git config --global user.email "alice@example.com"'], commonErrors: ['Oublier --global → config limitée au dépôt courant'], }, { id: 'git_add', name: 'git add', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git add ', summary: 'Indexer des modifications pour le prochain commit', - examples: ['git add fichier.txt', 'git add .'], commonErrors: ['git add . indexe TOUT — vérifier git status avant'], }, { id: 'git_commit', name: 'git commit', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git commit -m "message"', summary: 'Enregistrer un instantané des fichiers indexés', - examples: ['git commit -m "feat: ajoute la page contact"', 'git commit -am "fix: corrige le bug"'], commonErrors: ['Commit sans git add préalable → rien n\'est enregistré', 'Message vide ou non descriptif'], }, { id: 'git_status', name: 'git status', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git status', summary: 'Afficher l\'état du dépôt (fichiers modifiés, indexés)', - examples: ['git status', 'git status -s'], commonErrors: [], }, { id: 'git_log', name: 'git log', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git log [options]', summary: 'Afficher l\'historique des commits', - examples: ['git log', 'git log --oneline', 'git log --graph --oneline --all'], commonErrors: ['Sortie longue : taper q pour quitter le pager'], }, { id: 'git_diff', name: 'git diff', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git diff [options]', summary: 'Comparer les modifications (working dir, index, commits)', - examples: ['git diff', 'git diff --staged', 'git diff main feature'], commonErrors: ['git diff seul ne montre PAS les fichiers déjà indexés (utiliser --staged)'], }, { id: 'gitignore', name: '.gitignore', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: '# un motif par ligne dans le fichier .gitignore', summary: 'Exclure des fichiers du suivi Git', - examples: ['node_modules/', '*.log', '.env'], commonErrors: ['.gitignore n\'affecte PAS les fichiers déjà suivis (git rm --cached d\'abord)'], }, { id: 'git_branch', name: 'git branch', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git branch [nom]', summary: 'Lister, créer ou supprimer des branches', - examples: ['git branch', 'git branch feature/login', 'git switch -c feature/login'], commonErrors: ['git branch crée la branche mais ne bascule pas dessus (git switch / checkout)'], }, { id: 'git_merge', name: 'git merge', category: 'git', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git merge ', summary: 'Fusionner une branche dans la branche courante', - examples: ['git merge feature/login', 'git merge --no-ff feature/login'], commonErrors: ['Conflits de merge : résoudre, git add, puis git commit'], }, ], @@ -1283,49 +1220,42 @@ const baseCatalogue: CategoryMeta[] = [ id: 'git_remote', name: 'git remote', category: 'github-collaboration', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git remote [add ]', summary: 'Gérer les dépôts distants (remotes)', - examples: ['git remote -v', 'git remote add origin https://github.com/user/repo.git'], commonErrors: ['Confondre le nom du remote (origin) et le nom de la branche (main)'], }, { id: 'git_push', name: 'git push', category: 'github-collaboration', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git push [remote] [branche]', summary: 'Envoyer les commits locaux vers le dépôt distant', - examples: ['git push', 'git push -u origin main'], commonErrors: ['Premier push : utiliser -u pour lier la branche locale au remote'], }, { id: 'git_pull', name: 'git pull', category: 'github-collaboration', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git pull [remote] [branche]', summary: 'Récupérer ET fusionner les modifications distantes', - examples: ['git pull', 'git pull origin main'], commonErrors: ['git pull = git fetch + git merge — peut créer des conflits'], }, { id: 'git_fetch', name: 'git fetch', category: 'github-collaboration', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git fetch [remote]', summary: 'Récupérer les modifications distantes SANS les fusionner', - examples: ['git fetch', 'git fetch origin'], commonErrors: ['Contrairement à pull, fetch ne modifie pas votre branche de travail'], }, { id: 'git_clone', name: 'git clone', category: 'github-collaboration', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git clone [dossier]', summary: 'Cloner un dépôt distant en local', - examples: ['git clone https://github.com/user/repo.git', 'git clone git@github.com:user/repo.git'], commonErrors: ['HTTPS vs SSH : le clone SSH nécessite une clé configurée'], }, { id: 'git_rebase', name: 'git rebase', category: 'github-collaboration', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git rebase ', summary: 'Réappliquer des commits sur une autre base (historique linéaire)', - examples: ['git rebase main', 'git rebase -i HEAD~3'], commonErrors: ['Ne jamais rebaser une branche déjà partagée/poussée publiquement'], }, { id: 'git_cherry_pick', name: 'git cherry-pick', category: 'github-collaboration', level: 4, recommendedFor: ['linux', 'macos', 'windows'], variants: [], compatibility: ['linux', 'macos', 'windows'], syntax: 'git cherry-pick ', summary: 'Ré-appliquer un commit précis sur la branche courante', - examples: ['git cherry-pick a1b2c3d', 'git cherry-pick a1b2c3d..e4f5g6h'], commonErrors: ['Récupérer le hash via git log --oneline', 'Peut créer un conflit si le commit touche des lignes déjà modifiées'], }, ], @@ -1540,7 +1470,8 @@ const OFFICIAL_DOCS: Record = { }; /** - * Canonical catalogue: base data merged with verified official docs. + * Canonical catalogue: base data merged with explained examples, simulation + * status and verified official docs. * Single source of truth consumed by /app/reference, the landing counter, * and the curriculum consistency tests. */ @@ -1548,6 +1479,8 @@ export const commandCatalogue: CategoryMeta[] = baseCatalogue.map((category) => ...category, commands: category.commands.map((cmd) => ({ ...cmd, + examples: COMMAND_EXAMPLES[cmd.id] ?? [], + notSimulatedOn: NOT_SIMULATED[cmd.id] ?? [], officialDocs: OFFICIAL_DOCS[cmd.id] ?? [], })), })); diff --git a/src/app/data/commandExamples.ts b/src/app/data/commandExamples.ts new file mode 100644 index 0000000..9bedff4 --- /dev/null +++ b/src/app/data/commandExamples.ts @@ -0,0 +1,464 @@ +import type { CommandExample, EnvironmentId } from '../types/curriculum'; + +/** + * Explained examples for /app/reference, keyed by command id (commandCatalogue). + * + * Every example is something a learner can type as is. Paths use the practice + * terminal's files (~/documents/notes.txt, ~/documents/rapport.md, + * ~/projets/README.md, ~/projets/script.sh), so an example tried in a lesson + * terminal works instead of failing on a made-up file name. + * `src/test/commandReference.test.ts` replays each one through the engine. + * + * An example without `environments` is shown on every OS the command supports; + * Windows learners get PowerShell forms instead of bash ones where they differ. + */ + +const UNIX: EnvironmentId[] = ['linux', 'macos']; +const WIN: EnvironmentId[] = ['windows']; +const LINUX: EnvironmentId[] = ['linux']; +const MACOS: EnvironmentId[] = ['macos']; + +export const COMMAND_EXAMPLES: Record = { + // ─── Navigation ─────────────────────────────────────────── + pwd: [ + { command: 'pwd', explanation: 'Affiche le chemin complet du dossier où tu te trouves. Sous PowerShell, pwd est un raccourci de Get-Location.' }, + { command: 'Get-Location', explanation: 'La forme PowerShell complète : même résultat que pwd.', environments: WIN }, + ], + ls: [ + { command: 'ls', explanation: 'Liste les fichiers et dossiers visibles du dossier courant.', environments: UNIX }, + { command: 'ls -la', explanation: '-l affiche le détail (droits, propriétaire, taille, date) et -a ajoute les fichiers cachés, ceux dont le nom commence par un point.', environments: UNIX }, + { command: 'ls documents', explanation: 'Liste le contenu d\'un autre dossier sans t\'y déplacer.', environments: UNIX }, + { command: 'Get-ChildItem', explanation: 'Liste le contenu du dossier courant. ls et dir sont des raccourcis de cette commande.', environments: WIN }, + { command: 'Get-ChildItem -Force', explanation: 'Ajoute les fichiers cachés. ls -la, lui, ne fonctionne pas dans PowerShell.', environments: WIN }, + { command: 'Get-ChildItem documents', explanation: 'Liste le contenu d\'un autre dossier sans t\'y déplacer.', environments: WIN }, + ], + cd: [ + { command: 'cd documents', explanation: 'Entre dans le dossier documents, situé dans le dossier courant (chemin relatif).' }, + { command: 'cd ..', explanation: 'Remonte d\'un niveau, vers le dossier parent.' }, + { command: 'cd ~', explanation: 'Revient à ton dossier personnel, où que tu sois.' }, + { command: 'cd /tmp', explanation: 'Un chemin qui commence par / est absolu : il part de la racine du système, quel que soit le dossier courant.', environments: UNIX }, + { command: 'cd -', explanation: 'Revient au dossier où tu étais juste avant.', environments: UNIX }, + { command: 'cd C:\\Users\\user\\projets', explanation: 'Un chemin qui commence par la lettre du disque est absolu. PowerShell accepte aussi / comme séparateur.', environments: WIN }, + ], + clear_cls: [ + { command: 'clear', explanation: 'Efface l\'écran. Les commandes précédentes restent accessibles avec la flèche du haut.', environments: UNIX }, + { command: 'cls', explanation: 'Efface l\'écran de PowerShell (raccourci de Clear-Host ; clear fonctionne aussi).', environments: WIN }, + ], + tree: [ + { command: 'tree', explanation: 'Dessine l\'arborescence du dossier courant. Souvent à installer d\'abord : sudo apt install tree ou brew install tree.', environments: UNIX }, + { command: 'tree -L 2', explanation: 'Limite le dessin à deux niveaux de profondeur, pour les gros dossiers.', environments: UNIX }, + { command: 'tree', explanation: 'Dessine l\'arborescence des dossiers seulement.', environments: WIN }, + { command: 'tree /F', explanation: '/F ajoute les fichiers dans l\'arborescence.', environments: WIN }, + ], + + // ─── Fichiers & Dossiers ────────────────────────────────── + mkdir: [ + { command: 'mkdir archives', explanation: 'Crée le dossier archives dans le dossier courant.' }, + { command: 'mkdir -p projets/web/css', explanation: '-p crée aussi les dossiers intermédiaires manquants (web, puis css), sans erreur s\'ils existent déjà.', environments: UNIX }, + { command: 'mkdir projets\\web\\css', explanation: 'PowerShell crée tout seul les dossiers intermédiaires manquants.', environments: WIN }, + ], + touch: [ + { command: 'touch todo.txt', explanation: 'Crée un fichier vide. S\'il existe déjà, touch met seulement à jour sa date de modification.', environments: UNIX }, + { command: 'New-Item todo.txt -ItemType File', explanation: 'Crée un fichier vide. PowerShell n\'a pas de touch, et New-Item refuse d\'écraser un fichier qui existe déjà.', environments: WIN }, + ], + cp: [ + { command: 'cp documents/notes.txt documents/notes-copie.txt', explanation: 'Copie un fichier sous un nouveau nom.', environments: UNIX }, + { command: 'cp documents/notes.txt projets/', explanation: 'Quand la destination est un dossier existant, le fichier y est copié sous son nom d\'origine.', environments: UNIX }, + { command: 'cp -r documents sauvegarde', explanation: '-r (récursif) copie un dossier avec tout son contenu. Sans -r, cp refuse de copier un dossier.', environments: UNIX }, + { command: 'Copy-Item documents\\notes.txt documents\\notes-copie.txt', explanation: 'Copie un fichier sous un nouveau nom. cp et copy sont des raccourcis de Copy-Item.', environments: WIN }, + { command: 'Copy-Item documents sauvegarde -Recurse', explanation: '-Recurse copie le dossier avec son contenu. Sans lui, PowerShell ne crée qu\'un dossier vide.', environments: WIN }, + ], + mv: [ + { command: 'mv documents/rapport.md documents/rapport-final.md', explanation: 'Renomme un fichier : mv sert à la fois à déplacer et à renommer.', environments: UNIX }, + { command: 'mv documents/notes.txt projets/', explanation: 'Déplace le fichier dans le dossier projets, en gardant son nom.', environments: UNIX }, + { command: 'Move-Item documents\\rapport.md documents\\rapport-final.md', explanation: 'Renomme un fichier. mv et move sont des raccourcis de Move-Item.', environments: WIN }, + { command: 'Move-Item documents\\notes.txt projets\\', explanation: 'Déplace le fichier dans le dossier projets, en gardant son nom.', environments: WIN }, + ], + rm: [ + { command: 'rm documents/rapport.md', explanation: 'Supprime le fichier définitivement : le terminal n\'a pas de corbeille.', environments: UNIX }, + { command: 'rm -r downloads', explanation: '-r supprime un dossier et tout ce qu\'il contient. Relis la commande avant d\'appuyer sur Entrée.', environments: UNIX }, + { command: 'Remove-Item documents\\rapport.md', explanation: 'Supprime le fichier définitivement, sans passer par la corbeille.', environments: WIN }, + { command: 'Remove-Item downloads -Recurse', explanation: '-Recurse supprime un dossier et tout ce qu\'il contient.', environments: WIN }, + ], + + // ─── Lecture de fichiers ────────────────────────────────── + cat: [ + { command: 'cat documents/notes.txt', explanation: 'Affiche tout le contenu du fichier d\'un coup. Idéal pour les fichiers courts.', environments: UNIX }, + { command: 'cat -n documents/notes.txt', explanation: '-n numérote les lignes.', environments: UNIX }, + { command: 'Get-Content documents\\notes.txt', explanation: 'Affiche le contenu du fichier. cat et type sont des raccourcis de Get-Content.', environments: WIN }, + ], + less_more: [ + { command: 'less documents/notes.txt', explanation: 'Affiche le fichier page par page : flèches ou Espace pour avancer, / pour chercher un mot, q pour quitter.', environments: UNIX }, + { command: 'more documents\\notes.txt', explanation: 'Affiche le fichier page par page : Espace pour avancer, q pour quitter.', environments: WIN }, + ], + head_tail: [ + { command: 'head -n 3 documents/notes.txt', explanation: 'Affiche les 3 premières lignes. Sans -n, head en montre 10.', environments: UNIX }, + { command: 'tail -n 2 documents/notes.txt', explanation: 'Affiche les 2 dernières lignes. Avec -f, tail continue d\'afficher les lignes ajoutées au fichier, pratique pour suivre un journal (Ctrl+C pour arrêter).', environments: UNIX }, + { command: 'Get-Content documents\\notes.txt -TotalCount 3', explanation: 'Affiche les 3 premières lignes.', environments: WIN }, + { command: 'Get-Content documents\\notes.txt -Tail 2', explanation: 'Affiche les 2 dernières lignes. Ajoute -Wait pour suivre le fichier en direct.', environments: WIN }, + ], + + // ─── Recherche & Inspection ─────────────────────────────── + grep: [ + { command: 'grep Apprendre documents/notes.txt', explanation: 'Affiche les lignes qui contiennent le mot. grep distingue majuscules et minuscules : apprendre ne trouverait rien.', environments: UNIX }, + { command: 'grep -i fin documents/notes.txt', explanation: '-i ignore la casse : la ligne « Fin du fichier » est trouvée.', environments: UNIX }, + { command: 'grep -rn bash documents', explanation: '-r cherche dans tous les fichiers du dossier et -n ajoute le numéro de chaque ligne trouvée.', environments: UNIX }, + { command: 'Select-String -Pattern Apprendre -Path documents\\notes.txt', explanation: 'L\'équivalent PowerShell de grep. Attention : Select-String ignore la casse par défaut (-CaseSensitive pour la respecter).', environments: WIN }, + ], + find: [ + { command: 'find . -name "*.txt"', explanation: 'Cherche, depuis le dossier courant (.) et dans tous ses sous-dossiers, les fichiers dont le nom finit par .txt. Les guillemets empêchent le shell de remplacer * trop tôt.', environments: UNIX }, + { command: 'find . -type d', explanation: 'Liste uniquement les dossiers.', environments: UNIX }, + { command: 'Get-ChildItem -Recurse -Filter *.txt', explanation: 'L\'équivalent PowerShell : cherche les fichiers .txt dans le dossier courant et tous ses sous-dossiers.', environments: WIN }, + ], + wc: [ + { command: 'wc documents/notes.txt', explanation: 'Compte les lignes, les mots et les octets du fichier, dans cet ordre.', environments: UNIX }, + { command: 'wc -l documents/notes.txt', explanation: '-l ne compte que les lignes.', environments: UNIX }, + { command: 'ls | wc -l', explanation: 'Avec un pipe, wc compte ce qu\'une autre commande produit : ici, le nombre d\'éléments du dossier.', environments: UNIX }, + { command: '(Get-Content documents\\notes.txt).Count', explanation: 'Nombre de lignes du fichier : Get-Content renvoie une liste de lignes, .Count les compte.', environments: WIN }, + ], + + // ─── Système ────────────────────────────────────────────── + echo: [ + { command: 'echo "Bonjour"', explanation: 'Affiche le texte. Les guillemets gardent les espaces tels quels.' }, + { command: 'echo $HOME', explanation: 'Affiche la valeur d\'une variable : le $ demande au shell de la remplacer par son contenu.', environments: UNIX }, + { command: 'echo "Utilisateur : $USER"', explanation: 'Entre guillemets doubles, les variables sont remplacées. Entre guillemets simples, elles restent écrites telles quelles.', environments: UNIX }, + { command: 'echo $env:USERPROFILE', explanation: 'Dans PowerShell, les variables d\'environnement s\'écrivent $env:NOM.', environments: WIN }, + ], + date: [ + { command: 'date', explanation: 'Affiche la date et l\'heure actuelles.', environments: UNIX }, + { command: 'date +"%Y-%m-%d %H:%M"', explanation: 'Choisit le format : année-mois-jour heure:minute, pratique pour nommer un fichier de sauvegarde.', environments: UNIX }, + { command: 'Get-Date', explanation: 'Affiche la date et l\'heure actuelles.', environments: WIN }, + { command: 'Get-Date -Format "yyyy-MM-dd HH:mm"', explanation: 'Choisit le format. Attention à la casse : MM pour les mois, mm pour les minutes.', environments: WIN }, + ], + uname: [ + { command: 'uname', explanation: 'Affiche le nom du système : Linux, ou Darwin sur macOS.' }, + { command: 'uname -a', explanation: 'Toutes les informations : système, nom de la machine, version du noyau, architecture.' }, + ], + getcomputerinfo: [ + { command: 'Get-ComputerInfo', explanation: 'Liste des centaines d\'informations sur Windows et la machine. Peut prendre quelques secondes.' }, + { command: '(Get-ComputerInfo).WindowsProductName', explanation: 'Les parenthèses exécutent d\'abord la commande, puis le point lit une seule propriété : ici, l\'édition de Windows.' }, + { command: '$PSVersionTable.PSVersion', explanation: 'Affiche la version de PowerShell.' }, + ], + history: [ + { command: 'history', explanation: 'Affiche les commandes tapées précédemment, numérotées.', environments: UNIX }, + { command: 'history | grep cd', explanation: 'Retrouve une ancienne commande en filtrant l\'historique.', environments: UNIX }, + { command: 'Get-History', explanation: 'Affiche les commandes tapées dans cette session PowerShell (history est un raccourci).', environments: WIN }, + ], + man: [ + { command: 'man ls', explanation: 'Ouvre le manuel de ls : flèches pour défiler, / pour chercher, q pour quitter.', environments: UNIX }, + { command: 'man grep', explanation: 'Chaque commande a son manuel : la section OPTIONS liste tout ce qu\'on peut ajouter.', environments: UNIX }, + { command: 'Get-Help Get-ChildItem', explanation: 'Aide d\'une commande PowerShell.', environments: WIN }, + { command: 'Get-Help Get-ChildItem -Examples', explanation: 'Seulement des exemples prêts à l\'emploi : souvent le plus utile.', environments: WIN }, + ], + alias: [ + { command: 'alias ll=\'ls -la\'', explanation: 'Crée un raccourci : taper ll lancera ls -la. Il disparaît à la fermeture du terminal, sauf si tu l\'ajoutes dans ~/.bashrc ou ~/.zshrc.', environments: UNIX }, + { command: 'alias', explanation: 'Liste les raccourcis définis.', environments: UNIX }, + { command: 'Set-Alias -Name np -Value notepad', explanation: 'Crée un raccourci PowerShell. Il ne peut pas contenir d\'options : pour l\'équivalent de ls -la, il faut écrire une fonction.', environments: WIN }, + { command: 'Get-Alias', explanation: 'Liste les raccourcis : on y découvre que ls, cat ou cd sont des raccourcis de commandes PowerShell.', environments: WIN }, + ], + open: [ + { command: 'open documents/notes.txt', explanation: 'Ouvre le fichier avec l\'application par défaut, comme un double-clic dans le Finder.' }, + { command: 'open .', explanation: 'Ouvre le dossier courant dans le Finder.' }, + { command: 'open https://terminallearning.dev', explanation: 'Ouvre l\'adresse dans le navigateur par défaut.' }, + ], + pbcopy_pbpaste: [ + { command: 'cat documents/notes.txt | pbcopy', explanation: 'Copie le contenu du fichier dans le presse-papiers, prêt à être collé ailleurs.' }, + { command: 'pbpaste > copie.txt', explanation: 'Colle le contenu du presse-papiers dans un nouveau fichier.' }, + ], + brew: [ + { command: 'brew install tree', explanation: 'Installe un programme : Homebrew le télécharge et le rend disponible dans le terminal.' }, + { command: 'brew update', explanation: 'Met à jour la liste des programmes disponibles. Pour mettre à jour ceux qui sont installés : brew upgrade.' }, + { command: 'brew list', explanation: 'Liste les programmes installés avec Homebrew.' }, + ], + winget: [ + { command: 'winget search vscode', explanation: 'Cherche un logiciel dans le catalogue et affiche son identifiant.' }, + { command: 'winget install Git.Git', explanation: 'Installe un logiciel à partir de son identifiant exact.' }, + { command: 'winget list', explanation: 'Liste les logiciels installés sur la machine.' }, + ], + + // ─── Permissions & Utilisateurs ─────────────────────────── + chmod: [ + { command: 'chmod +x projets/script.sh', explanation: 'Rend le script exécutable. Sans ce droit, ./script.sh répond « Permission denied ».', environments: UNIX }, + { command: 'chmod 644 documents/notes.txt', explanation: 'Notation octale : 6 (lecture + écriture) pour toi, 4 (lecture seule) pour le groupe et pour les autres.', environments: UNIX }, + { command: 'chmod go-r projets/.env', explanation: 'Retire la lecture au groupe (g) et aux autres (o). Utile pour un fichier qui contient des mots de passe.', environments: UNIX }, + { command: 'icacls documents\\notes.txt', explanation: 'Affiche les droits du fichier. Windows ne gère pas les droits avec rwx mais avec des listes d\'accès (ACL).', environments: WIN }, + ], + chown: [ + { command: 'sudo chown user:user documents/notes.txt', explanation: 'Change le propriétaire et le groupe du fichier (propriétaire:groupe). Il faut presque toujours sudo.', environments: UNIX }, + { command: 'sudo chown -R user:user projets', explanation: '-R applique le changement au dossier et à tout son contenu.', environments: UNIX }, + { command: 'icacls documents\\notes.txt /setowner user', explanation: 'Change le propriétaire du fichier sous Windows (dans un terminal administrateur).', environments: WIN }, + ], + whoami: [ + { command: 'whoami', explanation: 'Affiche le nom de l\'utilisateur connecté. Utile avant une commande sensible, ou après sudo.' }, + ], + id: [ + { command: 'id', explanation: 'Affiche ton identifiant (uid), ton groupe principal et tous les groupes dont tu fais partie.', environments: UNIX }, + { command: 'id -Gn', explanation: 'Seulement les noms de tes groupes.', environments: UNIX }, + { command: 'whoami /groups', explanation: 'Liste les groupes de ton compte Windows.', environments: WIN }, + ], + sudo: [ + { command: 'sudo apt update', explanation: 'Lance la commande en administrateur (root). Le terminal demande ton mot de passe, qui ne s\'affiche pas pendant que tu le tapes.', environments: LINUX }, + { command: 'sudo whoami', explanation: 'Répond root : la commande tourne bien en administrateur.', environments: UNIX }, + { command: 'Start-Process powershell -Verb RunAs', explanation: 'Ouvre un nouveau PowerShell en administrateur, après une confirmation de Windows. Windows 11 propose aussi une commande sudo, à activer dans les paramètres.', environments: WIN }, + ], + umask: [ + { command: 'umask', explanation: 'Affiche le masque actuel : les droits retirés d\'office aux nouveaux fichiers. 022 est la valeur la plus courante.' }, + { command: 'umask 027', explanation: 'Les nouveaux fichiers ne seront plus lisibles par les autres utilisateurs. Valable jusqu\'à la fermeture du terminal.' }, + ], + getacl: [ + { command: 'Get-Acl documents\\notes.txt', explanation: 'Affiche le propriétaire et les droits d\'accès du fichier.' }, + { command: 'Get-Acl documents\\notes.txt | Format-List', explanation: 'Format-List affiche chaque propriété sur sa propre ligne : plus lisible.' }, + ], + + // ─── Processus & Tâches ─────────────────────────────────── + ps: [ + { command: 'ps', explanation: 'Liste les processus lancés depuis ce terminal.', environments: UNIX }, + { command: 'ps aux', explanation: 'Tous les processus de la machine, avec leur propriétaire, leur numéro (PID) et leur usage du processeur et de la mémoire.', environments: UNIX }, + { command: 'Get-Process', explanation: 'Liste les processus en cours, avec leur numéro (Id) et leur mémoire.', environments: WIN }, + { command: 'tasklist', explanation: 'La commande Windows classique, qui fonctionne aussi dans PowerShell.', environments: WIN }, + ], + kill: [ + { command: 'kill 1234', explanation: 'Demande au processus n° 1234 de s\'arrêter proprement (signal TERM). Son numéro se trouve avec ps.', environments: UNIX }, + { command: 'kill -9 1234', explanation: 'Force l\'arrêt immédiat (signal KILL), sans laisser le programme sauvegarder. En dernier recours.', environments: UNIX }, + { command: 'Stop-Process -Id 1234', explanation: 'Arrête le processus n° 1234. Son numéro se trouve avec Get-Process.', environments: WIN }, + { command: 'Stop-Process -Name notepad', explanation: 'Arrête un programme par son nom plutôt que par son numéro.', environments: WIN }, + ], + top_htop: [ + { command: 'top', explanation: 'Tableau de bord en direct des processus, triés par usage du processeur. q pour quitter.', environments: UNIX }, + { command: 'htop', explanation: 'Une version plus lisible et colorée de top, souvent à installer (apt install htop, brew install htop).', environments: UNIX }, + { command: 'tasklist', explanation: 'Liste les processus. Pour une vue en direct, ouvre le Gestionnaire des tâches (Ctrl+Maj+Échap).', environments: WIN }, + ], + jobs: [ + { command: 'sleep 100 &', explanation: 'Le & lance la commande en arrière-plan : le terminal te rend la main tout de suite.', environments: UNIX }, + { command: 'jobs', explanation: 'Liste les tâches en arrière-plan de ce terminal, avec leur numéro.', environments: UNIX }, + { command: 'Start-Job { Start-Sleep 100 }', explanation: 'Lance le bloc de commandes entre accolades en arrière-plan.', environments: WIN }, + { command: 'Get-Job', explanation: 'Liste les tâches en arrière-plan et leur état.', environments: WIN }, + ], + bg: [ + { command: 'bg %1', explanation: 'Relance en arrière-plan la tâche n° 1, par exemple après l\'avoir mise en pause avec Ctrl+Z.', environments: UNIX }, + { command: 'Start-Job { Start-Sleep 100 }', explanation: 'PowerShell n\'a pas de bg : on lance directement la tâche en arrière-plan.', environments: WIN }, + ], + fg: [ + { command: 'fg %1', explanation: 'Ramène la tâche n° 1 au premier plan : elle reprend le contrôle du terminal.', environments: UNIX }, + { command: 'fg', explanation: 'Sans numéro, ramène la dernière tâche lancée.', environments: UNIX }, + { command: 'Receive-Job -Id 1 -Wait', explanation: 'Attend la fin de la tâche n° 1 et affiche son résultat.', environments: WIN }, + ], + + // ─── Pipes & Redirections ───────────────────────────────── + redirect_output: [ + { command: 'echo "Bonjour" > salut.txt', explanation: '> envoie la sortie dans un fichier au lieu de l\'écran. Le fichier est créé, ou écrasé s\'il existait.' }, + { command: 'echo "Encore" >> salut.txt', explanation: '>> ajoute à la fin du fichier sans effacer ce qu\'il contient.' }, + { command: 'ls > liste.txt', explanation: 'Enregistre la liste des fichiers dans liste.txt.', environments: UNIX }, + { command: 'Get-ChildItem | Out-File liste.txt', explanation: 'Out-File est la forme longue de > dans PowerShell.', environments: WIN }, + ], + pipes: [ + { command: 'ls | wc -l', explanation: 'Le | envoie la sortie de ls à wc, qui compte les lignes.', environments: UNIX }, + { command: 'cat documents/notes.txt | grep Pratiquer', explanation: 'Filtre le contenu du fichier pour ne garder que la ligne qui contient « Pratiquer ».', environments: UNIX }, + { command: 'ps aux | grep bash', explanation: 'Cherche un programme précis dans la longue liste des processus.', environments: UNIX }, + { command: 'Get-Content documents\\notes.txt | Select-String Pratiquer', explanation: 'Dans PowerShell, le pipe transporte des objets : ici, les lignes du fichier, filtrées par Select-String.', environments: WIN }, + ], + tee: [ + { command: 'ls | tee liste.txt', explanation: 'Affiche la sortie à l\'écran ET l\'écrit dans liste.txt, comme un raccord en T de plomberie.', environments: UNIX }, + { command: 'ls | tee -a liste.txt', explanation: '-a ajoute au fichier au lieu de l\'écraser.', environments: UNIX }, + { command: 'Get-ChildItem | Tee-Object liste.txt', explanation: 'L\'équivalent PowerShell de tee.', environments: WIN }, + ], + redirect_stderr: [ + { command: 'ls inexistant 2> erreurs.txt', explanation: '2> envoie les messages d\'erreur dans un fichier. Les résultats normaux, eux, restent à l\'écran.', environments: UNIX }, + { command: 'ls inexistant 2>/dev/null', explanation: '/dev/null est un trou noir : le message d\'erreur disparaît.', environments: UNIX }, + { command: 'Get-ChildItem inexistant 2>$null', explanation: 'Dans PowerShell, $null joue le rôle de /dev/null.', environments: WIN }, + ], + redirect_stderr_stdout: [ + { command: 'ls documents inexistant > tout.txt 2>&1', explanation: 'Envoie la sortie normale ET les erreurs dans le même fichier. L\'ordre compte : > d\'abord, 2>&1 ensuite.', environments: UNIX }, + { command: 'ls inexistant 2>&1 | grep inexistant', explanation: 'Fait passer les erreurs dans le pipe, pour pouvoir les filtrer comme du texte normal.', environments: UNIX }, + { command: 'Get-ChildItem inexistant *> tout.txt', explanation: '*> redirige tous les flux de PowerShell (sortie, erreurs, avertissements) vers le fichier.', environments: WIN }, + ], + + // ─── Archives & Compression ─────────────────────────────── + tar: [ + { command: 'tar -czf sauvegarde.tar.gz documents', explanation: 'Crée (c) une archive compressée (z) dans le fichier (f) sauvegarde.tar.gz, avec le dossier documents.' }, + { command: 'tar -tzf sauvegarde.tar.gz', explanation: 'Liste (t) le contenu de l\'archive sans l\'extraire.' }, + { command: 'tar -xzf sauvegarde.tar.gz', explanation: 'Extrait (x) l\'archive dans le dossier courant. Windows 10 et 11 incluent aussi tar.' }, + ], + zip_unzip: [ + { command: 'zip -r documents.zip documents', explanation: '-r inclut le dossier et tout son contenu dans l\'archive.', environments: UNIX }, + { command: 'unzip documents.zip', explanation: 'Extrait l\'archive dans le dossier courant.', environments: UNIX }, + { command: 'Compress-Archive -Path documents -DestinationPath documents.zip', explanation: 'Crée une archive .zip avec le dossier documents.', environments: WIN }, + { command: 'Expand-Archive documents.zip -DestinationPath extrait', explanation: 'Extrait l\'archive dans le dossier extrait.', environments: WIN }, + ], + + // ─── Variables & Scripts ────────────────────────────────── + export: [ + { command: 'export EDITOR=nano', explanation: 'Crée une variable d\'environnement, transmise aux programmes lancés depuis ce terminal. Pas d\'espace autour du =.', environments: UNIX }, + { command: 'export PATH="$PATH:$HOME/bin"', explanation: 'Ajoute un dossier au PATH, la liste des dossiers où le shell cherche les commandes. $PATH au début garde les dossiers déjà présents.', environments: UNIX }, + { command: 'echo $EDITOR', explanation: 'Vérifie la valeur de la variable.', environments: UNIX }, + { command: '$env:EDITOR = "notepad"', explanation: 'Crée une variable d\'environnement pour cette session PowerShell. Ici, les espaces autour du = sont permis.', environments: WIN }, + { command: '$env:Path += ";C:\\outils"', explanation: 'Ajoute un dossier au PATH. Sous Windows, les dossiers sont séparés par ; et non par :.', environments: WIN }, + ], + env: [ + { command: 'env', explanation: 'Liste toutes les variables d\'environnement et leur valeur.', environments: UNIX }, + { command: 'env | grep HOME', explanation: 'Ne garde que les lignes qui parlent de HOME.', environments: UNIX }, + { command: 'Get-ChildItem Env:', explanation: 'Liste toutes les variables d\'environnement : PowerShell les présente comme un disque nommé Env:.', environments: WIN }, + { command: '$env:USERNAME', explanation: 'Affiche une seule variable.', environments: WIN }, + ], + source: [ + { command: 'source ~/.bashrc', explanation: 'Relit ta configuration dans le terminal actuel : un alias ou une variable que tu viens d\'ajouter s\'applique tout de suite, sans rouvrir le terminal.', environments: LINUX }, + { command: '. ~/.bashrc', explanation: 'Le point suivi d\'une espace est un raccourci de source.', environments: LINUX }, + { command: 'source ~/.zshrc', explanation: 'Même chose pour zsh, le shell par défaut de macOS.', environments: MACOS }, + { command: '. $PROFILE', explanation: 'Recharge ton profil PowerShell, l\'équivalent de ~/.bashrc.', environments: WIN }, + ], + run_script: [ + { command: 'cd projets', explanation: 'Le script de l\'exemple se trouve dans le dossier projets.' }, + { command: 'chmod +x script.sh', explanation: 'Donne le droit d\'exécution au script. Une seule fois suffit.', environments: UNIX }, + { command: './script.sh', explanation: 'Lance le script du dossier courant. Le ./ est obligatoire : par sécurité, le shell ne cherche pas les commandes dans le dossier courant.', environments: UNIX }, + { command: 'bash script.sh', explanation: 'Lance le script avec bash, même sans droit d\'exécution.', environments: UNIX }, + { command: 'Set-Content bonjour.ps1 \'Write-Output "Bonjour"\'', explanation: 'Crée un petit script PowerShell : les scripts Windows finissent par .ps1.', environments: WIN }, + { command: '.\\bonjour.ps1', explanation: 'Lance le script. Si Windows refuse, autorise tes scripts une fois pour toutes : Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.', environments: WIN }, + ], + crontab: [ + { command: 'crontab -l', explanation: 'Liste tes tâches planifiées.', environments: UNIX }, + { command: 'crontab -e', explanation: 'Ouvre l\'éditeur pour ajouter une tâche. Une ligne comme 0 9 * * 1-5 ~/sauvegarde.sh lance le script à 9 h, du lundi au vendredi.', environments: UNIX }, + { command: 'Get-ScheduledTask', explanation: 'Liste les tâches planifiées de Windows, l\'équivalent de cron.', environments: WIN }, + ], + printenv: [ + { command: 'printenv', explanation: 'Liste toutes les variables d\'environnement.' }, + { command: 'printenv HOME', explanation: 'Affiche une seule variable. Ici, pas de $ devant le nom.' }, + ], + + // ─── Réseau & SSH ───────────────────────────────────────── + ping: [ + { command: 'ping -c 4 google.com', explanation: 'Envoie 4 paquets et mesure le temps de réponse. Sans -c, Linux et macOS continuent jusqu\'à Ctrl+C.', environments: UNIX }, + { command: 'ping -c 4 8.8.8.8', explanation: 'On peut viser une adresse IP. Si l\'IP répond mais pas le nom de domaine, le problème vient du DNS.', environments: UNIX }, + { command: 'ping google.com', explanation: 'Windows envoie 4 paquets par défaut, puis s\'arrête.', environments: WIN }, + { command: 'ping -n 10 google.com', explanation: 'Sous Windows, -n choisit le nombre de paquets (et non -c).', environments: WIN }, + ], + curl: [ + { command: 'curl https://api.github.com', explanation: 'Télécharge la réponse d\'une adresse et l\'affiche dans le terminal.' }, + { command: 'curl -I https://example.com', explanation: '-I ne demande que les en-têtes de la réponse : code de statut, type de contenu, taille.' }, + { command: 'curl -L -o page.html https://example.com', explanation: '-o enregistre la réponse dans un fichier et -L suit les redirections.' }, + { command: 'Invoke-WebRequest -Uri https://api.github.com', explanation: 'La commande PowerShell équivalente, qui renvoie un objet avec le code de statut, les en-têtes et le contenu.', environments: WIN }, + ], + wget: [ + { command: 'wget https://example.com/fichier.zip', explanation: 'Télécharge le fichier dans le dossier courant.', environments: LINUX }, + { command: 'wget -O archive.zip https://example.com/fichier.zip', explanation: '-O choisit le nom du fichier enregistré.', environments: LINUX }, + { command: 'wget -c https://example.com/fichier.zip', explanation: '-c reprend un téléchargement interrompu au lieu de tout recommencer.', environments: LINUX }, + { command: 'curl -O https://example.com/fichier.zip', explanation: 'macOS n\'a pas wget par défaut : curl -O télécharge le fichier sous son nom d\'origine.', environments: MACOS }, + { command: 'Invoke-WebRequest -Uri https://example.com/fichier.zip -OutFile fichier.zip', explanation: '-OutFile enregistre le fichier. Sans lui, la réponse est seulement affichée.', environments: WIN }, + ], + dns: [ + { command: 'nslookup google.com', explanation: 'Demande à ton serveur DNS l\'adresse IP qui correspond au nom de domaine.' }, + { command: 'dig +short google.com', explanation: 'Réponse courte : seulement les adresses IP.', environments: UNIX }, + { command: 'dig @1.1.1.1 google.com', explanation: 'Pose la question à un serveur DNS précis (ici celui de Cloudflare), pour comparer avec le tien.', environments: UNIX }, + { command: 'Resolve-DnsName google.com', explanation: 'L\'équivalent PowerShell, plus détaillé que nslookup.', environments: WIN }, + ], + ssh: [ + { command: 'ssh user@serveur.example.com', explanation: 'Ouvre une session sur une machine distante, au nom de l\'utilisateur user. Tape exit pour revenir.' }, + { command: 'ssh -p 2222 user@serveur.example.com', explanation: '-p (minuscule) choisit le port, quand le serveur n\'écoute pas sur le port 22 habituel.' }, + { command: 'ssh-keygen -t ed25519 -C "moi@example.com"', explanation: 'Crée une paire de clés. La clé privée ne quitte jamais ta machine ; la clé publique (.pub) se dépose sur les serveurs.' }, + { command: 'ssh-copy-id user@serveur.example.com', explanation: 'Dépose ta clé publique sur le serveur : tu t\'y connecteras ensuite sans mot de passe.', environments: UNIX }, + ], + scp: [ + { command: 'scp documents/notes.txt user@serveur.example.com:/home/user/', explanation: 'Copie un fichier vers une machine distante. Le : sépare le nom du serveur du chemin sur ce serveur.' }, + { command: 'scp -r documents user@serveur.example.com:/home/user/', explanation: '-r copie un dossier entier.' }, + { command: 'scp -P 2222 documents/notes.txt user@serveur.example.com:/home/user/', explanation: '-P (majuscule) choisit le port, alors que ssh utilise -p minuscule.' }, + { command: 'scp user@serveur.example.com:/home/user/rapport.pdf .', explanation: 'Dans l\'autre sens : récupère un fichier distant dans le dossier courant (.).' }, + ], + + // ─── Git Fondamentaux ───────────────────────────────────── + git_init: [ + { command: 'git init', explanation: 'Transforme le dossier courant en dépôt Git : un dossier caché .git apparaît pour stocker l\'historique.' }, + { command: 'git init mon-projet', explanation: 'Crée le dossier mon-projet et l\'initialise directement.' }, + ], + git_config: [ + { command: 'git config --global user.name "Alice Martin"', explanation: 'Ton nom, inscrit dans chacun de tes commits. --global : valable pour tous tes dépôts.' }, + { command: 'git config --global user.email "alice@example.com"', explanation: 'Ton adresse, inscrite dans chaque commit. Sur GitHub, elle relie tes commits à ton compte.' }, + { command: 'git config --list', explanation: 'Affiche la configuration active.' }, + ], + git_add: [ + { command: 'git add README.md', explanation: 'Place le fichier dans la zone de préparation : il fera partie du prochain commit.' }, + { command: 'git add .', explanation: 'Prépare tous les changements du dossier courant. Vérifie d\'abord avec git status ce qui va partir.' }, + ], + git_commit: [ + { command: 'git commit -m "feat: ajoute la page contact"', explanation: 'Enregistre les changements préparés, avec un message qui dit ce qui change.' }, + { command: 'git commit -am "fix: corrige le lien du menu"', explanation: '-a prépare d\'abord les fichiers déjà suivis et modifiés. Les fichiers nouveaux, eux, demandent toujours un git add.' }, + ], + git_status: [ + { command: 'git status', explanation: 'Montre ta branche et l\'état des fichiers : modifiés, préparés pour le commit, ou pas encore suivis.' }, + { command: 'git status -s', explanation: 'Version courte : une ligne par fichier (M modifié, A ajouté, ?? pas suivi).' }, + ], + git_log: [ + { command: 'git log', explanation: 'L\'historique complet des commits, du plus récent au plus ancien. q pour quitter.' }, + { command: 'git log --oneline', explanation: 'Un commit par ligne : identifiant court et message.' }, + { command: 'git log --graph --oneline --all', explanation: 'Dessine toutes les branches et leurs fusions.' }, + ], + git_diff: [ + { command: 'git diff', explanation: 'Les changements pas encore préparés avec git add.' }, + { command: 'git diff --staged', explanation: 'Les changements préparés, ceux qui partiront dans le prochain commit.' }, + { command: 'git diff main feature/login', explanation: 'Compare deux branches.' }, + ], + gitignore: [ + { command: 'echo "node_modules/" >> .gitignore', explanation: 'Ajoute une règle au fichier .gitignore : Git ignorera tout le dossier node_modules.' }, + { command: 'echo "*.log" >> .gitignore', explanation: '* remplace n\'importe quel texte : tous les fichiers qui finissent par .log sont ignorés.' }, + { command: 'cat .gitignore', explanation: 'Affiche les règles : une par ligne.', environments: UNIX }, + { command: 'Get-Content .gitignore', explanation: 'Affiche les règles : une par ligne.', environments: WIN }, + ], + git_branch: [ + { command: 'git branch', explanation: 'Liste les branches ; l\'étoile marque celle où tu te trouves.' }, + { command: 'git branch feature/contact', explanation: 'Crée la branche, sans t\'y déplacer.' }, + { command: 'git switch feature/contact', explanation: 'Bascule sur la branche.' }, + { command: 'git switch -c feature/menu', explanation: 'Crée la branche et bascule dessus en une seule commande.' }, + ], + git_merge: [ + { command: 'git merge feature/login', explanation: 'Intègre dans ta branche actuelle les commits de feature/login.' }, + { command: 'git merge --no-ff feature/login', explanation: 'Crée toujours un commit de fusion, même quand Git pourrait simplement avancer ta branche : l\'historique garde la trace de la branche.' }, + ], + + // ─── GitHub & Collaboration ─────────────────────────────── + git_remote: [ + { command: 'git remote -v', explanation: 'Liste les dépôts distants et leurs adresses.' }, + { command: 'git remote add upstream https://github.com/alice/mon-projet.git', explanation: 'Relie ton dépôt à un autre dépôt distant, sous le nom upstream. Le premier s\'appelle en général origin.' }, + ], + git_push: [ + { command: 'git push -u origin main', explanation: 'Premier envoi : publie la branche main et la relie à origin. Ensuite, git push suffit.' }, + { command: 'git push', explanation: 'Envoie tes nouveaux commits vers la branche distante reliée.' }, + ], + git_pull: [ + { command: 'git pull', explanation: 'Récupère les nouveaux commits du dépôt distant et les fusionne dans ta branche.' }, + { command: 'git pull origin main', explanation: 'Précise le dépôt distant et la branche à récupérer.' }, + ], + git_fetch: [ + { command: 'git fetch', explanation: 'Télécharge les nouveautés du dépôt distant sans toucher à tes fichiers : tu décides ensuite quoi fusionner.' }, + { command: 'git fetch origin', explanation: 'Précise le dépôt distant à interroger.' }, + ], + git_clone: [ + { command: 'git clone https://github.com/alice/mon-projet.git', explanation: 'Copie le dépôt complet, avec tout son historique, dans un nouveau dossier mon-projet.' }, + { command: 'git clone git@github.com:alice/mon-projet.git', explanation: 'Même chose par SSH : il faut une clé SSH ajoutée à ton compte GitHub.' }, + ], + git_rebase: [ + { command: 'git rebase main', explanation: 'Rejoue tes commits par-dessus la dernière version de main, pour un historique en ligne droite.' }, + { command: 'git rebase -i HEAD~3', explanation: 'Mode interactif : réordonne, fusionne ou renomme tes 3 derniers commits.' }, + ], + git_cherry_pick: [ + { command: 'git cherry-pick a1b2c3d', explanation: 'Copie un seul commit, repéré par son identifiant (git log --oneline), dans ta branche actuelle.' }, + { command: 'git cherry-pick a1b2c3d..e4f5a6b', explanation: 'Copie une série de commits. Attention : le premier de la plage (a1b2c3d) n\'est pas inclus.' }, + ], +}; + +/** + * Commands the practice terminal does not simulate yet, per environment. The + * reference says so, so a learner who tries one in a lesson knows why it fails. + * `commandReference.test.ts` keeps this list in sync with the engine. + */ +export const NOT_SIMULATED: Record = { + tree: ['linux', 'macos', 'windows'], + less_more: ['linux', 'macos', 'windows'], + find: ['linux', 'macos', 'windows'], + getcomputerinfo: ['windows'], + alias: ['linux', 'macos', 'windows'], + id: ['linux', 'macos', 'windows'], + umask: ['linux', 'macos'], + jobs: ['linux', 'macos', 'windows'], + bg: ['linux', 'macos', 'windows'], + fg: ['linux', 'macos', 'windows'], + tar: ['linux', 'macos', 'windows'], + zip_unzip: ['linux', 'macos', 'windows'], +}; diff --git a/src/app/types/curriculum.ts b/src/app/types/curriculum.ts index 888a6d8..1339cca 100644 --- a/src/app/types/curriculum.ts +++ b/src/app/types/curriculum.ts @@ -54,6 +54,17 @@ export interface OfficialDoc { url: string; } +// --- Explained example (reference page) --- + +export interface CommandExample { + /** Exactly what the learner types. */ + command: string; + /** What it does, written for a beginner. */ + explanation: string; + /** Shown only on these environments; omitted = every environment the command supports. */ + environments?: EnvironmentId[]; +} + // --- Enriched command (for reference + lessons) --- export interface EnrichedCommand { @@ -66,8 +77,10 @@ export interface EnrichedCommand { compatibility: EnvironmentId[]; syntax: string; summary: string; - examples: string[]; + examples: CommandExample[]; commonErrors: string[]; + /** Environments where the practice terminal does not simulate this command yet. */ + notSimulatedOn?: EnvironmentId[]; /** Authoritative documentation links shown on /app/reference. Optional. */ officialDocs?: OfficialDoc[]; } diff --git a/src/test/commandReference.test.ts b/src/test/commandReference.test.ts new file mode 100644 index 0000000..f659bee --- /dev/null +++ b/src/test/commandReference.test.ts @@ -0,0 +1,86 @@ +/** + * /app/reference examples (commandExamples.ts): consistent with the catalogue, + * and runnable in the practice terminal. The replay found 224 of 464 examples + * printing an error (26 September 2026): made-up file names, comments inside the + * command, bash shown to Windows learners. Remaining gaps are listed in + * commandReferenceGaps.ts and may only shrink. + */ +import { describe, it, expect } from 'vitest'; +import { commandCatalogue } from '../app/data/commandCatalogue'; +import { COMMAND_EXAMPLES, NOT_SIMULATED } from '../app/data/commandExamples'; +import { REFERENCE_ENVS, replayReference } from './commandReferenceReplay'; +import { KNOWN_REFERENCE_GAPS } from './commandReferenceGaps'; + +const commands = commandCatalogue.flatMap((c) => c.commands); +const ids = new Set(commands.map((c) => c.id)); + +describe('reference examples — data', () => { + it('every catalogue command has explained examples, and every entry belongs to a command', () => { + expect(commands.length).toBeGreaterThan(70); + for (const cmd of commands) expect(COMMAND_EXAMPLES[cmd.id], cmd.id).toBeDefined(); + for (const id of Object.keys(COMMAND_EXAMPLES)) expect(ids.has(id), `orphan examples: ${id}`).toBe(true); + }); + + it('each OS a command supports shows at least one example', () => { + for (const cmd of commands) { + for (const env of REFERENCE_ENVS) { + if (!cmd.compatibility.includes(env)) continue; + const shown = cmd.examples.filter((ex) => !ex.environments || ex.environments.includes(env)); + expect(shown.length, `${cmd.id} shows no example on ${env}`).toBeGreaterThan(0); + } + } + }); + + it('examples only target OSes the command supports, and every one is explained', () => { + for (const cmd of commands) { + for (const ex of cmd.examples) { + expect(ex.command.trim(), cmd.id).toBe(ex.command); + expect(ex.command.length, cmd.id).toBeGreaterThan(0); + expect(ex.explanation.length, `${cmd.id}: ${ex.command}`).toBeGreaterThan(10); + for (const env of ex.environments ?? []) { + expect(cmd.compatibility, `${cmd.id}: ${ex.command} targets ${env}`).toContain(env); + } + } + } + }); + + it('an example is a command, not a command followed by a comment', () => { + // The old catalogue had "tree /F (Windows : …)" and "… 2>/dev/null # ignorer": + // a learner copying them typed the comment too. Notes belong in `explanation`. + for (const cmd of commands) { + for (const ex of cmd.examples) { + expect(ex.command, cmd.id).not.toMatch(/\s#\s|\s{2,}\(/); + } + } + }); + + it('NOT_SIMULATED names real commands and OSes they support', () => { + for (const [id, envs] of Object.entries(NOT_SIMULATED)) { + const cmd = commands.find((c) => c.id === id); + expect(cmd, id).toBeDefined(); + for (const env of envs) expect(cmd!.compatibility, `${id} ${env}`).toContain(env); + expect(cmd!.notSimulatedOn).toEqual(envs); + } + }); +}); + +describe('reference examples — replayed in the practice terminal', () => { + const rows = replayReference(); + const failing = new Set( + rows.filter((r) => r.errors.length > 0 && !NOT_SIMULATED[r.commandId]?.includes(r.env)).map((r) => r.key), + ); + + it('replays every shown example', () => { + expect(rows.length).toBeGreaterThan(300); + }); + + it('no new example prints an error', () => { + const added = [...failing].filter((k) => !KNOWN_REFERENCE_GAPS.has(k)); + expect(added, 'new failing examples — fix them, or the engine').toEqual([]); + }); + + it('fixed gaps are removed from the list (it may only shrink)', () => { + const fixed = [...KNOWN_REFERENCE_GAPS].filter((k) => !failing.has(k)); + expect(fixed, 'these now pass — delete them from commandReferenceGaps.ts').toEqual([]); + }); +}); diff --git a/src/test/commandReferenceGaps.ts b/src/test/commandReferenceGaps.ts new file mode 100644 index 0000000..952491c --- /dev/null +++ b/src/test/commandReferenceGaps.ts @@ -0,0 +1,50 @@ +/** + * Reference examples that still print an error in the practice terminal + * (commandReference.test.ts). The list may only shrink: fix the engine, then + * delete the line. Commands listed in NOT_SIMULATED (commandExamples.ts) are + * exempt, since the reference already tells the learner they are not simulated. + * + * Causes, by family: + * - PowerShell cmdlets and parameters not simulated yet: Get-Date, Get-History, + * Get-Help, Set-Content, Get-ScheduledTask, tasklist, Get-Content -TotalCount / -Tail, + * `$env:Path +=`, `$env:USERNAME`. + * - Unix options not simulated yet: `cat -n`, `grep -r` on a directory, ssh-copy-id. + * - Git: `git commit -am` loses its message (engine bug), `git rebase -i` prints its + * warning as an error, cherry-pick examples name commits the practice repository does not have. + * + * Key: ` [] `. + */ +export const KNOWN_REFERENCE_GAPS = new Set([ + "cat [linux] cat -n documents/notes.txt", + "cat [macos] cat -n documents/notes.txt", + "head_tail [windows] Get-Content documents\\notes.txt -TotalCount 3", + "head_tail [windows] Get-Content documents\\notes.txt -Tail 2", + "grep [linux] grep -rn bash documents", + "grep [macos] grep -rn bash documents", + "date [windows] Get-Date", + "date [windows] Get-Date -Format \"yyyy-MM-dd HH:mm\"", + "history [windows] Get-History", + "man [windows] Get-Help Get-ChildItem", + "man [windows] Get-Help Get-ChildItem -Examples", + "ps [windows] tasklist", + "top_htop [windows] tasklist", + "export [windows] $env:Path += \";C:\\outils\"", + "env [windows] $env:USERNAME", + "run_script [windows] Set-Content bonjour.ps1 'Write-Output \"Bonjour\"'", + "run_script [windows] .\\bonjour.ps1", + "crontab [windows] Get-ScheduledTask", + "ssh [linux] ssh-copy-id user@serveur.example.com", + "ssh [macos] ssh-copy-id user@serveur.example.com", + "git_commit [linux] git commit -am \"fix: corrige le lien du menu\"", + "git_commit [macos] git commit -am \"fix: corrige le lien du menu\"", + "git_commit [windows] git commit -am \"fix: corrige le lien du menu\"", + "git_rebase [linux] git rebase -i HEAD~3", + "git_rebase [macos] git rebase -i HEAD~3", + "git_rebase [windows] git rebase -i HEAD~3", + "git_cherry_pick [linux] git cherry-pick a1b2c3d", + "git_cherry_pick [linux] git cherry-pick a1b2c3d..e4f5a6b", + "git_cherry_pick [macos] git cherry-pick a1b2c3d", + "git_cherry_pick [macos] git cherry-pick a1b2c3d..e4f5a6b", + "git_cherry_pick [windows] git cherry-pick a1b2c3d", + "git_cherry_pick [windows] git cherry-pick a1b2c3d..e4f5a6b", +]); diff --git a/src/test/commandReferenceReplay.ts b/src/test/commandReferenceReplay.ts new file mode 100644 index 0000000..a434df9 --- /dev/null +++ b/src/test/commandReferenceReplay.ts @@ -0,0 +1,60 @@ +/** + * Replays every /app/reference example through the terminal engine (shared by + * commandReference.test.ts and ad-hoc checks). Same idea as lessonTheoryReplay: + * what the reference tells a learner to type must work in the practice terminal, + * or be listed as a known gap. + */ +import { commandCatalogue } from '../app/data/commandCatalogue'; +import { createInitialState, processCommand } from '../app/data/terminalEngine'; +import { gitRepoWithRemote } from '../app/data/lessonSetup'; +import type { TerminalState } from '../app/data/commands/types'; +import type { EnvironmentId } from '../app/types/curriculum'; + +export const REFERENCE_ENVS = ['linux', 'macos', 'windows'] as const; +export type ReferenceEnv = (typeof REFERENCE_ENVS)[number]; + +/** Git examples run inside a repository with a commit, an `origin` remote and a feature/login branch. */ +const GIT_CATEGORIES = new Set(['git', 'github-collaboration']); + +function startState(categoryId: string): TerminalState { + const base = createInitialState(); + if (!GIT_CATEGORIES.has(categoryId)) return base; + const repo = gitRepoWithRemote.apply(base); + if (!repo.git) throw new Error('gitRepoWithRemote must create a repository'); + return { ...repo, git: { ...repo.git, branches: [...repo.git.branches, 'feature/login'] } }; +} + +export interface ReplayedExample { + /** ` [] ` — stable key for the known-gaps list. */ + key: string; + commandId: string; + env: ReferenceEnv; + command: string; + errors: string[]; +} + +/** Examples of each command run in order, from a fresh state per command and environment. */ +export function replayReference(): ReplayedExample[] { + const rows: ReplayedExample[] = []; + for (const category of commandCatalogue) { + for (const cmd of category.commands) { + for (const env of REFERENCE_ENVS) { + if (!cmd.compatibility.includes(env as EnvironmentId)) continue; + let state = startState(category.id); + for (const ex of cmd.examples) { + if (ex.environments && !ex.environments.includes(env)) continue; + const result = processCommand(state, ex.command, env); + state = result.newState; + rows.push({ + key: `${cmd.id} [${env}] ${ex.command}`, + commandId: cmd.id, + env, + command: ex.command, + errors: result.lines.filter((l) => l.type === 'error').map((l) => l.text), + }); + } + } + } + } + return rows; +}