Skip to content

Commit 64d9bd8

Browse files
moranshe-maxclaude
andcommitted
refactor(cli): extract shared scaffold logic into scaffold-shared
Move create's post-scaffold steps (push entities, deploy site, install AI skills, print the project/dashboard/site summary) plus getTemplateById and DEFAULT_TEMPLATE_ID into a new scaffold-shared.ts, so they can be reused by sibling commands. Pure refactor — no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1c3918b commit 64d9bd8

2 files changed

Lines changed: 191 additions & 141 deletions

File tree

packages/cli/src/cli/commands/project/create.ts

Lines changed: 20 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
1-
import { basename, join, resolve } from "node:path";
1+
import { basename, resolve } from "node:path";
22
import type { Option } from "@clack/prompts";
3-
import { confirm, group, isCancel, select, text } from "@clack/prompts";
3+
import { group, select, text } from "@clack/prompts";
44
import { Argument, type Command } from "commander";
5-
import { execa } from "execa";
65
import kebabCase from "lodash/kebabCase";
76
import 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";
148
import { InvalidInputError } from "@/core/errors.js";
15-
import { deploySite, isDirEmpty, pushEntities } from "@/core/index.js";
9+
import { isDirEmpty } from "@/core/index.js";
1610
import type { Template } from "@/core/project/index.js";
1711
import {
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

2623
interface 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-
4631
function validateNonInteractiveFlags(command: Command): void {
4732
const { path } = command.opts<CreateOptions>();
4833

@@ -55,7 +40,7 @@ function validateNonInteractiveFlags(command: Command): void {
5540

5641
async 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

117102
async 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

295176
async 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

Comments
 (0)