diff --git a/.claude/hooks/verify-gate.mjs b/.claude/hooks/verify-gate.mjs new file mode 100644 index 0000000..ba8a4b4 --- /dev/null +++ b/.claude/hooks/verify-gate.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node +/** + * Hook Stop — CraftCode : porte de vérification. + * + * À chaque fin de tour, vérifie qu'aucune édition de code (frontend/ ou backend/) + * ne reste sans `npm run check` postérieur. Si c'est le cas, BLOQUE la clôture et + * renvoie à Claude la consigne de lancer le check du bon dossier. But : faire + * respecter par l'outil — et non par la vigilance de l'assistant — le principe + * « ne jamais affirmer 'c'est fait' sans avoir vérifié ». Cf. DECISIONS.md. + * + * Pendant Stop du hook PostToolUse summarize-change.mjs : l'un montre chaque + * changement, l'autre garantit qu'il a été vérifié. + * + * Signal objectif (pas d'analyse du langage naturel) : on repère dans le + * transcript la dernière exécution de `npm run check`, puis les dossiers de code + * édités APRÈS elle. S'il en reste, on bloque. + * + * Garde anti-boucle : si `stop_hook_active` est vrai (Claude poursuit déjà à + * cause d'un blocage de ce hook), on laisse passer. + * + * Ne lève jamais d'exception ni de code d'erreur : un hook qui plante ne doit + * jamais interrompre le travail (toute erreur ⇒ exit 0, la clôture passe). + */ +import { readFileSync } from 'node:fs'; + +const ROOT = '/Users/julien/Dev/CraftCode'; + +/** Dossiers de code soumis à la porte, avec la commande de vérification associée. */ +const GATED = [ + { dir: 'frontend', command: 'cd frontend && npm run check' }, + { dir: 'backend', command: 'cd backend && npm run check' }, +]; + +const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']); + +function readStdin() { + try { + return readFileSync(0, 'utf8'); + } catch { + return ''; + } +} + +/** Dossier gardé (frontend|backend) d'un chemin de fichier, ou null. */ +function gatedDirOf(filePath) { + if (typeof filePath !== 'string') return null; + // Normalise vers un chemin relatif à la racine du projet. + const rel = filePath.startsWith(ROOT + '/') ? filePath.slice(ROOT.length + 1) : filePath; + const match = GATED.find((g) => rel === g.dir || rel.startsWith(g.dir + '/')); + return match ? match.dir : null; +} + +/** Une commande Bash est-elle un `npm run check` ? */ +function isCheckCommand(command) { + return typeof command === 'string' && /npm run check/.test(command); +} + +/** + * Parcourt le transcript (JSONL) dans l'ordre et renvoie l'ensemble des dossiers + * de code édités APRÈS le dernier `npm run check` (ou depuis le début si aucun). + */ +function dirsEditedSinceLastCheck(transcriptPath) { + const raw = readFileSync(transcriptPath, 'utf8'); + const pending = new Set(); + + for (const line of raw.split('\n')) { + if (!line.trim()) continue; + let entry; + try { + entry = JSON.parse(line); + } catch { + continue; // ligne illisible : on ignore, on ne bloque pas pour autant + } + + const content = entry?.message?.content; + if (!Array.isArray(content)) continue; + + for (const block of content) { + if (block?.type !== 'tool_use') continue; + + if (EDIT_TOOLS.has(block.name)) { + const dir = gatedDirOf(block?.input?.file_path); + if (dir) pending.add(dir); + } else if (block.name === 'Bash' && isCheckCommand(block?.input?.command)) { + // Un check est propre à un dossier (`cd frontend && npm run check`) : il ne + // dédouane que le(s) dossier(s) qu'il cible. S'il n'en cible aucun + // explicitement, on considère qu'il couvre tout (best-effort, évite de + // bloquer à tort). + const command = block.input.command; + const targeted = GATED.filter((g) => command.includes(g.dir)); + if (targeted.length === 0) pending.clear(); + else targeted.forEach((g) => pending.delete(g.dir)); + } + } + } + + return pending; +} + +function main() { + let payload; + try { + payload = JSON.parse(readStdin()); + } catch { + process.exit(0); + } + + // Anti-boucle : ne pas re-bloquer une clôture déjà relancée par ce hook. + if (payload?.stop_hook_active) process.exit(0); + + const transcriptPath = payload?.transcript_path; + if (!transcriptPath) process.exit(0); + + let pending; + try { + pending = dirsEditedSinceLastCheck(transcriptPath); + } catch { + process.exit(0); // transcript illisible : on laisse passer + } + + if (pending.size === 0) process.exit(0); + + const commands = GATED.filter((g) => pending.has(g.dir)) + .map((g) => ` ${g.command}`) + .join('\n'); + const zones = [...pending].join(' et '); + + const reason = + `Porte de vérification : des fichiers de ${zones} ont été modifiés sans ` + + `\`npm run check\` depuis. Avant de clore, lance la vérification du ou des ` + + `dossiers touchés et colle sa sortie :\n${commands}\n` + + `Si un test échoue, dis-le plutôt que d'affirmer que c'est fait.`; + + process.stdout.write(JSON.stringify({ decision: 'block', reason })); + process.exit(0); +} + +main(); diff --git a/.claude/settings.json b/.claude/settings.json index 6191a88..a043193 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -38,6 +38,16 @@ } ] } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/verify-gate.mjs\"" + } + ] + } ] } } diff --git a/DECISIONS.md b/DECISIONS.md index 332acbd..7ba046f 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -30,6 +30,45 @@ Entrées **antéchronologiques** (la plus récente en haut). La date au format ` --- +## 2026-06-17 — Porte de vérification (hook Stop) avant clôture + +- **Contexte** : le principe « ne jamais affirmer 'c'est fait' sans avoir lancé + `npm run check` » reposait uniquement sur la vigilance de l'assistant. Le hook + `summarize-change.mjs` rend les changements visibles, mais rien ne garantit + qu'ils ont été vérifiés. +- **Décision** : un hook `Stop` `verify-gate.mjs` qui, en fin de tour, repère + dans le transcript les éditions de code (`frontend/`/`backend/`) postérieures au + dernier `npm run check` et **bloque la clôture** (`decision: "block"`) tant + qu'il en reste, en renvoyant la commande exacte à lancer. Garde anti-boucle via + `stop_hook_active` ; toute erreur du hook ⇒ `exit 0` (ne bloque jamais le + travail). Version générique ajoutée au `claude-code-starter-kit/`. +- **Options écartées** : un simple rappel non bloquant (rejeté — repose encore sur + la vigilance, ce que l'idée vise à supprimer) ; détecter l'affirmation « c'est + fait » en langage naturel (rejeté — non fiable, on gate sur un signal objectif : + édition de code sans check postérieur). +- **Pourquoi** : faire respecter par l'outil la porte qualité (= la CI), pendant + `Stop` du résumé `PostToolUse` — l'un montre, l'autre vérifie. +- **Trace** : branche `main` (session du 2026-06-17). + +## 2026-06-17 — Outil « Mettre en place Claude Code » + kit de démarrage + +- **Contexte** : besoin de documenter, dans l'app elle-même, comment installer + Claude Code dans un projet (fichiers markdown, hooks, slash-commands & skills, + settings.json & MCP), et de fournir un point de départ réutilisable hors de + CraftCode. +- **Décision** : (1) un nouvel outil pédagogique `claude-code-setup` (hub + + détail, 4 sujets) suivant ADD-A-TOOL.md à la lettre, contenu statique dans + `core/data/` ; (2) un kit versionné `claude-code-starter-kit/` à la racine, + généralisé depuis le setup réel de CraftCode (hooks, commandes `/brief` et + `/add-module`, CLAUDE.md squelette, settings.json avec placeholder MCP par ENV). +- **Options écartées** : servir le contenu pédagogique via l'API/seed (rejeté — + le contenu enseigné reste statique côté front, seul le registre Tool passe par + la base, cf. STATE-AND-DATA.md) ; lier le kit aux chemins de CraftCode (rejeté + au profit d'un kit autoportant et générique). +- **Pourquoi** : rendre la mise en place de Claude Code apprenable dans l'app et + rejouable sur tout projet, sans secret ni dépendance au dépôt CraftCode. +- **Trace** : branche `main` (session du 2026-06-17). + ## 2026-06-17 — Journal de décisions + résumé automatique des changements - **Contexte** : les décisions prises en session avec Claude Code partaient trop vite, diff --git a/backend/seed/data.js b/backend/seed/data.js index e88fd1b..928350a 100644 --- a/backend/seed/data.js +++ b/backend/seed/data.js @@ -44,6 +44,16 @@ const tools = [ available: true, order: 4, }, + { + slug: 'claude-code-setup', + name: 'Mettre en place Claude Code', + description: + 'Les quatre briques pour installer Claude Code dans un projet : fichiers markdown, hooks, slash-commands & skills, settings.json & serveurs MCP.', + icon: 'terminal', + route: '/claude-code-setup', + available: true, + order: 5, + }, ]; const checklist = [ @@ -58,19 +68,22 @@ const checklist = [ { category: 'Lisibilité & Nommage', label: 'Pas de code commenté laissé en place', - description: 'Le code mort ou commenté a été supprimé ; l’historique git suffit à le retrouver.', + description: + 'Le code mort ou commenté a été supprimé ; l’historique git suffit à le retrouver.', order: 2, }, { category: 'Lisibilité & Nommage', label: 'Les commentaires expliquent le « pourquoi »', - description: 'Les commentaires justifient les choix non évidents plutôt que de paraphraser le code.', + description: + 'Les commentaires justifient les choix non évidents plutôt que de paraphraser le code.', order: 3, }, { category: 'Lisibilité & Nommage', label: 'Formatage cohérent', - description: 'Indentation, style et conventions sont homogènes avec le reste du projet (linter/formatter).', + description: + 'Indentation, style et conventions sont homogènes avec le reste du projet (linter/formatter).', order: 4, }, @@ -78,19 +91,22 @@ const checklist = [ { category: 'Tests', label: 'Les nouveaux comportements sont testés', - description: 'Chaque nouvelle fonctionnalité ou correction est couverte par au moins un test.', + description: + 'Chaque nouvelle fonctionnalité ou correction est couverte par au moins un test.', order: 1, }, { category: 'Tests', label: 'Les cas limites sont couverts', - description: 'Valeurs nulles, vides, négatives ou extrêmes sont testées, pas seulement le cas nominal.', + description: + 'Valeurs nulles, vides, négatives ou extrêmes sont testées, pas seulement le cas nominal.', order: 2, }, { category: 'Tests', label: 'Les tests sont lisibles et isolés', - description: 'Chaque test vérifie une chose, sans dépendre de l’ordre d’exécution ni d’un état partagé.', + description: + 'Chaque test vérifie une chose, sans dépendre de l’ordre d’exécution ni d’un état partagé.', order: 3, }, @@ -98,25 +114,29 @@ const checklist = [ { category: 'Sécurité', label: 'Aucun secret codé en dur', - description: 'Mots de passe, clés API et tokens passent par des variables d’environnement, jamais le code.', + description: + 'Mots de passe, clés API et tokens passent par des variables d’environnement, jamais le code.', order: 1, }, { category: 'Sécurité', label: 'Les entrées utilisateur sont validées', - description: 'Toute donnée externe est validée et assainie avant traitement (injection, XSS).', + description: + 'Toute donnée externe est validée et assainie avant traitement (injection, XSS).', order: 2, }, { category: 'Sécurité', label: 'Les erreurs ne fuitent pas d’infos sensibles', - description: 'Les messages d’erreur exposés ne révèlent ni stack trace ni détails internes au client.', + description: + 'Les messages d’erreur exposés ne révèlent ni stack trace ni détails internes au client.', order: 3, }, { category: 'Sécurité', label: 'Les dépendances sont à jour et sûres', - description: 'Pas de dépendance vulnérable connue ; les versions sont épinglées de façon raisonnable.', + description: + 'Pas de dépendance vulnérable connue ; les versions sont épinglées de façon raisonnable.', order: 4, }, @@ -124,19 +144,22 @@ const checklist = [ { category: 'Performance', label: 'Pas de requête dans une boucle (N+1)', - description: 'Les accès base de données ou réseau sont regroupés plutôt que répétés dans une boucle.', + description: + 'Les accès base de données ou réseau sont regroupés plutôt que répétés dans une boucle.', order: 1, }, { category: 'Performance', label: 'Pas de calcul inutile répété', - description: 'Les résultats coûteux et constants sont mémorisés ou sortis de la boucle.', + description: + 'Les résultats coûteux et constants sont mémorisés ou sortis de la boucle.', order: 2, }, { category: 'Performance', label: 'Les ressources sont libérées', - description: 'Connexions, fichiers et abonnements sont fermés/désabonnés pour éviter les fuites.', + description: + 'Connexions, fichiers et abonnements sont fermés/désabonnés pour éviter les fuites.', order: 3, }, @@ -144,25 +167,29 @@ const checklist = [ { category: 'Architecture & SOLID', label: 'Les fonctions font une seule chose', - description: 'Chaque fonction a une responsabilité unique et un niveau d’abstraction cohérent.', + description: + 'Chaque fonction a une responsabilité unique et un niveau d’abstraction cohérent.', order: 1, }, { category: 'Architecture & SOLID', label: 'Pas de duplication (DRY)', - description: 'La logique répétée est factorisée dans une fonction ou un module réutilisable.', + description: + 'La logique répétée est factorisée dans une fonction ou un module réutilisable.', order: 2, }, { category: 'Architecture & SOLID', label: 'Faible couplage entre modules', - description: 'Les modules dépendent d’abstractions, pas d’implémentations concrètes (inversion de dépendance).', + description: + 'Les modules dépendent d’abstractions, pas d’implémentations concrètes (inversion de dépendance).', order: 3, }, { category: 'Architecture & SOLID', label: 'Le code respecte les conventions du projet', - description: 'La structure des dossiers et les patterns suivent ceux déjà établis dans la base de code.', + description: + 'La structure des dossiers et les patterns suivent ceux déjà établis dans la base de code.', order: 4, }, ]; diff --git a/claude-code-starter-kit/.claude/commands/add-module.md b/claude-code-starter-kit/.claude/commands/add-module.md new file mode 100644 index 0000000..3e1c8ef --- /dev/null +++ b/claude-code-starter-kit/.claude/commands/add-module.md @@ -0,0 +1,31 @@ +--- +description: Scaffolder un nouveau module en suivant la procédure du projet +argument-hint: "" +--- + +Tu vas ajouter un nouveau module au projet en suivant **exactement** la procédure +documentée (ex. un fichier ADD-A-MODULE.md) et les conventions du dépôt +(@CONVENTIONS.md). La source de vérité reste le code existant : imite un module +déjà en place plutôt que d'inventer une structure. + +Arguments fournis : `$ARGUMENTS` +(format attendu : `slug "Nom affiché"` ; si le slug ou le nom manque, demande-le +avant de commencer.) + +Déroule les étapes dans l'ordre, sans en sauter. Adapte cette trame à ta stack ; +exemple type pour une feature front + back : + +1. **Modèle / types** — l'interface ou le schéma du module. +2. **Données / contenu** — la source de vérité (constante, fixture, migration). +3. **Vue principale** — le composant ou l'endpoint d'entrée du module. +4. **Vue de détail** (si applicable) — résolution réactive d'un paramètre, repli + propre si l'identifiant est inconnu. +5. **Routage** — branche le module dans la table de routes / le routeur. +6. **Navigation** — ajoute le point d'entrée (menu, index). +7. **Persistance** — l'entrée de seed / migration si le module y figure. + +Puis **vérifie** avec la commande de contrôle du projet (lint + tests/build) et +colle sa sortie. + +Termine par un résumé des fichiers créés/modifiés. Ne committe rien sans demande +explicite. diff --git a/claude-code-starter-kit/.claude/commands/brief.md b/claude-code-starter-kit/.claude/commands/brief.md new file mode 100644 index 0000000..cfd2275 --- /dev/null +++ b/claude-code-starter-kit/.claude/commands/brief.md @@ -0,0 +1,31 @@ +--- +description: Cadre une tâche et impose des check-ins visibles +argument-hint: +--- + +Tâche brute du développeur : $ARGUMENTS + +Tu travailles en trois phases. Ne saute aucune phase. + +## Phase 1 — Cadrage (AVANT tout code) +Affiche, puis ARRÊTE-TOI : +- **Intention** : reformule en 1-2 phrases ce que tu as compris. +- **Périmètre** : liste les fichiers/dossiers que tu vas modifier ET ceux que tu + ne toucheras pas. Aucun chemin = tu demandes lesquels. +- **Critères d'acceptation** : binaires. « Fait quand : … ». +- **Ambiguïtés** : au plus 3 questions. Sinon, déclare l'hypothèse retenue. +- **STOP.** Attends mon « go » explicite avant d'écrire la moindre ligne. + +## Phase 2 — Exécution (seulement après « go ») +- Après CHAQUE étape, affiche une ligne d'état : + `✅ [fait] · 📂 [fichiers touchés] · ➡️ [prochaine étape]` +- Avant toute décision structurante — architecture, nouvelle dépendance, schéma + de base, suppression/renommage de fichier — **STOP** : présente 2 options + maximum avec ta recommandation, et attends mon choix. +- Ne fais QUE ce qui est demandé. Aucun refactor ou fichier en plus du périmètre. +- Si tu dois sortir du périmètre annoncé, signale-le et demande avant de continuer. + +## Phase 3 — Clôture +- **Résumé** : ce qui a changé, ce qui reste, et la commande exacte de vérification. +- N'affirme « fait / corrigé » qu'après avoir lancé la vérification du projet + (lint + tests/build) et collé sa sortie. Si un test échoue, dis-le. diff --git a/claude-code-starter-kit/.claude/hooks/format-edited-file.mjs b/claude-code-starter-kit/.claude/hooks/format-edited-file.mjs new file mode 100644 index 0000000..f278e2f --- /dev/null +++ b/claude-code-starter-kit/.claude/hooks/format-edited-file.mjs @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * Hook PostToolUse (Edit|Write|MultiEdit) — formatage automatique. + * + * Après chaque édition de fichier par Claude Code, formate le fichier touché + * avec le Prettier le plus proche (remonte l'arborescence depuis le fichier + * jusqu'à trouver node_modules/.bin/prettier). Générique : aucun chemin de + * projet en dur. Adapte le formateur (eslint --fix, gofmt, black…) à ta stack. + * + * Ne lève jamais d'exception : un hook qui plante ne doit pas bloquer le travail. + */ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, parse } from 'node:path'; + +const PRETTIER_EXT = /\.(ts|js|mjs|cjs|jsx|tsx|html|scss|css|json|md|yml|yaml)$/; + +function readStdin() { + try { + return readFileSync(0, 'utf8'); + } catch { + return ''; + } +} + +/** Cherche node_modules/.bin/prettier en remontant depuis le fichier édité. */ +function findPrettierBin(filePath) { + let dir = dirname(filePath); + const { root } = parse(dir); + while (true) { + const bin = join(dir, 'node_modules', '.bin', 'prettier'); + if (existsSync(bin)) return bin; + if (dir === root) return null; + dir = dirname(dir); + } +} + +function main() { + let payload; + try { + payload = JSON.parse(readStdin()); + } catch { + process.exit(0); + } + + const filePath = payload?.tool_input?.file_path; + if (!filePath || !PRETTIER_EXT.test(filePath)) process.exit(0); + + const bin = findPrettierBin(filePath); + if (bin) { + try { + execFileSync(bin, ['--write', '--ignore-unknown', filePath], { + stdio: 'ignore', + }); + } catch { + /* fichier ignoré par .prettierignore ou non parsable : on n'échoue pas */ + } + } + + process.exit(0); +} + +main(); diff --git a/claude-code-starter-kit/.claude/hooks/summarize-change.mjs b/claude-code-starter-kit/.claude/hooks/summarize-change.mjs new file mode 100644 index 0000000..80c904e --- /dev/null +++ b/claude-code-starter-kit/.claude/hooks/summarize-change.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * Hook PostToolUse (Edit|Write|MultiEdit) — résumé visible des changements. + * + * Après chaque modification de fichier par Claude Code, affiche un résumé + * lisible : outil utilisé, fichier touché, lignes +/− par rapport au dernier + * commit. But : que le développeur VOIE ce que l'assistant opère, sans dépendre + * de sa vigilance. Générique : la racine du dépôt vient de $CLAUDE_PROJECT_DIR + * (repli sur le répertoire courant). + * + * Le message remonte via le champ `systemMessage` de la sortie JSON, affiché à + * l'utilisateur. Ne lève jamais d'exception : un hook ne doit pas bloquer. + */ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { relative } from 'node:path'; + +const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + +function readStdin() { + try { + return readFileSync(0, 'utf8'); + } catch { + return ''; + } +} + +function git(args) { + try { + return execFileSync('git', ['-C', ROOT, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return ''; + } +} + +/** Renvoie { added, removed, statut } pour un fichier, ou null si rien à dire. */ +function diffStat(filePath) { + const rel = relative(ROOT, filePath); + + // Fichier non suivi (nouvellement créé) : `git diff` ne le voit pas. + const status = git(['status', '--porcelain', '--', rel]); + if (status.startsWith('??')) { + let lines = 0; + try { + lines = readFileSync(filePath, 'utf8').split('\n').length; + } catch { + /* illisible : on laisse 0 */ + } + return { added: lines, removed: 0, statut: 'nouveau fichier' }; + } + + // Fichier suivi : delta cumulé par rapport au dernier commit. + const numstat = git(['diff', 'HEAD', '--numstat', '--', rel]); + if (!numstat) return null; // aucun changement vs HEAD (ex. reformatage idempotent) + + const [added, removed] = numstat.split('\n')[0].split('\t'); + return { + added: added === '-' ? '?' : Number(added), + removed: removed === '-' ? '?' : Number(removed), + statut: 'vs dernier commit', + }; +} + +function main() { + let payload; + try { + payload = JSON.parse(readStdin()); + } catch { + process.exit(0); + } + + const filePath = payload?.tool_input?.file_path; + if (!filePath) process.exit(0); + + const tool = payload?.tool_name ?? 'Edit'; + const rel = relative(ROOT, filePath); + const stat = diffStat(filePath); + + const summary = stat + ? `📝 ${tool} · ${rel} · +${stat.added} −${stat.removed} (${stat.statut})` + : `📝 ${tool} · ${rel} · aucun changement net (vs dernier commit)`; + + process.stdout.write(JSON.stringify({ systemMessage: summary })); + process.exit(0); +} + +main(); diff --git a/claude-code-starter-kit/.claude/hooks/verify-gate.mjs b/claude-code-starter-kit/.claude/hooks/verify-gate.mjs new file mode 100644 index 0000000..5b9a574 --- /dev/null +++ b/claude-code-starter-kit/.claude/hooks/verify-gate.mjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +/** + * Hook Stop — porte de vérification. + * + * À chaque fin de tour, vérifie qu'aucune édition de code ne reste sans commande + * de vérification postérieure. Si c'est le cas, BLOQUE la clôture et renvoie à + * Claude Code la consigne de lancer la vérification. But : faire respecter par + * l'outil — et non par la vigilance de l'assistant — le principe « ne jamais + * affirmer 'c'est fait' sans avoir vérifié ». Cf. CLAUDE.md / DECISIONS.md. + * + * Pendant Stop du hook PostToolUse summarize-change.mjs : l'un montre chaque + * changement, l'autre garantit qu'il a été vérifié. + * + * Signal objectif (pas d'analyse du langage naturel) : on repère dans le + * transcript la dernière exécution de la commande de vérification, puis les + * fichiers de code édités APRÈS elle. S'il en reste, on bloque. + * + * Garde anti-boucle : si `stop_hook_active` est vrai, on laisse passer. + * Toute erreur ⇒ exit 0 (la clôture passe) : un hook ne doit jamais bloquer le + * travail sur son propre bug. + * + * ─── À ADAPTER à ton projet ───────────────────────────────────────────────── + * • CHECK_COMMAND : la commande qui prouve que le code tient (lint + tests/build). + * • CHECK_PATTERN : comment la reconnaître dans le transcript. + * • IGNORED : préfixes/extensions à NE PAS garder (docs, config, .claude…). + * Pour un monorepo multi-dossiers, remplace la commande unique par une liste + * { dir, command } et calcule les dossiers édités, comme dans le hook équivalent + * du projet CraftCode dont ce kit est issu. + * ──────────────────────────────────────────────────────────────────────────── + */ +import { readFileSync } from 'node:fs'; +import { relative } from 'node:path'; + +const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + +const CHECK_COMMAND = 'npm run check'; // À ADAPTER +const CHECK_PATTERN = /npm run check/; // À ADAPTER + +/** Chemins relatifs à NE PAS soumettre à la porte (édition libre). À ADAPTER. */ +const IGNORED = ['.claude/', 'docs/']; +const IGNORED_EXT = ['.md', '.txt']; + +const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']); + +function readStdin() { + try { + return readFileSync(0, 'utf8'); + } catch { + return ''; + } +} + +/** Le fichier doit-il déclencher la porte ? (false pour docs/config ignorés). */ +function isGated(filePath) { + if (typeof filePath !== 'string') return false; + const rel = relative(ROOT, filePath); + if (rel.startsWith('..')) return false; // hors du dépôt + if (IGNORED.some((p) => rel.startsWith(p))) return false; + if (IGNORED_EXT.some((ext) => rel.endsWith(ext))) return false; + return true; +} + +/** Reste-t-il des fichiers de code édités après le dernier check ? */ +function hasUncheckedEdits(transcriptPath) { + const raw = readFileSync(transcriptPath, 'utf8'); + let pending = false; + + for (const line of raw.split('\n')) { + if (!line.trim()) continue; + let entry; + try { + entry = JSON.parse(line); + } catch { + continue; // ligne illisible : on ignore + } + + const content = entry?.message?.content; + if (!Array.isArray(content)) continue; + + for (const block of content) { + if (block?.type !== 'tool_use') continue; + + if (EDIT_TOOLS.has(block.name)) { + if (isGated(block?.input?.file_path)) pending = true; + } else if (block.name === 'Bash' && CHECK_PATTERN.test(block?.input?.command ?? '')) { + pending = false; // un check remet les compteurs à zéro + } + } + } + + return pending; +} + +function main() { + let payload; + try { + payload = JSON.parse(readStdin()); + } catch { + process.exit(0); + } + + if (payload?.stop_hook_active) process.exit(0); // anti-boucle + + const transcriptPath = payload?.transcript_path; + if (!transcriptPath) process.exit(0); + + let pending; + try { + pending = hasUncheckedEdits(transcriptPath); + } catch { + process.exit(0); // transcript illisible : on laisse passer + } + + if (!pending) process.exit(0); + + const reason = + `Porte de vérification : du code a été modifié sans vérification depuis. ` + + `Avant de clore, lance \`${CHECK_COMMAND}\` et colle sa sortie. ` + + `Si un test échoue, dis-le plutôt que d'affirmer que c'est fait.`; + + process.stdout.write(JSON.stringify({ decision: 'block', reason })); + process.exit(0); +} + +main(); diff --git a/claude-code-starter-kit/.claude/settings.json b/claude-code-starter-kit/.claude/settings.json new file mode 100644 index 0000000..ecccb7d --- /dev/null +++ b/claude-code-starter-kit/.claude/settings.json @@ -0,0 +1,48 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint)", + "Bash(npm test)", + "Bash(npm run build)", + "Bash(git status*)", + "Bash(git diff*)", + "Bash(git log*)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/format-edited-file.mjs\"" + }, + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/summarize-change.mjs\"" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/verify-gate.mjs\"" + } + ] + } + ] + }, + "mcpServers": { + "database": { + "command": "npx", + "args": ["-y", "@some/mcp-server"], + "env": { + "DATABASE_URI": "${DATABASE_URI}" + } + } + } +} diff --git a/claude-code-starter-kit/.claude/skills/example-skill/SKILL.md b/claude-code-starter-kit/.claude/skills/example-skill/SKILL.md new file mode 100644 index 0000000..435a49a --- /dev/null +++ b/claude-code-starter-kit/.claude/skills/example-skill/SKILL.md @@ -0,0 +1,31 @@ +--- +name: example-skill +description: >- + Squelette d'exemple. Décris ICI, précisément, QUAND ce skill doit s'activer : + c'est cette description que l'assistant lit pour décider de l'invoquer. Énonce + les déclencheurs concrets (« utiliser quand l'utilisateur demande X ou Y »). +--- + +# + +Une phrase sur ce que fait ce skill et la valeur qu'il apporte. + +## Quand l'utiliser + +Le ou les cas concrets qui doivent déclencher l'activation. Sois explicite : un +skill bien décrit s'active de lui-même, sans qu'on ait à s'en souvenir. + +## Quand NE PAS l'utiliser + +Les cas voisins où un autre skill (ou aucun) convient mieux. Évite les +activations parasites. + +## Procédure + +1. Première étape. +2. Deuxième étape. +3. Vérification : comment savoir que c'est réussi. + +> Un skill peut s'accompagner de fichiers de référence dans son dossier +> (scripts, gabarits, documents) que la procédure ci-dessus va lire ou exécuter. +> Garde ce SKILL.md court : il décrit, il ne duplique pas. diff --git a/claude-code-starter-kit/CLAUDE.md b/claude-code-starter-kit/CLAUDE.md new file mode 100644 index 0000000..8449373 --- /dev/null +++ b/claude-code-starter-kit/CLAUDE.md @@ -0,0 +1,44 @@ + + +# + + +Ex. : Frontend dans `web/` · backend dans `api/` · base PostgreSQL. + +## Documentation projet + + + +- @ARCHITECTURE.md — carte du projet, stack, flux de données. +- @COMMANDS.md — comment lancer, tester, vérifier. +- @CONVENTIONS.md — nommage, style, conventions de commit. +- @DECISIONS.md — journal des décisions structurantes (le *pourquoi*, hors du diff). + +## Style de travail (assistant) + +- **Sobriété** : répondre en prose, réserver les listes aux procédures et comparaisons. +- **Honnêteté des résultats** : ne jamais affirmer « c'est fait » sans avoir lancé + la vérification (voir @COMMANDS.md) ; si un test échoue, le dire avec sa sortie. +- **Agir quand l'info suffit** : recommander plutôt que survoler ; ne poser une + question que si elle change ce qu'on va faire. +- **Source de vérité = le code existant** : suivre les patterns déjà en place plutôt + qu'en inventer de nouveaux. + +## Décisions & traçabilité + +- **Journal des décisions** (@DECISIONS.md) : à chaque décision structurante — + architecture, nouvelle dépendance, schéma, convention transverse, renommage ou + suppression — ajouter une entrée datée (contexte, décision, options écartées, + pourquoi). Pas pour les modifications de routine. +- **Hooks** : un hook PostToolUse formate chaque fichier édité et résume le + changement ; un hook Stop (`verify-gate.mjs`) bloque la clôture tant que du + code édité n'a pas été vérifié par la commande de check (voir `.claude/hooks/`, + à adapter à ton projet). Rien à faire — c'est automatique. diff --git a/claude-code-starter-kit/README.md b/claude-code-starter-kit/README.md new file mode 100644 index 0000000..331e91d --- /dev/null +++ b/claude-code-starter-kit/README.md @@ -0,0 +1,91 @@ +# Claude Code Starter Kit + +Un point de départ réutilisable pour mettre en place **Claude Code** dans +n'importe quel projet. Tout est générique : copie le dossier, remplace les +`<…>` et adapte à ta stack. Aucun secret n'est versionné — les valeurs +sensibles passent par des variables d'environnement. + +Ce kit est dérivé d'un setup réel et éprouvé (le projet CraftCode), généralisé. + +## Ce que contient le kit + +``` +claude-code-starter-kit/ +├─ CLAUDE.md # Mémoire du projet (lue au démarrage) +├─ README.md # Ce fichier +└─ .claude/ + ├─ settings.json # Permissions, hooks, serveurs MCP + ├─ hooks/ + │ ├─ format-edited-file.mjs # Formate chaque fichier édité (PostToolUse) + │ └─ summarize-change.mjs # Résume chaque changement +/− (PostToolUse) + ├─ commands/ + │ ├─ brief.md # /brief — cadre une tâche, impose des check-ins + │ └─ add-module.md # /add-module — scaffolde un module selon la procédure + └─ skills/ + └─ example-skill/SKILL.md # Squelette d'un skill +``` + +### Rôle de chaque fichier + +- **CLAUDE.md** — la mémoire du projet. Claude Code la lit automatiquement au + démarrage : description du projet, style de travail attendu, et liens + « @fichier.md » vers les guides détaillés. Garde-le court ; il oriente, il ne + duplique pas le code. + +- **.claude/settings.json** — la configuration de session, versionnée et + partagée par l'équipe. Trois blocs : `permissions` (allow-list de commandes + sûres pour éviter de re-valider à chaque fois), `hooks` (branchement des + scripts ci-dessous), et `mcpServers` (déclaration d'outils externes). À savoir : + le JSON n'autorise pas les commentaires ; les secrets se réfèrent par `${NOM}`, + jamais en clair. + +- **.claude/hooks/format-edited-file.mjs** — hook PostToolUse qui formate + automatiquement tout fichier que l'assistant édite (cherche le Prettier le plus + proche). Remplace le formateur par celui de ta stack si besoin. + +- **.claude/hooks/summarize-change.mjs** — hook PostToolUse qui affiche, après + chaque édition, un résumé visible (`+ajouts −retraits` vs dernier commit) via le + champ `systemMessage`. But : voir ce que l'assistant change, sans dépendre de sa + vigilance. + +- **.claude/commands/brief.md** — slash-command `/brief ` : impose un + cadrage (intention, périmètre, critères) avec un STOP avant tout code. + +- **.claude/commands/add-module.md** — slash-command `/add-module ""` : + scaffolde un nouveau module en suivant la procédure et les conventions du projet. + +- **.claude/skills/example-skill/SKILL.md** — gabarit d'un skill : une capacité + que l'assistant invoque de lui-même quand sa `description` correspond à la tâche. + +## Minimum vital vs optionnel + +- **Minimum vital** : `CLAUDE.md`. À lui seul, il donne à Claude Code le contexte + essentiel. Commence là. +- **Fortement recommandé** : `.claude/settings.json` avec quelques permissions + ciblées — réduit nettement les confirmations répétitives. +- **Optionnel, à valeur ajoutée** : les hooks (automatisme et visibilité), les + slash-commands (procédures réutilisables), les skills (capacités auto-activées), + les serveurs MCP (accès à des outils externes). Ajoute-les quand un besoin réel + apparaît, pas par principe. + +## Démarrer Claude Code sur un nouveau projet + +1. **Copie le kit** à la racine de ton projet : + `cp -r claude-code-starter-kit/CLAUDE.md claude-code-starter-kit/.claude .` + (puis supprime le kit si tu l'avais cloné à part). +2. **Renseigne CLAUDE.md** : remplace les `<…>`, décris le projet, et ne garde + que les lignes « @fichier.md » qui pointent vers des documents existants. +3. **Ajuste les permissions** dans `.claude/settings.json` : ne laisse que des + commandes sûres et propres à ta stack (ex. `Bash(pytest*)`, `Bash(go test*)`). +4. **Adapte les hooks** à ton formateur si tu n'utilises pas Prettier ; sinon, + laisse-les. Ils sont déjà branchés dans `settings.json`. +5. **Configure les serveurs MCP** si besoin : remplace le placeholder `database` + par ton serveur, et passe les secrets par des variables d'environnement + (`${NOM}`). Sinon, retire le bloc `mcpServers`. +6. **Personnalise les commandes** `brief.md` / `add-module.md` (commande de + vérification, étapes propres au projet) ou ajoute les tiennes. +7. **Lance Claude Code** dans le dossier du projet : il lit `CLAUDE.md` et + `.claude/settings.json` au démarrage. Vérifie que `/brief` apparaît et que les + hooks se déclenchent (un résumé `📝` s'affiche après une édition). +8. **Itère** : enrichis CLAUDE.md et ajoute hooks/commands/skills au fil des + besoins réels. diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 603eae5..cbee776 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -33,5 +33,6 @@ export class AppComponent { { label: 'Bonnes pratiques', link: '/bonnes-pratiques' }, { label: 'Principes SOLID', link: '/solid' }, { label: 'Design Patterns', link: '/design-patterns' }, + { label: 'Claude Code', link: '/claude-code-setup' }, ]; } diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 2b33913..cd8e4ea 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -54,9 +54,23 @@ export const routes: Routes = [ { path: 'design-patterns/:pattern', loadComponent: () => - import( - './features/design-pattern-detail/design-pattern-detail.component' - ).then((m) => m.DesignPatternDetailComponent), + import('./features/design-pattern-detail/design-pattern-detail.component').then( + (m) => m.DesignPatternDetailComponent + ), + }, + { + path: 'claude-code-setup', + loadComponent: () => + import('./features/claude-code-setup/claude-code-setup.component').then( + (m) => m.ClaudeCodeSetupComponent + ), + }, + { + path: 'claude-code-setup/:sujet', + loadComponent: () => + import('./features/claude-code-setup-detail/claude-code-setup-detail.component').then( + (m) => m.ClaudeCodeSetupDetailComponent + ), }, { path: '**', redirectTo: '' }, ]; diff --git a/frontend/src/app/core/data/claude-code-topics.ts b/frontend/src/app/core/data/claude-code-topics.ts new file mode 100644 index 0000000..4610596 --- /dev/null +++ b/frontend/src/app/core/data/claude-code-topics.ts @@ -0,0 +1,226 @@ +import { ClaudeCodeTopic } from '../models/claude-code-topic.model'; + +/** + * Source unique du contenu pédagogique « Mettre en place Claude Code dans un + * projet ». Quatre sujets, chacun avec un `slug` stable qui sert de paramètre + * de route (page de détail `/claude-code-setup/:sujet`). + * + * Consommé par le hub (claude-code-setup) et les pages de détail + * (claude-code-setup-detail). Les exemples de code sont illustratifs : ils + * montrent une forme à imiter, ils ne sont jamais exécutés. + */ +export const CLAUDE_CODE_TOPICS: ClaudeCodeTopic[] = [ + { + slug: 'fichiers-markdown', + icon: 'description', + titre: 'Les fichiers markdown', + accroche: + 'Le fichier CLAUDE.md est la mémoire du projet : il dit à Claude Code comment travailler ici.', + definition: + 'Claude Code lit automatiquement le CLAUDE.md à la racine au démarrage. C’est le minimum vital : un document court qui décrit le projet, le style de travail attendu et les commandes de vérification. On l’enrichit par des fichiers thématiques importés via la syntaxe « @ ».', + pourquoi: [ + 'Contexte partagé : l’assistant connaît la stack, l’arborescence et les conventions sans qu’on les répète à chaque session.', + 'Cohérence : les règles de nommage, de style et de commit sont au même endroit, versionnées avec le code.', + 'Découpage : un CLAUDE.md court qui « @importe » des guides spécialisés reste lisible et facile à faire évoluer.', + 'Onboarding : un nouvel arrivant — humain ou assistant — lit les mêmes documents.', + ], + exemples: [ + { + legende: 'Squelette minimal d’un CLAUDE.md à la racine', + code: `# Mon projet + +Frontend dans \`web/\` · backend dans \`api/\`. Base PostgreSQL. + +## Documentation +- @ARCHITECTURE.md — carte du projet et flux de données. +- @COMMANDS.md — lancer, tester, vérifier. + +## Style de travail +- Répondre en prose sobre, agir quand l’info suffit. +- Ne jamais affirmer « c’est fait » sans avoir lancé la vérification.`, + }, + { + legende: 'Importer un guide spécialisé depuis le CLAUDE.md', + code: `## Conventions +- @NAMING-CONVENTIONS.md — règles de nommage (inférées du code existant). +- @GIT-CONVENTIONS.md — Conventional Commits : types, scope, branches. + +# La ligne « @chemin.md » charge le fichier dans le contexte au démarrage.`, + }, + ], + commentFaire: [ + 'Commence petit : un CLAUDE.md d’une page suffit, enrichis-le quand un besoin réel apparaît.', + 'Décris ce que le code ne dit pas : le « pourquoi », les pièges, les commandes de vérification.', + 'Garde la source de vérité dans le code : le CLAUDE.md guide, il ne duplique pas l’implémentation.', + 'Versionne-le : il évolue avec le projet et profite à toute l’équipe.', + ], + }, + { + slug: 'hooks', + icon: 'cable', + titre: 'Les hooks', + accroche: + 'Un hook exécute ta commande à un moment précis du cycle de l’assistant — sans dépendre de sa vigilance.', + definition: + 'Les hooks sont des commandes déclarées dans settings.json, déclenchées sur un événement (PostToolUse, PreToolUse, Stop…). Le hook reçoit un payload JSON sur l’entrée standard et peut formater un fichier, valider une règle ou afficher un message. C’est le bon outil quand on veut un comportement automatique et garanti.', + pourquoi: [ + 'Automatisme garanti : le formatage ou la vérification s’exécute toujours, pas « quand l’assistant y pense ».', + 'Visibilité : un hook PostToolUse peut résumer chaque changement pour que le développeur valide en connaissance de cause.', + 'Garde-fous : un hook peut signaler une violation de convention (valeur en dur, secret) au moment où elle apparaît.', + 'Robustesse : un hook bien écrit n’échoue jamais bruyamment — il ne doit pas bloquer le travail.', + ], + exemples: [ + { + legende: 'Brancher un hook PostToolUse dans .claude/settings.json', + code: `{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "node \\"$CLAUDE_PROJECT_DIR/.claude/hooks/format-edited-file.mjs\\"" + } + ] + } + ] + } +}`, + }, + { + legende: 'Hook .mjs minimal — lit le payload, ne plante jamais', + code: `#!/usr/bin/env node +// PostToolUse : reçoit un JSON sur stdin, agit, sort proprement. +import { readFileSync } from 'node:fs'; + +function readStdin() { + try { return readFileSync(0, 'utf8'); } catch { return ''; } +} + +let payload; +try { payload = JSON.parse(readStdin()); } catch { process.exit(0); } + +const filePath = payload?.tool_input?.file_path; +if (!filePath) process.exit(0); + +// … formater / valider le fichier ici … + +// systemMessage est affiché à l’utilisateur (pas seulement au contexte). +process.stdout.write(JSON.stringify({ systemMessage: \`Vu : \${filePath}\` })); +process.exit(0);`, + }, + ], + commentFaire: [ + 'Choisis l’événement juste : PostToolUse pour réagir à une édition, PreToolUse pour valider avant.', + 'Filtre avec « matcher » pour ne déclencher que sur les outils concernés (Edit, Write…).', + 'Ne plante jamais : entoure les lectures de try/catch et sors en code 0 par défaut.', + 'Remonte un message utile via « systemMessage » plutôt que d’agir en silence.', + ], + }, + { + slug: 'commands-et-skills', + icon: 'bolt', + titre: 'Slash-commands & skills', + accroche: + 'Une slash-command est un prompt réutilisable ; un skill, une capacité que l’assistant invoque quand elle s’applique.', + definition: + 'Une slash-command est un fichier markdown dans .claude/commands/ : son corps devient un prompt, lancé par « /nom ». Un skill vit dans un dossier avec un SKILL.md décrit par un frontmatter ; l’assistant le charge de lui-même dès que sa description correspond à la tâche. Les deux capitalisent un savoir-faire répétable.', + pourquoi: [ + 'Réutilisation : on encode une fois une procédure (cadrer une tâche, ajouter un module) et on la rejoue d’un mot.', + 'Cohérence : la même commande produit le même cadrage, quel que soit le moment.', + 'Découverte : un skill bien décrit s’active automatiquement — pas besoin de se souvenir de l’invoquer.', + 'Partage : commandes et skills sont versionnés, donc disponibles pour toute l’équipe.', + ], + exemples: [ + { + legende: 'Slash-command .claude/commands/brief.md', + code: `--- +description: Cadre une tâche et impose des check-ins visibles +argument-hint: +--- + +Tâche brute : $ARGUMENTS + +Travaille en trois phases. Phase 1 — cadrage : reformule l’intention, +liste le périmètre, donne les critères d’acceptation, puis STOP et +attends mon « go » avant d’écrire la moindre ligne.`, + }, + { + legende: 'Structure d’un skill : SKILL.md avec frontmatter', + code: `--- +name: example-skill +description: Décrit QUAND utiliser ce skill — l’assistant s’en sert pour + décider de l’activer. Sois précis sur le déclencheur. +--- + +# Quoi +Une phrase sur ce que fait le skill. + +# Quand l’utiliser +Le cas concret qui doit déclencher l’activation. + +# Procédure +1. Étape un. +2. Étape deux.`, + }, + ], + commentFaire: [ + 'Soigne la « description » d’un skill : c’est elle qui décide de l’activation, pas le contenu.', + 'Utilise $ARGUMENTS dans une commande pour injecter ce que l’utilisateur tape après « /nom ».', + 'Garde une commande focalisée sur une intention claire plutôt qu’un couteau suisse.', + 'Commence par une slash-command ; passe au skill quand la logique mérite d’être chargée automatiquement.', + ], + }, + { + slug: 'settings-et-mcp', + icon: 'settings', + titre: 'settings.json & serveurs MCP', + accroche: + 'settings.json règle permissions et hooks ; les serveurs MCP ouvrent à Claude Code des outils externes (base, API…).', + definition: + 'Le fichier .claude/settings.json (versionné, partagé par l’équipe) configure la session : liste d’autorisations pour réduire les confirmations, branchement des hooks, et déclaration de serveurs MCP. Un serveur MCP expose des outils — par exemple une base de données — que l’assistant peut appeler. Les secrets passent par des variables d’environnement, jamais en clair.', + pourquoi: [ + 'Moins de friction : une allow-list ciblée évite de re-valider les commandes sûres et fréquentes.', + 'Partage : settings.json est versionné ; settings.local.json reste personnel et hors dépôt.', + 'Extensibilité : un serveur MCP donne à l’assistant un accès direct et typé à un service externe.', + 'Sécurité : références d’ENV pour les secrets — aucune clé ni token dans le dépôt.', + ], + exemples: [ + { + legende: 'Permissions ciblées dans .claude/settings.json', + code: `{ + "permissions": { + "allow": [ + "Bash(npm run lint)", + "Bash(npm test)", + "Bash(git status*)", + "Bash(git diff*)" + ] + } +}`, + }, + { + legende: 'Serveur MCP avec secret via variable d’environnement', + code: `{ + "mcpServers": { + "database": { + "command": "npx", + "args": ["-y", "@some/mcp-server"], + "env": { + // Référence d’ENV — jamais la valeur en clair dans le dépôt. + "DATABASE_URI": "\${DATABASE_URI}" + } + } + } +}`, + }, + ], + commentFaire: [ + 'N’autorise que des commandes sûres et précises : préfère « Bash(git status*) » à un blanc-seing.', + 'Mets les secrets dans des variables d’environnement et référence-les par « ${NOM} ».', + 'Garde le personnel hors dépôt : settings.local.json pour ce qui ne regarde que toi.', + 'Vérifie qu’un serveur MCP est nécessaire avant de l’ajouter : chaque outil élargit la surface.', + ], + }, +]; diff --git a/frontend/src/app/core/models/claude-code-topic.model.ts b/frontend/src/app/core/models/claude-code-topic.model.ts new file mode 100644 index 0000000..ce1ca3c --- /dev/null +++ b/frontend/src/app/core/models/claude-code-topic.model.ts @@ -0,0 +1,29 @@ +/** Un bloc de code illustratif (affiché tel quel, jamais exécuté). */ +export interface ClaudeCodeExample { + /** Légende du bloc (ex. « Squelette minimal d'un CLAUDE.md »). */ + legende: string; + /** Le code / la configuration — extrait illustratif, commenté. */ + code: string; +} + +/** + * Un sujet de mise en place de Claude Code dans un projet (fichiers markdown, + * hooks, slash-commands & skills, settings.json & serveurs MCP). + */ +export interface ClaudeCodeTopic { + /** Slug court pour la route de détail (ex. `hooks`). Stable : sert d'URL. */ + slug: string; + icon: string; + /** Titre du sujet (carte d'aperçu + en-tête de la page de détail). */ + titre: string; + /** Accroche courte (une phrase) affichée sur la carte d'aperçu. */ + accroche: string; + /** Définition / cadrage du sujet (en-tête de la page de détail). */ + definition: string; + /** Pourquoi ce sujet compte — points développés (page de détail). */ + pourquoi: string[]; + /** Exemples concrets et commentés (fichiers, config, hooks…). */ + exemples: ClaudeCodeExample[]; + /** Conseils pratiques pour le mettre en place (impératif, tutoiement). */ + commentFaire: string[]; +} diff --git a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.html b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.html new file mode 100644 index 0000000..a12fdc5 --- /dev/null +++ b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.html @@ -0,0 +1,87 @@ +@if (topic(); as t) { + + +
+ +

{{ t.titre }}

+
+ +

{{ t.accroche }}

+

{{ t.definition }}

+ + +
+

+ + Pourquoi ça compte +

+
    + @for (point of t.pourquoi; track point) { +
  • {{ point }}
  • + } +
+
+ + +
+

+ + En pratique +

+
+ @for (exemple of t.exemples; track exemple.legende) { + + +

{{ exemple.legende }}

+
{{ exemple.code }}
+
+
+ } +
+
+ + +
+

+ + Comment le mettre en place +

+
    + @for (conseil of t.commentFaire; track conseil) { +
  • {{ conseil }}
  • + } +
+
+ + + +} diff --git a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.scss b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.scss new file mode 100644 index 0000000..bdd2ebe --- /dev/null +++ b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.scss @@ -0,0 +1,169 @@ +:host { + display: block; + max-width: 960px; + margin: 0 auto; + padding: var(--cc-space-5) var(--cc-space-4) var(--cc-space-8); +} + +.ccd-header { + display: flex; + align-items: center; + gap: var(--cc-space-3); + margin-bottom: var(--cc-space-4); + + &__icon { + color: var(--cc-accent); + flex-shrink: 0; + } + + h1 { + margin: 0; + color: var(--cc-ink); + font-size: clamp(1.5rem, 3.5vw, 2rem); + font-weight: 700; + line-height: var(--cc-lh-tight); + } +} + +.ccd-accroche { + margin: 0 0 var(--cc-space-3); + max-width: var(--cc-measure); + color: var(--cc-ink); + font-size: 1.1rem; + font-weight: 600; + line-height: var(--cc-lh-tight); +} + +.ccd-definition { + margin: 0 0 var(--cc-space-6); + max-width: var(--cc-measure); + color: var(--cc-ink-soft); + font-size: 1.05rem; + line-height: var(--cc-lh-body); +} + +.ccd-block { + margin-bottom: var(--cc-space-6); + + &__title { + display: flex; + align-items: center; + gap: var(--cc-space-2); + margin: 0 0 var(--cc-space-3); + font-size: clamp(1.2rem, 2.8vw, 1.4rem); + font-weight: 600; + color: var(--cc-ink); + + mat-icon { + color: var(--cc-accent); + } + } +} + +.ccd-list { + margin: 0; + padding-left: var(--cc-space-5); + max-width: var(--cc-measure); + color: var(--cc-ink-soft); + line-height: var(--cc-lh-body); + + li { + margin-bottom: var(--cc-space-2); + } +} + +/* Exemples concrets avec blocs de code */ +.examples { + display: grid; + gap: var(--cc-space-4); +} + +.example { + border-radius: var(--cc-radius-md); + + &__legende { + margin: 0 0 var(--cc-space-3); + color: var(--cc-ink-soft); + font-size: var(--cc-fs-small); + font-style: italic; + } +} + +.code { + margin: 0; + padding: var(--cc-space-4); + background: var(--cc-surface-2); + border: 1px solid var(--cc-border); + border-radius: var(--cc-radius-sm); + color: var(--cc-ink); + font-family: + 'SFMono-Regular', 'Cascadia Code', Consolas, 'Courier New', monospace; + font-size: 0.82rem; + line-height: 1.55; + overflow-x: auto; + white-space: pre; + tab-size: 2; +} + +/* Parcours séquentiel précédent / suivant */ +.topic-nav { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: var(--cc-space-3); + margin-top: var(--cc-space-7); + padding-top: var(--cc-space-4); + border-top: 1px solid var(--cc-border); +} + +.topic-nav__link { + display: inline-flex; + align-items: center; + gap: var(--cc-space-2); + min-height: 44px; + max-width: 100%; + padding: var(--cc-space-2) var(--cc-space-4); + border: 1px solid var(--cc-border); + border-radius: var(--cc-radius-md); + background: var(--cc-surface); + color: var(--cc-ink); + text-decoration: none; + transition: + border-color var(--cc-transition), + box-shadow var(--cc-transition); + + &:hover, + &:focus-visible { + border-color: var(--cc-primary); + box-shadow: var(--cc-shadow-1); + } + + &--next { + margin-left: auto; + text-align: right; + } + + mat-icon { + color: var(--cc-primary); + flex-shrink: 0; + } +} + +.topic-nav__meta { + display: flex; + flex-direction: column; + min-width: 0; +} + +.topic-nav__dir { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--cc-ink-soft); +} + +.topic-nav__titre { + font-weight: 600; + line-height: var(--cc-lh-tight); +} diff --git a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.ts b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.ts new file mode 100644 index 0000000..e7fdbc4 --- /dev/null +++ b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.ts @@ -0,0 +1,81 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; + +import { CLAUDE_CODE_TOPICS } from '../../core/data/claude-code-topics'; +import { ClaudeCodeTopic } from '../../core/models/claude-code-topic.model'; +import { + BreadcrumbComponent, + BreadcrumbItem, +} from '../../shared/components/breadcrumb/breadcrumb.component'; + +/** Ordre du parcours — dérivé de l'unique source CLAUDE_CODE_TOPICS. */ +const TOPIC_SLUGS = CLAUDE_CODE_TOPICS.map((t) => t.slug); + +/** Un sujet voisin pour le parcours séquentiel (précédent/suivant). */ +interface TopicLink { + slug: string; + titre: string; +} + +/** + * Page de détail d'un sujet de mise en place de Claude Code : définition, + * pourquoi, exemples commentés, et comment le mettre en place. + * + * Le sujet est résolu depuis le paramètre de route `:sujet`. Un slug inconnu + * redirige vers le hub `/claude-code-setup`. Le suivi réactif du paramètre + * couvre la navigation précédent/suivant entre routes sœurs (Angular réutilise + * alors le composant) — même pattern que la page de détail SOLID. + */ +@Component({ + selector: 'app-claude-code-setup-detail', + imports: [RouterLink, BreadcrumbComponent, MatCardModule, MatIconModule], + templateUrl: './claude-code-setup-detail.component.html', + styleUrl: './claude-code-setup-detail.component.scss', +}) +export class ClaudeCodeSetupDetailComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + + /** Slug courant, suivi de façon réactive (cf. constructeur). */ + private readonly slug = signal(''); + + /** Sujet courant (undefined si slug inconnu → redirection). */ + readonly topic = signal(undefined); + + /** Fil d'Ariane : Accueil › Claude Code › {titre du sujet}. */ + readonly breadcrumb = computed(() => [ + { label: 'Accueil', link: '/' }, + { label: 'Claude Code', link: '/claude-code-setup' }, + { label: this.topic()?.titre ?? '' }, + ]); + + /** Sujet précédent / suivant du parcours (undefined aux extrémités). */ + readonly prev = computed(() => this.neighbor(-1)); + readonly next = computed(() => this.neighbor(1)); + + constructor() { + this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => { + const slug = params.get('sujet') ?? ''; + const topic = CLAUDE_CODE_TOPICS.find((t) => t.slug === slug); + if (!topic) { + this.router.navigate(['/claude-code-setup']); + return; + } + this.slug.set(slug); + this.topic.set(topic); + }); + } + + /** Sujet voisin dans TOPIC_SLUGS (delta -1 = précédent, +1 = suivant). */ + private neighbor(delta: number): TopicLink | undefined { + const index = TOPIC_SLUGS.indexOf(this.slug()); + if (index === -1) return undefined; + const slug = TOPIC_SLUGS[index + delta]; + if (!slug) return undefined; + const titre = CLAUDE_CODE_TOPICS.find((t) => t.slug === slug)?.titre ?? ''; + return { slug, titre }; + } +} diff --git a/frontend/src/app/features/claude-code-setup/claude-code-setup.component.html b/frontend/src/app/features/claude-code-setup/claude-code-setup.component.html new file mode 100644 index 0000000..6833097 --- /dev/null +++ b/frontend/src/app/features/claude-code-setup/claude-code-setup.component.html @@ -0,0 +1,47 @@ + + +
+

Mettre en place Claude Code

+

+ Quatre briques pour installer Claude Code dans un projet : les fichiers + markdown qui le cadrent, les hooks qui automatisent, les slash-commands & + skills qui capitalisent, et la configuration qui ouvre l'accès aux outils. +

+
+ + +
+

+ Bien configuré, Claude Code connaît ton projet, respecte tes conventions et + automatise le répétitif. +

+

+ Choisis un sujet ci-dessous pour le découvrir en détail : à quoi il sert, + pourquoi il compte, des exemples concrets et commentés, et comment le mettre + en place. Un kit de démarrage réutilisable accompagne ces pages dans le + dépôt (dossier claude-code-starter-kit). +

+
+ + + diff --git a/frontend/src/app/features/claude-code-setup/claude-code-setup.component.scss b/frontend/src/app/features/claude-code-setup/claude-code-setup.component.scss new file mode 100644 index 0000000..f0e1213 --- /dev/null +++ b/frontend/src/app/features/claude-code-setup/claude-code-setup.component.scss @@ -0,0 +1,123 @@ +:host { + display: block; + max-width: 960px; + margin: 0 auto; + padding: var(--cc-space-5) var(--cc-space-4) var(--cc-space-8); +} + +.cc-setup-header { + margin-bottom: var(--cc-space-5); + + h1 { + margin: 0; + color: var(--cc-ink); + font-size: clamp(1.6rem, 4vw, 2.25rem); + font-weight: 700; + line-height: var(--cc-lh-tight); + } + + .subtitle { + margin: var(--cc-space-2) 0 0; + max-width: var(--cc-measure); + color: var(--cc-ink-soft); + line-height: var(--cc-lh-body); + } +} + +/* Hero — donne le ton (établi cuivré) */ +.hero { + background: linear-gradient( + 135deg, + var(--cc-primary) 0%, + var(--cc-primary-hover) 100% + ); + color: var(--cc-on-primary); + border-radius: var(--cc-radius-lg); + padding: var(--cc-space-6); + margin-bottom: var(--cc-space-6); + box-shadow: var(--cc-shadow-2); + + &__lead { + margin: 0 0 var(--cc-space-3); + font-size: clamp(1.15rem, 2.5vw, 1.5rem); + font-weight: 600; + line-height: var(--cc-lh-tight); + } + + &__body { + margin: 0; + max-width: var(--cc-measure); + color: var(--cc-on-primary); + line-height: var(--cc-lh-body); + } +} + +/* Grille de cartes-sujets (le hub) */ +.topics { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--cc-space-4); +} + +.topic-card { + display: block; + height: 100%; + text-decoration: none; + color: inherit; + border-radius: var(--cc-radius-md); + + &__inner { + height: 100%; + border-radius: var(--cc-radius-md); + transition: + transform var(--cc-transition), + box-shadow var(--cc-transition), + border-color var(--cc-transition); + } + + &:hover &__inner, + &:focus-visible &__inner { + transform: translateY(-2px); + box-shadow: var(--cc-shadow-1); + border-color: var(--cc-primary); + } + + &__icon { + color: var(--cc-accent); + margin-bottom: var(--cc-space-3); + } + + &__title { + margin: 0; + font-size: var(--cc-fs-h3); + font-weight: 600; + color: var(--cc-ink); + line-height: var(--cc-lh-tight); + } + + &__accroche { + margin: var(--cc-space-3) 0 0; + color: var(--cc-ink-soft); + font-size: 0.9rem; + line-height: 1.5; + } + + &__cta { + display: inline-flex; + align-items: center; + gap: var(--cc-space-1); + margin-top: var(--cc-space-4); + color: var(--cc-primary); + font-size: var(--cc-fs-small); + font-weight: 600; + + mat-icon { + font-size: 1.1rem; + width: 1.1rem; + height: 1.1rem; + } + } +} diff --git a/frontend/src/app/features/claude-code-setup/claude-code-setup.component.ts b/frontend/src/app/features/claude-code-setup/claude-code-setup.component.ts new file mode 100644 index 0000000..70207cf --- /dev/null +++ b/frontend/src/app/features/claude-code-setup/claude-code-setup.component.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; + +import { CLAUDE_CODE_TOPICS } from '../../core/data/claude-code-topics'; +import { ClaudeCodeTopic } from '../../core/models/claude-code-topic.model'; +import { + BreadcrumbComponent, + BreadcrumbItem, +} from '../../shared/components/breadcrumb/breadcrumb.component'; + +/** + * Page pédagogique « Mettre en place Claude Code ». + * + * Hub des quatre sujets (fichiers markdown, hooks, slash-commands & skills, + * settings & MCP) : une carte par sujet, cliquable vers sa page de détail + * (`/claude-code-setup/:sujet`). Le contenu provient de la source unique + * CLAUDE_CODE_TOPICS. + */ +@Component({ + selector: 'app-claude-code-setup', + imports: [RouterLink, BreadcrumbComponent, MatCardModule, MatIconModule], + templateUrl: './claude-code-setup.component.html', + styleUrl: './claude-code-setup.component.scss', +}) +export class ClaudeCodeSetupComponent { + /** Fil d'Ariane : Accueil › Claude Code (page courante). */ + readonly breadcrumb: BreadcrumbItem[] = [ + { label: 'Accueil', link: '/' }, + { label: 'Claude Code' }, + ]; + + /** Les quatre sujets de mise en place, dans l'ordre pédagogique. */ + readonly topics: ClaudeCodeTopic[] = CLAUDE_CODE_TOPICS; +}