create-better-t-stack 3.41.3 → 3.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { D as setProcessMode, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as types_exports, y as getLatestCLIVersion } from "./src-BhgA6H5Y.mjs";
2
+ import { D as setProcessMode, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as types_exports, y as getLatestCLIVersion } from "./src-C0_lYEa0.mjs";
3
3
  import z from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/server";
5
5
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
package/dist/index.d.mts CHANGED
@@ -144,6 +144,7 @@ interface CreateProjectResult {
144
144
  projectDirectory: string;
145
145
  relativePath: string;
146
146
  error?: string;
147
+ warnings?: string[];
147
148
  }
148
149
  //#endregion
149
150
  //#region src/helpers/core/add-handler.d.ts
@@ -268,20 +269,20 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
268
269
  mcp?: {
269
270
  scope?: "project" | "global" | undefined;
270
271
  servers?: ("nx" | "better-auth" | "clerk" | "polar" | "neon" | "planetscale" | "supabase" | "convex" | "better-t-stack" | "context7" | "cloudflare-docs" | "shadcn" | "next-devtools" | "nuxt-docs" | "nuxt-ui-docs" | "svelte-docs" | "astro-docs" | "expo")[] | undefined;
271
- agents?: ("cursor" | "vscode" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "gemini-cli" | "goose" | "cline" | "antigravity" | "cline-cli" | "github-copilot-cli" | "grok-build" | "mcporter" | "claude-desktop")[] | undefined;
272
+ agents?: ("cursor" | "vscode" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "gemini-cli" | "kiro-cli" | "goose" | "cline" | "antigravity" | "cline-cli" | "github-copilot-cli" | "grok-build" | "mcporter" | "claude-desktop" | "fx" | "kilo-code" | "kimi-code")[] | undefined;
272
273
  } | undefined;
273
274
  skills?: {
274
275
  scope?: "project" | "global" | undefined;
275
- agents?: ("cursor" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "pi" | "gemini-cli" | "github-copilot" | "kiro-cli" | "droid" | "goose" | "cline" | "continue" | "amp" | "qwen-code" | "crush" | "antigravity" | "universal" | "adal" | "aider-desk" | "antigravity-cli" | "astrbot" | "augment" | "autohand-code" | "bob" | "codearts-agent" | "codebuddy" | "codemaker" | "codestudio" | "command-code" | "cortex" | "deepagents" | "devin" | "dexto" | "eve" | "firebender" | "forgecode" | "grok" | "hermes-agent" | "iflow-cli" | "inference-sh" | "jazz" | "junie" | "kilo" | "kimchi" | "kimi-code-cli" | "kode" | "lingma" | "loaf" | "mcpjam" | "mistral-vibe" | "moxby" | "mux" | "neovate" | "ona" | "clawdbot" | "openclaw" | "openhands" | "pochi" | "promptscript" | "qoder" | "qoder-cn" | "reasonix" | "replit" | "roo" | "rovodev" | "tabnine-cli" | "terramind" | "tinycloud" | "trae" | "trae-cn" | "warp" | "zcode" | "zencoder" | "zenflow")[] | undefined;
276
+ agents?: ("cursor" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "pi" | "gemini-cli" | "github-copilot" | "kiro-cli" | "droid" | "goose" | "cline" | "continue" | "amp" | "qwen-code" | "crush" | "antigravity" | "universal" | "adal" | "aider-desk" | "antigravity-cli" | "astrbot" | "augment" | "autohand-code" | "bob" | "codearts-agent" | "codebuddy" | "codemaker" | "codestudio" | "command-code" | "cortex" | "deepagents" | "devin" | "dexto" | "eve" | "firebender" | "forgecode" | "grok" | "hermes-agent" | "iflow-cli" | "inference-sh" | "jazz" | "junie" | "kilo" | "kimchi" | "kimi-code-cli" | "kode" | "lingma" | "loaf" | "mcpjam" | "minimax-code" | "mistral-vibe" | "moxby" | "mux" | "neovate" | "ona" | "clawdbot" | "openclaw" | "openhands" | "pochi" | "posit-assistant" | "promptscript" | "qoder" | "qoder-cn" | "reasonix" | "replit" | "roo" | "rovodev" | "tabnine-cli" | "terramind" | "tinycloud" | "trae" | "trae-cn" | "warp" | "zcode" | "zencoder" | "zenflow")[] | undefined;
276
277
  selections?: {
277
- source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
278
+ source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel/next.js" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
278
279
  skills: string[];
279
280
  }[] | undefined;
280
281
  } | undefined;
281
282
  ultracite?: {
282
283
  linter?: "biome" | "oxlint" | undefined;
283
- editors?: ("void" | "cursor" | "vscode" | "zed" | "windsurf" | "antigravity" | "bob" | "codebuddy" | "trae" | "kiro")[] | undefined;
284
- agents?: ("codex" | "opencode" | "pi" | "droid" | "goose" | "cline" | "continue" | "amp" | "aider" | "crush" | "vercel" | "universal" | "adal" | "bob" | "deepagents" | "devin" | "firebender" | "junie" | "mcpjam" | "mistral-vibe" | "mux" | "ona" | "openclaw" | "qoder" | "replit" | "warp" | "zencoder" | "claude" | "jules" | "lovable" | "snowflake-cortex" | "kimi-cli" | "copilot" | "firebase-studio" | "open-hands" | "gemini" | "augmentcode" | "kilo-code" | "roo-code" | "qwen" | "amazon-q-cli" | "cursor-cli")[] | undefined;
284
+ editors?: ("void" | "cursor" | "vscode" | "zed" | "windsurf" | "antigravity" | "universal" | "bob" | "codebuddy" | "trae" | "kiro")[] | undefined;
285
+ agents?: ("codex" | "opencode" | "pi" | "droid" | "goose" | "cline" | "continue" | "amp" | "aider" | "crush" | "vercel" | "kilo-code" | "universal" | "adal" | "bob" | "deepagents" | "devin" | "firebender" | "junie" | "mcpjam" | "mistral-vibe" | "mux" | "ona" | "openclaw" | "qoder" | "replit" | "warp" | "zencoder" | "claude" | "jules" | "lovable" | "snowflake-cortex" | "kimi-cli" | "copilot" | "firebase-studio" | "open-hands" | "gemini" | "augmentcode" | "roo-code" | "qwen" | "amazon-q-cli" | "cursor-cli")[] | undefined;
285
286
  hooks?: ("cursor" | "windsurf" | "codebuddy" | "claude" | "copilot")[] | undefined;
286
287
  } | undefined;
287
288
  } | undefined;
@@ -382,20 +383,20 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
382
383
  mcp?: {
383
384
  scope?: "project" | "global" | undefined;
384
385
  servers?: ("nx" | "better-auth" | "clerk" | "polar" | "neon" | "planetscale" | "supabase" | "convex" | "better-t-stack" | "context7" | "cloudflare-docs" | "shadcn" | "next-devtools" | "nuxt-docs" | "nuxt-ui-docs" | "svelte-docs" | "astro-docs" | "expo")[] | undefined;
385
- agents?: ("cursor" | "vscode" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "gemini-cli" | "goose" | "cline" | "antigravity" | "cline-cli" | "github-copilot-cli" | "grok-build" | "mcporter" | "claude-desktop")[] | undefined;
386
+ agents?: ("cursor" | "vscode" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "gemini-cli" | "kiro-cli" | "goose" | "cline" | "antigravity" | "cline-cli" | "github-copilot-cli" | "grok-build" | "mcporter" | "claude-desktop" | "fx" | "kilo-code" | "kimi-code")[] | undefined;
386
387
  } | undefined;
387
388
  skills?: {
388
389
  scope?: "project" | "global" | undefined;
389
- agents?: ("cursor" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "pi" | "gemini-cli" | "github-copilot" | "kiro-cli" | "droid" | "goose" | "cline" | "continue" | "amp" | "qwen-code" | "crush" | "antigravity" | "universal" | "adal" | "aider-desk" | "antigravity-cli" | "astrbot" | "augment" | "autohand-code" | "bob" | "codearts-agent" | "codebuddy" | "codemaker" | "codestudio" | "command-code" | "cortex" | "deepagents" | "devin" | "dexto" | "eve" | "firebender" | "forgecode" | "grok" | "hermes-agent" | "iflow-cli" | "inference-sh" | "jazz" | "junie" | "kilo" | "kimchi" | "kimi-code-cli" | "kode" | "lingma" | "loaf" | "mcpjam" | "mistral-vibe" | "moxby" | "mux" | "neovate" | "ona" | "clawdbot" | "openclaw" | "openhands" | "pochi" | "promptscript" | "qoder" | "qoder-cn" | "reasonix" | "replit" | "roo" | "rovodev" | "tabnine-cli" | "terramind" | "tinycloud" | "trae" | "trae-cn" | "warp" | "zcode" | "zencoder" | "zenflow")[] | undefined;
390
+ agents?: ("cursor" | "zed" | "windsurf" | "codex" | "claude-code" | "opencode" | "pi" | "gemini-cli" | "github-copilot" | "kiro-cli" | "droid" | "goose" | "cline" | "continue" | "amp" | "qwen-code" | "crush" | "antigravity" | "universal" | "adal" | "aider-desk" | "antigravity-cli" | "astrbot" | "augment" | "autohand-code" | "bob" | "codearts-agent" | "codebuddy" | "codemaker" | "codestudio" | "command-code" | "cortex" | "deepagents" | "devin" | "dexto" | "eve" | "firebender" | "forgecode" | "grok" | "hermes-agent" | "iflow-cli" | "inference-sh" | "jazz" | "junie" | "kilo" | "kimchi" | "kimi-code-cli" | "kode" | "lingma" | "loaf" | "mcpjam" | "minimax-code" | "mistral-vibe" | "moxby" | "mux" | "neovate" | "ona" | "clawdbot" | "openclaw" | "openhands" | "pochi" | "posit-assistant" | "promptscript" | "qoder" | "qoder-cn" | "reasonix" | "replit" | "roo" | "rovodev" | "tabnine-cli" | "terramind" | "tinycloud" | "trae" | "trae-cn" | "warp" | "zcode" | "zencoder" | "zenflow")[] | undefined;
390
391
  selections?: {
391
- source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
392
+ source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel/next.js" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
392
393
  skills: string[];
393
394
  }[] | undefined;
394
395
  } | undefined;
395
396
  ultracite?: {
396
397
  linter?: "biome" | "oxlint" | undefined;
397
- editors?: ("void" | "cursor" | "vscode" | "zed" | "windsurf" | "antigravity" | "bob" | "codebuddy" | "trae" | "kiro")[] | undefined;
398
- agents?: ("codex" | "opencode" | "pi" | "droid" | "goose" | "cline" | "continue" | "amp" | "aider" | "crush" | "vercel" | "universal" | "adal" | "bob" | "deepagents" | "devin" | "firebender" | "junie" | "mcpjam" | "mistral-vibe" | "mux" | "ona" | "openclaw" | "qoder" | "replit" | "warp" | "zencoder" | "claude" | "jules" | "lovable" | "snowflake-cortex" | "kimi-cli" | "copilot" | "firebase-studio" | "open-hands" | "gemini" | "augmentcode" | "kilo-code" | "roo-code" | "qwen" | "amazon-q-cli" | "cursor-cli")[] | undefined;
398
+ editors?: ("void" | "cursor" | "vscode" | "zed" | "windsurf" | "antigravity" | "universal" | "bob" | "codebuddy" | "trae" | "kiro")[] | undefined;
399
+ agents?: ("codex" | "opencode" | "pi" | "droid" | "goose" | "cline" | "continue" | "amp" | "aider" | "crush" | "vercel" | "kilo-code" | "universal" | "adal" | "bob" | "deepagents" | "devin" | "firebender" | "junie" | "mcpjam" | "mistral-vibe" | "mux" | "ona" | "openclaw" | "qoder" | "replit" | "warp" | "zencoder" | "claude" | "jules" | "lovable" | "snowflake-cortex" | "kimi-cli" | "copilot" | "firebase-studio" | "open-hands" | "gemini" | "augmentcode" | "roo-code" | "qwen" | "amazon-q-cli" | "cursor-cli")[] | undefined;
399
400
  hooks?: ("cursor" | "windsurf" | "codebuddy" | "claude" | "copilot")[] | undefined;
400
401
  } | undefined;
401
402
  } | undefined;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { C as DirectoryConflictError, E as ValidationError, S as DatabaseSetupError, T as UserCancelledError, _ as ProjectLauncherSchema, a as TEMPLATE_COUNT, b as CLIError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as ProjectCreationError, x as CompatibilityError } from "./src-BhgA6H5Y.mjs";
2
+ import { C as DirectoryConflictError, E as ValidationError, S as DatabaseSetupError, T as UserCancelledError, _ as ProjectLauncherSchema, a as TEMPLATE_COUNT, b as CLIError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as ProjectCreationError, x as CompatibilityError } from "./src-C0_lYEa0.mjs";
3
3
  export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, ProjectLauncherSchema, Result, SchemaNameSchema, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, getSchemaResult, router, sponsors };
@@ -320,14 +320,153 @@ async function runWithContextAsync(options, fn) {
320
320
  return cliStorage.run(createContext(options), fn);
321
321
  }
322
322
  //#endregion
323
+ //#region src/utils/glyphs.ts
324
+ const unicode = process.platform !== "win32";
325
+ const S_STEP_ACTIVE = unicode ? "◆" : "*";
326
+ const S_STEP_CANCEL = unicode ? "■" : "x";
327
+ const S_STEP_ERROR = unicode ? "▲" : "x";
328
+ const S_STEP_SUBMIT = unicode ? "◇" : "o";
329
+ const S_STEP_BACK = unicode ? "↶" : "<";
330
+ const S_BAR = unicode ? "│" : "|";
331
+ const S_BAR_END = unicode ? "└" : "—";
332
+ const S_RADIO_ACTIVE = unicode ? "●" : ">";
333
+ const S_RADIO_INACTIVE = unicode ? "○" : " ";
334
+ const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
335
+ const S_CHECKBOX_SELECTED = unicode ? "◼" : "[+]";
336
+ const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
337
+ const SPINNER_FRAMES = unicode ? [
338
+ "◒",
339
+ "◐",
340
+ "◓",
341
+ "◑"
342
+ ] : [
343
+ "•",
344
+ "o",
345
+ "O",
346
+ "0"
347
+ ];
348
+ //#endregion
349
+ //#region src/utils/interrupt.ts
350
+ let stepInterrupted = false;
351
+ let anyStepInterrupted = false;
352
+ let controller = null;
353
+ let listener = null;
354
+ /**
355
+ * Once files are on disk, Ctrl-C should only stop the step that is running. Holding a SIGINT
356
+ * listener keeps the CLI alive; the terminal's signal already ends the subprocess. Each
357
+ * optional step starts with startInterruptibleStep() so a cancel applies to that step only.
358
+ */
359
+ function beginInterruptibleScope() {
360
+ endInterruptibleScope();
361
+ stepInterrupted = false;
362
+ anyStepInterrupted = false;
363
+ listener = () => {
364
+ stepInterrupted = true;
365
+ anyStepInterrupted = true;
366
+ controller?.abort();
367
+ };
368
+ process.on("SIGINT", listener);
369
+ }
370
+ function endInterruptibleScope() {
371
+ if (listener) process.off("SIGINT", listener);
372
+ listener = null;
373
+ controller = null;
374
+ }
375
+ function startInterruptibleStep() {
376
+ stepInterrupted = false;
377
+ controller = listener ? new AbortController() : null;
378
+ }
379
+ /** True if the user pressed Ctrl-C during the current step. */
380
+ function wasInterrupted() {
381
+ return stepInterrupted;
382
+ }
383
+ function wasAnyStepInterrupted() {
384
+ return anyStepInterrupted;
385
+ }
386
+ function getInterruptSignal() {
387
+ return controller?.signal;
388
+ }
389
+ //#endregion
323
390
  //#region src/utils/terminal-output.ts
324
391
  const noopSpinner = {
325
392
  start() {},
326
393
  stop() {},
327
394
  message() {}
328
395
  };
396
+ const FRAME_MS = 80;
397
+ const HIDE_CURSOR = "\x1B[?25l";
398
+ const SHOW_CURSOR = "\x1B[?25h";
399
+ const CLEAR_LINE = "\r\x1B[2K";
400
+ let cursorHidden = false;
401
+ let restoreCursorOnExit = false;
402
+ function hideCursor() {
403
+ if (cursorHidden) return;
404
+ cursorHidden = true;
405
+ process.stdout.write(HIDE_CURSOR);
406
+ if (!restoreCursorOnExit) {
407
+ restoreCursorOnExit = true;
408
+ process.once("exit", () => {
409
+ if (cursorHidden) process.stdout.write(SHOW_CURSOR);
410
+ });
411
+ }
412
+ }
413
+ function showCursor() {
414
+ if (!cursorHidden) return;
415
+ cursorHidden = false;
416
+ process.stdout.write(SHOW_CURSOR);
417
+ }
418
+ /**
419
+ * The clack spinner puts stdin in raw mode and exits the process on Ctrl-C from its own
420
+ * keypress handler. This one never touches stdin, so Ctrl-C stays a SIGINT and the
421
+ * interrupt scope decides what happens.
422
+ */
423
+ function createTerminalSpinner() {
424
+ const out = process.stdout;
425
+ const animate = out.isTTY && !process.env.CI;
426
+ let text = "";
427
+ let active = false;
428
+ let interruptedBefore = false;
429
+ let frame = 0;
430
+ let dots = 0;
431
+ let timer;
432
+ const render = () => {
433
+ const suffix = ".".repeat(Math.floor(dots)).slice(0, 3);
434
+ out.write(`${CLEAR_LINE}${pc.magenta(SPINNER_FRAMES[frame])} ${text}${suffix}`);
435
+ frame = (frame + 1) % SPINNER_FRAMES.length;
436
+ dots = dots < 4 ? dots + .125 : 0;
437
+ };
438
+ const setText = (message) => {
439
+ text = message.replace(/\.+$/, "");
440
+ };
441
+ return {
442
+ start(message) {
443
+ if (active) return;
444
+ active = true;
445
+ interruptedBefore = wasInterrupted();
446
+ setText(message);
447
+ out.write(`${pc.gray(S_BAR)}\n`);
448
+ if (animate) {
449
+ hideCursor();
450
+ render();
451
+ timer = setInterval(render, FRAME_MS);
452
+ } else out.write(`${pc.magenta(SPINNER_FRAMES[0])} ${text}\n`);
453
+ },
454
+ message: setText,
455
+ stop(message) {
456
+ if (!active) return;
457
+ active = false;
458
+ if (timer) clearInterval(timer);
459
+ if (animate) {
460
+ out.write(CLEAR_LINE);
461
+ showCursor();
462
+ }
463
+ const cancelled = wasInterrupted() && !interruptedBefore;
464
+ out.write(cancelled ? `${pc.yellow(S_STEP_CANCEL)} ${text} (cancelled)\n` : `${pc.green(S_STEP_SUBMIT)} ${message || text}\n`);
465
+ }
466
+ };
467
+ }
329
468
  function createSpinner() {
330
- return isSilent() ? noopSpinner : spinner();
469
+ return isSilent() ? noopSpinner : createTerminalSpinner();
331
470
  }
332
471
  const baseConsola = createConsola({
333
472
  ...consola.options,
@@ -346,8 +485,9 @@ const cliLog = {
346
485
  success(message) {
347
486
  if (!isSilent()) log.success(message);
348
487
  },
488
+ /** Silent after a Ctrl-C in the current step: the cancelled line already said it. */
349
489
  error(message) {
350
- if (!isSilent()) log.error(message);
490
+ if (!isSilent() && !wasInterrupted()) log.error(message);
351
491
  },
352
492
  message(message) {
353
493
  if (!isSilent()) log.message(message);
@@ -355,7 +495,7 @@ const cliLog = {
355
495
  };
356
496
  const cliConsola = {
357
497
  error(message) {
358
- if (!isSilent()) baseConsola.error(message);
498
+ if (!isSilent() && !wasInterrupted()) baseConsola.error(message);
359
499
  },
360
500
  warn(message) {
361
501
  if (!isSilent()) baseConsola.warn(message);
@@ -381,6 +521,14 @@ var UserCancelledError = class extends TaggedError("UserCancelledError") {
381
521
  }
382
522
  };
383
523
  /**
524
+ * True for a direct cancellation or one that a setup step wrapped in a ProjectCreationError
525
+ * (for example cancelling a prompt inside addon or database setup).
526
+ */
527
+ function isUserCancellation(cause) {
528
+ if (UserCancelledError.is(cause)) return true;
529
+ return ProjectCreationError.is(cause) && UserCancelledError.is(cause.cause);
530
+ }
531
+ /**
384
532
  * General CLI error for validation failures, invalid flags, etc.
385
533
  */
386
534
  var CLIError = class extends TaggedError("CLIError") {};
@@ -923,7 +1071,7 @@ const WEB_FRAMEWORKS = [
923
1071
  ];
924
1072
  //#endregion
925
1073
  //#region src/utils/compatibility-rules.ts
926
- const TASK_RUNNER_ADDONS$1 = [
1074
+ const TASK_RUNNER_ADDONS = [
927
1075
  "turborepo",
928
1076
  "nx",
929
1077
  "vite-plus"
@@ -1170,13 +1318,13 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
1170
1318
  return allAddons.filter((addon) => {
1171
1319
  if (existingAddons.includes(addon)) return false;
1172
1320
  if (addon === "none") return false;
1173
- if (TASK_RUNNER_ADDONS$1.includes(addon) && existingAddons.some((existingAddon) => TASK_RUNNER_ADDONS$1.includes(existingAddon))) return false;
1321
+ if (TASK_RUNNER_ADDONS.includes(addon) && existingAddons.some((existingAddon) => TASK_RUNNER_ADDONS.includes(existingAddon))) return false;
1174
1322
  const { isCompatible } = validateAddonCompatibility(addon, frontend, auth, backend, runtime);
1175
1323
  return isCompatible;
1176
1324
  });
1177
1325
  }
1178
1326
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
1179
- if (addons.filter((addon) => TASK_RUNNER_ADDONS$1.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo', 'nx', and 'vite-plus' addons. Choose one task runner.");
1327
+ if (addons.filter((addon) => TASK_RUNNER_ADDONS.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo', 'nx', and 'vite-plus' addons. Choose one task runner.");
1180
1328
  for (const addon of addons) {
1181
1329
  if (addon === "none") continue;
1182
1330
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
@@ -1226,19 +1374,6 @@ function isGoBack(value) {
1226
1374
  }
1227
1375
  //#endregion
1228
1376
  //#region src/prompts/navigable.ts
1229
- const unicode = process.platform !== "win32";
1230
- const S_STEP_ACTIVE = unicode ? "◆" : "*";
1231
- const S_STEP_CANCEL = unicode ? "■" : "x";
1232
- const S_STEP_ERROR = unicode ? "▲" : "x";
1233
- const S_STEP_SUBMIT = unicode ? "◇" : "o";
1234
- const S_STEP_BACK = unicode ? "↶" : "<";
1235
- const S_BAR = unicode ? "│" : "|";
1236
- const S_BAR_END = unicode ? "└" : "—";
1237
- const S_RADIO_ACTIVE = unicode ? "●" : ">";
1238
- const S_RADIO_INACTIVE = unicode ? "○" : " ";
1239
- const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
1240
- const S_CHECKBOX_SELECTED = unicode ? "◼" : "[+]";
1241
- const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
1242
1377
  const promptsNavigatingBack = /* @__PURE__ */ new WeakSet();
1243
1378
  function keycap(label) {
1244
1379
  return pc.inverse(` ${label} `);
@@ -1430,28 +1565,46 @@ async function navigableConfirm(opts) {
1430
1565
  }
1431
1566
  }));
1432
1567
  }
1568
+ /** Keeps at most one value per exclusive set, preferring what was just selected. */
1569
+ function resolveExclusiveSelection(previous, next, exclusive) {
1570
+ let resolved = [...next];
1571
+ for (const set of exclusive) {
1572
+ const added = resolved.find((value) => set.includes(value) && !previous.includes(value));
1573
+ if (added === void 0) continue;
1574
+ resolved = resolved.filter((value) => !set.includes(value) || value === added);
1575
+ }
1576
+ return resolved;
1577
+ }
1433
1578
  async function navigableGroupMultiselect(opts) {
1434
1579
  const required = opts.required ?? true;
1580
+ const exclusive = opts.exclusive ?? [];
1581
+ const exclusiveValues = new Set(exclusive.flat());
1582
+ const exclusiveGroups = new Set(Object.entries(opts.options).filter(([, items]) => exclusive.some((set) => items.length > 1 && items.every((item) => set.includes(item.value)))).map(([group]) => group));
1435
1583
  const opt = (option, state, options = []) => {
1436
- const label = option.label ?? String(option.value);
1437
1584
  const isItem = option.group !== true && option.group !== false;
1585
+ const chooseOne = option.group === true && exclusiveGroups.has(String(option.value));
1586
+ const label = `${option.label ?? String(option.value)}${chooseOne ? pc.dim(" (choose one)") : ""}`;
1438
1587
  const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
1439
1588
  const isLast = isItem && next && next.group === true;
1440
1589
  const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
1441
- if (state === "active") return `${pc.dim(prefix)}${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1590
+ const radio = isItem && exclusiveValues.has(option.value);
1591
+ const activeBox = radio ? S_RADIO_INACTIVE : S_CHECKBOX_ACTIVE;
1592
+ const selectedBox = radio ? S_RADIO_ACTIVE : S_CHECKBOX_SELECTED;
1593
+ const inactiveBox = radio ? S_RADIO_INACTIVE : S_CHECKBOX_INACTIVE;
1594
+ if (state === "active") return `${pc.dim(prefix)}${pc.cyan(activeBox)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1442
1595
  if (state === "group-active") return `${prefix}${pc.cyan(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
1443
1596
  if (state === "group-active-selected") return `${prefix}${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}`;
1444
1597
  if (state === "selected") {
1445
- const selectedCheckbox = isItem ? pc.green(S_CHECKBOX_SELECTED) : "";
1598
+ const selectedCheckbox = isItem ? pc.green(selectedBox) : "";
1446
1599
  return `${pc.dim(prefix)}${selectedCheckbox} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1447
1600
  }
1448
1601
  if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
1449
- if (state === "active-selected") return `${pc.dim(prefix)}${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1602
+ if (state === "active-selected") return `${pc.dim(prefix)}${pc.green(selectedBox)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1450
1603
  if (state === "submitted") return `${pc.dim(label)}`;
1451
- const unselectedCheckbox = isItem ? pc.dim(S_CHECKBOX_INACTIVE) : "";
1604
+ const unselectedCheckbox = isItem ? pc.dim(inactiveBox) : "";
1452
1605
  return `${pc.dim(prefix)}${unselectedCheckbox} ${pc.dim(label)}`;
1453
1606
  };
1454
- return runWithNavigation(new GroupMultiSelectPrompt({
1607
+ const prompt = new GroupMultiSelectPrompt({
1455
1608
  options: opts.options,
1456
1609
  initialValues: opts.initialValues,
1457
1610
  cursorAt: opts.cursorAt,
@@ -1510,7 +1663,15 @@ async function navigableGroupMultiselect(opts) {
1510
1663
  }
1511
1664
  }
1512
1665
  }
1513
- }));
1666
+ });
1667
+ if (exclusive.length > 0) {
1668
+ let previous = [...opts.initialValues ?? []];
1669
+ prompt.on("cursor", (key) => {
1670
+ if (key === "space") prompt.value = resolveExclusiveSelection(previous, prompt.value ?? [], exclusive);
1671
+ previous = [...prompt.value ?? []];
1672
+ });
1673
+ }
1674
+ return runWithNavigation(prompt);
1514
1675
  }
1515
1676
  /** Use the remembered answer as the initial value only while it is still selectable. */
1516
1677
  function preferValidInitial(options, previous, fallback) {
@@ -1649,11 +1810,7 @@ function sortAndPruneGroupedOptions(groupedOptions) {
1649
1810
  });
1650
1811
  }
1651
1812
  function validateAddonSelection(selected) {
1652
- if ((selected?.filter((addon) => [
1653
- "turborepo",
1654
- "nx",
1655
- "vite-plus"
1656
- ].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
1813
+ if ((selected?.filter((addon) => TASK_RUNNER_ADDONS.includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
1657
1814
  }
1658
1815
  async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
1659
1816
  if (addons !== void 0) return addons;
@@ -1676,6 +1833,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime, previo
1676
1833
  options: groupedOptions,
1677
1834
  initialValues: (previousValue ?? DEFAULT_CONFIG.addons).filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
1678
1835
  required: false,
1836
+ exclusive: [TASK_RUNNER_ADDONS],
1679
1837
  validate: validateAddonSelection
1680
1838
  });
1681
1839
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -1699,6 +1857,7 @@ async function getAddonsToAdd(config) {
1699
1857
  message: "Select addons to add",
1700
1858
  options: groupedOptions,
1701
1859
  required: false,
1860
+ exclusive: [TASK_RUNNER_ADDONS],
1702
1861
  validate: validateAddonSelection
1703
1862
  });
1704
1863
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -1782,7 +1941,13 @@ function failureStage(cause) {
1782
1941
  * project replaced by placeholders: quoted names, URLs, emails, and paths.
1783
1942
  */
1784
1943
  function scrubReason(cause) {
1785
- const scrubbed = ((cause instanceof Error ? cause.message : String(cause)).split(/\r?\n/, 1)[0] ?? "").replaceAll(/(["'`])(?:(?!\1).)*\1/g, "<name>").replaceAll(/https?:\/\/[^\s)\]"'>]+/gi, "<url>").replaceAll(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "<email>").replaceAll(/(?:[a-zA-Z]:)?(?:[\\/][\w.@+ -]+)+[\\/][\w.@+-]+(?:[ \w.@+-]*\.\w+)?/g, "<path>").replaceAll(/\S*[\\/]\S*/g, "<path>").replaceAll(/\s+/g, " ").trim();
1944
+ const lines = (cause instanceof Error ? cause.message : String(cause)).split(/\r?\n/);
1945
+ const bullets = [];
1946
+ for (const line of lines.slice(1)) {
1947
+ if (!line.startsWith("- ")) break;
1948
+ bullets.push(line.slice(2));
1949
+ }
1950
+ const scrubbed = [lines[0] ?? "", ...bullets].join(" ").replaceAll(/(?<![\p{L}\p{N}\p{M}_])(["'`])(?:(?!\1).)*\1(?![\p{L}\p{N}\p{M}_])/gu, "<name>").replaceAll(/https?:\/\/[^\s)\]"'>]+/gi, "<url>").replaceAll(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "<email>").replaceAll(/(?:[a-zA-Z]:)?(?:[\\/][\w.@+ -]+)+[\\/][\w.@+-]+(?:[ \w.@+-]*\.\w+)?/g, "<path>").replaceAll(/\S*[\\/]\S*/g, "<path>").replaceAll(/\s+/g, " ").trim();
1786
1951
  return scrubbed.length > MAX_REASON_LENGTH ? scrubbed.slice(0, MAX_REASON_LENGTH) : scrubbed;
1787
1952
  }
1788
1953
  function clampValue(value) {
@@ -1925,13 +2090,22 @@ function getNodeToolingRequirements(config) {
1925
2090
  if (config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") addNodeRequirement(requirements, ">=22.0.0", "Wrangler 4");
1926
2091
  return requirements;
1927
2092
  }
1928
- function getLocalVersionRequirements(config, hostRuntime) {
1929
- const requirements = [{
1930
- tool: config.packageManager,
1931
- range: PACKAGE_MANAGER_VERSION_RANGES[config.packageManager],
1932
- reason: PACKAGE_MANAGER_REASONS[config.packageManager]
1933
- }];
2093
+ function getHostRuntime() {
2094
+ return process.versions.bun ? "bun" : "node";
2095
+ }
2096
+ /** Requirements that do not depend on the chosen stack, so they can run before any prompt. */
2097
+ function getBaselineRequirements(packageManager, hostRuntime) {
2098
+ const requirements = [];
2099
+ if (packageManager) requirements.push({
2100
+ tool: packageManager,
2101
+ range: PACKAGE_MANAGER_VERSION_RANGES[packageManager],
2102
+ reason: PACKAGE_MANAGER_REASONS[packageManager]
2103
+ });
1934
2104
  if (hostRuntime === "node") addNodeRequirement(requirements, ">=22.0.0", "create-better-t-stack");
2105
+ return requirements;
2106
+ }
2107
+ function getLocalVersionRequirements(config, hostRuntime) {
2108
+ const requirements = getBaselineRequirements(config.packageManager, hostRuntime);
1935
2109
  if (config.packageManager !== "bun") requirements.push(...getNodeToolingRequirements(config));
1936
2110
  else if (config.runtime === "node") addNodeRequirement(requirements, ">=22.0.0", "the selected Node.js server runtime");
1937
2111
  return requirements;
@@ -1945,7 +2119,11 @@ function formatRequirementError(requirement, rawVersion) {
1945
2119
  return null;
1946
2120
  }
1947
2121
  function validateLocalToolVersions(config, versions, hostRuntime) {
1948
- const requirements = getLocalVersionRequirements(config, hostRuntime);
2122
+ return validateRequirements(getLocalVersionRequirements(config, hostRuntime), versions);
2123
+ }
2124
+ const STACK_REQUIREMENTS_HEADLINE = "Your local toolchain does not meet this stack's requirements:";
2125
+ const BASELINE_REQUIREMENTS_HEADLINE = "Your local toolchain does not meet create-better-t-stack's requirements:";
2126
+ function validateRequirements(requirements, versions, headline = STACK_REQUIREMENTS_HEADLINE) {
1949
2127
  const failures = requirements.map((requirement) => formatRequirementError(requirement, versions[requirement.tool])).filter((failure) => failure !== null);
1950
2128
  if (failures.length === 0) return Result.ok(void 0);
1951
2129
  const upgradeInstructions = /* @__PURE__ */ new Set();
@@ -1955,7 +2133,7 @@ function validateLocalToolVersions(config, versions, hostRuntime) {
1955
2133
  else upgradeInstructions.add(PACKAGE_MANAGER_UPGRADE_INSTRUCTIONS[requirement.tool]);
1956
2134
  }
1957
2135
  return Result.err(new CLIError({ message: [
1958
- "Your local toolchain does not meet this stack's requirements:",
2136
+ headline,
1959
2137
  ...failures.map((failure) => `- ${failure}`),
1960
2138
  "",
1961
2139
  ...upgradeInstructions
@@ -1982,12 +2160,35 @@ async function readToolVersion(tool) {
1982
2160
  });
1983
2161
  return result.isOk() ? result.value : null;
1984
2162
  }
1985
- async function checkLocalRequirements(config) {
1986
- const hostRuntime = process.versions.bun ? "bun" : "node";
2163
+ async function readLocalToolVersions(packageManager, hostRuntime, needsNode) {
1987
2164
  const versions = {};
1988
- const packageManagerVersion = await readToolVersion(config.packageManager);
1989
- if (packageManagerVersion) versions[config.packageManager] = packageManagerVersion;
1990
- if (getLocalVersionRequirements(config, hostRuntime).some((requirement) => requirement.tool === "node")) versions.node = hostRuntime === "node" ? process.versions.node : await readToolVersion("node") ?? void 0;
2165
+ if (packageManager) {
2166
+ const packageManagerVersion = await readToolVersion(packageManager);
2167
+ if (packageManagerVersion) versions[packageManager] = packageManagerVersion;
2168
+ }
2169
+ if (needsNode) versions.node = hostRuntime === "node" ? process.versions.node : await readToolVersion("node") ?? void 0;
2170
+ return versions;
2171
+ }
2172
+ /**
2173
+ * Pre-prompt check. An inferred package manager can still be changed at its prompt, so it only
2174
+ * warns; an explicit one and the host Node.js version are fatal.
2175
+ */
2176
+ async function checkBaselineRequirements(packageManager, packageManagerIsExplicit) {
2177
+ if (shouldSkipExternalCommands()) return Result.ok({ warnings: [] });
2178
+ const hostRuntime = getHostRuntime();
2179
+ const requirements = getBaselineRequirements(packageManager, hostRuntime);
2180
+ const versions = await readLocalToolVersions(packageManager, hostRuntime, requirements.some((requirement) => requirement.tool === "node"));
2181
+ const fatal = requirements.filter((requirement) => requirement.tool === "node" || packageManagerIsExplicit);
2182
+ const fatalResult = validateRequirements(fatal, versions, BASELINE_REQUIREMENTS_HEADLINE);
2183
+ if (fatalResult.isErr()) return Result.err(fatalResult.error);
2184
+ const advisoryResult = validateRequirements(requirements.filter((requirement) => !fatal.includes(requirement)), versions, BASELINE_REQUIREMENTS_HEADLINE);
2185
+ if (advisoryResult.isErr()) return Result.ok({ warnings: [`${advisoryResult.error.message}\nYou can also choose a different package manager at the package manager step.`] });
2186
+ return Result.ok({ warnings: [] });
2187
+ }
2188
+ async function checkLocalRequirements(config) {
2189
+ const hostRuntime = getHostRuntime();
2190
+ const versions = await readLocalToolVersions(config.packageManager, hostRuntime, !shouldSkipExternalCommands() && getLocalVersionRequirements(config, hostRuntime).some((requirement) => requirement.tool === "node"));
2191
+ const packageManagerVersion = versions[config.packageManager];
1991
2192
  if (!shouldSkipExternalCommands()) {
1992
2193
  const validationResult = validateLocalToolVersions(config, versions, hostRuntime);
1993
2194
  if (validationResult.isErr()) return Result.err(validationResult.error);
@@ -2020,6 +2221,22 @@ const addPackageDependency = async (opts) => {
2020
2221
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
2021
2222
  };
2022
2223
  //#endregion
2224
+ //#region src/utils/optional-step.ts
2225
+ /**
2226
+ * Runs a setup step after the files are on disk. A failure is reported and the run continues;
2227
+ * a cancellation (prompt or Ctrl-C) is only a warning.
2228
+ */
2229
+ async function runOptionalStep(step, cancelledMessage) {
2230
+ startInterruptibleStep();
2231
+ const result = await step();
2232
+ if (result.isOk()) return;
2233
+ if (UserCancelledError.is(result.error) || wasInterrupted()) {
2234
+ cliLog.warn(pc.yellow(cancelledMessage));
2235
+ return;
2236
+ }
2237
+ cliConsola.error(pc.red(result.error.message));
2238
+ }
2239
+ //#endregion
2023
2240
  //#region src/helpers/addons/evlog-setup.ts
2024
2241
  const evlogBackends = [
2025
2242
  "hono",
@@ -2857,7 +3074,7 @@ const TEMPLATES$2 = {
2857
3074
  const DEFAULT_TEMPLATE$2 = "next-mdx";
2858
3075
  const DEFAULT_DEV_PORT$1 = 4e3;
2859
3076
  function aiChatDisabledForTemplate(template) {
2860
- return template === "next-mdx-static" || template.endsWith("-spa");
3077
+ return template === "next-mdx-static" || template === "astro" || template.endsWith("-spa");
2861
3078
  }
2862
3079
  function getFumadocsLinter(addons) {
2863
3080
  if (addons.includes("oxlint")) return "oxlint";
@@ -3101,6 +3318,26 @@ const MCP_AGENTS = [
3101
3318
  value: "goose",
3102
3319
  label: "Goose",
3103
3320
  scope: "global"
3321
+ },
3322
+ {
3323
+ value: "fx",
3324
+ label: "fx",
3325
+ scope: "global"
3326
+ },
3327
+ {
3328
+ value: "kilo-code",
3329
+ label: "Kilo Code",
3330
+ scope: "both"
3331
+ },
3332
+ {
3333
+ value: "kimi-code",
3334
+ label: "Kimi Code",
3335
+ scope: "both"
3336
+ },
3337
+ {
3338
+ value: "kiro-cli",
3339
+ label: "Kiro CLI",
3340
+ scope: "both"
3104
3341
  }
3105
3342
  ];
3106
3343
  const DEFAULT_SCOPE$1 = "project";
@@ -3142,8 +3379,7 @@ function getAllMcpServers(config) {
3142
3379
  key: "cloudflare-docs",
3143
3380
  label: "Cloudflare Docs",
3144
3381
  name: "cloudflare-docs",
3145
- target: "https://docs.mcp.cloudflare.com/sse",
3146
- transport: "sse"
3382
+ target: "https://docs.mcp.cloudflare.com/mcp"
3147
3383
  },
3148
3384
  {
3149
3385
  key: "convex",
@@ -3209,7 +3445,7 @@ function getAllMcpServers(config) {
3209
3445
  key: "better-auth",
3210
3446
  label: "Better Auth",
3211
3447
  name: "better-auth",
3212
- target: "https://mcp.inkeep.com/better-auth/mcp"
3448
+ target: "https://mcp.better-auth.com/mcp"
3213
3449
  },
3214
3450
  {
3215
3451
  key: "clerk",
@@ -3443,7 +3679,7 @@ const SKILL_SOURCES = {
3443
3679
  "vercel/ai": { label: "Vercel AI SDK" },
3444
3680
  "vercel/turborepo": { label: "Turborepo" },
3445
3681
  "yusukebe/hono-skill": { label: "Hono Backend" },
3446
- "vercel-labs/next-skills": { label: "Next.js Best Practices" },
3682
+ "vercel/next.js": { label: "Next.js" },
3447
3683
  "nuxt/ui": { label: "Nuxt UI" },
3448
3684
  "heroui-inc/heroui": { label: "HeroUI Native" },
3449
3685
  "shadcn/ui": { label: "shadcn/ui" },
@@ -3641,6 +3877,10 @@ const SKILLS_CLI_AGENT_OPTIONS = [
3641
3877
  value: "mcpjam",
3642
3878
  label: "MCPJam"
3643
3879
  },
3880
+ {
3881
+ value: "minimax-code",
3882
+ label: "MiniMax Code"
3883
+ },
3644
3884
  {
3645
3885
  value: "mistral-vibe",
3646
3886
  label: "Mistral Vibe"
@@ -3681,6 +3921,10 @@ const SKILLS_CLI_AGENT_OPTIONS = [
3681
3921
  value: "pochi",
3682
3922
  label: "Pochi"
3683
3923
  },
3924
+ {
3925
+ value: "posit-assistant",
3926
+ label: "Posit Assistant"
3927
+ },
3684
3928
  {
3685
3929
  value: "promptscript",
3686
3930
  label: "PromptScript"
@@ -3820,7 +4064,7 @@ function getRecommendedSourceKeys(config) {
3820
4064
  sources.push("shadcn/ui");
3821
4065
  }
3822
4066
  if ((webDeploy === "vercel" || serverDeploy === "vercel") && !sources.includes("vercel-labs/agent-skills")) sources.push("vercel-labs/agent-skills");
3823
- if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
4067
+ if (frontend.includes("next")) sources.push("vercel/next.js");
3824
4068
  if (frontend.includes("nuxt")) sources.push("nuxt/ui");
3825
4069
  if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
3826
4070
  if (hasNativeFrontend(frontend)) sources.push("expo/skills");
@@ -3851,11 +4095,20 @@ const CURATED_SKILLS_BY_SOURCE = {
3851
4095
  "vercel/ai": () => ["ai-sdk"],
3852
4096
  "vercel/turborepo": () => ["turborepo"],
3853
4097
  "yusukebe/hono-skill": () => ["hono"],
3854
- "vercel-labs/next-skills": () => ["next-best-practices", "next-cache-components"],
4098
+ "vercel/next.js": () => [
4099
+ "next-dev-loop",
4100
+ "next-cache-components-adoption",
4101
+ "next-cache-components-optimizer",
4102
+ "next-partial-prefetching-adoption"
4103
+ ],
3855
4104
  "nuxt/ui": () => ["nuxt-ui"],
3856
4105
  "heroui-inc/heroui": () => ["heroui-native"],
3857
4106
  "shadcn/ui": () => ["shadcn"],
3858
- "better-auth/skills": () => ["better-auth-best-practices"],
4107
+ "better-auth/skills": () => [
4108
+ "better-auth-best-practices",
4109
+ "better-auth-security-best-practices",
4110
+ "email-and-password-best-practices"
4111
+ ],
3859
4112
  "clerk/skills": (config) => {
3860
4113
  const skills = [
3861
4114
  "clerk",
@@ -3866,6 +4119,12 @@ const CURATED_SKILLS_BY_SOURCE = {
3866
4119
  "clerk-orgs"
3867
4120
  ];
3868
4121
  if (config.frontend.includes("next")) skills.push("clerk-nextjs-patterns");
4122
+ if (config.frontend.includes("react-router")) skills.push("clerk-react-router-patterns");
4123
+ if (config.frontend.includes("tanstack-start")) skills.push("clerk-tanstack-patterns");
4124
+ if (config.frontend.includes("tanstack-router")) skills.push("clerk-react-patterns");
4125
+ if (config.frontend.includes("nuxt")) skills.push("clerk-nuxt-patterns");
4126
+ if (config.frontend.includes("astro")) skills.push("clerk-astro-patterns");
4127
+ if (hasNativeFrontend(config.frontend)) skills.push("clerk-expo");
3869
4128
  return skills;
3870
4129
  },
3871
4130
  "neondatabase/agent-skills": () => ["neon-postgres"],
@@ -3878,11 +4137,13 @@ const CURATED_SKILLS_BY_SOURCE = {
3878
4137
  },
3879
4138
  "expo/skills": (config) => {
3880
4139
  const skills = [
4140
+ "expo-overview",
4141
+ "expo-router",
3881
4142
  "expo-dev-client",
3882
- "building-native-ui",
3883
- "native-data-fetching",
3884
- "expo-deployment",
3885
- "expo-cicd-workflows"
4143
+ "expo-native-ui",
4144
+ "expo-data-fetching",
4145
+ "eas-app-stores",
4146
+ "eas-workflows"
3886
4147
  ];
3887
4148
  if (config.frontend.includes("native-uniwind")) skills.push("expo-tailwind-setup");
3888
4149
  return skills;
@@ -4054,7 +4315,8 @@ async function setupStarlight(config) {
4054
4315
  "--yes",
4055
4316
  "--no-install",
4056
4317
  "--no-git",
4057
- "--skip-houston"
4318
+ "--skip-houston",
4319
+ "--no-ai"
4058
4320
  ].join(" ")}`);
4059
4321
  const appsDir = path.join(projectDir, "apps");
4060
4322
  await fs.ensureDir(appsDir);
@@ -4335,7 +4597,7 @@ const HOOKS = {
4335
4597
  claude: { label: "Claude Code" },
4336
4598
  copilot: { label: "GitHub Copilot" }
4337
4599
  };
4338
- const ULTRACITE_VERSION = "7.9.4";
4600
+ const ULTRACITE_VERSION = "7.10.7";
4339
4601
  const DEFAULT_LINTER = "biome";
4340
4602
  const DEFAULT_EDITORS = ["vscode"];
4341
4603
  const DEFAULT_AGENTS = ["universal"];
@@ -4418,13 +4680,20 @@ async function setupUltracite(config, gitHooks) {
4418
4680
  return navigableMultiselect({
4419
4681
  message: "Which editors do you want to configure (recommended)?",
4420
4682
  required: false,
4421
- options: [{
4422
- value: "vscode",
4423
- label: "VSCode / Cursor / Windsurf"
4424
- }, {
4425
- value: "zed",
4426
- label: "Zed"
4427
- }]
4683
+ options: [
4684
+ {
4685
+ value: "vscode",
4686
+ label: "VSCode / Cursor / Windsurf"
4687
+ },
4688
+ {
4689
+ value: "zed",
4690
+ label: "Zed"
4691
+ },
4692
+ {
4693
+ value: "universal",
4694
+ label: "Universal (.vscode/settings.json)"
4695
+ }
4696
+ ]
4428
4697
  });
4429
4698
  },
4430
4699
  agents: async () => {
@@ -4598,24 +4867,15 @@ async function setupWxt(config) {
4598
4867
  }
4599
4868
  //#endregion
4600
4869
  //#region src/helpers/addons/addons-setup.ts
4601
- async function runSetup(setupFn) {
4602
- const result = await setupFn();
4603
- if (result.isErr()) {
4604
- if (UserCancelledError.is(result.error)) throw result.error;
4605
- cliConsola.error(pc.red(result.error.message));
4606
- }
4607
- }
4608
- async function runAddonStep(addon, step) {
4609
- const result = await Result.tryPromise({
4610
- try: async () => step(),
4611
- catch: (e) => new AddonSetupError({
4612
- addon,
4613
- message: `Failed to set up ${addon}: ${e instanceof Error ? e.message : String(e)}`,
4614
- cause: e
4615
- })
4616
- });
4617
- if (result.isErr()) cliConsola.error(pc.red(result.error.message));
4618
- }
4870
+ const runSetup = (setupFn) => runOptionalStep(setupFn, "Addon setup cancelled.");
4871
+ const runAddonStep = (addon, step) => runOptionalStep(() => Result.tryPromise({
4872
+ try: async () => step(),
4873
+ catch: (e) => new AddonSetupError({
4874
+ addon,
4875
+ message: `Failed to set up ${addon}: ${e instanceof Error ? e.message : String(e)}`,
4876
+ cause: e
4877
+ })
4878
+ }), `${addon} setup cancelled.`);
4619
4879
  async function setupAddons(config) {
4620
4880
  const { addons, frontend, projectDir } = config;
4621
4881
  const hasWebFrontend = frontend.some((value) => types_exports.desktopWebFrontends.includes(value));
@@ -4725,15 +4985,19 @@ async function detectProjectConfig(projectDir) {
4725
4985
  }
4726
4986
  //#endregion
4727
4987
  //#region src/helpers/core/install-dependencies.ts
4988
+ const FORCE_KILL_AFTER_MS = 2e3;
4728
4989
  async function installDependencies({ projectDir, packageManager }) {
4729
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
4990
+ if (shouldSkipExternalCommands()) return Result.ok("installed");
4991
+ startInterruptibleStep();
4730
4992
  const s = createSpinner();
4731
4993
  s.start(`Running ${packageManager} install...`);
4732
4994
  const result = await Result.tryPromise({
4733
4995
  try: async () => {
4734
4996
  await $({
4735
4997
  cwd: projectDir,
4736
- stderr: "inherit"
4998
+ stderr: "inherit",
4999
+ cancelSignal: getInterruptSignal(),
5000
+ forceKillAfterDelay: FORCE_KILL_AFTER_MS
4737
5001
  })`${packageManager} install`;
4738
5002
  },
4739
5003
  catch: (e) => new ProjectCreationError({
@@ -4742,9 +5006,16 @@ async function installDependencies({ projectDir, packageManager }) {
4742
5006
  cause: e
4743
5007
  })
4744
5008
  });
4745
- if (result.isOk()) s.stop("Dependencies installed");
4746
- else s.stop(pc.red("Failed to install dependencies"));
4747
- return result;
5009
+ if (wasInterrupted()) {
5010
+ s.stop();
5011
+ return Result.ok("cancelled");
5012
+ }
5013
+ if (result.isOk()) {
5014
+ s.stop("Dependencies installed");
5015
+ return Result.ok("installed");
5016
+ }
5017
+ s.stop(pc.red("Failed to install dependencies"));
5018
+ return Result.err(result.error);
4748
5019
  }
4749
5020
  //#endregion
4750
5021
  //#region src/helpers/core/add-handler.ts
@@ -4772,11 +5043,6 @@ const HOOK_LINTER_ADDONS = [
4772
5043
  "oxlint",
4773
5044
  "vite-plus"
4774
5045
  ];
4775
- const TASK_RUNNER_ADDONS = [
4776
- "turborepo",
4777
- "nx",
4778
- "vite-plus"
4779
- ];
4780
5046
  const fileExistsErrorSchema = z.object({ code: z.literal("EEXIST") });
4781
5047
  const configPackageScopeSchema = z.object({ name: z.string().regex(/^@[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\/config$/) }).transform(({ name }) => name.slice(0, -7));
4782
5048
  const rootTypescriptVersionSchema = z.object({ devDependencies: z.object({ typescript: z.string().min(1) }) }).transform(({ devDependencies }) => devDependencies.typescript);
@@ -4889,7 +5155,12 @@ async function addHandler(input, options = {}) {
4889
5155
  mode,
4890
5156
  analyticsDisabled: input.disableAnalytics
4891
5157
  }, async () => {
4892
- const result = await addHandlerInternal(input);
5158
+ let result;
5159
+ try {
5160
+ result = await addHandlerInternal(input);
5161
+ } finally {
5162
+ endInterruptibleScope();
5163
+ }
4893
5164
  await reportAddOutcome(input, result);
4894
5165
  if (result.isOk()) return result.value;
4895
5166
  const error = result.error;
@@ -5092,6 +5363,7 @@ async function addHandlerInternal(input) {
5092
5363
  return Result.err(new CLIError({ message: `Failed to write files: ${writeResult.error.message}` }));
5093
5364
  }
5094
5365
  if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} files`));
5366
+ beginInterruptibleScope();
5095
5367
  const setupResult = await Result.tryPromise({
5096
5368
  try: async () => {
5097
5369
  if (addonsToAdd.length > 0) await setupAddons({
@@ -6825,7 +7097,9 @@ const PROJECT_LAUNCHERS = [
6825
7097
  }]
6826
7098
  }
6827
7099
  ];
6828
- async function detectProjectLaunchers(projectDir, detectCommand = commandExists, platform = process.platform) {
7100
+ async function detectProjectLaunchers(projectDir, options = {}) {
7101
+ const detectCommand = options.detectCommand ?? commandExists;
7102
+ const platform = options.platform ?? process.platform;
6829
7103
  const supportedDefinitions = PROJECT_LAUNCHERS.filter(({ platforms }) => platforms === void 0 || platforms.includes(platform));
6830
7104
  const commandsFor = (launcher) => launcher.commandsByPlatform?.[platform] ?? launcher.commands;
6831
7105
  const commands = [...new Set(supportedDefinitions.flatMap(commandsFor))];
@@ -6848,7 +7122,8 @@ async function detectProjectLaunchers(projectDir, detectCommand = commandExists,
6848
7122
  async function getProjectLauncherChoice(requestedLauncher, projectDir, options = {}) {
6849
7123
  if (requestedLauncher === "none") return Result.ok(void 0);
6850
7124
  if (!requestedLauncher && options.prompt === false) return Result.ok(void 0);
6851
- const availableLaunchers = await detectProjectLaunchers(projectDir, options.detectCommand, options.platform);
7125
+ const { prompt: _prompt, ...detectOptions } = options;
7126
+ const availableLaunchers = await detectProjectLaunchers(projectDir, detectOptions);
6852
7127
  if (requestedLauncher) {
6853
7128
  const launcher = availableLaunchers.find(({ id }) => id === requestedLauncher);
6854
7129
  if (launcher) return Result.ok(launcher);
@@ -7136,12 +7411,14 @@ async function formatCode(filePath, content) {
7136
7411
  });
7137
7412
  return result.isOk() ? result.value : null;
7138
7413
  }
7139
- async function formatProject(projectDir) {
7414
+ async function formatProject(projectDir, signal) {
7140
7415
  return Result.tryPromise({
7141
7416
  try: async () => {
7142
7417
  async function formatDirectory(dir) {
7418
+ if (signal?.aborted) return;
7143
7419
  const entries = await fs.readdir(dir, { withFileTypes: true });
7144
7420
  await Promise.all(entries.map(async (entry) => {
7421
+ if (signal?.aborted) return;
7145
7422
  const fullPath = path.join(dir, entry.name);
7146
7423
  if (entry.isDirectory()) await formatDirectory(fullPath);
7147
7424
  else if (entry.isFile()) await Result.tryPromise({
@@ -7943,8 +8220,19 @@ async function writeSupabaseEnvFile(projectDir, backend, databaseUrl) {
7943
8220
  })
7944
8221
  });
7945
8222
  }
7946
- function extractDbUrl(output) {
7947
- return output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/)?.[1] ?? null;
8223
+ function extractDbUrl(statusEnv) {
8224
+ return statusEnv.match(/^DB_URL="(postgresql:\/\/[^"]+)"$/m)?.[1] ?? null;
8225
+ }
8226
+ async function readSupabaseStatus(serverDir, packageManager) {
8227
+ const statusArgs = getPackageExecutionArgs(packageManager, "supabase status --agent no -o env");
8228
+ const result = await Result.tryPromise({
8229
+ try: async () => {
8230
+ const { stdout } = await execa(statusArgs[0], statusArgs.slice(1), { cwd: serverDir });
8231
+ return stdout;
8232
+ },
8233
+ catch: () => ""
8234
+ });
8235
+ return result.isOk() ? result.value : "";
7948
8236
  }
7949
8237
  async function initializeSupabase(serverDir, packageManager) {
7950
8238
  cliLog.info("Initializing Supabase project...");
@@ -7969,7 +8257,7 @@ async function initializeSupabase(serverDir, packageManager) {
7969
8257
  }
7970
8258
  async function startSupabase(serverDir, packageManager) {
7971
8259
  cliLog.info("Starting Supabase services (this may take a moment)...");
7972
- const supabaseStartArgs = getPackageExecutionArgs(packageManager, "supabase start");
8260
+ const supabaseStartArgs = getPackageExecutionArgs(packageManager, "supabase start --agent no");
7973
8261
  return Result.tryPromise({
7974
8262
  try: async () => {
7975
8263
  const subprocess = execa(supabaseStartArgs[0], supabaseStartArgs.slice(1), { cwd: serverDir });
@@ -8000,10 +8288,10 @@ function displayManualSupabaseInstructions(targetApp, output) {
8000
8288
  2. Install the Supabase CLI (e.g., \`npm install -g supabase\`).
8001
8289
  3. Run \`supabase init\` in your project's \`packages/db\` directory.
8002
8290
  4. Run \`supabase start\` in your project's \`packages/db\` directory.
8003
- 5. Copy the 'DB URL' from the output.${output ? `
8291
+ 5. Copy the database URL from the output.${output ? `
8004
8292
  ${pc.bold("Relevant output from `supabase start`:")}
8005
8293
  ${pc.dim(output)}` : ""}
8006
- 6. Add the DB URL to the .env file in \`${targetApp}/.env\` as \`DATABASE_URL\`:
8294
+ 6. Add the database URL to the .env file in \`${targetApp}/.env\` as \`DATABASE_URL\`:
8007
8295
  ${pc.gray("DATABASE_URL=\"your_supabase_db_url\"")}`);
8008
8296
  }
8009
8297
  async function setupSupabase(config, cliInput) {
@@ -8063,7 +8351,7 @@ async function setupSupabase(config, cliInput) {
8063
8351
  return writeSupabaseEnvFile(projectDir, backend, "");
8064
8352
  }
8065
8353
  const supabaseOutput = startResult.value;
8066
- const dbUrl = extractDbUrl(supabaseOutput);
8354
+ const dbUrl = extractDbUrl(await readSupabaseStatus(serverDir, packageManager));
8067
8355
  if (dbUrl) {
8068
8356
  const envResult = await writeSupabaseEnvFile(projectDir, backend, dbUrl);
8069
8357
  if (envResult.isOk()) cliLog.success(pc.green("Supabase local development setup ready!"));
@@ -8192,7 +8480,7 @@ async function createTursoDatabase(dbName, groupName) {
8192
8480
  catch: (e) => {
8193
8481
  const error = e;
8194
8482
  s.stop(pc.red(`Failed to create database "${dbName}"`));
8195
- if (error.message?.includes("already exists")) return new DatabaseSetupError({
8483
+ if (error.message?.includes("already exists") || error.message?.includes("is not available")) return new DatabaseSetupError({
8196
8484
  provider: "turso",
8197
8485
  message: "DATABASE_EXISTS",
8198
8486
  cause: e
@@ -8423,13 +8711,7 @@ async function setupDatabase(config, cliInput) {
8423
8711
  const dbPackageDir = path.join(projectDir, "packages/db");
8424
8712
  if (!await fs.pathExists(dbPackageDir)) return;
8425
8713
  if (usesAlchemyManagedDatabase(config)) return;
8426
- async function runSetup(setupFn) {
8427
- const result = await setupFn();
8428
- if (result.isErr()) {
8429
- if (UserCancelledError.is(result.error)) throw result.error;
8430
- cliConsola.error(pc.red(result.error.message));
8431
- }
8432
- }
8714
+ const runSetup = (setupFn) => runOptionalStep(setupFn, "Database setup cancelled. Configure the connection manually.");
8433
8715
  const resolvedCliInput = {
8434
8716
  ...cliInput,
8435
8717
  dbSetupOptions: mergeResolvedDbSetupOptions(dbSetup, config.dbSetupOptions, cliInput)
@@ -8447,29 +8729,44 @@ async function setupDatabase(config, cliInput) {
8447
8729
  }
8448
8730
  //#endregion
8449
8731
  //#region src/helpers/core/git.ts
8450
- async function initializeGit(projectDir, useGit) {
8732
+ async function initializeGit(projectDir, useGit, signal) {
8451
8733
  if (!useGit) return Result.ok(void 0);
8452
- if ((await $({
8734
+ const cancelled = () => Result.err(new ProjectCreationError({
8735
+ phase: "git-initialization",
8736
+ message: "Git initialization cancelled"
8737
+ }));
8738
+ const gitVersionResult = await $({
8453
8739
  cwd: projectDir,
8454
8740
  reject: false,
8455
- stderr: "pipe"
8456
- })`git --version`).exitCode !== 0) {
8741
+ stderr: "pipe",
8742
+ cancelSignal: signal
8743
+ })`git --version`;
8744
+ if (signal?.aborted) return cancelled();
8745
+ if (gitVersionResult.exitCode !== 0) {
8457
8746
  cliLog.warn(pc.yellow("Git is not installed"));
8458
8747
  return Result.ok(void 0);
8459
8748
  }
8460
8749
  const result = await $({
8461
8750
  cwd: projectDir,
8462
8751
  reject: false,
8463
- stderr: "pipe"
8752
+ stderr: "pipe",
8753
+ cancelSignal: signal
8464
8754
  })`git init`;
8755
+ if (signal?.aborted) return cancelled();
8465
8756
  if (result.exitCode !== 0) return Result.err(new ProjectCreationError({
8466
8757
  phase: "git-initialization",
8467
8758
  message: `Git initialization failed: ${result.stderr}`
8468
8759
  }));
8469
8760
  return Result.tryPromise({
8470
8761
  try: async () => {
8471
- await $({ cwd: projectDir })`git add -A`;
8472
- await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
8762
+ await $({
8763
+ cwd: projectDir,
8764
+ cancelSignal: signal
8765
+ })`git add -A`;
8766
+ await $({
8767
+ cwd: projectDir,
8768
+ cancelSignal: signal
8769
+ })`git commit -m ${"initial commit"}`;
8473
8770
  },
8474
8771
  catch: (e) => new ProjectCreationError({
8475
8772
  phase: "git-initialization",
@@ -8777,7 +9074,7 @@ function getTauriInstructions(runCmd, frontend) {
8777
9074
  }
8778
9075
  function getElectrobunInstructions(runCmd, frontend) {
8779
9076
  const staticBuildNote = getDesktopStaticBuildNote(frontend);
8780
- return `\n${pc.bold("Desktop app with Electrobun:")}\n${pc.cyan("•")} Start desktop app with HMR: ${`${runCmd} dev:desktop`}\n${pc.cyan("•")} Build stable desktop app (DMG/App): ${`${runCmd} build:desktop`}\n${pc.cyan("•")} Build canary desktop app: ${`${runCmd} build:desktop:canary`}\n${pc.yellow("NOTE:")} Electrobun wraps your web frontend in a desktop shell.\n See: https://framework.blackboard.sh/electrobun/${staticBuildNote ? `\n${staticBuildNote}` : ""}`;
9077
+ return `\n${pc.bold("Desktop app with Electrobun:")}\n${pc.cyan("•")} Start desktop app with HMR (runs the whole stack): ${`${runCmd} dev:desktop`}\n${pc.cyan("•")} Build stable desktop app (DMG/App): ${`${runCmd} build:desktop`}\n${pc.cyan("•")} Build canary desktop app: ${`${runCmd} build:desktop:canary`}\n${pc.yellow("NOTE:")} Electrobun wraps your web frontend in a desktop shell.\n The first desktop command downloads Hutch and the Electrobun toolchain.\n Packaged builds bake the API URL from apps/web/.env, so point it at a reachable server first.\n See: https://framework.blackboard.sh/electrobun/${staticBuildNote ? `\n${staticBuildNote}` : ""}`;
8781
9078
  }
8782
9079
  function getPwaInstructions() {
8783
9080
  return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA\n and React Router v7. See:\n https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
@@ -8896,7 +9193,8 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend,
8896
9193
  //#region src/helpers/core/create-project.ts
8897
9194
  /**
8898
9195
  * Creates a new project with the given configuration.
8899
- * Returns a Result with the project directory path on success, or an error on failure.
9196
+ * A failed dependency install is returned in the outcome instead of failing, since the project
9197
+ * is already on disk by then.
8900
9198
  */
8901
9199
  async function createProject(options, cliInput) {
8902
9200
  return Result.gen(async function* () {
@@ -8925,34 +9223,57 @@ async function createProject(options, cliInput) {
8925
9223
  cause: e
8926
9224
  }))));
8927
9225
  yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager, cliInput.packageManagerVersion));
8928
- if (!isConvex && options.database !== "none") yield* Result.await(Result.tryPromise({
8929
- try: () => setupDatabase(options, cliInput),
8930
- catch: (e) => new ProjectCreationError({
8931
- phase: "database-setup",
8932
- message: `Failed to setup database: ${e instanceof Error ? e.message : String(e)}`,
8933
- cause: e
8934
- })
8935
- }));
8936
- if (options.addons.length > 0 && options.addons[0] !== "none") yield* Result.await(Result.tryPromise({
8937
- try: () => setupAddons(options),
8938
- catch: (e) => new ProjectCreationError({
8939
- phase: "addons-setup",
8940
- message: `Failed to setup addons: ${e instanceof Error ? e.message : String(e)}`,
8941
- cause: e
8942
- })
8943
- }));
8944
- yield* Result.await(formatProject(projectDir));
8945
- if (!isSilent()) log.success("Project scaffolded");
8946
- if (options.install) yield* Result.await(installDependencies({
9226
+ beginInterruptibleScope();
9227
+ try {
9228
+ return yield* runPostScaffoldSteps(options, cliInput, projectDir, isConvex);
9229
+ } finally {
9230
+ endInterruptibleScope();
9231
+ }
9232
+ });
9233
+ }
9234
+ async function* runPostScaffoldSteps(options, cliInput, projectDir, isConvex) {
9235
+ if (!isConvex && options.database !== "none") yield* Result.await(Result.tryPromise({
9236
+ try: () => setupDatabase(options, cliInput),
9237
+ catch: (e) => new ProjectCreationError({
9238
+ phase: "database-setup",
9239
+ message: `Failed to setup database: ${e instanceof Error ? e.message : String(e)}`,
9240
+ cause: e
9241
+ })
9242
+ }));
9243
+ if (options.addons.length > 0 && options.addons[0] !== "none") yield* Result.await(Result.tryPromise({
9244
+ try: () => setupAddons(options),
9245
+ catch: (e) => new ProjectCreationError({
9246
+ phase: "addons-setup",
9247
+ message: `Failed to setup addons: ${e instanceof Error ? e.message : String(e)}`,
9248
+ cause: e
9249
+ })
9250
+ }));
9251
+ startInterruptibleStep();
9252
+ yield* Result.await(formatProject(projectDir, getInterruptSignal()));
9253
+ if (wasInterrupted()) cliLog.warn(pc.yellow("Formatting cancelled."));
9254
+ if (!isSilent()) log.success("Project scaffolded");
9255
+ let install = "skipped";
9256
+ let installError = null;
9257
+ if (options.install) {
9258
+ const installResult = await installDependencies({
8947
9259
  projectDir,
8948
9260
  packageManager: options.packageManager
8949
- }));
8950
- yield* Result.await(initializeGit(projectDir, options.git));
8951
- if (!isSilent()) await displayPostInstallInstructions({
8952
- ...options,
8953
- depsInstalled: options.install
8954
9261
  });
8955
- return Result.ok(projectDir);
9262
+ if (installResult.isErr()) {
9263
+ install = "failed";
9264
+ installError = installResult.error;
9265
+ } else install = installResult.value;
9266
+ }
9267
+ await runOptionalStep(() => initializeGit(projectDir, options.git, getInterruptSignal()), "Git initialization cancelled.");
9268
+ if (!isSilent()) await displayPostInstallInstructions({
9269
+ ...options,
9270
+ depsInstalled: install === "installed"
9271
+ });
9272
+ return Result.ok({
9273
+ projectDir,
9274
+ install,
9275
+ installError,
9276
+ interrupted: wasAnyStepInterrupted()
8956
9277
  });
8957
9278
  }
8958
9279
  async function setPackageManagerVersion(projectDir, packageManager, version) {
@@ -9030,7 +9351,7 @@ async function executeCreateProjectHandler(input, options) {
9030
9351
  async function reportCreateOutcome(input, result) {
9031
9352
  if (result.isOk()) return;
9032
9353
  const error = result.error;
9033
- if (UserCancelledError.is(error)) return;
9354
+ if (isUserCancellation(error)) return;
9034
9355
  await reportDiagnostic("cli_failed", {
9035
9356
  command: "create",
9036
9357
  mode: resolveInvocationMode(input.yes),
@@ -9050,7 +9371,7 @@ async function createProjectHandler(input, options = {}) {
9050
9371
  if (result.isOk()) return result.value;
9051
9372
  const error = result.error;
9052
9373
  const elapsedTimeMs = Date.now() - startTime;
9053
- if (UserCancelledError.is(error)) {
9374
+ if (isUserCancellation(error)) {
9054
9375
  if (silent) return createEmptyResult(timeScaffolded, elapsedTimeMs, error.message);
9055
9376
  return;
9056
9377
  }
@@ -9063,6 +9384,10 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
9063
9384
  if (!isSilent() && input.renderTitle !== false) renderTitle();
9064
9385
  if (!isSilent()) intro(pc.magenta("Configure your new project"));
9065
9386
  if (!isSilent() && input.yolo) log.warn(pc.yellow("YOLO mode enabled — compatibility checks are disabled."));
9387
+ if (!isSilent()) {
9388
+ const baseline = yield* Result.await(checkBaselineRequirements(input.packageManager ?? getUserPkgManager(), input.packageManager !== void 0));
9389
+ for (const warning of baseline.warnings) log.warn(pc.yellow(warning));
9390
+ }
9066
9391
  let currentPathInput;
9067
9392
  if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
9068
9393
  else if (input.yes && input.projectName) currentPathInput = input.projectName;
@@ -9208,12 +9533,27 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
9208
9533
  relativePath: config.relativePath
9209
9534
  });
9210
9535
  }
9211
- yield* Result.await(createProject(config, {
9536
+ const created = yield* Result.await(createProject(config, {
9212
9537
  manualDb: cliInput.manualDb ?? input.manualDb,
9213
9538
  dbSetupOptions: effectiveDbSetupOptions,
9214
9539
  packageManagerVersion: localRequirements.packageManagerVersion
9215
9540
  }));
9216
- await trackProjectCreation(config, input.disableAnalytics, resolveInvocationMode(input.yes));
9541
+ const mode = resolveInvocationMode(input.yes);
9542
+ await trackProjectCreation(config, input.disableAnalytics, mode);
9543
+ const warnings = [];
9544
+ if (created.interrupted) warnings.push("One or more setup steps were cancelled.");
9545
+ if (created.installError) {
9546
+ warnings.push(created.installError.message);
9547
+ await reportDiagnostic("cli_failed", {
9548
+ command: "create",
9549
+ mode,
9550
+ stage: "dependency-installation",
9551
+ error: errorClass(created.installError),
9552
+ reason: scrubReason(created.installError),
9553
+ packageManager: config.packageManager,
9554
+ backend: config.backend
9555
+ });
9556
+ }
9217
9557
  const historyResult = await addToHistory(config, reproducibleCommand);
9218
9558
  if (historyResult.isErr() && !isSilent()) {
9219
9559
  log.warn(pc.yellow(historyResult.error.message));
@@ -9244,7 +9584,8 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
9244
9584
  timeScaffolded,
9245
9585
  elapsedTimeMs,
9246
9586
  projectDirectory: config.projectDir,
9247
- relativePath: config.relativePath
9587
+ relativePath: config.relativePath,
9588
+ warnings: warnings.length > 0 ? warnings : void 0
9248
9589
  });
9249
9590
  });
9250
9591
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.41.3",
3
+ "version": "3.42.0",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "keywords": [
6
6
  "better-auth",
@@ -69,8 +69,8 @@
69
69
  "prepublishOnly": "npm run build"
70
70
  },
71
71
  "dependencies": {
72
- "@better-t-stack/template-generator": "^3.41.3",
73
- "@better-t-stack/types": "^3.41.3",
72
+ "@better-t-stack/template-generator": "^3.42.0",
73
+ "@better-t-stack/types": "^3.42.0",
74
74
  "@clack/core": "^1.4.3",
75
75
  "@clack/prompts": "^1.7.0",
76
76
  "@modelcontextprotocol/server": "2.0.0",