@@ -14,6 +14,7 @@ type TemplateFile = {
1414 source ?: string ;
1515 destination: string ;
1616 content ? : string ;
17+ render ? : boolean ;
1718} ;
1819
1920type TemplateScaffold = {
@@ -24,6 +25,8 @@ type TemplateScaffold = {
2425type InitOptions = {
2526 dryRun ?: boolean ;
2627 force ? : boolean ;
28+ prd ? : string ;
29+ tasks ? : string ;
2730 var ?: string [ ] ;
2831 githubCreate ? : boolean ;
2932 githubExecute ? : boolean ;
@@ -118,72 +121,82 @@ program
118121 . argument ( '[name]' , 'Project directory/name' )
119122 . option ( '--dry-run' , 'Print planned actions without writing files' )
120123 . option ( '-f, --force' , 'Overwrite existing files' )
124+ . option ( '--prd <path>' , 'Copy a local PRD markdown file into docs/PRD.md' )
125+ . option ( '--tasks <path>' , 'Copy a local tasks markdown file into docs/TASKS.md' )
121126 . option ( '--var <KEY=VALUE>' , 'Template variable override. Can be repeated.' , collectVars , [ ] )
122127 . option ( '--github-create' , 'Plan a GitHub repository creation with gh. Defaults to dry-run; add --github-execute to run it' )
123128 . option ( '--github-execute' , 'Execute the planned gh repo create command. Requires --github-create and cannot be combined with --dry-run' )
124129 . option ( '--github-visibility <public|private>' , 'GitHub repository visibility for --github-create' , parseGithubVisibility , 'private' )
125130 . action ( async ( template : TemplateKey , name : string | undefined , options : InitOptions ) => {
126- const projectName = name ?? template ;
127- const projectRoot = path . resolve ( process . cwd ( ) , projectName ) ;
128- const variables = buildVariables ( projectName , options . var ?? [ ] ) ;
129- const plan = await buildWritePlan ( templateScaffolds [ template ] , projectRoot , variables ) ;
130- const existing = plan . filter ( ( item ) => item . existed ) ;
131- const githubPlan = buildGithubPlan ( projectRoot , variables , options ) ;
132-
133- if ( options . githubExecute && ! options . githubCreate ) {
134- console . error ( JSON . stringify ( {
135- ok : false ,
136- error : '--github-execute requires --github-create so repository creation is always explicit.'
137- } , null , 2 ) ) ;
138- process . exitCode = 1 ;
139- return ;
140- }
131+ try {
132+ const projectName = name ?? template ;
133+ const projectRoot = path . resolve ( process . cwd ( ) , projectName ) ;
134+ const variables = buildVariables ( projectName , options . var ?? [ ] ) ;
135+ const plan = await buildWritePlan ( templateScaffolds [ template ] , projectRoot , variables , options ) ;
136+ const existing = plan . filter ( ( item ) => item . existed ) ;
137+ const githubPlan = buildGithubPlan ( projectRoot , variables , options ) ;
138+
139+ if ( options . githubExecute && ! options . githubCreate ) {
140+ console . error ( JSON . stringify ( {
141+ ok : false ,
142+ error : '--github-execute requires --github-create so repository creation is always explicit.'
143+ } , null , 2 ) ) ;
144+ process . exitCode = 1 ;
145+ return ;
146+ }
141147
142- if ( options . githubExecute && options . dryRun ) {
143- console . error ( JSON . stringify ( {
144- ok : false ,
145- error : '--github-execute cannot be combined with --dry-run. Run once without --github-execute to review the gh command first.'
146- } , null , 2 ) ) ;
147- process . exitCode = 1 ;
148- return ;
149- }
148+ if ( options . githubExecute && options . dryRun ) {
149+ console . error ( JSON . stringify ( {
150+ ok : false ,
151+ error : '--github-execute cannot be combined with --dry-run. Run once without --github-execute to review the gh command first.'
152+ } , null , 2 ) ) ;
153+ process . exitCode = 1 ;
154+ return ;
155+ }
156+
157+ if ( existing . length > 0 && ! options . force && ! options . dryRun ) {
158+ console . error ( JSON . stringify ( {
159+ ok : false ,
160+ error : 'Refusing to overwrite existing files. Re-run with --force to overwrite.' ,
161+ files : existing . map ( ( item ) => path . relative ( process . cwd ( ) , item . destination ) )
162+ } , null , 2 ) ) ;
163+ process . exitCode = 1 ;
164+ return ;
165+ }
166+
167+ if ( ! options . dryRun ) {
168+ for ( const item of plan ) {
169+ await mkdir ( path . dirname ( item . destination ) , { recursive : true } ) ;
170+ await writeFile ( item . destination , item . source , 'utf8' ) ;
171+ }
150172
151- if ( existing . length > 0 && ! options . force && ! options . dryRun ) {
173+ if ( githubPlan . mode === 'execute' ) {
174+ await runGithubCreate ( githubPlan . command ) ;
175+ }
176+ }
177+
178+ console . log ( JSON . stringify ( {
179+ ok : true ,
180+ command : 'init' ,
181+ template,
182+ projectName,
183+ projectRoot,
184+ mode : options . dryRun ? 'dry-run' : 'write' ,
185+ force : Boolean ( options . force ) ,
186+ github : githubPlan ,
187+ files : plan . map ( ( item ) => ( {
188+ path : path . relative ( process . cwd ( ) , item . destination ) ,
189+ existed : item . existed ,
190+ bytes : item . bytes
191+ } ) )
192+ } , null , 2 ) ) ;
193+ } catch ( error ) {
152194 console . error ( JSON . stringify ( {
153195 ok : false ,
154- error : 'Refusing to overwrite existing files. Re-run with --force to overwrite.' ,
155- files : existing . map ( ( item ) => path . relative ( process . cwd ( ) , item . destination ) )
196+ error : error instanceof Error ? error . message : 'Unknown init error'
156197 } , null , 2 ) ) ;
157198 process . exitCode = 1 ;
158- return ;
159199 }
160-
161- if ( ! options . dryRun ) {
162- for ( const item of plan ) {
163- await mkdir ( path . dirname ( item . destination ) , { recursive : true } ) ;
164- await writeFile ( item . destination , item . source , 'utf8' ) ;
165- }
166-
167- if ( githubPlan . mode === 'execute' ) {
168- await runGithubCreate ( githubPlan . command ) ;
169- }
170- }
171-
172- console . log ( JSON . stringify ( {
173- ok : true ,
174- command : 'init' ,
175- template,
176- projectName,
177- projectRoot,
178- mode : options . dryRun ? 'dry-run' : 'write' ,
179- force : Boolean ( options . force ) ,
180- github : githubPlan ,
181- files : plan . map ( ( item ) => ( {
182- path : path . relative ( process . cwd ( ) , item . destination ) ,
183- existed : item . existed ,
184- bytes : item . bytes
185- } ) )
186- } , null , 2 ) ) ;
187200 } ) ;
188201
189202await program . parseAsync ( process . argv ) ;
@@ -246,13 +259,15 @@ function buildVariables(projectName: string, overrides: string[]): Record<string
246259async function buildWritePlan (
247260 template : TemplateScaffold ,
248261 projectRoot : string ,
249- variables : Record < string , string >
262+ variables : Record < string , string > ,
263+ options : InitOptions
250264) : Promise < WritePlanItem [ ] > {
251265 const items : WritePlanItem [ ] = [ ] ;
266+ const files = [ ...template . files , ...await buildLocalInputFiles ( options ) ] ;
252267
253- for ( const file of template . files ) {
268+ for ( const file of files ) {
254269 const rawContent = file . content ?? await readFile ( path . join ( sourceRoot , file . source ?? '' ) , 'utf8' ) ;
255- const renderedContent = render ( rawContent , variables ) ;
270+ const renderedContent = file . render === false ? rawContent : render ( rawContent , variables ) ;
256271 const renderedDestination = render ( file . destination , variables ) ;
257272 const destination = path . join ( projectRoot , renderedDestination ) ;
258273
@@ -267,6 +282,39 @@ async function buildWritePlan(
267282 return items ;
268283}
269284
285+ async function buildLocalInputFiles ( options : InitOptions ) : Promise < TemplateFile [ ] > {
286+ const files : TemplateFile [ ] = [ ] ;
287+
288+ if ( options . prd ) {
289+ files . push ( {
290+ destination : 'docs/PRD.md' ,
291+ content : await readLocalInputFile ( options . prd , 'PRD' ) ,
292+ render : false
293+ } ) ;
294+ }
295+
296+ if ( options . tasks ) {
297+ files . push ( {
298+ destination : 'docs/TASKS.md' ,
299+ content : await readLocalInputFile ( options . tasks , 'tasks' ) ,
300+ render : false
301+ } ) ;
302+ }
303+
304+ return files ;
305+ }
306+
307+ async function readLocalInputFile ( inputPath : string , label : string ) : Promise < string > {
308+ const resolvedPath = path . resolve ( process . cwd ( ) , inputPath ) ;
309+
310+ try {
311+ return await readFile ( resolvedPath , 'utf8' ) ;
312+ } catch ( error ) {
313+ const detail = error instanceof Error ? error . message : 'Unknown file read error' ;
314+ throw new Error ( `Unable to read ${ label } input file at ${ resolvedPath } : ${ detail } ` ) ;
315+ }
316+ }
317+
270318function render ( content : string , variables : Record < string , string > ) : string {
271319 return content . replace ( / \{ \{ ( [ A - Z 0 - 9 _ ] + ) \} \} / g, ( _match , key : string ) => variables [ key ] ?? '' ) ;
272320}
0 commit comments