/ src / lib / server / workspaces / workspace-registry.ts
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  }