workspace-registry.ts
1 import fs from 'fs' 2 import path from 'path' 3 4 import { DATA_DIR } from '@/lib/server/data-dir' 5 import { hmrSingleton } from '@/lib/shared-utils' 6 import { genId } from '@/lib/id' 7 import { log } from '@/lib/server/logger' 8 import { 9 DEFAULT_WORKSPACE_ID, 10 type Workspace, 11 type WorkspaceRegistry, 12 } from '@/types/workspace' 13 14 const TAG = 'workspace-registry' 15 const FILE_PATH = path.join(DATA_DIR, 'workspace-registry.json') 16 17 interface RegistryCache { 18 loaded: boolean 19 registry: WorkspaceRegistry 20 } 21 22 const cache = hmrSingleton<RegistryCache>('workspaceRegistry_cache', () => ({ 23 loaded: false, 24 registry: { 25 workspaces: {}, 26 activeWorkspaceId: DEFAULT_WORKSPACE_ID, 27 }, 28 })) 29 30 function ensureLoaded(): void { 31 if (cache.loaded) return 32 cache.loaded = true 33 try { 34 if (fs.existsSync(FILE_PATH)) { 35 const raw = fs.readFileSync(FILE_PATH, 'utf8') 36 const parsed = JSON.parse(raw) as WorkspaceRegistry 37 cache.registry = { 38 workspaces: parsed?.workspaces ?? {}, 39 activeWorkspaceId: parsed?.activeWorkspaceId ?? DEFAULT_WORKSPACE_ID, 40 } 41 } 42 } catch (error) { 43 log.warn(TAG, `Failed to load workspace registry: ${error instanceof Error ? error.message : error}`) 44 } 45 if (!cache.registry.workspaces[DEFAULT_WORKSPACE_ID]) { 46 const now = Date.now() 47 cache.registry.workspaces[DEFAULT_WORKSPACE_ID] = { 48 id: DEFAULT_WORKSPACE_ID, 49 name: 'Default', 50 description: 'Default workspace', 51 dataDir: null, 52 color: '#3b82f6', 53 createdAt: now, 54 updatedAt: now, 55 } 56 persist() 57 } 58 } 59 60 function persist(): void { 61 try { 62 if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }) 63 fs.writeFileSync(FILE_PATH, JSON.stringify(cache.registry, null, 2), 'utf8') 64 } catch (error) { 65 log.error(TAG, `Failed to persist workspace registry: ${error instanceof Error ? error.message : error}`) 66 } 67 } 68 69 export function listWorkspaces(): Workspace[] { 70 ensureLoaded() 71 return Object.values(cache.registry.workspaces) 72 .sort((a, b) => a.createdAt - b.createdAt) 73 } 74 75 export function getActiveWorkspace(): Workspace { 76 ensureLoaded() 77 return cache.registry.workspaces[cache.registry.activeWorkspaceId] 78 ?? cache.registry.workspaces[DEFAULT_WORKSPACE_ID] 79 } 80 81 export function getWorkspace(id: string): Workspace | null { 82 ensureLoaded() 83 return cache.registry.workspaces[id] ?? null 84 } 85 86 export function createWorkspace(input: { 87 name: string 88 description?: string 89 color?: string 90 }): Workspace { 91 ensureLoaded() 92 const id = genId() 93 const now = Date.now() 94 const workspace: Workspace = { 95 id, 96 name: input.name, 97 description: input.description, 98 dataDir: null, 99 color: input.color ?? null, 100 createdAt: now, 101 updatedAt: now, 102 } 103 cache.registry.workspaces[id] = workspace 104 persist() 105 return workspace 106 } 107 108 export function updateWorkspace(id: string, patch: Partial<Workspace>): Workspace | null { 109 ensureLoaded() 110 const existing = cache.registry.workspaces[id] 111 if (!existing) return null 112 const next: Workspace = { 113 ...existing, 114 ...patch, 115 id: existing.id, 116 createdAt: existing.createdAt, 117 updatedAt: Date.now(), 118 } 119 cache.registry.workspaces[id] = next 120 persist() 121 return next 122 } 123 124 export function deleteWorkspace(id: string): boolean { 125 ensureLoaded() 126 if (id === DEFAULT_WORKSPACE_ID) return false 127 if (!cache.registry.workspaces[id]) return false 128 delete cache.registry.workspaces[id] 129 if (cache.registry.activeWorkspaceId === id) { 130 cache.registry.activeWorkspaceId = DEFAULT_WORKSPACE_ID 131 } 132 persist() 133 return true 134 } 135 136 export function setActiveWorkspace(id: string): Workspace | null { 137 ensureLoaded() 138 const target = cache.registry.workspaces[id] 139 if (!target) return null 140 cache.registry.activeWorkspaceId = id 141 persist() 142 return target 143 }