Aller au contenu

Projets et workspaces — modèle et API

Audience : développeur qui travaille sur le CRUD projets/workspaces ou intègre l’arborescence .arka-deck/.


Workspace (groupe — uniquement métadonnées)
├── Project A (= dossier disque + marker .arka-deck/project/marker.json)
├── Project B
└── Project C
  • Workspace : pure métadonnée. Persisté dans l’index global utilisateur.
  • Project : un dossier disque sur la machine. Le marker disque (<projectPath>/.arka-deck/project/marker.json) est la source de vérité.
  • Index global : <arkaHome>/index/projects.json + workspaces.json = vue agrégée reconstructible.

Path par défaut : ~/.arka-deck/ (override via env ARKA_DECK_HOME).


interface Workspace {
id: string; // UUID v4
name: string; // unique dans l'index, ≤ 80 chars
description: string;
createdAt: string; // ISO 8601
}

Persisté uniquement dans <arkaHome>/index/workspaces.json.

interface Project {
id: string; // UUID v4
name: string; // libellé affichable, ≤ 80 chars
description: string;
path: string; // chemin absolu du dossier projet, IMMUABLE
workspaceId: string;
createdAt: string;
lastUsedAt: string; // mis à jour à chaque ouverture
}

Persisté deux fois :

  • Marker disque : <projectPath>/.arka-deck/project/marker.json ← source de vérité
  • Index global : <arkaHome>/index/projects.json ← reconstructible depuis les markers

En cas de conflit (le marker dit X, l’index dit Y) → le marker gagne (cf. findByCwd qui réimporte automatiquement l’index si désynchronisé).


PortRôle
ProjectStoreLit/écrit le marker disque
ProjectIndexStoreLit/écrit l’index global utilisateur
WorkspaceStoreLit/écrit l’index global workspaces
InstalledItemsStoreLit la liste des items installés (agents/hooks/skills) — utilisé par purge
ChatSessionStoreLit/supprime les sessions chat liées (utilisé par forget/purge)
FilesystemWrapper FS (read/write/exists/rm/readdir/…)

ForProjects :

  • create({name, path, description, workspaceId}) — crée marker + index
  • list() — tous les projets, ordre lastUsedAt desc
  • listByWorkspace(workspaceId) — filtre
  • getById(id) — lookup index
  • forget(id) — retire de l’index, marker disque préservé
  • update({id, name, description, workspaceId}) — patch (path immuable)
  • touchLastUsed(id) — met à jour lastUsedAt
  • findByCwd(cwd) — détection auto au boot
  • purge(id) — forget + cleanup .arka-deck/ + agents installés + sessions chat

ForWorkspaces :

  • create({name, description}) — name doit être unique
  • list() — tous, du plus récent au plus ancien
  • getById(id)
  • update({id, name, description}) — re-vérifie unicité
  • delete(id) — refuse si projets rattachés (WorkspaceNotEmptyError)
AdapterFichier
FsProjectStoreadapters/outbound/filesystem/fs-project-store.ts
FsProjectIndexStoreadapters/outbound/filesystem/fs-project-index-store.ts
FsWorkspaceStoreadapters/outbound/filesystem/fs-workspace-store.ts

~/.arka-deck/ ← arkaHome (override par env ARKA_DECK_HOME)
index/
workspaces.json ← array de Workspace
projects.json ← array de Project (vue index)
preferences/
user.json ← prefs user (langue, etc.)
security.json ← allowlist paths autorisés
cache/
catalogue/... ← cache HYOS
debrief/... ← cache atoms HCM mémoire
providers/
providers.db ← SQLite instances providers
secrets/
secrets.key ← clé maître AES-256-GCM (mode 600)
chat/
chat.db ← SQLite sessions/messages chat
<projectPath>/ ← n'importe où sur disque
.arka-deck/
project/
marker.json ← source de vérité projet
agents/installed.json
hooks/installed.json
skills/installed.json
memory/ ← cf. addons-firstparty/memory-local/
.claude/
agents/<slug>/ ← agent recruté (profil HYOS)
settings.json ← settings Claude Code (jamais touché par arka-deck)

ForProjects.create({ name, path, description, workspaceId })
[Vérifie path = directory disque]
[Vérifie marker absent (anti-doublon)]
[Vérifie workspaceId existe]
[Filesystem.mkdir <path>/.arka-deck/project/, recursive]
[ProjectStore.save → marker.json]
[ProjectIndexStore.save → ajout à projects.json]
EventBus → 'project.created' (async)
ForProjects.findByCwd(cwd)
[ProjectStore.exists(cwd) ? = lookup marker]
├─ Marker absent → null (pas un projet arka-deck)
└─ Marker présent
[Lit marker → Project]
[ProjectIndexStore.getById → présent ?]
├─ Oui → renvoie projet
└─ Non → réimporte (l'index est reconstructible) + renvoie projet

Cas typique : un utilisateur fait git clone d’un projet existant sur une autre machine. arka-deck détecte le marker au prochain boot et l’ajoute à son index.

ModeIndexMarker disque.arka-deck/Agents .claude/agents/Sessions chat SQLite
forgetretirépréservépréservépréservéssupprimées
purgeretirésupprimésupprimésupprimés (uniquement les arka-deck installés)supprimées

purge ne touche jamais aux fichiers .claude/ non-arka-deck (settings user, agents non-arka). Seulement aux artefacts dont arka-deck a la trace dans installed.json.

Code : core/use-cases/projects/build-for-projects.ts:purgeProjectUseCase.

Le serveur web vérifie qu’un path de création projet est dans l’allowlist utilisateur avant d’écrire le marker. Allowlist par défaut : ~, ~/Documents, ~/Projects, etc. — configurable dans ~/.arka-deck/preferences/security.json.

Refus → HTTP 403 PATH_NOT_ALLOWED avec body listant les emplacements autorisés.


forWorkspaces.create({ name: 'Arkalabs', description: 'Workspace principal' });
  • name doit être unique global (pas par utilisateur — single-user)
  • Levée WorkspaceNameAlreadyExistsError sinon
await forWorkspaces.delete(workspaceId);
// → throw WorkspaceNotEmptyError si projets rattachés

Pas de cascade automatique. L’utilisateur doit explicitement déplacer ou supprimer ses projets avant.

L’UI WorkspaceDeleteTunnel :

  • Affiche la liste des projets rattachés si non-vide
  • Désactive le bouton “Supprimer” tant que attachedProjects.length > 0
  • Propose un raccourci “Aller aux projets” pour gérer

GET /api/workspaces → liste
GET /api/workspaces/:id → détail
POST /api/workspaces → crée
PUT /api/workspaces/:id → patch
DELETE /api/workspaces/:id → supprime (refuse si non-vide)
GET /api/workspaces/:id/projects → projets de ce workspace
GET /api/projects → liste tous projets
GET /api/projects/:id → détail
GET /api/projects/find-by-cwd?path=... → détection auto par marker
POST /api/projects → crée (vérifie allowlist + workspace existe)
PUT /api/projects/:id → patch (path immuable)
DELETE /api/projects/:id → forget (204)
DELETE /api/projects/:id?purge=true → purge complet, renvoie {deletedPaths}
POST /api/projects/:id/touch → touchLastUsed (200 + DTO)

TunnelStepsPérimètre
WorkspaceTunnel3 (nom → description → confirm)Création
WorkspaceEditTunnel3Édition
WorkspaceDeleteTunnel2 (vérification non-vide → confirm)Suppression bloquante si projets
ProjectTunnel5 (path → name → desc → workspace → confirm)Création avec FolderPicker + auto-suggest cwd serveur + alerte conflit
ProjectEditTunnel4 (name → desc → workspace → confirm)Édition (path immuable)
ProjectDeleteTunnel3 (mode forget/purge → confirm → résultat)Suppression avec choix mode + récap deletedPaths

Tous les tunnels exposent l’overlay loader bloquant pendant les mutations longues (busy={isSubmitting}).

  • HomeView : grille des projets du workspace courant + bandeau header workspace (Modifier / Supprimer accessibles directement).
  • ProjectView : détail d’un projet — agents installés, sessions chat, actions (Modifier projet / Options de lancement / Supprimer ce projet).
  • ContextBand (top bar) : sélecteur workspace + sélecteur projet + actions Edit/Delete dans le footer du dropdown workspace.

Le module projets/workspaces publie sur le bus arka-deck :

EventModePayload
workspace.createdasync{ workspaceId, name, createdAt }
workspace.updatedasync{ workspaceId, name, description }
workspace.deletedsync{ workspaceId }
project.createdasync{ projectId, projectPath, workspaceId, name }
project.updatedasync{ projectId, name, workspaceId }
project.forgottensync{ projectId }
project.purgedsync{ projectId, projectPath, deletedPaths }
project.last-usedasync{ projectId, lastUsedAt }
project.detected-by-cwdasync{ projectId, projectPath, cwd }

Subscribers prévus : mémoire (cleanup sessions au purge), gouvernance (audit créations), squad (recompose si projet ajouté).


Si votre module pose des fichiers dans <projectPath> (autres que .arka-deck/ qui est nettoyé d’office), implémentez un nettoyage côté purgeProjectUseCase.

Option A — Subscriber project.purged (recommandé)

Section intitulée « Option A — Subscriber project.purged (recommandé) »
bus.subscribe('project.purged', async (event) => {
await myStore.cleanupProject(event.projectPath);
});

Modifier core/use-cases/projects/build-for-projects.ts pour accepter un nouveau dep optionnel et appeler son cleanup. Préférer Option A pour respecter le découplage.


GarantieMécanisme
Pas de path traversalpathPolicy.isAllowed(path) côté route HTTP avant écriture marker
Allowlist utilisateur~/.arka-deck/preferences/security.json — patterns globs validés
Pas de write hors .arka-deck/ au createLe use-case ne touche que <projectPath>/.arka-deck/project/marker.json
Purge ne touche pas aux fichiers utilisateurCleanup limité aux items dans installed.json (track explicite)
Atomicité écriture marker + indextmp + rename POSIX
Détection corruptionMarkerCorruptedError HTTP 422 — le marker est inutilisable, l’index reste source

Le marker disque n’est pas exposé par les routes HTTP. L’UI consomme uniquement les DTOs serveur via /api/projects (l’index agrégé).

// ❌
const proj = await projectIndexStore.list().find((p) => p.path === cwd);
// ✅
const proj = await forProjects.findByCwd(cwd);

Le path est immuable côté Project.create. Pour changer le path :

  1. forget l’ancien projet (préserve le marker)
  2. create un nouveau projet au nouveau path
  3. Migrer manuellement les fichiers .arka-deck/ si besoin
// ❌
await forWorkspaces.delete(wid);
forceDeleteAll(wid);
// ✅
const projects = await forProjects.listByWorkspace(wid);
await Promise.all(projects.map((p) => forProjects.forget(p.id)));
await forWorkspaces.delete(wid);

Pour la vérité absolue sur l’état d’un projet, lire le marker disque (projectStore.load(path)). L’index peut être désynchronisé.


ActionFichier
Domain Projectcore/domain/project/project.ts
Domain Workspacecore/domain/workspace/workspace.ts
Use-cases projetscore/use-cases/projects/build-for-projects.ts
Use-cases workspacescore/use-cases/workspaces/build-for-workspaces.ts
Marker storeadapters/outbound/filesystem/fs-project-store.ts
Index storeadapters/outbound/filesystem/fs-project-index-store.ts
Workspace storeadapters/outbound/filesystem/fs-workspace-store.ts
Path policyadapters/outbound/system/preferences-path-policy.ts
Routes serveuradapters/inbound/web/server/src/routes/{projects,workspaces}.ts
Tunnels UIadapters/inbound/web/ui/src/components/tunnel/*Tunnel.tsx
Hooks UIadapters/inbound/web/ui/src/hooks/{useProjects,useWorkspaces}.ts
Errorscore/domain/errors.ts