22// SPDX-License-Identifier: Apache-2.0
33
44import { basename , dirname , join , parse , resolve } from 'node:path' ;
5- import { exec , execSync } from 'node:child_process' ;
5+ import { execFile , execFileSync } from 'node:child_process' ;
66import { cwd } from 'node:process' ;
77
88import { writeFile } from 'fs/promises' ;
@@ -37,6 +37,8 @@ export type Starter =
3737 | 'typescript'
3838 | 'vue' ;
3939
40+ type NPMClient = 'npm' | 'pnpm' ;
41+
4042export const startersData = {
4143 angular : {
4244 zip : `${ ELEMENTS_PAGES_BASE_URL } /starters/download/angular.zip` ,
@@ -110,11 +112,11 @@ export const startersData = {
110112
111113/* istanbul ignore next -- @preserve */
112114export async function archiveStarter ( projectDir : string , outDir : string ) {
113- const dist = ` ${ outDir } / ${ projectDir } ` ;
115+ const dist = join ( outDir , projectDir ) ;
114116 await copyProject ( projectDir ) ;
115117 writeAllAgentConfigs ( dist ) ;
116118 const packageJSON = await exportPackageFromWorkspace ( projectDir ) ;
117- await writeFile ( ` ${ dist } / package.json` , JSON . stringify ( packageJSON , undefined , 2 ) ) ;
119+ await writeFile ( join ( dist , ' package.json' ) , JSON . stringify ( packageJSON , undefined , 2 ) ) ;
118120 await zipProject ( dist ) ;
119121}
120122
@@ -134,7 +136,7 @@ async function zipProject(outDir: string) {
134136/* istanbul ignore next -- @preserve */
135137function copyProject ( projectDir : string ) {
136138 const ignoreDirs = new Set ( [ 'dist' , 'node_modules' , '.wireit' ] ) ;
137- cpSync ( projectDir , ` dist/ ${ projectDir } ` , {
139+ cpSync ( projectDir , join ( ' dist' , projectDir ) , {
138140 recursive : true ,
139141 filter : src => ! ignoreDirs . has ( basename ( src ) )
140142 } ) ;
@@ -179,8 +181,7 @@ export async function createStarter(starter: Starter, outDir: string = resolve(c
179181 if ( ! downloadPath ) {
180182 throw new Error ( `No download URL for starter "${ starter } "` ) ;
181183 }
182- const archivePath = `${ outDir } /${ starter } .zip` ;
183- const extractedPath = `${ outDir } /${ starter } ` ;
184+ const { archivePath, extractedPath } = createStarterPaths ( starter , outDir ) ;
184185 await downloadStarter ( downloadPath , archivePath ) ;
185186 await extractStarter ( archivePath , extractedPath ) ;
186187 await setupStarterGit ( extractedPath ) ;
@@ -202,6 +203,13 @@ export async function createStarter(starter: Starter, outDir: string = resolve(c
202203 }
203204}
204205
206+ export function createStarterPaths ( starter : Starter , outDir : string ) {
207+ return {
208+ archivePath : join ( outDir , `${ starter } .zip` ) ,
209+ extractedPath : join ( outDir , starter )
210+ } ;
211+ }
212+
205213/* istanbul ignore next -- @preserve */
206214async function downloadStarter ( starterPath : string , outPath : string ) {
207215 console . log ( '⏳ Downloading starter...' ) ;
@@ -225,7 +233,7 @@ async function setupStarterGit(extractedDir: string) {
225233 if ( hasGit && ! isGitRepository ( extractedDir ) ) {
226234 console . log ( '🔄 Initializing git...' ) ;
227235 await new Promise < void > ( ( resolve , reject ) => {
228- const child = exec ( `cd ${ extractedDir } && git init` ) ;
236+ const child = createGitInitProcess ( extractedDir ) ;
229237 child . on ( 'close' , code => ( code === 0 ? resolve ( ) : reject ( new Error ( `git init exited with code ${ code } ` ) ) ) ) ;
230238 child . on ( 'error' , reject ) ;
231239 } ) ;
@@ -234,6 +242,10 @@ async function setupStarterGit(extractedDir: string) {
234242 }
235243}
236244
245+ export function createGitInitProcess ( extractedDir : string ) {
246+ return execFile ( 'git' , [ 'init' , extractedDir ] ) ;
247+ }
248+
237249/* istanbul ignore next -- @preserve */
238250function isGitRepository ( directoryPath : string ) {
239251 // Check if .git directory exists directly in the given path
@@ -276,19 +288,17 @@ async function setupStarterNPM(extractedDir: string) {
276288
277289/* istanbul ignore next -- @preserve */
278290async function loginRegistry ( extractedDir : string ) {
279- const npmClient = await getNPMClient ( ) ;
291+ const npmClient = await getRequiredNPMClient ( ) ;
280292 console . log ( '🔒 Logging in to registry...' ) ;
281- execSync ( `cd ${ extractedDir } && ${ npmClient } login` , {
282- stdio : 'inherit'
283- } ) ;
293+ execPackageManagerSync ( npmClient , [ 'login' ] , extractedDir ) ;
284294}
285295
286296/* istanbul ignore next -- @preserve */
287297async function installFromRegistry ( extractedDir : string ) {
288- const npmClient = await getNPMClient ( ) ;
298+ const npmClient = await getRequiredNPMClient ( ) ;
289299 console . log ( '📦 Installing dependencies...' ) ;
290300 await new Promise < void > ( ( resolve , reject ) => {
291- const child = exec ( `cd ${ extractedDir } && ${ npmClient } install` ) ;
301+ const child = execPackageManager ( npmClient , [ ' install' ] , extractedDir ) ;
292302 child . on ( 'close' , code =>
293303 code === 0 ? resolve ( ) : reject ( new Error ( `${ npmClient } install exited with code ${ code } ` ) )
294304 ) ;
@@ -303,9 +313,7 @@ export async function startStarter(extractedPath: string) {
303313 console . log ( '🚀 Starting project...' ) ;
304314
305315 try {
306- execSync ( `cd ${ extractedPath } && ${ npmClient } run dev` , {
307- stdio : 'inherit'
308- } ) ;
316+ execPackageManagerSync ( npmClient , [ 'run' , 'dev' ] , extractedPath ) ;
309317 } catch ( e ) {
310318 if ( e instanceof Error && 'signal' in e && e . signal === 'SIGINT' ) {
311319 console . log ( '\n👋 Stopped.' ) ;
@@ -316,6 +324,21 @@ export async function startStarter(extractedPath: string) {
316324 }
317325}
318326
327+ async function getRequiredNPMClient ( ) {
328+ const npmClient = await getNPMClient ( ) ;
329+ if ( npmClient === 'npm' || npmClient === 'pnpm' ) return npmClient ;
330+ throw new Error ( 'No supported package manager found.' ) ;
331+ }
332+
333+ function execPackageManager ( npmClient : NPMClient , args : string [ ] , cwd : string ) {
334+ return npmClient === 'pnpm' ? execFile ( 'pnpm' , args , { cwd } ) : execFile ( 'npm' , args , { cwd } ) ;
335+ }
336+
337+ function execPackageManagerSync ( npmClient : NPMClient , args : string [ ] , cwd : string ) {
338+ const options = { cwd, stdio : 'inherit' as const } ;
339+ npmClient === 'pnpm' ? execFileSync ( 'pnpm' , args , options ) : execFileSync ( 'npm' , args , options ) ;
340+ }
341+
319342export const claudeProjectSettings = {
320343 $schema : 'https://json.schemastore.org/claude-code-settings.json' ,
321344 permissions : {
0 commit comments