2026-09-08 14:27:24 -07:00
#!/usr/bin/env node
2026-09-14 10:26:11 -07:00
import { readFile , writeFile , mkdtemp , rm , realpath } from "node:fs/promises" ;
2026-09-08 14:27:24 -07:00
import { parseQx , formatQx , lintQx } from "./source.js" ;
import { scaffoldAtom } from "./scaffold.js" ;
import { createGitCapabilityResolver } from "./git-resolver.js" ;
2026-09-10 18:27:41 -07:00
import { planEvolution } from "../capability-model/index.js" ;
2026-09-14 10:26:11 -07:00
import { snapshotRepository } from "./candidate-check.js" ;
2026-09-10 18:27:41 -07:00
import { planPinUpgrades , applyPinUpgrades , discoverUpgradeSpec , type UpgradeSpec } from "./pin-upgrades.js" ;
import path from "node:path" ;
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" ;
2026-09-14 10:26:11 -07:00
import { buildCheckedPackage , buildImmutableCandidate , snapshotCommit } from "./checked-build.js" ;
2026-09-13 22:07:18 -07:00
import { formatQuixosLock , loadQuixosLock , parseQuixosLockDocument } from "../resource-lock/index.js" ;
2026-09-14 10:26:11 -07:00
import { walkSyntax } from "./source.js" ;
import { inspectWorkbench } from "./authoring-inspect.js" ;
import { authoringContext } from "./authoring-context.js" ;
import { convergeAuthoring } from "./authoring-converge.js" ;
import { checkAuthoring } from "./authoring-check.js" ;
import { authoringWorklist } from "./authoring-worklist.js" ;
2026-09-13 22:07:18 -07:00
const authorSource = async ( root : string ) => {
2026-09-14 10:26:11 -07:00
const result = spawnSync ( "git" , [ "config" , "--get" , "remote.origin.url" ], { cwd : root , encoding : "utf8" });
2026-09-13 22:07:18 -07:00
if ( result . error || result . status !== 0 ) throw new Error ( "Managed resource has no origin" );
return { repository : result . stdout . trim (), commit : await snapshotCommit ( root )};
};
2026-09-08 14:27:24 -07:00
2026-09-14 10:26:11 -07:00
const readSpec = async ( value : string ) => {
if ( value === "-" ) { let input = "" ; for await ( const chunk of process . stdin ) { input += chunk ; if ( input . length > 1024 * 1024 ) throw new Error ( "Scaffold specification exceeds 1 MiB" ); } return JSON . parse ( input ); }
return JSON . parse ( value . trimStart (). startsWith ( "{" ) ? value : await readFile ( value , "utf8" ));
};
const planSummary = ( plan : Awaited < ReturnType < typeof planStructure >> ) => ({
root : plan . root , validation : plan . validation ,
changes : plan . changes . map ( change => ({ file : change . file , beforeBytes : change . before ? . length ?? 0 , afterBytes : change . after ? . length ?? 0 })),
note : "Structural plan only, not implementation verification. Run qx-workspace check while iterating." ,
});
2026-09-08 14:27:24 -07:00
const main = async () => {
const [ command , ... args ] = process . argv . slice ( 2 );
2026-09-14 10:26:11 -07:00
if ( command === "worklist" && ! args . includes ( "--help" )) {
if ( args . length !== 1 ) throw new Error ( "usage: quixos-qx worklist WORKBENCH" );
process . stdout . write ( ` ${ JSON . stringify ( await authoringWorklist ( args [ 0 ]), null , 2 ) } \n` );
return ;
}
if ([ "author-check" , "author-contract" ]. includes ( command ) && ! args . includes ( "--help" )) {
const [ root , output , ... flags ] = args ;
if ( ! root || ! output ) throw new Error ( "usage: quixos-qx author-check ROOT OUTPUT [--baseline FILE] [--reviews FILE]" );
const options : { baseline ?: string ; reviews ?: string } = {};
for ( let index = 0 ; index < flags . length ; index += 2 ) {
if ( ! flags [ index + 1 ]) throw new Error ( "Missing check option value" );
if ( flags [ index ] === "--baseline" ) options . baseline = flags [ index + 1 ];
else if ( flags [ index ] === "--reviews" ) options . reviews = flags [ index + 1 ];
else throw new Error ( `Unknown check option ${ flags [ index ] } ` );
}
const result = await checkAuthoring ( root , output , {... options , contractOnly : command === "author-contract" });
process . stdout . write ( ` ${ JSON . stringify ( result , null , 2 ) } \n` );
if ( result . blockers . length ) process . exitCode = 1 ;
return ;
}
if ([ "converge" , "_converge" ]. includes ( command ) && ! args . includes ( "--help" )) {
if ( ! args . length || args . length > 2 ) throw new Error ( "usage: quixos-qx converge WORKBENCH [REGISTERED_DIRECTORY] (join package writers first)" );
const context = await authoringContext ( args [ 0 ]);
if ( command === "converge" ) {
const result = spawnSync ( "flock" , [ "--exclusive" , "--nonblock" , "--conflict-exit-code" , "75" , path . join ( context . workbench , ".quixos/converge.lock" ),
process . execPath , process . argv [ 1 ], "_converge" , context . workbench , ...( args [ 1 ] ? [ args [ 1 ]] : [])], { stdio : "inherit" });
if ( result . error ) throw result . error ;
if ( result . status === 75 ) process . stderr . write ( "Another source coordinator is running; retry when it finishes.\n" );
process . exitCode = result . status ?? 1 ; return ;
}
const result = await convergeAuthoring ( context . workbench , args [ 1 ]);
process . stdout . write ( ` ${ JSON . stringify ( result , null , 2 ) } \n` );
if ( ! result . converged ) process . exitCode = 1 ;
return ;
}
if ( command === "check-committed" && ! args . includes ( "--help" )) {
const [ kind , repository , commit , log , ... extra ] = args ;
if ( ! [ "workspace" , "interface" , "package" ]. includes ( kind ) || ! log || extra . length ) throw new Error ( "usage: quixos-qx check-committed workspace|interface|package REPOSITORY COMMIT LOG_FILE" );
process . stdout . write ( ` ${ await buildImmutableCandidate ({ repository , commit } , kind as "workspace" | "interface" | "package", log)}\n` );
return ;
}
if ( ! command || command === "--help" || args . includes ( "--help" )) {
process . stdout . write ( "quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-function, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" +
"inspect WORKBENCH [RESOURCE] shows provisional contracts, with explicit historical fallback; never verification evidence.\n" +
"resources WORKBENCH lists registered editable repositories. Use qx-workspace for the workspace authoring workflow.\n" );
return ;
}
if ( command === "inspect" || command === "resources" ) {
if ( ! args [ 0 ] || args . length > ( command === "inspect" ? 2 : 1 )) throw new Error ( `usage: quixos-qx ${ command } WORKBENCH ${ command === "inspect" ? " [RESOURCE]" : "" } ` );
const result = command === "inspect" ? await inspectWorkbench ( args [ 0 ], args [ 1 ]) : await authoringContext ( args [ 0 ]);
process . stdout . write ( ` ${ JSON . stringify ( result , null , 2 ) } \n` );
return ;
}
if ( command === "source-baseline" ) {
if ( args . length !== 1 ) throw new Error ( "usage: quixos-qx source-baseline WORKBENCH" );
process . stdout . write ( ` ${ JSON . stringify ( await ( await authoringContext ( args [ 0 ])). baseline ()) } \n` );
return ;
}
2026-09-13 22:07:18 -07:00
if ( command === "scaffold-dependency" ) {
2026-09-14 10:26:11 -07:00
const [ root , kind , name , ... remaining ] = args ;
let repository : string | undefined , commit : string | undefined ;
const flags = [... remaining ];
if ( flags . length && ! flags [ 0 ]. startsWith ( "--" )) { repository = flags . shift (); commit = flags . shift (); }
else if ( root && [ "interface" , "package" ]. includes ( kind ) && name ) {
const context = await authoringContext ( root );
const alias = await realpath ( path . join ( context . workbench , ` ${ kind } s` , name )). catch (() => null );
const matches = context . resources . filter ( entry => entry . kind === kind && ( entry . resourceId === name || path . basename ( entry . directory ) === name || path . join ( context . workbench , entry . directory ) === alias ));
if ( matches . length !== 1 || ! matches [ 0 ]. source ) throw new Error ( `Select exactly one registered ${ kind } with qx-workspace resources; no match for ${ name } ` );
const selected = matches [ 0 ];
let source = selected . source ! ;
if ( flags . includes ( "--write" )) {
const retained = spawnSync ( "quixos-qx" , [ "converge" , context . workbench , selected . directory ], { encoding : "utf8" , env : {... process . env , QUIXOS_JJ_NO_CHECKPOINT : "1" }});
if ( retained . error || retained . status !== 0 ) throw new Error ( `Dependency source needs attention: ${ retained . error ? . message ?? retained . stdout ?? retained . stderr } ` );
source = JSON . parse ( retained . stdout ). candidate ;
}
repository = source . repository ; commit = source . commit ;
}
2026-09-13 22:07:18 -07:00
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 );
2026-09-14 10:26:11 -07:00
process . stdout . write ( ` ${ JSON . stringify ({... planSummary ( plan ), applied : flags . includes ( "--write" ) ? await applyStructure ( plan ) : undefined } , null, 2)}\n` );
2026-09-13 22:07:18 -07:00
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]" );
2026-09-14 10:26:11 -07:00
const spec = await readSpec ( specFile ) as ScaffoldRecipe ;
2026-09-13 22:07:18 -07:00
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 );
2026-09-14 10:26:11 -07:00
process . stdout . write ( ` ${ JSON . stringify ({... planSummary ( plan ), applied : flags . includes ( "--write" ) ? await applyStructure ( plan ) : undefined } , null, 2)}\n` );
2026-09-13 22:07:18 -07:00
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 ;
}
2026-09-10 18:27:41 -07:00
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-" ));
try { process . stdout . write ( ` ${ ( await snapshotRepository ( args [ 0 ], temporary )). treeDigest } \n` );} finally { await rm ( temporary , { recursive : true , force : true });}
return ;
}
if ( command === "pin-upgrade" ) {
const [ workbench , specFile , ... flags ] = args ;
if ( ! workbench || ! specFile ) throw new Error ( "usage: quixos-qx pin-upgrade WORKBENCH SPEC_JSON [--publish] [--resume UUID]" );
let publish = false , acceptEdits = false , resume : string | undefined ;
for ( let index = 0 ; index < flags . length ; index ++ ) {
if ( flags [ index ] === "--publish" ) publish = true ;
else if ( flags [ index ] === "--accept-edits" ) acceptEdits = true ;
else if ( flags [ index ] === "--resume" && /^[a-f0-9-]{36}$/ . test ( flags [ index + 1 ] ?? "" )) resume = flags [ ++ index ];
else throw new Error ( `Unknown pin-upgrade option ${ flags [ index ] } ` );
}
if ( acceptEdits && ! resume ) throw new Error ( "--accept-edits requires an existing refactor journal (--resume)" );
const plan = resume ? JSON . parse ( await readFile ( path . join ( workbench , ".quixos/upgrades" , ` ${ resume } .json` ), "utf8" )). plan
: await planPinUpgrades ( workbench , specFile === "auto" ? await discoverUpgradeSpec ( workbench ) : JSON . parse ( await readFile ( specFile , "utf8" )) as UpgradeSpec );
if ( resume && path . resolve ( workbench ) !== plan . workbench ) throw new Error ( "Upgrade journal belongs to another workbench" );
process . stdout . write ( ` ${ JSON . stringify ( publish ? await applyPinUpgrades ( plan , resume , undefined , { acceptEdits } ) : plan, null, 2)}\n` );
return ;
}
if ([ "scaffold-package" , "scaffold-function" , "scaffold-migration" , "scaffold-refresh" ]. includes ( command )) {
const [ root , specFile , ... flags ] = args ;
2026-09-13 22:07:18 -07:00
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" );
2026-09-14 10:26:11 -07:00
const parsed = parseQx ( `package Draft id "package:draft" revision "package:draft@1" { ${ spec . declaration } }` );
if ( parsed . diagnostics . length ) throw new Error ( parsed . diagnostics . map ( d => d . message ). join ( "\n" ));
const exported = [... walkSyntax ( parsed . root )]. filter ( node => [ "packageOperationExport" , "packageFunctionExport" , "packageConstructorExport" ]. includes ( node . kind ));
if ( exported . length !== 1 ) throw new Error ( "--declaration must contain exactly one function, operation or constructor export" );
const literal = exported [ 0 ]. children . find ( node => node . kind === "stringLiteral" );
if ( ! literal ) throw new Error ( "Declaration requires an authored export ID" );
spec . id = JSON . parse ( parsed . source . slice ( literal . start , literal . end ));
2026-09-13 22:07:18 -07:00
flags . splice ( declaration , 2 );
}
2026-09-14 10:26:11 -07:00
} else spec = await readSpec ( specFile ) as ScaffoldRecipe ;
2026-09-10 18:27:41 -07:00
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 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 ;
if ( flags . includes ( "--install" )) {
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" );
2026-09-13 22:07:18 -07:00
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 ) {
2026-09-10 18:27:41 -07:00
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 } ` );
}
2026-09-13 22:07:18 -07:00
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" );
2026-09-10 18:27:41 -07:00
}
2026-09-14 10:26:11 -07:00
process . stdout . write ( ` ${ JSON . stringify ({... planSummary ( plan ), applied } , null, 2)}\n` );
2026-09-10 18:27:41 -07:00
return ;
}
if ( command === "scaffold-structure" ) {
const [ root , spec , ... flags ] = args ;
if ( ! root || ! spec || flags . some (( flag ) => flag !== "--write" )) throw new Error ( "usage: quixos-qx scaffold-structure ROOT SPEC_JSON [--write]" );
2026-09-14 10:26:11 -07:00
const request = await readSpec ( spec ) as StructuralRequest ;
if ( request . kind !== "workspace" && ! request . source ) request . source = await authorSource ( root );
2026-09-10 18:27:41 -07:00
const plan = await planStructure ( root , request , process . env . QUIXOS_SNAPSHOT_MAP );
const applied = flags . includes ( "--write" ) ? await applyStructure ( plan ) : undefined ;
2026-09-14 10:26:11 -07:00
process . stdout . write ( ` ${ JSON . stringify ({... planSummary ( plan ), applied } , null, 2)}\n` );
2026-09-10 18:27:41 -07:00
return ;
}
if ( command === "scaffold-resume" ) {
const [ root , id , ... extra ] = args ;
if ( ! root || ! id || extra . length ) throw new Error ( "usage: quixos-qx scaffold-resume ROOT JOURNAL_ID" );
process . stdout . write ( ` ${ JSON . stringify ( await resumeStructure ( root , id ), null , 2 ) } \n` );
return ;
}
2026-09-14 10:26:11 -07:00
if ([ "check" , "check-resource" ]. includes ( command )) throw new Error ( "Use qx-workspace check in the registered repository; handwritten source/snapshot-map candidates are no longer an authoring check path" );
2026-09-10 18:27:41 -07:00
if ( command === "evolution" ) {
const [ baseline , candidate , reviews , ... extra ] = args ;
if ( ! baseline || ! candidate || extra . length ) throw new Error ( "usage: quixos-qx evolution BASELINE_JSON CANDIDATE_JSON [REVIEWS_JSON]" );
const before = baseline === "none" ? null : JSON . parse ( await readFile ( baseline , "utf8" ));
const after = JSON . parse ( await readFile ( candidate , "utf8" ));
const decisions = reviews ? JSON . parse ( await readFile ( reviews , "utf8" )) : [];
if ( ! Array . isArray ( decisions )) throw new Error ( "Reviews must be an array" );
process . stdout . write ( ` ${ JSON . stringify ( planEvolution ( before , after , { reviews : decisions } ), null, 2)}\n` );
return ;
}
2026-09-08 14:27:24 -07:00
if ( command === "scaffold-atom" ) {
const [ root , name , id , ... flags ] = args ;
if ( ! root || ! name || ! id || flags . some (( flag ) => flag !== "--write" )) throw new Error ( "usage: quixos-qx scaffold-atom ROOT NAME ID [--write]" );
const resolveResource = await createGitCapabilityResolver ({ checkoutRoot : ` ${ root } /.quixos/resource-checkouts` });
const plan = await scaffoldAtom ({ root , name , id , write : flags . includes ( "--write" ), resolveResource });
process . stdout . write ( ` ${ JSON . stringify ( plan , null , 2 ) } \n` );
return ;
}
const [ file , flag , ... rest ] = args ;
if ( ! file || rest . length || ( flag && flag !== "--write" ) || ! [ "parse" , "lint" , "format" ]. includes ( command ?? "" ) ||
( flag && command !== "format" )) throw new Error ( "usage: quixos-qx parse|lint|format FILE [--write (format only)]" );
const source = await readFile ( file , "utf8" );
if ( command === "format" ) {
const formatted = formatQx ( source );
if ( flag ) await writeFile ( file , formatted ); else process . stdout . write ( formatted );
} else {
const result = command === "parse" ? parseQx ( source , file ) : lintQx ( source , file );
process . stdout . write ( ` ${ JSON . stringify ( result , null , 2 ) } \n` );
if ( Array . isArray ( result ) ? result . some (( entry ) => entry . severity === "error" ) : result . diagnostics . length ) process . exitCode = 1 ;
}
};
main (). catch (( error : unknown ) => { console . error ( error instanceof Error ? error . message : error ); process . exitCode = 1 ; });