Commit Graph

11 Commits

Author SHA1 Message Date
08695a707d feat(p12a): services git distants (PAT/app_password) + clone HTTPS
Modèle de données :
- migration #11 git_credentials (secrets chiffrés SecretBox : secret_encrypted ; colonnes ssh/oauth posées pour P12b/P12c) ; #12 repos ALTER remote_url/git_service/credential_id (pas de FK)
- types partagés api.ts (GitCredentialSummary sans secret + hasSecret/secretLast4, CRUD, RemoteRepoSummary, Clone*) ; protocole additif : topic 'clones' + message clone_update (CloneOperation)

Backend :
- core/git-credentials.ts (GitCredentialsManager(db, box)) : CRUD chiffré, test() (GET /user), getSecret()/authContext() internes, NULLification de repos.credential_id à la suppression
- core/git-clients/ (github/gitlab/gitea) via fetch, sans dépendance : verify()+listRepos() paginés, erreurs typées AUTH_FAILED/RATE_LIMITED/UNREACHABLE, SSRF base_url http(s)
- core/git-auth.ts : withGitAuth (GIT_ASKPASS éphémère 0o700, secret par env, GIT_TERMINAL_PROMPT=0, jamais dans l'URL/.git/config, nettoyage finally)
- core/git.ts cloneRepo (spawn git clone --progress, parse progression, timeout)
- core/clone-manager.ts (EventEmitter) : clone async, dest confiné sous scanRoots + non existant, auto-enregistrement via addRepo + métadonnées de provenance, nettoyage du clone partiel, events topic 'clones'
- routes/git-connections.ts (CRUD + /test + /:id/repos + POST /repos/clone 202 + GET /repos/clone/:id) ; app.ts câble box→GitCredentialsManager + CloneManager→gateway ; gateway relaie 'clones'

Frontend :
- ws-client subscribeClones ; stores git-connections + clone (suivi WS)
- components/settings/GitConnectionsSection (liste + formulaire pat/app_password, secret jamais ré-affiché) inséré dans SettingsView ; CloneRepoModal (connexion → repos distants paginés → dest scanRoots[0] → barre de progression WS → redirection) ; bouton « Cloner » dans DashboardView ; i18n EN+FR

Tests : git-credentials (round-trip SecretBox, résumé sans secret, NULLification) ; acceptance-p12.mjs (clone bare local file:// → clone_update done + repo enregistré + secret ABSENT de l'API, de la DB et du .git/config)

Sous-phases restantes : P12b (SSH), P12c (OAuth device flow).
2026-06-27 14:27:18 +02:00
c8dd539571 feat(p10): archivage automatique des sessions terminées (rétention configurable)
- DB: migration #10 (sessions.archived_at + index), helpers archive/unarchive/archiveExpired (soft-archive, jamais de DELETE)
- core/retention-settings.ts: session_retention_days (défaut 30, 0=jamais, 1-3650) + session_purge_days (off)
- core/session-archive.ts: scheduler start/stop/sweep (calqué DiscoveryService), câblé dans runDaemon
- pty-manager.list({includeArchived}) + SessionSummary.archived + emitHistoricalUpdate
- routes: GET /sessions?includeArchived, POST/DELETE /sessions/:id/archive, POST /sessions/archive-now
- protocole additif: message WS session_archived (relayé par la gateway, abonnés 'sessions')
- settings: retentionDays/purgeDays exposés et validés dans PATCH /settings
- web: store sessions (showArchived + archive/unarchive), SessionsListView (toggle/badge/actions), SettingsView (slider rétention), i18n EN+FR
- tests: retention-settings + session-archive (vitest) + settings-routes étendu ; acceptance-p10.mjs (sweep, event WS, resume d'une session archivée → 201, rétention=0)
2026-06-27 13:31:28 +02:00
dacff2b98f feat(sessions): masquer les sessions externes + terminal plein écran
Masquage persistant des sessions Claude découvertes (lancées hors Arboretum)
qui polluent la liste — calqué sur repos.hidden :
- table hidden_sessions (#9) clé claudeSessionId ; helpers DB
- DiscoveryService pose le champ additif SessionSummary.hidden (resume/fork
  restent possibles, survit à un re-scan)
- routes POST/DELETE /sessions/:id/hide, POST /sessions/hide-discovered
  (masse, un clic), GET /sessions?includeHidden=true
- UI : bouton Masquer par ligne, « Masquer l'historique externe », toggle
  « Afficher les masquées », badge masquée ; store + i18n EN/FR + audit

Terminal plein écran (corrige le sizing) :
- SessionView root s'étire (flex-1) → xterm calcule les bonnes dimensions
- refit robuste (requestAnimationFrame + après resync), h-full sur le wrapper
- bouton plein écran (API Fullscreen), tabbar mobile masquée en fullbleed

DialogPrompt : options en liste verticale pleine largeur (plus de troncature
ni de chevauchement sur les libellés longs / multi-lignes).

Doc : centre d'aide (sessions), README EN/FR, site (showcase), CLAUDE.md.
Tests : discovery (hidden) + route hide/unhide/hide-discovered.
2026-06-22 16:02:27 +02:00
c6ea9f7930 feat(groups): session Claude unique par groupe via --add-dir (P6)
All checks were successful
Deploy site (production) / build-and-deploy (push) Successful in 21s
Un groupe lance désormais UNE seule session Claude couvrant tous ses
repos/worktrees (--add-dir) au lieu d'une session par repo, pour
travailler en simultané dans une conversation partagée. Le transport WS,
le flow control et l'attach sont inchangés (toujours 1 session, 1 channel).

- shared: SessionSummary.addedDirs/groupId (champs additifs optionnels) ;
  CreateGroupSessionRequest / GroupSessionResponse.
- server: --add-dir dans claude-launcher ; spawn({addDirs,groupId}) +
  persistance (migration #8 : sessions.added_dirs/group_id) ;
  POST /api/v1/groups/:id/session (résolution des dirs cote serveur) ;
  resume re-relie les dirs ; deleteGroup nullifie group_id.
- web: GroupSessionModal (modes feature / checkouts principaux) ;
  createGroupFeature/createGroupSession ; badge « groupe · N depots » ;
  sessionsInGroup dedupliquee.
- site: textes EN/FR « une seule session Claude » + diagramme hub.
- tests: claude-launcher, pty-manager (groupe), group-manager ;
  acceptation P5 etendue (session de groupe multi-repo). 272 tests verts.
2026-06-22 10:06:23 +02:00
33295a7aaf feat: clean install (Gitea en dur, scan off) + durcissement sécurité entreprise
Gitea
- lien « Code source » hardcodé (REPO_SOURCE_URL) vers le dépôt, toujours visible
- retrait complet du réglage configurable (api.ts, route+store settings, SettingsView, i18n, help, tests)

Découverte des dépôts
- aucune racine de scan par défaut → pas de scan au premier démarrage (clean install)
- corrige la découverte des dépôts à l'INTÉRIEUR d'une racine qui est elle-même un repo
  (depth 0 = conteneur de scan, on descend ; depth > 0 = feuille)

Sécurité « enterprise-deployable »
- en-têtes HTTP durcis (CSP, X-Frame-Options, nosniff, Referrer-Policy, HSTS conditionnel, no-store API), header Server retiré
- permissions DB 0o600 / dossier de données 0o700 ; error handler sanitisé ; garde Content-Type sur les mutations
- chiffrement au repos AES-256-GCM des secrets (server_secret, clé privée VAPID) via SecretBox
- journal d'audit (migration #7) + endpoint /audit-logs + RGPD export/effacement + UI Réglages
- SECURITY.md, docs/ENTERPRISE_DEPLOYMENT.md, sections README EN/FR, SBOM CycloneDX en CI

CI
- pack-smoke packe depuis le contexte du package (cd packages/server) au lieu de « npm pack -w » :
  corrige l'embarquement de @arboretum/shared dans le tarball

Purge des données personnelles du dépôt public
- suppression de spikes/s4-discovery/result.json, reformulation du VERDICT
- chemin de test générique, anonymisation des 5 fixtures de dialogues
2026-06-18 16:44:25 +02:00
fe2a3e66c7 feat: découverte automatique des dépôts git (scan + montrer/cacher)
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).
2026-06-18 14:45:12 +02:00
e38b3a5d5e feat: groupes de travail (P5)
Ajoute la notion de groupe — collection nommée de repos (membership légère
persistée, many-to-many) — pour piloter plusieurs repos en simultané :

- DB : migration #5 (tables groups + group_repos, FK ON DELETE CASCADE).
- GroupManager (synchrone, EventEmitter) + routes REST /api/v1/groups ;
  ne persiste que la membership, worktrees/sessions filtrés par repoId côté client.
- Protocole : GroupSummary, DTOs, topic WS « groups », events group_update/removed
  (additifs, PROTOCOL_VERSION inchangé).
- Web : store groups, GroupsListView, GroupView (réutilise RepoSection),
  grille multi-terminaux (TerminalGrid/TerminalCell), action « feature cross-repo »
  (orchestration client, tolérante aux échecs partiels), routes + i18n EN/FR.
- Tests : group-manager.test.ts + extension protocol.test.ts ; acceptance-p5.mjs.
2026-06-18 10:03:19 +02:00
664e2b583c P4-B: notifications Web Push (VAPID) sur passage en waiting
- db: migration id=4 `push_subscriptions` (liées au token d'auth) ; clés VAPID en settings
- PushService: bootstrap idempotent des clés VAPID, subscribe/unsubscribe/count,
  notify() best-effort (purge des abonnements 410/404 Gone) ; sender injectable pour les tests
- routes/push.ts: GET vapid-public-key, POST subscribe/unsubscribe/test (toutes sous auth globale)
- pty-manager: déclencheur push sur FRONT MONTANT vers waiting, débouncé 1500ms et annulable
  (faux positif ignoré) ; câblage app/index/config (--vapid-contact)
- shared/api.ts: types VapidKeyResponse / PushSubscribeRequest / PushUnsubscribeRequest
- deps: web-push (+ @types/web-push) côté serveur
- tests: push-service (idempotence, UPSERT, 410-purge, payload) + trigger pty-manager (169 verts)
2026-06-15 12:13:56 +02:00
08320e166e 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).
2026-06-12 18:31:06 +02:00
92c48f0978 P2: découverte & reprise des sessions Claude
Arboretum découvre désormais toutes les sessions Claude de la machine
(scan ~/.claude/projects + registre ~/.claude/sessions), distingue
vivantes/mortes par pid+procStart, et permet de reprendre une morte
(--resume dans son cwd d'origine) ou forker une vivante sans la corrompre.

- shared: SessionSummary enrichi (source, claudeSessionId, pid, resumable,
  attachable, registryStatus) — additif, PROTOCOL_VERSION inchangé ;
  types REST resume/fork.
- db: migration id:2 (claude_session_id, resumed_from).
- core: jsonl-discovery (parseur tolérant, scan asynchrone non bloquant),
  session-registry (vivacité pid+procStart), discovery-service (cache +
  refresh périodique + diff/broadcast), pty-manager (resume/fork + capture
  du claudeSessionId via le registre).
- routes: /sessions/:id/resume (garde-fou 409 anti-corruption sur session
  vivante) et /fork ; GET fusionné managées + découvertes ; relais WS.
- web: badges managed/discovered + busy/idle/waiting, actions conditionnelles
  (Open/Observe/Kill vs Fork/View vs Resume/Fork), vue read-only des sessions
  externes, i18n EN/FR.
- tests: jsonl-discovery, session-registry, discovery-service + resume/fork
  (130 verts) ; acceptation E2E acceptance-p2.mjs (sans quota) ALL GREEN.

Conforme aux verdicts S1 (resume dans cwd d'origine, vivacité pid+procStart)
et S4 (munge cwd, parseur tête+queue, priorité de titre).
2026-06-12 18:20:21 +02:00
4768b606e4 P1 spine: monorepo, shared WS protocol, server daemon
- npm workspaces (shared / server / web), TS strict, project refs
- @arboretum/shared: multiplexed WS protocol (JSON control + binary
  output frames: 1B type + u32le channel), flow-control constants
  (ACK 256K, HIGH 384K, LOW 128K, lagging 2M), REST types
- git-arboretum server: Fastify 5 + node:sqlite (single native dep:
  node-pty prebuilt), token auth (sha256 at rest, HMAC cookie, global
  login rate limit + backoff), strict Origin check on /api and /ws,
  PtyManager (2MiB ring with monotonic offset, resync replay = reset +
  256KiB tail, pause/resume only when ALL interactive clients exceed
  HIGH, observers never throttle, lagging clients resync), WS gateway
  (attach/stdin/resize/ack, heartbeat 30s), SIGTERM→SIGKILL 5s grace
- CLI: arboretum [--port 7317] [--bind 127.0.0.1] — non-loopback bind
  requires an explicit safety flag
- Smoke-tested: login/401/403-origin/spawn bash/kill/grace-SIGKILL all
  green
2026-06-11 22:04:09 +02:00