create-better-t-stack 3.22.1 → 3.23.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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { t as __reExport } from "./chunk-CHc3S52W.mjs";
3
- import { createRouterClient, os } from "@orpc/server";
2
+ import { n as __reExport, t as __exportAll } from "./chunk-BtN16TXe.mjs";
3
+ import { getAllJsonSchemas } from "@better-t-stack/types/json-schema";
4
+ import { os } from "@orpc/server";
4
5
  import { Result, Result as Result$1, TaggedError } from "better-result";
5
6
  import { createCli } from "trpc-cli";
6
7
  import z from "zod";
@@ -10,7 +11,7 @@ import envPaths from "env-paths";
10
11
  import fs from "fs-extra";
11
12
  import path from "node:path";
12
13
  import { fileURLToPath } from "node:url";
13
- import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps } from "@better-t-stack/template-generator";
14
+ import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps } from "@better-t-stack/template-generator";
14
15
  import consola, { consola as consola$1 } from "consola";
15
16
  import gradient from "gradient-string";
16
17
  import { $, execa } from "execa";
@@ -20,7 +21,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
20
21
  import { applyEdits, modify, parse } from "jsonc-parser";
21
22
  import os$1 from "node:os";
22
23
  import { format } from "oxfmt";
23
-
24
24
  //#region src/utils/get-package-manager.ts
25
25
  const getUserPkgManager = () => {
26
26
  const userAgent = process.env.npm_config_user_agent;
@@ -28,7 +28,6 @@ const getUserPkgManager = () => {
28
28
  if (userAgent?.startsWith("bun")) return "bun";
29
29
  return "npm";
30
30
  };
31
-
32
31
  //#endregion
33
32
  //#region src/constants.ts
34
33
  const __filename = fileURLToPath(import.meta.url);
@@ -83,6 +82,7 @@ const ADDON_COMPATIBILITY = {
83
82
  husky: [],
84
83
  lefthook: [],
85
84
  turborepo: [],
85
+ nx: [],
86
86
  starlight: [],
87
87
  ultracite: [],
88
88
  ruler: [],
@@ -94,7 +94,6 @@ const ADDON_COMPATIBILITY = {
94
94
  skills: [],
95
95
  none: []
96
96
  };
97
-
98
97
  //#endregion
99
98
  //#region src/utils/errors.ts
100
99
  /**
@@ -183,14 +182,14 @@ function displayError(error) {
183
182
  if (UserCancelledError.is(error)) cancel(pc.red(error.message));
184
183
  else consola.error(pc.red(error.message));
185
184
  }
186
-
187
185
  //#endregion
188
186
  //#region src/utils/get-latest-cli-version.ts
189
187
  function getLatestCLIVersionResult() {
190
188
  const packageJsonPath = path.join(PKG_ROOT, "package.json");
191
189
  return Result.try({
192
190
  try: () => {
193
- return fs.readJSONSync(packageJsonPath).version;
191
+ const packageJsonContent = fs.readJSONSync(packageJsonPath);
192
+ return String(packageJsonContent.version ?? "1.0.0");
194
193
  },
195
194
  catch: (e) => new CLIError({
196
195
  message: `Failed to read CLI version from package.json: ${e instanceof Error ? e.message : String(e)}`,
@@ -201,7 +200,6 @@ function getLatestCLIVersionResult() {
201
200
  function getLatestCLIVersion() {
202
201
  return getLatestCLIVersionResult().unwrapOr("1.0.0");
203
202
  }
204
-
205
203
  //#endregion
206
204
  //#region src/utils/project-history.ts
207
205
  const paths = envPaths("better-t-stack", { suffix: "" });
@@ -314,7 +312,6 @@ async function clearHistory() {
314
312
  })
315
313
  });
316
314
  }
317
-
318
315
  //#endregion
319
316
  //#region src/utils/render-title.ts
320
317
  const TITLE_TEXT = `
@@ -351,7 +348,6 @@ const renderTitle = () => {
351
348
  if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Better T Stack`));
352
349
  else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
353
350
  };
354
-
355
351
  //#endregion
356
352
  //#region src/commands/history.ts
357
353
  function formatStackSummary(entry) {
@@ -410,7 +406,6 @@ async function historyHandler(input) {
410
406
  log.message("");
411
407
  }
412
408
  }
413
-
414
409
  //#endregion
415
410
  //#region src/utils/open-url.ts
416
411
  async function openUrl(url) {
@@ -426,7 +421,6 @@ async function openUrl(url) {
426
421
  }
427
422
  await $({ stdio: "ignore" })`xdg-open ${url}`;
428
423
  }
429
-
430
424
  //#endregion
431
425
  //#region src/utils/sponsors.ts
432
426
  const SPONSORS_JSON_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
@@ -590,7 +584,6 @@ function normalizeSponsorFetchError(error) {
590
584
  cause: error
591
585
  });
592
586
  }
593
-
594
587
  //#endregion
595
588
  //#region src/commands/meta.ts
596
589
  const DOCS_URL = "https://better-t-stack.dev/docs";
@@ -619,13 +612,11 @@ async function openDocsCommand() {
619
612
  async function openBuilderCommand() {
620
613
  await openExternalUrl(BUILDER_URL, "Opened builder in your default browser.");
621
614
  }
622
-
623
615
  //#endregion
624
616
  //#region src/types.ts
625
- var types_exports = {};
617
+ var types_exports = /* @__PURE__ */ __exportAll({});
626
618
  import * as import__better_t_stack_types from "@better-t-stack/types";
627
619
  __reExport(types_exports, import__better_t_stack_types);
628
-
629
620
  //#endregion
630
621
  //#region src/utils/compatibility.ts
631
622
  const WEB_FRAMEWORKS = [
@@ -638,7 +629,6 @@ const WEB_FRAMEWORKS = [
638
629
  "solid",
639
630
  "astro"
640
631
  ];
641
-
642
632
  //#endregion
643
633
  //#region src/utils/compatibility-rules.ts
644
634
  function validationErr$1(message) {
@@ -761,6 +751,7 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth) {
761
751
  });
762
752
  }
763
753
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
754
+ if (addons.includes("turborepo") && addons.includes("nx")) return validationErr$1("Cannot combine 'turborepo' and 'nx' addons. Choose one monorepo tool.");
764
755
  for (const addon of addons) {
765
756
  if (addon === "none") continue;
766
757
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
@@ -793,7 +784,6 @@ function validateExamplesCompatibility(examples, backend, database, frontend, ap
793
784
  }
794
785
  return Result.ok(void 0);
795
786
  }
796
-
797
787
  //#endregion
798
788
  //#region src/utils/context.ts
799
789
  const cliStorage = new AsyncLocalStorage();
@@ -846,14 +836,12 @@ async function runWithContextAsync(options, fn) {
846
836
  };
847
837
  return cliStorage.run(ctx, fn);
848
838
  }
849
-
850
839
  //#endregion
851
840
  //#region src/utils/navigation.ts
852
841
  const GO_BACK_SYMBOL = Symbol("clack:goBack");
853
842
  function isGoBack(value) {
854
843
  return value === GO_BACK_SYMBOL;
855
844
  }
856
-
857
845
  //#endregion
858
846
  //#region src/prompts/navigable.ts
859
847
  /**
@@ -892,6 +880,9 @@ function getHint() {
892
880
  function getMultiHint() {
893
881
  return isFirstPrompt() ? KEYBOARD_HINT_MULTI_FIRST : KEYBOARD_HINT_MULTI;
894
882
  }
883
+ function normalizeValidationMessage(validationMessage) {
884
+ return validationMessage instanceof Error ? validationMessage.message : validationMessage;
885
+ }
895
886
  async function runWithNavigation(prompt) {
896
887
  let goBack = false;
897
888
  prompt.on("key", (char) => {
@@ -950,6 +941,7 @@ async function navigableMultiselect(opts) {
950
941
  required,
951
942
  validate(selected) {
952
943
  if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
944
+ return normalizeValidationMessage(opts.validate?.(selected));
953
945
  },
954
946
  render() {
955
947
  const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
@@ -1033,6 +1025,7 @@ async function navigableGroupMultiselect(opts) {
1033
1025
  selectableGroups: true,
1034
1026
  validate(selected) {
1035
1027
  if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
1028
+ return normalizeValidationMessage(opts.validate?.(selected));
1036
1029
  },
1037
1030
  render() {
1038
1031
  const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
@@ -1079,7 +1072,6 @@ async function navigableGroupMultiselect(opts) {
1079
1072
  }
1080
1073
  }));
1081
1074
  }
1082
-
1083
1075
  //#endregion
1084
1076
  //#region src/prompts/addons.ts
1085
1077
  function getAddonDisplay(addon) {
@@ -1090,6 +1082,10 @@ function getAddonDisplay(addon) {
1090
1082
  label = "Turborepo";
1091
1083
  hint = "High-performance build system";
1092
1084
  break;
1085
+ case "nx":
1086
+ label = "Nx";
1087
+ hint = "Smart monorepo orchestration and task graph";
1088
+ break;
1093
1089
  case "pwa":
1094
1090
  label = "PWA";
1095
1091
  hint = "Make your app installable and work offline";
@@ -1144,7 +1140,7 @@ function getAddonDisplay(addon) {
1144
1140
  break;
1145
1141
  case "mcp":
1146
1142
  label = "MCP";
1147
- hint = "Install MCP servers (docs, databases, SaaS) via add-mcp";
1143
+ hint = "Install MCP servers, including Better T Stack, via add-mcp";
1148
1144
  break;
1149
1145
  default:
1150
1146
  label = addon;
@@ -1156,8 +1152,8 @@ function getAddonDisplay(addon) {
1156
1152
  };
1157
1153
  }
1158
1154
  const ADDON_GROUPS = {
1159
- Tooling: [
1160
- "turborepo",
1155
+ "Monorepo & Tasks": ["turborepo", "nx"],
1156
+ "Code Quality": [
1161
1157
  "biome",
1162
1158
  "oxlint",
1163
1159
  "ultracite",
@@ -1165,100 +1161,91 @@ const ADDON_GROUPS = {
1165
1161
  "lefthook"
1166
1162
  ],
1167
1163
  Documentation: ["starlight", "fumadocs"],
1168
- Extensions: [
1164
+ "Platform Extensions": [
1169
1165
  "pwa",
1170
1166
  "tauri",
1171
1167
  "opentui",
1172
1168
  "wxt"
1173
1169
  ],
1174
- AI: [
1170
+ "AI & Agent Tools": [
1175
1171
  "ruler",
1176
1172
  "skills",
1177
1173
  "mcp"
1178
1174
  ]
1179
1175
  };
1176
+ function createGroupedOptions() {
1177
+ return Object.fromEntries(Object.keys(ADDON_GROUPS).map((group) => [group, []]));
1178
+ }
1179
+ function addOptionToGroup(groupedOptions, option) {
1180
+ for (const [group, addons] of Object.entries(ADDON_GROUPS)) if (addons.includes(option.value)) {
1181
+ groupedOptions[group]?.push(option);
1182
+ return;
1183
+ }
1184
+ }
1185
+ function sortAndPruneGroupedOptions(groupedOptions) {
1186
+ Object.keys(groupedOptions).forEach((group) => {
1187
+ if (groupedOptions[group].length === 0) {
1188
+ delete groupedOptions[group];
1189
+ return;
1190
+ }
1191
+ const groupOrder = ADDON_GROUPS[group] || [];
1192
+ groupedOptions[group].sort((a, b) => {
1193
+ return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
1194
+ });
1195
+ });
1196
+ }
1197
+ function validateAddonSelection(selected) {
1198
+ if (selected?.includes("turborepo") && selected.includes("nx")) return "Choose either Turborepo or Nx as your monorepo tool, not both.";
1199
+ }
1180
1200
  async function getAddonsChoice(addons, frontends, auth) {
1181
1201
  if (addons !== void 0) return addons;
1182
1202
  const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
1183
- const groupedOptions = {
1184
- Tooling: [],
1185
- Documentation: [],
1186
- Extensions: [],
1187
- AI: []
1188
- };
1203
+ const groupedOptions = createGroupedOptions();
1189
1204
  const frontendsArray = frontends || [];
1190
1205
  for (const addon of allAddons) {
1191
1206
  const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
1192
1207
  if (!isCompatible) continue;
1193
1208
  const { label, hint } = getAddonDisplay(addon);
1194
- const option = {
1209
+ addOptionToGroup(groupedOptions, {
1195
1210
  value: addon,
1196
1211
  label,
1197
1212
  hint
1198
- };
1199
- if (ADDON_GROUPS.Tooling.includes(addon)) groupedOptions.Tooling.push(option);
1200
- else if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
1201
- else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
1202
- else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
1213
+ });
1203
1214
  }
1204
- Object.keys(groupedOptions).forEach((group) => {
1205
- if (groupedOptions[group].length === 0) delete groupedOptions[group];
1206
- else {
1207
- const groupOrder = ADDON_GROUPS[group] || [];
1208
- groupedOptions[group].sort((a, b) => {
1209
- return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
1210
- });
1211
- }
1212
- });
1215
+ sortAndPruneGroupedOptions(groupedOptions);
1213
1216
  const response = await navigableGroupMultiselect({
1214
1217
  message: "Select addons",
1215
1218
  options: groupedOptions,
1216
1219
  initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
1217
- required: false
1220
+ required: false,
1221
+ validate: validateAddonSelection
1218
1222
  });
1219
1223
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1220
1224
  return response;
1221
1225
  }
1222
1226
  async function getAddonsToAdd(frontend, existingAddons = [], auth) {
1223
- const groupedOptions = {
1224
- Tooling: [],
1225
- Documentation: [],
1226
- Extensions: [],
1227
- AI: []
1228
- };
1227
+ const groupedOptions = createGroupedOptions();
1229
1228
  const frontendArray = frontend || [];
1230
1229
  const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons, auth);
1231
1230
  for (const addon of compatibleAddons) {
1232
1231
  const { label, hint } = getAddonDisplay(addon);
1233
- const option = {
1232
+ addOptionToGroup(groupedOptions, {
1234
1233
  value: addon,
1235
1234
  label,
1236
1235
  hint
1237
- };
1238
- if (ADDON_GROUPS.Tooling.includes(addon)) groupedOptions.Tooling.push(option);
1239
- else if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
1240
- else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
1241
- else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
1236
+ });
1242
1237
  }
1243
- Object.keys(groupedOptions).forEach((group) => {
1244
- if (groupedOptions[group].length === 0) delete groupedOptions[group];
1245
- else {
1246
- const groupOrder = ADDON_GROUPS[group] || [];
1247
- groupedOptions[group].sort((a, b) => {
1248
- return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
1249
- });
1250
- }
1251
- });
1238
+ sortAndPruneGroupedOptions(groupedOptions);
1252
1239
  if (Object.keys(groupedOptions).length === 0) return [];
1253
1240
  const response = await navigableGroupMultiselect({
1254
1241
  message: "Select addons to add",
1255
1242
  options: groupedOptions,
1256
- required: false
1243
+ required: false,
1244
+ validate: validateAddonSelection
1257
1245
  });
1258
1246
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1259
1247
  return response;
1260
1248
  }
1261
-
1262
1249
  //#endregion
1263
1250
  //#region src/utils/bts-config.ts
1264
1251
  const BTS_CONFIG_FILE = "bts.jsonc";
@@ -1289,7 +1276,26 @@ async function updateBtsConfig(projectDir, updates) {
1289
1276
  await fs.writeFile(configPath, content, "utf-8");
1290
1277
  } catch {}
1291
1278
  }
1292
-
1279
+ //#endregion
1280
+ //#region src/utils/input-hardening.ts
1281
+ function hasControlCharacters(value) {
1282
+ for (const char of value) {
1283
+ const charCode = char.charCodeAt(0);
1284
+ if (charCode < 32 || charCode === 127) return true;
1285
+ }
1286
+ return false;
1287
+ }
1288
+ function hardeningError(field, value, message) {
1289
+ return Result.err(new ValidationError({
1290
+ field,
1291
+ value,
1292
+ message
1293
+ }));
1294
+ }
1295
+ function validateAgentSafePathInput(value, field) {
1296
+ if (hasControlCharacters(value)) return hardeningError(field, value, `Invalid ${field}: control characters are not allowed.`);
1297
+ return Result.ok(void 0);
1298
+ }
1293
1299
  //#endregion
1294
1300
  //#region src/utils/add-package-deps.ts
1295
1301
  const addPackageDependency = async (opts) => {
@@ -1312,13 +1318,11 @@ const addPackageDependency = async (opts) => {
1312
1318
  for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
1313
1319
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
1314
1320
  };
1315
-
1316
1321
  //#endregion
1317
1322
  //#region src/utils/external-commands.ts
1318
1323
  function shouldSkipExternalCommands() {
1319
1324
  return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
1320
1325
  }
1321
-
1322
1326
  //#endregion
1323
1327
  //#region src/utils/package-runner.ts
1324
1328
  function splitCommandArgs(commandWithArgs) {
@@ -1413,7 +1417,33 @@ function getPackageRunnerPrefix(packageManager) {
1413
1417
  default: return ["npx"];
1414
1418
  }
1415
1419
  }
1416
-
1420
+ //#endregion
1421
+ //#region src/utils/terminal-output.ts
1422
+ const noopSpinner = {
1423
+ start() {},
1424
+ stop() {},
1425
+ message() {}
1426
+ };
1427
+ function createSpinner() {
1428
+ return isSilent() ? noopSpinner : spinner();
1429
+ }
1430
+ const cliLog = {
1431
+ info(message) {
1432
+ if (!isSilent()) log.info(message);
1433
+ },
1434
+ warn(message) {
1435
+ if (!isSilent()) log.warn(message);
1436
+ },
1437
+ success(message) {
1438
+ if (!isSilent()) log.success(message);
1439
+ },
1440
+ error(message) {
1441
+ if (!isSilent()) log.error(message);
1442
+ },
1443
+ message(message) {
1444
+ if (!isSilent()) log.message(message);
1445
+ }
1446
+ };
1417
1447
  //#endregion
1418
1448
  //#region src/helpers/addons/fumadocs-setup.ts
1419
1449
  const TEMPLATES$2 = {
@@ -1453,22 +1483,31 @@ const TEMPLATES$2 = {
1453
1483
  value: "tanstack-start-spa"
1454
1484
  }
1455
1485
  };
1486
+ const DEFAULT_TEMPLATE$2 = "next-mdx";
1487
+ const DEFAULT_DEV_PORT$1 = 4e3;
1456
1488
  async function setupFumadocs(config) {
1457
1489
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
1458
1490
  const { packageManager, projectDir } = config;
1459
- log.info("Setting up Fumadocs...");
1460
- const template = await select({
1461
- message: "Choose a template",
1462
- options: Object.entries(TEMPLATES$2).map(([key, template]) => ({
1463
- value: key,
1464
- label: template.label,
1465
- hint: template.hint
1466
- })),
1467
- initialValue: "next-mdx"
1468
- });
1469
- if (isCancel(template)) return userCancelled("Operation cancelled");
1491
+ cliLog.info("Setting up Fumadocs...");
1492
+ const configuredOptions = config.addonOptions?.fumadocs;
1493
+ let template = configuredOptions?.template;
1494
+ if (!template) if (isSilent()) template = DEFAULT_TEMPLATE$2;
1495
+ else {
1496
+ const selectedTemplate = await select({
1497
+ message: "Choose a template",
1498
+ options: Object.entries(TEMPLATES$2).map(([key, templateOption]) => ({
1499
+ value: key,
1500
+ label: templateOption.label,
1501
+ hint: templateOption.hint
1502
+ })),
1503
+ initialValue: DEFAULT_TEMPLATE$2
1504
+ });
1505
+ if (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
1506
+ template = selectedTemplate;
1507
+ }
1470
1508
  const templateArg = TEMPLATES$2[template].value;
1471
1509
  const isNextTemplate = template.startsWith("next-");
1510
+ const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT$1;
1472
1511
  const options = [
1473
1512
  `--template ${templateArg}`,
1474
1513
  `--pm ${packageManager}`,
@@ -1479,7 +1518,7 @@ async function setupFumadocs(config) {
1479
1518
  const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs ${options.join(" ")}`);
1480
1519
  const appsDir = path.join(projectDir, "apps");
1481
1520
  await fs.ensureDir(appsDir);
1482
- const s = spinner();
1521
+ const s = createSpinner();
1483
1522
  s.start("Running Fumadocs create command...");
1484
1523
  const result = await Result.tryPromise({
1485
1524
  try: async () => {
@@ -1492,7 +1531,7 @@ async function setupFumadocs(config) {
1492
1531
  if (await fs.pathExists(packageJsonPath)) {
1493
1532
  const packageJson = await fs.readJson(packageJsonPath);
1494
1533
  packageJson.name = "fumadocs";
1495
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
1534
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=${devPort}`;
1496
1535
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1497
1536
  }
1498
1537
  },
@@ -1509,10 +1548,24 @@ async function setupFumadocs(config) {
1509
1548
  s.stop("Fumadocs setup complete!");
1510
1549
  return Result.ok(void 0);
1511
1550
  }
1512
-
1513
1551
  //#endregion
1514
1552
  //#region src/helpers/addons/mcp-setup.ts
1515
1553
  const MCP_AGENTS = [
1554
+ {
1555
+ value: "antigravity",
1556
+ label: "Antigravity",
1557
+ scope: "global"
1558
+ },
1559
+ {
1560
+ value: "cline",
1561
+ label: "Cline VSCode Extension",
1562
+ scope: "global"
1563
+ },
1564
+ {
1565
+ value: "cline-cli",
1566
+ label: "Cline CLI",
1567
+ scope: "global"
1568
+ },
1516
1569
  {
1517
1570
  value: "cursor",
1518
1571
  label: "Cursor",
@@ -1538,6 +1591,16 @@ const MCP_AGENTS = [
1538
1591
  label: "Gemini CLI",
1539
1592
  scope: "both"
1540
1593
  },
1594
+ {
1595
+ value: "github-copilot-cli",
1596
+ label: "GitHub Copilot CLI",
1597
+ scope: "both"
1598
+ },
1599
+ {
1600
+ value: "mcporter",
1601
+ label: "MCPorter",
1602
+ scope: "both"
1603
+ },
1541
1604
  {
1542
1605
  value: "vscode",
1543
1606
  label: "VS Code (GitHub Copilot)",
@@ -1559,6 +1622,12 @@ const MCP_AGENTS = [
1559
1622
  scope: "global"
1560
1623
  }
1561
1624
  ];
1625
+ const DEFAULT_SCOPE$1 = "project";
1626
+ const DEFAULT_AGENTS$2 = [
1627
+ "cursor",
1628
+ "claude-code",
1629
+ "vscode"
1630
+ ];
1562
1631
  function uniqueValues$1(values) {
1563
1632
  return Array.from(new Set(values));
1564
1633
  }
@@ -1568,105 +1637,138 @@ function hasReactBasedFrontend$1(frontend) {
1568
1637
  function hasNativeFrontend$1(frontend) {
1569
1638
  return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
1570
1639
  }
1571
- function getRecommendedMcpServers(config) {
1572
- const servers = [];
1573
- servers.push({
1574
- key: "context7",
1575
- label: "Context7",
1576
- name: "context7",
1577
- target: "@upstash/context7-mcp"
1578
- });
1579
- if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") servers.push({
1580
- key: "cloudflare-docs",
1581
- label: "Cloudflare Docs",
1582
- name: "cloudflare-docs",
1583
- target: "https://docs.mcp.cloudflare.com/sse",
1584
- transport: "sse"
1585
- });
1586
- if (config.backend === "convex") servers.push({
1587
- key: "convex",
1588
- label: "Convex",
1589
- name: "convex",
1590
- target: "npx -y convex@latest mcp start"
1591
- });
1592
- if (hasReactBasedFrontend$1(config.frontend)) servers.push({
1593
- key: "shadcn",
1594
- label: "shadcn/ui",
1595
- name: "shadcn",
1596
- target: "npx -y shadcn@latest mcp"
1597
- });
1598
- if (config.frontend.includes("next")) servers.push({
1599
- key: "next-devtools",
1600
- label: "Next Devtools",
1601
- name: "next-devtools",
1602
- target: "npx -y next-devtools-mcp@latest"
1603
- });
1604
- if (config.frontend.includes("nuxt")) servers.push({
1605
- key: "nuxt-docs",
1606
- label: "Nuxt Docs",
1607
- name: "nuxt",
1608
- target: "https://nuxt.com/mcp"
1609
- }, {
1610
- key: "nuxt-ui-docs",
1611
- label: "Nuxt UI Docs",
1612
- name: "nuxt-ui",
1613
- target: "https://ui.nuxt.com/mcp"
1614
- });
1615
- if (config.frontend.includes("svelte")) servers.push({
1616
- key: "svelte-docs",
1617
- label: "Svelte Docs",
1618
- name: "svelte",
1619
- target: "https://mcp.svelte.dev/mcp"
1620
- });
1621
- if (config.frontend.includes("astro")) servers.push({
1622
- key: "astro-docs",
1623
- label: "Astro Docs",
1624
- name: "astro-docs",
1625
- target: "https://mcp.docs.astro.build/mcp"
1626
- });
1627
- if (config.dbSetup === "planetscale") servers.push({
1628
- key: "planetscale",
1629
- label: "PlanetScale",
1630
- name: "planetscale",
1631
- target: "https://mcp.pscale.dev/mcp/planetscale"
1632
- });
1633
- if (config.dbSetup === "neon") servers.push({
1634
- key: "neon",
1635
- label: "Neon",
1636
- name: "neon",
1637
- target: "https://mcp.neon.tech/mcp"
1638
- });
1639
- if (config.dbSetup === "supabase") servers.push({
1640
- key: "supabase",
1641
- label: "Supabase",
1642
- name: "supabase",
1643
- target: "https://mcp.supabase.com/mcp"
1644
- });
1645
- if (config.auth === "better-auth") servers.push({
1646
- key: "better-auth",
1647
- label: "Better Auth",
1648
- name: "better-auth",
1649
- target: "https://mcp.inkeep.com/better-auth/mcp"
1650
- });
1651
- if (config.auth === "clerk") servers.push({
1652
- key: "clerk",
1653
- label: "Clerk",
1654
- name: "clerk",
1655
- target: "https://mcp.clerk.com/mcp"
1656
- });
1657
- if (hasNativeFrontend$1(config.frontend)) servers.push({
1658
- key: "expo",
1659
- label: "Expo",
1660
- name: "expo-mcp",
1661
- target: "https://mcp.expo.dev/mcp"
1662
- });
1663
- if (config.payments === "polar") servers.push({
1664
- key: "polar",
1665
- label: "Polar",
1666
- name: "polar",
1667
- target: "https://mcp.polar.sh/mcp/polar-mcp"
1668
- });
1669
- return servers;
1640
+ function getAllMcpServers(config) {
1641
+ return [
1642
+ {
1643
+ key: "better-t-stack",
1644
+ label: "Better T Stack",
1645
+ name: "better-t-stack",
1646
+ target: getPackageExecutionCommand(config.packageManager, "create-better-t-stack@latest mcp")
1647
+ },
1648
+ {
1649
+ key: "context7",
1650
+ label: "Context7",
1651
+ name: "context7",
1652
+ target: "@upstash/context7-mcp"
1653
+ },
1654
+ {
1655
+ key: "nx",
1656
+ label: "Nx Workspace",
1657
+ name: "nx",
1658
+ target: "npx nx mcp ."
1659
+ },
1660
+ {
1661
+ key: "cloudflare-docs",
1662
+ label: "Cloudflare Docs",
1663
+ name: "cloudflare-docs",
1664
+ target: "https://docs.mcp.cloudflare.com/sse",
1665
+ transport: "sse"
1666
+ },
1667
+ {
1668
+ key: "convex",
1669
+ label: "Convex",
1670
+ name: "convex",
1671
+ target: "npx -y convex@latest mcp start"
1672
+ },
1673
+ {
1674
+ key: "shadcn",
1675
+ label: "shadcn/ui",
1676
+ name: "shadcn",
1677
+ target: "npx -y shadcn@latest mcp"
1678
+ },
1679
+ {
1680
+ key: "next-devtools",
1681
+ label: "Next Devtools",
1682
+ name: "next-devtools",
1683
+ target: "npx -y next-devtools-mcp@latest"
1684
+ },
1685
+ {
1686
+ key: "nuxt-docs",
1687
+ label: "Nuxt Docs",
1688
+ name: "nuxt",
1689
+ target: "https://nuxt.com/mcp"
1690
+ },
1691
+ {
1692
+ key: "nuxt-ui-docs",
1693
+ label: "Nuxt UI Docs",
1694
+ name: "nuxt-ui",
1695
+ target: "https://ui.nuxt.com/mcp"
1696
+ },
1697
+ {
1698
+ key: "svelte-docs",
1699
+ label: "Svelte Docs",
1700
+ name: "svelte",
1701
+ target: "https://mcp.svelte.dev/mcp"
1702
+ },
1703
+ {
1704
+ key: "astro-docs",
1705
+ label: "Astro Docs",
1706
+ name: "astro-docs",
1707
+ target: "https://mcp.docs.astro.build/mcp"
1708
+ },
1709
+ {
1710
+ key: "planetscale",
1711
+ label: "PlanetScale",
1712
+ name: "planetscale",
1713
+ target: "https://mcp.pscale.dev/mcp/planetscale"
1714
+ },
1715
+ {
1716
+ key: "neon",
1717
+ label: "Neon",
1718
+ name: "neon",
1719
+ target: "https://mcp.neon.tech/mcp"
1720
+ },
1721
+ {
1722
+ key: "supabase",
1723
+ label: "Supabase",
1724
+ name: "supabase",
1725
+ target: "https://mcp.supabase.com/mcp"
1726
+ },
1727
+ {
1728
+ key: "better-auth",
1729
+ label: "Better Auth",
1730
+ name: "better-auth",
1731
+ target: "https://mcp.inkeep.com/better-auth/mcp"
1732
+ },
1733
+ {
1734
+ key: "clerk",
1735
+ label: "Clerk",
1736
+ name: "clerk",
1737
+ target: "https://mcp.clerk.com/mcp"
1738
+ },
1739
+ {
1740
+ key: "expo",
1741
+ label: "Expo",
1742
+ name: "expo-mcp",
1743
+ target: "https://mcp.expo.dev/mcp"
1744
+ },
1745
+ {
1746
+ key: "polar",
1747
+ label: "Polar",
1748
+ name: "polar",
1749
+ target: "https://mcp.polar.sh/mcp/polar-mcp"
1750
+ }
1751
+ ];
1752
+ }
1753
+ function getRecommendedMcpServers(config, scope) {
1754
+ const serversByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
1755
+ const recommendedServerKeys = ["better-t-stack", "context7"];
1756
+ if (scope === "project" && config.addons.includes("nx")) recommendedServerKeys.push("nx");
1757
+ if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") recommendedServerKeys.push("cloudflare-docs");
1758
+ if (config.backend === "convex") recommendedServerKeys.push("convex");
1759
+ if (hasReactBasedFrontend$1(config.frontend)) recommendedServerKeys.push("shadcn");
1760
+ if (config.frontend.includes("next")) recommendedServerKeys.push("next-devtools");
1761
+ if (config.frontend.includes("nuxt")) recommendedServerKeys.push("nuxt-docs", "nuxt-ui-docs");
1762
+ if (config.frontend.includes("svelte")) recommendedServerKeys.push("svelte-docs");
1763
+ if (config.frontend.includes("astro")) recommendedServerKeys.push("astro-docs");
1764
+ if (config.dbSetup === "planetscale") recommendedServerKeys.push("planetscale");
1765
+ if (config.dbSetup === "neon") recommendedServerKeys.push("neon");
1766
+ if (config.dbSetup === "supabase") recommendedServerKeys.push("supabase");
1767
+ if (config.auth === "better-auth") recommendedServerKeys.push("better-auth");
1768
+ if (config.auth === "clerk") recommendedServerKeys.push("clerk");
1769
+ if (hasNativeFrontend$1(config.frontend)) recommendedServerKeys.push("expo");
1770
+ if (config.payments === "polar") recommendedServerKeys.push("polar");
1771
+ return uniqueValues$1(recommendedServerKeys).map((serverKey) => serversByKey.get(serverKey)).filter((server) => server !== void 0);
1670
1772
  }
1671
1773
  function filterAgentsForScope(scope) {
1672
1774
  return MCP_AGENTS.filter((a) => a.scope === "both" || a.scope === scope);
@@ -1674,67 +1776,83 @@ function filterAgentsForScope(scope) {
1674
1776
  async function setupMcp(config) {
1675
1777
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
1676
1778
  const { packageManager, projectDir } = config;
1677
- log.info("Setting up MCP servers...");
1678
- const scope = await select({
1679
- message: "Where should MCP servers be installed?",
1680
- options: [{
1681
- value: "project",
1682
- label: "Project",
1683
- hint: "Writes to project config files (recommended for teams)"
1684
- }, {
1685
- value: "global",
1686
- label: "Global",
1687
- hint: "Writes to user-level config files (personal machine)"
1688
- }],
1689
- initialValue: "project"
1690
- });
1691
- if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1692
- const recommendedServers = getRecommendedMcpServers(config);
1779
+ cliLog.info("Setting up MCP servers...");
1780
+ let scope = config.addonOptions?.mcp?.scope;
1781
+ if (!scope) if (isSilent()) scope = DEFAULT_SCOPE$1;
1782
+ else {
1783
+ const selectedScope = await select({
1784
+ message: "Where should MCP servers be installed?",
1785
+ options: [{
1786
+ value: "project",
1787
+ label: "Project",
1788
+ hint: "Writes to project config files (recommended for teams)"
1789
+ }, {
1790
+ value: "global",
1791
+ label: "Global",
1792
+ hint: "Writes to user-level config files (personal machine)"
1793
+ }],
1794
+ initialValue: DEFAULT_SCOPE$1
1795
+ });
1796
+ if (isCancel(selectedScope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1797
+ scope = selectedScope;
1798
+ }
1799
+ const recommendedServers = getRecommendedMcpServers(config, scope);
1693
1800
  if (recommendedServers.length === 0) return Result.ok(void 0);
1801
+ const allServersByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
1694
1802
  const serverOptions = recommendedServers.map((s) => ({
1695
1803
  value: s.key,
1696
1804
  label: s.label,
1697
1805
  hint: s.target
1698
1806
  }));
1699
- const selectedServerKeys = await multiselect({
1700
- message: "Select MCP servers to install",
1701
- options: serverOptions,
1702
- required: false,
1703
- initialValues: serverOptions.map((o) => o.value)
1704
- });
1705
- if (isCancel(selectedServerKeys)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1807
+ const configuredServerKeys = config.addonOptions?.mcp?.servers;
1808
+ const availableServerKeys = new Set(allServersByKey.keys());
1809
+ let selectedServerKeys = configuredServerKeys?.filter((serverKey) => availableServerKeys.has(serverKey)) ?? [];
1810
+ if (selectedServerKeys.length === 0 && configuredServerKeys === void 0) if (isSilent()) selectedServerKeys = serverOptions.map((o) => o.value);
1811
+ else {
1812
+ const promptedServerKeys = await multiselect({
1813
+ message: "Select MCP servers to install",
1814
+ options: serverOptions,
1815
+ required: false,
1816
+ initialValues: serverOptions.map((o) => o.value)
1817
+ });
1818
+ if (isCancel(promptedServerKeys)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1819
+ selectedServerKeys = [...promptedServerKeys];
1820
+ }
1706
1821
  if (selectedServerKeys.length === 0) return Result.ok(void 0);
1707
1822
  const agentOptions = filterAgentsForScope(scope).map((a) => ({
1708
1823
  value: a.value,
1709
1824
  label: a.label
1710
1825
  }));
1711
- const selectedAgents = await multiselect({
1712
- message: "Select agents to install MCP servers to",
1713
- options: agentOptions,
1714
- required: false,
1715
- initialValues: uniqueValues$1([
1716
- "cursor",
1717
- "claude-code",
1718
- "vscode"
1719
- ].filter((a) => agentOptions.some((o) => o.value === a)))
1720
- });
1721
- if (isCancel(selectedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1826
+ const defaultAgents = uniqueValues$1(DEFAULT_AGENTS$2.filter((agent) => agentOptions.some((option) => option.value === agent)));
1827
+ const configuredAgents = config.addonOptions?.mcp?.agents;
1828
+ let selectedAgents = configuredAgents?.filter((agent) => agentOptions.some((option) => option.value === agent)) ?? [];
1829
+ if (selectedAgents.length === 0 && configuredAgents === void 0) if (isSilent()) selectedAgents = defaultAgents;
1830
+ else {
1831
+ const promptedAgents = await multiselect({
1832
+ message: "Select agents to install MCP servers to",
1833
+ options: agentOptions,
1834
+ required: false,
1835
+ initialValues: defaultAgents
1836
+ });
1837
+ if (isCancel(promptedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1838
+ selectedAgents = [...promptedAgents];
1839
+ }
1722
1840
  if (selectedAgents.length === 0) return Result.ok(void 0);
1723
- const serversByKey = new Map(recommendedServers.map((s) => [s.key, s]));
1724
1841
  const selectedServers = [];
1725
1842
  for (const key of selectedServerKeys) {
1726
- const server = serversByKey.get(key);
1843
+ const server = allServersByKey.get(key);
1727
1844
  if (server) selectedServers.push(server);
1728
1845
  }
1729
1846
  if (selectedServers.length === 0) return Result.ok(void 0);
1730
- const installSpinner = spinner();
1847
+ const installSpinner = createSpinner();
1731
1848
  installSpinner.start("Installing MCP servers...");
1732
1849
  const runner = getPackageRunnerPrefix(packageManager);
1733
1850
  const globalFlags = scope === "global" ? ["-g"] : [];
1851
+ let successfulInstalls = 0;
1734
1852
  for (const server of selectedServers) {
1735
1853
  const transportFlags = server.transport ? ["-t", server.transport] : [];
1736
1854
  const headerFlags = (server.headers ?? []).flatMap((h) => ["--header", h]);
1737
- const agentFlags = selectedAgents.flatMap((a) => ["-a", a]);
1855
+ const agentFlags = selectedAgents.flatMap((agent) => ["-a", agent]);
1738
1856
  const args = [
1739
1857
  ...runner,
1740
1858
  "add-mcp@latest",
@@ -1759,12 +1877,22 @@ async function setupMcp(config) {
1759
1877
  message: `Failed to install MCP server '${server.name}': ${e instanceof Error ? e.message : String(e)}`,
1760
1878
  cause: e
1761
1879
  })
1762
- })).isErr()) log.warn(pc.yellow(`Warning: Could not install MCP server '${server.name}'`));
1880
+ })).isErr()) {
1881
+ cliLog.warn(pc.yellow(`Warning: Could not install MCP server '${server.name}'`));
1882
+ continue;
1883
+ }
1884
+ successfulInstalls += 1;
1885
+ }
1886
+ if (successfulInstalls === 0) {
1887
+ installSpinner.stop(pc.red("Failed to install MCP servers"));
1888
+ return Result.err(new AddonSetupError({
1889
+ addon: "mcp",
1890
+ message: `Failed to install all requested MCP servers: ${selectedServers.map((server) => server.name).join(", ")}`
1891
+ }));
1763
1892
  }
1764
- installSpinner.stop("MCP servers installed");
1893
+ installSpinner.stop(successfulInstalls === selectedServers.length ? "MCP servers installed" : "MCP servers installed with warnings");
1765
1894
  return Result.ok(void 0);
1766
1895
  }
1767
-
1768
1896
  //#endregion
1769
1897
  //#region src/helpers/addons/oxlint-setup.ts
1770
1898
  async function setupOxlint(projectDir, packageManager) {
@@ -1784,9 +1912,9 @@ async function setupOxlint(projectDir, packageManager) {
1784
1912
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1785
1913
  }
1786
1914
  if (shouldSkipExternalCommands()) return;
1787
- const s = spinner();
1788
- s.start("Initializing oxlint and oxfmt...");
1915
+ const s = createSpinner();
1789
1916
  try {
1917
+ s.start("Initializing oxlint and oxfmt...");
1790
1918
  const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
1791
1919
  await $({
1792
1920
  cwd: projectDir,
@@ -1810,62 +1938,75 @@ async function setupOxlint(projectDir, packageManager) {
1810
1938
  })
1811
1939
  });
1812
1940
  }
1813
-
1814
1941
  //#endregion
1815
1942
  //#region src/helpers/addons/ruler-setup.ts
1943
+ const DEFAULT_ASSISTANTS = [
1944
+ "agentsmd",
1945
+ "claude",
1946
+ "codex",
1947
+ "cursor"
1948
+ ];
1816
1949
  async function setupRuler(config) {
1817
1950
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
1818
1951
  const { packageManager, projectDir } = config;
1819
- log.info("Setting up Ruler...");
1952
+ cliLog.info("Setting up Ruler...");
1820
1953
  const rulerDir = path.join(projectDir, ".ruler");
1821
1954
  if (!await fs.pathExists(rulerDir)) {
1822
- log.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
1955
+ cliLog.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
1823
1956
  return Result.ok(void 0);
1824
1957
  }
1825
- const selectedEditors = await autocompleteMultiselect({
1826
- message: "Select AI assistants for Ruler",
1827
- options: Object.entries({
1828
- agentsmd: { label: "Agents.md" },
1829
- aider: { label: "Aider" },
1830
- amazonqcli: { label: "Amazon Q CLI" },
1831
- amp: { label: "AMP" },
1832
- antigravity: { label: "Antigravity" },
1833
- augmentcode: { label: "AugmentCode" },
1834
- claude: { label: "Claude Code" },
1835
- cline: { label: "Cline" },
1836
- codex: { label: "OpenAI Codex CLI" },
1837
- copilot: { label: "GitHub Copilot" },
1838
- crush: { label: "Crush" },
1839
- cursor: { label: "Cursor" },
1840
- factory: { label: "Factory" },
1841
- firebase: { label: "Firebase Studio" },
1842
- firebender: { label: "Firebender" },
1843
- "gemini-cli": { label: "Gemini CLI" },
1844
- goose: { label: "Goose" },
1845
- jules: { label: "Jules" },
1846
- junie: { label: "Junie" },
1847
- kilocode: { label: "Kilo Code" },
1848
- kiro: { label: "Kiro" },
1849
- mistral: { label: "Mistral" },
1850
- opencode: { label: "OpenCode" },
1851
- openhands: { label: "Open Hands" },
1852
- pi: { label: "Pi" },
1853
- qwen: { label: "Qwen" },
1854
- roo: { label: "RooCode" },
1855
- trae: { label: "Trae AI" },
1856
- warp: { label: "Warp" },
1857
- windsurf: { label: "Windsurf" },
1858
- zed: { label: "Zed" }
1859
- }).map(([key, v]) => ({
1860
- value: key,
1861
- label: v.label
1862
- })),
1863
- required: false
1864
- });
1865
- if (isCancel(selectedEditors)) return userCancelled("Operation cancelled");
1958
+ const EDITORS = {
1959
+ agentsmd: { label: "Agents.md" },
1960
+ aider: { label: "Aider" },
1961
+ amazonqcli: { label: "Amazon Q CLI" },
1962
+ amp: { label: "AMP" },
1963
+ antigravity: { label: "Antigravity" },
1964
+ augmentcode: { label: "AugmentCode" },
1965
+ claude: { label: "Claude Code" },
1966
+ cline: { label: "Cline" },
1967
+ codex: { label: "OpenAI Codex CLI" },
1968
+ copilot: { label: "GitHub Copilot" },
1969
+ crush: { label: "Crush" },
1970
+ cursor: { label: "Cursor" },
1971
+ factory: { label: "Factory" },
1972
+ firebase: { label: "Firebase Studio" },
1973
+ firebender: { label: "Firebender" },
1974
+ "gemini-cli": { label: "Gemini CLI" },
1975
+ goose: { label: "Goose" },
1976
+ "jetbrains-ai": { label: "JetBrains AI" },
1977
+ jules: { label: "Jules" },
1978
+ junie: { label: "Junie" },
1979
+ kilocode: { label: "Kilo Code" },
1980
+ kiro: { label: "Kiro" },
1981
+ mistral: { label: "Mistral" },
1982
+ opencode: { label: "OpenCode" },
1983
+ openhands: { label: "Open Hands" },
1984
+ pi: { label: "Pi" },
1985
+ qwen: { label: "Qwen" },
1986
+ roo: { label: "RooCode" },
1987
+ trae: { label: "Trae AI" },
1988
+ warp: { label: "Warp" },
1989
+ windsurf: { label: "Windsurf" },
1990
+ zed: { label: "Zed" }
1991
+ };
1992
+ const configuredAssistants = config.addonOptions?.ruler?.assistants;
1993
+ let selectedEditors = configuredAssistants ? [...configuredAssistants] : [];
1994
+ if (selectedEditors.length === 0 && configuredAssistants === void 0) if (isSilent()) selectedEditors = [...DEFAULT_ASSISTANTS];
1995
+ else {
1996
+ const promptSelection = await autocompleteMultiselect({
1997
+ message: "Select AI assistants for Ruler",
1998
+ options: Object.entries(EDITORS).map(([key, v]) => ({
1999
+ value: key,
2000
+ label: v.label
2001
+ })),
2002
+ required: false
2003
+ });
2004
+ if (isCancel(promptSelection)) return userCancelled("Operation cancelled");
2005
+ selectedEditors = [...promptSelection];
2006
+ }
1866
2007
  if (selectedEditors.length === 0) {
1867
- log.info("No AI assistants selected. To apply rules later, run:");
1868
- log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2008
+ cliLog.info("No AI assistants selected. To apply rules later, run:");
2009
+ cliLog.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
1869
2010
  return Result.ok(void 0);
1870
2011
  }
1871
2012
  const configFile = path.join(rulerDir, "ruler.toml");
@@ -1874,7 +2015,7 @@ async function setupRuler(config) {
1874
2015
  updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
1875
2016
  await fs.writeFile(configFile, updatedConfig);
1876
2017
  await addRulerScriptToPackageJson(projectDir, packageManager);
1877
- const s = spinner();
2018
+ const s = createSpinner();
1878
2019
  s.start("Applying rules with Ruler...");
1879
2020
  const applyResult = await Result.tryPromise({
1880
2021
  try: async () => {
@@ -1900,7 +2041,7 @@ async function setupRuler(config) {
1900
2041
  async function addRulerScriptToPackageJson(projectDir, packageManager) {
1901
2042
  const rootPackageJsonPath = path.join(projectDir, "package.json");
1902
2043
  if (!await fs.pathExists(rootPackageJsonPath)) {
1903
- log.warn("Root package.json not found, skipping ruler:apply script addition");
2044
+ cliLog.warn("Root package.json not found, skipping ruler:apply script addition");
1904
2045
  return;
1905
2046
  }
1906
2047
  const packageJson = await fs.readJson(rootPackageJsonPath);
@@ -1909,7 +2050,6 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
1909
2050
  packageJson.scripts["ruler:apply"] = rulerApplyCommand;
1910
2051
  await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
1911
2052
  }
1912
-
1913
2053
  //#endregion
1914
2054
  //#region src/helpers/addons/skills-setup.ts
1915
2055
  const SKILL_SOURCES = {
@@ -1920,6 +2060,7 @@ const SKILL_SOURCES = {
1920
2060
  "vercel-labs/next-skills": { label: "Next.js Best Practices" },
1921
2061
  "nuxt/ui": { label: "Nuxt UI" },
1922
2062
  "heroui-inc/heroui": { label: "HeroUI Native" },
2063
+ "shadcn/ui": { label: "shadcn/ui" },
1923
2064
  "better-auth/skills": { label: "Better Auth" },
1924
2065
  "clerk/skills": { label: "Clerk" },
1925
2066
  "neondatabase/agent-skills": { label: "Neon Database" },
@@ -2034,6 +2175,12 @@ const AVAILABLE_AGENTS = [
2034
2175
  label: "MCPJam"
2035
2176
  }
2036
2177
  ];
2178
+ const DEFAULT_SCOPE = "project";
2179
+ const DEFAULT_AGENTS$1 = [
2180
+ "cursor",
2181
+ "claude-code",
2182
+ "github-copilot"
2183
+ ];
2037
2184
  function hasReactBasedFrontend(frontend) {
2038
2185
  return frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
2039
2186
  }
@@ -2043,7 +2190,10 @@ function hasNativeFrontend(frontend) {
2043
2190
  function getRecommendedSourceKeys(config) {
2044
2191
  const sources = [];
2045
2192
  const { frontend, backend, dbSetup, auth, examples, addons, orm } = config;
2046
- if (hasReactBasedFrontend(frontend)) sources.push("vercel-labs/agent-skills");
2193
+ if (hasReactBasedFrontend(frontend)) {
2194
+ sources.push("vercel-labs/agent-skills");
2195
+ sources.push("shadcn/ui");
2196
+ }
2047
2197
  if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
2048
2198
  if (frontend.includes("nuxt")) sources.push("nuxt/ui");
2049
2199
  if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
@@ -2079,6 +2229,7 @@ const CURATED_SKILLS_BY_SOURCE = {
2079
2229
  "vercel-labs/next-skills": () => ["next-best-practices", "next-cache-components"],
2080
2230
  "nuxt/ui": () => ["nuxt-ui"],
2081
2231
  "heroui-inc/heroui": () => ["heroui-native"],
2232
+ "shadcn/ui": () => ["shadcn"],
2082
2233
  "better-auth/skills": () => ["better-auth-best-practices"],
2083
2234
  "clerk/skills": (config) => {
2084
2235
  const skills = [
@@ -2145,11 +2296,15 @@ async function setupSkills(config) {
2145
2296
  const btsConfig = await readBtsConfig(projectDir);
2146
2297
  const fullConfig = btsConfig ? {
2147
2298
  ...config,
2148
- addons: btsConfig.addons ?? config.addons
2299
+ addons: btsConfig.addons ?? config.addons,
2300
+ addonOptions: btsConfig.addonOptions ?? config.addonOptions
2149
2301
  } : config;
2150
2302
  const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
2151
- if (recommendedSourceKeys.length === 0) return Result.ok(void 0);
2152
- const skillOptions = uniqueValues(recommendedSourceKeys).flatMap((sourceKey) => {
2303
+ const skillsOptions = fullConfig.addonOptions?.skills;
2304
+ const configuredSourceKeys = uniqueValues((skillsOptions?.selections ?? []).map((selection) => selection.source));
2305
+ const sourceKeys = uniqueValues([...recommendedSourceKeys, ...configuredSourceKeys]);
2306
+ if (sourceKeys.length === 0) return Result.ok(void 0);
2307
+ const skillOptions = sourceKeys.flatMap((sourceKey) => {
2153
2308
  const source = SKILL_SOURCES[sourceKey];
2154
2309
  return getCuratedSkillNamesForSourceKey(sourceKey, fullConfig).map((skillName) => ({
2155
2310
  value: `${sourceKey}::${skillName}`,
@@ -2158,39 +2313,54 @@ async function setupSkills(config) {
2158
2313
  }));
2159
2314
  });
2160
2315
  if (skillOptions.length === 0) return Result.ok(void 0);
2161
- const scope = await select({
2162
- message: "Where should skills be installed?",
2163
- options: [{
2164
- value: "project",
2165
- label: "Project",
2166
- hint: "Writes to project config files (recommended for teams)"
2167
- }, {
2168
- value: "global",
2169
- label: "Global",
2170
- hint: "Writes to user-level config files (personal machine)"
2171
- }],
2172
- initialValue: "project"
2173
- });
2174
- if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2175
- const selectedSkills = await multiselect({
2176
- message: "Select skills to install",
2177
- options: skillOptions,
2178
- required: false,
2179
- initialValues: skillOptions.map((opt) => opt.value)
2180
- });
2181
- if (isCancel(selectedSkills)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2316
+ let scope = skillsOptions?.scope;
2317
+ if (!scope) if (isSilent()) scope = DEFAULT_SCOPE;
2318
+ else {
2319
+ const selectedScope = await select({
2320
+ message: "Where should skills be installed?",
2321
+ options: [{
2322
+ value: "project",
2323
+ label: "Project",
2324
+ hint: "Writes to project config files (recommended for teams)"
2325
+ }, {
2326
+ value: "global",
2327
+ label: "Global",
2328
+ hint: "Writes to user-level config files (personal machine)"
2329
+ }],
2330
+ initialValue: DEFAULT_SCOPE
2331
+ });
2332
+ if (isCancel(selectedScope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2333
+ scope = selectedScope;
2334
+ }
2335
+ const allSkillValues = skillOptions.map((opt) => opt.value);
2336
+ const configuredSelections = skillsOptions?.selections;
2337
+ let selectedSkills;
2338
+ if (configuredSelections !== void 0) selectedSkills = configuredSelections.flatMap((selection) => selection.skills.map((skill) => `${selection.source}::${skill}`));
2339
+ else if (isSilent()) selectedSkills = allSkillValues;
2340
+ else {
2341
+ const promptedSkills = await multiselect({
2342
+ message: "Select skills to install",
2343
+ options: skillOptions,
2344
+ required: false,
2345
+ initialValues: allSkillValues
2346
+ });
2347
+ if (isCancel(promptedSkills)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2348
+ selectedSkills = promptedSkills;
2349
+ }
2182
2350
  if (selectedSkills.length === 0) return Result.ok(void 0);
2183
- const selectedAgents = await multiselect({
2184
- message: "Select agents to install skills to",
2185
- options: AVAILABLE_AGENTS,
2186
- required: false,
2187
- initialValues: [
2188
- "cursor",
2189
- "claude-code",
2190
- "github-copilot"
2191
- ]
2192
- });
2193
- if (isCancel(selectedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2351
+ const configuredAgents = skillsOptions?.agents;
2352
+ let selectedAgents = configuredAgents ? [...configuredAgents] : [];
2353
+ if (selectedAgents.length === 0 && configuredAgents === void 0) if (isSilent()) selectedAgents = [...DEFAULT_AGENTS$1];
2354
+ else {
2355
+ const promptedAgents = await multiselect({
2356
+ message: "Select agents to install skills to",
2357
+ options: AVAILABLE_AGENTS,
2358
+ required: false,
2359
+ initialValues: [...DEFAULT_AGENTS$1]
2360
+ });
2361
+ if (isCancel(promptedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2362
+ selectedAgents = [...promptedAgents];
2363
+ }
2194
2364
  if (selectedAgents.length === 0) return Result.ok(void 0);
2195
2365
  const skillsBySource = {};
2196
2366
  for (const skillKey of selectedSkills) {
@@ -2198,42 +2368,50 @@ async function setupSkills(config) {
2198
2368
  if (!skillsBySource[source]) skillsBySource[source] = [];
2199
2369
  skillsBySource[source].push(skillName);
2200
2370
  }
2201
- const installSpinner = spinner();
2371
+ const installSpinner = createSpinner();
2202
2372
  installSpinner.start("Installing skills...");
2203
- const agentFlags = selectedAgents.map((a) => `-a ${a}`).join(" ");
2204
- const globalFlag = scope === "global" ? "-g" : "";
2205
- for (const [source, skills] of Object.entries(skillsBySource)) {
2206
- const skillFlags = skills.map((s) => `-s ${s}`).join(" ");
2207
- if ((await Result.tryPromise({
2208
- try: async () => {
2209
- const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${globalFlag} ${skillFlags} ${agentFlags} -y`);
2210
- await $({
2211
- cwd: projectDir,
2212
- env: { CI: "true" }
2213
- })`${args}`;
2214
- },
2215
- catch: (e) => new AddonSetupError({
2216
- addon: "skills",
2217
- message: `Failed to install skills from ${source}: ${e instanceof Error ? e.message : String(e)}`,
2218
- cause: e
2219
- })
2220
- })).isErr()) log.warn(pc.yellow(`Warning: Could not install skills from ${source}`));
2221
- }
2373
+ const runner = getPackageRunnerPrefix(packageManager);
2374
+ const globalFlags = scope === "global" ? ["-g"] : [];
2375
+ for (const [source, skills] of Object.entries(skillsBySource)) if ((await Result.tryPromise({
2376
+ try: async () => {
2377
+ const args = [
2378
+ ...runner,
2379
+ "skills@latest",
2380
+ "add",
2381
+ source,
2382
+ ...globalFlags,
2383
+ "--skill",
2384
+ ...skills,
2385
+ "--agent",
2386
+ ...selectedAgents,
2387
+ "-y"
2388
+ ];
2389
+ await $({
2390
+ cwd: projectDir,
2391
+ env: { CI: "true" }
2392
+ })`${args}`;
2393
+ },
2394
+ catch: (e) => new AddonSetupError({
2395
+ addon: "skills",
2396
+ message: `Failed to install skills from ${source}: ${e instanceof Error ? e.message : String(e)}`,
2397
+ cause: e
2398
+ })
2399
+ })).isErr()) cliLog.warn(pc.yellow(`Warning: Could not install skills from ${source}`));
2222
2400
  installSpinner.stop("Skills installed");
2223
2401
  return Result.ok(void 0);
2224
2402
  }
2225
-
2226
2403
  //#endregion
2227
2404
  //#region src/helpers/addons/starlight-setup.ts
2228
2405
  async function setupStarlight(config) {
2229
2406
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2230
2407
  const { packageManager, projectDir } = config;
2231
- const s = spinner();
2408
+ const s = createSpinner();
2232
2409
  s.start("Setting up Starlight docs...");
2233
2410
  const args = getPackageExecutionArgs(packageManager, `create-astro@latest ${[
2234
2411
  "docs",
2235
2412
  "--template",
2236
2413
  "starlight",
2414
+ "--yes",
2237
2415
  "--no-install",
2238
2416
  "--add",
2239
2417
  "tailwind",
@@ -2262,39 +2440,53 @@ async function setupStarlight(config) {
2262
2440
  s.stop("Starlight docs setup successfully!");
2263
2441
  return Result.ok(void 0);
2264
2442
  }
2265
-
2266
2443
  //#endregion
2267
2444
  //#region src/helpers/addons/tauri-setup.ts
2268
- async function setupTauri(config) {
2269
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2445
+ function buildTauriInitArgs(config) {
2270
2446
  const { packageManager, frontend, projectDir } = config;
2271
- const s = spinner();
2272
- const clientPackageDir = path.join(projectDir, "apps/web");
2273
- if (!await fs.pathExists(clientPackageDir)) return Result.ok(void 0);
2274
- s.start("Setting up Tauri desktop app support...");
2275
2447
  const hasReactRouter = frontend.includes("react-router");
2276
2448
  const hasNuxt = frontend.includes("nuxt");
2277
2449
  const hasSvelte = frontend.includes("svelte");
2278
2450
  const hasNext = frontend.includes("next");
2279
2451
  const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
2280
2452
  const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
2281
- const tauriArgs = [
2453
+ return [
2454
+ ...getPackageRunnerPrefix(packageManager),
2282
2455
  "@tauri-apps/cli@latest",
2283
2456
  "init",
2284
- `--app-name=${path.basename(projectDir)}`,
2285
- `--window-title=${path.basename(projectDir)}`,
2286
- `--frontend-dist=${frontendDist}`,
2287
- `--dev-url=${devUrl}`,
2288
- `--before-dev-command=${packageManager} run dev`,
2289
- `--before-build-command=${packageManager} run build`
2457
+ "--ci",
2458
+ "--app-name",
2459
+ path.basename(projectDir),
2460
+ "--window-title",
2461
+ path.basename(projectDir),
2462
+ "--frontend-dist",
2463
+ frontendDist,
2464
+ "--dev-url",
2465
+ devUrl,
2466
+ "--before-dev-command",
2467
+ `${packageManager} run dev`,
2468
+ "--before-build-command",
2469
+ `${packageManager} run build`
2290
2470
  ];
2291
- const prefix = getPackageRunnerPrefix(packageManager);
2471
+ }
2472
+ async function setupTauri(config) {
2473
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
2474
+ const { packageManager, frontend, projectDir } = config;
2475
+ const s = createSpinner();
2476
+ const clientPackageDir = path.join(projectDir, "apps/web");
2477
+ if (!await fs.pathExists(clientPackageDir)) return Result.ok(void 0);
2478
+ s.start("Setting up Tauri desktop app support...");
2479
+ const [command, ...args] = buildTauriInitArgs({
2480
+ packageManager,
2481
+ frontend,
2482
+ projectDir
2483
+ });
2292
2484
  const result = await Result.tryPromise({
2293
2485
  try: async () => {
2294
- await $({
2486
+ await execa(command, args, {
2295
2487
  cwd: clientPackageDir,
2296
2488
  env: { CI: "true" }
2297
- })`${[...prefix, ...tauriArgs]}`;
2489
+ });
2298
2490
  },
2299
2491
  catch: (e) => new AddonSetupError({
2300
2492
  addon: "tauri",
@@ -2309,7 +2501,6 @@ async function setupTauri(config) {
2309
2501
  s.stop("Tauri desktop app support configured successfully!");
2310
2502
  return Result.ok(void 0);
2311
2503
  }
2312
-
2313
2504
  //#endregion
2314
2505
  //#region src/helpers/addons/tui-setup.ts
2315
2506
  const TEMPLATES$1 = {
@@ -2326,20 +2517,36 @@ const TEMPLATES$1 = {
2326
2517
  hint: "SolidJS-based OpenTUI template"
2327
2518
  }
2328
2519
  };
2520
+ const DEFAULT_TEMPLATE$1 = "core";
2521
+ const TUI_LOCKFILES = [
2522
+ "bun.lock",
2523
+ "package-lock.json",
2524
+ "pnpm-lock.yaml",
2525
+ "yarn.lock"
2526
+ ];
2527
+ function resolveTuiTemplate(config) {
2528
+ const configuredTemplate = config.addonOptions?.opentui?.template;
2529
+ if (configuredTemplate) return configuredTemplate;
2530
+ if (isSilent()) return DEFAULT_TEMPLATE$1;
2531
+ }
2329
2532
  async function setupTui(config) {
2330
2533
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2331
2534
  const { packageManager, projectDir } = config;
2332
- log.info("Setting up OpenTUI...");
2333
- const template = await select({
2334
- message: "Choose a template",
2335
- options: Object.entries(TEMPLATES$1).map(([key, template]) => ({
2336
- value: key,
2337
- label: template.label,
2338
- hint: template.hint
2339
- })),
2340
- initialValue: "core"
2341
- });
2342
- if (isCancel(template)) return userCancelled("Operation cancelled");
2535
+ cliLog.info("Setting up OpenTUI...");
2536
+ let template = resolveTuiTemplate(config);
2537
+ if (!template) {
2538
+ const selectedTemplate = await select({
2539
+ message: "Choose a template",
2540
+ options: Object.entries(TEMPLATES$1).map(([key, templateOption]) => ({
2541
+ value: key,
2542
+ label: templateOption.label,
2543
+ hint: templateOption.hint
2544
+ })),
2545
+ initialValue: DEFAULT_TEMPLATE$1
2546
+ });
2547
+ if (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
2548
+ template = selectedTemplate;
2549
+ }
2343
2550
  const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
2344
2551
  const appsDir = path.join(projectDir, "apps");
2345
2552
  const ensureDirResult = await Result.tryPromise({
@@ -2351,7 +2558,7 @@ async function setupTui(config) {
2351
2558
  })
2352
2559
  });
2353
2560
  if (ensureDirResult.isErr()) return ensureDirResult;
2354
- const s = spinner();
2561
+ const s = createSpinner();
2355
2562
  s.start("Running OpenTUI create command...");
2356
2563
  const initResult = await Result.tryPromise({
2357
2564
  try: async () => {
@@ -2370,13 +2577,50 @@ async function setupTui(config) {
2370
2577
  }
2371
2578
  });
2372
2579
  if (initResult.isErr()) {
2373
- log.error(pc.red("Failed to set up OpenTUI"));
2580
+ cliLog.error(pc.red("Failed to set up OpenTUI"));
2374
2581
  return initResult;
2375
2582
  }
2583
+ const postProcessResult = await postProcessTuiWorkspace(path.join(appsDir, "tui"));
2584
+ if (postProcessResult.isErr()) {
2585
+ s.stop(pc.yellow("OpenTUI setup completed with warnings"));
2586
+ cliLog.warn(pc.yellow("OpenTUI setup completed but workspace normalization had warnings"));
2587
+ return postProcessResult;
2588
+ }
2376
2589
  s.stop("OpenTUI setup complete!");
2377
2590
  return Result.ok(void 0);
2378
2591
  }
2379
-
2592
+ async function postProcessTuiWorkspace(tuiDir) {
2593
+ const packageJsonPath = path.join(tuiDir, "package.json");
2594
+ const packageJsonResult = await Result.tryPromise({
2595
+ try: async () => {
2596
+ const packageJson = await fs.readJson(packageJsonPath);
2597
+ packageJson.scripts = packageJson.scripts || {};
2598
+ if (!packageJson.scripts["check-types"]) packageJson.scripts["check-types"] = "tsc --noEmit";
2599
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2600
+ },
2601
+ catch: (e) => new AddonSetupError({
2602
+ addon: "tui",
2603
+ message: `Failed to normalize OpenTUI package.json: ${e instanceof Error ? e.message : String(e)}`,
2604
+ cause: e
2605
+ })
2606
+ });
2607
+ if (packageJsonResult.isErr()) return packageJsonResult;
2608
+ for (const lockfile of TUI_LOCKFILES) {
2609
+ const lockfilePath = path.join(tuiDir, lockfile);
2610
+ const removeLockfileResult = await Result.tryPromise({
2611
+ try: async () => {
2612
+ if (await fs.pathExists(lockfilePath)) await fs.remove(lockfilePath);
2613
+ },
2614
+ catch: (e) => new AddonSetupError({
2615
+ addon: "tui",
2616
+ message: `Failed to remove nested OpenTUI lockfile '${lockfile}': ${e instanceof Error ? e.message : String(e)}`,
2617
+ cause: e
2618
+ })
2619
+ });
2620
+ if (removeLockfileResult.isErr()) return removeLockfileResult;
2621
+ }
2622
+ return Result.ok(void 0);
2623
+ }
2380
2624
  //#endregion
2381
2625
  //#region src/helpers/addons/ultracite-setup.ts
2382
2626
  const LINTERS = {
@@ -2435,6 +2679,10 @@ const HOOKS = {
2435
2679
  windsurf: { label: "Windsurf" },
2436
2680
  claude: { label: "Claude" }
2437
2681
  };
2682
+ const DEFAULT_LINTER = "biome";
2683
+ const DEFAULT_EDITORS = ["vscode", "cursor"];
2684
+ const DEFAULT_AGENTS = ["claude", "codex"];
2685
+ const DEFAULT_HOOKS = [];
2438
2686
  function getFrameworksFromFrontend(frontend) {
2439
2687
  const frameworkMap = {
2440
2688
  "tanstack-router": "react",
@@ -2452,70 +2700,7 @@ function getFrameworksFromFrontend(frontend) {
2452
2700
  for (const f of frontend) if (f !== "none" && frameworkMap[f]) frameworks.add(frameworkMap[f]);
2453
2701
  return Array.from(frameworks);
2454
2702
  }
2455
- async function setupUltracite(config, gitHooks) {
2456
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2457
- const { packageManager, projectDir, frontend } = config;
2458
- log.info("Setting up Ultracite...");
2459
- let result;
2460
- const groupResult = await Result.tryPromise({
2461
- try: async () => {
2462
- return await group({
2463
- linter: () => select({
2464
- message: "Choose linter/formatter",
2465
- options: Object.entries(LINTERS).map(([key, linter]) => ({
2466
- value: key,
2467
- label: linter.label,
2468
- hint: linter.hint
2469
- })),
2470
- initialValue: "biome"
2471
- }),
2472
- editors: () => multiselect({
2473
- message: "Choose editors",
2474
- options: Object.entries(EDITORS).map(([key, editor]) => ({
2475
- value: key,
2476
- label: editor.label
2477
- })),
2478
- required: true
2479
- }),
2480
- agents: () => multiselect({
2481
- message: "Choose agents",
2482
- options: Object.entries(AGENTS).map(([key, agent]) => ({
2483
- value: key,
2484
- label: agent.label
2485
- })),
2486
- required: true
2487
- }),
2488
- hooks: () => multiselect({
2489
- message: "Choose hooks",
2490
- options: Object.entries(HOOKS).map(([key, hook]) => ({
2491
- value: key,
2492
- label: hook.label
2493
- }))
2494
- })
2495
- }, { onCancel: () => {
2496
- throw new UserCancelledError({ message: "Operation cancelled" });
2497
- } });
2498
- },
2499
- catch: (e) => {
2500
- if (e instanceof UserCancelledError) return e;
2501
- return new AddonSetupError({
2502
- addon: "ultracite",
2503
- message: `Failed to get user preferences: ${e instanceof Error ? e.message : String(e)}`,
2504
- cause: e
2505
- });
2506
- }
2507
- });
2508
- if (groupResult.isErr()) {
2509
- if (UserCancelledError.is(groupResult.error)) return userCancelled(groupResult.error.message);
2510
- log.error(pc.red("Failed to set up Ultracite"));
2511
- return groupResult;
2512
- }
2513
- result = groupResult.value;
2514
- const linter = result.linter;
2515
- const editors = result.editors;
2516
- const agents = result.agents;
2517
- const hooks = result.hooks;
2518
- const frameworks = getFrameworksFromFrontend(frontend);
2703
+ function buildUltraciteInitArgs({ packageManager, linter, frameworks, editors, agents, hooks, gitHooks }) {
2519
2704
  const ultraciteArgs = [
2520
2705
  "init",
2521
2706
  "--pm",
@@ -2528,12 +2713,105 @@ async function setupUltracite(config, gitHooks) {
2528
2713
  if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
2529
2714
  if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
2530
2715
  if (gitHooks.length > 0) {
2531
- const integrations = [...gitHooks];
2532
- if (gitHooks.includes("husky")) integrations.push("lint-staged");
2716
+ const integrations = gitHooks.includes("husky") ? [...new Set([...gitHooks, "lint-staged"])] : gitHooks;
2533
2717
  ultraciteArgs.push("--integrations", ...integrations);
2534
2718
  }
2535
- const args = getPackageExecutionArgs(packageManager, `ultracite@latest ${ultraciteArgs.join(" ")} --skip-install`);
2536
- const s = spinner();
2719
+ return [
2720
+ ...getPackageRunnerPrefix(packageManager),
2721
+ "ultracite@latest",
2722
+ ...ultraciteArgs,
2723
+ "--skip-install",
2724
+ "--quiet"
2725
+ ];
2726
+ }
2727
+ async function setupUltracite(config, gitHooks) {
2728
+ if (shouldSkipExternalCommands()) return Result.ok(void 0);
2729
+ const { packageManager, projectDir, frontend } = config;
2730
+ cliLog.info("Setting up Ultracite...");
2731
+ const configuredOptions = config.addonOptions?.ultracite;
2732
+ let linter = configuredOptions?.linter;
2733
+ let editors = configuredOptions?.editors;
2734
+ let agents = configuredOptions?.agents;
2735
+ let hooks = configuredOptions?.hooks;
2736
+ if (!linter || !editors || !agents || !hooks) if (isSilent()) {
2737
+ linter = linter ?? DEFAULT_LINTER;
2738
+ editors = editors ?? [...DEFAULT_EDITORS];
2739
+ agents = agents ?? [...DEFAULT_AGENTS];
2740
+ hooks = hooks ?? [...DEFAULT_HOOKS];
2741
+ } else {
2742
+ const groupResult = await Result.tryPromise({
2743
+ try: async () => {
2744
+ return await group({
2745
+ linter: () => select({
2746
+ message: "Choose linter/formatter",
2747
+ options: Object.entries(LINTERS).map(([key, linterOption]) => ({
2748
+ value: key,
2749
+ label: linterOption.label,
2750
+ hint: linterOption.hint
2751
+ })),
2752
+ initialValue: linter ?? DEFAULT_LINTER
2753
+ }),
2754
+ editors: () => multiselect({
2755
+ message: "Choose editors",
2756
+ required: false,
2757
+ options: Object.entries(EDITORS).map(([key, editor]) => ({
2758
+ value: key,
2759
+ label: editor.label
2760
+ })),
2761
+ initialValues: editors ?? [...DEFAULT_EDITORS]
2762
+ }),
2763
+ agents: () => multiselect({
2764
+ message: "Choose agents",
2765
+ required: false,
2766
+ options: Object.entries(AGENTS).map(([key, agent]) => ({
2767
+ value: key,
2768
+ label: agent.label
2769
+ })),
2770
+ initialValues: agents ?? [...DEFAULT_AGENTS]
2771
+ }),
2772
+ hooks: () => multiselect({
2773
+ message: "Choose hooks",
2774
+ required: false,
2775
+ options: Object.entries(HOOKS).map(([key, hook]) => ({
2776
+ value: key,
2777
+ label: hook.label
2778
+ })),
2779
+ initialValues: hooks ?? [...DEFAULT_HOOKS]
2780
+ })
2781
+ }, { onCancel: () => {
2782
+ throw new UserCancelledError({ message: "Operation cancelled" });
2783
+ } });
2784
+ },
2785
+ catch: (e) => {
2786
+ if (e instanceof UserCancelledError) return e;
2787
+ return new AddonSetupError({
2788
+ addon: "ultracite",
2789
+ message: `Failed to get user preferences: ${e instanceof Error ? e.message : String(e)}`,
2790
+ cause: e
2791
+ });
2792
+ }
2793
+ });
2794
+ if (groupResult.isErr()) {
2795
+ if (UserCancelledError.is(groupResult.error)) return userCancelled(groupResult.error.message);
2796
+ cliLog.error(pc.red("Failed to set up Ultracite"));
2797
+ return groupResult;
2798
+ }
2799
+ linter = groupResult.value.linter;
2800
+ editors = groupResult.value.editors;
2801
+ agents = groupResult.value.agents;
2802
+ hooks = groupResult.value.hooks;
2803
+ }
2804
+ const frameworks = getFrameworksFromFrontend(frontend);
2805
+ const args = buildUltraciteInitArgs({
2806
+ packageManager,
2807
+ linter,
2808
+ frameworks,
2809
+ editors,
2810
+ agents,
2811
+ hooks,
2812
+ gitHooks
2813
+ });
2814
+ const s = createSpinner();
2537
2815
  s.start("Running Ultracite init command...");
2538
2816
  const initResult = await Result.tryPromise({
2539
2817
  try: async () => {
@@ -2552,13 +2830,12 @@ async function setupUltracite(config, gitHooks) {
2552
2830
  }
2553
2831
  });
2554
2832
  if (initResult.isErr()) {
2555
- log.error(pc.red("Failed to set up Ultracite"));
2833
+ cliLog.error(pc.red("Failed to set up Ultracite"));
2556
2834
  return initResult;
2557
2835
  }
2558
2836
  s.stop("Ultracite setup successfully!");
2559
2837
  return Result.ok(void 0);
2560
2838
  }
2561
-
2562
2839
  //#endregion
2563
2840
  //#region src/helpers/addons/wxt-setup.ts
2564
2841
  const TEMPLATES = {
@@ -2583,20 +2860,29 @@ const TEMPLATES = {
2583
2860
  hint: "Svelte template"
2584
2861
  }
2585
2862
  };
2863
+ const DEFAULT_TEMPLATE = "react";
2864
+ const DEFAULT_DEV_PORT = 5555;
2586
2865
  async function setupWxt(config) {
2587
2866
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2588
2867
  const { packageManager, projectDir } = config;
2589
- log.info("Setting up WXT...");
2590
- const template = await select({
2591
- message: "Choose a template",
2592
- options: Object.entries(TEMPLATES).map(([key, template]) => ({
2593
- value: key,
2594
- label: template.label,
2595
- hint: template.hint
2596
- })),
2597
- initialValue: "react"
2598
- });
2599
- if (isCancel(template)) return userCancelled("Operation cancelled");
2868
+ cliLog.info("Setting up WXT...");
2869
+ const configuredOptions = config.addonOptions?.wxt;
2870
+ let template = configuredOptions?.template;
2871
+ if (!template) if (isSilent()) template = DEFAULT_TEMPLATE;
2872
+ else {
2873
+ const selectedTemplate = await select({
2874
+ message: "Choose a template",
2875
+ options: Object.entries(TEMPLATES).map(([key, templateOption]) => ({
2876
+ value: key,
2877
+ label: templateOption.label,
2878
+ hint: templateOption.hint
2879
+ })),
2880
+ initialValue: DEFAULT_TEMPLATE
2881
+ });
2882
+ if (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
2883
+ template = selectedTemplate;
2884
+ }
2885
+ const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT;
2600
2886
  const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
2601
2887
  const appsDir = path.join(projectDir, "apps");
2602
2888
  const ensureDirResult = await Result.tryPromise({
@@ -2608,7 +2894,7 @@ async function setupWxt(config) {
2608
2894
  })
2609
2895
  });
2610
2896
  if (ensureDirResult.isErr()) return ensureDirResult;
2611
- const s = spinner();
2897
+ const s = createSpinner();
2612
2898
  s.start("Running WXT init command...");
2613
2899
  const initResult = await Result.tryPromise({
2614
2900
  try: async () => {
@@ -2627,7 +2913,7 @@ async function setupWxt(config) {
2627
2913
  }
2628
2914
  });
2629
2915
  if (initResult.isErr()) {
2630
- log.error(pc.red("Failed to set up WXT"));
2916
+ cliLog.error(pc.red("Failed to set up WXT"));
2631
2917
  return initResult;
2632
2918
  }
2633
2919
  const extensionDir = path.join(projectDir, "apps", "extension");
@@ -2637,7 +2923,7 @@ async function setupWxt(config) {
2637
2923
  if (await fs.pathExists(packageJsonPath)) {
2638
2924
  const packageJson = await fs.readJson(packageJsonPath);
2639
2925
  packageJson.name = "extension";
2640
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port 5555`;
2926
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port ${devPort}`;
2641
2927
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2642
2928
  }
2643
2929
  },
@@ -2646,11 +2932,10 @@ async function setupWxt(config) {
2646
2932
  message: `Failed to update package.json: ${e instanceof Error ? e.message : String(e)}`,
2647
2933
  cause: e
2648
2934
  })
2649
- })).isErr()) log.warn(pc.yellow("WXT setup completed but failed to update package.json"));
2935
+ })).isErr()) cliLog.warn(pc.yellow("WXT setup completed but failed to update package.json"));
2650
2936
  s.stop("WXT setup complete!");
2651
2937
  return Result.ok(void 0);
2652
2938
  }
2653
-
2654
2939
  //#endregion
2655
2940
  //#region src/helpers/addons/addons-setup.ts
2656
2941
  async function runSetup(setupFn) {
@@ -2747,7 +3032,6 @@ async function setupLefthook(projectDir) {
2747
3032
  projectDir
2748
3033
  });
2749
3034
  }
2750
-
2751
3035
  //#endregion
2752
3036
  //#region src/helpers/core/detect-project-config.ts
2753
3037
  async function detectProjectConfig(projectDir) {
@@ -2757,6 +3041,8 @@ async function detectProjectConfig(projectDir) {
2757
3041
  if (btsConfig) return {
2758
3042
  projectDir,
2759
3043
  projectName: path.basename(projectDir),
3044
+ addonOptions: btsConfig.addonOptions,
3045
+ dbSetupOptions: btsConfig.dbSetupOptions,
2760
3046
  database: btsConfig.database,
2761
3047
  orm: btsConfig.orm,
2762
3048
  backend: btsConfig.backend,
@@ -2778,12 +3064,11 @@ async function detectProjectConfig(projectDir) {
2778
3064
  });
2779
3065
  return result.isOk() ? result.value : null;
2780
3066
  }
2781
-
2782
3067
  //#endregion
2783
3068
  //#region src/helpers/core/install-dependencies.ts
2784
3069
  async function installDependencies({ projectDir, packageManager }) {
2785
3070
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2786
- const s = spinner();
3071
+ const s = createSpinner();
2787
3072
  s.start(`Running ${packageManager} install...`);
2788
3073
  const result = await Result.tryPromise({
2789
3074
  try: async () => {
@@ -2802,9 +3087,21 @@ async function installDependencies({ projectDir, packageManager }) {
2802
3087
  else s.stop(pc.red("Failed to install dependencies"));
2803
3088
  return result;
2804
3089
  }
2805
-
2806
3090
  //#endregion
2807
3091
  //#region src/helpers/core/add-handler.ts
3092
+ function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3093
+ if (!existingAddonOptions && !nextAddonOptions) return;
3094
+ const mergedAddonOptions = { ...existingAddonOptions };
3095
+ if (nextAddonOptions) for (const addonKey of Object.keys(nextAddonOptions)) {
3096
+ const existingOptionsForAddon = existingAddonOptions?.[addonKey];
3097
+ const nextOptionsForAddon = nextAddonOptions[addonKey];
3098
+ mergedAddonOptions[addonKey] = existingOptionsForAddon && nextOptionsForAddon ? {
3099
+ ...existingOptionsForAddon,
3100
+ ...nextOptionsForAddon
3101
+ } : nextOptionsForAddon;
3102
+ }
3103
+ return Object.keys(mergedAddonOptions).length > 0 ? mergedAddonOptions : void 0;
3104
+ }
2808
3105
  async function addHandler(input, options = {}) {
2809
3106
  const { silent = false } = options;
2810
3107
  return runWithContextAsync({ silent }, async () => {
@@ -2832,6 +3129,11 @@ async function addHandler(input, options = {}) {
2832
3129
  }
2833
3130
  async function addHandlerInternal(input) {
2834
3131
  const projectDir = input.projectDir || process.cwd();
3132
+ const hardeningResult = validateAgentSafePathInput(projectDir, "projectDir");
3133
+ if (hardeningResult.isErr()) return Result.err(new CLIError({
3134
+ message: hardeningResult.error.message,
3135
+ cause: hardeningResult.error
3136
+ }));
2835
3137
  if (!isSilent()) {
2836
3138
  renderTitle();
2837
3139
  intro(pc.magenta("Add addons to your Better-T-Stack project"));
@@ -2850,7 +3152,8 @@ async function addHandlerInternal(input) {
2850
3152
  projectDir
2851
3153
  });
2852
3154
  }
2853
- } else {
3155
+ } else if (isSilent()) return Result.err(new CLIError({ message: "Addons are required in silent mode. Provide them via add() or add-json." }));
3156
+ else {
2854
3157
  const promptResult = await Result.tryPromise({
2855
3158
  try: () => getAddonsToAdd(existingConfig.frontend, existingConfig.addons, existingConfig.auth),
2856
3159
  catch: (e) => {
@@ -2878,10 +3181,12 @@ async function addHandlerInternal(input) {
2878
3181
  }
2879
3182
  if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
2880
3183
  const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3184
+ const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
2881
3185
  const config = {
2882
3186
  projectName: existingConfig.projectName,
2883
3187
  projectDir,
2884
3188
  relativePath: ".",
3189
+ addonOptions: mergedAddonOptions,
2885
3190
  database: existingConfig.database,
2886
3191
  orm: existingConfig.orm,
2887
3192
  backend: existingConfig.backend,
@@ -2910,12 +3215,27 @@ async function addHandlerInternal(input) {
2910
3215
  }
2911
3216
  await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
2912
3217
  processAddonsDeps(vfs, config);
2913
- const writeResult = await writeTree({
3218
+ const tree = {
2914
3219
  root: vfs.toTree(""),
2915
3220
  fileCount: vfs.getFileCount(),
2916
3221
  directoryCount: vfs.getDirectoryCount(),
2917
3222
  config
2918
- }, projectDir);
3223
+ };
3224
+ if (input.dryRun) {
3225
+ if (!isSilent()) {
3226
+ log.success(pc.green("Dry run validation passed. No addon files were written."));
3227
+ log.info(pc.dim(`Planned addon files: ${vfs.getFileCount()}`));
3228
+ outro(pc.magenta("Dry run complete."));
3229
+ }
3230
+ return Result.ok({
3231
+ success: true,
3232
+ addedAddons: addonsToAdd,
3233
+ projectDir,
3234
+ dryRun: true,
3235
+ plannedFileCount: vfs.getFileCount()
3236
+ });
3237
+ }
3238
+ const writeResult = await writeTree(tree, projectDir);
2919
3239
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
2920
3240
  if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
2921
3241
  const setupResult = await Result.tryPromise({
@@ -2929,7 +3249,10 @@ async function addHandlerInternal(input) {
2929
3249
  }
2930
3250
  });
2931
3251
  if (setupResult.isErr()) return Result.err(setupResult.error);
2932
- await updateBtsConfig(projectDir, { addons: updatedAddons });
3252
+ await updateBtsConfig(projectDir, {
3253
+ addons: updatedAddons,
3254
+ addonOptions: config.addonOptions
3255
+ });
2933
3256
  if (input.install) {
2934
3257
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
2935
3258
  await installDependencies({
@@ -2945,10 +3268,10 @@ async function addHandlerInternal(input) {
2945
3268
  return Result.ok({
2946
3269
  success: true,
2947
3270
  addedAddons: addonsToAdd,
2948
- projectDir
3271
+ projectDir,
3272
+ plannedFileCount: vfs.getFileCount()
2949
3273
  });
2950
3274
  }
2951
-
2952
3275
  //#endregion
2953
3276
  //#region src/prompts/api.ts
2954
3277
  async function getApiChoice(Api, frontend, backend) {
@@ -2976,7 +3299,6 @@ async function getApiChoice(Api, frontend, backend) {
2976
3299
  if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
2977
3300
  return apiType;
2978
3301
  }
2979
-
2980
3302
  //#endregion
2981
3303
  //#region src/prompts/auth.ts
2982
3304
  async function getAuthChoice(auth, backend, frontend) {
@@ -3040,7 +3362,6 @@ async function getAuthChoice(auth, backend, frontend) {
3040
3362
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3041
3363
  return response;
3042
3364
  }
3043
-
3044
3365
  //#endregion
3045
3366
  //#region src/prompts/backend.ts
3046
3367
  const FULLSTACK_FRONTENDS = [
@@ -3094,7 +3415,6 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
3094
3415
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3095
3416
  return response;
3096
3417
  }
3097
-
3098
3418
  //#endregion
3099
3419
  //#region src/prompts/database.ts
3100
3420
  async function getDatabaseChoice(database, backend, runtime) {
@@ -3135,7 +3455,6 @@ async function getDatabaseChoice(database, backend, runtime) {
3135
3455
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3136
3456
  return response;
3137
3457
  }
3138
-
3139
3458
  //#endregion
3140
3459
  //#region src/prompts/database-setup.ts
3141
3460
  async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
@@ -3235,7 +3554,6 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
3235
3554
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3236
3555
  return response;
3237
3556
  }
3238
-
3239
3557
  //#endregion
3240
3558
  //#region src/prompts/examples.ts
3241
3559
  async function getExamplesChoice(examples, database, frontends, backend, api) {
@@ -3263,7 +3581,6 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
3263
3581
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3264
3582
  return response;
3265
3583
  }
3266
-
3267
3584
  //#endregion
3268
3585
  //#region src/prompts/frontend.ts
3269
3586
  async function getFrontendChoice(frontendOptions, backend, auth) {
@@ -3381,7 +3698,6 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
3381
3698
  return result;
3382
3699
  }
3383
3700
  }
3384
-
3385
3701
  //#endregion
3386
3702
  //#region src/prompts/git.ts
3387
3703
  async function getGitChoice(git) {
@@ -3393,7 +3709,6 @@ async function getGitChoice(git) {
3393
3709
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3394
3710
  return response;
3395
3711
  }
3396
-
3397
3712
  //#endregion
3398
3713
  //#region src/prompts/install.ts
3399
3714
  async function getinstallChoice(install) {
@@ -3405,7 +3720,6 @@ async function getinstallChoice(install) {
3405
3720
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3406
3721
  return response;
3407
3722
  }
3408
-
3409
3723
  //#endregion
3410
3724
  //#region src/prompts/navigable-group.ts
3411
3725
  /**
@@ -3462,7 +3776,6 @@ async function navigableGroup(prompts, opts) {
3462
3776
  setIsFirstPrompt$1(false);
3463
3777
  return results;
3464
3778
  }
3465
-
3466
3779
  //#endregion
3467
3780
  //#region src/prompts/orm.ts
3468
3781
  const ormOptions = {
@@ -3494,7 +3807,6 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
3494
3807
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3495
3808
  return response;
3496
3809
  }
3497
-
3498
3810
  //#endregion
3499
3811
  //#region src/prompts/package-manager.ts
3500
3812
  async function getPackageManagerChoice(packageManager) {
@@ -3523,7 +3835,6 @@ async function getPackageManagerChoice(packageManager) {
3523
3835
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3524
3836
  return response;
3525
3837
  }
3526
-
3527
3838
  //#endregion
3528
3839
  //#region src/prompts/payments.ts
3529
3840
  async function getPaymentsChoice(payments, auth, backend, frontends) {
@@ -3546,7 +3857,6 @@ async function getPaymentsChoice(payments, auth, backend, frontends) {
3546
3857
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3547
3858
  return response;
3548
3859
  }
3549
-
3550
3860
  //#endregion
3551
3861
  //#region src/prompts/runtime.ts
3552
3862
  async function getRuntimeChoice(runtime, backend) {
@@ -3574,7 +3884,6 @@ async function getRuntimeChoice(runtime, backend) {
3574
3884
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3575
3885
  return response;
3576
3886
  }
3577
-
3578
3887
  //#endregion
3579
3888
  //#region src/prompts/server-deploy.ts
3580
3889
  async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
@@ -3584,7 +3893,6 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
3584
3893
  if (runtime === "workers") return "cloudflare";
3585
3894
  return "none";
3586
3895
  }
3587
-
3588
3896
  //#endregion
3589
3897
  //#region src/prompts/web-deploy.ts
3590
3898
  function hasWebFrontend(frontends) {
@@ -3618,7 +3926,6 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
3618
3926
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3619
3927
  return response;
3620
3928
  }
3621
-
3622
3929
  //#endregion
3623
3930
  //#region src/prompts/config-prompts.ts
3624
3931
  async function gatherConfig(flags, projectName, projectDir, relativePath) {
@@ -3626,6 +3933,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
3626
3933
  projectName,
3627
3934
  projectDir,
3628
3935
  relativePath,
3936
+ addonOptions: flags.addonOptions,
3937
+ dbSetupOptions: flags.dbSetupOptions,
3629
3938
  frontend: flags.frontend ?? [...DEFAULT_CONFIG.frontend],
3630
3939
  backend: flags.backend ?? DEFAULT_CONFIG.backend,
3631
3940
  runtime: flags.runtime ?? DEFAULT_CONFIG.runtime,
@@ -3667,6 +3976,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
3667
3976
  projectName,
3668
3977
  projectDir,
3669
3978
  relativePath,
3979
+ addonOptions: flags.addonOptions,
3980
+ dbSetupOptions: flags.dbSetupOptions,
3670
3981
  frontend: result.frontend,
3671
3982
  backend: result.backend,
3672
3983
  runtime: result.runtime,
@@ -3685,7 +3996,6 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
3685
3996
  serverDeploy: result.serverDeploy
3686
3997
  };
3687
3998
  }
3688
-
3689
3999
  //#endregion
3690
4000
  //#region src/prompts/project-name.ts
3691
4001
  function isPathWithinCwd$1(targetPath) {
@@ -3735,7 +4045,6 @@ async function getProjectName(initialName) {
3735
4045
  }
3736
4046
  return projectPath;
3737
4047
  }
3738
-
3739
4048
  //#endregion
3740
4049
  //#region src/utils/telemetry.ts
3741
4050
  /**
@@ -3751,7 +4060,6 @@ function isTelemetryEnabled() {
3751
4060
  if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
3752
4061
  return true;
3753
4062
  }
3754
-
3755
4063
  //#endregion
3756
4064
  //#region src/utils/analytics.ts
3757
4065
  const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
@@ -3778,7 +4086,6 @@ async function trackProjectCreation(config, disableAnalytics = false) {
3778
4086
  catch: () => void 0
3779
4087
  });
3780
4088
  }
3781
-
3782
4089
  //#endregion
3783
4090
  //#region src/utils/display-config.ts
3784
4091
  function displayConfig(config) {
@@ -3821,7 +4128,6 @@ function displayConfig(config) {
3821
4128
  if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
3822
4129
  return configDisplay.join("\n");
3823
4130
  }
3824
-
3825
4131
  //#endregion
3826
4132
  //#region src/utils/project-directory.ts
3827
4133
  async function handleDirectoryConflict(currentPathInput) {
@@ -3909,10 +4215,11 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
3909
4215
  finalBaseName
3910
4216
  };
3911
4217
  }
3912
-
3913
4218
  //#endregion
3914
4219
  //#region src/utils/project-name-validation.ts
3915
4220
  function validateProjectName(name) {
4221
+ const hardeningResult = validateAgentSafePathInput(name, "projectName");
4222
+ if (hardeningResult.isErr()) return Result.err(hardeningResult.error);
3916
4223
  const result = types_exports.ProjectNameSchema.safeParse(name);
3917
4224
  if (!result.success) return Result.err(new ValidationError({
3918
4225
  field: "projectName",
@@ -3922,13 +4229,20 @@ function validateProjectName(name) {
3922
4229
  return Result.ok(void 0);
3923
4230
  }
3924
4231
  function extractAndValidateProjectName(projectName, projectDirectory) {
4232
+ if (projectName) {
4233
+ const projectNameInputResult = validateAgentSafePathInput(projectName, "projectName");
4234
+ if (projectNameInputResult.isErr()) return Result.err(projectNameInputResult.error);
4235
+ }
4236
+ if (projectDirectory) {
4237
+ const projectDirInputResult = validateAgentSafePathInput(projectDirectory, "projectDirectory");
4238
+ if (projectDirInputResult.isErr()) return Result.err(projectDirInputResult.error);
4239
+ }
3925
4240
  const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
3926
4241
  if (!derivedName) return Result.ok("");
3927
4242
  const validationResult = validateProjectName(projectName ? path.basename(projectName) : derivedName);
3928
4243
  if (validationResult.isErr()) return Result.err(validationResult.error);
3929
4244
  return Result.ok(projectName || derivedName);
3930
4245
  }
3931
-
3932
4246
  //#endregion
3933
4247
  //#region src/utils/templates.ts
3934
4248
  const TEMPLATE_PRESETS = {
@@ -4009,7 +4323,6 @@ function getTemplateDescription(template) {
4009
4323
  none: "No template - Full customization"
4010
4324
  }[template] || "";
4011
4325
  }
4012
-
4013
4326
  //#endregion
4014
4327
  //#region src/utils/config-processing.ts
4015
4328
  function processArrayOption(options) {
@@ -4025,6 +4338,8 @@ function deriveProjectName(projectName, projectDirectory) {
4025
4338
  function processFlags(options, projectName) {
4026
4339
  const config = {};
4027
4340
  if (options.api) config.api = options.api;
4341
+ if (options.addonOptions) config.addonOptions = options.addonOptions;
4342
+ if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
4028
4343
  if (options.backend) config.backend = options.backend;
4029
4344
  if (options.database) config.database = options.database;
4030
4345
  if (options.orm) config.orm = options.orm;
@@ -4061,7 +4376,6 @@ function validateArrayOptions(options) {
4061
4376
  if (examplesResult.isErr()) return examplesResult;
4062
4377
  return Result.ok(void 0);
4063
4378
  }
4064
-
4065
4379
  //#endregion
4066
4380
  //#region src/utils/config-validation.ts
4067
4381
  function validationErr(message) {
@@ -4244,7 +4558,6 @@ function validateConfigForProgrammaticUse(config) {
4244
4558
  return Result.ok(void 0);
4245
4559
  });
4246
4560
  }
4247
-
4248
4561
  //#endregion
4249
4562
  //#region src/validation.ts
4250
4563
  const CORE_STACK_FLAGS = new Set([
@@ -4304,7 +4617,6 @@ function validateConfigCompatibility(config, providedFlags, options) {
4304
4617
  if (options && providedFlags) return validateFullConfig(config, providedFlags, options);
4305
4618
  else return validateConfigForProgrammaticUse(config);
4306
4619
  }
4307
-
4308
4620
  //#endregion
4309
4621
  //#region src/utils/file-formatter.ts
4310
4622
  const formatOptions = {
@@ -4349,7 +4661,6 @@ async function formatProject(projectDir) {
4349
4661
  })
4350
4662
  });
4351
4663
  }
4352
-
4353
4664
  //#endregion
4354
4665
  //#region src/utils/env-utils.ts
4355
4666
  async function addEnvVariablesToFile(envPath, variables) {
@@ -4379,7 +4690,6 @@ async function addEnvVariablesToFile(envPath, variables) {
4379
4690
  if (newLines.length > 0 && newLines[newLines.length - 1] === "") newLines.pop();
4380
4691
  if (foundKeys.size > 0 || keysToAdd.size > foundKeys.size) await fs.writeFile(envPath, newLines.join("\n") + "\n");
4381
4692
  }
4382
-
4383
4693
  //#endregion
4384
4694
  //#region src/helpers/database-providers/d1-setup.ts
4385
4695
  async function setupCloudflareD1(config) {
@@ -4405,7 +4715,6 @@ async function setupCloudflareD1(config) {
4405
4715
  })
4406
4716
  });
4407
4717
  }
4408
-
4409
4718
  //#endregion
4410
4719
  //#region src/helpers/database-providers/docker-compose-setup.ts
4411
4720
  async function setupDockerCompose(config) {
@@ -4439,7 +4748,6 @@ function getDatabaseUrl(database, projectName) {
4439
4748
  default: return "";
4440
4749
  }
4441
4750
  }
4442
-
4443
4751
  //#endregion
4444
4752
  //#region src/utils/command-exists.ts
4445
4753
  async function commandExists(command) {
@@ -4452,28 +4760,58 @@ async function commandExists(command) {
4452
4760
  });
4453
4761
  return result.isOk() ? result.value : false;
4454
4762
  }
4455
-
4763
+ //#endregion
4764
+ //#region src/helpers/core/db-setup-options.ts
4765
+ const REMOTE_PROVISIONING_DB_SETUPS = [
4766
+ "turso",
4767
+ "neon",
4768
+ "prisma-postgres",
4769
+ "supabase",
4770
+ "mongodb-atlas"
4771
+ ];
4772
+ function requiresProvisioningGuardrails(dbSetup) {
4773
+ return REMOTE_PROVISIONING_DB_SETUPS.includes(dbSetup);
4774
+ }
4775
+ function resolveDbSetupMode(dbSetup, cliOptions = {}) {
4776
+ if (dbSetup === "none") return;
4777
+ const explicitMode = cliOptions.dbSetupOptions?.mode;
4778
+ if (explicitMode) return explicitMode;
4779
+ if (cliOptions.manualDb === true) return "manual";
4780
+ if (isSilent() && requiresProvisioningGuardrails(dbSetup)) return "manual";
4781
+ }
4782
+ function mergeResolvedDbSetupOptions(dbSetup, dbSetupOptions, cliOptions = {}) {
4783
+ if (dbSetup === "none") return;
4784
+ const resolvedMode = resolveDbSetupMode(dbSetup, {
4785
+ ...cliOptions,
4786
+ dbSetupOptions: dbSetupOptions ?? cliOptions.dbSetupOptions
4787
+ });
4788
+ if (!dbSetupOptions && !resolvedMode) return;
4789
+ return {
4790
+ ...dbSetupOptions,
4791
+ ...resolvedMode ? { mode: resolvedMode } : {}
4792
+ };
4793
+ }
4456
4794
  //#endregion
4457
4795
  //#region src/helpers/database-providers/mongodb-atlas-setup.ts
4458
4796
  async function checkAtlasCLI() {
4459
4797
  const exists = await commandExists("atlas");
4460
- if (exists) log.info("MongoDB Atlas CLI found");
4461
- else log.warn(pc.yellow("MongoDB Atlas CLI not found"));
4798
+ if (exists) cliLog.info("MongoDB Atlas CLI found");
4799
+ else cliLog.warn(pc.yellow("MongoDB Atlas CLI not found"));
4462
4800
  return exists;
4463
4801
  }
4464
4802
  async function initMongoDBAtlas(serverDir) {
4465
4803
  if (!await checkAtlasCLI()) {
4466
- log.info(pc.yellow("Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/"));
4804
+ cliLog.info(pc.yellow("Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/"));
4467
4805
  return databaseSetupError("mongodb-atlas", "MongoDB Atlas CLI not found");
4468
4806
  }
4469
- log.info("Running MongoDB Atlas setup...");
4807
+ cliLog.info("Running MongoDB Atlas setup...");
4470
4808
  const deployResult = await Result.tryPromise({
4471
4809
  try: async () => {
4472
4810
  await $({
4473
4811
  cwd: serverDir,
4474
4812
  stdio: "inherit"
4475
4813
  })`atlas deployments setup`;
4476
- log.success("MongoDB Atlas deployment ready");
4814
+ cliLog.success("MongoDB Atlas deployment ready");
4477
4815
  },
4478
4816
  catch: (e) => new DatabaseSetupError({
4479
4817
  provider: "mongodb-atlas",
@@ -4514,7 +4852,7 @@ async function writeEnvFile$3(projectDir, backend, config) {
4514
4852
  });
4515
4853
  }
4516
4854
  function displayManualSetupInstructions$3() {
4517
- log.info(`
4855
+ cliLog.info(`
4518
4856
  ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
4519
4857
 
4520
4858
  1. Install Atlas CLI:
@@ -4532,7 +4870,10 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
4532
4870
  }
4533
4871
  async function setupMongoDBAtlas(config, cliInput) {
4534
4872
  const { projectDir, backend } = config;
4535
- const manualDb = cliInput?.manualDb ?? false;
4873
+ const setupMode = resolveDbSetupMode("mongodb-atlas", {
4874
+ manualDb: cliInput?.manualDb,
4875
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
4876
+ });
4536
4877
  const serverDir = path.join(projectDir, "packages/db");
4537
4878
  const ensureDirResult = await Result.tryPromise({
4538
4879
  try: () => fs.ensureDir(serverDir),
@@ -4543,29 +4884,40 @@ async function setupMongoDBAtlas(config, cliInput) {
4543
4884
  })
4544
4885
  });
4545
4886
  if (ensureDirResult.isErr()) return ensureDirResult;
4546
- if (manualDb) {
4547
- log.info("MongoDB Atlas manual setup selected");
4887
+ if (setupMode === "manual") {
4888
+ cliLog.info("MongoDB Atlas manual setup selected");
4548
4889
  const envResult = await writeEnvFile$3(projectDir, backend);
4549
4890
  if (envResult.isErr()) return envResult;
4550
4891
  displayManualSetupInstructions$3();
4551
4892
  return Result.ok(void 0);
4552
4893
  }
4553
- const mode = await select({
4554
- message: "MongoDB Atlas setup: choose mode",
4555
- options: [{
4556
- label: "Automatic",
4557
- value: "auto",
4558
- hint: "Automated setup with provider CLI, sets .env"
4559
- }, {
4560
- label: "Manual",
4561
- value: "manual",
4562
- hint: "Manual setup, add env vars yourself"
4563
- }],
4564
- initialValue: "auto"
4565
- });
4566
- if (isCancel(mode)) return userCancelled("Operation cancelled");
4894
+ let mode = setupMode;
4895
+ if (!mode) {
4896
+ if (isSilent()) {
4897
+ cliLog.warn(pc.yellow("MongoDB Atlas automatic setup requires interactive input. Falling back to manual setup."));
4898
+ const envResult = await writeEnvFile$3(projectDir, backend);
4899
+ if (envResult.isErr()) return envResult;
4900
+ displayManualSetupInstructions$3();
4901
+ return Result.ok(void 0);
4902
+ }
4903
+ const promptedMode = await select({
4904
+ message: "MongoDB Atlas setup: choose mode",
4905
+ options: [{
4906
+ label: "Automatic",
4907
+ value: "auto",
4908
+ hint: "Automated setup with provider CLI, sets .env"
4909
+ }, {
4910
+ label: "Manual",
4911
+ value: "manual",
4912
+ hint: "Manual setup, add env vars yourself"
4913
+ }],
4914
+ initialValue: "auto"
4915
+ });
4916
+ if (isCancel(promptedMode)) return userCancelled("Operation cancelled");
4917
+ mode = promptedMode;
4918
+ }
4567
4919
  if (mode === "manual") {
4568
- log.info("MongoDB Atlas manual setup selected");
4920
+ cliLog.info("MongoDB Atlas manual setup selected");
4569
4921
  const envResult = await writeEnvFile$3(projectDir, backend);
4570
4922
  if (envResult.isErr()) return envResult;
4571
4923
  displayManualSetupInstructions$3();
@@ -4575,17 +4927,16 @@ async function setupMongoDBAtlas(config, cliInput) {
4575
4927
  if (mongoConfigResult.isOk()) {
4576
4928
  const envResult = await writeEnvFile$3(projectDir, backend, mongoConfigResult.value);
4577
4929
  if (envResult.isErr()) return envResult;
4578
- log.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
4930
+ cliLog.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
4579
4931
  return Result.ok(void 0);
4580
4932
  }
4581
4933
  if (UserCancelledError.is(mongoConfigResult.error)) return mongoConfigResult;
4582
- log.warn(pc.yellow("Falling back to local MongoDB configuration"));
4934
+ cliLog.warn(pc.yellow("Falling back to local MongoDB configuration"));
4583
4935
  const envResult = await writeEnvFile$3(projectDir, backend);
4584
4936
  if (envResult.isErr()) return envResult;
4585
4937
  displayManualSetupInstructions$3();
4586
4938
  return Result.ok(void 0);
4587
4939
  }
4588
-
4589
4940
  //#endregion
4590
4941
  //#region src/helpers/database-providers/neon-setup.ts
4591
4942
  const NEON_REGIONS = [
@@ -4623,7 +4974,7 @@ const NEON_REGIONS = [
4623
4974
  }
4624
4975
  ];
4625
4976
  async function executeNeonCommand(packageManager, commandArgsString, spinnerText) {
4626
- const s = spinner();
4977
+ const s = createSpinner();
4627
4978
  const args = getPackageExecutionArgs(packageManager, commandArgsString);
4628
4979
  if (spinnerText) s.start(spinnerText);
4629
4980
  return Result.tryPromise({
@@ -4686,7 +5037,7 @@ async function writeEnvFile$2(projectDir, backend, config) {
4686
5037
  });
4687
5038
  }
4688
5039
  async function setupWithNeonDb(projectDir, packageManager, backend) {
4689
- const s = spinner();
5040
+ const s = createSpinner();
4690
5041
  s.start("Creating Neon database using get-db...");
4691
5042
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
4692
5043
  const targetDir = path.join(projectDir, targetApp);
@@ -4719,7 +5070,7 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
4719
5070
  });
4720
5071
  }
4721
5072
  function displayManualSetupInstructions$2(target) {
4722
- log.info(`Manual Neon PostgreSQL Setup Instructions:
5073
+ cliLog.info(`Manual Neon PostgreSQL Setup Instructions:
4723
5074
 
4724
5075
  1. Get Neon with Better T Stack referral: https://get.neon.com/sbA3tIe
4725
5076
  2. Create a new project from the dashboard
@@ -4730,80 +5081,105 @@ DATABASE_URL="your_connection_string"`);
4730
5081
  }
4731
5082
  async function setupNeonPostgres(config, cliInput) {
4732
5083
  const { packageManager, projectDir, backend } = config;
4733
- const manualDb = cliInput?.manualDb ?? false;
5084
+ const setupMode = resolveDbSetupMode("neon", {
5085
+ manualDb: cliInput?.manualDb,
5086
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
5087
+ });
4734
5088
  const target = backend === "self" ? "apps/web" : "apps/server";
4735
- if (manualDb) {
5089
+ if (setupMode === "manual") {
4736
5090
  const envResult = await writeEnvFile$2(projectDir, backend);
4737
5091
  if (envResult.isErr()) return envResult;
4738
5092
  displayManualSetupInstructions$2(target);
4739
5093
  return Result.ok(void 0);
4740
5094
  }
4741
- const mode = await select({
4742
- message: "Neon setup: choose mode",
4743
- options: [{
4744
- label: "Automatic",
4745
- value: "auto",
4746
- hint: "Automated setup with provider CLI, sets .env"
4747
- }, {
4748
- label: "Manual",
4749
- value: "manual",
4750
- hint: "Manual setup, add env vars yourself"
4751
- }],
4752
- initialValue: "auto"
4753
- });
4754
- if (isCancel(mode)) return userCancelled("Operation cancelled");
4755
- if (mode === "manual") {
5095
+ let selectedMode = setupMode;
5096
+ if (!selectedMode) if (isSilent()) selectedMode = "manual";
5097
+ else {
5098
+ const promptedMode = await select({
5099
+ message: "Neon setup: choose mode",
5100
+ options: [{
5101
+ label: "Automatic",
5102
+ value: "auto",
5103
+ hint: "Automated setup with provider CLI, sets .env"
5104
+ }, {
5105
+ label: "Manual",
5106
+ value: "manual",
5107
+ hint: "Manual setup, add env vars yourself"
5108
+ }],
5109
+ initialValue: "auto"
5110
+ });
5111
+ if (isCancel(promptedMode)) return userCancelled("Operation cancelled");
5112
+ selectedMode = promptedMode;
5113
+ }
5114
+ if (selectedMode === "manual") {
4756
5115
  const envResult = await writeEnvFile$2(projectDir, backend);
4757
5116
  if (envResult.isErr()) return envResult;
4758
5117
  displayManualSetupInstructions$2(target);
4759
5118
  return Result.ok(void 0);
4760
5119
  }
4761
- const setupMethod = await select({
4762
- message: "Choose your Neon setup method:",
4763
- options: [{
4764
- label: "Quick setup with get-db",
4765
- value: "neondb",
4766
- hint: "fastest, no auth required"
4767
- }, {
4768
- label: "Custom setup with neonctl",
4769
- value: "neonctl",
4770
- hint: "More control - choose project name and region"
4771
- }],
4772
- initialValue: "neondb"
4773
- });
4774
- if (isCancel(setupMethod)) return userCancelled("Operation cancelled");
5120
+ let setupMethod = cliInput?.dbSetupOptions?.neon?.method ?? config.dbSetupOptions?.neon?.method;
5121
+ if (!setupMethod) if (isSilent()) setupMethod = "neondb";
5122
+ else {
5123
+ const promptedSetupMethod = await select({
5124
+ message: "Choose your Neon setup method:",
5125
+ options: [{
5126
+ label: "Quick setup with get-db",
5127
+ value: "neondb",
5128
+ hint: "fastest, no auth required"
5129
+ }, {
5130
+ label: "Custom setup with neonctl",
5131
+ value: "neonctl",
5132
+ hint: "More control - choose project name and region"
5133
+ }],
5134
+ initialValue: "neondb"
5135
+ });
5136
+ if (isCancel(promptedSetupMethod)) return userCancelled("Operation cancelled");
5137
+ setupMethod = promptedSetupMethod;
5138
+ }
4775
5139
  if (setupMethod === "neondb") {
4776
5140
  const neonDbResult = await setupWithNeonDb(projectDir, packageManager, backend);
4777
5141
  if (neonDbResult.isErr()) {
4778
- log.error(pc.red(neonDbResult.error.message));
5142
+ cliLog.error(pc.red(neonDbResult.error.message));
4779
5143
  const envResult = await writeEnvFile$2(projectDir, backend);
4780
5144
  if (envResult.isErr()) return envResult;
4781
5145
  displayManualSetupInstructions$2(target);
4782
- } else log.info(`Get Neon with Better T Stack referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
4783
- return neonDbResult;
5146
+ return Result.ok(void 0);
5147
+ }
5148
+ cliLog.info(`Get Neon with Better T Stack referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
5149
+ return Result.ok(void 0);
4784
5150
  }
4785
5151
  const suggestedProjectName = path.basename(projectDir);
4786
- const projectName = await text({
4787
- message: "Enter a name for your Neon project:",
4788
- defaultValue: suggestedProjectName,
4789
- initialValue: suggestedProjectName
4790
- });
4791
- if (isCancel(projectName)) return userCancelled("Operation cancelled");
4792
- const regionId = await select({
4793
- message: "Select a region for your Neon project:",
4794
- options: NEON_REGIONS,
4795
- initialValue: NEON_REGIONS[0].value
4796
- });
4797
- if (isCancel(regionId)) return userCancelled("Operation cancelled");
5152
+ let projectName = cliInput?.dbSetupOptions?.neon?.projectName ?? config.dbSetupOptions?.neon?.projectName;
5153
+ if (!projectName) if (isSilent()) projectName = suggestedProjectName;
5154
+ else {
5155
+ const promptedProjectName = await text({
5156
+ message: "Enter a name for your Neon project:",
5157
+ defaultValue: suggestedProjectName,
5158
+ initialValue: suggestedProjectName
5159
+ });
5160
+ if (isCancel(promptedProjectName)) return userCancelled("Operation cancelled");
5161
+ projectName = promptedProjectName;
5162
+ }
5163
+ let regionId = cliInput?.dbSetupOptions?.neon?.regionId ?? config.dbSetupOptions?.neon?.regionId;
5164
+ if (!regionId) if (isSilent()) regionId = NEON_REGIONS[0].value;
5165
+ else {
5166
+ const promptedRegionId = await select({
5167
+ message: "Select a region for your Neon project:",
5168
+ options: NEON_REGIONS,
5169
+ initialValue: NEON_REGIONS[0].value
5170
+ });
5171
+ if (isCancel(promptedRegionId)) return userCancelled("Operation cancelled");
5172
+ regionId = promptedRegionId;
5173
+ }
4798
5174
  const neonConfigResult = await createNeonProject(projectName, regionId, packageManager);
4799
5175
  if (neonConfigResult.isErr()) {
4800
- log.error(pc.red(neonConfigResult.error.message));
5176
+ cliLog.error(pc.red(neonConfigResult.error.message));
4801
5177
  const envResult = await writeEnvFile$2(projectDir, backend);
4802
5178
  if (envResult.isErr()) return envResult;
4803
5179
  displayManualSetupInstructions$2(target);
4804
5180
  return Result.ok(void 0);
4805
5181
  }
4806
- const finalSpinner = spinner();
5182
+ const finalSpinner = createSpinner();
4807
5183
  finalSpinner.start("Configuring database connection");
4808
5184
  const envResult = await writeEnvFile$2(projectDir, backend, neonConfigResult.value);
4809
5185
  if (envResult.isErr()) {
@@ -4811,10 +5187,9 @@ async function setupNeonPostgres(config, cliInput) {
4811
5187
  return envResult;
4812
5188
  }
4813
5189
  finalSpinner.stop("Neon database configured!");
4814
- log.info(`Get Neon with Better T Stack referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
5190
+ cliLog.info(`Get Neon with Better T Stack referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
4815
5191
  return Result.ok(void 0);
4816
5192
  }
4817
-
4818
5193
  //#endregion
4819
5194
  //#region src/helpers/database-providers/planetscale-setup.ts
4820
5195
  async function setupPlanetScale(config) {
@@ -4885,7 +5260,6 @@ async function setupPlanetScale(config) {
4885
5260
  })
4886
5261
  });
4887
5262
  }
4888
-
4889
5263
  //#endregion
4890
5264
  //#region src/helpers/database-providers/prisma-postgres-setup.ts
4891
5265
  const AVAILABLE_REGIONS = [
@@ -4914,16 +5288,31 @@ const AVAILABLE_REGIONS = [
4914
5288
  label: "US West (N. California)"
4915
5289
  }
4916
5290
  ];
4917
- async function setupWithCreateDb(serverDir, packageManager) {
4918
- log.info("Starting Prisma Postgres setup with create-db.");
4919
- const selectedRegion = await select({
4920
- message: "Select your preferred region:",
4921
- options: AVAILABLE_REGIONS,
4922
- initialValue: "ap-southeast-1"
4923
- });
4924
- if (isCancel(selectedRegion)) return userCancelled("Operation cancelled");
4925
- const createDbArgs = getPackageExecutionArgs(packageManager, `create-db@latest --json --region ${selectedRegion} --user-agent "aman/better-t-stack"`);
4926
- const s = spinner();
5291
+ const CREATE_DB_USER_AGENT = "aman/better-t-stack";
5292
+ async function setupWithCreateDb(serverDir, packageManager, regionId) {
5293
+ cliLog.info("Starting Prisma Postgres setup with create-db.");
5294
+ let selectedRegion = regionId;
5295
+ if (!selectedRegion) if (isSilent()) selectedRegion = "ap-southeast-1";
5296
+ else {
5297
+ const promptedRegion = await select({
5298
+ message: "Select your preferred region:",
5299
+ options: AVAILABLE_REGIONS,
5300
+ initialValue: "ap-southeast-1"
5301
+ });
5302
+ if (isCancel(promptedRegion)) return userCancelled("Operation cancelled");
5303
+ selectedRegion = promptedRegion;
5304
+ }
5305
+ const createDbArgs = [
5306
+ ...getPackageRunnerPrefix(packageManager),
5307
+ "create-db@latest",
5308
+ "create",
5309
+ "--json",
5310
+ "--region",
5311
+ selectedRegion,
5312
+ "--user-agent",
5313
+ CREATE_DB_USER_AGENT
5314
+ ];
5315
+ const s = createSpinner();
4927
5316
  s.start("Creating Prisma Postgres database...");
4928
5317
  const execResult = await Result.tryPromise({
4929
5318
  try: async () => {
@@ -4981,7 +5370,7 @@ async function writeEnvFile$1(projectDir, backend, config) {
4981
5370
  });
4982
5371
  }
4983
5372
  function displayManualSetupInstructions$1(target) {
4984
- log.info(`Manual Prisma PostgreSQL Setup Instructions:
5373
+ cliLog.info(`Manual Prisma PostgreSQL Setup Instructions:
4985
5374
 
4986
5375
  1. Visit https://console.prisma.io and create an account
4987
5376
  2. Create a new PostgreSQL database from the dashboard
@@ -4992,7 +5381,10 @@ DATABASE_URL="your_database_url"`);
4992
5381
  }
4993
5382
  async function setupPrismaPostgres(config, cliInput) {
4994
5383
  const { packageManager, projectDir, backend } = config;
4995
- const manualDb = cliInput?.manualDb ?? false;
5384
+ const setupMode = resolveDbSetupMode("prisma-postgres", {
5385
+ manualDb: cliInput?.manualDb,
5386
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
5387
+ });
4996
5388
  const dbDir = path.join(projectDir, "packages/db");
4997
5389
  const target = backend === "self" ? "apps/web" : "apps/server";
4998
5390
  const ensureDirResult = await Result.tryPromise({
@@ -5004,49 +5396,53 @@ async function setupPrismaPostgres(config, cliInput) {
5004
5396
  })
5005
5397
  });
5006
5398
  if (ensureDirResult.isErr()) return ensureDirResult;
5007
- if (manualDb) {
5399
+ if (setupMode === "manual") {
5008
5400
  const envResult = await writeEnvFile$1(projectDir, backend);
5009
5401
  if (envResult.isErr()) return envResult;
5010
5402
  displayManualSetupInstructions$1(target);
5011
5403
  return Result.ok(void 0);
5012
5404
  }
5013
- const setupMode = await select({
5014
- message: "Prisma Postgres setup: choose mode",
5015
- options: [{
5016
- label: "Automatic (create-db)",
5017
- value: "auto",
5018
- hint: "Provision a database via Prisma's create-db CLI"
5019
- }, {
5020
- label: "Manual",
5021
- value: "manual",
5022
- hint: "Add your own DATABASE_URL later"
5023
- }],
5024
- initialValue: "auto"
5025
- });
5026
- if (isCancel(setupMode)) return userCancelled("Operation cancelled");
5027
- if (setupMode === "manual") {
5405
+ let selectedSetupMode = setupMode;
5406
+ if (!selectedSetupMode) if (isSilent()) selectedSetupMode = "manual";
5407
+ else {
5408
+ const promptedSetupMode = await select({
5409
+ message: "Prisma Postgres setup: choose mode",
5410
+ options: [{
5411
+ label: "Automatic (create-db)",
5412
+ value: "auto",
5413
+ hint: "Provision a database via Prisma's create-db CLI"
5414
+ }, {
5415
+ label: "Manual",
5416
+ value: "manual",
5417
+ hint: "Add your own DATABASE_URL later"
5418
+ }],
5419
+ initialValue: "auto"
5420
+ });
5421
+ if (isCancel(promptedSetupMode)) return userCancelled("Operation cancelled");
5422
+ selectedSetupMode = promptedSetupMode;
5423
+ }
5424
+ if (selectedSetupMode === "manual") {
5028
5425
  const envResult = await writeEnvFile$1(projectDir, backend);
5029
5426
  if (envResult.isErr()) return envResult;
5030
5427
  displayManualSetupInstructions$1(target);
5031
5428
  return Result.ok(void 0);
5032
5429
  }
5033
- const prismaConfigResult = await setupWithCreateDb(dbDir, packageManager);
5430
+ const prismaConfigResult = await setupWithCreateDb(dbDir, packageManager, cliInput?.dbSetupOptions?.prismaPostgres?.regionId ?? config.dbSetupOptions?.prismaPostgres?.regionId);
5034
5431
  if (prismaConfigResult.isErr()) {
5035
5432
  if (UserCancelledError.is(prismaConfigResult.error)) return prismaConfigResult;
5036
- log.error(pc.red(prismaConfigResult.error.message));
5433
+ cliLog.error(pc.red(prismaConfigResult.error.message));
5037
5434
  const envResult = await writeEnvFile$1(projectDir, backend);
5038
5435
  if (envResult.isErr()) return envResult;
5039
5436
  displayManualSetupInstructions$1(target);
5040
- log.info("Setup completed with manual configuration required.");
5437
+ cliLog.info("Setup completed with manual configuration required.");
5041
5438
  return Result.ok(void 0);
5042
5439
  }
5043
5440
  const envResult = await writeEnvFile$1(projectDir, backend, prismaConfigResult.value);
5044
5441
  if (envResult.isErr()) return envResult;
5045
- log.success(pc.green("Prisma Postgres database configured successfully!"));
5046
- if (prismaConfigResult.value.claimUrl) log.info(pc.blue(`Claim URL saved to .env: ${prismaConfigResult.value.claimUrl}`));
5442
+ cliLog.success(pc.green("Prisma Postgres database configured successfully!"));
5443
+ if (prismaConfigResult.value.claimUrl) cliLog.info(pc.blue(`Claim URL saved to .env: ${prismaConfigResult.value.claimUrl}`));
5047
5444
  return Result.ok(void 0);
5048
5445
  }
5049
-
5050
5446
  //#endregion
5051
5447
  //#region src/helpers/database-providers/supabase-setup.ts
5052
5448
  async function writeSupabaseEnvFile(projectDir, backend, databaseUrl) {
@@ -5076,7 +5472,7 @@ function extractDbUrl(output) {
5076
5472
  return output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/)?.[1] ?? null;
5077
5473
  }
5078
5474
  async function initializeSupabase(serverDir, packageManager) {
5079
- log.info("Initializing Supabase project...");
5475
+ cliLog.info("Initializing Supabase project...");
5080
5476
  return Result.tryPromise({
5081
5477
  try: async () => {
5082
5478
  const supabaseInitArgs = getPackageExecutionArgs(packageManager, "supabase init");
@@ -5084,7 +5480,7 @@ async function initializeSupabase(serverDir, packageManager) {
5084
5480
  cwd: serverDir,
5085
5481
  stdio: "inherit"
5086
5482
  });
5087
- log.success("Supabase project initialized");
5483
+ cliLog.success("Supabase project initialized");
5088
5484
  },
5089
5485
  catch: (e) => {
5090
5486
  const error = e;
@@ -5097,7 +5493,7 @@ async function initializeSupabase(serverDir, packageManager) {
5097
5493
  });
5098
5494
  }
5099
5495
  async function startSupabase(serverDir, packageManager) {
5100
- log.info("Starting Supabase services (this may take a moment)...");
5496
+ cliLog.info("Starting Supabase services (this may take a moment)...");
5101
5497
  const supabaseStartArgs = getPackageExecutionArgs(packageManager, "supabase start");
5102
5498
  return Result.tryPromise({
5103
5499
  try: async () => {
@@ -5105,7 +5501,7 @@ async function startSupabase(serverDir, packageManager) {
5105
5501
  let stdoutData = "";
5106
5502
  if (subprocess.stdout) subprocess.stdout.on("data", (data) => {
5107
5503
  const text = data.toString();
5108
- process.stdout.write(text);
5504
+ if (!isSilent()) process.stdout.write(text);
5109
5505
  stdoutData += text;
5110
5506
  });
5111
5507
  if (subprocess.stderr) subprocess.stderr.pipe(process.stderr);
@@ -5123,8 +5519,8 @@ async function startSupabase(serverDir, packageManager) {
5123
5519
  }
5124
5520
  });
5125
5521
  }
5126
- function displayManualSupabaseInstructions(output) {
5127
- log.info(`"Manual Supabase Setup Instructions:"
5522
+ function displayManualSupabaseInstructions(targetApp, output) {
5523
+ cliLog.info(`"Manual Supabase Setup Instructions:"
5128
5524
  1. Ensure Docker is installed and running.
5129
5525
  2. Install the Supabase CLI (e.g., \`npm install -g supabase\`).
5130
5526
  3. Run \`supabase init\` in your project's \`packages/db\` directory.
@@ -5132,12 +5528,16 @@ function displayManualSupabaseInstructions(output) {
5132
5528
  5. Copy the 'DB URL' from the output.${output ? `
5133
5529
  ${pc.bold("Relevant output from `supabase start`:")}
5134
5530
  ${pc.dim(output)}` : ""}
5135
- 6. Add the DB URL to the .env file in \`packages/db/.env\` as \`DATABASE_URL\`:
5531
+ 6. Add the DB URL to the .env file in \`${targetApp}/.env\` as \`DATABASE_URL\`:
5136
5532
  ${pc.gray("DATABASE_URL=\"your_supabase_db_url\"")}`);
5137
5533
  }
5138
5534
  async function setupSupabase(config, cliInput) {
5139
5535
  const { projectDir, packageManager, backend } = config;
5140
- const manualDb = cliInput?.manualDb ?? false;
5536
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
5537
+ const setupMode = resolveDbSetupMode("supabase", {
5538
+ manualDb: cliInput?.manualDb,
5539
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
5540
+ });
5141
5541
  const serverDir = path.join(projectDir, "packages", "db");
5142
5542
  const ensureDirResult = await Result.tryPromise({
5143
5543
  try: () => fs.ensureDir(serverDir),
@@ -5148,56 +5548,60 @@ async function setupSupabase(config, cliInput) {
5148
5548
  })
5149
5549
  });
5150
5550
  if (ensureDirResult.isErr()) return ensureDirResult;
5151
- if (manualDb) {
5152
- displayManualSupabaseInstructions();
5551
+ if (setupMode === "manual") {
5552
+ displayManualSupabaseInstructions(targetApp);
5153
5553
  return writeSupabaseEnvFile(projectDir, backend, "");
5154
5554
  }
5155
- const mode = await select({
5156
- message: "Supabase setup: choose mode",
5157
- options: [{
5158
- label: "Automatic",
5159
- value: "auto",
5160
- hint: "Automated setup with provider CLI, sets .env"
5161
- }, {
5162
- label: "Manual",
5163
- value: "manual",
5164
- hint: "Manual setup, add env vars yourself"
5165
- }],
5166
- initialValue: "auto"
5167
- });
5168
- if (isCancel(mode)) return userCancelled("Operation cancelled");
5555
+ let mode = setupMode;
5556
+ if (!mode) if (isSilent()) mode = "manual";
5557
+ else {
5558
+ const promptedMode = await select({
5559
+ message: "Supabase setup: choose mode",
5560
+ options: [{
5561
+ label: "Automatic",
5562
+ value: "auto",
5563
+ hint: "Automated setup with provider CLI, sets .env"
5564
+ }, {
5565
+ label: "Manual",
5566
+ value: "manual",
5567
+ hint: "Manual setup, add env vars yourself"
5568
+ }],
5569
+ initialValue: "auto"
5570
+ });
5571
+ if (isCancel(promptedMode)) return userCancelled("Operation cancelled");
5572
+ mode = promptedMode;
5573
+ }
5169
5574
  if (mode === "manual") {
5170
- displayManualSupabaseInstructions();
5575
+ displayManualSupabaseInstructions(targetApp);
5171
5576
  return writeSupabaseEnvFile(projectDir, backend, "");
5172
5577
  }
5173
5578
  const initResult = await initializeSupabase(serverDir, packageManager);
5174
5579
  if (initResult.isErr()) {
5175
- log.error(pc.red(initResult.error.message));
5176
- displayManualSupabaseInstructions();
5580
+ cliLog.error(pc.red(initResult.error.message));
5581
+ displayManualSupabaseInstructions(targetApp);
5177
5582
  return writeSupabaseEnvFile(projectDir, backend, "");
5178
5583
  }
5179
5584
  const startResult = await startSupabase(serverDir, packageManager);
5180
5585
  if (startResult.isErr()) {
5181
- log.error(pc.red(startResult.error.message));
5182
- displayManualSupabaseInstructions();
5586
+ cliLog.error(pc.red(startResult.error.message));
5587
+ displayManualSupabaseInstructions(targetApp);
5183
5588
  return writeSupabaseEnvFile(projectDir, backend, "");
5184
5589
  }
5185
5590
  const supabaseOutput = startResult.value;
5186
5591
  const dbUrl = extractDbUrl(supabaseOutput);
5187
5592
  if (dbUrl) {
5188
5593
  const envResult = await writeSupabaseEnvFile(projectDir, backend, dbUrl);
5189
- if (envResult.isOk()) log.success(pc.green("Supabase local development setup ready!"));
5594
+ if (envResult.isOk()) cliLog.success(pc.green("Supabase local development setup ready!"));
5190
5595
  else {
5191
- log.error(pc.red("Supabase setup completed, but failed to update .env automatically."));
5192
- displayManualSupabaseInstructions(supabaseOutput);
5596
+ cliLog.error(pc.red("Supabase setup completed, but failed to update .env automatically."));
5597
+ displayManualSupabaseInstructions(targetApp, supabaseOutput);
5193
5598
  }
5194
5599
  return envResult;
5195
5600
  }
5196
- log.error(pc.yellow("Supabase started, but could not extract DB URL automatically."));
5197
- displayManualSupabaseInstructions(supabaseOutput);
5601
+ cliLog.error(pc.yellow("Supabase started, but could not extract DB URL automatically."));
5602
+ displayManualSupabaseInstructions(targetApp, supabaseOutput);
5198
5603
  return databaseSetupError("supabase", "Could not extract database URL from Supabase output. Please configure manually.");
5199
5604
  }
5200
-
5201
5605
  //#endregion
5202
5606
  //#region src/helpers/database-providers/turso-setup.ts
5203
5607
  async function isTursoInstalled() {
@@ -5213,7 +5617,7 @@ async function isTursoLoggedIn() {
5213
5617
  return result.isOk() ? result.value : false;
5214
5618
  }
5215
5619
  async function loginToTurso() {
5216
- const s = spinner();
5620
+ const s = createSpinner();
5217
5621
  s.start("Logging in to Turso...");
5218
5622
  return Result.tryPromise({
5219
5623
  try: async () => {
@@ -5231,7 +5635,7 @@ async function loginToTurso() {
5231
5635
  });
5232
5636
  }
5233
5637
  async function installTursoCLI(isMac) {
5234
- const s = spinner();
5638
+ const s = createSpinner();
5235
5639
  s.start("Installing Turso CLI...");
5236
5640
  return Result.tryPromise({
5237
5641
  try: async () => {
@@ -5255,7 +5659,7 @@ async function installTursoCLI(isMac) {
5255
5659
  });
5256
5660
  }
5257
5661
  async function getTursoGroups() {
5258
- const s = spinner();
5662
+ const s = createSpinner();
5259
5663
  s.start("Fetching Turso groups...");
5260
5664
  const result = await Result.tryPromise({
5261
5665
  try: async () => {
@@ -5288,7 +5692,7 @@ async function selectTursoGroup() {
5288
5692
  const groups = await getTursoGroups();
5289
5693
  if (groups.length === 0) return Result.ok(null);
5290
5694
  if (groups.length === 1) {
5291
- log.info(`Using the only available group: ${pc.blue(groups[0].name)}`);
5695
+ cliLog.info(`Using the only available group: ${pc.blue(groups[0].name)}`);
5292
5696
  return Result.ok(groups[0].name);
5293
5697
  }
5294
5698
  const selectedGroup = await select({
@@ -5302,7 +5706,7 @@ async function selectTursoGroup() {
5302
5706
  return Result.ok(selectedGroup);
5303
5707
  }
5304
5708
  async function createTursoDatabase(dbName, groupName) {
5305
- const s = spinner();
5709
+ const s = createSpinner();
5306
5710
  s.start(`Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`);
5307
5711
  const createResult = await Result.tryPromise({
5308
5712
  try: async () => {
@@ -5368,45 +5772,54 @@ async function writeEnvFile(projectDir, backend, config) {
5368
5772
  })
5369
5773
  });
5370
5774
  }
5371
- function displayManualSetupInstructions() {
5372
- log.info(`Manual Turso Setup Instructions:
5775
+ function displayManualSetupInstructions(targetApp) {
5776
+ cliLog.info(`Manual Turso Setup Instructions:
5373
5777
 
5374
5778
  1. Visit https://turso.tech and create an account
5375
5779
  2. Create a new database from the dashboard
5376
5780
  3. Get your database URL and authentication token
5377
- 4. Add these credentials to the .env file in apps/server/.env
5781
+ 4. Add these credentials to the .env file in ${targetApp}/.env
5378
5782
 
5379
5783
  DATABASE_URL=your_database_url
5380
5784
  DATABASE_AUTH_TOKEN=your_auth_token`);
5381
5785
  }
5382
5786
  async function setupTurso(config, cliInput) {
5383
5787
  const { projectDir, backend } = config;
5384
- const manualDb = cliInput?.manualDb ?? false;
5385
- const setupSpinner = spinner();
5386
- if (manualDb) {
5788
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
5789
+ const setupMode = resolveDbSetupMode("turso", {
5790
+ manualDb: cliInput?.manualDb,
5791
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
5792
+ });
5793
+ const setupSpinner = createSpinner();
5794
+ if (setupMode === "manual") {
5387
5795
  const envResult = await writeEnvFile(projectDir, backend);
5388
5796
  if (envResult.isErr()) return envResult;
5389
- displayManualSetupInstructions();
5797
+ displayManualSetupInstructions(targetApp);
5390
5798
  return Result.ok(void 0);
5391
5799
  }
5392
- const mode = await select({
5393
- message: "Turso setup: choose mode",
5394
- options: [{
5395
- label: "Automatic",
5396
- value: "auto",
5397
- hint: "Automated setup with provider CLI, sets .env"
5398
- }, {
5399
- label: "Manual",
5400
- value: "manual",
5401
- hint: "Manual setup, add env vars yourself"
5402
- }],
5403
- initialValue: "auto"
5404
- });
5405
- if (isCancel(mode)) return userCancelled("Operation cancelled");
5800
+ let mode = setupMode;
5801
+ if (!mode) if (isSilent()) mode = "manual";
5802
+ else {
5803
+ const promptedMode = await select({
5804
+ message: "Turso setup: choose mode",
5805
+ options: [{
5806
+ label: "Automatic",
5807
+ value: "auto",
5808
+ hint: "Automated setup with provider CLI, sets .env"
5809
+ }, {
5810
+ label: "Manual",
5811
+ value: "manual",
5812
+ hint: "Manual setup, add env vars yourself"
5813
+ }],
5814
+ initialValue: "auto"
5815
+ });
5816
+ if (isCancel(promptedMode)) return userCancelled("Operation cancelled");
5817
+ mode = promptedMode;
5818
+ }
5406
5819
  if (mode === "manual") {
5407
5820
  const envResult = await writeEnvFile(projectDir, backend);
5408
5821
  if (envResult.isErr()) return envResult;
5409
- displayManualSetupInstructions();
5822
+ displayManualSetupInstructions(targetApp);
5410
5823
  return Result.ok(void 0);
5411
5824
  }
5412
5825
  setupSpinner.start("Checking Turso CLI availability...");
@@ -5414,48 +5827,79 @@ async function setupTurso(config, cliInput) {
5414
5827
  const isMac = platform === "darwin";
5415
5828
  if (platform === "win32") {
5416
5829
  setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
5417
- log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
5830
+ cliLog.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
5418
5831
  const envResult = await writeEnvFile(projectDir, backend);
5419
5832
  if (envResult.isErr()) return envResult;
5420
- displayManualSetupInstructions();
5833
+ displayManualSetupInstructions(targetApp);
5421
5834
  return Result.ok(void 0);
5422
5835
  }
5423
5836
  setupSpinner.stop("Turso CLI availability checked");
5424
5837
  if (!await isTursoInstalled()) {
5425
- const shouldInstall = await confirm({
5426
- message: "Would you like to install Turso CLI?",
5427
- initialValue: true
5428
- });
5429
- if (isCancel(shouldInstall)) return userCancelled("Operation cancelled");
5838
+ let shouldInstall = cliInput?.dbSetupOptions?.turso?.installCli;
5839
+ if (shouldInstall === void 0) if (isSilent()) shouldInstall = false;
5840
+ else {
5841
+ const promptedInstall = await confirm({
5842
+ message: "Would you like to install Turso CLI?",
5843
+ initialValue: true
5844
+ });
5845
+ if (isCancel(promptedInstall)) return userCancelled("Operation cancelled");
5846
+ shouldInstall = promptedInstall;
5847
+ }
5430
5848
  if (!shouldInstall) {
5431
5849
  const envResult = await writeEnvFile(projectDir, backend);
5432
5850
  if (envResult.isErr()) return envResult;
5433
- displayManualSetupInstructions();
5851
+ displayManualSetupInstructions(targetApp);
5434
5852
  return Result.ok(void 0);
5435
5853
  }
5436
5854
  const installResult = await installTursoCLI(isMac);
5437
5855
  if (installResult.isErr()) {
5438
- log.error(pc.red(installResult.error.message));
5856
+ cliLog.error(pc.red(installResult.error.message));
5439
5857
  const envResult = await writeEnvFile(projectDir, backend);
5440
5858
  if (envResult.isErr()) return envResult;
5441
- displayManualSetupInstructions();
5859
+ displayManualSetupInstructions(targetApp);
5442
5860
  return Result.ok(void 0);
5443
5861
  }
5444
5862
  }
5445
5863
  if (!await isTursoLoggedIn()) {
5864
+ if (isSilent()) {
5865
+ cliLog.warn(pc.yellow("Turso CLI is not logged in. Falling back to manual setup."));
5866
+ const envResult = await writeEnvFile(projectDir, backend);
5867
+ if (envResult.isErr()) return envResult;
5868
+ displayManualSetupInstructions(targetApp);
5869
+ return Result.ok(void 0);
5870
+ }
5446
5871
  const loginResult = await loginToTurso();
5447
5872
  if (loginResult.isErr()) {
5448
- log.error(pc.red(loginResult.error.message));
5873
+ cliLog.error(pc.red(loginResult.error.message));
5874
+ const envResult = await writeEnvFile(projectDir, backend);
5875
+ if (envResult.isErr()) return envResult;
5876
+ displayManualSetupInstructions(targetApp);
5877
+ return Result.ok(void 0);
5878
+ }
5879
+ }
5880
+ let selectedGroup = cliInput?.dbSetupOptions?.turso?.groupName ?? config.dbSetupOptions?.turso?.groupName ?? null;
5881
+ if (!selectedGroup) if (isSilent()) selectedGroup = (await getTursoGroups())[0]?.name ?? null;
5882
+ else {
5883
+ const groupResult = await selectTursoGroup();
5884
+ if (groupResult.isErr()) return groupResult;
5885
+ selectedGroup = groupResult.value;
5886
+ }
5887
+ let suggestedName = cliInput?.dbSetupOptions?.turso?.databaseName ?? config.dbSetupOptions?.turso?.databaseName ?? path.basename(projectDir);
5888
+ if (isSilent()) {
5889
+ const createResult = await createTursoDatabase(suggestedName, selectedGroup);
5890
+ if (createResult.isErr()) {
5891
+ cliLog.error(pc.red(createResult.error.message));
5449
5892
  const envResult = await writeEnvFile(projectDir, backend);
5450
5893
  if (envResult.isErr()) return envResult;
5451
- displayManualSetupInstructions();
5894
+ displayManualSetupInstructions(targetApp);
5895
+ cliLog.success("Setup completed with manual configuration required.");
5452
5896
  return Result.ok(void 0);
5453
5897
  }
5898
+ const envResult = await writeEnvFile(projectDir, backend, createResult.value);
5899
+ if (envResult.isErr()) return envResult;
5900
+ cliLog.success("Turso database setup completed successfully!");
5901
+ return Result.ok(void 0);
5454
5902
  }
5455
- const groupResult = await selectTursoGroup();
5456
- if (groupResult.isErr()) return groupResult;
5457
- const selectedGroup = groupResult.value;
5458
- let suggestedName = path.basename(projectDir);
5459
5903
  while (true) {
5460
5904
  const dbNameResponse = await text({
5461
5905
  message: "Enter a name for your database:",
@@ -5468,24 +5912,23 @@ async function setupTurso(config, cliInput) {
5468
5912
  const createResult = await createTursoDatabase(dbName, selectedGroup);
5469
5913
  if (createResult.isErr()) {
5470
5914
  if (createResult.error.message === "DATABASE_EXISTS") {
5471
- log.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
5915
+ cliLog.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
5472
5916
  suggestedName = `${dbName}-${Math.floor(Math.random() * 1e3)}`;
5473
5917
  continue;
5474
5918
  }
5475
- log.error(pc.red(createResult.error.message));
5919
+ cliLog.error(pc.red(createResult.error.message));
5476
5920
  const envResult = await writeEnvFile(projectDir, backend);
5477
5921
  if (envResult.isErr()) return envResult;
5478
- displayManualSetupInstructions();
5479
- log.success("Setup completed with manual configuration required.");
5922
+ displayManualSetupInstructions(targetApp);
5923
+ cliLog.success("Setup completed with manual configuration required.");
5480
5924
  return Result.ok(void 0);
5481
5925
  }
5482
5926
  const envResult = await writeEnvFile(projectDir, backend, createResult.value);
5483
5927
  if (envResult.isErr()) return envResult;
5484
- log.success("Turso database setup completed successfully!");
5928
+ cliLog.success("Turso database setup completed successfully!");
5485
5929
  return Result.ok(void 0);
5486
5930
  }
5487
5931
  }
5488
-
5489
5932
  //#endregion
5490
5933
  //#region src/helpers/core/db-setup.ts
5491
5934
  async function setupDatabase(config, cliInput) {
@@ -5506,18 +5949,21 @@ async function setupDatabase(config, cliInput) {
5506
5949
  consola.error(pc.red(result.error.message));
5507
5950
  }
5508
5951
  }
5952
+ const resolvedCliInput = {
5953
+ ...cliInput,
5954
+ dbSetupOptions: mergeResolvedDbSetupOptions(dbSetup, config.dbSetupOptions, cliInput)
5955
+ };
5509
5956
  if (dbSetup === "docker") await runSetup(() => setupDockerCompose(config));
5510
- else if (database === "sqlite" && dbSetup === "turso") await runSetup(() => setupTurso(config, cliInput));
5957
+ else if (database === "sqlite" && dbSetup === "turso") await runSetup(() => setupTurso(config, resolvedCliInput));
5511
5958
  else if (database === "sqlite" && dbSetup === "d1") await runSetup(() => setupCloudflareD1(config));
5512
5959
  else if (database === "postgres") {
5513
- if (dbSetup === "prisma-postgres") await runSetup(() => setupPrismaPostgres(config, cliInput));
5514
- else if (dbSetup === "neon") await runSetup(() => setupNeonPostgres(config, cliInput));
5960
+ if (dbSetup === "prisma-postgres") await runSetup(() => setupPrismaPostgres(config, resolvedCliInput));
5961
+ else if (dbSetup === "neon") await runSetup(() => setupNeonPostgres(config, resolvedCliInput));
5515
5962
  else if (dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
5516
- else if (dbSetup === "supabase") await runSetup(() => setupSupabase(config, cliInput));
5963
+ else if (dbSetup === "supabase") await runSetup(() => setupSupabase(config, resolvedCliInput));
5517
5964
  } else if (database === "mysql" && dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
5518
- else if (database === "mongodb" && dbSetup === "mongodb-atlas") await runSetup(() => setupMongoDBAtlas(config, cliInput));
5965
+ else if (database === "mongodb" && dbSetup === "mongodb-atlas") await runSetup(() => setupMongoDBAtlas(config, resolvedCliInput));
5519
5966
  }
5520
-
5521
5967
  //#endregion
5522
5968
  //#region src/helpers/core/git.ts
5523
5969
  async function initializeGit(projectDir, useGit) {
@@ -5527,7 +5973,7 @@ async function initializeGit(projectDir, useGit) {
5527
5973
  reject: false,
5528
5974
  stderr: "pipe"
5529
5975
  })`git --version`).exitCode !== 0) {
5530
- log.warn(pc.yellow("Git is not installed"));
5976
+ cliLog.warn(pc.yellow("Git is not installed"));
5531
5977
  return Result.ok(void 0);
5532
5978
  }
5533
5979
  const result = await $({
@@ -5551,7 +5997,6 @@ async function initializeGit(projectDir, useGit) {
5551
5997
  })
5552
5998
  });
5553
5999
  }
5554
-
5555
6000
  //#endregion
5556
6001
  //#region src/utils/docker-utils.ts
5557
6002
  async function isDockerInstalled() {
@@ -5603,7 +6048,6 @@ async function getDockerStatus(database) {
5603
6048
  running: true
5604
6049
  };
5605
6050
  }
5606
-
5607
6051
  //#endregion
5608
6052
  //#region src/helpers/core/post-installation.ts
5609
6053
  async function displayPostInstallInstructions(config) {
@@ -5784,7 +6228,6 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
5784
6228
  else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
5785
6229
  return instructions.length ? `\n${instructions.join("\n")}` : "";
5786
6230
  }
5787
-
5788
6231
  //#endregion
5789
6232
  //#region src/helpers/core/create-project.ts
5790
6233
  /**
@@ -5872,7 +6315,6 @@ async function setPackageManagerVersion(projectDir, packageManager) {
5872
6315
  })
5873
6316
  });
5874
6317
  }
5875
-
5876
6318
  //#endregion
5877
6319
  //#region src/helpers/core/command-handlers.ts
5878
6320
  /**
@@ -5955,21 +6397,32 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
5955
6397
  });
5956
6398
  }
5957
6399
  }));
6400
+ yield* validateResolvedProjectPathInput(currentPathInput);
5958
6401
  let finalPathInput;
5959
6402
  let shouldClearDirectory;
5960
6403
  const conflictResult = yield* Result.await(handleDirectoryConflictResult(currentPathInput, input.directoryConflict));
5961
6404
  finalPathInput = conflictResult.finalPathInput;
5962
6405
  shouldClearDirectory = conflictResult.shouldClearDirectory;
5963
- const { finalResolvedPath, finalBaseName } = yield* Result.await(Result.tryPromise({
5964
- try: async () => setupProjectDirectory(finalPathInput, shouldClearDirectory),
5965
- catch: (e) => {
5966
- if (e instanceof UserCancelledError) return e;
5967
- return new CLIError({
5968
- message: e instanceof Error ? e.message : String(e),
5969
- cause: e
5970
- });
5971
- }
5972
- }));
6406
+ yield* validateResolvedProjectPathInput(finalPathInput);
6407
+ let finalResolvedPath;
6408
+ let finalBaseName;
6409
+ if (input.dryRun) {
6410
+ finalResolvedPath = finalPathInput === "." ? process.cwd() : path.resolve(process.cwd(), finalPathInput);
6411
+ finalBaseName = path.basename(finalResolvedPath);
6412
+ } else {
6413
+ const setupResult = yield* Result.await(Result.tryPromise({
6414
+ try: async () => setupProjectDirectory(finalPathInput, shouldClearDirectory),
6415
+ catch: (e) => {
6416
+ if (e instanceof UserCancelledError) return e;
6417
+ return new CLIError({
6418
+ message: e instanceof Error ? e.message : String(e),
6419
+ cause: e
6420
+ });
6421
+ }
6422
+ }));
6423
+ finalResolvedPath = setupResult.finalResolvedPath;
6424
+ finalBaseName = setupResult.finalBaseName;
6425
+ }
5973
6426
  const originalInput = {
5974
6427
  ...input,
5975
6428
  projectDirectory: input.projectName
@@ -6043,8 +6496,38 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
6043
6496
  }
6044
6497
  }));
6045
6498
  }
6046
- yield* Result.await(createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }));
6499
+ const effectiveDbSetupOptions = mergeResolvedDbSetupOptions(config.dbSetup, config.dbSetupOptions, {
6500
+ manualDb: cliInput.manualDb ?? input.manualDb,
6501
+ dbSetupOptions: cliInput.dbSetupOptions ?? input.dbSetupOptions
6502
+ });
6503
+ if (effectiveDbSetupOptions) config = {
6504
+ ...config,
6505
+ dbSetupOptions: effectiveDbSetupOptions
6506
+ };
6047
6507
  const reproducibleCommand = generateReproducibleCommand(config);
6508
+ if (input.dryRun) {
6509
+ const elapsedTimeMs = Date.now() - startTime;
6510
+ if (!isSilent()) {
6511
+ if (shouldClearDirectory) log.warn(pc.yellow(`Dry run: directory "${finalPathInput}" would be cleared due to overwrite strategy.`));
6512
+ log.success(pc.green("Dry run validation passed. No files were written."));
6513
+ log.message(pc.dim(`Target directory: ${finalResolvedPath}`));
6514
+ log.message(pc.dim(`Run without --dry-run to create the project.`));
6515
+ outro(pc.magenta("Dry run complete."));
6516
+ }
6517
+ return Result.ok({
6518
+ success: true,
6519
+ projectConfig: config,
6520
+ reproducibleCommand,
6521
+ timeScaffolded,
6522
+ elapsedTimeMs,
6523
+ projectDirectory: config.projectDir,
6524
+ relativePath: config.relativePath
6525
+ });
6526
+ }
6527
+ yield* Result.await(createProject(config, {
6528
+ manualDb: cliInput.manualDb ?? input.manualDb,
6529
+ dbSetupOptions: effectiveDbSetupOptions
6530
+ }));
6048
6531
  if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
6049
6532
  await trackProjectCreation(config, input.disableAnalytics);
6050
6533
  const historyResult = await addToHistory(config, reproducibleCommand);
@@ -6070,16 +6553,24 @@ function isPathWithinCwd(targetPath) {
6070
6553
  const rel = path.relative(process.cwd(), resolved);
6071
6554
  return !rel.startsWith("..") && !path.isAbsolute(rel);
6072
6555
  }
6073
- async function resolveProjectNameForSilent(input) {
6074
- const defaultConfig = getDefaultConfig();
6075
- const candidate = (input.projectName?.trim() || void 0) ?? defaultConfig.relativePath;
6076
- if (candidate === ".") return Result.ok(candidate);
6556
+ function validateResolvedProjectPathInput(candidate) {
6557
+ const hardeningResult = validateAgentSafePathInput(candidate, "projectName");
6558
+ if (hardeningResult.isErr()) return Result.err(new CLIError({
6559
+ message: hardeningResult.error.message,
6560
+ cause: hardeningResult.error
6561
+ }));
6562
+ if (candidate === ".") return Result.ok(void 0);
6077
6563
  const validationResult = validateProjectName(path.basename(candidate));
6078
6564
  if (validationResult.isErr()) return Result.err(new CLIError({
6079
6565
  message: validationResult.error.message,
6080
6566
  cause: validationResult.error
6081
6567
  }));
6082
6568
  if (!isPathWithinCwd(candidate)) return Result.err(new CLIError({ message: "Project path must be within current directory" }));
6569
+ return Result.ok(void 0);
6570
+ }
6571
+ async function resolveProjectNameForSilent(input) {
6572
+ const defaultConfig = getDefaultConfig();
6573
+ const candidate = (input.projectName?.trim() || void 0) ?? defaultConfig.relativePath;
6083
6574
  return Result.ok(candidate);
6084
6575
  }
6085
6576
  async function handleDirectoryConflictResult(currentPathInput, strategy) {
@@ -6132,9 +6623,51 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
6132
6623
  default: return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
6133
6624
  }
6134
6625
  }
6135
-
6136
6626
  //#endregion
6137
6627
  //#region src/index.ts
6628
+ const SchemaNameSchema = z.enum([
6629
+ "all",
6630
+ "cli",
6631
+ "database",
6632
+ "orm",
6633
+ "backend",
6634
+ "runtime",
6635
+ "frontend",
6636
+ "addons",
6637
+ "examples",
6638
+ "packageManager",
6639
+ "databaseSetup",
6640
+ "api",
6641
+ "auth",
6642
+ "payments",
6643
+ "webDeploy",
6644
+ "serverDeploy",
6645
+ "directoryConflict",
6646
+ "template",
6647
+ "addonOptions",
6648
+ "dbSetupOptions",
6649
+ "createInput",
6650
+ "addInput",
6651
+ "projectConfig",
6652
+ "betterTStackConfig",
6653
+ "initResult"
6654
+ ]).default("all");
6655
+ function getCliSchemaJson() {
6656
+ return createCli({
6657
+ router,
6658
+ name: "create-better-t-stack",
6659
+ version: getLatestCLIVersion()
6660
+ }).toJSON();
6661
+ }
6662
+ function getSchemaResult(name) {
6663
+ const schemas = getAllJsonSchemas();
6664
+ if (name === "all") return {
6665
+ cli: getCliSchemaJson(),
6666
+ schemas
6667
+ };
6668
+ if (name === "cli") return getCliSchemaJson();
6669
+ return schemas[name];
6670
+ }
6138
6671
  const router = os.router({
6139
6672
  create: os.meta({
6140
6673
  description: "Create a new Better-T-Stack project",
@@ -6144,6 +6677,7 @@ const router = os.router({
6144
6677
  template: types_exports.TemplateSchema.optional().describe("Use a predefined template"),
6145
6678
  yes: z.boolean().optional().default(false).describe("Use default configuration"),
6146
6679
  yolo: z.boolean().optional().default(false).describe("(WARNING - NOT RECOMMENDED) Bypass validations and compatibility checks"),
6680
+ dryRun: z.boolean().optional().default(false).describe("Validate setup without writing files"),
6147
6681
  verbose: z.boolean().optional().default(false).describe("Show detailed result information"),
6148
6682
  database: types_exports.DatabaseSchema.optional(),
6149
6683
  orm: types_exports.ORMSchema.optional(),
@@ -6164,15 +6698,26 @@ const router = os.router({
6164
6698
  directoryConflict: types_exports.DirectoryConflictSchema.optional(),
6165
6699
  renderTitle: z.boolean().optional(),
6166
6700
  disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics"),
6167
- manualDb: z.boolean().optional().default(false).describe("Skip automatic/manual database setup prompt and use manual setup")
6701
+ manualDb: z.boolean().optional().default(false).describe("Skip automatic/manual database setup prompt and use manual setup"),
6702
+ dbSetupOptions: types_exports.DbSetupOptionsSchema.optional().describe("Structured database setup options")
6168
6703
  })])).handler(async ({ input }) => {
6169
6704
  const [projectName, options] = input;
6170
6705
  const result = await createProjectHandler({
6171
6706
  projectName,
6172
6707
  ...options
6173
6708
  });
6174
- if (options.verbose) return result;
6709
+ if (options.verbose || options.dryRun) return result;
6710
+ }),
6711
+ createJson: os.meta({
6712
+ description: "Create a project from a raw JSON payload (agent-friendly)",
6713
+ jsonInput: true
6714
+ }).input(types_exports.CreateInputSchema).handler(async ({ input }) => {
6715
+ const result = await createProjectHandler(input, { silent: true });
6716
+ if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
6717
+ if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
6718
+ return result;
6175
6719
  }),
6720
+ schema: os.meta({ description: "Show runtime CLI and input schemas as JSON" }).input(z.object({ name: SchemaNameSchema.describe("Schema name to inspect") })).handler(({ input }) => getSchemaResult(input.name)),
6176
6721
  sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(showSponsorsCommand),
6177
6722
  docs: os.meta({ description: "Open Better-T-Stack documentation" }).handler(openDocsCommand),
6178
6723
  builder: os.meta({ description: "Open the web-based stack builder" }).handler(openBuilderCommand),
@@ -6184,6 +6729,15 @@ const router = os.router({
6184
6729
  })).handler(async ({ input }) => {
6185
6730
  await addHandler(input);
6186
6731
  }),
6732
+ addJson: os.meta({
6733
+ description: "Add addons from a raw JSON payload (agent-friendly)",
6734
+ jsonInput: true
6735
+ }).input(types_exports.AddInputSchema).handler(async ({ input }) => {
6736
+ const result = await addHandler(input, { silent: true });
6737
+ if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
6738
+ if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
6739
+ return result;
6740
+ }),
6187
6741
  history: os.meta({ description: "Show project creation history" }).input(z.object({
6188
6742
  limit: z.number().optional().default(10).describe("Number of entries to show"),
6189
6743
  clear: z.boolean().optional().default(false).describe("Clear all history"),
@@ -6192,7 +6746,6 @@ const router = os.router({
6192
6746
  await historyHandler(input);
6193
6747
  })
6194
6748
  });
6195
- const caller = createRouterClient(router, { context: {} });
6196
6749
  function createBtsCli() {
6197
6750
  return createCli({
6198
6751
  router,
@@ -6253,13 +6806,13 @@ async function create(projectName, options) {
6253
6806
  });
6254
6807
  }
6255
6808
  async function sponsors() {
6256
- return caller.sponsors();
6809
+ return showSponsorsCommand();
6257
6810
  }
6258
6811
  async function docs() {
6259
- return caller.docs();
6812
+ return openDocsCommand();
6260
6813
  }
6261
6814
  async function builder() {
6262
- return caller.builder();
6815
+ return openBuilderCommand();
6263
6816
  }
6264
6817
  /**
6265
6818
  * Programmatic API to generate a project in-memory (virtual filesystem).
@@ -6290,6 +6843,8 @@ async function createVirtual(options) {
6290
6843
  projectName: options.projectName || "my-project",
6291
6844
  projectDir: "/virtual",
6292
6845
  relativePath: "./virtual",
6846
+ addonOptions: options.addonOptions,
6847
+ dbSetupOptions: options.dbSetupOptions,
6293
6848
  database: options.database || "none",
6294
6849
  orm: options.orm || "none",
6295
6850
  backend: options.backend || "hono",
@@ -6330,6 +6885,5 @@ async function createVirtual(options) {
6330
6885
  async function add(options = {}) {
6331
6886
  return addHandler(options, { silent: true });
6332
6887
  }
6333
-
6334
6888
  //#endregion
6335
- export { DatabaseSetupError as _, VirtualFileSystem$1 as a, UserCancelledError as b, create as c, docs as d, generate$1 as f, CompatibilityError as g, CLIError as h, TEMPLATE_COUNT as i, createBtsCli as l, sponsors as m, GeneratorError$1 as n, add as o, router as p, Result$1 as r, builder as s, EMBEDDED_TEMPLATES$1 as t, createVirtual as u, DirectoryConflictError as v, ValidationError as x, ProjectCreationError as y };
6889
+ export { ProjectCreationError as C, DirectoryConflictError as S, ValidationError as T, types_exports as _, TEMPLATE_COUNT as a, CompatibilityError as b, builder as c, createVirtual as d, docs as f, sponsors as g, router as h, SchemaNameSchema as i, create as l, getSchemaResult as m, GeneratorError$1 as n, VirtualFileSystem$1 as o, generate$1 as p, Result$1 as r, add as s, EMBEDDED_TEMPLATES$1 as t, createBtsCli as u, getLatestCLIVersion as v, UserCancelledError as w, DatabaseSetupError as x, CLIError as y };