1- import { basename , join , resolve } from "node:path" ;
1+ import { basename , resolve } from "node:path" ;
22import type { Option } from "@clack/prompts" ;
3- import { confirm , group , isCancel , select , text } from "@clack/prompts" ;
3+ import { group , select , text } from "@clack/prompts" ;
44import { Argument , type Command } from "commander" ;
5- import { execa } from "execa" ;
65import kebabCase from "lodash/kebabCase" ;
76import type { CLIContext , RunCommandResult } from "@/cli/types.js" ;
8- import {
9- Base44Command ,
10- getDashboardUrl ,
11- onPromptCancel ,
12- theme ,
13- } from "@/cli/utils/index.js" ;
7+ import { Base44Command , onPromptCancel , theme } from "@/cli/utils/index.js" ;
148import { InvalidInputError } from "@/core/errors.js" ;
15- import { deploySite , isDirEmpty , pushEntities } from "@/core/index.js" ;
9+ import { isDirEmpty } from "@/core/index.js" ;
1610import type { Template } from "@/core/project/index.js" ;
1711import {
1812 createProjectFiles ,
1913 listTemplates ,
20- readProjectConfig ,
2114 setAppConfig ,
2215} from "@/core/project/index.js" ;
23-
24- const DEFAULT_TEMPLATE_ID = "backend-only" ;
16+ import {
17+ completeProjectSetup ,
18+ DEFAULT_TEMPLATE_ID ,
19+ getTemplateById ,
20+ printProjectSummary ,
21+ } from "./scaffold-shared.js" ;
2522
2623interface CreateOptions {
2724 name ?: string ;
@@ -31,18 +28,6 @@ interface CreateOptions {
3128 skills ?: boolean ;
3229}
3330
34- async function getTemplateById ( templateId : string ) : Promise < Template > {
35- const templates = await listTemplates ( ) ;
36- const template = templates . find ( ( t ) => t . id === templateId ) ;
37- if ( ! template ) {
38- const validIds = templates . map ( ( t ) => t . id ) . join ( ", " ) ;
39- throw new InvalidInputError ( `Template "${ templateId } " not found.` , {
40- hints : [ { message : `Use one of: ${ validIds } ` } ] ,
41- } ) ;
42- }
43- return template ;
44- }
45-
4631function validateNonInteractiveFlags ( command : Command ) : void {
4732 const { path } = command . opts < CreateOptions > ( ) ;
4833
@@ -55,7 +40,7 @@ function validateNonInteractiveFlags(command: Command): void {
5540
5641async function createInteractive (
5742 options : CreateOptions ,
58- ctx : Pick < CLIContext , "log" | "runTask" > ,
43+ ctx : CLIContext ,
5944) : Promise < RunCommandResult > {
6045 const templates = await listTemplates ( ) ;
6146 const templateOptions : Option < Template > [ ] = templates . map ( ( t ) => ( {
@@ -116,7 +101,7 @@ async function createInteractive(
116101
117102async function createNonInteractive (
118103 options : CreateOptions ,
119- ctx : Pick < CLIContext , "log" | "runTask" > ,
104+ ctx : CLIContext ,
120105) : Promise < RunCommandResult > {
121106 ctx . log . info ( `Creating a new project at ${ resolve ( options . path ! ) } ` ) ;
122107
@@ -155,8 +140,9 @@ async function executeCreate(
155140 skills ?: boolean ;
156141 isInteractive : boolean ;
157142 } ,
158- { log , runTask } : Pick < CLIContext , "log" | "runTask" > ,
143+ ctx : CLIContext ,
159144) : Promise < RunCommandResult > {
145+ const { log, runTask } = ctx ;
160146 const name = rawName . trim ( ) ;
161147 const resolvedPath = resolve ( projectPath ) ;
162148
@@ -176,124 +162,19 @@ async function executeCreate(
176162 } ,
177163 ) ;
178164
179- // Set app config in cache for sync access to getDashboardUrl and getAppClient
180165 setAppConfig ( { id : projectId , projectRoot : resolvedPath } ) ;
181166
182- const { project, entities } = await readProjectConfig ( resolvedPath ) ;
183- let finalAppUrl : string | undefined ;
184-
185- if ( entities . length > 0 ) {
186- let shouldPushEntities : boolean ;
187-
188- if ( isInteractive ) {
189- const result = await confirm ( {
190- message :
191- "Set up the backend data now? (This pushes the data models used by the template to Base44)" ,
192- } ) ;
193- shouldPushEntities = ! isCancel ( result ) && result ;
194- } else {
195- shouldPushEntities = ! ! deploy ;
196- }
197-
198- if ( shouldPushEntities ) {
199- await runTask (
200- `Pushing ${ entities . length } data models to Base44...` ,
201- async ( ) => {
202- await pushEntities ( entities ) ;
203- } ,
204- {
205- successMessage : theme . colors . base44Orange (
206- "Data models pushed successfully" ,
207- ) ,
208- errorMessage : "Failed to push data models" ,
209- } ,
210- ) ;
211- }
212- }
213-
214- if ( project . site ) {
215- const { installCommand, buildCommand, outputDirectory } = project . site ;
216-
217- let shouldDeploy : boolean ;
218-
219- if ( isInteractive ) {
220- const result = await confirm ( {
221- message : "Would you like to deploy the site now? (Hosted on Base44)" ,
222- } ) ;
223- shouldDeploy = ! isCancel ( result ) && result ;
224- } else {
225- shouldDeploy = ! ! deploy ;
226- }
227-
228- if ( shouldDeploy && installCommand && buildCommand && outputDirectory ) {
229- const { appUrl } = await runTask (
230- "Installing dependencies..." ,
231- async ( updateMessage ) => {
232- await execa ( { cwd : resolvedPath , shell : true } ) `${ installCommand } ` ;
233-
234- updateMessage ( "Building project..." ) ;
235- await execa ( { cwd : resolvedPath , shell : true } ) `${ buildCommand } ` ;
236-
237- updateMessage ( "Deploying site..." ) ;
238- return await deploySite ( join ( resolvedPath , outputDirectory ) ) ;
239- } ,
240- {
241- successMessage : theme . colors . base44Orange (
242- "Site deployed successfully" ,
243- ) ,
244- errorMessage : "Failed to deploy site" ,
245- } ,
246- ) ;
247-
248- finalAppUrl = appUrl ;
249- }
250- }
251-
252- // Add AI agent skills (--no-skills flag sets skills to false, otherwise defaults to true)
253- const shouldAddSkills = skills ;
254-
255- if ( shouldAddSkills ) {
256- try {
257- await runTask (
258- "Installing AI agent skills..." ,
259- async ( ) => {
260- await execa ( "npx" , [ "-y" , "skills" , "add" , "base44/skills" , "-y" ] , {
261- cwd : resolvedPath ,
262- shell : true ,
263- } ) ;
264- } ,
265- {
266- successMessage : theme . colors . base44Orange (
267- "AI agent skills added successfully" ,
268- ) ,
269- errorMessage :
270- "Failed to add AI agent skills - you can add them later with: npx skills add base44/skills" ,
271- } ,
272- ) ;
273- } catch {
274- // Skills installation is non-critical (e.g., user may not have git installed)
275- // The error message is already shown by runTask, so we just continue
276- }
277- }
278-
279- log . message (
280- `${ theme . styles . header ( "Project" ) } : ${ theme . colors . base44Orange ( name ) } ` ,
281- ) ;
282- log . message (
283- `${ theme . styles . header ( "Dashboard" ) } : ${ theme . colors . links ( getDashboardUrl ( projectId ) ) } ` ,
167+ const summary = await completeProjectSetup (
168+ { projectId, name, resolvedPath, deploy, skills, isInteractive } ,
169+ ctx ,
284170 ) ;
285-
286- if ( finalAppUrl ) {
287- log . message (
288- `${ theme . styles . header ( "Site" ) } : ${ theme . colors . links ( finalAppUrl ) } ` ,
289- ) ;
290- }
171+ printProjectSummary ( summary , log ) ;
291172
292173 return { outroMessage : "Your project is set up and ready to use" } ;
293174}
294175
295176async function createAction (
296- { log , runTask , isNonInteractive } : CLIContext ,
177+ ctx : CLIContext ,
297178 name : string | undefined ,
298179 options : CreateOptions ,
299180) : Promise < RunCommandResult > {
@@ -303,7 +184,7 @@ async function createAction(
303184
304185 const skipPrompts = ! ! ( options . name ?? name ) && ! ! options . path ;
305186
306- if ( ! skipPrompts && isNonInteractive ) {
187+ if ( ! skipPrompts && ctx . isNonInteractive ) {
307188 throw new InvalidInputError (
308189 "Project name and --path are required in non-interactive mode" ,
309190 {
@@ -316,8 +197,6 @@ async function createAction(
316197 ) ;
317198 }
318199
319- const ctx = { log, runTask } ;
320-
321200 if ( skipPrompts ) {
322201 return await createNonInteractive (
323202 { name : options . name ?? name , ...options } ,
0 commit comments