diff --git a/.changeset/init-adjustments.md b/.changeset/init-adjustments.md new file mode 100644 index 00000000..627f4c75 --- /dev/null +++ b/.changeset/init-adjustments.md @@ -0,0 +1,8 @@ +--- +"clerk": patch +--- + +Tighten the `clerk init` bootstrap flow: + +- Skip the redundant "Proceed?" scaffold confirmation when bootstrapping a new project (via `--starter` or on an empty directory). The scaffold plan is still previewed; only the now-superfluous prompt is removed since the user already opted in by starting bootstrap. +- Print bootstrap next steps (`cd `, ` dev`, etc.) after the optional "Install agent skills?" prompt so they remain the last thing visible when the command finishes. diff --git a/packages/cli-core/src/commands/init/index.test.ts b/packages/cli-core/src/commands/init/index.test.ts index 90572cbb..90674dfd 100644 --- a/packages/cli-core/src/commands/init/index.test.ts +++ b/packages/cli-core/src/commands/init/index.test.ts @@ -18,6 +18,7 @@ import * as scanMod from "./scan.ts"; import * as heuristics from "./heuristics.ts"; import * as skillsMod from "./skills.ts"; import * as bootstrapMod from "./bootstrap.ts"; +import * as nextStepsMod from "../../lib/next-steps.ts"; import { init } from "./index.ts"; const FAKE_CTX = { @@ -169,6 +170,75 @@ describe("init", () => { expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); }); + test("bootstrap flow skips scaffold Proceed? prompt (user already opted in)", async () => { + setup({ email: "test@test.com" }); + setupBootstrapSuccess(); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "app/layout.tsx", content: "", description: "" }], + postInstructions: [], + }); + + await init({}); + + expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); + expect(previewMod.previewAndConfirm).not.toHaveBeenCalled(); + expect(previewMod.previewPlan).toHaveBeenCalled(); + }); + + test("--starter skips scaffold Proceed? prompt even without -y", async () => { + setup({ email: "test@test.com" }); + spyOn(context, "gatherContext").mockResolvedValue(FAKE_CTX); + spyOn(config, "resolveProfile").mockResolvedValue({ profile: { appId: "app_123" } } as never); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "app/layout.tsx", content: "", description: "" }], + postInstructions: [], + }); + + await init({ starter: true }); + + expect(previewMod.previewAndConfirm).not.toHaveBeenCalled(); + expect(previewMod.previewPlan).toHaveBeenCalled(); + }); + + test("existing project without -y still prompts scaffold Proceed?", async () => { + setup({ email: "test@test.com" }); + spyOn(context, "gatherContext").mockResolvedValue(FAKE_CTX); + spyOn(config, "resolveProfile").mockResolvedValue({ profile: { appId: "app_123" } } as never); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "app/layout.tsx", content: "", description: "" }], + postInstructions: [], + }); + + await init({}); + + expect(previewMod.previewAndConfirm).toHaveBeenCalled(); + }); + + test("bootstrap prints next steps after skills install", async () => { + setup({ email: "test@test.com" }); + setupBootstrapSuccess(); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "app/layout.tsx", content: "", description: "" }], + postInstructions: [], + }); + + const callOrder: string[] = []; + spies.push( + spyOn(skillsMod, "installSkills").mockImplementation(async () => { + callOrder.push("installSkills"); + }), + ); + spies.push( + spyOn(nextStepsMod, "printNextSteps").mockImplementation(() => { + callOrder.push("printNextSteps"); + }), + ); + + await init({}); + + expect(callOrder.indexOf("installSkills")).toBeLessThan(callOrder.indexOf("printNextSteps")); + }); + test("blank dir with keyless framework auto-selects keyless when unauthenticated", async () => { setup(); diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index cea225da..6f90c72b 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -100,7 +100,10 @@ export async function init(options: InitOptions = {}) { // Short-circuit on a fully-clean re-run so env pull / skills prompt don't // execute when there's nothing to do. - const { alreadySetUp } = await detectAndInstall(ctx.cwd, ctx, overrides.skipConfirm); + // Bootstrap implies consent — the user already opted into project creation, so + // skip the scaffold "Proceed?" prompt as well. + const skipScaffoldConfirm = overrides.skipConfirm || bootstrap != null; + const { alreadySetUp } = await detectAndInstall(ctx.cwd, ctx, skipScaffoldConfirm); if (alreadySetUp) { log.success("\nClerk is already set up in this project."); @@ -117,15 +120,17 @@ export async function init(options: InitOptions = {}) { printKeylessInfo(); } - if (bootstrap) { - printBootstrapNextSteps(bootstrap, keyless); - } - if (options.skills !== false) { bar(); await installSkills(ctx.cwd, ctx.framework.dep, ctx.packageManager, overrides.skipConfirm); } + // Next steps print last so they stay on screen as the final thing the user sees. + if (bootstrap) { + bar(); + printBootstrapNextSteps(bootstrap, keyless); + } + outro("Done"); }