2026-09-10 18:27:41 -07:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
import { contentDigest } from "../capability-model/evolution.js" ;
import { validateMigrationCatalog , type MigrationCatalog , type MigrationDeclaration } from "../capability-model/migrations.js" ;
import { formatQuixosLock , type GitSource } from "../resource-lock/index.js" ;
import { parseQx , walkSyntax } from "./source.js" ;
import type { StructuralRequest } from "./structural-plan.js" ;
type Source = { repository : string ; commit : string };
type Registry = { generatedBy : "qx-scaffold-v1" ; name : string ; id : string ; revision : string ; exports : { name : string ; id : string ; file : string ; migration? : boolean }[]};
export type ScaffoldRecipe = {
2026-09-13 22:07:18 -07:00
template ?: "typescript" | "typescript-react" ;
2026-09-10 18:27:41 -07:00
source : Source ; directory? : string ; name? : string ; id? : string ; revision? : string ;
declaration? : string ;
tools ?: { quixos : Source ; protocol : Source ; helpers : Source ; sdk : Source };
nixifyPluginUrl? : string ;
migration? : Omit < MigrationDeclaration , "implementation" > & { contracts : Record < string , unknown >};
};
const marker = "// Generated by qx-scaffold-v1\n" ;
const json = ( value : unknown ) => ` ${ JSON . stringify ( value , null , 2 ) } \ n` ;
const source = ( value : Source ) : GitSource => {
const url = new URL ( value . repository );
if ( url . protocol !== "https:" || url . username || url . password || url . search || url . hash || ! /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/ . test ( value . commit )) throw new Error ( "Scaffolds require credential-free HTTPS sources and full exact commits" );
return { resolver : "git" , ... value };
};
const nixSource = ( value : Source ) => `git+ ${ source ( value ). repository } ?ref=refs/tags/quixos-reachability/ ${ value . commit } &rev= ${ value . commit } ` ;
const nixString = ( value : string ) => JSON . stringify ( value ). replaceAll ( "${" , "\\${" );
const safeName = ( name : string | undefined ) : string => {
if ( ! name || ! /^[A-Za-z_][A-Za-z0-9_]*$/ . test ( name )) throw new Error ( "Scaffold requires a simple authored name" );
return name ;
};
const ownedJson = async < T >( root : string , file : string ) : Promise < T > => {
const target = path . join ( root , file );
if ( ! ( await fs . realpath ( target )). startsWith ( ` ${ await fs . realpath ( root ) } /` )) throw new Error ( "Scaffold input escapes repository" );
const value = JSON . parse ( await fs . readFile ( target , "utf8" ));
if ( value . generatedBy !== "qx-scaffold-v1" ) throw new Error ( `Not scaffold-owned: ${ file } ` );
return value ;
};
/** Recipes describe structural edits; planStructure owns validation/journaling.
* Implementation files are created once and never rewritten by refresh. */
export const scaffoldRecipe = async ( root : string , command : "package" | "function" | "migration" | "refresh" , spec : ScaffoldRecipe ) : Promise < StructuralRequest > => {
source ( spec . source );
if ( spec . directory && ! /^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/ . test ( spec . directory )) throw new Error ( "Scaffold directory must be contained" );
const prefix = spec . directory ? ` ${ spec . directory } /` : "" ;
const files : StructuralRequest [ "files" ] = [];
const create = ( file : string , content : string ) => files . push ({ file : prefix + file , create : content });
const generated = ( file : string , content : string ) => files . push ({ file : prefix + file , generated : content });
let registry : Registry ;
let catalog : MigrationCatalog & { generatedBy : "qx-scaffold-v1" };
if ( command === "package" ) {
const name = safeName ( spec . name );
2026-09-13 22:07:18 -07:00
if ( spec . template && ! [ "typescript" , "typescript-react" ]. includes ( spec . template )) throw new Error ( "Unknown package template" );
const react = spec . template === "typescript-react" ;
2026-09-10 18:27:41 -07:00
if ( ! spec . id || ! spec . revision || ! spec . tools ) throw new Error ( "Package scaffold requires id, revision, and exact quixos/protocol/helpers/sdk tool sources" );
Object . values ( spec . tools ). forEach ( source );
registry = { generatedBy : "qx-scaffold-v1" , name , id : spec.id , revision : spec.revision , exports : []};
catalog = { generatedBy : "qx-scaffold-v1" , schemaVersion : 1 , contracts : {}, migrations : []};
2026-09-13 22:07:18 -07:00
if ( react ) registry . exports . push ({ name : "sourceGet" , id : `export: ${ name } :source` , file : "src/impl/sourceGet.ts" });
create ( "package.qx" , `package ${ name } id ${ JSON . stringify ( spec . id ) } revision ${ JSON . stringify ( spec . revision ) } { \ n ${ react ? ` function sourceGet id ${ JSON . stringify ( `export: ${ name } :source` ) } : unit -> string; \ n` : "" } } \ n` );
2026-09-10 18:27:41 -07:00
create ( "quixos.lock" , formatQuixosLock ({ formatVersion : 1 , quixos : source ( spec . tools . quixos ), resources : []}));
create ( "package.json" , json ({ name : `@quixos/ ${ name . toLowerCase () } ` , version : "0.1.0" , private : true , type : "module" , packageManager : "yarn@4.18.0" ,
2026-09-13 22:07:18 -07:00
scripts : { build : `tsc -p tsconfig.json ${ react ? " && node scripts/build-component.mjs" : "" } ` , typecheck : "tsc --noEmit" }, dependencies : { "@quixos/camino-package-runtime" : ` ${ spec . tools . sdk . repository } #commit= ${ spec . tools . sdk . commit } ` },
devDependencies : { "@types/node" : "^24" , typescript : "^7.0.2" , ...( react ? { react : "^18.3.1" , "@types/react" : "^18.3.12" , esbuild : "^0.25.12" } : {})}}));
create ( "tsconfig.json" , json ({ compilerOptions : { target : "ES2023" , module : "NodeNext" , moduleResolution : "NodeNext" , strict : true , types : [ "node" , ...( react ? [ "react" ] : [])], outDir : "dist" , rootDir : "src" , skipLibCheck : true , ...( react ? { jsx : "react-jsx" , esModuleInterop : true } : {})}, include : [ "src/**/*.ts" , "src/**/*.tsx" ]}));
if ( react ) {
create ( "src/component.tsx" , `// Props are opaque at the platform boundary until capability generics exist. \ nexport default function Component(_props: {camino: unknown; render: unknown; dispatch: (action: unknown) => void}) { \ n return <section><h1> ${ name } </h1><p>Edit this component, then run qx-workspace check.</p></section>; \ n} \ n` );
create ( "src/impl/sourceGet.ts" , `import {readFile} from "node:fs/promises"; \ nimport type {Implementation} from "../gen/qx.js"; \ nexport const handler: Implementation["sourceGet"] = () => readFile(new URL("./component.mjs", import.meta.url), "utf8"); \ n` );
create ( "scripts/build-component.mjs" , `import {build} from "esbuild"; \ nconst platform = new Map([ \ n ["react", "/__quixos/platform/react/v18.mjs"], \ n ["react/jsx-runtime", "/__quixos/platform/react-jsx-runtime/v18.mjs"], \ n ["react/jsx-dev-runtime", "/__quixos/platform/react-jsx-dev-runtime/v18.mjs"], \ n ["@quixos/web-studio-react-runtime", "/__quixos/platform/web-studio-react-runtime/v1.mjs"], \ n]); \ nawait build({entryPoints: ["dist/component.js"], outfile: "dist/component.mjs", bundle: true, format: "esm", platform: "browser", target: "es2022", plugins: [{name: "quixos-platform", setup(api) {api.onResolve({filter: /.*/}, ({path}) => platform.has(path) ? {path: platform.get(path), external: true} : undefined);}}]}); \ n` );
}
2026-09-10 18:27:41 -07:00
create ( ".gitignore" , "node_modules/\ndist/\n.quixos/\nresult\n.yarn/install-state.gz\n" );
create ( ".yarnrc.yml" , `nodeLinker: node-modules \ nenableScripts: true \ nnpmMinimalAgeGate: 0 \ napprovedGitRepositories: \ n - ${ JSON . stringify ( spec . tools . sdk . repository ) } \ nsupportedArchitectures: \ n os: [current, linux] \ n cpu: [current, x64, arm64] \ n libc: [current, glibc] \ n` );
const nixifyPluginUrl = spec . nixifyPluginUrl ?? "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/yarn-plugin-nixify-patched/raw/commit/4528fdd20b30d869262443b3f044549810e75fb8/dist/yarn-plugin-nixify.js" ;
const plugin = new URL ( nixifyPluginUrl );
if ( plugin . protocol !== "https:" || plugin . username || plugin . password || plugin . search || plugin . hash || ! /\/commit\/[a-f0-9]{40,64}\// . test ( plugin . pathname )) throw new Error ( "Nixify plugin must have an exact credential-free HTTPS commit URL" );
generated ( "quixos.toolchain.json" , json ({ generatedBy : "qx-scaffold-v1" , nixifyPluginUrl }));
2026-09-13 22:07:18 -07:00
create ( "quixos.check.json" , json ({ backend : "typescript" , bindingOutput : "src/gen/qx.ts" }));
2026-09-10 18:27:41 -07:00
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 = inputs@{ self, protocol, nixpkgs, flake-utils, helpers, ... }:
(import (toString helpers + "/quixos-package-helpers.nix")).mkCaminoTsYarnNixifyFlake {
inherit inputs nixpkgs flake-utils; packageRoot = ./.;
bundle = { entry = "dist/server.js"; };
migrationEntrypoint = "dist/migrate.js";
2026-09-13 22:07:18 -07:00
installServer = { libexecName = ${ JSON . stringify ( name . toLowerCase ()) } ; descriptorPath = "descriptor.quixos-package.txtpb"; ${ react ? 'extraFiles = [ { source = "dist/component.mjs"; target = "component.mjs"; } ];' : "" } };
2026-09-10 18:27:41 -07:00
};
} \ n` );
} else {
registry = await ownedJson < Registry >( root , prefix + "quixos.scaffold.json" );
catalog = await ownedJson < typeof catalog >( root , prefix + "quixos.migrations.json" );
2026-09-14 15:24:37 -07:00
// package.qx is authoritative. The registry remembers implementation paths,
// not a second declaration list that can erase an author's new exports.
const authored = await fs . readFile ( path . join ( root , prefix , "package.qx" ), "utf8" );
const syntax = parseQx ( authored );
if ( syntax . diagnostics . length ) throw new Error ( "Cannot refresh an invalid package.qx; fix the reported syntax first" );
const declaration = [... walkSyntax ( syntax . root )]. find ( node => node . kind === "packageResourceDecl" );
if ( ! declaration ) throw new Error ( "Expected a package declaration" );
const text = ( node : typeof declaration ) => authored . slice ( node . start , node . end );
const literals = declaration . children . filter ( node => node . kind === "stringLiteral" );
registry . name = text ( declaration . children . find ( node => node . kind === "identifier" ) ! );
registry . id = JSON . parse ( text ( literals [ 0 ]));
registry . revision = JSON . parse ( text ( literals [ 1 ]));
const previousExports = registry . exports ;
registry . exports = [];
for ( const node of walkSyntax ( declaration )) {
if ( ! [ "packageFunctionExport" , "packageOperationExport" , "packageConstructorExport" ]. includes ( node . kind )) continue ;
const name = safeName ( text ( node . children . find ( child => child . kind === "identifier" ) ! ));
const id = JSON . parse ( text ( node . children . find ( child => child . kind === "stringLiteral" ) ! ));
if ( registry . exports . some ( entry => entry . id === id || entry . name === name )) throw new Error ( "Duplicate package export name or ID" );
const old = previousExports . find ( entry => entry . id === id );
const file = old ? . file ?? `src/impl/ ${ name } .ts` ;
if ( ! old ) {
const exists = await fs . access ( path . join ( root , prefix , file )). then (() => true , () => false );
if ( ! exists ) {
const derived = [... walkSyntax ( node )]. some ( child => child . kind === "eventClause" );
create ( file , `import type {Implementation} from "../gen/qx.js"; \ nexport const handler: Implementation[ ${ JSON . stringify ( name ) } ] = ${ derived ? '{kind: "derived", get: ' : "" } async (_context) => { throw new Error( ${ JSON . stringify ( `Implement ${ name } ` ) } ); } ${ derived ? "}" : "" } ; \ n` );
}
}
registry . exports . push ({... old , name , id , file });
}
2026-09-10 18:27:41 -07:00
if ( command !== "refresh" ) {
const name = safeName ( spec . name );
if ( ! spec . id || registry . exports . some (( entry ) => entry . id === spec . id || entry . name === name )) throw new Error ( "New export requires a unique name and ID" );
const declaration = spec . declaration ?? `function ${ name } id ${ JSON . stringify ( spec . id ) } : unit -> unit;` ;
const parsed = parseQx ( `package Scaffold id "scaffold" revision "scaffold@1" { ${ declaration } }` );
const exports = [... walkSyntax ( parsed . root )]. filter (( node ) => [ "packageFunctionExport" , "packageOperationExport" , "packageConstructorExport" ]. includes ( node . kind ));
if ( parsed . diagnostics . length || exports . length !== 1 ) throw new Error ( "Expected one valid package export declaration" );
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${ declaration } }` ;
const node = exports [ 0 ];
const derived = [... walkSyntax ( node )]. some (( entry ) => entry . kind === "eventClause" );
if ( command === "migration" && ( node . kind !== "packageFunctionExport" || spec . declaration )) throw new Error ( "Migration exports use the scaffold's unit function declaration and dedicated migration entrypoint" );
if ( wrapped . slice ( node . children . find (( child ) => child . kind === "identifier" ) ! . start , node . children . find (( child ) => child . kind === "identifier" ) ! . end ) !== name
|| JSON . parse ( wrapped . slice ( node . children . find (( child ) => child . kind === "stringLiteral" ) ! . start , node . children . find (( child ) => child . kind === "stringLiteral" ) ! . end )) !== spec . id ) throw new Error ( "Declaration name/ID must match its registration" );
files . push ({ file : prefix + "package.qx" , edits : [{ operation : "append" , parent : { kind : "packageResourceDecl" , id : registry.id }, source : declaration }]});
const file = `src/ ${ command === "migration" ? "migrations" : "impl" } / ${ name } .ts` ;
const implementation = command === "migration" ? `import type {MigrationContext} from "@quixos/camino-package-runtime"; \ nexport const handler = async (_context: MigrationContext): Promise<void> => { throw new Error( ${ JSON . stringify ( `Implement migration ${ name } ` ) } ); }; \ n`
2026-09-13 22:07:18 -07:00
: `import type {Implementation} from "../gen/qx.js"; \ nexport const handler: Implementation[ ${ JSON . stringify ( name ) } ] = ${ derived ? '{kind: "derived", get: ' : "" } async (_context) => { throw new Error( ${ JSON . stringify ( `Implement ${ name } ` ) } ); } ${ derived ? "}" : "" } ; \ n` ;
2026-09-10 18:27:41 -07:00
create ( file , implementation );
registry . exports . push ({ name , id : spec.id , file , ...( command === "migration" ? { migration : true } : {})});
if ( command === "migration" ) {
if ( ! spec . migration ) throw new Error ( "Migration scaffold requires retained contracts and an explicit transition" );
const { contracts , ... transition } = spec . migration ;
for ( const [ digest , contract ] of Object . entries ( contracts )) {
if ( contentDigest ( contract ) !== digest || ( catalog . contracts [ digest ] && contentDigest ( catalog . contracts [ digest ]) !== digest )) throw new Error ( "Retained migration contract mismatch" );
catalog . contracts [ digest ] = contract ;
}
catalog . migrations . push ({... transition , implementation : { exportId : spec.id , file , digest : contentDigest ( implementation )}});
}
}
if ( command === "refresh" ) for ( const migration of catalog . migrations ) {
const file = path . join ( root , prefix , migration . implementation . file );
if ( ! ( await fs . realpath ( file )). startsWith ( ` ${ await fs . realpath ( path . join ( root , prefix )) } /` )) throw new Error ( "Migration implementation escapes package" );
migration . implementation . digest = contentDigest ( await fs . readFile ( file , "utf8" ));
}
}
validateMigrationCatalog ( catalog , new Set ( registry . exports . map (( entry ) => entry . id )));
generated ( "quixos.scaffold.json" , json ( registry ));
generated ( "quixos.migrations.json" , json ( catalog ));
2026-09-13 22:07:18 -07:00
generated ( "src/server.ts" , marker + `import {servePackageRuntime} from "@quixos/camino-package-runtime"; \ nimport {createRuntime} from "./gen/qx.js"; \ n` + registry . exports . filter (( entry ) => ! entry . migration ). map (( entry , index ) => `import {handler as impl ${ index } } from ${ JSON . stringify ( `./ ${ entry . file . slice ( 4 , - 3 ) } .js` ) } ; \ n` ). join ( "" ) +
2026-09-10 18:27:41 -07:00
`servePackageRuntime(createRuntime({ \ n` + registry . exports . map (( entry ) => ` ${ JSON . stringify ( entry . name ) } : ${ entry . migration ? 'async () => { throw new Error("Migration-only export"); }' : `impl ${ registry . exports . filter (( value ) => ! value . migration ). indexOf ( entry ) } ` } ,` ). join ( "\n" ) + ` \ n})); \ n` );
const migrations = registry . exports . filter (( entry ) => entry . migration );
generated ( "src/migrate.ts" , marker + `import {serveMigration} from "@quixos/camino-package-runtime"; \ n` + migrations . map (( entry , index ) => `import {handler as impl ${ index } } from ${ JSON . stringify ( `./ ${ entry . file . slice ( 4 , - 3 ) } .js` ) } ; \ n` ). join ( "" ) + `await serveMigration({ ${ migrations . map (( entry , index ) => ` ${ JSON . stringify ( entry . id ) } : impl ${ index } ` ). join ( ", " ) } }); \ n` );
generated ( "descriptor.quixos-package.txtpb" , `# Generated by qx-scaffold-v1 \ npackage_id: ${ JSON . stringify ( registry . id ) } \ npackage_revision_id: ${ JSON . stringify ( registry . revision ) } \ nruntime_protocol_version: "quixos-capabilities-v1" \ n` + registry . exports . map (( entry ) => `exports: { export_id: ${ JSON . stringify ( entry . id ) } runtime_symbol: ${ JSON . stringify ( entry . name ) } } \ n` ). join ( "" ));
return { kind : "package" , source : spec.source , resourceRoot : spec.directory , files };
};