import { defineStore } from 'pinia'; import { ref } from 'vue'; import type { CreateGroupRequest, CreateWorktreeRequest, GroupResponse, GroupsListResponse, GroupSummary, RepoSummary, SessionSummary, UpdateGroupRequest, WorktreeSummary, } from '@arboretum/shared'; import { api, ApiError } from '../lib/api'; import { wsClient, type GroupEvent } from '../lib/ws-client'; import { useWorktreesStore } from './worktrees'; /** Requête « feature cross-repo » : un worktree (et éventuellement une session) par repo. */ export type CrossRepoRequest = Omit; /** Résultat par repo d'une action cross-repo (tolérance aux échecs partiels). */ export interface CrossRepoResult { repoId: string; status: 'ok' | 'error'; message?: string; worktreePath?: string; sessionId?: string; } export const useGroupsStore = defineStore('groups', () => { const groups = ref([]); const selectedGroupId = ref(null); const loading = ref(false); const loadError = ref(null); let unsubscribe: (() => void) | null = null; function upsert(group: GroupSummary): void { const idx = groups.value.findIndex((g) => g.id === group.id); if (idx >= 0) groups.value.splice(idx, 1, group); else groups.value.push(group); } function removeLocal(id: string): void { groups.value = groups.value.filter((g) => g.id !== id); if (selectedGroupId.value === id) selectedGroupId.value = null; } function onEvent(e: GroupEvent): void { if (e.type === 'group_update') upsert(e.group); else removeLocal(e.groupId); } function byId(id: string): GroupSummary | undefined { return groups.value.find((g) => g.id === id); } // ---- getters dérivés : réutilisent le store worktrees par filtrage repoId (zéro refetch) ---- function reposInGroup(groupId: string): RepoSummary[] { const g = byId(groupId); if (!g) return []; const wt = useWorktreesStore(); return g.repoIds .map((id) => wt.repos.find((r) => r.id === id)) .filter((r): r is RepoSummary => r !== undefined); } function worktreesInGroup(groupId: string): WorktreeSummary[] { const g = byId(groupId); if (!g) return []; const ids = new Set(g.repoIds); return useWorktreesStore().worktrees.filter((w) => ids.has(w.repoId)); } function sessionsInGroup(groupId: string): SessionSummary[] { return worktreesInGroup(groupId).flatMap((w) => w.sessions); } async function fetchGroups(): Promise { loading.value = true; loadError.value = null; try { const res = await api.get('/api/v1/groups'); groups.value = res.groups; } catch (err) { loadError.value = err instanceof Error ? err.message : String(err); } finally { loading.value = false; } } async function createGroup(req: CreateGroupRequest): Promise { const res = await api.post('/api/v1/groups', req); upsert(res.group); return res.group; } async function updateGroup(id: string, patch: UpdateGroupRequest): Promise { const res = await api.patch(`/api/v1/groups/${id}`, patch); upsert(res.group); return res.group; } async function deleteGroup(id: string): Promise { await api.delete<{ ok: true }>(`/api/v1/groups/${id}`); removeLocal(id); } async function addRepoToGroup(groupId: string, repoId: string): Promise { const res = await api.post(`/api/v1/groups/${groupId}/repos`, { repoId }); upsert(res.group); return res.group; } async function removeRepoFromGroup(groupId: string, repoId: string): Promise { const res = await api.delete(`/api/v1/groups/${groupId}/repos/${repoId}`); upsert(res.group); return res.group; } /** * Crée le même worktree (et éventuellement une session) dans chaque repo, en une action. * Orchestration côté client : les worktrees sont indépendants entre repos, donc on tolère * les échecs partiels (un repo en échec n'empêche pas les autres). Feedback par repo. */ async function createCrossRepoFeature( repoIds: string[], req: CrossRepoRequest, onProgress?: (result: CrossRepoResult) => void, ): Promise { const wt = useWorktreesStore(); return Promise.all( repoIds.map(async (repoId): Promise => { try { const res = await wt.createWorktree(repoId, req); const result: CrossRepoResult = { repoId, status: 'ok', worktreePath: res.worktree.path, ...(res.session ? { sessionId: res.session.id } : {}), }; onProgress?.(result); return result; } catch (err) { const result: CrossRepoResult = { repoId, status: 'error', message: err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err), }; onProgress?.(result); return result; } }), ); } function startRealtime(): void { unsubscribe ??= wsClient.subscribeGroups(onEvent); } function stopRealtime(): void { unsubscribe?.(); unsubscribe = null; } return { groups, selectedGroupId, loading, loadError, byId, reposInGroup, worktreesInGroup, sessionsInGroup, fetchGroups, createGroup, updateGroup, deleteGroup, addRepoToGroup, removeRepoFromGroup, createCrossRepoFeature, startRealtime, stopRealtime, }; });