@apicircle/core 1.0.9 → 1.1.0

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.
@@ -1,4 +1,4 @@
1
- import { RequestAuth, GlobalSchema, GlobalGraphQL, GlobalFileAsset, Folder, Request, WorkspaceSynced, WorkspaceLocal, Environment, EnvPriorityRef, SecretKeyMeta, Assertion, MockServer, AssetGitRef, ExecutionPlan, WorkspaceSnapshotTrigger } from '@apicircle/shared';
1
+ import { RequestAuth, GlobalSchema, GlobalGraphQL, GlobalFileAsset, Folder, Request, WorkspaceSynced, WorkspaceLocal, Environment, EnvPriorityRef, SecretKeyMeta, SecretCryptoMeta, Assertion, MockServer, ReleaseVersion, LinkedWorkspace, ReleaseHistory, LinkedSnapshot, RequestOverride, EnvironmentVariableOverride, AssetGitRef, ExecutionPlan, WorkspaceSnapshotTrigger } from '@apicircle/shared';
2
2
 
3
3
  /**
4
4
  * One credential-bearing field discovered inside an export envelope.
@@ -240,6 +240,10 @@ type WorkspacePatch = {
240
240
  kind: 'folder.move';
241
241
  id: string;
242
242
  newParentId: string | null;
243
+ } | {
244
+ kind: 'folder.update';
245
+ id: string;
246
+ patch: Partial<Pick<Folder, 'name' | 'auth'>>;
243
247
  } | {
244
248
  kind: 'folder.import_apicircle';
245
249
  parsed: ParsedApicircleFolderExport;
@@ -259,6 +263,11 @@ type WorkspacePatch = {
259
263
  } | {
260
264
  kind: 'secretKey.upsert';
261
265
  meta: SecretKeyMeta;
266
+ } | {
267
+ kind: 'secret.crypto.set';
268
+ crypto: SecretCryptoMeta;
269
+ } | {
270
+ kind: 'secret.crypto.clear';
262
271
  } | {
263
272
  kind: 'assertion.upsert';
264
273
  requestId: string;
@@ -273,6 +282,49 @@ type WorkspacePatch = {
273
282
  } | {
274
283
  kind: 'mock.delete';
275
284
  id: string;
285
+ } | {
286
+ kind: 'release.publish';
287
+ entry: ReleaseVersion;
288
+ } | {
289
+ kind: 'release.deprecate';
290
+ version: string;
291
+ } | {
292
+ kind: 'release.yank';
293
+ version: string;
294
+ } | {
295
+ kind: 'linkedWorkspace.upsert';
296
+ link: LinkedWorkspace;
297
+ ledger?: ReleaseHistory;
298
+ snapshot?: LinkedSnapshot;
299
+ } | {
300
+ kind: 'linkedWorkspace.remove';
301
+ id: string;
302
+ } | {
303
+ kind: 'linkedWorkspace.applyUpdate';
304
+ id: string;
305
+ pinnedVersion: string | null;
306
+ snapshot: LinkedSnapshot;
307
+ ledger: ReleaseHistory;
308
+ requestOverrides: RequestOverride[];
309
+ envVarOverrides: EnvironmentVariableOverride[];
310
+ } | {
311
+ kind: 'linkedOverride.setRequest';
312
+ override: RequestOverride;
313
+ } | {
314
+ kind: 'linkedOverride.removeRequest';
315
+ linkedWorkspaceId: string;
316
+ itemId: string;
317
+ } | {
318
+ kind: 'linkedOverride.setEnvVar';
319
+ override: EnvironmentVariableOverride;
320
+ } | {
321
+ kind: 'linkedOverride.removeEnvVar';
322
+ linkedWorkspaceId: string;
323
+ envName: string;
324
+ varKey: string;
325
+ } | {
326
+ kind: 'linkedOverride.clearForLink';
327
+ linkedWorkspaceId: string;
276
328
  } | {
277
329
  kind: 'globalAsset.upsertFile';
278
330
  file: GlobalFileAsset;
@@ -1,4 +1,4 @@
1
- import { RequestAuth, GlobalSchema, GlobalGraphQL, GlobalFileAsset, Folder, Request, WorkspaceSynced, WorkspaceLocal, Environment, EnvPriorityRef, SecretKeyMeta, Assertion, MockServer, AssetGitRef, ExecutionPlan, WorkspaceSnapshotTrigger } from '@apicircle/shared';
1
+ import { RequestAuth, GlobalSchema, GlobalGraphQL, GlobalFileAsset, Folder, Request, WorkspaceSynced, WorkspaceLocal, Environment, EnvPriorityRef, SecretKeyMeta, SecretCryptoMeta, Assertion, MockServer, ReleaseVersion, LinkedWorkspace, ReleaseHistory, LinkedSnapshot, RequestOverride, EnvironmentVariableOverride, AssetGitRef, ExecutionPlan, WorkspaceSnapshotTrigger } from '@apicircle/shared';
2
2
 
3
3
  /**
4
4
  * One credential-bearing field discovered inside an export envelope.
@@ -240,6 +240,10 @@ type WorkspacePatch = {
240
240
  kind: 'folder.move';
241
241
  id: string;
242
242
  newParentId: string | null;
243
+ } | {
244
+ kind: 'folder.update';
245
+ id: string;
246
+ patch: Partial<Pick<Folder, 'name' | 'auth'>>;
243
247
  } | {
244
248
  kind: 'folder.import_apicircle';
245
249
  parsed: ParsedApicircleFolderExport;
@@ -259,6 +263,11 @@ type WorkspacePatch = {
259
263
  } | {
260
264
  kind: 'secretKey.upsert';
261
265
  meta: SecretKeyMeta;
266
+ } | {
267
+ kind: 'secret.crypto.set';
268
+ crypto: SecretCryptoMeta;
269
+ } | {
270
+ kind: 'secret.crypto.clear';
262
271
  } | {
263
272
  kind: 'assertion.upsert';
264
273
  requestId: string;
@@ -273,6 +282,49 @@ type WorkspacePatch = {
273
282
  } | {
274
283
  kind: 'mock.delete';
275
284
  id: string;
285
+ } | {
286
+ kind: 'release.publish';
287
+ entry: ReleaseVersion;
288
+ } | {
289
+ kind: 'release.deprecate';
290
+ version: string;
291
+ } | {
292
+ kind: 'release.yank';
293
+ version: string;
294
+ } | {
295
+ kind: 'linkedWorkspace.upsert';
296
+ link: LinkedWorkspace;
297
+ ledger?: ReleaseHistory;
298
+ snapshot?: LinkedSnapshot;
299
+ } | {
300
+ kind: 'linkedWorkspace.remove';
301
+ id: string;
302
+ } | {
303
+ kind: 'linkedWorkspace.applyUpdate';
304
+ id: string;
305
+ pinnedVersion: string | null;
306
+ snapshot: LinkedSnapshot;
307
+ ledger: ReleaseHistory;
308
+ requestOverrides: RequestOverride[];
309
+ envVarOverrides: EnvironmentVariableOverride[];
310
+ } | {
311
+ kind: 'linkedOverride.setRequest';
312
+ override: RequestOverride;
313
+ } | {
314
+ kind: 'linkedOverride.removeRequest';
315
+ linkedWorkspaceId: string;
316
+ itemId: string;
317
+ } | {
318
+ kind: 'linkedOverride.setEnvVar';
319
+ override: EnvironmentVariableOverride;
320
+ } | {
321
+ kind: 'linkedOverride.removeEnvVar';
322
+ linkedWorkspaceId: string;
323
+ envName: string;
324
+ varKey: string;
325
+ } | {
326
+ kind: 'linkedOverride.clearForLink';
327
+ linkedWorkspaceId: string;
276
328
  } | {
277
329
  kind: 'globalAsset.upsertFile';
278
330
  file: GlobalFileAsset;
@@ -39,10 +39,10 @@ var import_node_fs = require("fs");
39
39
  var path = __toESM(require("path"), 1);
40
40
  var import_shared = require("@apicircle/shared");
41
41
  var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
42
- var SYNCED_FILE = "workspace.synced.json";
42
+ var SYNCED_FILE = "workspace.json";
43
43
  var LOCAL_FILE = "workspace.local.json";
44
44
  async function loadFromFile(dir, options = {}) {
45
- const syncedPath = path.join(dir, SYNCED_FILE);
45
+ const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);
46
46
  const localPath = path.join(dir, LOCAL_FILE);
47
47
  let syncedRaw;
48
48
  try {
@@ -64,7 +64,7 @@ async function loadFromFile(dir, options = {}) {
64
64
  }
65
65
  async function saveToFile(dir, state, options = {}) {
66
66
  await import_node_fs.promises.mkdir(dir, { recursive: true });
67
- const syncedPath = path.join(dir, SYNCED_FILE);
67
+ const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);
68
68
  const localPath = path.join(dir, LOCAL_FILE);
69
69
  await ensureFile(syncedPath);
70
70
  const release = await import_proper_lockfile.default.lock(syncedPath, {
@@ -80,7 +80,7 @@ async function saveToFile(dir, state, options = {}) {
80
80
  }
81
81
  async function withWorkspace(dir, fn, options = {}) {
82
82
  await import_node_fs.promises.mkdir(dir, { recursive: true });
83
- const syncedPath = path.join(dir, SYNCED_FILE);
83
+ const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);
84
84
  const localPath = path.join(dir, LOCAL_FILE);
85
85
  await ensureFile(syncedPath);
86
86
  const release = await import_proper_lockfile.default.lock(syncedPath, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/workspace/fileBackedWorkspace.ts"],"sourcesContent":["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.synced.json ← matches WorkspaceSynced exactly, push-to-git target\n// workspace.local.json ← WorkspaceLocal, host-private (CLI/MCP doesn't push)\n//\n// The lock is held on `workspace.synced.json` because that's the file the\n// editor races against. Stale locks are released after 30s.\n// =============================================================================\n\nconst SYNCED_FILE = 'workspace.synced.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}\n\nexport interface SaveToFileOptions {\n /** Lock timeout (ms). Defaults to 30000. */\n lockTimeoutMs?: number;\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, 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, 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, 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":["fs","lockfile"]}
1
+ {"version":3,"sources":["../../src/workspace/fileBackedWorkspace.ts"],"sourcesContent":["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":["fs","lockfile"]}
@@ -1,13 +1,17 @@
1
- import { W as WorkspaceState } from '../patches-h_K1VcZR.cjs';
1
+ import { W as WorkspaceState } from '../patches-B3VGNVgf.cjs';
2
2
  import '@apicircle/shared';
3
3
 
4
4
  interface LoadFromFileOptions {
5
5
  /** When true, return `null` instead of throwing if the synced file is missing. */
6
6
  allowMissing?: boolean;
7
+ /** Override the synced filename. Defaults to `workspace.json`. */
8
+ syncedFilename?: string;
7
9
  }
8
10
  interface SaveToFileOptions {
9
11
  /** Lock timeout (ms). Defaults to 30000. */
10
12
  lockTimeoutMs?: number;
13
+ /** Override the synced filename. Defaults to `workspace.json`. */
14
+ syncedFilename?: string;
11
15
  }
12
16
  /**
13
17
  * Load both workspace documents from `dir`. The synced file is required;
@@ -1,13 +1,17 @@
1
- import { W as WorkspaceState } from '../patches-h_K1VcZR.js';
1
+ import { W as WorkspaceState } from '../patches-B3VGNVgf.js';
2
2
  import '@apicircle/shared';
3
3
 
4
4
  interface LoadFromFileOptions {
5
5
  /** When true, return `null` instead of throwing if the synced file is missing. */
6
6
  allowMissing?: boolean;
7
+ /** Override the synced filename. Defaults to `workspace.json`. */
8
+ syncedFilename?: string;
7
9
  }
8
10
  interface SaveToFileOptions {
9
11
  /** Lock timeout (ms). Defaults to 30000. */
10
12
  lockTimeoutMs?: number;
13
+ /** Override the synced filename. Defaults to `workspace.json`. */
14
+ syncedFilename?: string;
11
15
  }
12
16
  /**
13
17
  * Load both workspace documents from `dir`. The synced file is required;
@@ -2,7 +2,7 @@ import {
2
2
  loadFromFile,
3
3
  saveToFile,
4
4
  withWorkspace
5
- } from "../chunk-L5DQT7V6.js";
5
+ } from "../chunk-T6A4ICRL.js";
6
6
  export {
7
7
  loadFromFile,
8
8
  saveToFile,
@@ -31,12 +31,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var workspaceRegistry_exports = {};
32
32
  __export(workspaceRegistry_exports, {
33
33
  REGISTRY_FILE: () => REGISTRY_FILE,
34
+ WORKSPACE_DIR_PREFIX: () => WORKSPACE_DIR_PREFIX,
35
+ defaultApicircleRoot: () => defaultApicircleRoot,
34
36
  deleteWorkspaceById: () => deleteWorkspaceById,
35
37
  emptyRegistry: () => emptyRegistry,
36
38
  findWorkspaceEntry: () => findWorkspaceEntry,
37
39
  loadRegistry: () => loadRegistry,
38
40
  loadWorkspaceById: () => loadWorkspaceById,
39
- migrateLegacyWorkspace: () => migrateLegacyWorkspace,
40
41
  registerWorkspace: () => registerWorkspace,
41
42
  saveRegistry: () => saveRegistry,
42
43
  saveWorkspaceById: () => saveWorkspaceById,
@@ -45,6 +46,7 @@ __export(workspaceRegistry_exports, {
45
46
  });
46
47
  module.exports = __toCommonJS(workspaceRegistry_exports);
47
48
  var import_node_fs2 = require("fs");
49
+ var os = __toESM(require("os"), 1);
48
50
  var path2 = __toESM(require("path"), 1);
49
51
  var import_proper_lockfile2 = __toESM(require("proper-lockfile"), 1);
50
52
 
@@ -53,10 +55,10 @@ var import_node_fs = require("fs");
53
55
  var path = __toESM(require("path"), 1);
54
56
  var import_shared = require("@apicircle/shared");
55
57
  var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
56
- var SYNCED_FILE = "workspace.synced.json";
58
+ var SYNCED_FILE = "workspace.json";
57
59
  var LOCAL_FILE = "workspace.local.json";
58
60
  async function loadFromFile(dir, options = {}) {
59
- const syncedPath = path.join(dir, SYNCED_FILE);
61
+ const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);
60
62
  const localPath = path.join(dir, LOCAL_FILE);
61
63
  let syncedRaw;
62
64
  try {
@@ -78,7 +80,7 @@ async function loadFromFile(dir, options = {}) {
78
80
  }
79
81
  async function saveToFile(dir, state, options = {}) {
80
82
  await import_node_fs.promises.mkdir(dir, { recursive: true });
81
- const syncedPath = path.join(dir, SYNCED_FILE);
83
+ const syncedPath = path.join(dir, options.syncedFilename ?? SYNCED_FILE);
82
84
  const localPath = path.join(dir, LOCAL_FILE);
83
85
  await ensureFile(syncedPath);
84
86
  const release = await import_proper_lockfile.default.lock(syncedPath, {
@@ -148,11 +150,15 @@ function createEmptyLocalForSynced(synced) {
148
150
 
149
151
  // src/workspace/workspaceRegistry.ts
150
152
  var REGISTRY_FILE = "registry.json";
153
+ var WORKSPACE_DIR_PREFIX = "workspace-";
154
+ function defaultApicircleRoot() {
155
+ return path2.join(os.homedir(), ".apicircle");
156
+ }
151
157
  function emptyRegistry() {
152
158
  return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };
153
159
  }
154
160
  function workspaceDirFor(root, workspaceId) {
155
- return path2.join(root, workspaceId);
161
+ return path2.join(root, `${WORKSPACE_DIR_PREFIX}${workspaceId}`);
156
162
  }
157
163
  async function loadRegistry(root) {
158
164
  const filePath = path2.join(root, REGISTRY_FILE);
@@ -240,33 +246,6 @@ function findWorkspaceEntry(registry, idOrName) {
240
246
  const byName = registry.workspaces.find((w) => w.name.toLowerCase() === lower);
241
247
  return byName ?? null;
242
248
  }
243
- async function migrateLegacyWorkspace(args) {
244
- const { legacyDir, registryRoot, defaultName = "Workspace" } = args;
245
- const existing = await loadRegistry(registryRoot);
246
- if (existing) return { migrated: false, registry: existing };
247
- const legacyState = await loadFromFile(legacyDir, { allowMissing: true });
248
- if (!legacyState) {
249
- return { migrated: false, registry: emptyRegistry() };
250
- }
251
- const id = legacyState.synced.workspaceId;
252
- await saveToFile(workspaceDirFor(registryRoot, id), legacyState);
253
- const now = (/* @__PURE__ */ new Date()).toISOString();
254
- const entry = {
255
- id,
256
- name: defaultName,
257
- createdAt: legacyState.synced.meta.createdAt ?? now,
258
- lastOpenedAt: now
259
- };
260
- const registry = {
261
- schemaVersion: 1,
262
- activeWorkspaceId: id,
263
- workspaces: [entry]
264
- };
265
- await saveRegistry(registryRoot, registry);
266
- await import_node_fs2.promises.rm(path2.join(legacyDir, "workspace.synced.json"), { force: true });
267
- await import_node_fs2.promises.rm(path2.join(legacyDir, "workspace.local.json"), { force: true });
268
- return { migrated: true, registry };
269
- }
270
249
  function normalizeRegistry(raw) {
271
250
  return {
272
251
  schemaVersion: 1,
@@ -288,12 +267,13 @@ function isENOENT2(err) {
288
267
  // Annotate the CommonJS export names for ESM import in node:
289
268
  0 && (module.exports = {
290
269
  REGISTRY_FILE,
270
+ WORKSPACE_DIR_PREFIX,
271
+ defaultApicircleRoot,
291
272
  deleteWorkspaceById,
292
273
  emptyRegistry,
293
274
  findWorkspaceEntry,
294
275
  loadRegistry,
295
276
  loadWorkspaceById,
296
- migrateLegacyWorkspace,
297
277
  registerWorkspace,
298
278
  saveRegistry,
299
279
  saveWorkspaceById,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/workspace/workspaceRegistry.ts","../../src/workspace/fileBackedWorkspace.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\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. Mirrors the IDB-side `WorkspaceRegistry`\n// shape (`packages/ui-components/src/persistence/db.ts`) so the desktop app,\n// the CLI, and the MCP server all read the same JSON file format.\n//\n// Layout under `<root>/`:\n//\n// registry.json ← this module's source of truth\n// <workspace-id-1>/\n// workspace.synced.json\n// workspace.local.json\n// <workspace-id-2>/\n// ...\n//\n// Each workspace lives in its own subdirectory so concurrent writers (the\n// desktop's mirror + a CLI invocation against a different workspace) can't\n// step on each other. `proper-lockfile` still guards the registry file\n// itself when readers / writers race.\n// =============================================================================\n\nexport const REGISTRY_FILE = 'registry.json';\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 JSON pair. */\nexport function workspaceDirFor(root: string, workspaceId: string): string {\n return path.join(root, 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.synced.json` is\n * missing. 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.synced.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/**\n * One-time migration from the legacy single-workspace layout\n * (`<root>/workspace.synced.json` written next to the registry root) into\n * per-workspace subdirectories. Runs on first boot after the multi-workspace\n * rollout. No-op when the registry already exists.\n *\n * The legacy layout was: the desktop's userData/workspace/ directly held\n * `workspace.synced.json` + `workspace.local.json`. The new layout puts\n * those under `userData/workspaces/<id>/`. We read the legacy pair, write\n * it under the new layout keyed on its `synced.workspaceId`, then unlink\n * the legacy files so re-migration is impossible.\n */\nexport async function migrateLegacyWorkspace(args: {\n legacyDir: string;\n registryRoot: string;\n defaultName?: string;\n}): Promise<{ migrated: boolean; registry: WorkspaceRegistry }> {\n const { legacyDir, registryRoot, defaultName = 'Workspace' } = args;\n const existing = await loadRegistry(registryRoot);\n if (existing) return { migrated: false, registry: existing };\n const legacyState = await loadFromFile(legacyDir, { allowMissing: true });\n if (!legacyState) {\n return { migrated: false, registry: emptyRegistry() };\n }\n const id = legacyState.synced.workspaceId;\n await saveToFile(workspaceDirFor(registryRoot, id), legacyState);\n const now = new Date().toISOString();\n const entry: WorkspaceRegistryEntry = {\n id,\n name: defaultName,\n createdAt: legacyState.synced.meta.createdAt ?? now,\n lastOpenedAt: now,\n };\n const registry: WorkspaceRegistry = {\n schemaVersion: 1,\n activeWorkspaceId: id,\n workspaces: [entry],\n };\n await saveRegistry(registryRoot, registry);\n // Remove the legacy files so subsequent boots don't re-migrate (and so\n // the CLI / MCP don't accidentally read the stale copy).\n await fs.rm(path.join(legacyDir, 'workspace.synced.json'), { force: true });\n await fs.rm(path.join(legacyDir, 'workspace.local.json'), { force: true });\n return { migrated: true, registry };\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.synced.json ← matches WorkspaceSynced exactly, push-to-git target\n// workspace.local.json ← WorkspaceLocal, host-private (CLI/MCP doesn't push)\n//\n// The lock is held on `workspace.synced.json` because that's the file the\n// editor races against. Stale locks are released after 30s.\n// =============================================================================\n\nconst SYNCED_FILE = 'workspace.synced.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}\n\nexport interface SaveToFileOptions {\n /** Lock timeout (ms). Defaults to 30000. */\n lockTimeoutMs?: number;\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, 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, 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, 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\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,7 +1,13 @@
1
1
  export { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';
2
- import { W as WorkspaceState } from '../patches-h_K1VcZR.cjs';
2
+ import { W as WorkspaceState } from '../patches-B3VGNVgf.cjs';
3
3
 
4
4
  declare const REGISTRY_FILE = "registry.json";
5
+ declare const WORKSPACE_DIR_PREFIX = "workspace-";
6
+ /**
7
+ * The universal root for all API Circle workspace data: `~/.apicircle/`.
8
+ * Every consumer (desktop, CLI, MCP, VS Code) uses this as the default.
9
+ */
10
+ declare function defaultApicircleRoot(): string;
5
11
  interface WorkspaceRegistryEntry {
6
12
  /** Matches the in-workspace `synced.workspaceId`. */
7
13
  id: string;
@@ -21,7 +27,8 @@ interface WorkspaceRegistry {
21
27
  }
22
28
  /** Default empty registry. */
23
29
  declare function emptyRegistry(): WorkspaceRegistry;
24
- /** Compute the directory inside `<root>/` that holds a workspace's JSON pair. */
30
+ /** Compute the directory inside `<root>/` that holds a workspace's files.
31
+ * Layout: `<root>/workspace-<id>/`. */
25
32
  declare function workspaceDirFor(root: string, workspaceId: string): string;
26
33
  /** Load the registry from disk; returns `null` if the file is missing. */
27
34
  declare function loadRegistry(root: string): Promise<WorkspaceRegistry | null>;
@@ -29,8 +36,8 @@ declare function loadRegistry(root: string): Promise<WorkspaceRegistry | null>;
29
36
  declare function saveRegistry(root: string, registry: WorkspaceRegistry): Promise<void>;
30
37
  /**
31
38
  * Read a workspace's `{synced, local}` pair by id. Returns `null` if the
32
- * workspace subdirectory is missing OR its `workspace.synced.json` is
33
- * missing. Used by the CLI / MCP / desktop reader.
39
+ * workspace subdirectory is missing OR its `workspace.json` is missing.
40
+ * Used by the CLI / MCP / desktop reader.
34
41
  */
35
42
  declare function loadWorkspaceById(root: string, workspaceId: string): Promise<WorkspaceState | null>;
36
43
  /**
@@ -48,7 +55,7 @@ declare function saveWorkspaceById(root: string, workspaceId: string, state: Wor
48
55
  declare function deleteWorkspaceById(root: string, workspaceId: string): Promise<WorkspaceRegistry>;
49
56
  /**
50
57
  * Add a workspace to the registry. Caller is responsible for having
51
- * written `workspace.synced.json` first. Existing entry with the same id
58
+ * written `workspace.json` first. Existing entry with the same id
52
59
  * is replaced (idempotent update). Newly-registered workspaces become
53
60
  * the active one when there is no prior active.
54
61
  */
@@ -61,25 +68,5 @@ declare function setActiveWorkspace(root: string, workspaceId: string): Promise<
61
68
  * Returns `null` if no entry matches.
62
69
  */
63
70
  declare function findWorkspaceEntry(registry: WorkspaceRegistry, idOrName: string): WorkspaceRegistryEntry | null;
64
- /**
65
- * One-time migration from the legacy single-workspace layout
66
- * (`<root>/workspace.synced.json` written next to the registry root) into
67
- * per-workspace subdirectories. Runs on first boot after the multi-workspace
68
- * rollout. No-op when the registry already exists.
69
- *
70
- * The legacy layout was: the desktop's userData/workspace/ directly held
71
- * `workspace.synced.json` + `workspace.local.json`. The new layout puts
72
- * those under `userData/workspaces/<id>/`. We read the legacy pair, write
73
- * it under the new layout keyed on its `synced.workspaceId`, then unlink
74
- * the legacy files so re-migration is impossible.
75
- */
76
- declare function migrateLegacyWorkspace(args: {
77
- legacyDir: string;
78
- registryRoot: string;
79
- defaultName?: string;
80
- }): Promise<{
81
- migrated: boolean;
82
- registry: WorkspaceRegistry;
83
- }>;
84
71
 
85
- export { REGISTRY_FILE, type WorkspaceRegistry, type WorkspaceRegistryEntry, deleteWorkspaceById, emptyRegistry, findWorkspaceEntry, loadRegistry, loadWorkspaceById, migrateLegacyWorkspace, registerWorkspace, saveRegistry, saveWorkspaceById, setActiveWorkspace, workspaceDirFor };
72
+ export { REGISTRY_FILE, WORKSPACE_DIR_PREFIX, type WorkspaceRegistry, type WorkspaceRegistryEntry, defaultApicircleRoot, deleteWorkspaceById, emptyRegistry, findWorkspaceEntry, loadRegistry, loadWorkspaceById, registerWorkspace, saveRegistry, saveWorkspaceById, setActiveWorkspace, workspaceDirFor };