2026-10-10 17:09:03 -07:00
|
|
|
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<string, string>;
|
2026-10-12 12:32:28 -07:00
|
|
|
workspaceId?: string;
|
2026-10-10 17:09:03 -07:00
|
|
|
}) {
|
|
|
|
|
const source = workspaceSourcePin(options.source);
|
2026-10-12 12:32:28 -07:00
|
|
|
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");
|
2026-10-10 17:09:03 -07:00
|
|
|
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<string>();
|
|
|
|
|
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-"));
|
2026-10-12 12:32:28 -07:00
|
|
|
// This local association does not create or grant access to a hosted Graph.
|
|
|
|
|
const workspaceId = options.workspaceId ?? randomUUID();
|
2026-10-10 17:09:03 -07:00
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|