Unify workspace authoring, verification and scaffolding workflows
Use exact jj snapshots and one candidate-bound Nix builder for incremental checks, template validation and activation. Keep provenance internal and separate recovery checkpoint failures from local command success. Provision workspace-scoped managed package/interface repositories with recoverable Central effects. Add TypeScript/React presets, function and dependency commands, and scaffold enrollment for all TODO packages. Install authoring guides and controlled Codex sandbox rules. Invalidate module resolutions across cutover, including in-flight races, and content-address host platform entries. Strengthen domain-model and verification instructions. Validated real jj/Nix authoring, React/Slate dependency installation, bottom-up local Git publication, packaged CLI tests, PostgreSQL recovery/auth tests, Web Studio tests and host configuration. Public protocol/helpers and the validated 19-resource TODO template are published. Retained the approved exact private baseline and updated the installation's default template pin to 68d54f0d52be433ebf60bdc1faf7646c57f90307. Master and live deployments remain unchanged. See docs/WORKSPACE_AUTHORING_PROGRESS.md.
This commit is contained in:
@@ -11,9 +11,61 @@ import os from "node:os";
|
||||
import {spawnSync} from "node:child_process";
|
||||
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "./structural-plan.js";
|
||||
import {scaffoldRecipe, type ScaffoldRecipe} from "./scaffold-recipes.js";
|
||||
import {buildCheckedPackage, snapshotCommit} from "./checked-build.js";
|
||||
import {formatQuixosLock, loadQuixosLock, parseQuixosLockDocument} from "../resource-lock/index.js";
|
||||
|
||||
const authorSource = async (root: string) => {
|
||||
const result = spawnSync("git", ["remote", "get-url", "origin"], {cwd: root, encoding: "utf8"});
|
||||
if (result.error || result.status !== 0) throw new Error("Managed resource has no origin");
|
||||
return {repository: result.stdout.trim(), commit: await snapshotCommit(root)};
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (command === "scaffold-dependency") {
|
||||
const [root, kind, name, repository, commit, ...flags] = args;
|
||||
if (!root || !["interface", "package"].includes(kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") || !repository || !commit || flags.some(flag => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-dependency ROOT interface|package NAME REPOSITORY COMMIT [--write]");
|
||||
const resourceKind = kind as "package" | "interface";
|
||||
let entrypoint: "workspace" | "package" | "interface" | undefined;
|
||||
for (const candidate of ["workspace", "package", "interface"] as const) {
|
||||
try {await readFile(path.join(root, `${candidate}.qx`)); if (entrypoint) throw new Error("Ambiguous repository entrypoint"); entrypoint = candidate;}
|
||||
catch (error) {if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;}
|
||||
}
|
||||
if (!entrypoint) throw new Error("No QX repository entrypoint");
|
||||
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
|
||||
if (!lock.ok) throw new Error("Invalid resource lock");
|
||||
let target = "quixos.lock";
|
||||
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
|
||||
const parsed = parseQuixosLockDocument(await readFile(path.join(root, file), "utf8"));
|
||||
if (parsed.ok && parsed.document.resources.some(entry => entry.kind === kind && entry.binding === name)) target = file;
|
||||
}
|
||||
const request: StructuralRequest = {kind: entrypoint, source: await authorSource(root), files: [
|
||||
{file: `${entrypoint}.qx`, edits: [{operation: "import", kind: resourceKind, name}]},
|
||||
{file: target, edits: [{operation: "dependency", kind: resourceKind, name, source: {repository, commit}}]},
|
||||
]};
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
process.stdout.write(`${JSON.stringify({...plan, applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-interface") {
|
||||
const [root, specFile, ...flags] = args;
|
||||
if (!root || !specFile || flags.some(flag => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-interface ROOT SPEC_JSON [--write]");
|
||||
const spec = JSON.parse(await readFile(specFile, "utf8")) as ScaffoldRecipe;
|
||||
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id || !spec.revision || !spec.tools?.quixos) throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
|
||||
const request: StructuralRequest = {kind: "interface", source: spec.source, files: [
|
||||
{file: "interface.qx", create: `interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`},
|
||||
{file: "quixos.lock", create: formatQuixosLock({formatVersion: 1, quixos: {resolver: "git", ...spec.tools.quixos}, resources: []})},
|
||||
{file: ".gitignore", create: ".quixos/\n"},
|
||||
]};
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
process.stdout.write(`${JSON.stringify({...plan, applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "build-package") {
|
||||
if (args.length !== 3) throw new Error("usage: quixos-qx build-package COMMITTED_SOURCE SCHEMA PACKAGE_REVISION_ID");
|
||||
process.stdout.write(`${await buildCheckedPackage(args[0], args[1], args[2])}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "source-digest") {
|
||||
if (!args[0] || args.length !== 1) throw new Error("usage: quixos-qx source-digest ROOT");
|
||||
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-source-digest-"));
|
||||
@@ -47,8 +99,18 @@ const main = async () => {
|
||||
}
|
||||
if (["scaffold-package", "scaffold-function", "scaffold-migration", "scaffold-refresh"].includes(command)) {
|
||||
const [root, specFile, ...flags] = args;
|
||||
let spec: ScaffoldRecipe;
|
||||
if (command === "scaffold-function" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(specFile ?? "")) {
|
||||
const registry = JSON.parse(await readFile(path.join(root, "quixos.scaffold.json"), "utf8"));
|
||||
spec = {source: await authorSource(root), name: specFile, id: `export:${registry.name}:${specFile}`};
|
||||
const declaration = flags.indexOf("--declaration");
|
||||
if (declaration >= 0) {
|
||||
if (!flags[declaration + 1]) throw new Error("--declaration requires a QX declaration file");
|
||||
spec.declaration = await readFile(flags[declaration + 1], "utf8");
|
||||
flags.splice(declaration, 2);
|
||||
}
|
||||
} else spec = JSON.parse(await readFile(specFile, "utf8")) as ScaffoldRecipe;
|
||||
if (!root || !specFile || flags.some((flag) => !["--write", "--install"].includes(flag)) || (flags.includes("--install") && !flags.includes("--write"))) throw new Error("usage: quixos-qx scaffold-package|function|migration|refresh ROOT SPEC_JSON [--write [--install]]");
|
||||
const spec = JSON.parse(await readFile(specFile, "utf8")) as ScaffoldRecipe;
|
||||
const request = await scaffoldRecipe(root, command.slice(9) as "package" | "function" | "migration" | "refresh", spec);
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
const applied = flags.includes("--write") ? await applyStructure(plan) : undefined;
|
||||
@@ -56,10 +118,15 @@ const main = async () => {
|
||||
const cwd = path.resolve(root, spec.directory ?? "");
|
||||
const toolchain = JSON.parse(await readFile(path.join(cwd, "quixos.toolchain.json"), "utf8"));
|
||||
if (toolchain.generatedBy !== "qx-scaffold-v1" || typeof toolchain.nixifyPluginUrl !== "string") throw new Error("Missing scaffold toolchain");
|
||||
for (const [executable, args] of [["corepack", ["yarn", "plugin", "import", toolchain.nixifyPluginUrl]], ["corepack", ["yarn", "config", "set", "generateDefaultNix", "false"]], ["corepack", ["yarn", "config", "set", "individualNixPackaging", "true"]], ["corepack", ["yarn", "install"]], ["corepack", ["yarn", "typecheck"]], ["nix", ["flake", "lock"]]] as const) {
|
||||
for (const [executable, args] of [["corepack", ["yarn", "plugin", "import", toolchain.nixifyPluginUrl]], ["corepack", ["yarn", "config", "set", "generateDefaultNix", "false"]], ["corepack", ["yarn", "config", "set", "individualNixPackaging", "true"]], ["corepack", ["yarn", "install"]]] as const) {
|
||||
const result = spawnSync(executable, [...args], {cwd, stdio: ["inherit", 2, 2]});
|
||||
if (result.error || result.status !== 0) throw new Error(`Scaffold files retained; ${executable} ${args.join(" ")} failed: ${result.error?.message ?? result.status}`);
|
||||
}
|
||||
try {await readFile(path.join(cwd, "yarn-project.nix"));}
|
||||
catch {throw new Error("Nixify did not generate yarn-project.nix. It skips repositories under the OS temporary directory; use an ordinary workspace checkout and retry installation.");}
|
||||
await snapshotCommit(cwd);
|
||||
const locked = spawnSync("nix", ["flake", "lock"], {cwd, stdio: ["inherit", 2, 2]});
|
||||
if (locked.error || locked.status !== 0) throw new Error("Scaffold files retained; nix flake lock failed");
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({...plan, applied}, null, 2)}\n`);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user