Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@

---

## 🌿 30 septembre 2026 — Git se comporte comme le vrai Git

*Modules Git et GitHub · valeurs attendues relevées sur Git 2.56 · écarts de la théorie : 158 → 148*

Le Git du simulateur tenait une simple liste de noms de fichiers. `git add` acceptait un fichier qui n'existait pas et répondait `staged:`, ce que le vrai Git n'écrit jamais. `git status` ignorait les fichiers modifiés, et `git diff` affichait toujours le même exemple inventé. Git lit maintenant le contenu réel des fichiers, et chaque sortie a été comparée à celle de Git 2.56 sur une copie du projet des leçons.

- **`git status` dit la vérité.** Fichiers non suivis, modifiés, supprimés ou prêts à être commités, avec les mêmes phrases et le même ordre que Git. Depuis un sous-dossier, les chemins s'écrivent `../README.md`, comme dans Git. `git status -s` donne la version courte (`M`, `A`, `??`).
- **`git add` est silencieux quand tout va bien**, et refuse un fichier inexistant avec l'erreur de Git. Il respecte aussi le `.gitignore` : `git add .env` est refusé, sauf avec `--force`.
- **Le premier commit ressemble au vrai** : `[main (root-commit) …]`, le nombre de lignes ajoutées et supprimées, et une ligne `create mode` par nouveau fichier. `git commit -a`, `git restore`, `git rm`, `git reset` et `git stash` agissent sur les vrais fichiers.
- **`git diff` montre la vraie différence**, ligne par ligne, avec les identifiants de fichiers que calcule Git. Les lignes retirées s'affichent en rouge et les lignes ajoutées en vert.
- **Des leçons plus justes.** Dans les leçons, `~/projets` est un vrai dépôt, avec son dossier `.git/` et un `.gitignore` qui protège déjà le fichier `.env`. La leçon « git diff » part d'une modification à comparer. Les exemples de théorie qui ajoutaient des fichiers inexistants, ou qui enchaînaient `git remote` sans entrer dans le dossier cloné, sont corrigés, dans les leçons comme dans la page Référence.
- **Changer d'environnement redémarre le terminal de la leçon**, pour qu'une session Linux ne devienne pas une session PowerShell en cours de route.

---

## 📱 29 septembre 2026 — Un terminal lisible sur téléphone comme sur ordinateur

*Terminal des leçons · mobile 390 px, tablette 1024 px, ordinateur 1280 px*
Expand Down
10 changes: 10 additions & 0 deletions STORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ Ce projet a été construit avec l'aide de Claude — l'IA d'Anthropic, des mod

---

## Un Git qui ne ment plus (30 septembre 2026)

Dans le simulateur, `git add fichier-qui-n-existe-pas.txt` répondait poliment `staged:`. Le vrai Git refuse : `fatal: pathspec … did not match any files`. Ce n'était pas un détail. Nos propres tests ajoutaient des fichiers imaginaires, et la leçon sur `git add .` avertissait que la commande embarque le fichier `.env`, alors que notre Git cachait justement les fichiers commençant par un point. La leçon et le moteur se contredisaient.

J'ai reconstruit le modèle autour de ce que Git garde vraiment : le contenu du dernier commit, la zone de préparation et les fichiers du dossier. Pour chaque sortie, la valeur attendue vient d'un vrai Git lancé sur une copie du projet des leçons, jusqu'aux identifiants `index 7d825c5..6827029` d'un diff. Deux conventions ont demandé une décision. Le simulateur stocke les fichiers sans leur dernier saut de ligne, alors Git les relit avec. Le dépôt préparé contient maintenant un `.gitignore` qui protège `.env`, parce qu'un exercice ne doit jamais apprendre à commiter un secret.

Le rejeu de la théorie a trouvé ce que personne n'avait vu. Une leçon clonait trois fois le même dépôt dans le même dossier, puis lançait `git remote` sans entrer dans le dossier cloné. Un vrai terminal aurait refusé les deux. Les exemples tapent maintenant `cd projet`, comme on le ferait vraiment.

---

## Regarder l'écran, pas seulement les tests (29 septembre 2026)

Tous les tests étaient verts quand Thierry m'a demandé de vérifier moi-même, dans un navigateur, sur téléphone et sur ordinateur, que chaque correction se voyait vraiment. J'ai tapé `Get-ScheduledTask` dans le terminal d'une leçon, en largeur de téléphone. Le tableau s'est effondré : trois colonnes coupées en morceaux, une ligne de tirets orpheline. Le moteur répondait exactement comme PowerShell ; c'est l'affichage qui trahissait la réponse. Aucun test ne pouvait le voir, puisque les tests lisent le texte, pas l'écran.
Expand Down
10 changes: 6 additions & 4 deletions src/app/components/LessonPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,10 @@ function LessonContent({ mod, lesson, moduleId, lessonId }: {
? [`📚 ${lesson.title}`, ``, ...setupNote, `✓ Exercice déjà complété — « Suivant » pour continuer, ou pratique librement ci-dessous.`, ``]
: [`📚 ${lesson.title}`, ``, ...setupNote, `Exercice : ${stripInlineMarkdown(effectiveInstruction)}`, ``]
: [`📚 ${lesson.title}`, ``, `Terminal libre — pratiquez les commandes ci-dessous.`, ``];
// Read once per terminal mount (lesson change or « Réinitialiser » remount).
// Read once per terminal mount (lesson or environment change, « Réinitialiser »).
const buildInitialState = useCallback(
() => (setup ? setup.apply(createInitialState()) : createInitialState()),
[setup],
() => (setup ? setup.apply(createInitialState(), selectedEnv) : createInitialState()),
[setup, selectedEnv],
);

return (
Expand Down Expand Up @@ -385,7 +385,9 @@ function LessonContent({ mod, lesson, moduleId, lessonId }: {
</Button>
</div>
<TerminalEmulator
key={terminalKey}
// A new environment is a new session: its prepared state (a Git repository
// with Linux or Windows file modes) is only read when the terminal mounts.
key={`${terminalKey}-${selectedEnv}`}
onCommand={handleCommand}
welcomeMessage={welcomeMessage}
className="flex-1 min-h-0"
Expand Down
8 changes: 5 additions & 3 deletions src/app/components/TerminalEmulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ function getEnvPrompt(state: TerminalState, env: SelectedEnvironment): string {

interface TerminalLine {
id: number;
type: 'prompt' | 'output' | 'error' | 'success' | 'info';
/** `removed`: standard output shown in red, as git shows deleted lines and unstaged changes. */
type: 'prompt' | 'output' | 'error' | 'success' | 'info' | 'removed';
text: string;
prompt?: string;
}
Expand Down Expand Up @@ -125,6 +126,7 @@ const LINE_COLOR: Record<TerminalLine['type'], string> = {
error: 'text-[var(--github-red)]',
success: 'text-[#3fb950]',
info: 'text-[#58a6ff]',
removed: 'text-[var(--github-red)]',
};

let lineCounter = 0;
Expand Down Expand Up @@ -227,7 +229,7 @@ export function TerminalEmulator({ onCommand, welcomeMessage, className = '', us
// A script can clear the screen and then print: keep what came after.
setLines(result.lines.map((l: OutputLine) => ({
id: nextId(),
type: l.type === 'error' ? 'error' as const : l.type === 'success' ? 'success' as const : 'output' as const,
type: l.type === 'error' || l.type === 'success' || l.type === 'removed' ? l.type : 'output' as const,
text: l.text,
})));
setTermState(result.newState);
Expand All @@ -240,7 +242,7 @@ export function TerminalEmulator({ onCommand, welcomeMessage, className = '', us
{ id: nextId(), type: 'prompt', text: trimmed, prompt },
...result.lines.map((l: OutputLine) => ({
id: nextId(),
type: l.type === 'error' ? 'error' as const : l.type === 'success' ? 'success' as const : 'output' as const,
type: l.type === 'error' || l.type === 'success' || l.type === 'removed' ? l.type : 'output' as const,
text: l.text,
})),
];
Expand Down
2 changes: 1 addition & 1 deletion src/app/data/commandExamples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ export const COMMAND_EXAMPLES: Record<string, CommandExample[]> = {
],
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.' },
{ command: 'git clone git@github.com:alice/site.git', explanation: 'Même chose par SSH, ici pour le dépôt site : 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.' },
Expand Down
Loading
Loading