feat(p7): moteur git détaillé, API fichiers et watcher FS temps réel

Fondations de la refonte « vrai IDE IA worktree » (chantier P7→P12).

- git.ts : listChanges (status --porcelain=v2 -z + --numstat fusionnés),
  fileDiff (diff unifié borné, refus binaire, untracked via --no-index),
  stage/unstage/restore/clean, commitStaged, amendCommit (refus si poussé),
  fetchRemote, pull (ff-only|rebase), lastCommit, resolveGitDir,
  isSafeRelativePath ; worktreeStatus enrichi (compteurs staged/unstaged/
  conflict + dernier commit), champs additifs WorktreeGitStatus.
- core/fs-watcher.ts : FsWatcherService (chokidar, refcount + pool LRU borné,
  ignore .git sauf HEAD/index, débounce) → invalide factsCache, rediffuse
  worktree_update et émet worktree_changes.
- WorktreeManager : getWorktreeChanges/getFileDiff/stage/unstage/discard/
  fetch/pull/watch/unwatch/assertPathInWorktree ; commitWorktree { mode, amend }.
- routes/git.ts (changes/diff/stage/unstage/discard/fetch/pull) et
  routes/files.ts (GET/PUT contenu fichier, bornage strict au worktree) ;
  fs/list?includeFiles=1 (flag isFile).
- protocole ADDITIF (PROTOCOL_VERSION inchangé) : messages WS watch/unwatch
  (validés dans parseClientMessage) + worktree_changes poussé ciblé par la
  gateway ; front wsClient.watchWorktree + lib/git-api.ts + api.put.
- tests git/fs-watcher/protocole + acceptance-p7.mjs (ALL GREEN).
- dépendance : chokidar (serveur).
This commit is contained in:
2026-06-27 12:15:00 +02:00
parent 062bb64d41
commit be43911dc0
21 changed files with 1610 additions and 21 deletions

View File

@@ -18,7 +18,7 @@ import type { FsEntry, FsListResponse } from '@arboretum/shared';
*/
export function registerFsRoutes(app: FastifyInstance): void {
app.get('/api/v1/fs/list', async (req, reply) => {
const q = req.query as { path?: string; markRepos?: string; showHidden?: string };
const q = req.query as { path?: string; markRepos?: string; showHidden?: string; includeFiles?: string };
const raw = q.path && q.path.length > 0 ? q.path : homedir();
if (!raw.startsWith('/')) {
return reply.status(400).send({ error: { code: 'BAD_PATH', message: 'path must be absolute' } });
@@ -41,6 +41,7 @@ export function registerFsRoutes(app: FastifyInstance): void {
const markRepos = q.markRepos === '1' || q.markRepos === 'true';
const showHidden = q.showHidden === '1' || q.showHidden === 'true';
const includeFiles = q.includeFiles === '1' || q.includeFiles === 'true';
let dirents;
try {
@@ -63,13 +64,24 @@ export function registerFsRoutes(app: FastifyInstance): void {
isDir = false;
}
}
if (!isDir) continue;
const full = join(abs, d.name);
if (!isDir) {
// En mode includeFiles, on remonte aussi les fichiers (arbre de l'IDE worktree).
if (!includeFiles) continue;
entries.push({ name: d.name, path: full, isFile: true });
continue;
}
const entry: FsEntry = { name: d.name, path: full };
if (markRepos && existsSync(join(full, '.git'))) entry.isRepo = true;
entries.push(entry);
}
entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
// Dossiers d'abord, puis fichiers ; tri insensible à la casse dans chaque groupe.
entries.sort((a, b) => {
const af = a.isFile ? 1 : 0;
const bf = b.isFile ? 1 : 0;
if (af !== bf) return af - bf;
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
const res: FsListResponse = {
path: abs,