@@ -4,9 +4,11 @@ import { initConfig } from "../config/mod.ts";
44import { resolveConfig } from "../config/mod.ts" ;
55import { ExitCode } from "../config/types.ts" ;
66import { generateStacks } from "../compose/mod.ts" ;
7- import type { GenerateOptions } from "../compose/mod.ts" ;
8- import { join } from "@std/path" ;
9- import { exists } from "@std/fs" ;
7+ import type { ComposeData , GenerateOptions } from "../compose/mod.ts" ;
8+ import { join , resolve } from "@std/path" ;
9+ import { ensureDir , exists } from "@std/fs" ;
10+ import { parse as parseYaml , stringify as stringifyYaml } from "@std/yaml" ;
11+ import { renderStack } from "../render/mod.ts" ;
1012
1113/**
1214 * Parse and execute CLI commands.
@@ -65,6 +67,7 @@ export function buildCli(): Command {
6567 detect,
6668 preset,
6769 profile,
70+ writeGitignore,
6871 force,
6972 dryRun,
7073 cwd : Deno . cwd ( ) ,
@@ -100,6 +103,10 @@ export function buildCli(): Command {
100103 . option ( "--stacks <names:string>" , "Comma-separated list of stack names to generate." )
101104 . option ( "--output-dir <path:string>" , "Write generated stacks to a specific directory." )
102105 . option ( "--profile <name:string>" , "Use a specific profile." )
106+ . option (
107+ "--override <files:string>" ,
108+ "Comma-separated list of override files to apply." ,
109+ )
103110 . action ( async ( options : Record < string , unknown > ) => {
104111 try {
105112 const profile = options . profile as string | undefined ;
@@ -108,15 +115,19 @@ export function buildCli(): Command {
108115 const config = await resolveConfig ( { profile, cwd : Deno . cwd ( ) } ) ;
109116 const repoRoot = config . base . repoRoot ?? Deno . cwd ( ) ;
110117
118+ // Parse override file paths
119+ const overrideFiles = options . override
120+ ? ( options . override as string ) . split ( "," ) . map ( ( s : string ) => s . trim ( ) ) . filter ( Boolean )
121+ : undefined ;
122+
111123 const genOptions : GenerateOptions = {
112124 stacks : options . stacks
113125 ? ( options . stacks as string ) . split ( "," ) . map ( ( s : string ) => s . trim ( ) )
114126 : undefined ,
115- configStackNames : config . base . stack . names ,
116127 repoRoot,
117128 outputDir : options . outputDir as string | undefined ,
118129 dryRun,
119- network : config . base . stack . network ,
130+ overrides : overrideFiles ,
120131 } ;
121132
122133 const result = await generateStacks ( genOptions ) ;
@@ -163,9 +174,91 @@ export function buildCli(): Command {
163174 "--override <files:string>" ,
164175 "Comma-separated list of override files to apply before rendering." ,
165176 )
166- . action ( ( ) => {
167- console . error ( "render: not yet implemented (issue #5)" ) ;
168- Deno . exit ( 1 ) ;
177+ . option ( "--dry-run" , "Print rendered output without writing files." )
178+ . action ( async ( options : Record < string , unknown > ) => {
179+ try {
180+ const profile = options . profile as string | undefined ;
181+ const strict = options . strict as boolean | undefined ;
182+ const dryRun = options . dryRun as boolean | undefined ;
183+ const outputDir = options . outputDir as string | undefined ;
184+
185+ const config = await resolveConfig ( { profile, cwd : Deno . cwd ( ) } ) ;
186+ const repoRoot = config . base . repoRoot ?? Deno . cwd ( ) ;
187+ const renderOutputDir = outputDir || config . base . render . outputDirectory ;
188+
189+ // 1. Generate stacks (in memory)
190+ const genResult = await generateStacks ( {
191+ stacks : options . stacks
192+ ? ( options . stacks as string ) . split ( "," ) . map ( ( s : string ) => s . trim ( ) )
193+ : undefined ,
194+ repoRoot,
195+ outputDir : undefined , // generate in memory only
196+ dryRun : true , // generate in memory for render
197+ overrides : options . override
198+ ? ( options . override as string ) . split ( "," ) . map ( ( s : string ) => s . trim ( ) )
199+ : undefined ,
200+ } ) ;
201+
202+ if ( genResult . errors . length > 0 ) {
203+ for ( const e of genResult . errors ) console . error ( `error: ${ e } ` ) ;
204+ Deno . exit ( ExitCode . DriftOrValidation ) ;
205+ }
206+
207+ // 2. Render each generated stack
208+ const allWarnings : string [ ] = [ ] ;
209+ const results : Record < string , string > = { } ;
210+ let hasUnresolved = false ;
211+
212+ for ( const [ stackName , yamlContent ] of Object . entries ( genResult . generated ) ) {
213+ const parsed = parseYaml ( yamlContent ) as ComposeData ;
214+ const projectDir = repoRoot ; // generated stacks live at repo root
215+
216+ const result = await renderStack ( {
217+ data : parsed ,
218+ projectDir,
219+ repoRoot,
220+ strict,
221+ } ) ;
222+
223+ allWarnings . push ( ...result . warnings ) ;
224+ if ( result . hasUnresolved ) hasUnresolved = true ;
225+
226+ results [ stackName ] = `# Rendered by stackctl render — do not edit manually.\n${
227+ stringifyYaml ( result . data , {
228+ indent : 2 ,
229+ lineWidth : 120 ,
230+ } as Record < string , unknown > )
231+ } `;
232+ }
233+
234+ // 3. Print warnings
235+ for ( const w of allWarnings ) {
236+ console . error ( `warning: ${ w } ` ) ;
237+ }
238+
239+ // 4. Output
240+ if ( dryRun ) {
241+ for ( const [ name , content ] of Object . entries ( results ) ) {
242+ console . log ( `# --- rendered: ${ name } ---` ) ;
243+ console . log ( content ) ;
244+ }
245+ } else {
246+ const outDir = resolve ( repoRoot , renderOutputDir ) ;
247+ await ensureDir ( outDir ) ;
248+ for ( const [ name , content ] of Object . entries ( results ) ) {
249+ const outPath = join ( outDir , `${ name } .rendered.yml` ) ;
250+ await Deno . writeTextFile ( outPath , content ) ;
251+ console . log ( `wrote: ${ outPath } ` ) ;
252+ }
253+ }
254+
255+ if ( strict && hasUnresolved ) {
256+ Deno . exit ( ExitCode . DriftOrValidation ) ;
257+ }
258+ } catch ( err : unknown ) {
259+ console . error ( `error: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
260+ Deno . exit ( ExitCode . UnexpectedError ) ;
261+ }
169262 } ) ;
170263
171264 // --- up (issue #6) ---
0 commit comments