@@ -101,7 +101,28 @@ func determineOutputDirectory(cliOutputPath string, apiRequestOutputPath *string
101101 return outputDir , nil
102102}
103103
104- // This handler renders a boilerplate template with the provided variables.
104+ // prepareOutputDirectory determines the output directory path and creates it if needed.
105+ // This is shared logic used by both HandleBoilerplateRender and HandleBoilerplateRenderInline.
106+ //
107+ // Parameters:
108+ // - cliOutputPath: The base output path from CLI flag
109+ // - apiOutputPath: Optional subdirectory from API request (validated for security)
110+ //
111+ // Returns the absolute output directory path or an error.
112+ func prepareOutputDirectory (cliOutputPath string , apiOutputPath * string ) (string , error ) {
113+ // Determine the final output directory path
114+ outputDir , err := determineOutputDirectory (cliOutputPath , apiOutputPath )
115+ if err != nil {
116+ return "" , fmt .Errorf ("failed to determine output directory: %w" , err )
117+ }
118+
119+ // Create the output directory if it doesn't exist
120+ if err := os .MkdirAll (outputDir , 0755 ); err != nil {
121+ return "" , fmt .Errorf ("failed to create output directory: %w" , err )
122+ }
123+
124+ return outputDir , nil
125+ }
105126
106127// HandleBoilerplateRender renders a boilerplate template with the provided variables
107128func HandleBoilerplateRender (runbookPath string , cliOutputPath string ) gin.HandlerFunc {
@@ -140,33 +161,13 @@ func HandleBoilerplateRender(runbookPath string, cliOutputPath string) gin.Handl
140161 return
141162 }
142163
143- // Determine the output directory
144- outputDir , err := determineOutputDirectory (cliOutputPath , req .OutputPath )
164+ // Prepare the output directory (determine path and create it)
165+ // Files accumulate across multiple template renders in the same runbook
166+ outputDir , err := prepareOutputDirectory (cliOutputPath , req .OutputPath )
145167 if err != nil {
146- slog .Error ("Failed to determine output directory" , "error" , err )
147- c .JSON (http .StatusBadRequest , gin.H {
148- "error" : "Invalid output path" ,
149- "details" : err .Error (),
150- })
151- return
152- }
153-
154- // Create the output directory if it doesn't exist
155- if err := os .MkdirAll (outputDir , 0755 ); err != nil {
156- slog .Error ("Failed to create output directory" , "error" , err )
157- c .JSON (http .StatusInternalServerError , gin.H {
158- "error" : "Failed to create output directory" ,
159- "details" : err .Error (),
160- })
161- return
162- }
163-
164- // Clear any existing files in the output directory before rendering
165- // This ensures each render starts fresh without leftover files from previous renders
166- if err := deleteDirectoryContents (outputDir ); err != nil {
167- slog .Error ("Failed to clear output directory" , "error" , err )
168+ slog .Error ("Failed to prepare output directory" , "error" , err )
168169 c .JSON (http .StatusInternalServerError , gin.H {
169- "error" : "Failed to clear output directory" ,
170+ "error" : "Failed to prepare output directory" ,
170171 "details" : err .Error (),
171172 })
172173 return
@@ -318,7 +319,7 @@ func preConvertJSONTypes(value any, variableType bpVariables.BoilerplateType) an
318319}
319320
320321// HandleBoilerplateRenderInline renders boilerplate templates provided directly in the request body
321- func HandleBoilerplateRenderInline () gin.HandlerFunc {
322+ func HandleBoilerplateRenderInline (cliOutputPath string ) gin.HandlerFunc {
322323 return func (c * gin.Context ) {
323324 var req RenderInlineRequest
324325 if err := c .ShouldBindJSON (& req ); err != nil {
@@ -376,21 +377,22 @@ func HandleBoilerplateRenderInline() gin.HandlerFunc {
376377 slog .Debug ("Wrote template file" , "path" , fullPath )
377378 }
378379
379- // Create a temporary output directory
380- outputDir , err := os .MkdirTemp ("" , "boilerplate-output-*" )
380+ // Always render to a temp directory first for inline templates.
381+ // This prevents boilerplate from clearing existing files in the persistent output.
382+ tempOutputDir , err := os .MkdirTemp ("" , "boilerplate-output-*" )
381383 if err != nil {
382- slog .Error ("Failed to create output directory" , "error" , err )
384+ slog .Error ("Failed to create temp output directory" , "error" , err )
383385 c .JSON (http .StatusInternalServerError , gin.H {
384386 "error" : "Failed to create output directory" ,
385387 "details" : err .Error (),
386388 })
387389 return
388390 }
389- defer os .RemoveAll (outputDir ) // Clean up output directory when done
390- slog .Info ("Created temporary output directory" , "outputDir" , outputDir )
391+ defer os .RemoveAll (tempOutputDir )
392+ slog .Info ("Created temporary output directory" , "outputDir" , tempOutputDir )
391393
392- // Render the template using the boilerplate package
393- err = renderBoilerplateTemplate (tempDir , outputDir , req .Variables )
394+ // Render the template to the temp directory
395+ err = renderBoilerplateTemplate (tempDir , tempOutputDir , req .Variables )
394396 if err != nil {
395397 slog .Error ("Failed to render boilerplate template" , "error" , err )
396398 c .JSON (http .StatusInternalServerError , gin.H {
@@ -402,8 +404,8 @@ func HandleBoilerplateRenderInline() gin.HandlerFunc {
402404
403405 slog .Info ("Successfully rendered boilerplate template" )
404406
405- // Read all rendered files from the output directory
406- renderedFiles , err := readAllFilesInDirectory (outputDir )
407+ // Read all rendered files from the temp output directory
408+ renderedFiles , err := readAllFilesInDirectory (tempOutputDir )
407409 if err != nil {
408410 slog .Error ("Failed to read rendered files" , "error" , err )
409411 c .JSON (http .StatusInternalServerError , gin.H {
@@ -413,8 +415,53 @@ func HandleBoilerplateRenderInline() gin.HandlerFunc {
413415 return
414416 }
415417
416- // Build file tree from the generated output
417- fileTree , err := buildFileTreeWithRoot (outputDir , "" )
418+ // If generateFile is true, copy rendered files to the persistent output directory
419+ // This merges with existing files instead of replacing them
420+ var persistentOutputDir string
421+ if req .GenerateFile {
422+ persistentOutputDir , err = prepareOutputDirectory (cliOutputPath , nil )
423+ if err != nil {
424+ slog .Error ("Failed to prepare output directory" , "error" , err )
425+ c .JSON (http .StatusInternalServerError , gin.H {
426+ "error" : "Failed to prepare output directory" ,
427+ "details" : err .Error (),
428+ })
429+ return
430+ }
431+
432+ // Copy each rendered file to the persistent output (merging with existing files)
433+ for relPath , file := range renderedFiles {
434+ fullPath := filepath .Join (persistentOutputDir , relPath )
435+
436+ // Create parent directories if needed
437+ if err := os .MkdirAll (filepath .Dir (fullPath ), 0755 ); err != nil {
438+ slog .Error ("Failed to create directory" , "path" , filepath .Dir (fullPath ), "error" , err )
439+ c .JSON (http .StatusInternalServerError , gin.H {
440+ "error" : "Failed to create directory structure" ,
441+ "details" : err .Error (),
442+ })
443+ return
444+ }
445+
446+ if err := os .WriteFile (fullPath , []byte (file .Content ), 0644 ); err != nil {
447+ slog .Error ("Failed to write file" , "path" , fullPath , "error" , err )
448+ c .JSON (http .StatusInternalServerError , gin.H {
449+ "error" : "Failed to write file" ,
450+ "details" : err .Error (),
451+ })
452+ return
453+ }
454+ slog .Debug ("Copied file to persistent output" , "path" , fullPath )
455+ }
456+ slog .Info ("Successfully copied files to persistent output" , "outputDir" , persistentOutputDir )
457+ }
458+
459+ // Build file tree - use persistent dir if files were generated, otherwise temp
460+ fileTreeDir := tempOutputDir
461+ if persistentOutputDir != "" {
462+ fileTreeDir = persistentOutputDir
463+ }
464+ fileTree , err := buildFileTreeWithRoot (fileTreeDir , "" )
418465 if err != nil {
419466 slog .Error ("Failed to build file tree" , "error" , err )
420467 c .JSON (http .StatusInternalServerError , gin.H {
0 commit comments