P3-A: worktrees multi-repo & cycle de vie (backend)

Enregistrement de repos et gestion de leurs worktrees git, source de vérité
= git (worktrees dérivés + cache court par repo), corrélation worktree ↔
sessions par cwd, mutations sérialisées par repo.

- shared: RepoSummary, PostCreateHook, WorktreeSummary, WorktreeGitStatus ;
  messages WS repo_update/worktree_update/*_removed ; topic sub 'worktrees'
  (+ parseClientMessage) ; DTOs REST repos/worktrees.
- db: migration id:3 (table repos).
- core/git.ts: couche git sûre (execFile, jamais de shell, -- avant chemins,
  GIT_OPTIONAL_LOCKS=0), parseWorktreePorcelain, list/add/remove/prune/status/
  ahead-behind, validation branche + chemin.
- core/claude-trust.ts: pré-trust atomique de ~/.claude.json (spike S3).
- core/worktree-manager.ts: repos CRUD, create (worktree add + pré-trust +
  hooks post-create + startSession optionnel), adopt, delete (garde-fous 409
  dirty / 400 main / 409 session live), prune ; events.
- routes/repos.ts + routes/worktrees.ts, câblage app.ts, gateway topic worktrees.
- tests: git (repos tmp réels), claude-trust, worktree-manager (146 verts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:31:06 +02:00
parent fb175bc7c5
commit acd920ebcd
16 changed files with 1208 additions and 10 deletions

View File

@@ -0,0 +1,41 @@
// Pré-trust programmatique (spike S3) : écrit projects["<worktree>"].hasTrustDialogAccepted = true
// dans ~/.claude.json pour éviter le dialogue Trust au 1er lancement de `claude` dans un worktree neuf.
// Écriture ATOMIQUE (tmp + rename). Le chemin est injectable pour les tests (jamais le vrai HOME).
import { readFileSync, writeFileSync, renameSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
export function claudeConfigPath(): string {
return join(homedir(), '.claude.json');
}
/**
* Marque un worktree comme déjà approuvé. Tolérant : fichier absent → on le crée ; fichier corrompu
* → on ABANDONNE le pré-trust (on ne l'écrase pas, pour ne pas perdre la config existante). Retourne
* true si le pré-trust a été écrit, false sinon (l'appelant continue : ce n'est pas bloquant).
*/
export function preTrustProject(worktreePath: string, filePath = claudeConfigPath()): boolean {
let data: Record<string, unknown> = {};
try {
const parsed = JSON.parse(readFileSync(filePath, 'utf8')) as unknown;
if (typeof parsed !== 'object' || parsed === null) return false; // contenu inattendu : on n'écrase pas
data = parsed as Record<string, unknown>;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') return false; // corrompu/illisible : abandon
data = {}; // fichier absent : on part d'un objet vide
}
const projects =
typeof data.projects === 'object' && data.projects !== null
? (data.projects as Record<string, Record<string, unknown>>)
: {};
projects[worktreePath] = { ...(projects[worktreePath] ?? {}), hasTrustDialogAccepted: true };
data.projects = projects;
try {
const tmp = `${filePath}.arb-tmp`;
writeFileSync(tmp, JSON.stringify(data, null, 2));
renameSync(tmp, filePath); // rename atomique : jamais de fichier à moitié écrit
return true;
} catch {
return false;
}
}