diff --git a/src/capability-language/development-sources.ts b/src/capability-language/development-sources.ts index 97e8d26..e6d540a 100644 --- a/src/capability-language/development-sources.ts +++ b/src/capability-language/development-sources.ts @@ -16,8 +16,11 @@ export async function prepareDevelopmentSources(options: { directory: string; cacheDirectory: string; locations?: Record; + workspaceId?: string; }) { const source = workspaceSourcePin(options.source); + if (options.workspaceId !== undefined && !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(options.workspaceId)) + throw Error("Development source binding requires a Workspace UUID"); const destination = path.resolve(options.directory); const cache = path.resolve(options.cacheDirectory); if (cache === destination || cache.startsWith(destination + path.sep)) @@ -43,7 +46,8 @@ export async function prepareDevelopmentSources(options: { const parent = path.dirname(destination); await mkdir(parent, { recursive: true }); const staging = await mkdtemp(path.join(parent, ".development-sources-")); - const workspaceId = randomUUID(); + // This local association does not create or grant access to a hosted Graph. + const workspaceId = options.workspaceId ?? randomUUID(); try { const resources = []; for (const entry of closure.resources) { diff --git a/src/capability-language/tool-cli.ts b/src/capability-language/tool-cli.ts index 41a382d..7de8d1b 100644 --- a/src/capability-language/tool-cli.ts +++ b/src/capability-language/tool-cli.ts @@ -73,7 +73,7 @@ const main = async () => { "--dir", ...(args[1] !== "restore" ? ["--locations"] : []), ...(args[1] === "checkout" ? ["--repository-prefix"] : []), - ...(args[1] === "develop" ? ["--cache"] : []), + ...(args[1] === "develop" ? ["--cache", "--workspace-id"] : []), ...(args[1] === "backup" ? ["--output"] : []), ].includes(args[i]) || !args[i + 1] || @@ -100,7 +100,11 @@ const main = async () => { if (args[1] === "develop") { if (!flags.get("--cache")) throw Error("source develop requires --cache DIRECTORY"); const { prepareDevelopmentSources } = await import("./development-sources.js"); - result = await prepareDevelopmentSources({ ...options, cacheDirectory: path.resolve(flags.get("--cache")!) }); + result = await prepareDevelopmentSources({ + ...options, + cacheDirectory: path.resolve(flags.get("--cache")!), + ...(flags.has("--workspace-id") ? { workspaceId: flags.get("--workspace-id")! } : {}), + }); } else if (args[1] === "backup") { if (!flags.get("--output")) throw Error("source backup requires --output DIRECTORY"); const { backupWorkspaceSources } = await import("./source-backup.js"); diff --git a/test/source-closure.test.ts b/test/source-closure.test.ts index 4fb8c3e..3cb19d1 100644 --- a/test/source-closure.test.ts +++ b/test/source-closure.test.ts @@ -172,6 +172,27 @@ test("exact source closure recovers through explicitly selected bundles, preserv directory: path.join(temporary, "b"), }); assert.notEqual(a.workspaceId, b.workspaceId); + const assigned = "f8f2f8a3-328c-45a2-847a-31869ad28ce7"; + const c = await prepareDevelopmentSources({ + source: root, + cacheDirectory: directory, + directory: path.join(temporary, "assigned"), + workspaceId: assigned, + }); + assert.equal(c.workspaceId, assigned); + assert.equal( + JSON.parse(await readFile(path.join(c.directory, ".quixos/resource-graph.json"), "utf8")).workspaceId, + assigned, + ); + await assert.rejects( + prepareDevelopmentSources({ + source: root, + cacheDirectory: directory, + directory: path.join(temporary, "invalid"), + workspaceId: "not-a-workspace", + }), + /Workspace UUID/, + ); const aRoot = path.join(a.directory, "root"), bRoot = path.join(b.directory, "root"); assert.equal((await exec("git", ["remote", "get-url", "origin"], { cwd: aRoot })).stdout.trim(), root.repository);