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.
- package/README.md +34 -3
- package/dist/{chunk-C8ucw2H5.mjs → chunk-BtN16TXe.mjs} +12 -21
- package/dist/cli.mjs +289 -5
- package/dist/index.d.mts +710 -3
- package/dist/index.mjs +2 -206
- package/dist/{command-handlers-Dvaw7W_l.mjs → src-COTG6r9y.mjs} +1483 -729
- package/dist/virtual.d.mts +2 -1
- package/dist/virtual.mjs +3 -3
- package/package.json +14 -13
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as __reExport, t as __exportAll } from "./chunk-
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
"
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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 =
|
|
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 =
|
|
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((
|
|
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())
|
|
1880
|
+
})).isErr()) {
|
|
1881
|
+
cliLog.warn(pc.yellow(`Warning: Could not install MCP server '${server.name}'`));
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
successfulInstalls += 1;
|
|
1761
1885
|
}
|
|
1762
|
-
|
|
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 =
|
|
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
|
-
|
|
1952
|
+
cliLog.info("Setting up Ruler...");
|
|
1818
1953
|
const rulerDir = path.join(projectDir, ".ruler");
|
|
1819
1954
|
if (!await fs.pathExists(rulerDir)) {
|
|
1820
|
-
|
|
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
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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
|
-
|
|
1866
|
-
|
|
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 =
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
2150
|
-
const
|
|
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
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
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
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
"
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
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 =
|
|
2371
|
+
const installSpinner = createSpinner();
|
|
2200
2372
|
installSpinner.start("Installing skills...");
|
|
2201
|
-
const
|
|
2202
|
-
const
|
|
2203
|
-
for (const [source, skills] of Object.entries(skillsBySource)) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2453
|
+
return [
|
|
2454
|
+
...getPackageRunnerPrefix(packageManager),
|
|
2280
2455
|
"@tauri-apps/cli@latest",
|
|
2281
2456
|
"init",
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2588
|
-
const
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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())
|
|
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 =
|
|
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
|
|
3218
|
+
const tree = {
|
|
2912
3219
|
root: vfs.toTree(""),
|
|
2913
3220
|
fileCount: vfs.getFileCount(),
|
|
2914
3221
|
directoryCount: vfs.getDirectoryCount(),
|
|
2915
3222
|
config
|
|
2916
|
-
}
|
|
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, {
|
|
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)
|
|
4459
|
-
else
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
4545
|
-
|
|
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
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
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
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4781
|
-
|
|
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
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
}
|
|
4795
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5044
|
-
if (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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
5150
|
-
displayManualSupabaseInstructions();
|
|
5551
|
+
if (setupMode === "manual") {
|
|
5552
|
+
displayManualSupabaseInstructions(targetApp);
|
|
5151
5553
|
return writeSupabaseEnvFile(projectDir, backend, "");
|
|
5152
5554
|
}
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
|
5594
|
+
if (envResult.isOk()) cliLog.success(pc.green("Supabase local development setup ready!"));
|
|
5188
5595
|
else {
|
|
5189
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
5383
|
-
const
|
|
5384
|
-
|
|
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
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
5512
|
-
else if (dbSetup === "neon") await runSetup(() => setupNeonPostgres(config,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6072
|
-
const
|
|
6073
|
-
|
|
6074
|
-
|
|
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
|
-
|
|
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 };
|