import type { DatabaseSync } from "node:sqlite"; import type { RunStatus, WorkspaceRecord } from "@shared/types"; import type { ReadAll, Transaction } from "./helpers"; import { ensureId, normalizeOptionalId, nowIso, toBoolean } from "./helpers"; export function buildWorkspaceMethods( getDb: () => DatabaseSync, readAll: ReadAll, transaction: Transaction, ) { return { saveWorkspace(input: { id?: string | null; companyId: string; name: string; localPath: string; repoUrl: string; repoRef: string; projectId?: string | null; isPrimary: boolean; }) { const id = ensureId(input.id ?? null); const ts = nowIso(); if (input.isPrimary) { getDb().prepare("update workspaces set is_primary = 0 where company_id = ?").run(input.companyId); } const existing = getDb().prepare("select id from workspaces where id = ? and company_id = ?").get(id, input.companyId) as { id: string } | undefined; if (existing) { getDb() .prepare( "update workspaces set name = ?, local_path = ?, repo_url = ?, repo_ref = ?, project_id = ?, is_primary = ?, updated_at = ? where id = ? and company_id = ?", ) .run(input.name, input.localPath, input.repoUrl, input.repoRef, normalizeOptionalId(input.projectId), input.isPrimary ? 1 : 0, ts, id, input.companyId); } else { getDb() .prepare( "insert into workspaces (id, company_id, name, local_path, repo_url, repo_ref, project_id, state, is_primary, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, 'ready', ?, ?, ?)", ) .run(id, input.companyId, input.name, input.localPath, input.repoUrl, input.repoRef, normalizeOptionalId(input.projectId), input.isPrimary ? 1 : 0, ts, ts); } return id; }, getWorkspace(id: string | null | undefined): WorkspaceRecord | null { if (!id) return null; const row = getDb() .prepare( "select id, company_id as companyId, name, local_path as localPath, repo_url as repoUrl, repo_ref as repoRef, project_id as projectId, state, is_primary as isPrimary, created_at as createdAt, updated_at as updatedAt from workspaces where id = ?", ) .get(id) as WorkspaceRecord | undefined; return row ? { ...row, isPrimary: toBoolean(row.isPrimary) } : null; }, getProjectPrimaryWorkspace(projectId: string | null | undefined, companyId: string): WorkspaceRecord | null { if (!projectId) return null; const row = getDb() .prepare( "select id, company_id as companyId, name, local_path as localPath, repo_url as repoUrl, repo_ref as repoRef, project_id as projectId, state, is_primary as isPrimary, created_at as createdAt, updated_at as updatedAt from workspaces where company_id = ? and project_id = ? order by is_primary desc, created_at asc limit 1", ) .get(companyId, projectId) as WorkspaceRecord | undefined; return row ? { ...row, isPrimary: toBoolean(row.isPrimary) } : null; }, listWorkspaceLocalPaths(companyId: string): string[] { const rows = getDb() .prepare("select local_path as localPath from workspaces where company_id = ? and local_path is not null") .all(companyId) as Array<{ localPath: string }>; return rows.map((r) => r.localPath).filter(Boolean); }, getAnyCompanyWorkspace(companyId: string): WorkspaceRecord | null { const row = getDb() .prepare( "select id, company_id as companyId, name, local_path as localPath, repo_url as repoUrl, repo_ref as repoRef, project_id as projectId, state, is_primary as isPrimary, created_at as createdAt, updated_at as updatedAt from workspaces where company_id = ? order by is_primary desc, created_at asc limit 1", ) .get(companyId) as WorkspaceRecord | undefined; return row ? { ...row, isPrimary: toBoolean(row.isPrimary) } : null; }, deleteWorkspace(id: string, companyId: string) { transaction(() => { getDb().prepare("update agents set workspace_id = null where workspace_id = ? and company_id = ?").run(id, companyId); getDb().prepare("update tasks set workspace_id = null where workspace_id = ? and company_id = ?").run(id, companyId); getDb().prepare("delete from workspaces where id = ? and company_id = ?").run(id, companyId); }); }, acquireWorkspaceLock(workspaceId: string, runId: string): boolean { const ts = nowIso(); const result = getDb() .prepare( `update workspaces set lock_holder_run_id = ?, lock_acquired_at = ?, state = 'busy', updated_at = ? where id = ? and (lock_holder_run_id is null or lock_holder_run_id = ?)`, ) .run(runId, ts, ts, workspaceId, runId); return result.changes > 0; }, claimWorkspaceLock(workspaceId: string, runId: string): boolean { const holder = getDb() .prepare("select lock_holder_run_id as lockHolderRunId from workspaces where id = ?") .get(workspaceId) as { lockHolderRunId: string | null } | undefined; if (!holder) return false; if (!holder.lockHolderRunId || holder.lockHolderRunId === runId) { const ts = nowIso(); const result = getDb() .prepare( `update workspaces set lock_holder_run_id = ?, lock_acquired_at = ?, state = 'busy', updated_at = ? where id = ? and (lock_holder_run_id is null or lock_holder_run_id = ?)`, ) .run(runId, ts, ts, workspaceId, runId); return result.changes > 0; } const blockingRun = getDb() .prepare("select status from runs where id = ?") .get(holder.lockHolderRunId) as { status: RunStatus } | undefined; if (blockingRun && (blockingRun.status === "queued" || blockingRun.status === "running")) { return false; } const ts = nowIso(); const result = getDb() .prepare( `update workspaces set lock_holder_run_id = ?, lock_acquired_at = ?, state = 'busy', updated_at = ? where id = ? and lock_holder_run_id = ?`, ) .run(runId, ts, ts, workspaceId, holder.lockHolderRunId); return result.changes > 0; }, releaseWorkspaceLock(workspaceId: string, runId: string) { getDb() .prepare( `update workspaces set lock_holder_run_id = null, lock_acquired_at = null, state = 'ready', updated_at = ? where id = ? and lock_holder_run_id = ?`, ) .run(nowIso(), workspaceId, runId); }, releaseAllWorkspaceLocks() { getDb() .prepare( "update workspaces set lock_holder_run_id = null, lock_acquired_at = null, state = 'ready', updated_at = ? where lock_holder_run_id is not null", ) .run(nowIso()); }, releaseStaleWorkspaceLocks(): number { const result = getDb() .prepare( `update workspaces set lock_holder_run_id = null, lock_acquired_at = null, state = 'ready', updated_at = ? where lock_holder_run_id is not null and not exists ( select 1 from runs where runs.id = workspaces.lock_holder_run_id and runs.status in ('queued', 'running') )`, ) .run(nowIso()); return Number(result.changes); }, isWorkspaceLocked(workspaceId: string): boolean { const row = getDb() .prepare("select lock_holder_run_id from workspaces where id = ?") .get(workspaceId) as { lock_holder_run_id: string | null } | undefined; return Boolean(row?.lock_holder_run_id); }, }; }