11import {
22 cancel ,
3+ intro ,
34 isCancel ,
45 log ,
56 select ,
@@ -9,16 +10,24 @@ import {
910import fs from "fs-extra" ;
1011import path from "node:path" ;
1112
12- import { scaffoldCreateTemplate } from "../templates/render-create-template" ;
13+ import {
14+ scaffoldCreateTemplate ,
15+ } from "../templates/render-create-template" ;
1316import {
1417 CreateCommandInputSchema ,
1518 CreateTemplateSchema ,
19+ type CreatePromptContext ,
20+ type CreateTargetPathState ,
1621 type CreateCommandInput ,
1722 type CreateTemplate ,
1823 type InitCommandInput ,
1924 type SchemaPreset ,
2025} from "../types" ;
21- import { runInitCommand } from "./init" ;
26+ import {
27+ collectInitContext ,
28+ executeInitContext ,
29+ } from "./init" ;
30+ import { getCreatePrismaIntro } from "../ui/branding" ;
2231
2332const DEFAULT_PROJECT_NAME = "my-app" ;
2433const DEFAULT_TEMPLATE : CreateTemplate = "hono" ;
@@ -38,21 +47,33 @@ function formatPathForDisplay(filePath: string): string {
3847 return path . relative ( process . cwd ( ) , filePath ) || "." ;
3948}
4049
50+ function validateProjectName ( value : string | undefined ) : string | undefined {
51+ const trimmed = String ( value ?? "" ) . trim ( ) ;
52+ if ( trimmed . length === 0 ) {
53+ return "Please enter a project name." ;
54+ }
55+
56+ if ( trimmed === "." || trimmed === ".." ) {
57+ return "Project name cannot be '.' or '..'." ;
58+ }
59+
60+ if ( path . isAbsolute ( trimmed ) ) {
61+ return "Use a relative project name instead of an absolute path." ;
62+ }
63+
64+ return undefined ;
65+ }
66+
4167async function promptForProjectName ( ) : Promise < string | undefined > {
4268 const projectName = await text ( {
4369 message : "Project name" ,
4470 placeholder : DEFAULT_PROJECT_NAME ,
4571 initialValue : DEFAULT_PROJECT_NAME ,
46- validate : ( value ) => {
47- const trimmed = String ( value ?? "" ) . trim ( ) ;
48- return trimmed . length > 0
49- ? undefined
50- : "Please enter a valid project name." ;
51- } ,
72+ validate : validateProjectName ,
5273 } ) ;
5374
5475 if ( isCancel ( projectName ) ) {
55- cancel ( "Cancelled ." ) ;
76+ cancel ( "Operation cancelled ." ) ;
5677 return undefined ;
5778 }
5879
@@ -78,24 +99,63 @@ async function promptForCreateTemplate(): Promise<CreateTemplate | undefined> {
7899 } ) ;
79100
80101 if ( isCancel ( template ) ) {
81- cancel ( "Cancelled ." ) ;
102+ cancel ( "Operation cancelled ." ) ;
82103 return undefined ;
83104 }
84105
85106 return CreateTemplateSchema . parse ( template ) ;
86107}
87108
88- async function isDirectoryEmpty ( directoryPath : string ) : Promise < boolean > {
89- if ( ! ( await fs . pathExists ( directoryPath ) ) ) {
90- return true ;
109+ async function inspectTargetPath ( targetPath : string ) : Promise < CreateTargetPathState > {
110+ if ( ! ( await fs . pathExists ( targetPath ) ) ) {
111+ return {
112+ exists : false ,
113+ isDirectory : true ,
114+ isEmptyDirectory : true ,
115+ } ;
116+ }
117+
118+ const stats = await fs . stat ( targetPath ) ;
119+ if ( ! stats . isDirectory ( ) ) {
120+ return {
121+ exists : true ,
122+ isDirectory : false ,
123+ isEmptyDirectory : false ,
124+ } ;
91125 }
92126
93- const entries = await fs . readdir ( directoryPath ) ;
94- return entries . length === 0 ;
127+ const entries = await fs . readdir ( targetPath ) ;
128+ return {
129+ exists : true ,
130+ isDirectory : true ,
131+ isEmptyDirectory : entries . length === 0 ,
132+ } ;
95133}
96134
97135export async function runCreateCommand ( rawInput : CreateCommandInput = { } ) : Promise < void > {
98- const input = CreateCommandInputSchema . parse ( rawInput ) ;
136+ try {
137+ const input = CreateCommandInputSchema . parse ( rawInput ) ;
138+
139+ intro ( getCreatePrismaIntro ( ) ) ;
140+
141+ const context = await collectCreateContext ( input ) ;
142+ if ( ! context ) {
143+ return ;
144+ }
145+
146+ await executeCreateContext ( context ) ;
147+ } catch ( error ) {
148+ cancel (
149+ `Create command failed: ${
150+ error instanceof Error ? error . message : String ( error )
151+ } `
152+ ) ;
153+ }
154+ }
155+
156+ async function collectCreateContext (
157+ input : CreateCommandInput
158+ ) : Promise < CreatePromptContext | undefined > {
99159 const useDefaults = input . yes === true ;
100160 const force = input . force === true ;
101161
@@ -116,9 +176,20 @@ export async function runCreateCommand(rawInput: CreateCommandInput = {}): Promi
116176 input . schemaPreset ?? DEFAULT_SCHEMA_PRESET ;
117177
118178 const targetDirectory = path . resolve ( process . cwd ( ) , projectName ) ;
119- const targetExists = await fs . pathExists ( targetDirectory ) ;
120- const targetIsEmpty = await isDirectoryEmpty ( targetDirectory ) ;
121- if ( targetExists && ! targetIsEmpty && ! force ) {
179+ const targetPathState = await inspectTargetPath ( targetDirectory ) ;
180+ if ( targetPathState . exists && ! targetPathState . isDirectory ) {
181+ cancel (
182+ `Target path ${ formatPathForDisplay (
183+ targetDirectory
184+ ) } already exists and is not a directory. Choose a different project name.`
185+ ) ;
186+ return ;
187+ }
188+ if (
189+ targetPathState . exists &&
190+ ! targetPathState . isEmptyDirectory &&
191+ ! force
192+ ) {
122193 cancel (
123194 `Target directory ${ formatPathForDisplay (
124195 targetDirectory
@@ -127,14 +198,47 @@ export async function runCreateCommand(rawInput: CreateCommandInput = {}): Promi
127198 return ;
128199 }
129200
201+ const initInput : InitCommandInput = {
202+ yes : input . yes ,
203+ verbose : input . verbose ,
204+ provider : input . provider ,
205+ packageManager : input . packageManager ,
206+ prismaPostgres : input . prismaPostgres ,
207+ databaseUrl : input . databaseUrl ,
208+ install : input . install ,
209+ generate : input . generate ,
210+ schemaPreset,
211+ } ;
212+
213+ const initContext = await collectInitContext ( initInput , {
214+ skipIntro : true ,
215+ projectDir : targetDirectory ,
216+ } ) ;
217+ if ( ! initContext ) {
218+ return ;
219+ }
220+
221+ return {
222+ targetDirectory,
223+ targetPathState,
224+ force,
225+ template,
226+ schemaPreset,
227+ projectPackageName : toPackageName ( path . basename ( targetDirectory ) ) ,
228+ initContext,
229+ } ;
230+ }
231+
232+ async function executeCreateContext ( context : CreatePromptContext ) : Promise < void > {
130233 const scaffoldSpinner = spinner ( ) ;
131- scaffoldSpinner . start ( `Scaffolding ${ template } project...` ) ;
234+ scaffoldSpinner . start ( `Scaffolding ${ context . template } project...` ) ;
132235 try {
133236 await scaffoldCreateTemplate ( {
134- projectDir : targetDirectory ,
135- projectName : toPackageName ( path . basename ( targetDirectory ) ) ,
136- template,
137- schemaPreset,
237+ projectDir : context . targetDirectory ,
238+ projectName : context . projectPackageName ,
239+ template : context . template ,
240+ schemaPreset : context . schemaPreset ,
241+ packageManager : context . initContext . packageManager ,
138242 } ) ;
139243 scaffoldSpinner . stop ( "Project files scaffolded." ) ;
140244 } catch ( error ) {
@@ -143,28 +247,21 @@ export async function runCreateCommand(rawInput: CreateCommandInput = {}): Promi
143247 return ;
144248 }
145249
146- if ( targetExists && ! targetIsEmpty && force ) {
250+ if (
251+ context . targetPathState . exists &&
252+ ! context . targetPathState . isEmptyDirectory &&
253+ context . force
254+ ) {
147255 log . warn (
148- `Used --force in non-empty directory ${ formatPathForDisplay ( targetDirectory ) } .`
256+ `Used --force in non-empty directory ${ formatPathForDisplay ( context . targetDirectory ) } .`
149257 ) ;
150258 }
151259
152- const initInput : InitCommandInput = {
153- yes : input . yes ,
154- verbose : input . verbose ,
155- provider : input . provider ,
156- packageManager : input . packageManager ,
157- prismaPostgres : input . prismaPostgres ,
158- databaseUrl : input . databaseUrl ,
159- install : input . install ,
160- generate : input . generate ,
161- schemaPreset,
162- } ;
163-
164- const cdStep = `- cd ${ formatPathForDisplay ( targetDirectory ) } ` ;
165- await runInitCommand ( initInput , {
260+ const cdStep = `- cd ${ formatPathForDisplay ( context . targetDirectory ) } ` ;
261+ await executeInitContext ( context . initContext , {
166262 skipIntro : true ,
167263 prependNextSteps : [ cdStep ] ,
168- projectDir : targetDirectory ,
264+ projectDir : context . targetDirectory ,
265+ includeDevNextStep : true ,
169266 } ) ;
170267}
0 commit comments