@apicircle/core 1.1.0 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/workspace/registry.cjs +7 -0
- package/dist/workspace/registry.cjs.map +1 -1
- package/dist/workspace/registry.d.cts +12 -1
- package/dist/workspace/registry.d.ts +12 -1
- package/dist/workspace/registry.js +6 -0
- package/dist/workspace/registry.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -153,6 +153,7 @@ import { loadFromFile, saveToFile, withWorkspace } from '@apicircle/core/workspa
|
|
|
153
153
|
// Multi-workspace registry (registry.json + per-id subdirectories)
|
|
154
154
|
import {
|
|
155
155
|
defaultApicircleRoot,
|
|
156
|
+
resolveApicircleRoot,
|
|
156
157
|
loadRegistry,
|
|
157
158
|
saveRegistry,
|
|
158
159
|
loadWorkspaceById,
|
|
@@ -170,8 +171,10 @@ import {
|
|
|
170
171
|
advisory locking, so concurrent CLI runs don't corrupt each other.
|
|
171
172
|
- **`/workspace/registry`** — many workspaces, one root. `registry.json` at
|
|
172
173
|
the top, `workspaces/<id>/` subdirectories underneath. All surfaces
|
|
173
|
-
(desktop, CLI, MCP, VS Code) default to `~/.apicircle
|
|
174
|
-
`
|
|
174
|
+
(desktop, CLI, MCP, VS Code) default to `~/.apicircle/`. Use
|
|
175
|
+
`resolveApicircleRoot()` to honor the `APICIRCLE_WORKSPACES_ROOT` override
|
|
176
|
+
(CI / tests / relocated stores); `defaultApicircleRoot()` is the raw
|
|
177
|
+
`~/.apicircle/` fallback it builds on.
|
|
175
178
|
|
|
176
179
|
## Use cases
|
|
177
180
|
|
|
@@ -39,6 +39,7 @@ __export(workspaceRegistry_exports, {
|
|
|
39
39
|
loadRegistry: () => loadRegistry,
|
|
40
40
|
loadWorkspaceById: () => loadWorkspaceById,
|
|
41
41
|
registerWorkspace: () => registerWorkspace,
|
|
42
|
+
resolveApicircleRoot: () => resolveApicircleRoot,
|
|
42
43
|
saveRegistry: () => saveRegistry,
|
|
43
44
|
saveWorkspaceById: () => saveWorkspaceById,
|
|
44
45
|
setActiveWorkspace: () => setActiveWorkspace,
|
|
@@ -154,6 +155,11 @@ var WORKSPACE_DIR_PREFIX = "workspace-";
|
|
|
154
155
|
function defaultApicircleRoot() {
|
|
155
156
|
return path2.join(os.homedir(), ".apicircle");
|
|
156
157
|
}
|
|
158
|
+
function resolveApicircleRoot() {
|
|
159
|
+
const override = process.env.APICIRCLE_WORKSPACES_ROOT;
|
|
160
|
+
if (override && override.length > 0) return path2.resolve(override);
|
|
161
|
+
return defaultApicircleRoot();
|
|
162
|
+
}
|
|
157
163
|
function emptyRegistry() {
|
|
158
164
|
return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };
|
|
159
165
|
}
|
|
@@ -275,6 +281,7 @@ function isENOENT2(err) {
|
|
|
275
281
|
loadRegistry,
|
|
276
282
|
loadWorkspaceById,
|
|
277
283
|
registerWorkspace,
|
|
284
|
+
resolveApicircleRoot,
|
|
278
285
|
saveRegistry,
|
|
279
286
|
saveWorkspaceById,
|
|
280
287
|
setActiveWorkspace,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/workspace/workspaceRegistry.ts","../../src/workspace/fileBackedWorkspace.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport { loadFromFile, saveToFile } from './fileBackedWorkspace';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// On-disk multi-workspace registry. All surfaces (desktop, CLI, MCP server,\n// VS Code extension) read the same `~/.apicircle/` root.\n//\n// Layout under `<root>/` (= ~/.apicircle or <repo>/.apicircle):\n//\n// registry.json ← this module's source of truth\n// workspace-<id-1>/\n// workspace.json\n// workspace.local.json\n// attachments/\n// workspace-<id-2>/\n// ...\n//\n// Each workspace lives in its own `workspace-<id>` directory so concurrent\n// writers (the desktop's mirror + a CLI invocation against a different\n// workspace) can't step on each other. The flat naming keeps the structure\n// identical whether there's one workspace or ten — no surprise layout\n// changes when multi-workspace is introduced. `proper-lockfile` still\n// guards the registry file itself when readers / writers race.\n// =============================================================================\n\nexport const REGISTRY_FILE = 'registry.json';\nexport const WORKSPACE_DIR_PREFIX = 'workspace-';\n\n/**\n * The universal root for all API Circle workspace data: `~/.apicircle/`.\n * Every consumer (desktop, CLI, MCP, VS Code) uses this as the default.\n */\nexport function defaultApicircleRoot(): string {\n return path.join(os.homedir(), '.apicircle');\n}\n\nexport interface WorkspaceRegistryEntry {\n /** Matches the in-workspace `synced.workspaceId`. */\n id: string;\n /** Human-readable label. Local to this device — never pushed to git. */\n name: string;\n /** ISO timestamp; bumped every time this workspace is opened or written. */\n lastOpenedAt: string;\n /** ISO timestamp; set when the workspace was first registered. */\n createdAt: string;\n}\n\nexport interface WorkspaceRegistry {\n schemaVersion: 1;\n /** id of the workspace the desktop UI should boot into. `null` only on\n * an empty registry (no workspaces have been seeded yet). */\n activeWorkspaceId: string | null;\n workspaces: WorkspaceRegistryEntry[];\n}\n\n/** Default empty registry. */\nexport function emptyRegistry(): WorkspaceRegistry {\n return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };\n}\n\n/** Compute the directory inside `<root>/` that holds a workspace's files.\n * Layout: `<root>/workspace-<id>/`. */\nexport function workspaceDirFor(root: string, workspaceId: string): string {\n return path.join(root, `${WORKSPACE_DIR_PREFIX}${workspaceId}`);\n}\n\n/** Load the registry from disk; returns `null` if the file is missing. */\nexport async function loadRegistry(root: string): Promise<WorkspaceRegistry | null> {\n const filePath = path.join(root, REGISTRY_FILE);\n try {\n const raw = await fs.readFile(filePath, 'utf-8');\n const parsed = JSON.parse(raw) as WorkspaceRegistry;\n return normalizeRegistry(parsed);\n } catch (err) {\n if (isENOENT(err)) return null;\n throw err;\n }\n}\n\n/** Save the registry atomically (`<file>.tmp` + rename). */\nexport async function saveRegistry(root: string, registry: WorkspaceRegistry): Promise<void> {\n await fs.mkdir(root, { recursive: true });\n const filePath = path.join(root, REGISTRY_FILE);\n const tmp = `${filePath}.tmp`;\n await ensureFile(filePath);\n // Lock the registry file itself so two writers can't tear-write the JSON.\n const release = await lockfile.lock(filePath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: 30000,\n });\n try {\n await fs.writeFile(tmp, JSON.stringify(registry, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await fs.rename(tmp, filePath);\n } finally {\n await release();\n }\n}\n\n/**\n * Read a workspace's `{synced, local}` pair by id. Returns `null` if the\n * workspace subdirectory is missing OR its `workspace.json` is missing.\n * Used by the CLI / MCP / desktop reader.\n */\nexport async function loadWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceState | null> {\n return loadFromFile(workspaceDirFor(root, workspaceId), { allowMissing: true });\n}\n\n/**\n * Write a workspace's pair under `<root>/<id>/`. Idempotent; the directory\n * is created on first write. Bumps the registry entry's `lastOpenedAt`\n * and writes the registry back if the entry exists.\n */\nexport async function saveWorkspaceById(\n root: string,\n workspaceId: string,\n state: WorkspaceState,\n): Promise<void> {\n await saveToFile(workspaceDirFor(root, workspaceId), state);\n}\n\n/**\n * Remove a workspace from disk: deletes its directory, drops it from the\n * registry. If it was the active workspace, the next-most-recent remaining\n * workspace becomes active. If no workspaces remain, `activeWorkspaceId`\n * goes to `null` and the caller should seed a fresh workspace.\n */\nexport async function deleteWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const remaining = registry.workspaces.filter((w) => w.id !== workspaceId);\n await fs.rm(workspaceDirFor(root, workspaceId), { recursive: true, force: true });\n let nextActive = registry.activeWorkspaceId;\n if (nextActive === workspaceId) {\n nextActive =\n [...remaining].sort((a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt))[0]?.id ?? null;\n }\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: nextActive,\n workspaces: remaining,\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Add a workspace to the registry. Caller is responsible for having\n * written `workspace.json` first. Existing entry with the same id\n * is replaced (idempotent update). Newly-registered workspaces become\n * the active one when there is no prior active.\n */\nexport async function registerWorkspace(\n root: string,\n entry: WorkspaceRegistryEntry,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const otherEntries = registry.workspaces.filter((w) => w.id !== entry.id);\n const next: WorkspaceRegistry = {\n schemaVersion: 1,\n activeWorkspaceId: registry.activeWorkspaceId ?? entry.id,\n workspaces: [...otherEntries, entry],\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/** Set the active workspace id. Throws if the id isn't in the registry. */\nexport async function setActiveWorkspace(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n if (!registry.workspaces.some((w) => w.id === workspaceId)) {\n throw new Error(`workspace ${workspaceId} is not in the registry at ${root}`);\n }\n const now = new Date().toISOString();\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: workspaceId,\n workspaces: registry.workspaces.map((w) =>\n w.id === workspaceId ? { ...w, lastOpenedAt: now } : w,\n ),\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Look up a workspace registry entry by id OR by name (case-insensitive).\n * Used by the CLI's `--workspace <selector>` resolver — accepts both forms.\n * Returns `null` if no entry matches.\n */\nexport function findWorkspaceEntry(\n registry: WorkspaceRegistry,\n idOrName: string,\n): WorkspaceRegistryEntry | null {\n const exactId = registry.workspaces.find((w) => w.id === idOrName);\n if (exactId) return exactId;\n const lower = idOrName.toLowerCase();\n const byName = registry.workspaces.find((w) => w.name.toLowerCase() === lower);\n return byName ?? null;\n}\n\n/** Normalize a parsed registry so downstream code can rely on its shape. */\nfunction normalizeRegistry(raw: WorkspaceRegistry): WorkspaceRegistry {\n return {\n schemaVersion: 1,\n activeWorkspaceId: raw.activeWorkspaceId ?? null,\n workspaces: Array.isArray(raw.workspaces) ? raw.workspaces : [],\n };\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: 0o600 });\n }\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\n// Re-export commonly-used helpers so consumers can import from one place.\nexport type { WorkspaceLocal, WorkspaceSynced };\n","import { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport { FONT_SIZE_PERCENT_DEFAULT } from '@apicircle/shared';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// fileBackedWorkspace — load/save a `{ synced, local }` pair as two JSON\n// files on disk, with a `proper-lockfile` advisory lock so concurrent CLI /\n// MCP writers can't corrupt the document.\n//\n// Layout (relative to the directory passed in):\n// workspace.json ← matches WorkspaceSynced exactly\n// workspace.local.json ← WorkspaceLocal, host-private (CLI/MCP doesn't push)\n//\n// The lock is held on `workspace.json` because that's the file the\n// editor races against. Stale locks are released after 30s.\n// =============================================================================\n\nconst SYNCED_FILE = 'workspace.json';\nconst LOCAL_FILE = 'workspace.local.json';\n\nexport interface LoadFromFileOptions {\n /** When true, return `null` instead of throwing if the synced file is missing. */\n allowMissing?: boolean;\n /** Override the synced filename. Defaults to `workspace.json`. */\n syncedFilename?: string;\n}\n\nexport interface SaveToFileOptions {\n /** Lock timeout (ms). Defaults to 30000. */\n lockTimeoutMs?: number;\n /** Override the synced filename. Defaults to `workspace.json`. */\n syncedFilename?: string;\n}\n\n/**\n * Load both workspace documents from `dir`. The synced file is required;\n * the local file is optional and falls back to a minimal empty shape so a\n * CLI on a fresh machine can still operate (it just won't have history /\n * overrides until the desktop app runs once).\n */\nexport async function loadFromFile(\n dir: string,\n options: LoadFromFileOptions = {},\n): Promise<WorkspaceState | null> {\n const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n let syncedRaw: string;\n try {\n syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n } catch (err) {\n if (options.allowMissing && isENOENT(err)) return null;\n throw err;\n }\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n local = { ...local, attachmentCache: local.attachmentCache ?? {} };\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n\n return { synced, local };\n}\n\n/**\n * Atomically write both documents back to disk. Acquires an advisory lock\n * on the synced file for the duration of the write so a parallel CLI /\n * MCP / desktop save can't interleave.\n *\n * Both files are written via `<file>.tmp` + rename so a crash mid-write\n * never leaves a partial JSON document on disk.\n */\nexport async function saveToFile(\n dir: string,\n state: WorkspaceState,\n options: SaveToFileOptions = {},\n): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n // proper-lockfile requires the target file to exist. Touch it on first save.\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n await writeJsonAtomic(syncedPath, state.synced);\n await writeJsonAtomic(localPath, state.local);\n } finally {\n await release();\n }\n}\n\n/**\n * Run a load → mutate → save cycle under one lock so a single mutation\n * can't be clobbered by a racing reader-then-writer.\n */\nexport async function withWorkspace<T>(\n dir: string,\n fn: (state: WorkspaceState) => Promise<{ next: WorkspaceState; result?: T }>,\n options: SaveToFileOptions = {},\n): Promise<T | undefined> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n const syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n local = { ...local, attachmentCache: local.attachmentCache ?? {} };\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n const out = await fn({ synced, local });\n await writeJsonAtomic(syncedPath, out.next.synced);\n await writeJsonAtomic(localPath, out.next.local);\n return out.result;\n } finally {\n await release();\n }\n}\n\n// ---------------------------------------------------------------------------\n// internals\n// ---------------------------------------------------------------------------\n\n// File mode for workspace JSON: owner read/write only. Default `fs.writeFile`\n// uses 0o666 minus umask (typically 0o644 — world-readable). The workspace\n// docs carry the synced state (which after redaction is mostly safe to read\n// but still includes per-workspace metadata) and the local state (which\n// holds the encrypted Secret Vault payload table, session metadata, and the\n// vault entries themselves). On multi-user POSIX hosts (CI runners,\n// classroom VMs, shared dev servers) the default would leak both. 0o600\n// keeps the file owner-only. Windows ignores POSIX modes — the inherited\n// per-user ACL under %USERPROFILE% is what protects it there.\nconst WORKSPACE_FILE_MODE = 0o600;\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: WORKSPACE_FILE_MODE });\n }\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fs.writeFile(tmp, JSON.stringify(value, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: WORKSPACE_FILE_MODE,\n });\n await fs.rename(tmp, filePath);\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\nfunction createEmptyLocalForSynced(synced: WorkspaceSynced): WorkspaceLocal {\n return {\n schemaVersion: 1,\n workspaceId: synced.workspaceId,\n executionPlans: {},\n history: { requestRuns: [], planRuns: [] },\n secretIndex: { entries: {} },\n sessions: { github: { workspace: null, links: {} } },\n connectedRepo: null,\n workingBranch: null,\n seededWorkspaceSha: null,\n retiredBranch: null,\n sync: {\n lastPulledSnapshot: null,\n lastPulledSha: null,\n lastPulledAt: null,\n dirtyKeys: [],\n },\n linkedCollections: {},\n attachmentCache: {},\n globalContext: {},\n mockRuntime: { active: {} },\n ui: {\n activeRequestId: null,\n sidebarExpandedSections: [],\n themeId: 'one-dark-pro',\n fontId: 'system-sans',\n fontSizePercent: FONT_SIZE_PERCENT_DEFAULT,\n },\n settings: { validateOnSend: true, monacoConsumesWheel: false },\n snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 },\n };\n}\n"],"mappings":"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","names":["import_node_fs","path","import_proper_lockfile","fs","lockfile","fs","fs","isENOENT","ensureFile","lockfile"]}
|
|
1
|
+
{"version":3,"sources":["../../src/workspace/workspaceRegistry.ts","../../src/workspace/fileBackedWorkspace.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport { loadFromFile, saveToFile } from './fileBackedWorkspace';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// On-disk multi-workspace registry. All surfaces (desktop, CLI, MCP server,\n// VS Code extension) read the same `~/.apicircle/` root.\n//\n// Layout under `<root>/` (= ~/.apicircle or <repo>/.apicircle):\n//\n// registry.json ← this module's source of truth\n// workspace-<id-1>/\n// workspace.json\n// workspace.local.json\n// attachments/\n// workspace-<id-2>/\n// ...\n//\n// Each workspace lives in its own `workspace-<id>` directory so concurrent\n// writers (the desktop's mirror + a CLI invocation against a different\n// workspace) can't step on each other. The flat naming keeps the structure\n// identical whether there's one workspace or ten — no surprise layout\n// changes when multi-workspace is introduced. `proper-lockfile` still\n// guards the registry file itself when readers / writers race.\n// =============================================================================\n\nexport const REGISTRY_FILE = 'registry.json';\nexport const WORKSPACE_DIR_PREFIX = 'workspace-';\n\n/**\n * The universal root for all API Circle workspace data: `~/.apicircle/`.\n * Every consumer (desktop, CLI, MCP, VS Code) uses this as the default.\n */\nexport function defaultApicircleRoot(): string {\n return path.join(os.homedir(), '.apicircle');\n}\n\n/**\n * The apicircle root every surface should actually consult — honors the\n * `APICIRCLE_WORKSPACES_ROOT` override (CI, tests, and power users who relocate\n * their workspace store) before falling back to `~/.apicircle/`.\n *\n * Desktop, CLI, MCP, and the VS Code extension all resolve through here so a\n * relocated root stays consistent across surfaces. Prefer this over calling\n * `defaultApicircleRoot()` directly unless you specifically want to ignore the\n * override.\n */\nexport function resolveApicircleRoot(): string {\n const override = process.env.APICIRCLE_WORKSPACES_ROOT;\n if (override && override.length > 0) return path.resolve(override);\n return defaultApicircleRoot();\n}\n\nexport interface WorkspaceRegistryEntry {\n /** Matches the in-workspace `synced.workspaceId`. */\n id: string;\n /** Human-readable label. Local to this device — never pushed to git. */\n name: string;\n /** ISO timestamp; bumped every time this workspace is opened or written. */\n lastOpenedAt: string;\n /** ISO timestamp; set when the workspace was first registered. */\n createdAt: string;\n}\n\nexport interface WorkspaceRegistry {\n schemaVersion: 1;\n /** id of the workspace the desktop UI should boot into. `null` only on\n * an empty registry (no workspaces have been seeded yet). */\n activeWorkspaceId: string | null;\n workspaces: WorkspaceRegistryEntry[];\n}\n\n/** Default empty registry. */\nexport function emptyRegistry(): WorkspaceRegistry {\n return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };\n}\n\n/** Compute the directory inside `<root>/` that holds a workspace's files.\n * Layout: `<root>/workspace-<id>/`. */\nexport function workspaceDirFor(root: string, workspaceId: string): string {\n return path.join(root, `${WORKSPACE_DIR_PREFIX}${workspaceId}`);\n}\n\n/** Load the registry from disk; returns `null` if the file is missing. */\nexport async function loadRegistry(root: string): Promise<WorkspaceRegistry | null> {\n const filePath = path.join(root, REGISTRY_FILE);\n try {\n const raw = await fs.readFile(filePath, 'utf-8');\n const parsed = JSON.parse(raw) as WorkspaceRegistry;\n return normalizeRegistry(parsed);\n } catch (err) {\n if (isENOENT(err)) return null;\n throw err;\n }\n}\n\n/** Save the registry atomically (`<file>.tmp` + rename). */\nexport async function saveRegistry(root: string, registry: WorkspaceRegistry): Promise<void> {\n await fs.mkdir(root, { recursive: true });\n const filePath = path.join(root, REGISTRY_FILE);\n const tmp = `${filePath}.tmp`;\n await ensureFile(filePath);\n // Lock the registry file itself so two writers can't tear-write the JSON.\n const release = await lockfile.lock(filePath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: 30000,\n });\n try {\n await fs.writeFile(tmp, JSON.stringify(registry, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await fs.rename(tmp, filePath);\n } finally {\n await release();\n }\n}\n\n/**\n * Read a workspace's `{synced, local}` pair by id. Returns `null` if the\n * workspace subdirectory is missing OR its `workspace.json` is missing.\n * Used by the CLI / MCP / desktop reader.\n */\nexport async function loadWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceState | null> {\n return loadFromFile(workspaceDirFor(root, workspaceId), { allowMissing: true });\n}\n\n/**\n * Write a workspace's pair under `<root>/<id>/`. Idempotent; the directory\n * is created on first write. Bumps the registry entry's `lastOpenedAt`\n * and writes the registry back if the entry exists.\n */\nexport async function saveWorkspaceById(\n root: string,\n workspaceId: string,\n state: WorkspaceState,\n): Promise<void> {\n await saveToFile(workspaceDirFor(root, workspaceId), state);\n}\n\n/**\n * Remove a workspace from disk: deletes its directory, drops it from the\n * registry. If it was the active workspace, the next-most-recent remaining\n * workspace becomes active. If no workspaces remain, `activeWorkspaceId`\n * goes to `null` and the caller should seed a fresh workspace.\n */\nexport async function deleteWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const remaining = registry.workspaces.filter((w) => w.id !== workspaceId);\n await fs.rm(workspaceDirFor(root, workspaceId), { recursive: true, force: true });\n let nextActive = registry.activeWorkspaceId;\n if (nextActive === workspaceId) {\n nextActive =\n [...remaining].sort((a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt))[0]?.id ?? null;\n }\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: nextActive,\n workspaces: remaining,\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Add a workspace to the registry. Caller is responsible for having\n * written `workspace.json` first. Existing entry with the same id\n * is replaced (idempotent update). Newly-registered workspaces become\n * the active one when there is no prior active.\n */\nexport async function registerWorkspace(\n root: string,\n entry: WorkspaceRegistryEntry,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const otherEntries = registry.workspaces.filter((w) => w.id !== entry.id);\n const next: WorkspaceRegistry = {\n schemaVersion: 1,\n activeWorkspaceId: registry.activeWorkspaceId ?? entry.id,\n workspaces: [...otherEntries, entry],\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/** Set the active workspace id. Throws if the id isn't in the registry. */\nexport async function setActiveWorkspace(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n if (!registry.workspaces.some((w) => w.id === workspaceId)) {\n throw new Error(`workspace ${workspaceId} is not in the registry at ${root}`);\n }\n const now = new Date().toISOString();\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: workspaceId,\n workspaces: registry.workspaces.map((w) =>\n w.id === workspaceId ? { ...w, lastOpenedAt: now } : w,\n ),\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Look up a workspace registry entry by id OR by name (case-insensitive).\n * Used by the CLI's `--workspace <selector>` resolver — accepts both forms.\n * Returns `null` if no entry matches.\n */\nexport function findWorkspaceEntry(\n registry: WorkspaceRegistry,\n idOrName: string,\n): WorkspaceRegistryEntry | null {\n const exactId = registry.workspaces.find((w) => w.id === idOrName);\n if (exactId) return exactId;\n const lower = idOrName.toLowerCase();\n const byName = registry.workspaces.find((w) => w.name.toLowerCase() === lower);\n return byName ?? null;\n}\n\n/** Normalize a parsed registry so downstream code can rely on its shape. */\nfunction normalizeRegistry(raw: WorkspaceRegistry): WorkspaceRegistry {\n return {\n schemaVersion: 1,\n activeWorkspaceId: raw.activeWorkspaceId ?? null,\n workspaces: Array.isArray(raw.workspaces) ? raw.workspaces : [],\n };\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: 0o600 });\n }\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\n// Re-export commonly-used helpers so consumers can import from one place.\nexport type { WorkspaceLocal, WorkspaceSynced };\n","import { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport { FONT_SIZE_PERCENT_DEFAULT } from '@apicircle/shared';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// fileBackedWorkspace — load/save a `{ synced, local }` pair as two JSON\n// files on disk, with a `proper-lockfile` advisory lock so concurrent CLI /\n// MCP writers can't corrupt the document.\n//\n// Layout (relative to the directory passed in):\n// workspace.json ← matches WorkspaceSynced exactly\n// workspace.local.json ← WorkspaceLocal, host-private (CLI/MCP doesn't push)\n//\n// The lock is held on `workspace.json` because that's the file the\n// editor races against. Stale locks are released after 30s.\n// =============================================================================\n\nconst SYNCED_FILE = 'workspace.json';\nconst LOCAL_FILE = 'workspace.local.json';\n\nexport interface LoadFromFileOptions {\n /** When true, return `null` instead of throwing if the synced file is missing. */\n allowMissing?: boolean;\n /** Override the synced filename. Defaults to `workspace.json`. */\n syncedFilename?: string;\n}\n\nexport interface SaveToFileOptions {\n /** Lock timeout (ms). Defaults to 30000. */\n lockTimeoutMs?: number;\n /** Override the synced filename. Defaults to `workspace.json`. */\n syncedFilename?: string;\n}\n\n/**\n * Load both workspace documents from `dir`. The synced file is required;\n * the local file is optional and falls back to a minimal empty shape so a\n * CLI on a fresh machine can still operate (it just won't have history /\n * overrides until the desktop app runs once).\n */\nexport async function loadFromFile(\n dir: string,\n options: LoadFromFileOptions = {},\n): Promise<WorkspaceState | null> {\n const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n let syncedRaw: string;\n try {\n syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n } catch (err) {\n if (options.allowMissing && isENOENT(err)) return null;\n throw err;\n }\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n local = { ...local, attachmentCache: local.attachmentCache ?? {} };\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n\n return { synced, local };\n}\n\n/**\n * Atomically write both documents back to disk. Acquires an advisory lock\n * on the synced file for the duration of the write so a parallel CLI /\n * MCP / desktop save can't interleave.\n *\n * Both files are written via `<file>.tmp` + rename so a crash mid-write\n * never leaves a partial JSON document on disk.\n */\nexport async function saveToFile(\n dir: string,\n state: WorkspaceState,\n options: SaveToFileOptions = {},\n): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n\n // proper-lockfile requires the target file to exist. Touch it on first save.\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n await writeJsonAtomic(syncedPath, state.synced);\n await writeJsonAtomic(localPath, state.local);\n } finally {\n await release();\n }\n}\n\n/**\n * Run a load → mutate → save cycle under one lock so a single mutation\n * can't be clobbered by a racing reader-then-writer.\n */\nexport async function withWorkspace<T>(\n dir: string,\n fn: (state: WorkspaceState) => Promise<{ next: WorkspaceState; result?: T }>,\n options: SaveToFileOptions = {},\n): Promise<T | undefined> {\n await fs.mkdir(dir, { recursive: true });\n const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);\n const localPath = path.join(dir, LOCAL_FILE);\n await ensureFile(syncedPath);\n\n const release = await lockfile.lock(syncedPath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: options.lockTimeoutMs ?? 30000,\n });\n try {\n const syncedRaw = await fs.readFile(syncedPath, 'utf-8');\n const synced = JSON.parse(syncedRaw) as WorkspaceSynced;\n let local: WorkspaceLocal;\n try {\n local = JSON.parse(await fs.readFile(localPath, 'utf-8')) as WorkspaceLocal;\n local = { ...local, attachmentCache: local.attachmentCache ?? {} };\n } catch (err) {\n if (!isENOENT(err)) throw err;\n local = createEmptyLocalForSynced(synced);\n }\n const out = await fn({ synced, local });\n await writeJsonAtomic(syncedPath, out.next.synced);\n await writeJsonAtomic(localPath, out.next.local);\n return out.result;\n } finally {\n await release();\n }\n}\n\n// ---------------------------------------------------------------------------\n// internals\n// ---------------------------------------------------------------------------\n\n// File mode for workspace JSON: owner read/write only. Default `fs.writeFile`\n// uses 0o666 minus umask (typically 0o644 — world-readable). The workspace\n// docs carry the synced state (which after redaction is mostly safe to read\n// but still includes per-workspace metadata) and the local state (which\n// holds the encrypted Secret Vault payload table, session metadata, and the\n// vault entries themselves). On multi-user POSIX hosts (CI runners,\n// classroom VMs, shared dev servers) the default would leak both. 0o600\n// keeps the file owner-only. Windows ignores POSIX modes — the inherited\n// per-user ACL under %USERPROFILE% is what protects it there.\nconst WORKSPACE_FILE_MODE = 0o600;\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: WORKSPACE_FILE_MODE });\n }\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fs.writeFile(tmp, JSON.stringify(value, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: WORKSPACE_FILE_MODE,\n });\n await fs.rename(tmp, filePath);\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\nfunction createEmptyLocalForSynced(synced: WorkspaceSynced): WorkspaceLocal {\n return {\n schemaVersion: 1,\n workspaceId: synced.workspaceId,\n executionPlans: {},\n history: { requestRuns: [], planRuns: [] },\n secretIndex: { entries: {} },\n sessions: { github: { workspace: null, links: {} } },\n connectedRepo: null,\n workingBranch: null,\n seededWorkspaceSha: null,\n retiredBranch: null,\n sync: {\n lastPulledSnapshot: null,\n lastPulledSha: null,\n lastPulledAt: null,\n dirtyKeys: [],\n },\n linkedCollections: {},\n attachmentCache: {},\n globalContext: {},\n mockRuntime: { active: {} },\n ui: {\n activeRequestId: null,\n sidebarExpandedSections: [],\n themeId: 'one-dark-pro',\n fontId: 'system-sans',\n fontSizePercent: FONT_SIZE_PERCENT_DEFAULT,\n },\n settings: { validateOnSend: true, monacoConsumesWheel: false },\n snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 },\n };\n}\n"],"mappings":"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","names":["import_node_fs","path","import_proper_lockfile","fs","lockfile","fs","fs","isENOENT","ensureFile","lockfile"]}
|
|
@@ -8,6 +8,17 @@ declare const WORKSPACE_DIR_PREFIX = "workspace-";
|
|
|
8
8
|
* Every consumer (desktop, CLI, MCP, VS Code) uses this as the default.
|
|
9
9
|
*/
|
|
10
10
|
declare function defaultApicircleRoot(): string;
|
|
11
|
+
/**
|
|
12
|
+
* The apicircle root every surface should actually consult — honors the
|
|
13
|
+
* `APICIRCLE_WORKSPACES_ROOT` override (CI, tests, and power users who relocate
|
|
14
|
+
* their workspace store) before falling back to `~/.apicircle/`.
|
|
15
|
+
*
|
|
16
|
+
* Desktop, CLI, MCP, and the VS Code extension all resolve through here so a
|
|
17
|
+
* relocated root stays consistent across surfaces. Prefer this over calling
|
|
18
|
+
* `defaultApicircleRoot()` directly unless you specifically want to ignore the
|
|
19
|
+
* override.
|
|
20
|
+
*/
|
|
21
|
+
declare function resolveApicircleRoot(): string;
|
|
11
22
|
interface WorkspaceRegistryEntry {
|
|
12
23
|
/** Matches the in-workspace `synced.workspaceId`. */
|
|
13
24
|
id: string;
|
|
@@ -69,4 +80,4 @@ declare function setActiveWorkspace(root: string, workspaceId: string): Promise<
|
|
|
69
80
|
*/
|
|
70
81
|
declare function findWorkspaceEntry(registry: WorkspaceRegistry, idOrName: string): WorkspaceRegistryEntry | null;
|
|
71
82
|
|
|
72
|
-
export { REGISTRY_FILE, WORKSPACE_DIR_PREFIX, type WorkspaceRegistry, type WorkspaceRegistryEntry, defaultApicircleRoot, deleteWorkspaceById, emptyRegistry, findWorkspaceEntry, loadRegistry, loadWorkspaceById, registerWorkspace, saveRegistry, saveWorkspaceById, setActiveWorkspace, workspaceDirFor };
|
|
83
|
+
export { REGISTRY_FILE, WORKSPACE_DIR_PREFIX, type WorkspaceRegistry, type WorkspaceRegistryEntry, defaultApicircleRoot, deleteWorkspaceById, emptyRegistry, findWorkspaceEntry, loadRegistry, loadWorkspaceById, registerWorkspace, resolveApicircleRoot, saveRegistry, saveWorkspaceById, setActiveWorkspace, workspaceDirFor };
|
|
@@ -8,6 +8,17 @@ declare const WORKSPACE_DIR_PREFIX = "workspace-";
|
|
|
8
8
|
* Every consumer (desktop, CLI, MCP, VS Code) uses this as the default.
|
|
9
9
|
*/
|
|
10
10
|
declare function defaultApicircleRoot(): string;
|
|
11
|
+
/**
|
|
12
|
+
* The apicircle root every surface should actually consult — honors the
|
|
13
|
+
* `APICIRCLE_WORKSPACES_ROOT` override (CI, tests, and power users who relocate
|
|
14
|
+
* their workspace store) before falling back to `~/.apicircle/`.
|
|
15
|
+
*
|
|
16
|
+
* Desktop, CLI, MCP, and the VS Code extension all resolve through here so a
|
|
17
|
+
* relocated root stays consistent across surfaces. Prefer this over calling
|
|
18
|
+
* `defaultApicircleRoot()` directly unless you specifically want to ignore the
|
|
19
|
+
* override.
|
|
20
|
+
*/
|
|
21
|
+
declare function resolveApicircleRoot(): string;
|
|
11
22
|
interface WorkspaceRegistryEntry {
|
|
12
23
|
/** Matches the in-workspace `synced.workspaceId`. */
|
|
13
24
|
id: string;
|
|
@@ -69,4 +80,4 @@ declare function setActiveWorkspace(root: string, workspaceId: string): Promise<
|
|
|
69
80
|
*/
|
|
70
81
|
declare function findWorkspaceEntry(registry: WorkspaceRegistry, idOrName: string): WorkspaceRegistryEntry | null;
|
|
71
82
|
|
|
72
|
-
export { REGISTRY_FILE, WORKSPACE_DIR_PREFIX, type WorkspaceRegistry, type WorkspaceRegistryEntry, defaultApicircleRoot, deleteWorkspaceById, emptyRegistry, findWorkspaceEntry, loadRegistry, loadWorkspaceById, registerWorkspace, saveRegistry, saveWorkspaceById, setActiveWorkspace, workspaceDirFor };
|
|
83
|
+
export { REGISTRY_FILE, WORKSPACE_DIR_PREFIX, type WorkspaceRegistry, type WorkspaceRegistryEntry, defaultApicircleRoot, deleteWorkspaceById, emptyRegistry, findWorkspaceEntry, loadRegistry, loadWorkspaceById, registerWorkspace, resolveApicircleRoot, saveRegistry, saveWorkspaceById, setActiveWorkspace, workspaceDirFor };
|
|
@@ -13,6 +13,11 @@ var WORKSPACE_DIR_PREFIX = "workspace-";
|
|
|
13
13
|
function defaultApicircleRoot() {
|
|
14
14
|
return path.join(os.homedir(), ".apicircle");
|
|
15
15
|
}
|
|
16
|
+
function resolveApicircleRoot() {
|
|
17
|
+
const override = process.env.APICIRCLE_WORKSPACES_ROOT;
|
|
18
|
+
if (override && override.length > 0) return path.resolve(override);
|
|
19
|
+
return defaultApicircleRoot();
|
|
20
|
+
}
|
|
16
21
|
function emptyRegistry() {
|
|
17
22
|
return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };
|
|
18
23
|
}
|
|
@@ -133,6 +138,7 @@ export {
|
|
|
133
138
|
loadRegistry,
|
|
134
139
|
loadWorkspaceById,
|
|
135
140
|
registerWorkspace,
|
|
141
|
+
resolveApicircleRoot,
|
|
136
142
|
saveRegistry,
|
|
137
143
|
saveWorkspaceById,
|
|
138
144
|
setActiveWorkspace,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/workspace/workspaceRegistry.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport { loadFromFile, saveToFile } from './fileBackedWorkspace';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// On-disk multi-workspace registry. All surfaces (desktop, CLI, MCP server,\n// VS Code extension) read the same `~/.apicircle/` root.\n//\n// Layout under `<root>/` (= ~/.apicircle or <repo>/.apicircle):\n//\n// registry.json ← this module's source of truth\n// workspace-<id-1>/\n// workspace.json\n// workspace.local.json\n// attachments/\n// workspace-<id-2>/\n// ...\n//\n// Each workspace lives in its own `workspace-<id>` directory so concurrent\n// writers (the desktop's mirror + a CLI invocation against a different\n// workspace) can't step on each other. The flat naming keeps the structure\n// identical whether there's one workspace or ten — no surprise layout\n// changes when multi-workspace is introduced. `proper-lockfile` still\n// guards the registry file itself when readers / writers race.\n// =============================================================================\n\nexport const REGISTRY_FILE = 'registry.json';\nexport const WORKSPACE_DIR_PREFIX = 'workspace-';\n\n/**\n * The universal root for all API Circle workspace data: `~/.apicircle/`.\n * Every consumer (desktop, CLI, MCP, VS Code) uses this as the default.\n */\nexport function defaultApicircleRoot(): string {\n return path.join(os.homedir(), '.apicircle');\n}\n\nexport interface WorkspaceRegistryEntry {\n /** Matches the in-workspace `synced.workspaceId`. */\n id: string;\n /** Human-readable label. Local to this device — never pushed to git. */\n name: string;\n /** ISO timestamp; bumped every time this workspace is opened or written. */\n lastOpenedAt: string;\n /** ISO timestamp; set when the workspace was first registered. */\n createdAt: string;\n}\n\nexport interface WorkspaceRegistry {\n schemaVersion: 1;\n /** id of the workspace the desktop UI should boot into. `null` only on\n * an empty registry (no workspaces have been seeded yet). */\n activeWorkspaceId: string | null;\n workspaces: WorkspaceRegistryEntry[];\n}\n\n/** Default empty registry. */\nexport function emptyRegistry(): WorkspaceRegistry {\n return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };\n}\n\n/** Compute the directory inside `<root>/` that holds a workspace's files.\n * Layout: `<root>/workspace-<id>/`. */\nexport function workspaceDirFor(root: string, workspaceId: string): string {\n return path.join(root, `${WORKSPACE_DIR_PREFIX}${workspaceId}`);\n}\n\n/** Load the registry from disk; returns `null` if the file is missing. */\nexport async function loadRegistry(root: string): Promise<WorkspaceRegistry | null> {\n const filePath = path.join(root, REGISTRY_FILE);\n try {\n const raw = await fs.readFile(filePath, 'utf-8');\n const parsed = JSON.parse(raw) as WorkspaceRegistry;\n return normalizeRegistry(parsed);\n } catch (err) {\n if (isENOENT(err)) return null;\n throw err;\n }\n}\n\n/** Save the registry atomically (`<file>.tmp` + rename). */\nexport async function saveRegistry(root: string, registry: WorkspaceRegistry): Promise<void> {\n await fs.mkdir(root, { recursive: true });\n const filePath = path.join(root, REGISTRY_FILE);\n const tmp = `${filePath}.tmp`;\n await ensureFile(filePath);\n // Lock the registry file itself so two writers can't tear-write the JSON.\n const release = await lockfile.lock(filePath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: 30000,\n });\n try {\n await fs.writeFile(tmp, JSON.stringify(registry, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await fs.rename(tmp, filePath);\n } finally {\n await release();\n }\n}\n\n/**\n * Read a workspace's `{synced, local}` pair by id. Returns `null` if the\n * workspace subdirectory is missing OR its `workspace.json` is missing.\n * Used by the CLI / MCP / desktop reader.\n */\nexport async function loadWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceState | null> {\n return loadFromFile(workspaceDirFor(root, workspaceId), { allowMissing: true });\n}\n\n/**\n * Write a workspace's pair under `<root>/<id>/`. Idempotent; the directory\n * is created on first write. Bumps the registry entry's `lastOpenedAt`\n * and writes the registry back if the entry exists.\n */\nexport async function saveWorkspaceById(\n root: string,\n workspaceId: string,\n state: WorkspaceState,\n): Promise<void> {\n await saveToFile(workspaceDirFor(root, workspaceId), state);\n}\n\n/**\n * Remove a workspace from disk: deletes its directory, drops it from the\n * registry. If it was the active workspace, the next-most-recent remaining\n * workspace becomes active. If no workspaces remain, `activeWorkspaceId`\n * goes to `null` and the caller should seed a fresh workspace.\n */\nexport async function deleteWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const remaining = registry.workspaces.filter((w) => w.id !== workspaceId);\n await fs.rm(workspaceDirFor(root, workspaceId), { recursive: true, force: true });\n let nextActive = registry.activeWorkspaceId;\n if (nextActive === workspaceId) {\n nextActive =\n [...remaining].sort((a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt))[0]?.id ?? null;\n }\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: nextActive,\n workspaces: remaining,\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Add a workspace to the registry. Caller is responsible for having\n * written `workspace.json` first. Existing entry with the same id\n * is replaced (idempotent update). Newly-registered workspaces become\n * the active one when there is no prior active.\n */\nexport async function registerWorkspace(\n root: string,\n entry: WorkspaceRegistryEntry,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const otherEntries = registry.workspaces.filter((w) => w.id !== entry.id);\n const next: WorkspaceRegistry = {\n schemaVersion: 1,\n activeWorkspaceId: registry.activeWorkspaceId ?? entry.id,\n workspaces: [...otherEntries, entry],\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/** Set the active workspace id. Throws if the id isn't in the registry. */\nexport async function setActiveWorkspace(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n if (!registry.workspaces.some((w) => w.id === workspaceId)) {\n throw new Error(`workspace ${workspaceId} is not in the registry at ${root}`);\n }\n const now = new Date().toISOString();\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: workspaceId,\n workspaces: registry.workspaces.map((w) =>\n w.id === workspaceId ? { ...w, lastOpenedAt: now } : w,\n ),\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Look up a workspace registry entry by id OR by name (case-insensitive).\n * Used by the CLI's `--workspace <selector>` resolver — accepts both forms.\n * Returns `null` if no entry matches.\n */\nexport function findWorkspaceEntry(\n registry: WorkspaceRegistry,\n idOrName: string,\n): WorkspaceRegistryEntry | null {\n const exactId = registry.workspaces.find((w) => w.id === idOrName);\n if (exactId) return exactId;\n const lower = idOrName.toLowerCase();\n const byName = registry.workspaces.find((w) => w.name.toLowerCase() === lower);\n return byName ?? null;\n}\n\n/** Normalize a parsed registry so downstream code can rely on its shape. */\nfunction normalizeRegistry(raw: WorkspaceRegistry): WorkspaceRegistry {\n return {\n schemaVersion: 1,\n activeWorkspaceId: raw.activeWorkspaceId ?? null,\n workspaces: Array.isArray(raw.workspaces) ? raw.workspaces : [],\n };\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: 0o600 });\n }\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\n// Re-export commonly-used helpers so consumers can import from one place.\nexport type { WorkspaceLocal, WorkspaceSynced };\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/workspace/workspaceRegistry.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport lockfile from 'proper-lockfile';\nimport type { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';\nimport { loadFromFile, saveToFile } from './fileBackedWorkspace';\nimport type { WorkspaceState } from './patches';\n\n// =============================================================================\n// On-disk multi-workspace registry. All surfaces (desktop, CLI, MCP server,\n// VS Code extension) read the same `~/.apicircle/` root.\n//\n// Layout under `<root>/` (= ~/.apicircle or <repo>/.apicircle):\n//\n// registry.json ← this module's source of truth\n// workspace-<id-1>/\n// workspace.json\n// workspace.local.json\n// attachments/\n// workspace-<id-2>/\n// ...\n//\n// Each workspace lives in its own `workspace-<id>` directory so concurrent\n// writers (the desktop's mirror + a CLI invocation against a different\n// workspace) can't step on each other. The flat naming keeps the structure\n// identical whether there's one workspace or ten — no surprise layout\n// changes when multi-workspace is introduced. `proper-lockfile` still\n// guards the registry file itself when readers / writers race.\n// =============================================================================\n\nexport const REGISTRY_FILE = 'registry.json';\nexport const WORKSPACE_DIR_PREFIX = 'workspace-';\n\n/**\n * The universal root for all API Circle workspace data: `~/.apicircle/`.\n * Every consumer (desktop, CLI, MCP, VS Code) uses this as the default.\n */\nexport function defaultApicircleRoot(): string {\n return path.join(os.homedir(), '.apicircle');\n}\n\n/**\n * The apicircle root every surface should actually consult — honors the\n * `APICIRCLE_WORKSPACES_ROOT` override (CI, tests, and power users who relocate\n * their workspace store) before falling back to `~/.apicircle/`.\n *\n * Desktop, CLI, MCP, and the VS Code extension all resolve through here so a\n * relocated root stays consistent across surfaces. Prefer this over calling\n * `defaultApicircleRoot()` directly unless you specifically want to ignore the\n * override.\n */\nexport function resolveApicircleRoot(): string {\n const override = process.env.APICIRCLE_WORKSPACES_ROOT;\n if (override && override.length > 0) return path.resolve(override);\n return defaultApicircleRoot();\n}\n\nexport interface WorkspaceRegistryEntry {\n /** Matches the in-workspace `synced.workspaceId`. */\n id: string;\n /** Human-readable label. Local to this device — never pushed to git. */\n name: string;\n /** ISO timestamp; bumped every time this workspace is opened or written. */\n lastOpenedAt: string;\n /** ISO timestamp; set when the workspace was first registered. */\n createdAt: string;\n}\n\nexport interface WorkspaceRegistry {\n schemaVersion: 1;\n /** id of the workspace the desktop UI should boot into. `null` only on\n * an empty registry (no workspaces have been seeded yet). */\n activeWorkspaceId: string | null;\n workspaces: WorkspaceRegistryEntry[];\n}\n\n/** Default empty registry. */\nexport function emptyRegistry(): WorkspaceRegistry {\n return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };\n}\n\n/** Compute the directory inside `<root>/` that holds a workspace's files.\n * Layout: `<root>/workspace-<id>/`. */\nexport function workspaceDirFor(root: string, workspaceId: string): string {\n return path.join(root, `${WORKSPACE_DIR_PREFIX}${workspaceId}`);\n}\n\n/** Load the registry from disk; returns `null` if the file is missing. */\nexport async function loadRegistry(root: string): Promise<WorkspaceRegistry | null> {\n const filePath = path.join(root, REGISTRY_FILE);\n try {\n const raw = await fs.readFile(filePath, 'utf-8');\n const parsed = JSON.parse(raw) as WorkspaceRegistry;\n return normalizeRegistry(parsed);\n } catch (err) {\n if (isENOENT(err)) return null;\n throw err;\n }\n}\n\n/** Save the registry atomically (`<file>.tmp` + rename). */\nexport async function saveRegistry(root: string, registry: WorkspaceRegistry): Promise<void> {\n await fs.mkdir(root, { recursive: true });\n const filePath = path.join(root, REGISTRY_FILE);\n const tmp = `${filePath}.tmp`;\n await ensureFile(filePath);\n // Lock the registry file itself so two writers can't tear-write the JSON.\n const release = await lockfile.lock(filePath, {\n retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },\n stale: 30000,\n });\n try {\n await fs.writeFile(tmp, JSON.stringify(registry, null, 2) + '\\n', {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await fs.rename(tmp, filePath);\n } finally {\n await release();\n }\n}\n\n/**\n * Read a workspace's `{synced, local}` pair by id. Returns `null` if the\n * workspace subdirectory is missing OR its `workspace.json` is missing.\n * Used by the CLI / MCP / desktop reader.\n */\nexport async function loadWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceState | null> {\n return loadFromFile(workspaceDirFor(root, workspaceId), { allowMissing: true });\n}\n\n/**\n * Write a workspace's pair under `<root>/<id>/`. Idempotent; the directory\n * is created on first write. Bumps the registry entry's `lastOpenedAt`\n * and writes the registry back if the entry exists.\n */\nexport async function saveWorkspaceById(\n root: string,\n workspaceId: string,\n state: WorkspaceState,\n): Promise<void> {\n await saveToFile(workspaceDirFor(root, workspaceId), state);\n}\n\n/**\n * Remove a workspace from disk: deletes its directory, drops it from the\n * registry. If it was the active workspace, the next-most-recent remaining\n * workspace becomes active. If no workspaces remain, `activeWorkspaceId`\n * goes to `null` and the caller should seed a fresh workspace.\n */\nexport async function deleteWorkspaceById(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const remaining = registry.workspaces.filter((w) => w.id !== workspaceId);\n await fs.rm(workspaceDirFor(root, workspaceId), { recursive: true, force: true });\n let nextActive = registry.activeWorkspaceId;\n if (nextActive === workspaceId) {\n nextActive =\n [...remaining].sort((a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt))[0]?.id ?? null;\n }\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: nextActive,\n workspaces: remaining,\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Add a workspace to the registry. Caller is responsible for having\n * written `workspace.json` first. Existing entry with the same id\n * is replaced (idempotent update). Newly-registered workspaces become\n * the active one when there is no prior active.\n */\nexport async function registerWorkspace(\n root: string,\n entry: WorkspaceRegistryEntry,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n const otherEntries = registry.workspaces.filter((w) => w.id !== entry.id);\n const next: WorkspaceRegistry = {\n schemaVersion: 1,\n activeWorkspaceId: registry.activeWorkspaceId ?? entry.id,\n workspaces: [...otherEntries, entry],\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/** Set the active workspace id. Throws if the id isn't in the registry. */\nexport async function setActiveWorkspace(\n root: string,\n workspaceId: string,\n): Promise<WorkspaceRegistry> {\n const registry = (await loadRegistry(root)) ?? emptyRegistry();\n if (!registry.workspaces.some((w) => w.id === workspaceId)) {\n throw new Error(`workspace ${workspaceId} is not in the registry at ${root}`);\n }\n const now = new Date().toISOString();\n const next: WorkspaceRegistry = {\n ...registry,\n activeWorkspaceId: workspaceId,\n workspaces: registry.workspaces.map((w) =>\n w.id === workspaceId ? { ...w, lastOpenedAt: now } : w,\n ),\n };\n await saveRegistry(root, next);\n return next;\n}\n\n/**\n * Look up a workspace registry entry by id OR by name (case-insensitive).\n * Used by the CLI's `--workspace <selector>` resolver — accepts both forms.\n * Returns `null` if no entry matches.\n */\nexport function findWorkspaceEntry(\n registry: WorkspaceRegistry,\n idOrName: string,\n): WorkspaceRegistryEntry | null {\n const exactId = registry.workspaces.find((w) => w.id === idOrName);\n if (exactId) return exactId;\n const lower = idOrName.toLowerCase();\n const byName = registry.workspaces.find((w) => w.name.toLowerCase() === lower);\n return byName ?? null;\n}\n\n/** Normalize a parsed registry so downstream code can rely on its shape. */\nfunction normalizeRegistry(raw: WorkspaceRegistry): WorkspaceRegistry {\n return {\n schemaVersion: 1,\n activeWorkspaceId: raw.activeWorkspaceId ?? null,\n workspaces: Array.isArray(raw.workspaces) ? raw.workspaces : [],\n };\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath);\n } catch (err) {\n if (!isENOENT(err)) throw err;\n await fs.writeFile(filePath, '{}', { encoding: 'utf-8', mode: 0o600 });\n }\n}\n\nfunction isENOENT(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\n// Re-export commonly-used helpers so consumers can import from one place.\nexport type { WorkspaceLocal, WorkspaceSynced };\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apicircle/core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
],
|
|
92
92
|
"dependencies": {
|
|
93
93
|
"proper-lockfile": "^4.1.2",
|
|
94
|
-
"@apicircle/shared": "1.1.
|
|
94
|
+
"@apicircle/shared": "1.1.3"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"@types/proper-lockfile": "^4.1.4",
|