create-better-t-stack 3.22.3 → 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,9 @@
1
1
  #!/usr/bin/env node
2
- import { n as __reExport, t as __exportAll } from "./chunk-C8ucw2H5.mjs";
3
- import { Result, TaggedError } from "better-result";
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";
5
+ import { Result, Result as Result$1, TaggedError } from "better-result";
6
+ import { createCli } from "trpc-cli";
4
7
  import z from "zod";
5
8
  import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
6
9
  import pc from "picocolors";
@@ -8,7 +11,7 @@ import envPaths from "env-paths";
8
11
  import fs from "fs-extra";
9
12
  import path from "node:path";
10
13
  import { fileURLToPath } from "node:url";
11
- import { EMBEDDED_TEMPLATES, VirtualFileSystem, dependencyVersionMap, generate, 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";
12
15
  import consola, { consola as consola$1 } from "consola";
13
16
  import gradient from "gradient-string";
14
17
  import { $, execa } from "execa";
@@ -16,9 +19,8 @@ import { writeTree } from "@better-t-stack/template-generator/fs-writer";
16
19
  import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
17
20
  import { AsyncLocalStorage } from "node:async_hooks";
18
21
  import { applyEdits, modify, parse } from "jsonc-parser";
19
- import os from "node:os";
22
+ import os$1 from "node:os";
20
23
  import { format } from "oxfmt";
21
-
22
24
  //#region src/utils/get-package-manager.ts
23
25
  const getUserPkgManager = () => {
24
26
  const userAgent = process.env.npm_config_user_agent;
@@ -26,7 +28,6 @@ const getUserPkgManager = () => {
26
28
  if (userAgent?.startsWith("bun")) return "bun";
27
29
  return "npm";
28
30
  };
29
-
30
31
  //#endregion
31
32
  //#region src/constants.ts
32
33
  const __filename = fileURLToPath(import.meta.url);
@@ -81,6 +82,7 @@ const ADDON_COMPATIBILITY = {
81
82
  husky: [],
82
83
  lefthook: [],
83
84
  turborepo: [],
85
+ nx: [],
84
86
  starlight: [],
85
87
  ultracite: [],
86
88
  ruler: [],
@@ -92,7 +94,6 @@ const ADDON_COMPATIBILITY = {
92
94
  skills: [],
93
95
  none: []
94
96
  };
95
-
96
97
  //#endregion
97
98
  //#region src/utils/errors.ts
98
99
  /**
@@ -181,14 +182,14 @@ function displayError(error) {
181
182
  if (UserCancelledError.is(error)) cancel(pc.red(error.message));
182
183
  else consola.error(pc.red(error.message));
183
184
  }
184
-
185
185
  //#endregion
186
186
  //#region src/utils/get-latest-cli-version.ts
187
187
  function getLatestCLIVersionResult() {
188
188
  const packageJsonPath = path.join(PKG_ROOT, "package.json");
189
189
  return Result.try({
190
190
  try: () => {
191
- return fs.readJSONSync(packageJsonPath).version;
191
+ const packageJsonContent = fs.readJSONSync(packageJsonPath);
192
+ return String(packageJsonContent.version ?? "1.0.0");
192
193
  },
193
194
  catch: (e) => new CLIError({
194
195
  message: `Failed to read CLI version from package.json: ${e instanceof Error ? e.message : String(e)}`,
@@ -199,7 +200,6 @@ function getLatestCLIVersionResult() {
199
200
  function getLatestCLIVersion() {
200
201
  return getLatestCLIVersionResult().unwrapOr("1.0.0");
201
202
  }
202
-
203
203
  //#endregion
204
204
  //#region src/utils/project-history.ts
205
205
  const paths = envPaths("better-t-stack", { suffix: "" });
@@ -312,7 +312,6 @@ async function clearHistory() {
312
312
  })
313
313
  });
314
314
  }
315
-
316
315
  //#endregion
317
316
  //#region src/utils/render-title.ts
318
317
  const TITLE_TEXT = `
@@ -349,7 +348,6 @@ const renderTitle = () => {
349
348
  if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Better T Stack`));
350
349
  else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
351
350
  };
352
-
353
351
  //#endregion
354
352
  //#region src/commands/history.ts
355
353
  function formatStackSummary(entry) {
@@ -408,7 +406,6 @@ async function historyHandler(input) {
408
406
  log.message("");
409
407
  }
410
408
  }
411
-
412
409
  //#endregion
413
410
  //#region src/utils/open-url.ts
414
411
  async function openUrl(url) {
@@ -424,7 +421,6 @@ async function openUrl(url) {
424
421
  }
425
422
  await $({ stdio: "ignore" })`xdg-open ${url}`;
426
423
  }
427
-
428
424
  //#endregion
429
425
  //#region src/utils/sponsors.ts
430
426
  const SPONSORS_JSON_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
@@ -588,7 +584,6 @@ function normalizeSponsorFetchError(error) {
588
584
  cause: error
589
585
  });
590
586
  }
591
-
592
587
  //#endregion
593
588
  //#region src/commands/meta.ts
594
589
  const DOCS_URL = "https://better-t-stack.dev/docs";
@@ -617,13 +612,11 @@ async function openDocsCommand() {
617
612
  async function openBuilderCommand() {
618
613
  await openExternalUrl(BUILDER_URL, "Opened builder in your default browser.");
619
614
  }
620
-
621
615
  //#endregion
622
616
  //#region src/types.ts
623
617
  var types_exports = /* @__PURE__ */ __exportAll({});
624
618
  import * as import__better_t_stack_types from "@better-t-stack/types";
625
619
  __reExport(types_exports, import__better_t_stack_types);
626
-
627
620
  //#endregion
628
621
  //#region src/utils/compatibility.ts
629
622
  const WEB_FRAMEWORKS = [
@@ -636,7 +629,6 @@ const WEB_FRAMEWORKS = [
636
629
  "solid",
637
630
  "astro"
638
631
  ];
639
-
640
632
  //#endregion
641
633
  //#region src/utils/compatibility-rules.ts
642
634
  function validationErr$1(message) {
@@ -759,6 +751,7 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth) {
759
751
  });
760
752
  }
761
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.");
762
755
  for (const addon of addons) {
763
756
  if (addon === "none") continue;
764
757
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
@@ -791,7 +784,6 @@ function validateExamplesCompatibility(examples, backend, database, frontend, ap
791
784
  }
792
785
  return Result.ok(void 0);
793
786
  }
794
-
795
787
  //#endregion
796
788
  //#region src/utils/context.ts
797
789
  const cliStorage = new AsyncLocalStorage();
@@ -844,14 +836,12 @@ async function runWithContextAsync(options, fn) {
844
836
  };
845
837
  return cliStorage.run(ctx, fn);
846
838
  }
847
-
848
839
  //#endregion
849
840
  //#region src/utils/navigation.ts
850
841
  const GO_BACK_SYMBOL = Symbol("clack:goBack");
851
842
  function isGoBack(value) {
852
843
  return value === GO_BACK_SYMBOL;
853
844
  }
854
-
855
845
  //#endregion
856
846
  //#region src/prompts/navigable.ts
857
847
  /**
@@ -890,6 +880,9 @@ function getHint() {
890
880
  function getMultiHint() {
891
881
  return isFirstPrompt() ? KEYBOARD_HINT_MULTI_FIRST : KEYBOARD_HINT_MULTI;
892
882
  }
883
+ function normalizeValidationMessage(validationMessage) {
884
+ return validationMessage instanceof Error ? validationMessage.message : validationMessage;
885
+ }
893
886
  async function runWithNavigation(prompt) {
894
887
  let goBack = false;
895
888
  prompt.on("key", (char) => {
@@ -948,6 +941,7 @@ async function navigableMultiselect(opts) {
948
941
  required,
949
942
  validate(selected) {
950
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));
951
945
  },
952
946
  render() {
953
947
  const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
@@ -1031,6 +1025,7 @@ async function navigableGroupMultiselect(opts) {
1031
1025
  selectableGroups: true,
1032
1026
  validate(selected) {
1033
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));
1034
1029
  },
1035
1030
  render() {
1036
1031
  const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
@@ -1077,7 +1072,6 @@ async function navigableGroupMultiselect(opts) {
1077
1072
  }
1078
1073
  }));
1079
1074
  }
1080
-
1081
1075
  //#endregion
1082
1076
  //#region src/prompts/addons.ts
1083
1077
  function getAddonDisplay(addon) {
@@ -1088,6 +1082,10 @@ function getAddonDisplay(addon) {
1088
1082
  label = "Turborepo";
1089
1083
  hint = "High-performance build system";
1090
1084
  break;
1085
+ case "nx":
1086
+ label = "Nx";
1087
+ hint = "Smart monorepo orchestration and task graph";
1088
+ break;
1091
1089
  case "pwa":
1092
1090
  label = "PWA";
1093
1091
  hint = "Make your app installable and work offline";
@@ -1142,7 +1140,7 @@ function getAddonDisplay(addon) {
1142
1140
  break;
1143
1141
  case "mcp":
1144
1142
  label = "MCP";
1145
- hint = "Install MCP servers (docs, databases, SaaS) via add-mcp";
1143
+ hint = "Install MCP servers, including Better T Stack, via add-mcp";
1146
1144
  break;
1147
1145
  default:
1148
1146
  label = addon;
@@ -1154,8 +1152,8 @@ function getAddonDisplay(addon) {
1154
1152
  };
1155
1153
  }
1156
1154
  const ADDON_GROUPS = {
1157
- Tooling: [
1158
- "turborepo",
1155
+ "Monorepo & Tasks": ["turborepo", "nx"],
1156
+ "Code Quality": [
1159
1157
  "biome",
1160
1158
  "oxlint",
1161
1159
  "ultracite",
@@ -1163,100 +1161,91 @@ const ADDON_GROUPS = {
1163
1161
  "lefthook"
1164
1162
  ],
1165
1163
  Documentation: ["starlight", "fumadocs"],
1166
- Extensions: [
1164
+ "Platform Extensions": [
1167
1165
  "pwa",
1168
1166
  "tauri",
1169
1167
  "opentui",
1170
1168
  "wxt"
1171
1169
  ],
1172
- AI: [
1170
+ "AI & Agent Tools": [
1173
1171
  "ruler",
1174
1172
  "skills",
1175
1173
  "mcp"
1176
1174
  ]
1177
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
+ }
1178
1200
  async function getAddonsChoice(addons, frontends, auth) {
1179
1201
  if (addons !== void 0) return addons;
1180
1202
  const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
1181
- const groupedOptions = {
1182
- Tooling: [],
1183
- Documentation: [],
1184
- Extensions: [],
1185
- AI: []
1186
- };
1203
+ const groupedOptions = createGroupedOptions();
1187
1204
  const frontendsArray = frontends || [];
1188
1205
  for (const addon of allAddons) {
1189
1206
  const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
1190
1207
  if (!isCompatible) continue;
1191
1208
  const { label, hint } = getAddonDisplay(addon);
1192
- const option = {
1209
+ addOptionToGroup(groupedOptions, {
1193
1210
  value: addon,
1194
1211
  label,
1195
1212
  hint
1196
- };
1197
- if (ADDON_GROUPS.Tooling.includes(addon)) groupedOptions.Tooling.push(option);
1198
- else if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
1199
- else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
1200
- else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
1213
+ });
1201
1214
  }
1202
- Object.keys(groupedOptions).forEach((group) => {
1203
- if (groupedOptions[group].length === 0) delete groupedOptions[group];
1204
- else {
1205
- const groupOrder = ADDON_GROUPS[group] || [];
1206
- groupedOptions[group].sort((a, b) => {
1207
- return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
1208
- });
1209
- }
1210
- });
1215
+ sortAndPruneGroupedOptions(groupedOptions);
1211
1216
  const response = await navigableGroupMultiselect({
1212
1217
  message: "Select addons",
1213
1218
  options: groupedOptions,
1214
1219
  initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
1215
- required: false
1220
+ required: false,
1221
+ validate: validateAddonSelection
1216
1222
  });
1217
1223
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1218
1224
  return response;
1219
1225
  }
1220
1226
  async function getAddonsToAdd(frontend, existingAddons = [], auth) {
1221
- const groupedOptions = {
1222
- Tooling: [],
1223
- Documentation: [],
1224
- Extensions: [],
1225
- AI: []
1226
- };
1227
+ const groupedOptions = createGroupedOptions();
1227
1228
  const frontendArray = frontend || [];
1228
1229
  const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons, auth);
1229
1230
  for (const addon of compatibleAddons) {
1230
1231
  const { label, hint } = getAddonDisplay(addon);
1231
- const option = {
1232
+ addOptionToGroup(groupedOptions, {
1232
1233
  value: addon,
1233
1234
  label,
1234
1235
  hint
1235
- };
1236
- if (ADDON_GROUPS.Tooling.includes(addon)) groupedOptions.Tooling.push(option);
1237
- else if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
1238
- else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
1239
- else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
1236
+ });
1240
1237
  }
1241
- Object.keys(groupedOptions).forEach((group) => {
1242
- if (groupedOptions[group].length === 0) delete groupedOptions[group];
1243
- else {
1244
- const groupOrder = ADDON_GROUPS[group] || [];
1245
- groupedOptions[group].sort((a, b) => {
1246
- return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
1247
- });
1248
- }
1249
- });
1238
+ sortAndPruneGroupedOptions(groupedOptions);
1250
1239
  if (Object.keys(groupedOptions).length === 0) return [];
1251
1240
  const response = await navigableGroupMultiselect({
1252
1241
  message: "Select addons to add",
1253
1242
  options: groupedOptions,
1254
- required: false
1243
+ required: false,
1244
+ validate: validateAddonSelection
1255
1245
  });
1256
1246
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1257
1247
  return response;
1258
1248
  }
1259
-
1260
1249
  //#endregion
1261
1250
  //#region src/utils/bts-config.ts
1262
1251
  const BTS_CONFIG_FILE = "bts.jsonc";
@@ -1287,7 +1276,26 @@ async function updateBtsConfig(projectDir, updates) {
1287
1276
  await fs.writeFile(configPath, content, "utf-8");
1288
1277
  } catch {}
1289
1278
  }
1290
-
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
+ }
1291
1299
  //#endregion
1292
1300
  //#region src/utils/add-package-deps.ts
1293
1301
  const addPackageDependency = async (opts) => {
@@ -1310,13 +1318,11 @@ const addPackageDependency = async (opts) => {
1310
1318
  for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
1311
1319
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
1312
1320
  };
1313
-
1314
1321
  //#endregion
1315
1322
  //#region src/utils/external-commands.ts
1316
1323
  function shouldSkipExternalCommands() {
1317
1324
  return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
1318
1325
  }
1319
-
1320
1326
  //#endregion
1321
1327
  //#region src/utils/package-runner.ts
1322
1328
  function splitCommandArgs(commandWithArgs) {
@@ -1411,7 +1417,33 @@ function getPackageRunnerPrefix(packageManager) {
1411
1417
  default: return ["npx"];
1412
1418
  }
1413
1419
  }
1414
-
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
+ };
1415
1447
  //#endregion
1416
1448
  //#region src/helpers/addons/fumadocs-setup.ts
1417
1449
  const TEMPLATES$2 = {
@@ -1451,22 +1483,31 @@ const TEMPLATES$2 = {
1451
1483
  value: "tanstack-start-spa"
1452
1484
  }
1453
1485
  };
1486
+ const DEFAULT_TEMPLATE$2 = "next-mdx";
1487
+ const DEFAULT_DEV_PORT$1 = 4e3;
1454
1488
  async function setupFumadocs(config) {
1455
1489
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
1456
1490
  const { packageManager, projectDir } = config;
1457
- log.info("Setting up Fumadocs...");
1458
- const template = await select({
1459
- message: "Choose a template",
1460
- options: Object.entries(TEMPLATES$2).map(([key, template]) => ({
1461
- value: key,
1462
- label: template.label,
1463
- hint: template.hint
1464
- })),
1465
- initialValue: "next-mdx"
1466
- });
1467
- 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
+ }
1468
1508
  const templateArg = TEMPLATES$2[template].value;
1469
1509
  const isNextTemplate = template.startsWith("next-");
1510
+ const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT$1;
1470
1511
  const options = [
1471
1512
  `--template ${templateArg}`,
1472
1513
  `--pm ${packageManager}`,
@@ -1477,7 +1518,7 @@ async function setupFumadocs(config) {
1477
1518
  const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs ${options.join(" ")}`);
1478
1519
  const appsDir = path.join(projectDir, "apps");
1479
1520
  await fs.ensureDir(appsDir);
1480
- const s = spinner();
1521
+ const s = createSpinner();
1481
1522
  s.start("Running Fumadocs create command...");
1482
1523
  const result = await Result.tryPromise({
1483
1524
  try: async () => {
@@ -1490,7 +1531,7 @@ async function setupFumadocs(config) {
1490
1531
  if (await fs.pathExists(packageJsonPath)) {
1491
1532
  const packageJson = await fs.readJson(packageJsonPath);
1492
1533
  packageJson.name = "fumadocs";
1493
- 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}`;
1494
1535
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1495
1536
  }
1496
1537
  },
@@ -1507,10 +1548,24 @@ async function setupFumadocs(config) {
1507
1548
  s.stop("Fumadocs setup complete!");
1508
1549
  return Result.ok(void 0);
1509
1550
  }
1510
-
1511
1551
  //#endregion
1512
1552
  //#region src/helpers/addons/mcp-setup.ts
1513
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
+ },
1514
1569
  {
1515
1570
  value: "cursor",
1516
1571
  label: "Cursor",
@@ -1536,6 +1591,16 @@ const MCP_AGENTS = [
1536
1591
  label: "Gemini CLI",
1537
1592
  scope: "both"
1538
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
+ },
1539
1604
  {
1540
1605
  value: "vscode",
1541
1606
  label: "VS Code (GitHub Copilot)",
@@ -1557,6 +1622,12 @@ const MCP_AGENTS = [
1557
1622
  scope: "global"
1558
1623
  }
1559
1624
  ];
1625
+ const DEFAULT_SCOPE$1 = "project";
1626
+ const DEFAULT_AGENTS$2 = [
1627
+ "cursor",
1628
+ "claude-code",
1629
+ "vscode"
1630
+ ];
1560
1631
  function uniqueValues$1(values) {
1561
1632
  return Array.from(new Set(values));
1562
1633
  }
@@ -1566,105 +1637,138 @@ function hasReactBasedFrontend$1(frontend) {
1566
1637
  function hasNativeFrontend$1(frontend) {
1567
1638
  return frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
1568
1639
  }
1569
- function getRecommendedMcpServers(config) {
1570
- const servers = [];
1571
- servers.push({
1572
- key: "context7",
1573
- label: "Context7",
1574
- name: "context7",
1575
- target: "@upstash/context7-mcp"
1576
- });
1577
- if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") servers.push({
1578
- key: "cloudflare-docs",
1579
- label: "Cloudflare Docs",
1580
- name: "cloudflare-docs",
1581
- target: "https://docs.mcp.cloudflare.com/sse",
1582
- transport: "sse"
1583
- });
1584
- if (config.backend === "convex") servers.push({
1585
- key: "convex",
1586
- label: "Convex",
1587
- name: "convex",
1588
- target: "npx -y convex@latest mcp start"
1589
- });
1590
- if (hasReactBasedFrontend$1(config.frontend)) servers.push({
1591
- key: "shadcn",
1592
- label: "shadcn/ui",
1593
- name: "shadcn",
1594
- target: "npx -y shadcn@latest mcp"
1595
- });
1596
- if (config.frontend.includes("next")) servers.push({
1597
- key: "next-devtools",
1598
- label: "Next Devtools",
1599
- name: "next-devtools",
1600
- target: "npx -y next-devtools-mcp@latest"
1601
- });
1602
- if (config.frontend.includes("nuxt")) servers.push({
1603
- key: "nuxt-docs",
1604
- label: "Nuxt Docs",
1605
- name: "nuxt",
1606
- target: "https://nuxt.com/mcp"
1607
- }, {
1608
- key: "nuxt-ui-docs",
1609
- label: "Nuxt UI Docs",
1610
- name: "nuxt-ui",
1611
- target: "https://ui.nuxt.com/mcp"
1612
- });
1613
- if (config.frontend.includes("svelte")) servers.push({
1614
- key: "svelte-docs",
1615
- label: "Svelte Docs",
1616
- name: "svelte",
1617
- target: "https://mcp.svelte.dev/mcp"
1618
- });
1619
- if (config.frontend.includes("astro")) servers.push({
1620
- key: "astro-docs",
1621
- label: "Astro Docs",
1622
- name: "astro-docs",
1623
- target: "https://mcp.docs.astro.build/mcp"
1624
- });
1625
- if (config.dbSetup === "planetscale") servers.push({
1626
- key: "planetscale",
1627
- label: "PlanetScale",
1628
- name: "planetscale",
1629
- target: "https://mcp.pscale.dev/mcp/planetscale"
1630
- });
1631
- if (config.dbSetup === "neon") servers.push({
1632
- key: "neon",
1633
- label: "Neon",
1634
- name: "neon",
1635
- target: "https://mcp.neon.tech/mcp"
1636
- });
1637
- if (config.dbSetup === "supabase") servers.push({
1638
- key: "supabase",
1639
- label: "Supabase",
1640
- name: "supabase",
1641
- target: "https://mcp.supabase.com/mcp"
1642
- });
1643
- if (config.auth === "better-auth") servers.push({
1644
- key: "better-auth",
1645
- label: "Better Auth",
1646
- name: "better-auth",
1647
- target: "https://mcp.inkeep.com/better-auth/mcp"
1648
- });
1649
- if (config.auth === "clerk") servers.push({
1650
- key: "clerk",
1651
- label: "Clerk",
1652
- name: "clerk",
1653
- target: "https://mcp.clerk.com/mcp"
1654
- });
1655
- if (hasNativeFrontend$1(config.frontend)) servers.push({
1656
- key: "expo",
1657
- label: "Expo",
1658
- name: "expo-mcp",
1659
- target: "https://mcp.expo.dev/mcp"
1660
- });
1661
- if (config.payments === "polar") servers.push({
1662
- key: "polar",
1663
- label: "Polar",
1664
- name: "polar",
1665
- target: "https://mcp.polar.sh/mcp/polar-mcp"
1666
- });
1667
- 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);
1668
1772
  }
1669
1773
  function filterAgentsForScope(scope) {
1670
1774
  return MCP_AGENTS.filter((a) => a.scope === "both" || a.scope === scope);
@@ -1672,67 +1776,83 @@ function filterAgentsForScope(scope) {
1672
1776
  async function setupMcp(config) {
1673
1777
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
1674
1778
  const { packageManager, projectDir } = config;
1675
- log.info("Setting up MCP servers...");
1676
- const scope = await select({
1677
- message: "Where should MCP servers be installed?",
1678
- options: [{
1679
- value: "project",
1680
- label: "Project",
1681
- hint: "Writes to project config files (recommended for teams)"
1682
- }, {
1683
- value: "global",
1684
- label: "Global",
1685
- hint: "Writes to user-level config files (personal machine)"
1686
- }],
1687
- initialValue: "project"
1688
- });
1689
- if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1690
- 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);
1691
1800
  if (recommendedServers.length === 0) return Result.ok(void 0);
1801
+ const allServersByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
1692
1802
  const serverOptions = recommendedServers.map((s) => ({
1693
1803
  value: s.key,
1694
1804
  label: s.label,
1695
1805
  hint: s.target
1696
1806
  }));
1697
- const selectedServerKeys = await multiselect({
1698
- message: "Select MCP servers to install",
1699
- options: serverOptions,
1700
- required: false,
1701
- initialValues: serverOptions.map((o) => o.value)
1702
- });
1703
- 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
+ }
1704
1821
  if (selectedServerKeys.length === 0) return Result.ok(void 0);
1705
1822
  const agentOptions = filterAgentsForScope(scope).map((a) => ({
1706
1823
  value: a.value,
1707
1824
  label: a.label
1708
1825
  }));
1709
- const selectedAgents = await multiselect({
1710
- message: "Select agents to install MCP servers to",
1711
- options: agentOptions,
1712
- required: false,
1713
- initialValues: uniqueValues$1([
1714
- "cursor",
1715
- "claude-code",
1716
- "vscode"
1717
- ].filter((a) => agentOptions.some((o) => o.value === a)))
1718
- });
1719
- 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
+ }
1720
1840
  if (selectedAgents.length === 0) return Result.ok(void 0);
1721
- const serversByKey = new Map(recommendedServers.map((s) => [s.key, s]));
1722
1841
  const selectedServers = [];
1723
1842
  for (const key of selectedServerKeys) {
1724
- const server = serversByKey.get(key);
1843
+ const server = allServersByKey.get(key);
1725
1844
  if (server) selectedServers.push(server);
1726
1845
  }
1727
1846
  if (selectedServers.length === 0) return Result.ok(void 0);
1728
- const installSpinner = spinner();
1847
+ const installSpinner = createSpinner();
1729
1848
  installSpinner.start("Installing MCP servers...");
1730
1849
  const runner = getPackageRunnerPrefix(packageManager);
1731
1850
  const globalFlags = scope === "global" ? ["-g"] : [];
1851
+ let successfulInstalls = 0;
1732
1852
  for (const server of selectedServers) {
1733
1853
  const transportFlags = server.transport ? ["-t", server.transport] : [];
1734
1854
  const headerFlags = (server.headers ?? []).flatMap((h) => ["--header", h]);
1735
- const agentFlags = selectedAgents.flatMap((a) => ["-a", a]);
1855
+ const agentFlags = selectedAgents.flatMap((agent) => ["-a", agent]);
1736
1856
  const args = [
1737
1857
  ...runner,
1738
1858
  "add-mcp@latest",
@@ -1757,12 +1877,22 @@ async function setupMcp(config) {
1757
1877
  message: `Failed to install MCP server '${server.name}': ${e instanceof Error ? e.message : String(e)}`,
1758
1878
  cause: e
1759
1879
  })
1760
- })).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;
1761
1885
  }
1762
- installSpinner.stop("MCP servers installed");
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
+ }));
1892
+ }
1893
+ installSpinner.stop(successfulInstalls === selectedServers.length ? "MCP servers installed" : "MCP servers installed with warnings");
1763
1894
  return Result.ok(void 0);
1764
1895
  }
1765
-
1766
1896
  //#endregion
1767
1897
  //#region src/helpers/addons/oxlint-setup.ts
1768
1898
  async function setupOxlint(projectDir, packageManager) {
@@ -1782,9 +1912,9 @@ async function setupOxlint(projectDir, packageManager) {
1782
1912
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1783
1913
  }
1784
1914
  if (shouldSkipExternalCommands()) return;
1785
- const s = spinner();
1786
- s.start("Initializing oxlint and oxfmt...");
1915
+ const s = createSpinner();
1787
1916
  try {
1917
+ s.start("Initializing oxlint and oxfmt...");
1788
1918
  const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
1789
1919
  await $({
1790
1920
  cwd: projectDir,
@@ -1808,62 +1938,75 @@ async function setupOxlint(projectDir, packageManager) {
1808
1938
  })
1809
1939
  });
1810
1940
  }
1811
-
1812
1941
  //#endregion
1813
1942
  //#region src/helpers/addons/ruler-setup.ts
1943
+ const DEFAULT_ASSISTANTS = [
1944
+ "agentsmd",
1945
+ "claude",
1946
+ "codex",
1947
+ "cursor"
1948
+ ];
1814
1949
  async function setupRuler(config) {
1815
1950
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
1816
1951
  const { packageManager, projectDir } = config;
1817
- log.info("Setting up Ruler...");
1952
+ cliLog.info("Setting up Ruler...");
1818
1953
  const rulerDir = path.join(projectDir, ".ruler");
1819
1954
  if (!await fs.pathExists(rulerDir)) {
1820
- 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."));
1821
1956
  return Result.ok(void 0);
1822
1957
  }
1823
- const selectedEditors = await autocompleteMultiselect({
1824
- message: "Select AI assistants for Ruler",
1825
- options: Object.entries({
1826
- agentsmd: { label: "Agents.md" },
1827
- aider: { label: "Aider" },
1828
- amazonqcli: { label: "Amazon Q CLI" },
1829
- amp: { label: "AMP" },
1830
- antigravity: { label: "Antigravity" },
1831
- augmentcode: { label: "AugmentCode" },
1832
- claude: { label: "Claude Code" },
1833
- cline: { label: "Cline" },
1834
- codex: { label: "OpenAI Codex CLI" },
1835
- copilot: { label: "GitHub Copilot" },
1836
- crush: { label: "Crush" },
1837
- cursor: { label: "Cursor" },
1838
- factory: { label: "Factory" },
1839
- firebase: { label: "Firebase Studio" },
1840
- firebender: { label: "Firebender" },
1841
- "gemini-cli": { label: "Gemini CLI" },
1842
- goose: { label: "Goose" },
1843
- jules: { label: "Jules" },
1844
- junie: { label: "Junie" },
1845
- kilocode: { label: "Kilo Code" },
1846
- kiro: { label: "Kiro" },
1847
- mistral: { label: "Mistral" },
1848
- opencode: { label: "OpenCode" },
1849
- openhands: { label: "Open Hands" },
1850
- pi: { label: "Pi" },
1851
- qwen: { label: "Qwen" },
1852
- roo: { label: "RooCode" },
1853
- trae: { label: "Trae AI" },
1854
- warp: { label: "Warp" },
1855
- windsurf: { label: "Windsurf" },
1856
- zed: { label: "Zed" }
1857
- }).map(([key, v]) => ({
1858
- value: key,
1859
- label: v.label
1860
- })),
1861
- required: false
1862
- });
1863
- 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
+ }
1864
2007
  if (selectedEditors.length === 0) {
1865
- log.info("No AI assistants selected. To apply rules later, run:");
1866
- 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")}`));
1867
2010
  return Result.ok(void 0);
1868
2011
  }
1869
2012
  const configFile = path.join(rulerDir, "ruler.toml");
@@ -1872,7 +2015,7 @@ async function setupRuler(config) {
1872
2015
  updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
1873
2016
  await fs.writeFile(configFile, updatedConfig);
1874
2017
  await addRulerScriptToPackageJson(projectDir, packageManager);
1875
- const s = spinner();
2018
+ const s = createSpinner();
1876
2019
  s.start("Applying rules with Ruler...");
1877
2020
  const applyResult = await Result.tryPromise({
1878
2021
  try: async () => {
@@ -1898,7 +2041,7 @@ async function setupRuler(config) {
1898
2041
  async function addRulerScriptToPackageJson(projectDir, packageManager) {
1899
2042
  const rootPackageJsonPath = path.join(projectDir, "package.json");
1900
2043
  if (!await fs.pathExists(rootPackageJsonPath)) {
1901
- 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");
1902
2045
  return;
1903
2046
  }
1904
2047
  const packageJson = await fs.readJson(rootPackageJsonPath);
@@ -1907,7 +2050,6 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
1907
2050
  packageJson.scripts["ruler:apply"] = rulerApplyCommand;
1908
2051
  await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
1909
2052
  }
1910
-
1911
2053
  //#endregion
1912
2054
  //#region src/helpers/addons/skills-setup.ts
1913
2055
  const SKILL_SOURCES = {
@@ -1918,6 +2060,7 @@ const SKILL_SOURCES = {
1918
2060
  "vercel-labs/next-skills": { label: "Next.js Best Practices" },
1919
2061
  "nuxt/ui": { label: "Nuxt UI" },
1920
2062
  "heroui-inc/heroui": { label: "HeroUI Native" },
2063
+ "shadcn/ui": { label: "shadcn/ui" },
1921
2064
  "better-auth/skills": { label: "Better Auth" },
1922
2065
  "clerk/skills": { label: "Clerk" },
1923
2066
  "neondatabase/agent-skills": { label: "Neon Database" },
@@ -2032,6 +2175,12 @@ const AVAILABLE_AGENTS = [
2032
2175
  label: "MCPJam"
2033
2176
  }
2034
2177
  ];
2178
+ const DEFAULT_SCOPE = "project";
2179
+ const DEFAULT_AGENTS$1 = [
2180
+ "cursor",
2181
+ "claude-code",
2182
+ "github-copilot"
2183
+ ];
2035
2184
  function hasReactBasedFrontend(frontend) {
2036
2185
  return frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("tanstack-start") || frontend.includes("next");
2037
2186
  }
@@ -2041,7 +2190,10 @@ function hasNativeFrontend(frontend) {
2041
2190
  function getRecommendedSourceKeys(config) {
2042
2191
  const sources = [];
2043
2192
  const { frontend, backend, dbSetup, auth, examples, addons, orm } = config;
2044
- 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
+ }
2045
2197
  if (frontend.includes("next")) sources.push("vercel-labs/next-skills");
2046
2198
  if (frontend.includes("nuxt")) sources.push("nuxt/ui");
2047
2199
  if (frontend.includes("native-uniwind")) sources.push("heroui-inc/heroui");
@@ -2077,6 +2229,7 @@ const CURATED_SKILLS_BY_SOURCE = {
2077
2229
  "vercel-labs/next-skills": () => ["next-best-practices", "next-cache-components"],
2078
2230
  "nuxt/ui": () => ["nuxt-ui"],
2079
2231
  "heroui-inc/heroui": () => ["heroui-native"],
2232
+ "shadcn/ui": () => ["shadcn"],
2080
2233
  "better-auth/skills": () => ["better-auth-best-practices"],
2081
2234
  "clerk/skills": (config) => {
2082
2235
  const skills = [
@@ -2143,11 +2296,15 @@ async function setupSkills(config) {
2143
2296
  const btsConfig = await readBtsConfig(projectDir);
2144
2297
  const fullConfig = btsConfig ? {
2145
2298
  ...config,
2146
- addons: btsConfig.addons ?? config.addons
2299
+ addons: btsConfig.addons ?? config.addons,
2300
+ addonOptions: btsConfig.addonOptions ?? config.addonOptions
2147
2301
  } : config;
2148
2302
  const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
2149
- if (recommendedSourceKeys.length === 0) return Result.ok(void 0);
2150
- 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) => {
2151
2308
  const source = SKILL_SOURCES[sourceKey];
2152
2309
  return getCuratedSkillNamesForSourceKey(sourceKey, fullConfig).map((skillName) => ({
2153
2310
  value: `${sourceKey}::${skillName}`,
@@ -2156,39 +2313,54 @@ async function setupSkills(config) {
2156
2313
  }));
2157
2314
  });
2158
2315
  if (skillOptions.length === 0) return Result.ok(void 0);
2159
- const scope = await select({
2160
- message: "Where should skills be installed?",
2161
- options: [{
2162
- value: "project",
2163
- label: "Project",
2164
- hint: "Writes to project config files (recommended for teams)"
2165
- }, {
2166
- value: "global",
2167
- label: "Global",
2168
- hint: "Writes to user-level config files (personal machine)"
2169
- }],
2170
- initialValue: "project"
2171
- });
2172
- if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2173
- const selectedSkills = await multiselect({
2174
- message: "Select skills to install",
2175
- options: skillOptions,
2176
- required: false,
2177
- initialValues: skillOptions.map((opt) => opt.value)
2178
- });
2179
- 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
+ }
2180
2350
  if (selectedSkills.length === 0) return Result.ok(void 0);
2181
- const selectedAgents = await multiselect({
2182
- message: "Select agents to install skills to",
2183
- options: AVAILABLE_AGENTS,
2184
- required: false,
2185
- initialValues: [
2186
- "cursor",
2187
- "claude-code",
2188
- "github-copilot"
2189
- ]
2190
- });
2191
- 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
+ }
2192
2364
  if (selectedAgents.length === 0) return Result.ok(void 0);
2193
2365
  const skillsBySource = {};
2194
2366
  for (const skillKey of selectedSkills) {
@@ -2196,42 +2368,50 @@ async function setupSkills(config) {
2196
2368
  if (!skillsBySource[source]) skillsBySource[source] = [];
2197
2369
  skillsBySource[source].push(skillName);
2198
2370
  }
2199
- const installSpinner = spinner();
2371
+ const installSpinner = createSpinner();
2200
2372
  installSpinner.start("Installing skills...");
2201
- const agentFlags = selectedAgents.map((a) => `-a ${a}`).join(" ");
2202
- const globalFlag = scope === "global" ? "-g" : "";
2203
- for (const [source, skills] of Object.entries(skillsBySource)) {
2204
- const skillFlags = skills.map((s) => `-s ${s}`).join(" ");
2205
- if ((await Result.tryPromise({
2206
- try: async () => {
2207
- const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${globalFlag} ${skillFlags} ${agentFlags} -y`);
2208
- await $({
2209
- cwd: projectDir,
2210
- env: { CI: "true" }
2211
- })`${args}`;
2212
- },
2213
- catch: (e) => new AddonSetupError({
2214
- addon: "skills",
2215
- message: `Failed to install skills from ${source}: ${e instanceof Error ? e.message : String(e)}`,
2216
- cause: e
2217
- })
2218
- })).isErr()) log.warn(pc.yellow(`Warning: Could not install skills from ${source}`));
2219
- }
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}`));
2220
2400
  installSpinner.stop("Skills installed");
2221
2401
  return Result.ok(void 0);
2222
2402
  }
2223
-
2224
2403
  //#endregion
2225
2404
  //#region src/helpers/addons/starlight-setup.ts
2226
2405
  async function setupStarlight(config) {
2227
2406
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2228
2407
  const { packageManager, projectDir } = config;
2229
- const s = spinner();
2408
+ const s = createSpinner();
2230
2409
  s.start("Setting up Starlight docs...");
2231
2410
  const args = getPackageExecutionArgs(packageManager, `create-astro@latest ${[
2232
2411
  "docs",
2233
2412
  "--template",
2234
2413
  "starlight",
2414
+ "--yes",
2235
2415
  "--no-install",
2236
2416
  "--add",
2237
2417
  "tailwind",
@@ -2260,39 +2440,53 @@ async function setupStarlight(config) {
2260
2440
  s.stop("Starlight docs setup successfully!");
2261
2441
  return Result.ok(void 0);
2262
2442
  }
2263
-
2264
2443
  //#endregion
2265
2444
  //#region src/helpers/addons/tauri-setup.ts
2266
- async function setupTauri(config) {
2267
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2445
+ function buildTauriInitArgs(config) {
2268
2446
  const { packageManager, frontend, projectDir } = config;
2269
- const s = spinner();
2270
- const clientPackageDir = path.join(projectDir, "apps/web");
2271
- if (!await fs.pathExists(clientPackageDir)) return Result.ok(void 0);
2272
- s.start("Setting up Tauri desktop app support...");
2273
2447
  const hasReactRouter = frontend.includes("react-router");
2274
2448
  const hasNuxt = frontend.includes("nuxt");
2275
2449
  const hasSvelte = frontend.includes("svelte");
2276
2450
  const hasNext = frontend.includes("next");
2277
2451
  const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
2278
2452
  const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
2279
- const tauriArgs = [
2453
+ return [
2454
+ ...getPackageRunnerPrefix(packageManager),
2280
2455
  "@tauri-apps/cli@latest",
2281
2456
  "init",
2282
- `--app-name=${path.basename(projectDir)}`,
2283
- `--window-title=${path.basename(projectDir)}`,
2284
- `--frontend-dist=${frontendDist}`,
2285
- `--dev-url=${devUrl}`,
2286
- `--before-dev-command=${packageManager} run dev`,
2287
- `--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`
2288
2470
  ];
2289
- 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
+ });
2290
2484
  const result = await Result.tryPromise({
2291
2485
  try: async () => {
2292
- await $({
2486
+ await execa(command, args, {
2293
2487
  cwd: clientPackageDir,
2294
2488
  env: { CI: "true" }
2295
- })`${[...prefix, ...tauriArgs]}`;
2489
+ });
2296
2490
  },
2297
2491
  catch: (e) => new AddonSetupError({
2298
2492
  addon: "tauri",
@@ -2307,7 +2501,6 @@ async function setupTauri(config) {
2307
2501
  s.stop("Tauri desktop app support configured successfully!");
2308
2502
  return Result.ok(void 0);
2309
2503
  }
2310
-
2311
2504
  //#endregion
2312
2505
  //#region src/helpers/addons/tui-setup.ts
2313
2506
  const TEMPLATES$1 = {
@@ -2324,20 +2517,36 @@ const TEMPLATES$1 = {
2324
2517
  hint: "SolidJS-based OpenTUI template"
2325
2518
  }
2326
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
+ }
2327
2532
  async function setupTui(config) {
2328
2533
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2329
2534
  const { packageManager, projectDir } = config;
2330
- log.info("Setting up OpenTUI...");
2331
- const template = await select({
2332
- message: "Choose a template",
2333
- options: Object.entries(TEMPLATES$1).map(([key, template]) => ({
2334
- value: key,
2335
- label: template.label,
2336
- hint: template.hint
2337
- })),
2338
- initialValue: "core"
2339
- });
2340
- 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
+ }
2341
2550
  const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
2342
2551
  const appsDir = path.join(projectDir, "apps");
2343
2552
  const ensureDirResult = await Result.tryPromise({
@@ -2349,7 +2558,7 @@ async function setupTui(config) {
2349
2558
  })
2350
2559
  });
2351
2560
  if (ensureDirResult.isErr()) return ensureDirResult;
2352
- const s = spinner();
2561
+ const s = createSpinner();
2353
2562
  s.start("Running OpenTUI create command...");
2354
2563
  const initResult = await Result.tryPromise({
2355
2564
  try: async () => {
@@ -2368,13 +2577,50 @@ async function setupTui(config) {
2368
2577
  }
2369
2578
  });
2370
2579
  if (initResult.isErr()) {
2371
- log.error(pc.red("Failed to set up OpenTUI"));
2580
+ cliLog.error(pc.red("Failed to set up OpenTUI"));
2372
2581
  return initResult;
2373
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
+ }
2374
2589
  s.stop("OpenTUI setup complete!");
2375
2590
  return Result.ok(void 0);
2376
2591
  }
2377
-
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
+ }
2378
2624
  //#endregion
2379
2625
  //#region src/helpers/addons/ultracite-setup.ts
2380
2626
  const LINTERS = {
@@ -2433,6 +2679,10 @@ const HOOKS = {
2433
2679
  windsurf: { label: "Windsurf" },
2434
2680
  claude: { label: "Claude" }
2435
2681
  };
2682
+ const DEFAULT_LINTER = "biome";
2683
+ const DEFAULT_EDITORS = ["vscode", "cursor"];
2684
+ const DEFAULT_AGENTS = ["claude", "codex"];
2685
+ const DEFAULT_HOOKS = [];
2436
2686
  function getFrameworksFromFrontend(frontend) {
2437
2687
  const frameworkMap = {
2438
2688
  "tanstack-router": "react",
@@ -2450,70 +2700,7 @@ function getFrameworksFromFrontend(frontend) {
2450
2700
  for (const f of frontend) if (f !== "none" && frameworkMap[f]) frameworks.add(frameworkMap[f]);
2451
2701
  return Array.from(frameworks);
2452
2702
  }
2453
- async function setupUltracite(config, gitHooks) {
2454
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2455
- const { packageManager, projectDir, frontend } = config;
2456
- log.info("Setting up Ultracite...");
2457
- let result;
2458
- const groupResult = await Result.tryPromise({
2459
- try: async () => {
2460
- return await group({
2461
- linter: () => select({
2462
- message: "Choose linter/formatter",
2463
- options: Object.entries(LINTERS).map(([key, linter]) => ({
2464
- value: key,
2465
- label: linter.label,
2466
- hint: linter.hint
2467
- })),
2468
- initialValue: "biome"
2469
- }),
2470
- editors: () => multiselect({
2471
- message: "Choose editors",
2472
- options: Object.entries(EDITORS).map(([key, editor]) => ({
2473
- value: key,
2474
- label: editor.label
2475
- })),
2476
- required: true
2477
- }),
2478
- agents: () => multiselect({
2479
- message: "Choose agents",
2480
- options: Object.entries(AGENTS).map(([key, agent]) => ({
2481
- value: key,
2482
- label: agent.label
2483
- })),
2484
- required: true
2485
- }),
2486
- hooks: () => multiselect({
2487
- message: "Choose hooks",
2488
- options: Object.entries(HOOKS).map(([key, hook]) => ({
2489
- value: key,
2490
- label: hook.label
2491
- }))
2492
- })
2493
- }, { onCancel: () => {
2494
- throw new UserCancelledError({ message: "Operation cancelled" });
2495
- } });
2496
- },
2497
- catch: (e) => {
2498
- if (e instanceof UserCancelledError) return e;
2499
- return new AddonSetupError({
2500
- addon: "ultracite",
2501
- message: `Failed to get user preferences: ${e instanceof Error ? e.message : String(e)}`,
2502
- cause: e
2503
- });
2504
- }
2505
- });
2506
- if (groupResult.isErr()) {
2507
- if (UserCancelledError.is(groupResult.error)) return userCancelled(groupResult.error.message);
2508
- log.error(pc.red("Failed to set up Ultracite"));
2509
- return groupResult;
2510
- }
2511
- result = groupResult.value;
2512
- const linter = result.linter;
2513
- const editors = result.editors;
2514
- const agents = result.agents;
2515
- const hooks = result.hooks;
2516
- const frameworks = getFrameworksFromFrontend(frontend);
2703
+ function buildUltraciteInitArgs({ packageManager, linter, frameworks, editors, agents, hooks, gitHooks }) {
2517
2704
  const ultraciteArgs = [
2518
2705
  "init",
2519
2706
  "--pm",
@@ -2526,12 +2713,105 @@ async function setupUltracite(config, gitHooks) {
2526
2713
  if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
2527
2714
  if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
2528
2715
  if (gitHooks.length > 0) {
2529
- const integrations = [...gitHooks];
2530
- if (gitHooks.includes("husky")) integrations.push("lint-staged");
2716
+ const integrations = gitHooks.includes("husky") ? [...new Set([...gitHooks, "lint-staged"])] : gitHooks;
2531
2717
  ultraciteArgs.push("--integrations", ...integrations);
2532
2718
  }
2533
- const args = getPackageExecutionArgs(packageManager, `ultracite@latest ${ultraciteArgs.join(" ")} --skip-install`);
2534
- 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();
2535
2815
  s.start("Running Ultracite init command...");
2536
2816
  const initResult = await Result.tryPromise({
2537
2817
  try: async () => {
@@ -2550,13 +2830,12 @@ async function setupUltracite(config, gitHooks) {
2550
2830
  }
2551
2831
  });
2552
2832
  if (initResult.isErr()) {
2553
- log.error(pc.red("Failed to set up Ultracite"));
2833
+ cliLog.error(pc.red("Failed to set up Ultracite"));
2554
2834
  return initResult;
2555
2835
  }
2556
2836
  s.stop("Ultracite setup successfully!");
2557
2837
  return Result.ok(void 0);
2558
2838
  }
2559
-
2560
2839
  //#endregion
2561
2840
  //#region src/helpers/addons/wxt-setup.ts
2562
2841
  const TEMPLATES = {
@@ -2581,20 +2860,29 @@ const TEMPLATES = {
2581
2860
  hint: "Svelte template"
2582
2861
  }
2583
2862
  };
2863
+ const DEFAULT_TEMPLATE = "react";
2864
+ const DEFAULT_DEV_PORT = 5555;
2584
2865
  async function setupWxt(config) {
2585
2866
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2586
2867
  const { packageManager, projectDir } = config;
2587
- log.info("Setting up WXT...");
2588
- const template = await select({
2589
- message: "Choose a template",
2590
- options: Object.entries(TEMPLATES).map(([key, template]) => ({
2591
- value: key,
2592
- label: template.label,
2593
- hint: template.hint
2594
- })),
2595
- initialValue: "react"
2596
- });
2597
- 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;
2598
2886
  const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
2599
2887
  const appsDir = path.join(projectDir, "apps");
2600
2888
  const ensureDirResult = await Result.tryPromise({
@@ -2606,7 +2894,7 @@ async function setupWxt(config) {
2606
2894
  })
2607
2895
  });
2608
2896
  if (ensureDirResult.isErr()) return ensureDirResult;
2609
- const s = spinner();
2897
+ const s = createSpinner();
2610
2898
  s.start("Running WXT init command...");
2611
2899
  const initResult = await Result.tryPromise({
2612
2900
  try: async () => {
@@ -2625,7 +2913,7 @@ async function setupWxt(config) {
2625
2913
  }
2626
2914
  });
2627
2915
  if (initResult.isErr()) {
2628
- log.error(pc.red("Failed to set up WXT"));
2916
+ cliLog.error(pc.red("Failed to set up WXT"));
2629
2917
  return initResult;
2630
2918
  }
2631
2919
  const extensionDir = path.join(projectDir, "apps", "extension");
@@ -2635,7 +2923,7 @@ async function setupWxt(config) {
2635
2923
  if (await fs.pathExists(packageJsonPath)) {
2636
2924
  const packageJson = await fs.readJson(packageJsonPath);
2637
2925
  packageJson.name = "extension";
2638
- 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}`;
2639
2927
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2640
2928
  }
2641
2929
  },
@@ -2644,11 +2932,10 @@ async function setupWxt(config) {
2644
2932
  message: `Failed to update package.json: ${e instanceof Error ? e.message : String(e)}`,
2645
2933
  cause: e
2646
2934
  })
2647
- })).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"));
2648
2936
  s.stop("WXT setup complete!");
2649
2937
  return Result.ok(void 0);
2650
2938
  }
2651
-
2652
2939
  //#endregion
2653
2940
  //#region src/helpers/addons/addons-setup.ts
2654
2941
  async function runSetup(setupFn) {
@@ -2745,7 +3032,6 @@ async function setupLefthook(projectDir) {
2745
3032
  projectDir
2746
3033
  });
2747
3034
  }
2748
-
2749
3035
  //#endregion
2750
3036
  //#region src/helpers/core/detect-project-config.ts
2751
3037
  async function detectProjectConfig(projectDir) {
@@ -2755,6 +3041,8 @@ async function detectProjectConfig(projectDir) {
2755
3041
  if (btsConfig) return {
2756
3042
  projectDir,
2757
3043
  projectName: path.basename(projectDir),
3044
+ addonOptions: btsConfig.addonOptions,
3045
+ dbSetupOptions: btsConfig.dbSetupOptions,
2758
3046
  database: btsConfig.database,
2759
3047
  orm: btsConfig.orm,
2760
3048
  backend: btsConfig.backend,
@@ -2776,12 +3064,11 @@ async function detectProjectConfig(projectDir) {
2776
3064
  });
2777
3065
  return result.isOk() ? result.value : null;
2778
3066
  }
2779
-
2780
3067
  //#endregion
2781
3068
  //#region src/helpers/core/install-dependencies.ts
2782
3069
  async function installDependencies({ projectDir, packageManager }) {
2783
3070
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
2784
- const s = spinner();
3071
+ const s = createSpinner();
2785
3072
  s.start(`Running ${packageManager} install...`);
2786
3073
  const result = await Result.tryPromise({
2787
3074
  try: async () => {
@@ -2800,9 +3087,21 @@ async function installDependencies({ projectDir, packageManager }) {
2800
3087
  else s.stop(pc.red("Failed to install dependencies"));
2801
3088
  return result;
2802
3089
  }
2803
-
2804
3090
  //#endregion
2805
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
+ }
2806
3105
  async function addHandler(input, options = {}) {
2807
3106
  const { silent = false } = options;
2808
3107
  return runWithContextAsync({ silent }, async () => {
@@ -2830,6 +3129,11 @@ async function addHandler(input, options = {}) {
2830
3129
  }
2831
3130
  async function addHandlerInternal(input) {
2832
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
+ }));
2833
3137
  if (!isSilent()) {
2834
3138
  renderTitle();
2835
3139
  intro(pc.magenta("Add addons to your Better-T-Stack project"));
@@ -2848,7 +3152,8 @@ async function addHandlerInternal(input) {
2848
3152
  projectDir
2849
3153
  });
2850
3154
  }
2851
- } 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 {
2852
3157
  const promptResult = await Result.tryPromise({
2853
3158
  try: () => getAddonsToAdd(existingConfig.frontend, existingConfig.addons, existingConfig.auth),
2854
3159
  catch: (e) => {
@@ -2876,10 +3181,12 @@ async function addHandlerInternal(input) {
2876
3181
  }
2877
3182
  if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
2878
3183
  const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3184
+ const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
2879
3185
  const config = {
2880
3186
  projectName: existingConfig.projectName,
2881
3187
  projectDir,
2882
3188
  relativePath: ".",
3189
+ addonOptions: mergedAddonOptions,
2883
3190
  database: existingConfig.database,
2884
3191
  orm: existingConfig.orm,
2885
3192
  backend: existingConfig.backend,
@@ -2908,12 +3215,27 @@ async function addHandlerInternal(input) {
2908
3215
  }
2909
3216
  await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
2910
3217
  processAddonsDeps(vfs, config);
2911
- const writeResult = await writeTree({
3218
+ const tree = {
2912
3219
  root: vfs.toTree(""),
2913
3220
  fileCount: vfs.getFileCount(),
2914
3221
  directoryCount: vfs.getDirectoryCount(),
2915
3222
  config
2916
- }, 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);
2917
3239
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
2918
3240
  if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
2919
3241
  const setupResult = await Result.tryPromise({
@@ -2927,7 +3249,10 @@ async function addHandlerInternal(input) {
2927
3249
  }
2928
3250
  });
2929
3251
  if (setupResult.isErr()) return Result.err(setupResult.error);
2930
- await updateBtsConfig(projectDir, { addons: updatedAddons });
3252
+ await updateBtsConfig(projectDir, {
3253
+ addons: updatedAddons,
3254
+ addonOptions: config.addonOptions
3255
+ });
2931
3256
  if (input.install) {
2932
3257
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
2933
3258
  await installDependencies({
@@ -2943,10 +3268,10 @@ async function addHandlerInternal(input) {
2943
3268
  return Result.ok({
2944
3269
  success: true,
2945
3270
  addedAddons: addonsToAdd,
2946
- projectDir
3271
+ projectDir,
3272
+ plannedFileCount: vfs.getFileCount()
2947
3273
  });
2948
3274
  }
2949
-
2950
3275
  //#endregion
2951
3276
  //#region src/prompts/api.ts
2952
3277
  async function getApiChoice(Api, frontend, backend) {
@@ -2974,7 +3299,6 @@ async function getApiChoice(Api, frontend, backend) {
2974
3299
  if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
2975
3300
  return apiType;
2976
3301
  }
2977
-
2978
3302
  //#endregion
2979
3303
  //#region src/prompts/auth.ts
2980
3304
  async function getAuthChoice(auth, backend, frontend) {
@@ -3038,7 +3362,6 @@ async function getAuthChoice(auth, backend, frontend) {
3038
3362
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3039
3363
  return response;
3040
3364
  }
3041
-
3042
3365
  //#endregion
3043
3366
  //#region src/prompts/backend.ts
3044
3367
  const FULLSTACK_FRONTENDS = [
@@ -3092,7 +3415,6 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
3092
3415
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3093
3416
  return response;
3094
3417
  }
3095
-
3096
3418
  //#endregion
3097
3419
  //#region src/prompts/database.ts
3098
3420
  async function getDatabaseChoice(database, backend, runtime) {
@@ -3133,7 +3455,6 @@ async function getDatabaseChoice(database, backend, runtime) {
3133
3455
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3134
3456
  return response;
3135
3457
  }
3136
-
3137
3458
  //#endregion
3138
3459
  //#region src/prompts/database-setup.ts
3139
3460
  async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
@@ -3233,7 +3554,6 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
3233
3554
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3234
3555
  return response;
3235
3556
  }
3236
-
3237
3557
  //#endregion
3238
3558
  //#region src/prompts/examples.ts
3239
3559
  async function getExamplesChoice(examples, database, frontends, backend, api) {
@@ -3261,7 +3581,6 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
3261
3581
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3262
3582
  return response;
3263
3583
  }
3264
-
3265
3584
  //#endregion
3266
3585
  //#region src/prompts/frontend.ts
3267
3586
  async function getFrontendChoice(frontendOptions, backend, auth) {
@@ -3379,7 +3698,6 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
3379
3698
  return result;
3380
3699
  }
3381
3700
  }
3382
-
3383
3701
  //#endregion
3384
3702
  //#region src/prompts/git.ts
3385
3703
  async function getGitChoice(git) {
@@ -3391,7 +3709,6 @@ async function getGitChoice(git) {
3391
3709
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3392
3710
  return response;
3393
3711
  }
3394
-
3395
3712
  //#endregion
3396
3713
  //#region src/prompts/install.ts
3397
3714
  async function getinstallChoice(install) {
@@ -3403,7 +3720,6 @@ async function getinstallChoice(install) {
3403
3720
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3404
3721
  return response;
3405
3722
  }
3406
-
3407
3723
  //#endregion
3408
3724
  //#region src/prompts/navigable-group.ts
3409
3725
  /**
@@ -3460,7 +3776,6 @@ async function navigableGroup(prompts, opts) {
3460
3776
  setIsFirstPrompt$1(false);
3461
3777
  return results;
3462
3778
  }
3463
-
3464
3779
  //#endregion
3465
3780
  //#region src/prompts/orm.ts
3466
3781
  const ormOptions = {
@@ -3492,7 +3807,6 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
3492
3807
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3493
3808
  return response;
3494
3809
  }
3495
-
3496
3810
  //#endregion
3497
3811
  //#region src/prompts/package-manager.ts
3498
3812
  async function getPackageManagerChoice(packageManager) {
@@ -3521,7 +3835,6 @@ async function getPackageManagerChoice(packageManager) {
3521
3835
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3522
3836
  return response;
3523
3837
  }
3524
-
3525
3838
  //#endregion
3526
3839
  //#region src/prompts/payments.ts
3527
3840
  async function getPaymentsChoice(payments, auth, backend, frontends) {
@@ -3544,7 +3857,6 @@ async function getPaymentsChoice(payments, auth, backend, frontends) {
3544
3857
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3545
3858
  return response;
3546
3859
  }
3547
-
3548
3860
  //#endregion
3549
3861
  //#region src/prompts/runtime.ts
3550
3862
  async function getRuntimeChoice(runtime, backend) {
@@ -3572,7 +3884,6 @@ async function getRuntimeChoice(runtime, backend) {
3572
3884
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3573
3885
  return response;
3574
3886
  }
3575
-
3576
3887
  //#endregion
3577
3888
  //#region src/prompts/server-deploy.ts
3578
3889
  async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
@@ -3582,7 +3893,6 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
3582
3893
  if (runtime === "workers") return "cloudflare";
3583
3894
  return "none";
3584
3895
  }
3585
-
3586
3896
  //#endregion
3587
3897
  //#region src/prompts/web-deploy.ts
3588
3898
  function hasWebFrontend(frontends) {
@@ -3616,7 +3926,6 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
3616
3926
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3617
3927
  return response;
3618
3928
  }
3619
-
3620
3929
  //#endregion
3621
3930
  //#region src/prompts/config-prompts.ts
3622
3931
  async function gatherConfig(flags, projectName, projectDir, relativePath) {
@@ -3624,6 +3933,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
3624
3933
  projectName,
3625
3934
  projectDir,
3626
3935
  relativePath,
3936
+ addonOptions: flags.addonOptions,
3937
+ dbSetupOptions: flags.dbSetupOptions,
3627
3938
  frontend: flags.frontend ?? [...DEFAULT_CONFIG.frontend],
3628
3939
  backend: flags.backend ?? DEFAULT_CONFIG.backend,
3629
3940
  runtime: flags.runtime ?? DEFAULT_CONFIG.runtime,
@@ -3665,6 +3976,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
3665
3976
  projectName,
3666
3977
  projectDir,
3667
3978
  relativePath,
3979
+ addonOptions: flags.addonOptions,
3980
+ dbSetupOptions: flags.dbSetupOptions,
3668
3981
  frontend: result.frontend,
3669
3982
  backend: result.backend,
3670
3983
  runtime: result.runtime,
@@ -3683,7 +3996,6 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
3683
3996
  serverDeploy: result.serverDeploy
3684
3997
  };
3685
3998
  }
3686
-
3687
3999
  //#endregion
3688
4000
  //#region src/prompts/project-name.ts
3689
4001
  function isPathWithinCwd$1(targetPath) {
@@ -3733,7 +4045,6 @@ async function getProjectName(initialName) {
3733
4045
  }
3734
4046
  return projectPath;
3735
4047
  }
3736
-
3737
4048
  //#endregion
3738
4049
  //#region src/utils/telemetry.ts
3739
4050
  /**
@@ -3749,7 +4060,6 @@ function isTelemetryEnabled() {
3749
4060
  if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
3750
4061
  return true;
3751
4062
  }
3752
-
3753
4063
  //#endregion
3754
4064
  //#region src/utils/analytics.ts
3755
4065
  const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
@@ -3776,7 +4086,6 @@ async function trackProjectCreation(config, disableAnalytics = false) {
3776
4086
  catch: () => void 0
3777
4087
  });
3778
4088
  }
3779
-
3780
4089
  //#endregion
3781
4090
  //#region src/utils/display-config.ts
3782
4091
  function displayConfig(config) {
@@ -3819,7 +4128,6 @@ function displayConfig(config) {
3819
4128
  if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
3820
4129
  return configDisplay.join("\n");
3821
4130
  }
3822
-
3823
4131
  //#endregion
3824
4132
  //#region src/utils/project-directory.ts
3825
4133
  async function handleDirectoryConflict(currentPathInput) {
@@ -3907,10 +4215,11 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
3907
4215
  finalBaseName
3908
4216
  };
3909
4217
  }
3910
-
3911
4218
  //#endregion
3912
4219
  //#region src/utils/project-name-validation.ts
3913
4220
  function validateProjectName(name) {
4221
+ const hardeningResult = validateAgentSafePathInput(name, "projectName");
4222
+ if (hardeningResult.isErr()) return Result.err(hardeningResult.error);
3914
4223
  const result = types_exports.ProjectNameSchema.safeParse(name);
3915
4224
  if (!result.success) return Result.err(new ValidationError({
3916
4225
  field: "projectName",
@@ -3920,13 +4229,20 @@ function validateProjectName(name) {
3920
4229
  return Result.ok(void 0);
3921
4230
  }
3922
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
+ }
3923
4240
  const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
3924
4241
  if (!derivedName) return Result.ok("");
3925
4242
  const validationResult = validateProjectName(projectName ? path.basename(projectName) : derivedName);
3926
4243
  if (validationResult.isErr()) return Result.err(validationResult.error);
3927
4244
  return Result.ok(projectName || derivedName);
3928
4245
  }
3929
-
3930
4246
  //#endregion
3931
4247
  //#region src/utils/templates.ts
3932
4248
  const TEMPLATE_PRESETS = {
@@ -4007,7 +4323,6 @@ function getTemplateDescription(template) {
4007
4323
  none: "No template - Full customization"
4008
4324
  }[template] || "";
4009
4325
  }
4010
-
4011
4326
  //#endregion
4012
4327
  //#region src/utils/config-processing.ts
4013
4328
  function processArrayOption(options) {
@@ -4023,6 +4338,8 @@ function deriveProjectName(projectName, projectDirectory) {
4023
4338
  function processFlags(options, projectName) {
4024
4339
  const config = {};
4025
4340
  if (options.api) config.api = options.api;
4341
+ if (options.addonOptions) config.addonOptions = options.addonOptions;
4342
+ if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
4026
4343
  if (options.backend) config.backend = options.backend;
4027
4344
  if (options.database) config.database = options.database;
4028
4345
  if (options.orm) config.orm = options.orm;
@@ -4059,7 +4376,6 @@ function validateArrayOptions(options) {
4059
4376
  if (examplesResult.isErr()) return examplesResult;
4060
4377
  return Result.ok(void 0);
4061
4378
  }
4062
-
4063
4379
  //#endregion
4064
4380
  //#region src/utils/config-validation.ts
4065
4381
  function validationErr(message) {
@@ -4242,7 +4558,6 @@ function validateConfigForProgrammaticUse(config) {
4242
4558
  return Result.ok(void 0);
4243
4559
  });
4244
4560
  }
4245
-
4246
4561
  //#endregion
4247
4562
  //#region src/validation.ts
4248
4563
  const CORE_STACK_FLAGS = new Set([
@@ -4302,7 +4617,6 @@ function validateConfigCompatibility(config, providedFlags, options) {
4302
4617
  if (options && providedFlags) return validateFullConfig(config, providedFlags, options);
4303
4618
  else return validateConfigForProgrammaticUse(config);
4304
4619
  }
4305
-
4306
4620
  //#endregion
4307
4621
  //#region src/utils/file-formatter.ts
4308
4622
  const formatOptions = {
@@ -4347,7 +4661,6 @@ async function formatProject(projectDir) {
4347
4661
  })
4348
4662
  });
4349
4663
  }
4350
-
4351
4664
  //#endregion
4352
4665
  //#region src/utils/env-utils.ts
4353
4666
  async function addEnvVariablesToFile(envPath, variables) {
@@ -4377,7 +4690,6 @@ async function addEnvVariablesToFile(envPath, variables) {
4377
4690
  if (newLines.length > 0 && newLines[newLines.length - 1] === "") newLines.pop();
4378
4691
  if (foundKeys.size > 0 || keysToAdd.size > foundKeys.size) await fs.writeFile(envPath, newLines.join("\n") + "\n");
4379
4692
  }
4380
-
4381
4693
  //#endregion
4382
4694
  //#region src/helpers/database-providers/d1-setup.ts
4383
4695
  async function setupCloudflareD1(config) {
@@ -4403,7 +4715,6 @@ async function setupCloudflareD1(config) {
4403
4715
  })
4404
4716
  });
4405
4717
  }
4406
-
4407
4718
  //#endregion
4408
4719
  //#region src/helpers/database-providers/docker-compose-setup.ts
4409
4720
  async function setupDockerCompose(config) {
@@ -4437,7 +4748,6 @@ function getDatabaseUrl(database, projectName) {
4437
4748
  default: return "";
4438
4749
  }
4439
4750
  }
4440
-
4441
4751
  //#endregion
4442
4752
  //#region src/utils/command-exists.ts
4443
4753
  async function commandExists(command) {
@@ -4450,28 +4760,58 @@ async function commandExists(command) {
4450
4760
  });
4451
4761
  return result.isOk() ? result.value : false;
4452
4762
  }
4453
-
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
+ }
4454
4794
  //#endregion
4455
4795
  //#region src/helpers/database-providers/mongodb-atlas-setup.ts
4456
4796
  async function checkAtlasCLI() {
4457
4797
  const exists = await commandExists("atlas");
4458
- if (exists) log.info("MongoDB Atlas CLI found");
4459
- 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"));
4460
4800
  return exists;
4461
4801
  }
4462
4802
  async function initMongoDBAtlas(serverDir) {
4463
4803
  if (!await checkAtlasCLI()) {
4464
- 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/"));
4465
4805
  return databaseSetupError("mongodb-atlas", "MongoDB Atlas CLI not found");
4466
4806
  }
4467
- log.info("Running MongoDB Atlas setup...");
4807
+ cliLog.info("Running MongoDB Atlas setup...");
4468
4808
  const deployResult = await Result.tryPromise({
4469
4809
  try: async () => {
4470
4810
  await $({
4471
4811
  cwd: serverDir,
4472
4812
  stdio: "inherit"
4473
4813
  })`atlas deployments setup`;
4474
- log.success("MongoDB Atlas deployment ready");
4814
+ cliLog.success("MongoDB Atlas deployment ready");
4475
4815
  },
4476
4816
  catch: (e) => new DatabaseSetupError({
4477
4817
  provider: "mongodb-atlas",
@@ -4512,7 +4852,7 @@ async function writeEnvFile$3(projectDir, backend, config) {
4512
4852
  });
4513
4853
  }
4514
4854
  function displayManualSetupInstructions$3() {
4515
- log.info(`
4855
+ cliLog.info(`
4516
4856
  ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
4517
4857
 
4518
4858
  1. Install Atlas CLI:
@@ -4530,7 +4870,10 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
4530
4870
  }
4531
4871
  async function setupMongoDBAtlas(config, cliInput) {
4532
4872
  const { projectDir, backend } = config;
4533
- const manualDb = cliInput?.manualDb ?? false;
4873
+ const setupMode = resolveDbSetupMode("mongodb-atlas", {
4874
+ manualDb: cliInput?.manualDb,
4875
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
4876
+ });
4534
4877
  const serverDir = path.join(projectDir, "packages/db");
4535
4878
  const ensureDirResult = await Result.tryPromise({
4536
4879
  try: () => fs.ensureDir(serverDir),
@@ -4541,29 +4884,40 @@ async function setupMongoDBAtlas(config, cliInput) {
4541
4884
  })
4542
4885
  });
4543
4886
  if (ensureDirResult.isErr()) return ensureDirResult;
4544
- if (manualDb) {
4545
- log.info("MongoDB Atlas manual setup selected");
4887
+ if (setupMode === "manual") {
4888
+ cliLog.info("MongoDB Atlas manual setup selected");
4546
4889
  const envResult = await writeEnvFile$3(projectDir, backend);
4547
4890
  if (envResult.isErr()) return envResult;
4548
4891
  displayManualSetupInstructions$3();
4549
4892
  return Result.ok(void 0);
4550
4893
  }
4551
- const mode = await select({
4552
- message: "MongoDB Atlas setup: choose mode",
4553
- options: [{
4554
- label: "Automatic",
4555
- value: "auto",
4556
- hint: "Automated setup with provider CLI, sets .env"
4557
- }, {
4558
- label: "Manual",
4559
- value: "manual",
4560
- hint: "Manual setup, add env vars yourself"
4561
- }],
4562
- initialValue: "auto"
4563
- });
4564
- 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
+ }
4565
4919
  if (mode === "manual") {
4566
- log.info("MongoDB Atlas manual setup selected");
4920
+ cliLog.info("MongoDB Atlas manual setup selected");
4567
4921
  const envResult = await writeEnvFile$3(projectDir, backend);
4568
4922
  if (envResult.isErr()) return envResult;
4569
4923
  displayManualSetupInstructions$3();
@@ -4573,17 +4927,16 @@ async function setupMongoDBAtlas(config, cliInput) {
4573
4927
  if (mongoConfigResult.isOk()) {
4574
4928
  const envResult = await writeEnvFile$3(projectDir, backend, mongoConfigResult.value);
4575
4929
  if (envResult.isErr()) return envResult;
4576
- 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."));
4577
4931
  return Result.ok(void 0);
4578
4932
  }
4579
4933
  if (UserCancelledError.is(mongoConfigResult.error)) return mongoConfigResult;
4580
- log.warn(pc.yellow("Falling back to local MongoDB configuration"));
4934
+ cliLog.warn(pc.yellow("Falling back to local MongoDB configuration"));
4581
4935
  const envResult = await writeEnvFile$3(projectDir, backend);
4582
4936
  if (envResult.isErr()) return envResult;
4583
4937
  displayManualSetupInstructions$3();
4584
4938
  return Result.ok(void 0);
4585
4939
  }
4586
-
4587
4940
  //#endregion
4588
4941
  //#region src/helpers/database-providers/neon-setup.ts
4589
4942
  const NEON_REGIONS = [
@@ -4621,7 +4974,7 @@ const NEON_REGIONS = [
4621
4974
  }
4622
4975
  ];
4623
4976
  async function executeNeonCommand(packageManager, commandArgsString, spinnerText) {
4624
- const s = spinner();
4977
+ const s = createSpinner();
4625
4978
  const args = getPackageExecutionArgs(packageManager, commandArgsString);
4626
4979
  if (spinnerText) s.start(spinnerText);
4627
4980
  return Result.tryPromise({
@@ -4684,7 +5037,7 @@ async function writeEnvFile$2(projectDir, backend, config) {
4684
5037
  });
4685
5038
  }
4686
5039
  async function setupWithNeonDb(projectDir, packageManager, backend) {
4687
- const s = spinner();
5040
+ const s = createSpinner();
4688
5041
  s.start("Creating Neon database using get-db...");
4689
5042
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
4690
5043
  const targetDir = path.join(projectDir, targetApp);
@@ -4717,7 +5070,7 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
4717
5070
  });
4718
5071
  }
4719
5072
  function displayManualSetupInstructions$2(target) {
4720
- log.info(`Manual Neon PostgreSQL Setup Instructions:
5073
+ cliLog.info(`Manual Neon PostgreSQL Setup Instructions:
4721
5074
 
4722
5075
  1. Get Neon with Better T Stack referral: https://get.neon.com/sbA3tIe
4723
5076
  2. Create a new project from the dashboard
@@ -4728,80 +5081,105 @@ DATABASE_URL="your_connection_string"`);
4728
5081
  }
4729
5082
  async function setupNeonPostgres(config, cliInput) {
4730
5083
  const { packageManager, projectDir, backend } = config;
4731
- const manualDb = cliInput?.manualDb ?? false;
5084
+ const setupMode = resolveDbSetupMode("neon", {
5085
+ manualDb: cliInput?.manualDb,
5086
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
5087
+ });
4732
5088
  const target = backend === "self" ? "apps/web" : "apps/server";
4733
- if (manualDb) {
5089
+ if (setupMode === "manual") {
4734
5090
  const envResult = await writeEnvFile$2(projectDir, backend);
4735
5091
  if (envResult.isErr()) return envResult;
4736
5092
  displayManualSetupInstructions$2(target);
4737
5093
  return Result.ok(void 0);
4738
5094
  }
4739
- const mode = await select({
4740
- message: "Neon setup: choose mode",
4741
- options: [{
4742
- label: "Automatic",
4743
- value: "auto",
4744
- hint: "Automated setup with provider CLI, sets .env"
4745
- }, {
4746
- label: "Manual",
4747
- value: "manual",
4748
- hint: "Manual setup, add env vars yourself"
4749
- }],
4750
- initialValue: "auto"
4751
- });
4752
- if (isCancel(mode)) return userCancelled("Operation cancelled");
4753
- 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") {
4754
5115
  const envResult = await writeEnvFile$2(projectDir, backend);
4755
5116
  if (envResult.isErr()) return envResult;
4756
5117
  displayManualSetupInstructions$2(target);
4757
5118
  return Result.ok(void 0);
4758
5119
  }
4759
- const setupMethod = await select({
4760
- message: "Choose your Neon setup method:",
4761
- options: [{
4762
- label: "Quick setup with get-db",
4763
- value: "neondb",
4764
- hint: "fastest, no auth required"
4765
- }, {
4766
- label: "Custom setup with neonctl",
4767
- value: "neonctl",
4768
- hint: "More control - choose project name and region"
4769
- }],
4770
- initialValue: "neondb"
4771
- });
4772
- 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
+ }
4773
5139
  if (setupMethod === "neondb") {
4774
5140
  const neonDbResult = await setupWithNeonDb(projectDir, packageManager, backend);
4775
5141
  if (neonDbResult.isErr()) {
4776
- log.error(pc.red(neonDbResult.error.message));
5142
+ cliLog.error(pc.red(neonDbResult.error.message));
4777
5143
  const envResult = await writeEnvFile$2(projectDir, backend);
4778
5144
  if (envResult.isErr()) return envResult;
4779
5145
  displayManualSetupInstructions$2(target);
4780
- } else log.info(`Get Neon with Better T Stack referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
4781
- 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);
4782
5150
  }
4783
5151
  const suggestedProjectName = path.basename(projectDir);
4784
- const projectName = await text({
4785
- message: "Enter a name for your Neon project:",
4786
- defaultValue: suggestedProjectName,
4787
- initialValue: suggestedProjectName
4788
- });
4789
- if (isCancel(projectName)) return userCancelled("Operation cancelled");
4790
- const regionId = await select({
4791
- message: "Select a region for your Neon project:",
4792
- options: NEON_REGIONS,
4793
- initialValue: NEON_REGIONS[0].value
4794
- });
4795
- 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
+ }
4796
5174
  const neonConfigResult = await createNeonProject(projectName, regionId, packageManager);
4797
5175
  if (neonConfigResult.isErr()) {
4798
- log.error(pc.red(neonConfigResult.error.message));
5176
+ cliLog.error(pc.red(neonConfigResult.error.message));
4799
5177
  const envResult = await writeEnvFile$2(projectDir, backend);
4800
5178
  if (envResult.isErr()) return envResult;
4801
5179
  displayManualSetupInstructions$2(target);
4802
5180
  return Result.ok(void 0);
4803
5181
  }
4804
- const finalSpinner = spinner();
5182
+ const finalSpinner = createSpinner();
4805
5183
  finalSpinner.start("Configuring database connection");
4806
5184
  const envResult = await writeEnvFile$2(projectDir, backend, neonConfigResult.value);
4807
5185
  if (envResult.isErr()) {
@@ -4809,10 +5187,9 @@ async function setupNeonPostgres(config, cliInput) {
4809
5187
  return envResult;
4810
5188
  }
4811
5189
  finalSpinner.stop("Neon database configured!");
4812
- 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")}`);
4813
5191
  return Result.ok(void 0);
4814
5192
  }
4815
-
4816
5193
  //#endregion
4817
5194
  //#region src/helpers/database-providers/planetscale-setup.ts
4818
5195
  async function setupPlanetScale(config) {
@@ -4883,7 +5260,6 @@ async function setupPlanetScale(config) {
4883
5260
  })
4884
5261
  });
4885
5262
  }
4886
-
4887
5263
  //#endregion
4888
5264
  //#region src/helpers/database-providers/prisma-postgres-setup.ts
4889
5265
  const AVAILABLE_REGIONS = [
@@ -4912,16 +5288,31 @@ const AVAILABLE_REGIONS = [
4912
5288
  label: "US West (N. California)"
4913
5289
  }
4914
5290
  ];
4915
- async function setupWithCreateDb(serverDir, packageManager) {
4916
- log.info("Starting Prisma Postgres setup with create-db.");
4917
- const selectedRegion = await select({
4918
- message: "Select your preferred region:",
4919
- options: AVAILABLE_REGIONS,
4920
- initialValue: "ap-southeast-1"
4921
- });
4922
- if (isCancel(selectedRegion)) return userCancelled("Operation cancelled");
4923
- const createDbArgs = getPackageExecutionArgs(packageManager, `create-db@latest --json --region ${selectedRegion} --user-agent "aman/better-t-stack"`);
4924
- 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();
4925
5316
  s.start("Creating Prisma Postgres database...");
4926
5317
  const execResult = await Result.tryPromise({
4927
5318
  try: async () => {
@@ -4979,7 +5370,7 @@ async function writeEnvFile$1(projectDir, backend, config) {
4979
5370
  });
4980
5371
  }
4981
5372
  function displayManualSetupInstructions$1(target) {
4982
- log.info(`Manual Prisma PostgreSQL Setup Instructions:
5373
+ cliLog.info(`Manual Prisma PostgreSQL Setup Instructions:
4983
5374
 
4984
5375
  1. Visit https://console.prisma.io and create an account
4985
5376
  2. Create a new PostgreSQL database from the dashboard
@@ -4990,7 +5381,10 @@ DATABASE_URL="your_database_url"`);
4990
5381
  }
4991
5382
  async function setupPrismaPostgres(config, cliInput) {
4992
5383
  const { packageManager, projectDir, backend } = config;
4993
- const manualDb = cliInput?.manualDb ?? false;
5384
+ const setupMode = resolveDbSetupMode("prisma-postgres", {
5385
+ manualDb: cliInput?.manualDb,
5386
+ dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
5387
+ });
4994
5388
  const dbDir = path.join(projectDir, "packages/db");
4995
5389
  const target = backend === "self" ? "apps/web" : "apps/server";
4996
5390
  const ensureDirResult = await Result.tryPromise({
@@ -5002,49 +5396,53 @@ async function setupPrismaPostgres(config, cliInput) {
5002
5396
  })
5003
5397
  });
5004
5398
  if (ensureDirResult.isErr()) return ensureDirResult;
5005
- if (manualDb) {
5399
+ if (setupMode === "manual") {
5006
5400
  const envResult = await writeEnvFile$1(projectDir, backend);
5007
5401
  if (envResult.isErr()) return envResult;
5008
5402
  displayManualSetupInstructions$1(target);
5009
5403
  return Result.ok(void 0);
5010
5404
  }
5011
- const setupMode = await select({
5012
- message: "Prisma Postgres setup: choose mode",
5013
- options: [{
5014
- label: "Automatic (create-db)",
5015
- value: "auto",
5016
- hint: "Provision a database via Prisma's create-db CLI"
5017
- }, {
5018
- label: "Manual",
5019
- value: "manual",
5020
- hint: "Add your own DATABASE_URL later"
5021
- }],
5022
- initialValue: "auto"
5023
- });
5024
- if (isCancel(setupMode)) return userCancelled("Operation cancelled");
5025
- 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") {
5026
5425
  const envResult = await writeEnvFile$1(projectDir, backend);
5027
5426
  if (envResult.isErr()) return envResult;
5028
5427
  displayManualSetupInstructions$1(target);
5029
5428
  return Result.ok(void 0);
5030
5429
  }
5031
- const prismaConfigResult = await setupWithCreateDb(dbDir, packageManager);
5430
+ const prismaConfigResult = await setupWithCreateDb(dbDir, packageManager, cliInput?.dbSetupOptions?.prismaPostgres?.regionId ?? config.dbSetupOptions?.prismaPostgres?.regionId);
5032
5431
  if (prismaConfigResult.isErr()) {
5033
5432
  if (UserCancelledError.is(prismaConfigResult.error)) return prismaConfigResult;
5034
- log.error(pc.red(prismaConfigResult.error.message));
5433
+ cliLog.error(pc.red(prismaConfigResult.error.message));
5035
5434
  const envResult = await writeEnvFile$1(projectDir, backend);
5036
5435
  if (envResult.isErr()) return envResult;
5037
5436
  displayManualSetupInstructions$1(target);
5038
- log.info("Setup completed with manual configuration required.");
5437
+ cliLog.info("Setup completed with manual configuration required.");
5039
5438
  return Result.ok(void 0);
5040
5439
  }
5041
5440
  const envResult = await writeEnvFile$1(projectDir, backend, prismaConfigResult.value);
5042
5441
  if (envResult.isErr()) return envResult;
5043
- log.success(pc.green("Prisma Postgres database configured successfully!"));
5044
- 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}`));
5045
5444
  return Result.ok(void 0);
5046
5445
  }
5047
-
5048
5446
  //#endregion
5049
5447
  //#region src/helpers/database-providers/supabase-setup.ts
5050
5448
  async function writeSupabaseEnvFile(projectDir, backend, databaseUrl) {
@@ -5074,7 +5472,7 @@ function extractDbUrl(output) {
5074
5472
  return output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/)?.[1] ?? null;
5075
5473
  }
5076
5474
  async function initializeSupabase(serverDir, packageManager) {
5077
- log.info("Initializing Supabase project...");
5475
+ cliLog.info("Initializing Supabase project...");
5078
5476
  return Result.tryPromise({
5079
5477
  try: async () => {
5080
5478
  const supabaseInitArgs = getPackageExecutionArgs(packageManager, "supabase init");
@@ -5082,7 +5480,7 @@ async function initializeSupabase(serverDir, packageManager) {
5082
5480
  cwd: serverDir,
5083
5481
  stdio: "inherit"
5084
5482
  });
5085
- log.success("Supabase project initialized");
5483
+ cliLog.success("Supabase project initialized");
5086
5484
  },
5087
5485
  catch: (e) => {
5088
5486
  const error = e;
@@ -5095,7 +5493,7 @@ async function initializeSupabase(serverDir, packageManager) {
5095
5493
  });
5096
5494
  }
5097
5495
  async function startSupabase(serverDir, packageManager) {
5098
- log.info("Starting Supabase services (this may take a moment)...");
5496
+ cliLog.info("Starting Supabase services (this may take a moment)...");
5099
5497
  const supabaseStartArgs = getPackageExecutionArgs(packageManager, "supabase start");
5100
5498
  return Result.tryPromise({
5101
5499
  try: async () => {
@@ -5103,7 +5501,7 @@ async function startSupabase(serverDir, packageManager) {
5103
5501
  let stdoutData = "";
5104
5502
  if (subprocess.stdout) subprocess.stdout.on("data", (data) => {
5105
5503
  const text = data.toString();
5106
- process.stdout.write(text);
5504
+ if (!isSilent()) process.stdout.write(text);
5107
5505
  stdoutData += text;
5108
5506
  });
5109
5507
  if (subprocess.stderr) subprocess.stderr.pipe(process.stderr);
@@ -5121,8 +5519,8 @@ async function startSupabase(serverDir, packageManager) {
5121
5519
  }
5122
5520
  });
5123
5521
  }
5124
- function displayManualSupabaseInstructions(output) {
5125
- log.info(`"Manual Supabase Setup Instructions:"
5522
+ function displayManualSupabaseInstructions(targetApp, output) {
5523
+ cliLog.info(`"Manual Supabase Setup Instructions:"
5126
5524
  1. Ensure Docker is installed and running.
5127
5525
  2. Install the Supabase CLI (e.g., \`npm install -g supabase\`).
5128
5526
  3. Run \`supabase init\` in your project's \`packages/db\` directory.
@@ -5130,12 +5528,16 @@ function displayManualSupabaseInstructions(output) {
5130
5528
  5. Copy the 'DB URL' from the output.${output ? `
5131
5529
  ${pc.bold("Relevant output from `supabase start`:")}
5132
5530
  ${pc.dim(output)}` : ""}
5133
- 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\`:
5134
5532
  ${pc.gray("DATABASE_URL=\"your_supabase_db_url\"")}`);
5135
5533
  }
5136
5534
  async function setupSupabase(config, cliInput) {
5137
5535
  const { projectDir, packageManager, backend } = config;
5138
- 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
+ });
5139
5541
  const serverDir = path.join(projectDir, "packages", "db");
5140
5542
  const ensureDirResult = await Result.tryPromise({
5141
5543
  try: () => fs.ensureDir(serverDir),
@@ -5146,56 +5548,60 @@ async function setupSupabase(config, cliInput) {
5146
5548
  })
5147
5549
  });
5148
5550
  if (ensureDirResult.isErr()) return ensureDirResult;
5149
- if (manualDb) {
5150
- displayManualSupabaseInstructions();
5551
+ if (setupMode === "manual") {
5552
+ displayManualSupabaseInstructions(targetApp);
5151
5553
  return writeSupabaseEnvFile(projectDir, backend, "");
5152
5554
  }
5153
- const mode = await select({
5154
- message: "Supabase setup: choose mode",
5155
- options: [{
5156
- label: "Automatic",
5157
- value: "auto",
5158
- hint: "Automated setup with provider CLI, sets .env"
5159
- }, {
5160
- label: "Manual",
5161
- value: "manual",
5162
- hint: "Manual setup, add env vars yourself"
5163
- }],
5164
- initialValue: "auto"
5165
- });
5166
- 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
+ }
5167
5574
  if (mode === "manual") {
5168
- displayManualSupabaseInstructions();
5575
+ displayManualSupabaseInstructions(targetApp);
5169
5576
  return writeSupabaseEnvFile(projectDir, backend, "");
5170
5577
  }
5171
5578
  const initResult = await initializeSupabase(serverDir, packageManager);
5172
5579
  if (initResult.isErr()) {
5173
- log.error(pc.red(initResult.error.message));
5174
- displayManualSupabaseInstructions();
5580
+ cliLog.error(pc.red(initResult.error.message));
5581
+ displayManualSupabaseInstructions(targetApp);
5175
5582
  return writeSupabaseEnvFile(projectDir, backend, "");
5176
5583
  }
5177
5584
  const startResult = await startSupabase(serverDir, packageManager);
5178
5585
  if (startResult.isErr()) {
5179
- log.error(pc.red(startResult.error.message));
5180
- displayManualSupabaseInstructions();
5586
+ cliLog.error(pc.red(startResult.error.message));
5587
+ displayManualSupabaseInstructions(targetApp);
5181
5588
  return writeSupabaseEnvFile(projectDir, backend, "");
5182
5589
  }
5183
5590
  const supabaseOutput = startResult.value;
5184
5591
  const dbUrl = extractDbUrl(supabaseOutput);
5185
5592
  if (dbUrl) {
5186
5593
  const envResult = await writeSupabaseEnvFile(projectDir, backend, dbUrl);
5187
- 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!"));
5188
5595
  else {
5189
- log.error(pc.red("Supabase setup completed, but failed to update .env automatically."));
5190
- displayManualSupabaseInstructions(supabaseOutput);
5596
+ cliLog.error(pc.red("Supabase setup completed, but failed to update .env automatically."));
5597
+ displayManualSupabaseInstructions(targetApp, supabaseOutput);
5191
5598
  }
5192
5599
  return envResult;
5193
5600
  }
5194
- log.error(pc.yellow("Supabase started, but could not extract DB URL automatically."));
5195
- displayManualSupabaseInstructions(supabaseOutput);
5601
+ cliLog.error(pc.yellow("Supabase started, but could not extract DB URL automatically."));
5602
+ displayManualSupabaseInstructions(targetApp, supabaseOutput);
5196
5603
  return databaseSetupError("supabase", "Could not extract database URL from Supabase output. Please configure manually.");
5197
5604
  }
5198
-
5199
5605
  //#endregion
5200
5606
  //#region src/helpers/database-providers/turso-setup.ts
5201
5607
  async function isTursoInstalled() {
@@ -5211,7 +5617,7 @@ async function isTursoLoggedIn() {
5211
5617
  return result.isOk() ? result.value : false;
5212
5618
  }
5213
5619
  async function loginToTurso() {
5214
- const s = spinner();
5620
+ const s = createSpinner();
5215
5621
  s.start("Logging in to Turso...");
5216
5622
  return Result.tryPromise({
5217
5623
  try: async () => {
@@ -5229,7 +5635,7 @@ async function loginToTurso() {
5229
5635
  });
5230
5636
  }
5231
5637
  async function installTursoCLI(isMac) {
5232
- const s = spinner();
5638
+ const s = createSpinner();
5233
5639
  s.start("Installing Turso CLI...");
5234
5640
  return Result.tryPromise({
5235
5641
  try: async () => {
@@ -5253,7 +5659,7 @@ async function installTursoCLI(isMac) {
5253
5659
  });
5254
5660
  }
5255
5661
  async function getTursoGroups() {
5256
- const s = spinner();
5662
+ const s = createSpinner();
5257
5663
  s.start("Fetching Turso groups...");
5258
5664
  const result = await Result.tryPromise({
5259
5665
  try: async () => {
@@ -5286,7 +5692,7 @@ async function selectTursoGroup() {
5286
5692
  const groups = await getTursoGroups();
5287
5693
  if (groups.length === 0) return Result.ok(null);
5288
5694
  if (groups.length === 1) {
5289
- 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)}`);
5290
5696
  return Result.ok(groups[0].name);
5291
5697
  }
5292
5698
  const selectedGroup = await select({
@@ -5300,7 +5706,7 @@ async function selectTursoGroup() {
5300
5706
  return Result.ok(selectedGroup);
5301
5707
  }
5302
5708
  async function createTursoDatabase(dbName, groupName) {
5303
- const s = spinner();
5709
+ const s = createSpinner();
5304
5710
  s.start(`Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`);
5305
5711
  const createResult = await Result.tryPromise({
5306
5712
  try: async () => {
@@ -5366,94 +5772,134 @@ async function writeEnvFile(projectDir, backend, config) {
5366
5772
  })
5367
5773
  });
5368
5774
  }
5369
- function displayManualSetupInstructions() {
5370
- log.info(`Manual Turso Setup Instructions:
5775
+ function displayManualSetupInstructions(targetApp) {
5776
+ cliLog.info(`Manual Turso Setup Instructions:
5371
5777
 
5372
5778
  1. Visit https://turso.tech and create an account
5373
5779
  2. Create a new database from the dashboard
5374
5780
  3. Get your database URL and authentication token
5375
- 4. Add these credentials to the .env file in apps/server/.env
5781
+ 4. Add these credentials to the .env file in ${targetApp}/.env
5376
5782
 
5377
5783
  DATABASE_URL=your_database_url
5378
5784
  DATABASE_AUTH_TOKEN=your_auth_token`);
5379
5785
  }
5380
5786
  async function setupTurso(config, cliInput) {
5381
5787
  const { projectDir, backend } = config;
5382
- const manualDb = cliInput?.manualDb ?? false;
5383
- const setupSpinner = spinner();
5384
- 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") {
5385
5795
  const envResult = await writeEnvFile(projectDir, backend);
5386
5796
  if (envResult.isErr()) return envResult;
5387
- displayManualSetupInstructions();
5797
+ displayManualSetupInstructions(targetApp);
5388
5798
  return Result.ok(void 0);
5389
5799
  }
5390
- const mode = await select({
5391
- message: "Turso setup: choose mode",
5392
- options: [{
5393
- label: "Automatic",
5394
- value: "auto",
5395
- hint: "Automated setup with provider CLI, sets .env"
5396
- }, {
5397
- label: "Manual",
5398
- value: "manual",
5399
- hint: "Manual setup, add env vars yourself"
5400
- }],
5401
- initialValue: "auto"
5402
- });
5403
- 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
+ }
5404
5819
  if (mode === "manual") {
5405
5820
  const envResult = await writeEnvFile(projectDir, backend);
5406
5821
  if (envResult.isErr()) return envResult;
5407
- displayManualSetupInstructions();
5822
+ displayManualSetupInstructions(targetApp);
5408
5823
  return Result.ok(void 0);
5409
5824
  }
5410
5825
  setupSpinner.start("Checking Turso CLI availability...");
5411
- const platform = os.platform();
5826
+ const platform = os$1.platform();
5412
5827
  const isMac = platform === "darwin";
5413
5828
  if (platform === "win32") {
5414
5829
  setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
5415
- 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."));
5416
5831
  const envResult = await writeEnvFile(projectDir, backend);
5417
5832
  if (envResult.isErr()) return envResult;
5418
- displayManualSetupInstructions();
5833
+ displayManualSetupInstructions(targetApp);
5419
5834
  return Result.ok(void 0);
5420
5835
  }
5421
5836
  setupSpinner.stop("Turso CLI availability checked");
5422
5837
  if (!await isTursoInstalled()) {
5423
- const shouldInstall = await confirm({
5424
- message: "Would you like to install Turso CLI?",
5425
- initialValue: true
5426
- });
5427
- 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
+ }
5428
5848
  if (!shouldInstall) {
5429
5849
  const envResult = await writeEnvFile(projectDir, backend);
5430
5850
  if (envResult.isErr()) return envResult;
5431
- displayManualSetupInstructions();
5851
+ displayManualSetupInstructions(targetApp);
5432
5852
  return Result.ok(void 0);
5433
5853
  }
5434
5854
  const installResult = await installTursoCLI(isMac);
5435
5855
  if (installResult.isErr()) {
5436
- log.error(pc.red(installResult.error.message));
5856
+ cliLog.error(pc.red(installResult.error.message));
5437
5857
  const envResult = await writeEnvFile(projectDir, backend);
5438
5858
  if (envResult.isErr()) return envResult;
5439
- displayManualSetupInstructions();
5859
+ displayManualSetupInstructions(targetApp);
5440
5860
  return Result.ok(void 0);
5441
5861
  }
5442
5862
  }
5443
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
+ }
5444
5871
  const loginResult = await loginToTurso();
5445
5872
  if (loginResult.isErr()) {
5446
- 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));
5447
5892
  const envResult = await writeEnvFile(projectDir, backend);
5448
5893
  if (envResult.isErr()) return envResult;
5449
- displayManualSetupInstructions();
5894
+ displayManualSetupInstructions(targetApp);
5895
+ cliLog.success("Setup completed with manual configuration required.");
5450
5896
  return Result.ok(void 0);
5451
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);
5452
5902
  }
5453
- const groupResult = await selectTursoGroup();
5454
- if (groupResult.isErr()) return groupResult;
5455
- const selectedGroup = groupResult.value;
5456
- let suggestedName = path.basename(projectDir);
5457
5903
  while (true) {
5458
5904
  const dbNameResponse = await text({
5459
5905
  message: "Enter a name for your database:",
@@ -5466,24 +5912,23 @@ async function setupTurso(config, cliInput) {
5466
5912
  const createResult = await createTursoDatabase(dbName, selectedGroup);
5467
5913
  if (createResult.isErr()) {
5468
5914
  if (createResult.error.message === "DATABASE_EXISTS") {
5469
- log.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
5915
+ cliLog.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
5470
5916
  suggestedName = `${dbName}-${Math.floor(Math.random() * 1e3)}`;
5471
5917
  continue;
5472
5918
  }
5473
- log.error(pc.red(createResult.error.message));
5919
+ cliLog.error(pc.red(createResult.error.message));
5474
5920
  const envResult = await writeEnvFile(projectDir, backend);
5475
5921
  if (envResult.isErr()) return envResult;
5476
- displayManualSetupInstructions();
5477
- log.success("Setup completed with manual configuration required.");
5922
+ displayManualSetupInstructions(targetApp);
5923
+ cliLog.success("Setup completed with manual configuration required.");
5478
5924
  return Result.ok(void 0);
5479
5925
  }
5480
5926
  const envResult = await writeEnvFile(projectDir, backend, createResult.value);
5481
5927
  if (envResult.isErr()) return envResult;
5482
- log.success("Turso database setup completed successfully!");
5928
+ cliLog.success("Turso database setup completed successfully!");
5483
5929
  return Result.ok(void 0);
5484
5930
  }
5485
5931
  }
5486
-
5487
5932
  //#endregion
5488
5933
  //#region src/helpers/core/db-setup.ts
5489
5934
  async function setupDatabase(config, cliInput) {
@@ -5504,18 +5949,21 @@ async function setupDatabase(config, cliInput) {
5504
5949
  consola.error(pc.red(result.error.message));
5505
5950
  }
5506
5951
  }
5952
+ const resolvedCliInput = {
5953
+ ...cliInput,
5954
+ dbSetupOptions: mergeResolvedDbSetupOptions(dbSetup, config.dbSetupOptions, cliInput)
5955
+ };
5507
5956
  if (dbSetup === "docker") await runSetup(() => setupDockerCompose(config));
5508
- else if (database === "sqlite" && dbSetup === "turso") await runSetup(() => setupTurso(config, cliInput));
5957
+ else if (database === "sqlite" && dbSetup === "turso") await runSetup(() => setupTurso(config, resolvedCliInput));
5509
5958
  else if (database === "sqlite" && dbSetup === "d1") await runSetup(() => setupCloudflareD1(config));
5510
5959
  else if (database === "postgres") {
5511
- if (dbSetup === "prisma-postgres") await runSetup(() => setupPrismaPostgres(config, cliInput));
5512
- 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));
5513
5962
  else if (dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
5514
- else if (dbSetup === "supabase") await runSetup(() => setupSupabase(config, cliInput));
5963
+ else if (dbSetup === "supabase") await runSetup(() => setupSupabase(config, resolvedCliInput));
5515
5964
  } else if (database === "mysql" && dbSetup === "planetscale") await runSetup(() => setupPlanetScale(config));
5516
- 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));
5517
5966
  }
5518
-
5519
5967
  //#endregion
5520
5968
  //#region src/helpers/core/git.ts
5521
5969
  async function initializeGit(projectDir, useGit) {
@@ -5525,7 +5973,7 @@ async function initializeGit(projectDir, useGit) {
5525
5973
  reject: false,
5526
5974
  stderr: "pipe"
5527
5975
  })`git --version`).exitCode !== 0) {
5528
- log.warn(pc.yellow("Git is not installed"));
5976
+ cliLog.warn(pc.yellow("Git is not installed"));
5529
5977
  return Result.ok(void 0);
5530
5978
  }
5531
5979
  const result = await $({
@@ -5549,7 +5997,6 @@ async function initializeGit(projectDir, useGit) {
5549
5997
  })
5550
5998
  });
5551
5999
  }
5552
-
5553
6000
  //#endregion
5554
6001
  //#region src/utils/docker-utils.ts
5555
6002
  async function isDockerInstalled() {
@@ -5585,7 +6032,7 @@ function getDockerInstallInstructions(platform, database) {
5585
6032
  return `${pc.yellow("IMPORTANT:")} Docker required for ${databaseName}. Install for ${platformName}:\n${pc.blue(installUrl)}`;
5586
6033
  }
5587
6034
  async function getDockerStatus(database) {
5588
- const platform = os.platform();
6035
+ const platform = os$1.platform();
5589
6036
  if (!await isDockerInstalled()) return {
5590
6037
  installed: false,
5591
6038
  running: false,
@@ -5601,7 +6048,6 @@ async function getDockerStatus(database) {
5601
6048
  running: true
5602
6049
  };
5603
6050
  }
5604
-
5605
6051
  //#endregion
5606
6052
  //#region src/helpers/core/post-installation.ts
5607
6053
  async function displayPostInstallInstructions(config) {
@@ -5782,7 +6228,6 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
5782
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`}`);
5783
6229
  return instructions.length ? `\n${instructions.join("\n")}` : "";
5784
6230
  }
5785
-
5786
6231
  //#endregion
5787
6232
  //#region src/helpers/core/create-project.ts
5788
6233
  /**
@@ -5851,7 +6296,7 @@ async function setPackageManagerVersion(projectDir, packageManager) {
5851
6296
  if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
5852
6297
  const versionResult = await Result.tryPromise({
5853
6298
  try: async () => {
5854
- const { stdout } = await $({ cwd: os.tmpdir() })`${packageManager} -v`;
6299
+ const { stdout } = await $({ cwd: os$1.tmpdir() })`${packageManager} -v`;
5855
6300
  return stdout.trim();
5856
6301
  },
5857
6302
  catch: () => null
@@ -5870,7 +6315,6 @@ async function setPackageManagerVersion(projectDir, packageManager) {
5870
6315
  })
5871
6316
  });
5872
6317
  }
5873
-
5874
6318
  //#endregion
5875
6319
  //#region src/helpers/core/command-handlers.ts
5876
6320
  /**
@@ -5953,21 +6397,32 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
5953
6397
  });
5954
6398
  }
5955
6399
  }));
6400
+ yield* validateResolvedProjectPathInput(currentPathInput);
5956
6401
  let finalPathInput;
5957
6402
  let shouldClearDirectory;
5958
6403
  const conflictResult = yield* Result.await(handleDirectoryConflictResult(currentPathInput, input.directoryConflict));
5959
6404
  finalPathInput = conflictResult.finalPathInput;
5960
6405
  shouldClearDirectory = conflictResult.shouldClearDirectory;
5961
- const { finalResolvedPath, finalBaseName } = yield* Result.await(Result.tryPromise({
5962
- try: async () => setupProjectDirectory(finalPathInput, shouldClearDirectory),
5963
- catch: (e) => {
5964
- if (e instanceof UserCancelledError) return e;
5965
- return new CLIError({
5966
- message: e instanceof Error ? e.message : String(e),
5967
- cause: e
5968
- });
5969
- }
5970
- }));
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
+ }
5971
6426
  const originalInput = {
5972
6427
  ...input,
5973
6428
  projectDirectory: input.projectName
@@ -6041,8 +6496,38 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
6041
6496
  }
6042
6497
  }));
6043
6498
  }
6044
- 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
+ };
6045
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
+ }));
6046
6531
  if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
6047
6532
  await trackProjectCreation(config, input.disableAnalytics);
6048
6533
  const historyResult = await addToHistory(config, reproducibleCommand);
@@ -6068,16 +6553,24 @@ function isPathWithinCwd(targetPath) {
6068
6553
  const rel = path.relative(process.cwd(), resolved);
6069
6554
  return !rel.startsWith("..") && !path.isAbsolute(rel);
6070
6555
  }
6071
- async function resolveProjectNameForSilent(input) {
6072
- const defaultConfig = getDefaultConfig();
6073
- const candidate = (input.projectName?.trim() || void 0) ?? defaultConfig.relativePath;
6074
- 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);
6075
6563
  const validationResult = validateProjectName(path.basename(candidate));
6076
6564
  if (validationResult.isErr()) return Result.err(new CLIError({
6077
6565
  message: validationResult.error.message,
6078
6566
  cause: validationResult.error
6079
6567
  }));
6080
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;
6081
6574
  return Result.ok(candidate);
6082
6575
  }
6083
6576
  async function handleDirectoryConflictResult(currentPathInput, strategy) {
@@ -6130,6 +6623,267 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
6130
6623
  default: return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
6131
6624
  }
6132
6625
  }
6133
-
6134
6626
  //#endregion
6135
- export { openDocsCommand as a, getLatestCLIVersion as c, DatabaseSetupError as d, DirectoryConflictError as f, ValidationError as h, openBuilderCommand as i, CLIError as l, UserCancelledError as m, addHandler as n, showSponsorsCommand as o, ProjectCreationError as p, types_exports as r, historyHandler as s, createProjectHandler as t, CompatibilityError as u };
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
+ }
6671
+ const router = os.router({
6672
+ create: os.meta({
6673
+ description: "Create a new Better-T-Stack project",
6674
+ default: true,
6675
+ negateBooleans: true
6676
+ }).input(z.tuple([types_exports.ProjectNameSchema.optional(), z.object({
6677
+ template: types_exports.TemplateSchema.optional().describe("Use a predefined template"),
6678
+ yes: z.boolean().optional().default(false).describe("Use default configuration"),
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"),
6681
+ verbose: z.boolean().optional().default(false).describe("Show detailed result information"),
6682
+ database: types_exports.DatabaseSchema.optional(),
6683
+ orm: types_exports.ORMSchema.optional(),
6684
+ auth: types_exports.AuthSchema.optional(),
6685
+ payments: types_exports.PaymentsSchema.optional(),
6686
+ frontend: z.array(types_exports.FrontendSchema).optional(),
6687
+ addons: z.array(types_exports.AddonsSchema).optional(),
6688
+ examples: z.array(types_exports.ExamplesSchema).optional(),
6689
+ git: z.boolean().optional(),
6690
+ packageManager: types_exports.PackageManagerSchema.optional(),
6691
+ install: z.boolean().optional(),
6692
+ dbSetup: types_exports.DatabaseSetupSchema.optional(),
6693
+ backend: types_exports.BackendSchema.optional(),
6694
+ runtime: types_exports.RuntimeSchema.optional(),
6695
+ api: types_exports.APISchema.optional(),
6696
+ webDeploy: types_exports.WebDeploySchema.optional(),
6697
+ serverDeploy: types_exports.ServerDeploySchema.optional(),
6698
+ directoryConflict: types_exports.DirectoryConflictSchema.optional(),
6699
+ renderTitle: z.boolean().optional(),
6700
+ disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics"),
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")
6703
+ })])).handler(async ({ input }) => {
6704
+ const [projectName, options] = input;
6705
+ const result = await createProjectHandler({
6706
+ projectName,
6707
+ ...options
6708
+ });
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;
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)),
6721
+ sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(showSponsorsCommand),
6722
+ docs: os.meta({ description: "Open Better-T-Stack documentation" }).handler(openDocsCommand),
6723
+ builder: os.meta({ description: "Open the web-based stack builder" }).handler(openBuilderCommand),
6724
+ add: os.meta({ description: "Add addons to an existing Better-T-Stack project" }).input(z.object({
6725
+ addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
6726
+ install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
6727
+ packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
6728
+ projectDir: z.string().optional().describe("Project directory (defaults to current)")
6729
+ })).handler(async ({ input }) => {
6730
+ await addHandler(input);
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
+ }),
6741
+ history: os.meta({ description: "Show project creation history" }).input(z.object({
6742
+ limit: z.number().optional().default(10).describe("Number of entries to show"),
6743
+ clear: z.boolean().optional().default(false).describe("Clear all history"),
6744
+ json: z.boolean().optional().default(false).describe("Output as JSON")
6745
+ })).handler(async ({ input }) => {
6746
+ await historyHandler(input);
6747
+ })
6748
+ });
6749
+ function createBtsCli() {
6750
+ return createCli({
6751
+ router,
6752
+ name: "create-better-t-stack",
6753
+ version: getLatestCLIVersion()
6754
+ });
6755
+ }
6756
+ /**
6757
+ * Programmatic API to create a new Better-T-Stack project.
6758
+ * Returns a Result type - no console output, no interactive prompts.
6759
+ *
6760
+ * @example
6761
+ * ```typescript
6762
+ * import { create, Result } from "create-better-t-stack";
6763
+ *
6764
+ * const result = await create("my-app", {
6765
+ * frontend: ["tanstack-router"],
6766
+ * backend: "hono",
6767
+ * runtime: "bun",
6768
+ * database: "sqlite",
6769
+ * orm: "drizzle",
6770
+ * });
6771
+ *
6772
+ * result.match({
6773
+ * ok: (data) => console.log(`Project created at: ${data.projectDirectory}`),
6774
+ * err: (error) => console.error(`Failed: ${error.message}`),
6775
+ * });
6776
+ *
6777
+ * // Or use unwrapOr for a default value
6778
+ * const data = result.unwrapOr(null);
6779
+ * ```
6780
+ */
6781
+ async function create(projectName, options) {
6782
+ const input = {
6783
+ ...options,
6784
+ projectName,
6785
+ renderTitle: false,
6786
+ verbose: true,
6787
+ disableAnalytics: options?.disableAnalytics ?? true,
6788
+ directoryConflict: options?.directoryConflict ?? "error"
6789
+ };
6790
+ return Result.tryPromise({
6791
+ try: async () => {
6792
+ const result = await createProjectHandler(input, { silent: true });
6793
+ if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
6794
+ if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
6795
+ return result;
6796
+ },
6797
+ catch: (e) => {
6798
+ if (e instanceof UserCancelledError) return e;
6799
+ if (e instanceof CLIError) return e;
6800
+ if (e instanceof ProjectCreationError) return e;
6801
+ return new CLIError({
6802
+ message: e instanceof Error ? e.message : String(e),
6803
+ cause: e
6804
+ });
6805
+ }
6806
+ });
6807
+ }
6808
+ async function sponsors() {
6809
+ return showSponsorsCommand();
6810
+ }
6811
+ async function docs() {
6812
+ return openDocsCommand();
6813
+ }
6814
+ async function builder() {
6815
+ return openBuilderCommand();
6816
+ }
6817
+ /**
6818
+ * Programmatic API to generate a project in-memory (virtual filesystem).
6819
+ * Returns a Result with a VirtualFileTree without writing to disk.
6820
+ * Useful for web previews and testing.
6821
+ *
6822
+ * @example
6823
+ * ```typescript
6824
+ * import { createVirtual, EMBEDDED_TEMPLATES, Result } from "create-better-t-stack";
6825
+ *
6826
+ * const result = await createVirtual({
6827
+ * frontend: ["tanstack-router"],
6828
+ * backend: "hono",
6829
+ * runtime: "bun",
6830
+ * database: "sqlite",
6831
+ * orm: "drizzle",
6832
+ * });
6833
+ *
6834
+ * result.match({
6835
+ * ok: (tree) => console.log(`Generated ${tree.fileCount} files`),
6836
+ * err: (error) => console.error(`Failed: ${error.message}`),
6837
+ * });
6838
+ * ```
6839
+ */
6840
+ async function createVirtual(options) {
6841
+ return generate({
6842
+ config: {
6843
+ projectName: options.projectName || "my-project",
6844
+ projectDir: "/virtual",
6845
+ relativePath: "./virtual",
6846
+ addonOptions: options.addonOptions,
6847
+ dbSetupOptions: options.dbSetupOptions,
6848
+ database: options.database || "none",
6849
+ orm: options.orm || "none",
6850
+ backend: options.backend || "hono",
6851
+ runtime: options.runtime || "bun",
6852
+ frontend: options.frontend || ["tanstack-router"],
6853
+ addons: options.addons || [],
6854
+ examples: options.examples || [],
6855
+ auth: options.auth || "none",
6856
+ payments: options.payments || "none",
6857
+ git: options.git ?? false,
6858
+ packageManager: options.packageManager || "bun",
6859
+ install: false,
6860
+ dbSetup: options.dbSetup || "none",
6861
+ api: options.api || "trpc",
6862
+ webDeploy: options.webDeploy || "none",
6863
+ serverDeploy: options.serverDeploy || "none"
6864
+ },
6865
+ templates: EMBEDDED_TEMPLATES
6866
+ });
6867
+ }
6868
+ /**
6869
+ * Programmatic API to add addons to an existing Better-T-Stack project.
6870
+ *
6871
+ * @example
6872
+ * ```typescript
6873
+ * import { add } from "create-better-t-stack";
6874
+ *
6875
+ * const result = await add({
6876
+ * addons: ["biome", "husky"],
6877
+ * install: true,
6878
+ * });
6879
+ *
6880
+ * if (result?.success) {
6881
+ * console.log(`Added: ${result.addedAddons.join(", ")}`);
6882
+ * }
6883
+ * ```
6884
+ */
6885
+ async function add(options = {}) {
6886
+ return addHandler(options, { silent: true });
6887
+ }
6888
+ //#endregion
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 };