2026-09-08 14:27:24 -07:00
#!/usr/bin/env node
2026-09-10 18:27:41 -07:00
import { readFile , writeFile , mkdtemp , rm } 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" ;
import { checkWorkspaceCandidate , checkResourceCandidate , snapshotRepository } from "./candidate-check.js" ;
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-13 22:07:18 -07:00
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 )};
};
2026-09-08 14:27:24 -07:00
const main = async () => {
const [ command , ... args ] = process . argv . slice ( 2 );
2026-09-13 22:07:18 -07:00
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 ;
}
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 ( command === "check-resource" ) {
const [ root , kind , repository , commit , output , ... extra ] = args ;
if ( ! root || ! output || ! [ "package" , "interface" ]. includes ( kind ) || extra . length ) throw new Error ( "usage: quixos-qx check-resource ROOT package|interface REPOSITORY COMMIT OUTPUT" );
const result = await checkResourceCandidate ({ root , kind : kind as "package" | "interface" , source : { repository , commit }, output , publishedOnly : true });
process . stdout . write ( ` ${ JSON . stringify ( result , null , 2 ) } \n` );
if ( result . blockers . length ) process . exitCode = 1 ;
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" );
flags . splice ( declaration , 2 );
}
} else spec = JSON . parse ( await readFile ( specFile , "utf8" )) 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
}
process . stdout . write ( ` ${ JSON . stringify ({... plan , applied } , null, 2)}\n` );
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]" );
const request = JSON . parse ( await readFile ( spec , "utf8" )) as StructuralRequest ;
const plan = await planStructure ( root , request , process . env . QUIXOS_SNAPSHOT_MAP );
const applied = flags . includes ( "--write" ) ? await applyStructure ( plan ) : undefined ;
process . stdout . write ( ` ${ JSON . stringify ({... plan , applied } , null, 2)}\n` );
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 ;
}
if ( command === "check" ) {
const [ root , output , ... flags ] = args ;
if ( ! root || ! output || flags . length % 2 ) throw new Error ( "usage: quixos-qx check ROOT OUTPUT [--snapshot-map FILE] [--baseline FILE] [--reviews FILE]" );
const values = new Map < string , string > ();
for ( let index = 0 ; index < flags . length ; index += 2 ) {
if ( ! [ "--snapshot-map" , "--baseline" , "--reviews" ]. includes ( flags [ index ])) throw new Error ( `Unknown check option ${ flags [ index ] } ` );
values . set ( flags [ index ], flags [ index + 1 ]);
}
const result = await checkWorkspaceCandidate ({ root , output , snapshotMap : values . get ( "--snapshot-map" ), baseline : values . get ( "--baseline" ), reviews : values . get ( "--reviews" ) });
process . stdout . write ( ` ${ JSON . stringify ( result , null , 2 ) } \n` );
if ( result . blockers . length ) process . exitCode = 1 ;
return ;
}
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 ; });