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