Scan borné du système de fichiers (racines configurables, défaut home ; profondeur/nombre/timeout bornés ; symlinks non suivis ; exclusions node_modules/dotdirs) qui auto-enregistre les nouveaux dépôts. Insertion atomique ON CONFLICT DO NOTHING (idempotence + anti-résurrection d'un dépôt masqué + anti-course). Scan au démarrage + bouton manuel + re-scan périodique (RepoDiscoveryService, démarré dans runDaemon). Colonne repos.hidden : masquer = conservé en DB mais exclu du dashboard et jamais ré-ajouté ; supprimer = re-découvrable. UI : bouton œil par dépôt + bascule afficher-les-masqués sur le dashboard, section Découverte dans les Réglages (racines + intervalle, allow-list stricte). Robustesse : listAllWorktrees tolère l'échec git par dépôt ; flag --no-discover (escape hatch + hermétisme des acceptations).
70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
import type { CreateRepoRequest, DiscoverReposResponse, RepoResponse, ReposListResponse, UpdateRepoRequest } from '@arboretum/shared';
|
|
import type { WorktreeManager } from '../core/worktree-manager.js';
|
|
import type { Db } from '../db/index.js';
|
|
import { readScanRoots } from '../core/scan-settings.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, db: Db): void {
|
|
app.get('/api/v1/repos', async (): Promise<ReposListResponse> => ({ repos: await wt.listRepos() }));
|
|
|
|
// Scan manuel : découvre et auto-enregistre les repos sous les racines configurées (settings).
|
|
app.post('/api/v1/repos/discover', async (_req, reply) => {
|
|
try {
|
|
const res: DiscoverReposResponse = await wt.discoverRepos({ roots: readScanRoots(db) });
|
|
return reply.send(res);
|
|
} catch (err) {
|
|
return sendManagerError(reply, err);
|
|
}
|
|
});
|
|
|
|
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 } : {}),
|
|
...(typeof body.hidden === 'boolean' ? { hidden: body.hidden } : {}),
|
|
});
|
|
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 });
|
|
});
|
|
}
|