import { defineStore } from 'pinia'; import { ref } from 'vue'; import type { CreateWorktreeRequest, CreateWorktreeResponse, DiscoverReposResponse, RepoBranchesResponse, RepoResponse, ReposListResponse, RepoSummary, SessionResponse, SessionSummary, StartRepoSessionRequest, WorktreeResponse, WorktreeSummary, WorktreesListResponse, } from '@arboretum/shared'; import { api } from '../lib/api'; import { wsClient, type WorktreeEvent } from '../lib/ws-client'; export const useWorktreesStore = defineStore('worktrees', () => { const repos = ref([]); const worktrees = ref([]); const loading = ref(false); const loadError = ref(null); // Préférence d'affichage (purement client) : révéler les sessions externes (lancées hors Arboretum) // dans les fiches worktree. Par défaut masquées pour éviter les listes à rallonge. const showExternalSessions = ref(false); let unsubscribe: (() => void) | null = null; function upsertRepo(repo: RepoSummary): void { const idx = repos.value.findIndex((r) => r.id === repo.id); if (idx >= 0) repos.value.splice(idx, 1, repo); else repos.value.push(repo); } function removeRepoLocal(id: string): void { repos.value = repos.value.filter((r) => r.id !== id); worktrees.value = worktrees.value.filter((w) => w.repoId !== id); } function upsertWorktree(wt: WorktreeSummary): void { const idx = worktrees.value.findIndex((w) => w.path === wt.path); if (idx >= 0) worktrees.value.splice(idx, 1, wt); else worktrees.value.push(wt); } function removeWorktreeLocal(path: string): void { worktrees.value = worktrees.value.filter((w) => w.path !== path); } function onEvent(e: WorktreeEvent): void { if (e.type === 'repo_update') upsertRepo(e.repo); else if (e.type === 'repo_removed') removeRepoLocal(e.repoId); else if (e.type === 'worktree_update') upsertWorktree(e.worktree); else removeWorktreeLocal(e.path); } function worktreesForRepo(repoId: string): WorktreeSummary[] { return worktrees.value .filter((w) => w.repoId === repoId) .sort((a, b) => Number(b.isMain) - Number(a.isMain) || a.path.localeCompare(b.path)); } async function fetchAll(): Promise { loading.value = true; loadError.value = null; try { const [r, w] = await Promise.all([ api.get('/api/v1/repos'), api.get('/api/v1/worktrees'), ]); repos.value = r.repos; worktrees.value = w.worktrees; } catch (err) { loadError.value = err instanceof Error ? err.message : String(err); } finally { loading.value = false; } } async function refreshRepoWorktrees(repoId: string): Promise { const w = await api.get(`/api/v1/repos/${repoId}/worktrees`); for (const wt of w.worktrees) upsertWorktree(wt); } async function addRepo(path: string, label?: string): Promise { const res = await api.post('/api/v1/repos', { path, ...(label ? { label } : {}) }); upsertRepo(res.repo); await refreshRepoWorktrees(res.repo.id); return res.repo; } async function removeRepo(id: string): Promise { await api.delete<{ ok: true }>(`/api/v1/repos/${id}`); removeRepoLocal(id); } /** Masque/ré-affiche un repo (persiste `hidden` côté serveur ; émis aussi par WS). */ async function setHidden(id: string, hidden: boolean): Promise { const res = await api.patch(`/api/v1/repos/${id}`, { hidden }); upsertRepo(res.repo); } /** Lance un scan de découverte ; les nouveaux repos arrivent par WS, on resynchronise par sûreté. */ async function discover(): Promise { const res = await api.post('/api/v1/repos/discover'); await fetchAll(); return res; } async function createWorktree(repoId: string, req: CreateWorktreeRequest): Promise { const res = await api.post(`/api/v1/repos/${repoId}/worktrees`, req); upsertWorktree(res.worktree); return res; } /** * Lance une session sur le checkout principal du repo (« bosser sur la branche principale » sans * worktree). Avec `branch`, le serveur crée/bascule la branche d'abord. La session et le worktree * mis à jour arrivent aussi par WS ; on retourne la session pour permettre la navigation immédiate. */ async function startMainSession(repoId: string, req: StartRepoSessionRequest = {}): Promise { const res = await api.post(`/api/v1/repos/${repoId}/session`, req); return res.session; } async function deleteWorktree(repoId: string, path: string, force: boolean): Promise { await api.delete<{ ok: true }>(`/api/v1/repos/${repoId}/worktrees?path=${encodeURIComponent(path)}&force=${force}`); removeWorktreeLocal(path); } async function prune(repoId: string): Promise { await api.post<{ ok: true }>(`/api/v1/repos/${repoId}/worktrees/prune`); await refreshRepoWorktrees(repoId); } /** Branches du repo (locales/remote + défaut) pour le sélecteur de branche de base. */ async function fetchBranches(repoId: string): Promise { return api.get(`/api/v1/repos/${repoId}/branches`); } /** Commit (add -A + commit) dans un worktree ; le worktree mis à jour arrive aussi par WS. */ async function commitWorktree(repoId: string, path: string, message: string): Promise { const res = await api.post(`/api/v1/repos/${repoId}/worktrees/commit`, { path, message }); upsertWorktree(res.worktree); return res.worktree; } /** Push de la branche d'un worktree (upstream auto si absent). */ async function pushWorktree(repoId: string, path: string): Promise { const res = await api.post(`/api/v1/repos/${repoId}/worktrees/push`, { path }); upsertWorktree(res.worktree); return res.worktree; } /** « Passer en principal » : la branche du worktree devient le checkout principal (worktree supprimé). */ async function promoteWorktree(repoId: string, path: string, force = false): Promise { await api.post<{ worktree: WorktreeSummary | null }>(`/api/v1/repos/${repoId}/worktrees/promote`, { path, force }); removeWorktreeLocal(path); await refreshRepoWorktrees(repoId); } function startRealtime(): void { unsubscribe ??= wsClient.subscribeWorktrees(onEvent); } function stopRealtime(): void { unsubscribe?.(); unsubscribe = null; } return { repos, worktrees, loading, loadError, showExternalSessions, worktreesForRepo, fetchAll, addRepo, removeRepo, setHidden, discover, refreshRepoWorktrees, createWorktree, startMainSession, deleteWorktree, prune, fetchBranches, commitWorktree, pushWorktree, promoteWorktree, startRealtime, stopRealtime, }; });