@apicircle/core 1.0.0 → 1.0.2
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/LICENSE +110 -110
- package/README.md +181 -12
- package/dist/chunk-SGI6KGQ7.js +129 -0
- package/dist/chunk-SGI6KGQ7.js.map +1 -0
- package/dist/index.cjs +7 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/workspace/file-backed.js +5 -122
- package/dist/workspace/file-backed.js.map +1 -1
- package/dist/workspace/registry.cjs +301 -0
- package/dist/workspace/registry.cjs.map +1 -0
- package/dist/workspace/registry.d.cts +85 -0
- package/dist/workspace/registry.d.ts +85 -0
- package/dist/workspace/registry.js +162 -0
- package/dist/workspace/registry.js.map +1 -0
- package/package.json +63 -43
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export { WorkspaceLocal, WorkspaceSynced } from '@apicircle/shared';
|
|
2
|
+
import { W as WorkspaceState } from '../patches-N7mvDpXn.js';
|
|
3
|
+
|
|
4
|
+
declare const REGISTRY_FILE = "registry.json";
|
|
5
|
+
interface WorkspaceRegistryEntry {
|
|
6
|
+
/** Matches the in-workspace `synced.workspaceId`. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Human-readable label. Local to this device — never pushed to git. */
|
|
9
|
+
name: string;
|
|
10
|
+
/** ISO timestamp; bumped every time this workspace is opened or written. */
|
|
11
|
+
lastOpenedAt: string;
|
|
12
|
+
/** ISO timestamp; set when the workspace was first registered. */
|
|
13
|
+
createdAt: string;
|
|
14
|
+
}
|
|
15
|
+
interface WorkspaceRegistry {
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
/** id of the workspace the desktop UI should boot into. `null` only on
|
|
18
|
+
* an empty registry (no workspaces have been seeded yet). */
|
|
19
|
+
activeWorkspaceId: string | null;
|
|
20
|
+
workspaces: WorkspaceRegistryEntry[];
|
|
21
|
+
}
|
|
22
|
+
/** Default empty registry. */
|
|
23
|
+
declare function emptyRegistry(): WorkspaceRegistry;
|
|
24
|
+
/** Compute the directory inside `<root>/` that holds a workspace's JSON pair. */
|
|
25
|
+
declare function workspaceDirFor(root: string, workspaceId: string): string;
|
|
26
|
+
/** Load the registry from disk; returns `null` if the file is missing. */
|
|
27
|
+
declare function loadRegistry(root: string): Promise<WorkspaceRegistry | null>;
|
|
28
|
+
/** Save the registry atomically (`<file>.tmp` + rename). */
|
|
29
|
+
declare function saveRegistry(root: string, registry: WorkspaceRegistry): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* 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.
|
|
34
|
+
*/
|
|
35
|
+
declare function loadWorkspaceById(root: string, workspaceId: string): Promise<WorkspaceState | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Write a workspace's pair under `<root>/<id>/`. Idempotent; the directory
|
|
38
|
+
* is created on first write. Bumps the registry entry's `lastOpenedAt`
|
|
39
|
+
* and writes the registry back if the entry exists.
|
|
40
|
+
*/
|
|
41
|
+
declare function saveWorkspaceById(root: string, workspaceId: string, state: WorkspaceState): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Remove a workspace from disk: deletes its directory, drops it from the
|
|
44
|
+
* registry. If it was the active workspace, the next-most-recent remaining
|
|
45
|
+
* workspace becomes active. If no workspaces remain, `activeWorkspaceId`
|
|
46
|
+
* goes to `null` and the caller should seed a fresh workspace.
|
|
47
|
+
*/
|
|
48
|
+
declare function deleteWorkspaceById(root: string, workspaceId: string): Promise<WorkspaceRegistry>;
|
|
49
|
+
/**
|
|
50
|
+
* Add a workspace to the registry. Caller is responsible for having
|
|
51
|
+
* written `workspace.synced.json` first. Existing entry with the same id
|
|
52
|
+
* is replaced (idempotent update). Newly-registered workspaces become
|
|
53
|
+
* the active one when there is no prior active.
|
|
54
|
+
*/
|
|
55
|
+
declare function registerWorkspace(root: string, entry: WorkspaceRegistryEntry): Promise<WorkspaceRegistry>;
|
|
56
|
+
/** Set the active workspace id. Throws if the id isn't in the registry. */
|
|
57
|
+
declare function setActiveWorkspace(root: string, workspaceId: string): Promise<WorkspaceRegistry>;
|
|
58
|
+
/**
|
|
59
|
+
* Look up a workspace registry entry by id OR by name (case-insensitive).
|
|
60
|
+
* Used by the CLI's `--workspace <selector>` resolver — accepts both forms.
|
|
61
|
+
* Returns `null` if no entry matches.
|
|
62
|
+
*/
|
|
63
|
+
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
|
+
|
|
85
|
+
export { REGISTRY_FILE, type WorkspaceRegistry, type WorkspaceRegistryEntry, deleteWorkspaceById, emptyRegistry, findWorkspaceEntry, loadRegistry, loadWorkspaceById, migrateLegacyWorkspace, registerWorkspace, saveRegistry, saveWorkspaceById, setActiveWorkspace, workspaceDirFor };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadFromFile,
|
|
3
|
+
saveToFile
|
|
4
|
+
} from "../chunk-SGI6KGQ7.js";
|
|
5
|
+
|
|
6
|
+
// src/workspace/workspaceRegistry.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import lockfile from "proper-lockfile";
|
|
10
|
+
var REGISTRY_FILE = "registry.json";
|
|
11
|
+
function emptyRegistry() {
|
|
12
|
+
return { schemaVersion: 1, activeWorkspaceId: null, workspaces: [] };
|
|
13
|
+
}
|
|
14
|
+
function workspaceDirFor(root, workspaceId) {
|
|
15
|
+
return path.join(root, workspaceId);
|
|
16
|
+
}
|
|
17
|
+
async function loadRegistry(root) {
|
|
18
|
+
const filePath = path.join(root, REGISTRY_FILE);
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
return normalizeRegistry(parsed);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (isENOENT(err)) return null;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function saveRegistry(root, registry) {
|
|
29
|
+
await fs.mkdir(root, { recursive: true });
|
|
30
|
+
const filePath = path.join(root, REGISTRY_FILE);
|
|
31
|
+
const tmp = `${filePath}.tmp`;
|
|
32
|
+
await ensureFile(filePath);
|
|
33
|
+
const release = await lockfile.lock(filePath, {
|
|
34
|
+
retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },
|
|
35
|
+
stale: 3e4
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await fs.writeFile(tmp, JSON.stringify(registry, null, 2) + "\n", {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
mode: 384
|
|
41
|
+
});
|
|
42
|
+
await fs.rename(tmp, filePath);
|
|
43
|
+
} finally {
|
|
44
|
+
await release();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function loadWorkspaceById(root, workspaceId) {
|
|
48
|
+
return loadFromFile(workspaceDirFor(root, workspaceId), { allowMissing: true });
|
|
49
|
+
}
|
|
50
|
+
async function saveWorkspaceById(root, workspaceId, state) {
|
|
51
|
+
await saveToFile(workspaceDirFor(root, workspaceId), state);
|
|
52
|
+
}
|
|
53
|
+
async function deleteWorkspaceById(root, workspaceId) {
|
|
54
|
+
const registry = await loadRegistry(root) ?? emptyRegistry();
|
|
55
|
+
const remaining = registry.workspaces.filter((w) => w.id !== workspaceId);
|
|
56
|
+
await fs.rm(workspaceDirFor(root, workspaceId), { recursive: true, force: true });
|
|
57
|
+
let nextActive = registry.activeWorkspaceId;
|
|
58
|
+
if (nextActive === workspaceId) {
|
|
59
|
+
nextActive = [...remaining].sort((a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt))[0]?.id ?? null;
|
|
60
|
+
}
|
|
61
|
+
const next = {
|
|
62
|
+
...registry,
|
|
63
|
+
activeWorkspaceId: nextActive,
|
|
64
|
+
workspaces: remaining
|
|
65
|
+
};
|
|
66
|
+
await saveRegistry(root, next);
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
async function registerWorkspace(root, entry) {
|
|
70
|
+
const registry = await loadRegistry(root) ?? emptyRegistry();
|
|
71
|
+
const otherEntries = registry.workspaces.filter((w) => w.id !== entry.id);
|
|
72
|
+
const next = {
|
|
73
|
+
schemaVersion: 1,
|
|
74
|
+
activeWorkspaceId: registry.activeWorkspaceId ?? entry.id,
|
|
75
|
+
workspaces: [...otherEntries, entry]
|
|
76
|
+
};
|
|
77
|
+
await saveRegistry(root, next);
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
async function setActiveWorkspace(root, workspaceId) {
|
|
81
|
+
const registry = await loadRegistry(root) ?? emptyRegistry();
|
|
82
|
+
if (!registry.workspaces.some((w) => w.id === workspaceId)) {
|
|
83
|
+
throw new Error(`workspace ${workspaceId} is not in the registry at ${root}`);
|
|
84
|
+
}
|
|
85
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
86
|
+
const next = {
|
|
87
|
+
...registry,
|
|
88
|
+
activeWorkspaceId: workspaceId,
|
|
89
|
+
workspaces: registry.workspaces.map(
|
|
90
|
+
(w) => w.id === workspaceId ? { ...w, lastOpenedAt: now } : w
|
|
91
|
+
)
|
|
92
|
+
};
|
|
93
|
+
await saveRegistry(root, next);
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
function findWorkspaceEntry(registry, idOrName) {
|
|
97
|
+
const exactId = registry.workspaces.find((w) => w.id === idOrName);
|
|
98
|
+
if (exactId) return exactId;
|
|
99
|
+
const lower = idOrName.toLowerCase();
|
|
100
|
+
const byName = registry.workspaces.find((w) => w.name.toLowerCase() === lower);
|
|
101
|
+
return byName ?? null;
|
|
102
|
+
}
|
|
103
|
+
async function migrateLegacyWorkspace(args) {
|
|
104
|
+
const { legacyDir, registryRoot, defaultName = "Workspace" } = args;
|
|
105
|
+
const existing = await loadRegistry(registryRoot);
|
|
106
|
+
if (existing) return { migrated: false, registry: existing };
|
|
107
|
+
const legacyState = await loadFromFile(legacyDir, { allowMissing: true });
|
|
108
|
+
if (!legacyState) {
|
|
109
|
+
return { migrated: false, registry: emptyRegistry() };
|
|
110
|
+
}
|
|
111
|
+
const id = legacyState.synced.workspaceId;
|
|
112
|
+
await saveToFile(workspaceDirFor(registryRoot, id), legacyState);
|
|
113
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
114
|
+
const entry = {
|
|
115
|
+
id,
|
|
116
|
+
name: defaultName,
|
|
117
|
+
createdAt: legacyState.synced.meta.createdAt ?? now,
|
|
118
|
+
lastOpenedAt: now
|
|
119
|
+
};
|
|
120
|
+
const registry = {
|
|
121
|
+
schemaVersion: 1,
|
|
122
|
+
activeWorkspaceId: id,
|
|
123
|
+
workspaces: [entry]
|
|
124
|
+
};
|
|
125
|
+
await saveRegistry(registryRoot, registry);
|
|
126
|
+
await fs.rm(path.join(legacyDir, "workspace.synced.json"), { force: true });
|
|
127
|
+
await fs.rm(path.join(legacyDir, "workspace.local.json"), { force: true });
|
|
128
|
+
return { migrated: true, registry };
|
|
129
|
+
}
|
|
130
|
+
function normalizeRegistry(raw) {
|
|
131
|
+
return {
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
activeWorkspaceId: raw.activeWorkspaceId ?? null,
|
|
134
|
+
workspaces: Array.isArray(raw.workspaces) ? raw.workspaces : []
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function ensureFile(filePath) {
|
|
138
|
+
try {
|
|
139
|
+
await fs.access(filePath);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (!isENOENT(err)) throw err;
|
|
142
|
+
await fs.writeFile(filePath, "{}", { encoding: "utf-8", mode: 384 });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function isENOENT(err) {
|
|
146
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
REGISTRY_FILE,
|
|
150
|
+
deleteWorkspaceById,
|
|
151
|
+
emptyRegistry,
|
|
152
|
+
findWorkspaceEntry,
|
|
153
|
+
loadRegistry,
|
|
154
|
+
loadWorkspaceById,
|
|
155
|
+
migrateLegacyWorkspace,
|
|
156
|
+
registerWorkspace,
|
|
157
|
+
saveRegistry,
|
|
158
|
+
saveWorkspaceById,
|
|
159
|
+
setActiveWorkspace,
|
|
160
|
+
workspaceDirFor
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/workspace/workspaceRegistry.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"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apicircle/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Request execution, environment resolution, auth signing, assertions, spec imports, and the applyMutation workspace engine for
|
|
6
|
+
"description": "Request execution, environment resolution, auth signing, assertions, spec imports, and the applyMutation workspace engine for API Circle Studio.",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -15,58 +15,78 @@
|
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
|
-
"main": "./
|
|
19
|
-
"types": "./
|
|
18
|
+
"main": "./src/index.ts",
|
|
19
|
+
"types": "./src/index.ts",
|
|
20
20
|
"exports": {
|
|
21
|
-
".":
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./workspace/file-backed": "./src/workspace/fileBackedWorkspace.ts",
|
|
23
|
+
"./workspace/registry": "./src/workspace/workspaceRegistry.ts",
|
|
24
|
+
"./test/mock-idp": "./src/auth/oauth2/__fixtures__/mockIdp.ts"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.cts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"default": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"types": "./dist/index.d.cts",
|
|
41
|
+
"default": "./dist/index.cjs"
|
|
42
|
+
}
|
|
25
43
|
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
"./workspace/file-backed": {
|
|
45
|
+
"import": {
|
|
46
|
+
"types": "./dist/workspace/file-backed.d.ts",
|
|
47
|
+
"default": "./dist/workspace/file-backed.js"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"types": "./dist/workspace/file-backed.d.cts",
|
|
51
|
+
"default": "./dist/workspace/file-backed.cjs"
|
|
52
|
+
}
|
|
35
53
|
},
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
"./workspace/registry": {
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "./dist/workspace/registry.d.ts",
|
|
57
|
+
"default": "./dist/workspace/registry.js"
|
|
58
|
+
},
|
|
59
|
+
"require": {
|
|
60
|
+
"types": "./dist/workspace/registry.d.cts",
|
|
61
|
+
"default": "./dist/workspace/registry.cjs"
|
|
62
|
+
}
|
|
45
63
|
},
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
64
|
+
"./test/mock-idp": {
|
|
65
|
+
"import": {
|
|
66
|
+
"types": "./dist/test/mock-idp.d.ts",
|
|
67
|
+
"default": "./dist/test/mock-idp.js"
|
|
68
|
+
},
|
|
69
|
+
"require": {
|
|
70
|
+
"types": "./dist/test/mock-idp.d.cts",
|
|
71
|
+
"default": "./dist/test/mock-idp.cjs"
|
|
72
|
+
}
|
|
49
73
|
}
|
|
50
74
|
}
|
|
51
75
|
},
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsup",
|
|
78
|
+
"check": "tsc --noEmit",
|
|
79
|
+
"test": "vitest run",
|
|
80
|
+
"clean": "rm -rf dist node_modules"
|
|
81
|
+
},
|
|
55
82
|
"dependencies": {
|
|
56
|
-
"
|
|
57
|
-
"
|
|
83
|
+
"@apicircle/shared": "workspace:*",
|
|
84
|
+
"proper-lockfile": "^4.1.2"
|
|
58
85
|
},
|
|
59
86
|
"devDependencies": {
|
|
60
87
|
"@types/proper-lockfile": "^4.1.4",
|
|
61
88
|
"tsup": "^8.3.0",
|
|
62
89
|
"typescript": "^5.4.0",
|
|
63
90
|
"vitest": "^2.0.0"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
"build": "tsup",
|
|
67
|
-
"check": "tsc --noEmit",
|
|
68
|
-
"test": "vitest run",
|
|
69
|
-
"clean": "rm -rf dist node_modules"
|
|
70
|
-
},
|
|
71
|
-
"module": "./dist/index.js"
|
|
72
|
-
}
|
|
91
|
+
}
|
|
92
|
+
}
|