import { execFile as callback } from "node:child_process"; import { promisify } from "node:util"; import { randomUUID } from "node:crypto"; import { mkdir, mkdtemp, lstat, rename, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { materializeWorkspaceSources, workspaceSourcePin, type WorkspaceSourcePin } from "./source-closure.js"; const exec = promisify(callback); /** Developer workflow, not an ordinary Workspace Agent's implicit permission * to publish against canonical repositories. Creates independent Git/jj copies, * with instance binding outside committed source. No Graph is started here. */ export async function prepareDevelopmentSources(options: { source: WorkspaceSourcePin; 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)) throw Error("The retained source cache must live outside the disposable workbench"); if ( await lstat(destination).catch((error: NodeJS.ErrnoException) => { if (error.code !== "ENOENT") throw error; return null; }) ) throw Error("Choose a new development workbench directory; existing checkouts are never overwritten"); const closure = await materializeWorkspaceSources({ source, directory: options.cacheDirectory, locations: options.locations, }); const origins = new Set(); for (const entry of closure.resources) { const identity = `${entry.kind}:${entry.repository}`; if (origins.has(identity)) throw Error("Multiple revisions of one repository require explicit editable selection"); origins.add(identity); } const parent = path.dirname(destination); await mkdir(parent, { recursive: true }); const staging = await mkdtemp(path.join(parent, ".development-sources-")); // 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) { const relative = entry.kind === "workspace" ? "root" : "resources/" + entry.directory; const checkout = path.join(staging, relative); await mkdir(path.dirname(checkout), { recursive: true }); await exec("git", [ "clone", "--no-hardlinks", "--", path.join(path.resolve(options.cacheDirectory), entry.directory), checkout, ]); await exec("git", ["remote", "set-url", "origin", entry.repository], { cwd: checkout }); await exec("git", ["checkout", "--detach", entry.commit], { cwd: checkout }); await exec("jj", ["git", "init", "--colocate"], { cwd: checkout, env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0" }, }); await exec("jj", ["describe", "-m", "Develop canonical application source"], { cwd: checkout, env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0" }, }); if (entry.kind !== "workspace") resources.push({ kind: entry.kind, directory: relative, source: { resolver: "git", repository: entry.repository, commit: entry.commit }, }); } await mkdir(path.join(staging, ".quixos"), { mode: 0o700 }); await writeFile( path.join(staging, ".quixos/resource-graph.json"), JSON.stringify({ formatVersion: 1, workspaceId, resources }, null, 2) + "\n", { mode: 0o600 }, ); await writeFile( path.join(staging, ".quixos/development-source.json"), JSON.stringify({ source, workspaceId }, null, 2) + "\n", { mode: 0o600 }, ); await rename(staging, destination); return { directory: destination, source, workspaceId, repositories: closure.resources.length }; } finally { await rm(staging, { recursive: true, force: true }); } }