From 6b2170f2c36294ae65dcab88b919cc19691d8ea1 Mon Sep 17 00:00:00 2001 From: "Thierry V." <46031203+thierryvm@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:57:03 +0200 Subject: [PATCH 1/2] fix(landing): only list commands the terminal runs and the lessons teach The hero listed, per environment and level, 22 commands the simulator did not know (systemctl, launchctl, Get-Service, New-PSDrive...) and 4 no lesson taught. Each listed command now runs in that environment and appears as a word of its own in that environment's lesson code or exercises; landingTotals.test.ts checks both, and fails on the old list. - module subtitle: five levels, up to Git and AI (was "two levels") - module counts from MODULE_PREVIEWS instead of a hardcoded 11 - roadmap: real git branches, the app install guide and the explained reference examples are available; in progress now names multi-step exercises and the taught commands not simulated yet (find, sort, diff) - tests 2 700+ -> 2 900+; docs/plan.md banner brought up to #400 - CHANGELOG and STORY Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 ++++++++ STORY.md | 10 ++++++++ docs/plan.md | 4 ++-- src/app/components/Landing.tsx | 6 ++--- src/app/data/landingContent.ts | 43 +++++++++++++++++----------------- src/test/landingTotals.test.ts | 42 +++++++++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03307af..61df963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ --- +## 🪧 29 septembre 2026 — La page d'accueil ne promet que ce qui existe + +*Page d'accueil · feuille de route · un test empêche la dérive* + +- **Des commandes qui marchent vraiment.** Sous « Choisissez votre environnement », la page listait par niveau des commandes que le terminal ne connaissait pas : `systemctl`, `launchctl`, `caffeinate`, `Get-Service`, `New-PSDrive` et dix-sept autres. Un visiteur qui en tapait une recevait « commande introuvable ». Chaque commande affichée est maintenant enseignée dans une leçon et fonctionne dans le terminal, et un test le vérifie à chaque changement. +- **Le bon résumé des modules.** « Du système de fichiers à la redirection de flux — deux niveaux » datait des débuts : le parcours va aujourd'hui jusqu'à Git et à l'IA, sur cinq niveaux. +- **Une feuille de route à jour.** Les vraies branches Git, le guide d'installation de l'app et les exemples expliqués de la Référence passent dans « Disponible ». « En cours » annonce ce qui reste vraiment à faire : les exercices en plusieurs étapes, et les commandes enseignées que le terminal ne simule pas encore (`find`, `sort`, `diff`, `tar`…). + +--- + ## 🌳 29 septembre 2026 — Les branches Git existent pour de vrai *Modules Git et GitHub · valeurs attendues relevées sur Git 2.56 · identifiants de commit identiques à ceux du vrai Git* diff --git a/STORY.md b/STORY.md index 7d0c3a5..f27ec57 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 --- +## La page d'accueil, relue comme un visiteur (29 septembre 2026) + +Thierry m'a demandé de vérifier que la page d'accueil reflète ce qui existe vraiment. Les chiffres étaient justes : un test les compare déjà au programme réel. Le problème était ailleurs. Sous chaque environnement, la page affiche des commandes par niveau, et j'ai tapé chacune d'elles dans le terminal au lieu de les relire. Vingt-deux répondaient « commande introuvable », et quatre autres n'étaient enseignées dans aucune leçon. `systemctl`, `launchctl`, `Get-Service` : des promesses écrites au début du projet, jamais tenues depuis. + +La correction tient en deux gestes : ne garder que des commandes enseignées qui fonctionnent, et ajouter un test qui exécute chaque commande affichée. La prochaine promesse non tenue fera échouer la vérification avant d'arriver en ligne. + +Au passage, le même test a mis au jour une dette plus profonde. Une vingtaine de commandes sont expliquées dans les leçons sans exister dans le terminal : `find`, `sort`, `diff`, `tar`, `Where-Object`… La feuille de route publique le dit maintenant, dans « En cours ». + +--- + ## Des branches qui existent vraiment (29 septembre 2026) Thierry avait posé la question simplement : « Pour Git, c'est très basique, je présume que c'est l'équivalent d'un premier niveau ? » C'était vrai, et pour une raison précise. On ne peut pas enseigner les branches avec un simulateur où une branche n'est qu'un nom. Il l'a dit mieux que moi : on n'apprend rien correctement sans une théorie et une pratique qui fonctionnent. Avant d'écrire la moindre leçon avancée, il fallait donc que le moteur fusionne pour de bon. diff --git a/docs/plan.md b/docs/plan.md index aea3e93..b571ec1 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,7 +1,7 @@ # Terminal Learning — Plan de lancement public -> Dernière mise à jour : **26 septembre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353)** : #389 vraie couche shell (**198/198 exercices sans ligne rouge**) · #390 sorties d'un vrai shell · #391 rejeu permanent de la théorie · #392 `cp`/`mv` vers un dossier + chemins Windows natifs (écarts théorie **174 → 162**) · #393 agents d'audit remis à jour + `terminal-fidelity-auditor` · roadmap publique corrigée. Suite : messages d'erreur PowerShell, page Référence, écarts restants. -> Précédent (24 septembre 2026) : P0 livré — #383 dépendances (react-router 7.18.4, npm audit 41 → 24, 0 critique) + #384 THI-340 (preuve d'âge posée sur tous les retours OAuth). P1 « fidélité du terminal » (THI-353) en cours — #385 état de départ par leçon, #386 ligatures monospace (vérifié sur iPhone, THI-354), #387 scripts / `$PROFILE` / ExecutionPolicy / `chmod` : exercices validés malgré une erreur **44 → 3**. Suite : redirections (`2>`, `>`, `tee`), écarts théorie ↔ terminal, puis garde « pas de validation si erreur ». Précédent : 6 juin 2026 — audit complet pré-pause (voir CHANGELOG). +> Dernière mise à jour : **29 septembre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353), suite** : #396 exemples de la Référence tous exécutables · #397 tableaux, lignes vides et touches du clavier mobile · #398 haut de l'app installée dégagé du flou d'iOS 26 · #399 git lit les vrais fichiers, sorties identiques à Git 2.56 · **#400 vraies branches Git** (graphe de commits, identifiants identiques au vrai Git, fusions et vrais conflits ; deux audits de fidélité et deux revues de code) · page d'accueil : ne promet plus que des commandes simulées. Suite : exercices en plusieurs étapes (validés sur l'état du terminal), puis les ~20 commandes enseignées mais pas encore simulées (`find`, `sort`, `diff`, `tar`, `Where-Object`…). +> Précédent : **26 septembre 2026 CEST** — **🩺 Grand Check-up, P1 « fidélité du terminal » (THI-353)** : #389 vraie couche shell (**198/198 exercices sans ligne rouge**) · #390 sorties d'un vrai shell · #391 rejeu permanent de la théorie · #392 `cp`/`mv` vers un dossier + chemins Windows natifs (écarts théorie **174 → 162**) · #393 agents d'audit remis à jour + `terminal-fidelity-auditor` · roadmap publique corrigée. Suite : messages d'erreur PowerShell, page Référence, écarts restants. > Statut global : **Phase 5 EN COURS** — Curriculum Expansion : 11 modules ✅, 66 leçons, **2500+ tests unitaires** + 176 E2E — **Vision consolidée** : LTI-first (ADR-001), BYOK OpenRouter 4-tiers (ADR-002), TTFR KPI central (ADR-003), Classroom Composer UI (ADR-004), AI Tutor V1 décisions gelées (ADR-005 — stockage, rate-limit, guardrails), Solo-sustainable practices (ADR-006), tuteur IA socratique dès A1, i18n FR/NL/EN — Architecture stratégique précédente (THI-35) : Terminal Sentinel (Phase 5.5) ✅, RBAC complet (Phase 7) ✅, Admin Panel (Phase 9), PWA avancée (Phase finale) — **Epic Web 2026 Compliance** (THI-96) : 6/8 sub-issues livrées (THI-97 → THI-102), reste Desktop a11y + CSS moderne 2026 — **Phase 7b (AI Tutor V1) ✅ COMPLETE + V1.5 SÉQUENCÉE** : THI-115 ✅, THI-109 ✅ (gate-zero guardrail), THI-110 ✅ (keyManager AES-GCM), THI-120 ✅ (Sentry scrubber), **THI-111 ✅ COEUR FONCTIONNEL** (PR #188 — sanitizer + 4 providers + panel + 287 AI tests, audits guardrail 9.4/10 + security 8.8/10 + ui A11y exemplary), **THI-147 ✅ FIX SAFE-AREA iPhone PWA** (PR #189), **panel actif en Production** (`VITE_AI_TUTOR_ENABLED=true` + `VITE_AI_TUTOR_OPENROUTER_MODEL=anthropic/claude-haiku-4-5` activés Production+Preview par @cowork) — **Sprint sécurité 1-2 mai 2026 ✅ CLOS** : audit security-auditor 8.1/10 → ~8.6/10 post-sprint, 11 PRs livrées (#168 à #178), 5 HIGH/MEDIUM Done (THI-133/134/135/137/140), 4 MEDIUM ciblés en backlog (THI-136/138/139/112), agent `route-attack-auditor` créé — **Session 4 mai 2026** : THI-111 livré + THI-147 livré + 5 tickets V1.5 backlog créés (THI-142 lessonContext renforcé HIGH, THI-143 frustration heuristic + détection sémantique user MEDIUM, THI-144 system prompt v1.1.0 + ADR-008 + eval suite MEDIUM, THI-145 chat assistant role-based Phase 9+ LOW, THI-146 modèle défaut Haiku HIGH déjà actif via env var, THI-148 extend tutor scope platform meta-questions V1.0.1 P1 1h30 estimé) — **Posture validée** : pas de rush deadline, qualité/scalabilité/perf non négociables, plan respecté en ordre, **mea culpa explicite** à chaque round trio @thierry / @cc-terminallearning / @cowork (estimation 30 min → 1h30, privacy `userProgress` retirée V1.0.1, hypothèse `transform` mobile réfutée par diagnostic Chrome DevTools MCP) — **Verdict empirique Haiku 4.5 (capturé 4 mai 21h par @cowork via Chrome MCP)** : 5 tests qualitatifs, score moyen **9.3/10** — Test 1 méta-plateforme 8/10, Test 2 fichiers cachés 9/10, Test 3 hallucination 9.5/10, Test 4 frustration 10/10 (Haiku bascule mode direct AUTONOMEMENT), Test 5 jailbreak 10/10. **Reprio backlog tranchée @cowork** : THI-146 ✅ SUCCÈS validé, **THI-142 → Low** (Haiku gère déjà bien le contexte leçon), **THI-143 → Low** (Haiku résout naturellement la frustration via compréhension contextuelle), **THI-148 P1 INCHANGÉ** (scope ≠ modèle prouvé Test 1, GO IMMÉDIAT), THI-144 P2 Medium (peut englober THI-148 + eval suite), THI-145 P3 Low (Phase 9+). **ROI méthode scientifique** : ~4-6h économisées (THI-142/143 reportés V2). **Next ordonné — décision @cowork 5 mai matin** : (1) **THI-150 EN COURS** (12ᵉ agent `mobile-responsive-auditor`, ex-brick 3a de THI-149 epic Done, 11 sections / ≥48 checkpoints + bonus Section 11 Desktop Preservation + checkpoints BUG-FAB-001 visibility/contrast/detachment), (2) THI-151 audit Playwright WebKit + matrice bugs (ex-brick 3b), (3) THI-152 mini-PRs fix séquentielles (ex-brick 3c, critère ABSOLU **ne pas casser desktop**), (4) THI-148 extend tutor scope méta-plateforme V1.0.1 (1h30 honnête, scope statique platformContext, bump v1.0.0→v1.0.1, audit guardrail Règle 10 obligatoire), (5) **THI-144 enrichi P1** system prompt v1.1.0 + ADR-008 + eval suite (intègre 5 micro-frictions identifiées par cross-validation ChatGPT sur session 8 tours @thierry — compound questions, sur-explication internal mechanics, indices répétés, platformContext absent confirme THI-148, conclusion ouverte), (6) THI-112 onboarding AiKeySetup + picker modèle curated, (7) THI-114 Web Worker isolation, (8) THI-145 chat role-based Phase 9+. **THI-142/143 reportés V2** (Haiku 9.3/10 résout 80% naturellement). --- diff --git a/src/app/components/Landing.tsx b/src/app/components/Landing.tsx index 16635e8..4ac203f 100644 --- a/src/app/components/Landing.tsx +++ b/src/app/components/Landing.tsx @@ -86,7 +86,7 @@ export function Landing() { const handleShare = async () => { const url = 'https://terminallearning.dev'; - const text = 'Apprends le terminal gratuitement — 11 modules interactifs, Linux / macOS / Windows.'; + const text = `Apprends le terminal gratuitement — ${MODULE_PREVIEWS.length} modules interactifs, Linux / macOS / Windows.`; if (navigator.share) { try { await navigator.share({ title: 'Terminal Learning', text, url }); @@ -396,7 +396,7 @@ export function Landing() { {MODULE_PREVIEWS.length} modules progressifs

- Du système de fichiers à la redirection de flux — deux niveaux, sans prérequis pour commencer. + Du système de fichiers à Git et à l'IA — cinq niveaux, sans prérequis pour commencer.

@@ -436,7 +436,7 @@ export function Landing() {

- 11 modules inclus — aucun compte requis. + {MODULE_PREVIEWS.length} modules inclus — aucun compte requis.

diff --git a/src/app/data/landingContent.ts b/src/app/data/landingContent.ts index a7866de..71f9633 100644 --- a/src/app/data/landingContent.ts +++ b/src/app/data/landingContent.ts @@ -80,7 +80,7 @@ export const ROADMAP_AVAILABLE: readonly RoadmapGroup[] = [ items: [ '11 modules progressifs (navigation → IA pour dev)', 'Terminal interactif : pipes, redirections, scripts', - `Référence enrichie (${TOTAL_COMMANDS}+ commandes)`, + `Référence enrichie (${TOTAL_COMMANDS}+ commandes), exemples expliqués à essayer dans le terminal`, 'Parcours guidé par niveaux + Dashboard de progression', ], }, @@ -90,6 +90,7 @@ export const ROADMAP_AVAILABLE: readonly RoadmapGroup[] = [ 'Linux / macOS / Windows — sélection + adaptation par OS', 'Chemins Windows natifs dans PowerShell (\\, C:\\Users)', 'Partage natif (Web Share API)', + 'Guide d\'installation de l\'app (iOS / Android / ordinateur)', ], }, { @@ -116,7 +117,8 @@ export const ROADMAP_AVAILABLE: readonly RoadmapGroup[] = [ 'Accessibilité WCAG 2.2 (cibles tactiles 44 px, focus visible, safe-area iOS)', 'Sécurité durcie (CSP SHA-256, rate limiting, endpoints gated)', 'Âge minimum vérifié à l\'inscription (RGPD mineurs)', - 'Chaque changement vérifié : 2 700+ tests + 21 agents d\'audit', + 'Git réaliste : vraies branches, fusions et conflits', + 'Chaque changement vérifié : 2 900+ tests + 21 agents d\'audit', ], }, ]; @@ -128,14 +130,14 @@ export const ROADMAP_IN_PROGRESS: readonly RoadmapGroup[] = [ 'Terminal fidèle à un vrai shell (bash et PowerShell)', 'Chaque exemple de leçon identique à ce que le terminal affiche', 'Messages d\'erreur PowerShell réalistes', + 'Chaque commande des leçons exécutable dans le terminal (find, sort, diff, tar…)', ], }, { group: 'Curriculum', items: [ - 'Page Référence plus pédagogique pour les débutants', + 'Exercices en plusieurs étapes, validés sur l\'état du terminal', 'Une variante PowerShell pour chaque leçon', - 'Historique Git réaliste dans les leçons avancées', ], }, ]; @@ -154,7 +156,6 @@ export const ROADMAP_PLANNED: readonly RoadmapGroup[] = [ 'Mode histoire narratif', 'Badges & Open Badges 3.0 (CEFR + EQF)', 'Révisions intelligentes', - 'Guide d\'installation PWA (iOS / Android / Desktop)', ], }, { @@ -183,7 +184,7 @@ export const TRUST_BADGES = [ { icon: Github, label: '100% Open Source', href: 'https://github.com/thierryvm/TerminalLearning' }, { icon: Infinity, label: 'Free Forever', href: undefined }, { icon: Lock, label: 'GDPR Compliant', href: undefined }, - { icon: CheckCircle2, label: '2 700+ tests · CI verte', href: 'https://github.com/thierryvm/TerminalLearning/actions' }, + { icon: CheckCircle2, label: '2 900+ tests · CI verte', href: 'https://github.com/thierryvm/TerminalLearning/actions' }, ] as const; // ── Module icons map ────────────────────────────────────────────────────────── @@ -265,21 +266,21 @@ export const ENV_LEVELS: Record', '>>', 'wc -l'], + commands: ['chmod', 'chown', 'ps aux', 'grep', 'grep -r', 'top', 'kill', 'killall', '|', '>', '>>', 'wc -l'], color: 'text-blue-400', bg: 'bg-blue-500/5', border: 'border-blue-500/20', }, { level: 3, label: 'Automatisation & réseau', - description: 'Tu scripts, te connectes à distance et déploies — workflows de professionnel Linux.', - commands: ['ssh', 'scp', 'curl', 'wget', 'git', 'bash script.sh', 'export', 'env', 'tar -czf', 'cron', 'systemctl'], + description: 'Tu écris des scripts, te connectes à distance et versionnes ton code avec Git.', + commands: ['ssh', 'scp', 'curl', 'wget', 'ping', 'git', 'sudo', 'export', 'env', 'source', 'crontab -l', 'tee'], color: 'text-purple-400', bg: 'bg-purple-500/5', border: 'border-purple-500/20', }, ], @@ -293,16 +294,16 @@ export const ENV_LEVELS: Record', 'open -a', 'pbcopy', 'pbpaste', 'defaults read'], + label: 'Système & recherche', + description: 'Tu contrôles les processus, cherches dans les fichiers et chaînes les commandes.', + commands: ['chmod', 'chown', 'ps aux', 'top', 'kill', 'grep', 'grep -r', 'head', 'tail', 'wc -l', '|', '>'], color: 'text-violet-400', bg: 'bg-violet-500/5', border: 'border-violet-500/20', }, { level: 3, - label: 'Homebrew & automatisation', - description: 'Tu gères les packages, configures le système et automatises tes workflows macOS.', - commands: ['brew install', 'brew update', 'brew list', 'ssh', 'git', 'launchctl', 'caffeinate', 'defaults write', 'xcode-select', 'curl', 'xargs'], + label: 'Réseau & automatisation', + description: 'Tu te connectes à distance, télécharges des fichiers et versionnes ton code avec Git.', + commands: ['ssh', 'scp', 'curl', 'wget', 'ping', 'git', 'export', 'env', 'source', 'tee'], color: 'text-purple-400', bg: 'bg-purple-500/5', border: 'border-purple-500/20', }, ], @@ -311,21 +312,21 @@ export const ENV_LEVELS: Record { }); }); }); + +// The hero lists commands per environment and level. Until 29 September 2026 it +// promised 22 commands the terminal did not know (systemctl, launchctl, +// Get-Service, New-PSDrive…) and 4 no lesson taught: a visitor who typed one got "commande +// introuvable". Every command shown must run in that environment and be taught. +describe('landingContent — the commands the hero promises exist', () => { + /** What a learner of `env` reads in code blocks and exercises: the commands a lesson teaches. */ + const taughtIn = (env: SelectedEnvironment) => curriculum.flatMap((m) => m.lessons.flatMap((l) => [ + ...l.blocks.filter((b) => b.type === 'code').map((b) => b.contentByEnv?.[env] ?? b.content), + ...(l.exercise ? [l.exercise.instructionByEnv?.[env] ?? l.exercise.instruction, l.exercise.hintByEnv?.[env] ?? l.exercise.hint] : []), + ])).join('\n'); + /** The whole command as a word of its own: `top` does not count inside `stop`, nor `env` inside `environment`. */ + const asWord = (command: string) => new RegExp(`(^|[^\\w-])${command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^\\w-])`, 'm'); + for (const [env, levels] of Object.entries(ENV_LEVELS) as Array<[SelectedEnvironment, (typeof ENV_LEVELS)[SelectedEnvironment]]>) { + for (const level of levels) { + it(`${env} level ${level.level}: every command runs and is taught`, () => { + const taught = taughtIn(env); + for (const command of level.commands.filter((c) => !['|', '>', '>>'].includes(c))) { + const out = processCommand(createInitialState(), command, env).lines.map((l) => l.text).join('\n'); + expect(out, `${env}: ${command}`).not.toMatch(/commande introuvable|n'est pas reconnu|n'est pas simulée?/); + expect(asWord(command).test(taught), `${env}: ${command} is in no lesson`).toBe(true); + } + }); + } + } + + it('the module section names as many levels as the modules use', () => { + const landing = readFileSync(resolve(process.cwd(), 'src/app/components/Landing.tsx'), 'utf-8'); + const words = ['zéro', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept']; + const said = landing.match(/— (\p{L}+) niveaux/u); + expect(said, 'the "N niveaux" subtitle was not found in Landing.tsx').not.toBeNull(); + expect(words.indexOf(said![1])).toBe(Math.max(...MODULE_PREVIEWS.map((m) => m.level ?? 1))); + }); + + it('the roadmap counts the modules there are', () => { + expect(ROADMAP_AVAILABLE.flatMap((g) => g.items).some((item) => item.startsWith(`${MODULE_PREVIEWS.length} modules`))).toBe(true); + }); +}); From 5202c9ab32c95ad302e0b35c7ea9ec6b443daea8 Mon Sep 17 00:00:00 2001 From: "Thierry V." <46031203+thierryvm@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:09:10 +0200 Subject: [PATCH 2/2] fix(landing): the terminal preview shows what real shells print ls sorts its names and adds no slash; PowerShell answers Get-Location with its Path table and Test-Path with True, instead of bash-style listings. The prompt follows cd (~/projects, C:\Users\user\projects), blank output lines keep their height, ls keeps its double spaces, and long commands wrap at the edge like a terminal, not after a hyphen. Outputs captured from GNU bash and PowerShell 7. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../components/landing/TerminalPreview.tsx | 87 +++++++++++++------ src/test/terminalPreview.test.tsx | 25 ++++++ 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61df963..4b86263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ *Page d'accueil · feuille de route · un test empêche la dérive* - **Des commandes qui marchent vraiment.** Sous « Choisissez votre environnement », la page listait par niveau des commandes que le terminal ne connaissait pas : `systemctl`, `launchctl`, `caffeinate`, `Get-Service`, `New-PSDrive` et dix-sept autres. Un visiteur qui en tapait une recevait « commande introuvable ». Chaque commande affichée est maintenant enseignée dans une leçon et fonctionne dans le terminal, et un test le vérifie à chaque changement. +- **Un aperçu de terminal fidèle.** L'animation de la page d'accueil montrait sous Windows des réponses de bash, et sous Linux un `ls` qui ajoutait des `/` et ne triait pas. Elle reprend maintenant ce qu'affichent un vrai bash, zsh et PowerShell 7 (le tableau `Path` de `Get-Location`, `Test-Path` qui répond `True`), et l'invite suit le dossier courant après `cd projects`. - **Le bon résumé des modules.** « Du système de fichiers à la redirection de flux — deux niveaux » datait des débuts : le parcours va aujourd'hui jusqu'à Git et à l'IA, sur cinq niveaux. - **Une feuille de route à jour.** Les vraies branches Git, le guide d'installation de l'app et les exemples expliqués de la Référence passent dans « Disponible ». « En cours » annonce ce qui reste vraiment à faire : les exercices en plusieurs étapes, et les commandes enseignées que le terminal ne simule pas encore (`find`, `sort`, `diff`, `tar`…). diff --git a/src/app/components/landing/TerminalPreview.tsx b/src/app/components/landing/TerminalPreview.tsx index b8e9207..c339924 100644 --- a/src/app/components/landing/TerminalPreview.tsx +++ b/src/app/components/landing/TerminalPreview.tsx @@ -5,31 +5,42 @@ import type { SelectedEnvironment } from '../../context/EnvironmentContext'; interface TerminalLine { type: 'prompt' | 'output'; text: string; + /** Folder the command was typed in, below the home folder ('' = home): the prompt shows it. */ + dir?: string; +} + +/** A step of the demo; `into` is the folder a `cd` step enters, so the next prompts show it. */ +interface Step { + command: string; + output: string[]; + into?: string; } // ─── Per-environment sequences ──────────────────────────────────────────────── +// What a real shell prints (GNU bash, macOS zsh, PowerShell 7): `ls` sorts its +// names and adds no `/` to folders; PowerShell answers Get-Location with a table. -const SEQUENCES: Record> = { +const SEQUENCES: Record = { linux: [ { command: 'pwd', output: ['/home/user'] }, - { command: 'ls', output: ['documents/ downloads/ projects/ notes.txt'] }, - { command: 'cd projects', output: [] }, + { command: 'ls', output: ['documents downloads notes.txt projects'] }, + { command: 'cd projects', output: [], into: 'projects' }, { command: 'mkdir my-app', output: [] }, - { command: 'ls', output: ['my-app/'] }, + { command: 'ls', output: ['my-app'] }, ], macos: [ { command: 'pwd', output: ['/Users/user'] }, - { command: 'ls', output: ['Desktop Documents Downloads projects notes.txt'] }, - { command: 'cd projects', output: [] }, + { command: 'ls', output: ['Desktop Documents Downloads notes.txt projects'] }, + { command: 'cd projects', output: [], into: 'projects' }, { command: 'mkdir my-app', output: [] }, - { command: 'ls', output: ['my-app/'] }, + { command: 'ls', output: ['my-app'] }, ], windows: [ - { command: 'Get-Location', output: ['Path', '----', 'C:\\Users\\user'] }, - { command: 'Get-ChildItem', output: ['Documents Downloads projects notes.txt'] }, - { command: 'Set-Location projects', output: [] }, - { command: 'New-Item -Type Directory my-app', output: [' Directory: C:\\Users\\user\\projects', '', 'my-app'] }, - { command: 'Get-ChildItem', output: ['my-app/'] }, + { command: 'Get-Location', output: ['', 'Path', '----', 'C:\\Users\\user', ''] }, + { command: 'Set-Location projects', output: [], into: 'projects' }, + { command: 'New-Item -ItemType Directory my-app | Out-Null', output: [] }, + { command: 'Test-Path my-app', output: ['True'] }, + { command: 'Get-ChildItem -Name', output: ['my-app'] }, ], }; @@ -62,22 +73,40 @@ function useReducedMotion(): boolean { return reduced; } +/** The folder each step is typed in: home, then wherever the last `cd` went. */ +function stepDirs(env: SelectedEnvironment): string[] { + let dir = ''; + return SEQUENCES[env].map((step) => { + const here = dir; + if (step.into !== undefined) dir = step.into; + return here; + }); +} + function buildStaticLines(env: SelectedEnvironment): TerminalLine[] { - return SEQUENCES[env].flatMap((step) => [ - { type: 'prompt' as const, text: step.command }, + const dirs = stepDirs(env); + return SEQUENCES[env].flatMap((step, i) => [ + { type: 'prompt' as const, text: step.command, dir: dirs[i] }, ...step.output.map((text) => ({ type: 'output', text })), ]); } +/** Where the demo ends: the folder of the prompt left waiting after the last step. */ +function finalDir(env: SelectedEnvironment): string { + const last = SEQUENCES[env].filter((step) => step.into !== undefined).pop(); + return last?.into ?? ''; +} + // ─── Env-aware prompt renderer ──────────────────────────────────────────────── -function PromptSpan({ env }: { env: SelectedEnvironment }) { +// The prompts of the practice terminal (TerminalEmulator.tsx getEnvPrompt), which show the current folder. +function PromptSpan({ env, dir = '' }: { env: SelectedEnvironment; dir?: string }) { if (env === 'linux') { return ( <> user@terminal : - ~ + {dir ? `~/${dir}` : '~'} $ ); @@ -86,8 +115,7 @@ function PromptSpan({ env }: { env: SelectedEnvironment }) { return ( <> ➜ - ~ - % + {` ${dir ? `~/${dir}` : '~'} `} ); } @@ -95,7 +123,7 @@ function PromptSpan({ env }: { env: SelectedEnvironment }) { return ( <> PS - C:\Users\user + {dir ? `C:\\Users\\user\\${dir}` : 'C:\\Users\\user'} > ); @@ -107,6 +135,7 @@ export function TerminalPreview() { const [animatedLines, setAnimatedLines] = useState([]); const [typingText, setTypingText] = useState(''); + const [typingDir, setTypingDir] = useState(''); const [showCursor, setShowCursor] = useState(true); const containerRef = useRef(null); const timeoutRef = useRef | null>(null); @@ -145,12 +174,15 @@ export function TerminalPreview() { async function runAnimation() { const sequence = SEQUENCES[selectedEnv]; + const dirs = stepDirs(selectedEnv); while (!cancelledRef.current) { setAnimatedLines([]); setTypingText(''); + setTypingDir(''); - for (const step of sequence) { + for (const [index, step] of sequence.entries()) { if (cancelledRef.current) return; + const dir = dirs[index]; for (let i = 0; i <= step.command.length; i++) { if (cancelledRef.current) return; @@ -160,7 +192,8 @@ export function TerminalPreview() { if (cancelledRef.current) return; setTypingText(''); - setAnimatedLines((prev) => [...prev, { type: 'prompt', text: step.command }]); + setAnimatedLines((prev) => [...prev, { type: 'prompt', text: step.command, dir }]); + if (step.into !== undefined) setTypingDir(step.into); await new Promise((r) => { timeoutRef.current = setTimeout(r, 120); }); @@ -213,19 +246,21 @@ export function TerminalPreview() { {lines.map((line, i) => (
{line.type === 'prompt' ? ( -
- + // A terminal wraps a long command at the screen's edge, not after a hyphen. +
+ {line.text}
) : ( -
{line.text}
+ // A blank line of output keeps its height (PowerShell frames its tables with them). +
{line.text || '\u00a0'}
)}
))} {/* Active typing line */} -
- +
+ {typingText} { localStorage.removeItem('tl-environment'); }); }); + +// ── What a real shell shows ────────────────────────────────────────────────── + +describe('TerminalPreview — prompts and blank lines as in a real shell', () => { + afterEach(() => localStorage.removeItem('tl-environment')); + + it('the prompt follows cd: home first, then the projects folder', () => { + localStorage.removeItem('tl-environment'); + const { container } = renderPreview(true); + const prompts = [...container.querySelectorAll('.text-blue-400')].map((el) => el.textContent); + expect(prompts[0]).toBe('~'); + expect(prompts[prompts.length - 1]).toBe('~/projects'); + }); + + it('PowerShell: the prompt shows the folder, and the blank lines around Get-Location stay visible', () => { + localStorage.setItem('tl-environment', 'windows'); + const { container, getByText } = renderPreview(true); + const content = container.querySelector('.text-left')!; + expect(content.textContent).toContain('PS C:\\Users\\user\\projects> '); + // PowerShell frames the Path table with blank lines: they render as a non-breaking space, not as nothing. + const blanks = [...content.querySelectorAll('.whitespace-pre-wrap')].filter((el) => el.textContent === '\u00a0'); + expect(blanks.length).toBe(2); + expect(getByText('Path')).toBeTruthy(); + }); +});