import fs from "node:fs/promises"; import path from "node:path"; import type { ScaffoldRecipe } from "./scaffold-recipes.js"; import type { StructuralRequest } from "./structural-plan.js"; import { parseQx, walkSyntax } from "./source.js"; import { addImplementation } from "./implementation-edit.js"; import { formatQuixosLock } from "../resource-lock/index.js"; import { javascriptTarget } from "../execution-world/native-backends.js"; const json = (value: unknown) => JSON.stringify(value, null, 2) + "\n"; const q = (value: string) => JSON.stringify(value); const nixString = (value: string) => q(value).replaceAll("${", "\\${"); export async function isExecutionScaffold(root: string, prefix: string): Promise { try { return JSON.parse(await fs.readFile(path.join(root, prefix, "quixos.check.json"), "utf8")).backend === "execution"; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; } } /** Insert ordinary authored code; declarations remain the only export inventory. */ export async function executionScaffold( root: string, command: string, spec: ScaffoldRecipe, prefix: string, nixSource: (source: NonNullable["helpers"]) => string, ): Promise { if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id) throw new Error("Execution scaffold requires a simple name and an ID"); if (spec.initialFiles && command !== "package") throw new Error("Initial files are only for new packages"); const files: StructuralRequest["files"] = []; const create = (file: string, content: string) => files.push({ file: prefix + file, create: content }); if (command === "package") { if (!spec.tools || !spec.revision) throw new Error("Execution package requires exact tool sources and a revision"); create( "package.qx", `symbol JavaScript id ${q(javascriptTarget)};\npackage ${spec.name} id ${q(spec.id)} revision ${q(spec.revision)} {\n}\n`, ); create( "quixos.lock", formatQuixosLock({ formatVersion: 1, quixos: { resolver: "git", ...spec.tools.quixos }, resources: [] }), ); create("quixos.check.json", json({ backend: "execution" })); create("components.nix", "{\n}\n"); create( "package.json", json({ name: `@quixos/${spec.name.toLowerCase()}`, private: true, type: "module", packageManager: "yarn@4.18.0", scripts: { typecheck: "qx-workspace check" }, }), ); create( ".gitignore", "node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n.yarn/cache/\n.yarn/install-state.gz\n.yarn/unplugged/\n", ); create( ".yarnrc.yml", 'enableScripts: true\n\ngenerateDefaultNix: false\n\nindividualNixPackaging: true\n\nnodeLinker: node-modules\n\nsupportedArchitectures:\n os:\n - current\n - linux\n cpu:\n - current\n - x64\n - arm64\n libc:\n - current\n - glibc\n\nplugins:\n - checksum: 271de9a785321a37c564b8e2a6b5cd689850b20572d5fee8ec0a6b8e7a5d4cfb59e533524af2b4a537e00da12828abd062eaec6978556e3dd9149a020db2591b\n path: .yarn/plugins/yarn-plugin-nixify.cjs\n spec: "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js"\n', ); for (const lane of ["browser", "server", "remote"]) create( `src/${lane}.ts`, `import { createRegistry } from "./gen/${lane === "remote" ? "remote" : "qx"}.js";\nexport const registry = createRegistry({});\n`, ); create( "flake.nix", `{ inputs.protocol.url = ${nixString(nixSource(spec.tools.protocol))}; inputs.nixpkgs.follows = "protocol/nixpkgs"; inputs.flake-utils.follows = "protocol/flake-utils"; inputs.helpers = { url = ${nixString(nixSource(spec.tools.helpers))}; flake = false; }; outputs = { nixpkgs, flake-utils, helpers, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; camino = import (helpers + "/quixos-package-helpers.nix"); in { devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; }; quixosPackages.checkedArtifacts = context: let pkgs = camino.packageSet { inherit nixpkgs system context; }; contract = builtins.fromJSON (builtins.readFile context.buildContract); package = contract.definition.package; portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports; remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports; modules = import ./components.nix; dependencies = if builtins.pathExists ./yarn-project.nix then camino.mkCaminoYarnDependencies { inherit pkgs; src = ./.; } else null; in camino.assembleCaminoPackage { inherit pkgs context; artifacts = pkgs.lib.optional (portable != []) (camino.mkCaminoTypeScriptPackage { inherit pkgs context dependencies; src = ./.; entry = "src/server.ts"; entries.browser = "src/browser.ts"; targets = pkgs.lib.unique (map (target: if target == "target:org.quixos.execution.javascript@1" then "browser" else if target == "server" then "server" else throw "Unsupported TypeScript backend target") (pkgs.lib.concatMap (entry: entry.execution.targets) portable)); }) ++ pkgs.lib.optional (remote != []) (camino.mkCaminoTypeScriptService { inherit pkgs context dependencies; src = ./.; entry = "src/remote.ts"; }) ++ pkgs.lib.optional (modules != {}) (camino.mkCaminoReactPackage { inherit pkgs context dependencies modules; src = ./.; }); }; }); } `, ); create( "README.md", `# ${spec.name}\n\nThis execution package exports checked browser/server modules and a process service.\nUse scaffold function with an explicit execution contract to add a typed handler.\nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers\nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build\noutputs, not authored source. The Nix composition supplies the canonical package contract\nand explicit runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from this package to build it independently, or from a\nworkspace to also check installed bindings. Activate the resulting report separately through the workspace\nhost. A source-only structural check is not runtime qualification.\n`, ); } else if (command === "function") { if (!spec.declaration) throw new Error("Execution functions require a declaration with explicit execution effect and targets"); const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${spec.declaration} }`; const parsed = parseQx(wrapped); const pkg = parsed.root.children.find((node) => node.kind === "packageResourceDecl"); const exports = [...walkSyntax(parsed.root)].filter((node) => ["packageFunctionExport", "packageOperationExport"].includes(node.kind), ); const declaration = exports[0]; if ( parsed.diagnostics.length || exports.length !== 1 || !declaration || parsed.root.children.length !== 1 || pkg?.children.filter((node) => node.kind === "packageExport").length !== 1 || pkg?.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind)) ) throw new Error("Expected one function or operation export with an execution contract"); const text = (node: { start: number; end: number }) => wrapped.slice(node.start, node.end); const name = declaration.children.find((node) => node.kind === "identifier"); const id = declaration.children.find((node) => node.kind === "stringLiteral"); const execution = declaration.children.find((node) => node.kind === "executionClause"); if (!name || text(name) !== spec.name || !id || JSON.parse(text(id)) !== spec.id || !execution) throw new Error("Declaration name/ID must match; an explicit execution contract is required"); const tokens = parsed.tokens .filter((token) => !token.trivia && token.start >= execution.start && token.end <= execution.end) .map((token) => token.text); const remote = tokens[1] === "remote"; const declaredTargets = remote ? ["remote"] : [...walkSyntax(execution)].filter((node) => node.kind === "identifier").map(text); const targets = declaredTargets.length ? declaredTargets : ["server"]; if ( !targets.length || new Set(targets).size !== targets.length || targets.some((target) => !(remote ? ["remote"] : ["JavaScript", "server"]).includes(target)) ) throw new Error("This TypeScript scaffold supports the JavaScript target, server, and remote service handlers"); const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8"); const syntax = parseQx(authored); const packageNode = syntax.root.children.find((node) => node.kind === "packageResourceDecl"); if (syntax.diagnostics.length || !packageNode) throw new Error("Fix package.qx before adding a handler"); for (const node of [...walkSyntax(packageNode)].filter((node) => ["packageFunctionExport", "packageOperationExport", "packageConstructorExport"].includes(node.kind), )) { const identifier = node.children.find((child) => child.kind === "identifier"); const literal = node.children.find((child) => child.kind === "stringLiteral"); if ( (identifier && authored.slice(identifier.start, identifier.end) === spec.name) || (literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id) ) throw new Error("New export requires a unique name and ID"); } const packageID = packageNode.children.find((node) => node.kind === "stringLiteral")!; files.push({ file: prefix + "package.qx", edits: [ { operation: "append", parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(packageID.start, packageID.end)) }, source: spec.declaration, }, ], }); create( `src/impl/${spec.name}.ts`, `import type { ${remote ? "Remote" : "Portable"}Implementation } from "../gen/${remote ? "remote" : "qx"}.js";\nexport const handler: ${remote ? "Remote" : "Portable"}Implementation[${q(spec.name)}] = async (_context) => { throw new Error(${q("Implement " + spec.name)}); };\n`, ); for (const target of targets) { const file = prefix + `src/${target === "JavaScript" ? "browser" : target}.ts`; const before = await fs.readFile(path.join(root, file), "utf8"); files.push({ file, expected: before, replace: addImplementation(before, "createRegistry", spec.name, `./impl/${spec.name}.js`), }); } } else throw new Error("Execution packages use ordinary factory transactions; migration scaffolding is not supported"); if (spec.initialFiles) { for (const [file, content] of Object.entries(spec.initialFiles)) { if ( typeof content !== "string" || !/^(?:package\.qx|(?:package|components|flake|yarn-project)\.nix|package\.json|yarn\.lock|\.yarnrc\.yml|\.gitignore|\.yarn\/plugins\/[A-Za-z0-9_.-]+\.cjs|quixos\.controllers\.json|README\.md|(?:src|queries)\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+)$/.test( file, ) || file.split("/").some((part) => part === ".." || part === "." || part === "gen") ) throw new Error("Not an initial authored execution file: " + file); const previous = files.findIndex((entry) => entry.file === prefix + file); if (previous >= 0) files.splice(previous, 1); create(file, content); } } return { kind: "package", source: spec.source, resourceRoot: spec.directory, validation: "syntax", files }; }