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,39 @@
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, readFileSync, writeFileSync, rmSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { preTrustProject } from '../src/core/claude-trust.js';
describe('preTrustProject', () => {
let dir: string;
let cfg: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'arb-trust-'));
cfg = join(dir, '.claude.json');
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
it('crée le fichier sil est absent et marque le worktree approuvé', () => {
expect(preTrustProject('/home/u/proj-wt-x', cfg)).toBe(true);
const data = JSON.parse(readFileSync(cfg, 'utf8'));
expect(data.projects['/home/u/proj-wt-x'].hasTrustDialogAccepted).toBe(true);
});
it('préserve la config existante (autres clés et projets)', () => {
writeFileSync(cfg, JSON.stringify({ theme: 'dark', projects: { '/other': { foo: 1 } } }));
expect(preTrustProject('/home/u/new', cfg)).toBe(true);
const data = JSON.parse(readFileSync(cfg, 'utf8'));
expect(data.theme).toBe('dark');
expect(data.projects['/other']).toEqual({ foo: 1 });
expect(data.projects['/home/u/new'].hasTrustDialogAccepted).toBe(true);
});
it('fichier corrompu → abandon SANS écraser (retourne false)', () => {
writeFileSync(cfg, '{ this is not json');
expect(preTrustProject('/home/u/x', cfg)).toBe(false);
expect(readFileSync(cfg, 'utf8')).toBe('{ this is not json'); // intact
expect(existsSync(`${cfg}.arb-tmp`)).toBe(false);
});
});