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).
This commit is contained in:
56
packages/server/src/routes/repos.ts
Normal file
56
packages/server/src/routes/repos.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import type { CreateRepoRequest, RepoResponse, ReposListResponse, UpdateRepoRequest } from '@arboretum/shared';
|
||||
import type { WorktreeManager } from '../core/worktree-manager.js';
|
||||
|
||||
/** Mappe une erreur du manager (statusCode + code) vers une réponse REST normalisée. */
|
||||
export function sendManagerError(reply: FastifyReply, err: unknown): FastifyReply {
|
||||
const e = err as { statusCode?: number; code?: string; message?: string };
|
||||
return reply.status(e.statusCode ?? 500).send({ error: { code: e.code ?? 'INTERNAL', message: e.message ?? 'Internal error' } });
|
||||
}
|
||||
|
||||
export function registerRepoRoutes(app: FastifyInstance, wt: WorktreeManager): void {
|
||||
app.get('/api/v1/repos', async (): Promise<ReposListResponse> => ({ repos: await wt.listRepos() }));
|
||||
|
||||
app.post('/api/v1/repos', async (req, reply) => {
|
||||
const body = req.body as Partial<CreateRepoRequest> | null;
|
||||
if (!body || typeof body.path !== 'string') {
|
||||
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'path (absolute) is required' } });
|
||||
}
|
||||
try {
|
||||
const repo = await wt.addRepo({
|
||||
path: body.path,
|
||||
...(body.label !== undefined ? { label: body.label } : {}),
|
||||
...(body.postCreateHooks !== undefined ? { postCreateHooks: body.postCreateHooks } : {}),
|
||||
...(body.preTrust !== undefined ? { preTrust: body.preTrust } : {}),
|
||||
});
|
||||
const res: RepoResponse = { repo };
|
||||
return reply.status(201).send(res);
|
||||
} catch (err) {
|
||||
return sendManagerError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/v1/repos/:id', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const body = (req.body as Partial<UpdateRepoRequest> | null) ?? {};
|
||||
try {
|
||||
const repo = await wt.updateRepo(id, {
|
||||
...(body.label !== undefined ? { label: body.label } : {}),
|
||||
...(body.postCreateHooks !== undefined ? { postCreateHooks: body.postCreateHooks } : {}),
|
||||
...(body.preTrust !== undefined ? { preTrust: body.preTrust } : {}),
|
||||
});
|
||||
const res: RepoResponse = { repo };
|
||||
return reply.send(res);
|
||||
} catch (err) {
|
||||
return sendManagerError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/v1/repos/:id', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
if (!wt.removeRepo(id)) {
|
||||
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No repo with this id' } });
|
||||
}
|
||||
return reply.send({ ok: true });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user