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:
@@ -5,11 +5,14 @@ import {
|
||||
PROTOCOL_VERSION,
|
||||
encodeBinaryFrame,
|
||||
parseClientMessage,
|
||||
type RepoSummary,
|
||||
type ServerMessage,
|
||||
type SessionSummary,
|
||||
type WorktreeSummary,
|
||||
} from '@arboretum/shared';
|
||||
import type { ClientBinding, PtyManager } from '../core/pty-manager.js';
|
||||
import type { DiscoveryService } from '../core/discovery-service.js';
|
||||
import type { WorktreeManager } from '../core/worktree-manager.js';
|
||||
|
||||
const HEARTBEAT_MS = 30_000;
|
||||
|
||||
@@ -22,6 +25,7 @@ export function registerWsGateway(
|
||||
app: FastifyInstance,
|
||||
manager: PtyManager,
|
||||
discovery: DiscoveryService,
|
||||
worktrees: WorktreeManager,
|
||||
serverVersion: string,
|
||||
): void {
|
||||
app.get('/ws', { websocket: true }, (socket: WebSocket, req) => {
|
||||
@@ -30,6 +34,7 @@ export function registerWsGateway(
|
||||
let nextChannel = 1;
|
||||
let helloDone = false;
|
||||
let subscribedSessions = false;
|
||||
let subscribedWorktrees = false;
|
||||
let alive = true;
|
||||
|
||||
const send = (msg: ServerMessage): void => {
|
||||
@@ -49,9 +54,25 @@ export function registerWsGateway(
|
||||
const onDiscoveryUpdate = (session: SessionSummary): void => {
|
||||
if (subscribedSessions) send({ type: 'session_update', session });
|
||||
};
|
||||
const onRepoUpdate = (repo: RepoSummary): void => {
|
||||
if (subscribedWorktrees) send({ type: 'repo_update', repo });
|
||||
};
|
||||
const onRepoRemoved = (repoId: string): void => {
|
||||
if (subscribedWorktrees) send({ type: 'repo_removed', repoId });
|
||||
};
|
||||
const onWorktreeUpdate = (e: { repoId: string; worktree: WorktreeSummary }): void => {
|
||||
if (subscribedWorktrees) send({ type: 'worktree_update', ...e });
|
||||
};
|
||||
const onWorktreeRemoved = (e: { repoId: string; path: string }): void => {
|
||||
if (subscribedWorktrees) send({ type: 'worktree_removed', ...e });
|
||||
};
|
||||
manager.on('session_update', onSessionUpdate);
|
||||
manager.on('session_exit', onSessionExit);
|
||||
discovery.on('discovery_update', onDiscoveryUpdate);
|
||||
worktrees.on('repo_update', onRepoUpdate);
|
||||
worktrees.on('repo_removed', onRepoRemoved);
|
||||
worktrees.on('worktree_update', onWorktreeUpdate);
|
||||
worktrees.on('worktree_removed', onWorktreeRemoved);
|
||||
|
||||
const heartbeat = setInterval(() => {
|
||||
if (!alive) {
|
||||
@@ -89,6 +110,7 @@ export function registerWsGateway(
|
||||
}
|
||||
case 'sub': {
|
||||
subscribedSessions = msg.topics.includes('sessions');
|
||||
subscribedWorktrees = msg.topics.includes('worktrees');
|
||||
return;
|
||||
}
|
||||
case 'attach': {
|
||||
@@ -160,6 +182,10 @@ export function registerWsGateway(
|
||||
manager.off('session_update', onSessionUpdate);
|
||||
manager.off('session_exit', onSessionExit);
|
||||
discovery.off('discovery_update', onDiscoveryUpdate);
|
||||
worktrees.off('repo_update', onRepoUpdate);
|
||||
worktrees.off('repo_removed', onRepoRemoved);
|
||||
worktrees.off('worktree_update', onWorktreeUpdate);
|
||||
worktrees.off('worktree_removed', onWorktreeRemoved);
|
||||
for (const [, st] of channels) manager.detach(st.sessionId, st.binding);
|
||||
channels.clear();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user