11/**
22 * charter run / stackbilt run — architect + scaffold in one step.
33 *
4+ * Uses the MCP gateway scaffold endpoint (TarotScript → materializer)
5+ * when an API key is available, producing deployment-ready Cloudflare
6+ * Workers with wrangler.toml, .ai/ governance, tests, and typed handlers.
7+ *
8+ * Falls back to the engine /build endpoint when no API key is set.
9+ *
410 * Usage:
511 * stackbilt run "Multi-tenant SaaS API with auth and billing"
612 * stackbilt run --file spec.md
@@ -14,24 +20,11 @@ import type { CLIOptions } from '../index';
1420import { EXIT_CODE , CLIError } from '../index' ;
1521import { getFlag } from '../flags' ;
1622import { loadCredentials } from '../credentials' ;
17- import { EngineClient , type BuildRequest , type BuildResult } from '../http-client' ;
23+ import { EngineClient , type BuildRequest , type ScaffoldResult } from '../http-client' ;
1824
1925// ─── Animation ──────────────────────────────────────────────
2026
21- interface Phase {
22- label : string ;
23- extract : ( r : BuildResult ) => string ;
24- }
25-
26- const PHASES : Phase [ ] = [
27- { label : 'PRODUCT' , extract : r => `${ r . requirements . keywords . length } requirements extracted` } ,
28- { label : 'UX' , extract : r => `${ Math . max ( 1 , Math . ceil ( r . requirements . keywords . length / 4 ) ) } user journeys mapped` } ,
29- { label : 'RISK' , extract : r => `${ r . compatibility . tensions . length + 3 } risks identified, ${ Math . max ( 1 , r . compatibility . tensions . length ) } critical` } ,
30- { label : 'ARCHITECT' , extract : r => `${ r . stack . length } components, ${ r . compatibility . pairs . length } integrations` } ,
31- { label : 'TDD' , extract : r => `${ Object . keys ( r . scaffold ) . filter ( f => f . includes ( 'test' ) ) . length + 5 } test scenarios generated` } ,
32- { label : 'SPRINT' , extract : r => `${ Object . keys ( r . scaffold ) . filter ( f => f . endsWith ( '.adf' ) || f . endsWith ( '.md' ) ) . length } ADRs, sprint plan ready` } ,
33- ] ;
34-
27+ const PHASE_LABELS = [ 'PRODUCT' , 'UX' , 'RISK' , 'ARCHITECT' , 'TDD' , 'SPRINT' ] ;
3528const SPINNER = [ '⠋' , '⠙' , '⠹' , '⠸' , '⠼' , '⠴' , '⠦' , '⠧' , '⠇' , '⠏' ] ;
3629
3730function delay ( ms : number ) : Promise < void > {
@@ -56,6 +49,23 @@ function slugify(description: string): string {
5649 return words . join ( '-' ) || 'my-project' ;
5750}
5851
52+ function phaseDetail ( label : string , result : ScaffoldResult ) : string {
53+ const fileCount = result . files . length ;
54+ const adfFiles = result . files . filter ( f => f . path . endsWith ( '.adf' ) ) . length ;
55+ const testFiles = result . files . filter ( f => f . path . includes ( 'test' ) ) . length ;
56+ const configFiles = result . files . filter ( f => f . path === 'wrangler.toml' || f . path === 'package.json' || f . path === 'tsconfig.json' ) . length ;
57+
58+ switch ( label ) {
59+ case 'PRODUCT' : return `requirements extracted from intent` ;
60+ case 'UX' : return `interface patterns mapped` ;
61+ case 'RISK' : return `threats identified and mitigated` ;
62+ case 'ARCHITECT' : return `${ fileCount } files, ${ configFiles } configs generated` ;
63+ case 'TDD' : return `${ testFiles || 1 } test file${ testFiles !== 1 ? 's' : '' } generated` ;
64+ case 'SPRINT' : return `${ adfFiles } governance files, sprint ready` ;
65+ default : return 'done' ;
66+ }
67+ }
68+
5969// ─── Command ────────────────────────────────────────────────
6070
6171export async function runCommand ( options : CLIOptions , args : string [ ] ) : Promise < number > {
@@ -76,15 +86,7 @@ export async function runCommand(options: CLIOptions, args: string[]): Promise<n
7686 if ( ! description ) throw new CLIError ( 'Empty description.' ) ;
7787
7888 // Parse flags
79- const request : BuildRequest = { description, constraints : { } } ;
80- if ( args . includes ( '--cloudflare-only' ) ) request . constraints ! . cloudflareOnly = true ;
81- const fw = getFlag ( args , '--framework' ) ;
82- if ( fw ) request . constraints ! . framework = fw ;
83- const db = getFlag ( args , '--database' ) ;
84- if ( db ) request . constraints ! . database = db ;
8589 const seedStr = getFlag ( args , '--seed' ) ;
86- if ( seedStr ) request . seed = parseInt ( seedStr , 10 ) ;
87-
8890 const outputDir = getFlag ( args , '--output' ) ?? `./${ slugify ( description ) } ` ;
8991 const dryRun = args . includes ( '--dry-run' ) ;
9092
@@ -93,97 +95,124 @@ export async function runCommand(options: CLIOptions, args: string[]): Promise<n
9395 const baseUrl = getFlag ( args , '--url' ) ;
9496 const client = new EngineClient ( { baseUrl : baseUrl ?? creds ?. baseUrl , apiKey : creds ?. apiKey } ) ;
9597
98+ // Determine path: gateway (with API key) or engine fallback
99+ const useGateway = ! ! creds ?. apiKey ;
100+
101+ let scaffoldPromise : Promise < ScaffoldResult > ;
102+
103+ if ( useGateway ) {
104+ // Gateway path — produces deployment-ready output (wrangler.toml, .ai/, tests)
105+ scaffoldPromise = client . scaffold ( {
106+ description,
107+ project_type : args . includes ( '--cloudflare-only' ) ? 'worker' : undefined ,
108+ complexity : undefined ,
109+ seed : seedStr ? parseInt ( seedStr , 10 ) : undefined ,
110+ } ) ;
111+ } else {
112+ // Engine fallback — basic scaffold
113+ const request : BuildRequest = { description, constraints : { } } ;
114+ if ( args . includes ( '--cloudflare-only' ) ) request . constraints ! . cloudflareOnly = true ;
115+ const fw = getFlag ( args , '--framework' ) ;
116+ if ( fw ) request . constraints ! . framework = fw ;
117+ const db = getFlag ( args , '--database' ) ;
118+ if ( db ) request . constraints ! . database = db ;
119+ if ( seedStr ) request . seed = parseInt ( seedStr , 10 ) ;
120+
121+ scaffoldPromise = client . build ( request ) . then ( r => ( {
122+ files : Object . entries ( r . scaffold ) . map ( ( [ p , content ] ) => ( { path : p , content } ) ) ,
123+ fileSource : 'engine' as const ,
124+ nextSteps : [ 'npm install' , 'npm run dev' ] ,
125+ seed : r . seed ,
126+ receipt : r . receipt ,
127+ } ) ) ;
128+ }
129+
96130 // JSON mode — no animation
97131 if ( options . format === 'json' ) {
98- const result = await client . build ( request ) ;
132+ const result = await scaffoldPromise ;
99133 console . log ( JSON . stringify ( { ...result , outputDir, dryRun } , null , 2 ) ) ;
100134 if ( ! dryRun ) {
101- writeFiles ( outputDir , Object . entries ( result . scaffold ) ) ;
102- cacheResult ( result , options . configPath ) ;
135+ writeFiles ( outputDir , result . files ) ;
103136 }
104137 return EXIT_CODE . SUCCESS ;
105138 }
106139
107140 // Interactive mode — animated output
108141 const isTTY = process . stdout . isTTY === true ;
109- const buildPromise = client . build ( request ) ;
110142
111143 console . log ( '' ) ;
144+ if ( ! useGateway ) {
145+ console . log ( ' \x1b[2m(tip: run `charter login --key sb_live_xxx` for deployment-ready scaffolds)\x1b[0m' ) ;
146+ console . log ( '' ) ;
147+ }
112148
113149 if ( isTTY ) {
114- // Show spinner phases while build is in-flight
115150 let spinIdx = 0 ;
116- const phaseLines = PHASES . map ( p => ` ${ SPINNER [ 0 ] } ${ p . label . padEnd ( 12 ) } working...` ) ;
117151
118- // Print initial phase lines
119- for ( const line of phaseLines ) {
120- console . log ( `\x1b[2m${ line } \x1b[0m` ) ;
152+ for ( const label of PHASE_LABELS ) {
153+ console . log ( `\x1b[2m ${ SPINNER [ 0 ] } ${ label . padEnd ( 12 ) } working...\x1b[0m` ) ;
121154 }
122155
123- // Animate spinners until build completes
124156 let done = false ;
125- let result ! : BuildResult ;
157+ let result ! : ScaffoldResult ;
126158
127- buildPromise . then ( r => { result = r ; done = true ; } ) . catch ( ( ) => { done = true ; } ) ;
159+ scaffoldPromise . then ( r => { result = r ; done = true ; } ) . catch ( ( ) => { done = true ; } ) ;
128160
129161 while ( ! done ) {
130162 spinIdx = ( spinIdx + 1 ) % SPINNER . length ;
131- cursorUp ( PHASES . length ) ;
132- for ( let i = 0 ; i < PHASES . length ; i ++ ) {
163+ cursorUp ( PHASE_LABELS . length ) ;
164+ for ( const label of PHASE_LABELS ) {
133165 clearLine ( ) ;
134- process . stdout . write ( `\x1b[2m ${ SPINNER [ spinIdx ] } ${ PHASES [ i ] . label . padEnd ( 12 ) } working...\x1b[0m\n` ) ;
166+ process . stdout . write ( `\x1b[2m ${ SPINNER [ spinIdx ] } ${ label . padEnd ( 12 ) } working...\x1b[0m\n` ) ;
135167 }
136168 await delay ( 80 ) ;
137169 }
138170
139- // Re-await to propagate errors
140- result = await buildPromise ;
171+ result = await scaffoldPromise ;
141172
142- // Replace spinners with completed checkmarks
143- cursorUp ( PHASES . length ) ;
144- for ( const phase of PHASES ) {
173+ cursorUp ( PHASE_LABELS . length ) ;
174+ for ( const label of PHASE_LABELS ) {
145175 clearLine ( ) ;
146- const detail = phase . extract ( result ) ;
147- process . stdout . write ( ` \x1b[32m❩\x1b[0m ${ phase . label . padEnd ( 12 ) } ${ detail . padEnd ( 36 ) } \x1b[32m✓\x1b[0m\n` ) ;
176+ const detail = phaseDetail ( label , result ) ;
177+ process . stdout . write ( ` \x1b[32m❩\x1b[0m ${ label . padEnd ( 12 ) } ${ detail . padEnd ( 36 ) } \x1b[32m✓\x1b[0m\n` ) ;
148178 await delay ( 120 ) ;
149179 }
150180 } else {
151- // Non-TTY: just wait and print
152- const result = await buildPromise ;
153- for ( const phase of PHASES ) {
154- console . log ( ` ❩ ${ phase . label . padEnd ( 12 ) } ${ phase . extract ( result ) . padEnd ( 36 ) } ✓` ) ;
181+ const result = await scaffoldPromise ;
182+ for ( const label of PHASE_LABELS ) {
183+ console . log ( ` ❩ ${ label . padEnd ( 12 ) } ${ phaseDetail ( label , result ) . padEnd ( 36 ) } ✓` ) ;
155184 }
156- await writeResult ( result ) ;
157185 }
158186
159- // Write files
160- const result = await buildPromise ;
161- const files = Object . entries ( result . scaffold ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) ) ;
187+ const result = await scaffoldPromise ;
162188
163189 console . log ( '' ) ;
164190 if ( dryRun ) {
165- console . log ( ` → ${ files . length } files would be scaffolded to ${ outputDir } /` ) ;
166- for ( const [ name ] of files ) {
167- console . log ( ` ${ name } ` ) ;
191+ console . log ( ` → ${ result . files . length } files would be scaffolded to ${ outputDir } /` ) ;
192+ for ( const f of result . files ) {
193+ console . log ( ` ${ f . path } ` ) ;
168194 }
169195 console . log ( '' ) ;
170196 console . log ( ' (dry run — no files written)' ) ;
171197 } else {
172- writeFiles ( outputDir , files ) ;
173- cacheResult ( result , options . configPath ) ;
174- console . log ( ` → ${ files . length } files scaffolded to ${ outputDir } /` ) ;
175- console . log ( ` → Architecture governed · seed: ${ result . seed } ` ) ;
198+ writeFiles ( outputDir , result . files ) ;
199+ console . log ( ` → ${ result . files . length } files scaffolded to ${ outputDir } /` ) ;
200+ console . log ( ` → Architecture governed · seed: ${ result . seed ?? 'deterministic' } ` ) ;
201+ if ( result . nextSteps && result . nextSteps . length > 0 ) {
202+ console . log ( '' ) ;
203+ console . log ( ' Next steps:' ) ;
204+ for ( const step of result . nextSteps ) {
205+ console . log ( ` ${ step } ` ) ;
206+ }
207+ }
176208 }
177209
178210 console . log ( '' ) ;
179211 return EXIT_CODE . SUCCESS ;
180212}
181213
182- // Placeholder for non-TTY path
183- async function writeResult ( _r : BuildResult ) : Promise < void > { }
184-
185- function writeFiles ( outputDir : string , files : [ string , string ] [ ] ) : void {
186- for ( const [ name , content ] of files ) {
214+ function writeFiles ( outputDir : string , files : Array < { path : string ; content : string } > ) : void {
215+ for ( const { path : name , content } of files ) {
187216 const target = path . join ( outputDir , name ) ;
188217 const dir = path . dirname ( target ) ;
189218 if ( ! fs . existsSync ( dir ) ) {
@@ -192,14 +221,3 @@ function writeFiles(outputDir: string, files: [string, string][]): void {
192221 fs . writeFileSync ( target , content ) ;
193222 }
194223}
195-
196- function cacheResult ( result : BuildResult , configPath : string ) : void {
197- const dir = configPath || '.charter' ;
198- if ( ! fs . existsSync ( dir ) ) {
199- fs . mkdirSync ( dir , { recursive : true } ) ;
200- }
201- fs . writeFileSync (
202- path . join ( dir , 'last-build.json' ) ,
203- JSON . stringify ( result , null , 2 ) ,
204- ) ;
205- }
0 commit comments