create-better-t-stack 3.27.4 → 3.28.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 +17 -17
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +18 -12
- package/dist/index.mjs +1 -1
- package/dist/{src-nmBsmday.mjs → src-CHenuE55.mjs} +994 -308
- package/package.json +3 -3
|
@@ -5,7 +5,7 @@ import { initTRPC } from "@trpc/server";
|
|
|
5
5
|
import { Result, Result as Result$1, TaggedError } from "better-result";
|
|
6
6
|
import { createCli } from "trpc-cli";
|
|
7
7
|
import z from "zod";
|
|
8
|
-
import { cancel, confirm,
|
|
8
|
+
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
9
9
|
import pc from "picocolors";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import envPaths from "env-paths";
|
|
@@ -86,6 +86,7 @@ const ADDON_COMPATIBILITY = {
|
|
|
86
86
|
opentui: [],
|
|
87
87
|
wxt: [],
|
|
88
88
|
skills: [],
|
|
89
|
+
evlog: [],
|
|
89
90
|
none: []
|
|
90
91
|
};
|
|
91
92
|
//#endregion
|
|
@@ -757,7 +758,7 @@ function splitFrontends(values = []) {
|
|
|
757
758
|
}
|
|
758
759
|
function ensureSingleWebAndNative(frontends) {
|
|
759
760
|
const { web, native } = splitFrontends(frontends);
|
|
760
|
-
if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
|
|
761
|
+
if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid, astro");
|
|
761
762
|
if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
|
|
762
763
|
return Result.ok(void 0);
|
|
763
764
|
}
|
|
@@ -765,18 +766,36 @@ const FULLSTACK_FRONTENDS$1 = [
|
|
|
765
766
|
"next",
|
|
766
767
|
"tanstack-start",
|
|
767
768
|
"nuxt",
|
|
769
|
+
"svelte",
|
|
768
770
|
"astro"
|
|
769
771
|
];
|
|
772
|
+
const EVLOG_SERVER_BACKENDS = [
|
|
773
|
+
"hono",
|
|
774
|
+
"express",
|
|
775
|
+
"fastify",
|
|
776
|
+
"elysia"
|
|
777
|
+
];
|
|
778
|
+
const EVLOG_FULLSTACK_FRONTENDS = FULLSTACK_FRONTENDS$1;
|
|
779
|
+
const evlogCompatibilityMessage = "evlog addon supports Hono, Express, Fastify, Elysia, or backend self with Next.js, TanStack Start, Nuxt, SvelteKit, or Astro. Convex and backend none are not supported yet.";
|
|
780
|
+
function supportsEvlogAddon(frontend = [], backend, _runtime) {
|
|
781
|
+
if (!backend) return true;
|
|
782
|
+
if (EVLOG_SERVER_BACKENDS.includes(backend)) return true;
|
|
783
|
+
if (backend === "self") {
|
|
784
|
+
if (frontend.length === 0) return true;
|
|
785
|
+
return frontend.some((f) => EVLOG_FULLSTACK_FRONTENDS.includes(f));
|
|
786
|
+
}
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
770
789
|
function validateSelfBackendCompatibility(providedFlags, options, config) {
|
|
771
790
|
const backend = config.backend || options.backend;
|
|
772
791
|
const frontends = config.frontend || options.frontend || [];
|
|
773
792
|
if (backend === "self") {
|
|
774
793
|
const { web, native } = splitFrontends(frontends);
|
|
775
|
-
if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, or --frontend astro.
|
|
794
|
+
if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, or --frontend astro.");
|
|
776
795
|
if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
|
|
777
796
|
}
|
|
778
797
|
const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
|
|
779
|
-
if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend astro, or choose a different backend.
|
|
798
|
+
if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend astro, or choose a different backend.");
|
|
780
799
|
return Result.ok(void 0);
|
|
781
800
|
}
|
|
782
801
|
function validateWorkersCompatibility(providedFlags, options, config) {
|
|
@@ -849,7 +868,11 @@ function validateServerDeployRequiresBackend(serverDeploy, backend) {
|
|
|
849
868
|
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
|
|
850
869
|
return Result.ok(void 0);
|
|
851
870
|
}
|
|
852
|
-
function validateAddonCompatibility(addon, frontend, _auth) {
|
|
871
|
+
function validateAddonCompatibility(addon, frontend, _auth, backend, runtime) {
|
|
872
|
+
if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
|
|
873
|
+
isCompatible: false,
|
|
874
|
+
reason: evlogCompatibilityMessage
|
|
875
|
+
};
|
|
853
876
|
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
|
|
854
877
|
if (compatibleFrontends.length > 0) {
|
|
855
878
|
if (!frontend.some((f) => compatibleFrontends.includes(f))) return {
|
|
@@ -859,23 +882,26 @@ function validateAddonCompatibility(addon, frontend, _auth) {
|
|
|
859
882
|
}
|
|
860
883
|
return { isCompatible: true };
|
|
861
884
|
}
|
|
862
|
-
function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth) {
|
|
885
|
+
function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, backend, runtime) {
|
|
863
886
|
return allAddons.filter((addon) => {
|
|
864
887
|
if (existingAddons.includes(addon)) return false;
|
|
865
888
|
if (addon === "none") return false;
|
|
866
|
-
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
|
|
889
|
+
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth, backend, runtime);
|
|
867
890
|
return isCompatible;
|
|
868
891
|
});
|
|
869
892
|
}
|
|
870
|
-
function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
|
|
893
|
+
function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
|
|
871
894
|
if (addons.includes("turborepo") && addons.includes("nx")) return validationErr$1("Cannot combine 'turborepo' and 'nx' addons. Choose one monorepo tool.");
|
|
872
895
|
for (const addon of addons) {
|
|
873
896
|
if (addon === "none") continue;
|
|
874
|
-
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
|
|
897
|
+
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
|
|
875
898
|
if (!isCompatible) return validationErr$1(`Incompatible addon/frontend combination: ${reason}`);
|
|
876
899
|
}
|
|
877
900
|
return Result.ok(void 0);
|
|
878
901
|
}
|
|
902
|
+
function validateAddonsAgainstConfig(addons = [], config) {
|
|
903
|
+
return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
|
|
904
|
+
}
|
|
879
905
|
function validatePaymentsCompatibility(payments, auth, _backend, frontends = []) {
|
|
880
906
|
if (!payments || payments === "none") return Result.ok(void 0);
|
|
881
907
|
if (payments === "polar") {
|
|
@@ -1207,6 +1233,10 @@ function getAddonDisplay(addon) {
|
|
|
1207
1233
|
label = "MCP";
|
|
1208
1234
|
hint = "Install MCP servers, including Better T Stack, via add-mcp";
|
|
1209
1235
|
break;
|
|
1236
|
+
case "evlog":
|
|
1237
|
+
label = "evlog";
|
|
1238
|
+
hint = "Request logging with Better Auth context and AI SDK telemetry";
|
|
1239
|
+
break;
|
|
1210
1240
|
default:
|
|
1211
1241
|
label = addon;
|
|
1212
1242
|
hint = `Add ${addon}`;
|
|
@@ -1233,6 +1263,7 @@ const ADDON_GROUPS = {
|
|
|
1233
1263
|
"opentui",
|
|
1234
1264
|
"wxt"
|
|
1235
1265
|
],
|
|
1266
|
+
Observability: ["evlog"],
|
|
1236
1267
|
"AI & Agent Tools": ["skills", "mcp"]
|
|
1237
1268
|
};
|
|
1238
1269
|
function createGroupedOptions() {
|
|
@@ -1259,13 +1290,13 @@ function sortAndPruneGroupedOptions(groupedOptions) {
|
|
|
1259
1290
|
function validateAddonSelection(selected) {
|
|
1260
1291
|
if (selected?.includes("turborepo") && selected.includes("nx")) return "Choose either Turborepo or Nx as your monorepo tool, not both.";
|
|
1261
1292
|
}
|
|
1262
|
-
async function getAddonsChoice(addons, frontends, auth) {
|
|
1293
|
+
async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
|
|
1263
1294
|
if (addons !== void 0) return addons;
|
|
1264
1295
|
const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
|
|
1265
1296
|
const groupedOptions = createGroupedOptions();
|
|
1266
1297
|
const frontendsArray = frontends || [];
|
|
1267
1298
|
for (const addon of allAddons) {
|
|
1268
|
-
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
|
|
1299
|
+
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth, backend, runtime);
|
|
1269
1300
|
if (!isCompatible) continue;
|
|
1270
1301
|
const { label, hint } = getAddonDisplay(addon);
|
|
1271
1302
|
addOptionToGroup(groupedOptions, {
|
|
@@ -1285,10 +1316,10 @@ async function getAddonsChoice(addons, frontends, auth) {
|
|
|
1285
1316
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
1286
1317
|
return response;
|
|
1287
1318
|
}
|
|
1288
|
-
async function getAddonsToAdd(
|
|
1319
|
+
async function getAddonsToAdd(config) {
|
|
1289
1320
|
const groupedOptions = createGroupedOptions();
|
|
1290
|
-
const frontendArray = frontend || [];
|
|
1291
|
-
const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray,
|
|
1321
|
+
const frontendArray = config.frontend || [];
|
|
1322
|
+
const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, config.addons, config.auth, config.backend, config.runtime);
|
|
1292
1323
|
for (const addon of compatibleAddons) {
|
|
1293
1324
|
const { label, hint } = getAddonDisplay(addon);
|
|
1294
1325
|
addOptionToGroup(groupedOptions, {
|
|
@@ -1381,6 +1412,597 @@ const addPackageDependency = async (opts) => {
|
|
|
1381
1412
|
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
1382
1413
|
};
|
|
1383
1414
|
//#endregion
|
|
1415
|
+
//#region src/helpers/addons/evlog-setup.ts
|
|
1416
|
+
const evlogBackends = [
|
|
1417
|
+
"hono",
|
|
1418
|
+
"express",
|
|
1419
|
+
"fastify",
|
|
1420
|
+
"elysia"
|
|
1421
|
+
];
|
|
1422
|
+
const evlogWebFrontends = [
|
|
1423
|
+
"next",
|
|
1424
|
+
"nuxt",
|
|
1425
|
+
"svelte",
|
|
1426
|
+
"tanstack-start",
|
|
1427
|
+
"astro"
|
|
1428
|
+
];
|
|
1429
|
+
function isEvlogBackend(backend) {
|
|
1430
|
+
return evlogBackends.includes(backend);
|
|
1431
|
+
}
|
|
1432
|
+
function getEvlogWebFrontend(frontends) {
|
|
1433
|
+
return frontends.find((frontend) => evlogWebFrontends.includes(frontend));
|
|
1434
|
+
}
|
|
1435
|
+
function shouldIdentifyWebAuth(config) {
|
|
1436
|
+
return config.auth === "better-auth" && config.backend === "self";
|
|
1437
|
+
}
|
|
1438
|
+
function prependMissingImports(content, imports) {
|
|
1439
|
+
const missingImports = imports.filter((line) => !content.includes(line));
|
|
1440
|
+
if (missingImports.length === 0) return content;
|
|
1441
|
+
const importBlock = `${missingImports.join("\n")}\n`;
|
|
1442
|
+
const referenceMatch = content.match(/^(?:\/\/\/ <reference[^\n]*>\n)+/);
|
|
1443
|
+
if (referenceMatch) return `${referenceMatch[0]}${importBlock}${content.slice(referenceMatch[0].length)}`;
|
|
1444
|
+
return `${importBlock}${content}`;
|
|
1445
|
+
}
|
|
1446
|
+
function addNamedImport(content, moduleName, names) {
|
|
1447
|
+
const importRegex = new RegExp(`import \\{([^}]+)\\} from "${moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}";`);
|
|
1448
|
+
const match = content.match(importRegex);
|
|
1449
|
+
if (!match) return prependMissingImports(content, [`import { ${names.join(", ")} } from "${moduleName}";`]);
|
|
1450
|
+
const nextNames = [...match[1].split(",").map((name) => name.trim()).filter(Boolean)];
|
|
1451
|
+
for (const name of names) if (!nextNames.includes(name)) nextNames.push(name);
|
|
1452
|
+
return content.replace(match[0], `import { ${nextNames.join(", ")} } from "${moduleName}";`);
|
|
1453
|
+
}
|
|
1454
|
+
function insertBeforeOnce(content, marker, snippet, alreadyPresent) {
|
|
1455
|
+
if (content.includes(alreadyPresent)) return content;
|
|
1456
|
+
if (!content.includes(marker)) return content;
|
|
1457
|
+
return content.replace(marker, `${snippet}${marker}`);
|
|
1458
|
+
}
|
|
1459
|
+
function insertAfterOnce(content, marker, snippet, alreadyPresent) {
|
|
1460
|
+
if (content.includes(alreadyPresent)) return content;
|
|
1461
|
+
if (!content.includes(marker)) return content;
|
|
1462
|
+
return content.replace(marker, `${marker}${snippet}`);
|
|
1463
|
+
}
|
|
1464
|
+
async function writeFileIfChanged(filePath, content) {
|
|
1465
|
+
if ((await fs.pathExists(filePath) ? await fs.readFile(filePath, "utf-8") : void 0) === content) return;
|
|
1466
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
1467
|
+
await fs.writeFile(filePath, content);
|
|
1468
|
+
}
|
|
1469
|
+
async function updateFileIfExists(filePath, update) {
|
|
1470
|
+
if (!await fs.pathExists(filePath)) return;
|
|
1471
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1472
|
+
const nextContent = update(content);
|
|
1473
|
+
if (nextContent !== content) await fs.writeFile(filePath, nextContent);
|
|
1474
|
+
}
|
|
1475
|
+
function usesCreateAuthFactory(config) {
|
|
1476
|
+
return config.runtime === "workers" || config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare";
|
|
1477
|
+
}
|
|
1478
|
+
function getAuthImportLine(config) {
|
|
1479
|
+
return usesCreateAuthFactory(config) ? `import { createAuth } from "@${config.projectName}/auth";` : `import { auth } from "@${config.projectName}/auth";`;
|
|
1480
|
+
}
|
|
1481
|
+
function getAuthExpression(config) {
|
|
1482
|
+
return usesCreateAuthFactory(config) ? "createAuth()" : "auth";
|
|
1483
|
+
}
|
|
1484
|
+
function addAiSdkEvlogTelemetry(content, loggerExpression) {
|
|
1485
|
+
let nextContent = addNamedImport(content, "evlog/ai", ["createAILogger", "createEvlogIntegration"]);
|
|
1486
|
+
if (!nextContent.includes("const ai = createAILogger(")) nextContent = nextContent.replace(/^(\s*)const model = wrapLanguageModel\({/m, (_match, indent) => `${indent}const ai = createAILogger(${loggerExpression});\n${indent}const model = wrapLanguageModel({`);
|
|
1487
|
+
if (!nextContent.includes("model: ai.wrap(model)")) nextContent = nextContent.replace(/(const result = streamText\({\n\s*)model,/, "$1model: ai.wrap(model),");
|
|
1488
|
+
if (!nextContent.includes("createEvlogIntegration(ai)")) nextContent = nextContent.replace(/(messages:\s*await convertToModelMessages\([^)]+\),?)/, (match) => `${match.endsWith(",") ? match : `${match},`}\n\t\texperimental_telemetry: {\n\t\t\tisEnabled: true,\n\t\t\tintegrations: [createEvlogIntegration(ai)],\n\t\t},`);
|
|
1489
|
+
return nextContent;
|
|
1490
|
+
}
|
|
1491
|
+
function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
|
|
1492
|
+
let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
|
|
1493
|
+
const usesAuthFactory = authExpression.endsWith("()");
|
|
1494
|
+
const evlogAuthExpression = `${authExpression} as BetterAuthInstance`;
|
|
1495
|
+
const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
|
|
1496
|
+
const identifySnippet = usesAuthFactory ? "" : `const identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});\n\n`;
|
|
1497
|
+
const identifyUserSetup = usesAuthFactory ? `\n\tconst identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});` : "";
|
|
1498
|
+
if (backend === "hono") {
|
|
1499
|
+
nextContent = insertBeforeOnce(nextContent, "const app = new Hono", identifySnippet, "createAuthMiddleware(");
|
|
1500
|
+
return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use("*", async (c, next) => {${identifyUserSetup}\n\tawait identifyUser(c.get("log"), c.req.raw.headers, c.req.path);\n\tawait next();\n});`, "identifyUser(c.get(\"log\")");
|
|
1501
|
+
}
|
|
1502
|
+
if (backend === "express") {
|
|
1503
|
+
nextContent = insertBeforeOnce(nextContent, "const app = express();", identifySnippet, "createAuthMiddleware(");
|
|
1504
|
+
return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use(async (req, _res, next) => {${identifyUserSetup}\n\tawait identifyUser(req.log, req.headers, req.path);\n\tnext();\n});`, "identifyUser(req.log");
|
|
1505
|
+
}
|
|
1506
|
+
if (backend === "fastify") {
|
|
1507
|
+
nextContent = addNamedImport(nextContent, "evlog/fastify", ["useLogger"]);
|
|
1508
|
+
nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", identifySnippet, "createAuthMiddleware(");
|
|
1509
|
+
return insertAfterOnce(nextContent, "fastify.register(evlog);", `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
|
|
1510
|
+
}
|
|
1511
|
+
nextContent = insertBeforeOnce(nextContent, "new Elysia", identifySnippet, "createAuthMiddleware(");
|
|
1512
|
+
return insertAfterOnce(nextContent, ".use(evlog())", `\n\t.derive(async ({ request, log }) => {${identifyUserSetup.replace(/\n\t/g, "\n ")}\n\t\tawait identifyUser(log, request.headers, new URL(request.url).pathname);\n\t\treturn {};\n\t})`, "identifyUser(log");
|
|
1513
|
+
}
|
|
1514
|
+
function addEvlogServerSetup(content, backend, serviceName) {
|
|
1515
|
+
const initSnippet = `initLogger({\n\tenv: { service: "${serviceName}" },\n});\n\n`;
|
|
1516
|
+
if (backend === "hono") {
|
|
1517
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog, type EvlogVariables } from \"evlog/hono\";"]);
|
|
1518
|
+
nextContent = insertBeforeOnce(nextContent, "const app = new Hono", initSnippet, "initLogger({");
|
|
1519
|
+
nextContent = nextContent.replace("const app = new Hono();", "const app = new Hono<EvlogVariables>();");
|
|
1520
|
+
nextContent = nextContent.replace("import { logger } from \"hono/logger\";\n", "").replace(/\napp\.use\(logger\(\)\);/, "");
|
|
1521
|
+
return insertAfterOnce(nextContent, "const app = new Hono<EvlogVariables>();", "\n\napp.use(evlog());", "app.use(evlog());");
|
|
1522
|
+
}
|
|
1523
|
+
if (backend === "express") {
|
|
1524
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/express\";"]);
|
|
1525
|
+
nextContent = insertBeforeOnce(nextContent, "const app = express();", initSnippet, "initLogger({");
|
|
1526
|
+
return insertAfterOnce(nextContent, "const app = express();", "\n\napp.use(evlog());", "app.use(evlog());");
|
|
1527
|
+
}
|
|
1528
|
+
if (backend === "fastify") {
|
|
1529
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/fastify\";"]);
|
|
1530
|
+
nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", initSnippet, "initLogger({");
|
|
1531
|
+
return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", "fastify.register(evlog);\n", "fastify.register(evlog);");
|
|
1532
|
+
}
|
|
1533
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/elysia\";"]);
|
|
1534
|
+
nextContent = insertBeforeOnce(nextContent, "new Elysia", initSnippet, "initLogger({");
|
|
1535
|
+
for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, "\n .use(evlog())", ".use(evlog())");
|
|
1536
|
+
return nextContent;
|
|
1537
|
+
}
|
|
1538
|
+
function addNuxtEvlogSetup(content, serviceName) {
|
|
1539
|
+
let nextContent = content;
|
|
1540
|
+
if (!nextContent.includes("\"evlog/nuxt\"") && !nextContent.includes("'evlog/nuxt'")) nextContent = nextContent.replace(/modules:\s*\[/, (match) => `${match}\n "evlog/nuxt",`);
|
|
1541
|
+
if (!nextContent.includes("evlog:")) nextContent = nextContent.replace(/\n\}\)\s*$/, (match) => {
|
|
1542
|
+
const contentBeforeConfigClose = nextContent.slice(0, -match.length);
|
|
1543
|
+
return `${!/[,{]\s*$/.test(contentBeforeConfigClose) ? "," : ""}\n evlog: {\n env: { service: "${serviceName}" },\n },\n})`;
|
|
1544
|
+
});
|
|
1545
|
+
return nextContent;
|
|
1546
|
+
}
|
|
1547
|
+
function addSvelteViteEvlogSetup(content, serviceName) {
|
|
1548
|
+
let nextContent = prependMissingImports(content, ["import evlog from \"evlog/vite\";"]);
|
|
1549
|
+
if (nextContent.includes("evlog({")) return nextContent;
|
|
1550
|
+
return nextContent.replace("plugins: [tailwindcss(), sveltekit()],", `plugins: [\n tailwindcss(),\n sveltekit(),\n evlog({ service: "${serviceName}" }),\n ],`);
|
|
1551
|
+
}
|
|
1552
|
+
function addSvelteHooksEvlogSetup(content) {
|
|
1553
|
+
let nextContent = prependMissingImports(content, ["import { createEvlogHooks } from \"evlog/sveltekit\";"]);
|
|
1554
|
+
if (!nextContent.includes("export const handle") && !nextContent.includes("const authHandle")) {
|
|
1555
|
+
if (!nextContent.includes("createEvlogHooks()")) nextContent = `${nextContent.trimEnd()}\n\nexport const { handle, handleError } = createEvlogHooks();\n`;
|
|
1556
|
+
return nextContent;
|
|
1557
|
+
}
|
|
1558
|
+
nextContent = prependMissingImports(nextContent, ["import { sequence } from \"@sveltejs/kit/hooks\";"]);
|
|
1559
|
+
if (!nextContent.includes("const { handle: evlogHandle, handleError }")) nextContent = nextContent.replace(/((?:import .+\n)+)/, `$1\nconst { handle: evlogHandle, handleError } = createEvlogHooks();\n\n`);
|
|
1560
|
+
nextContent = nextContent.replace(/export const handle(:\s*Handle)?\s*=\s*async/, (_match, typeAnnotation) => `const authHandle${typeAnnotation ?? ""} = async`);
|
|
1561
|
+
if (!nextContent.includes("sequence(evlogHandle, authHandle)")) nextContent = `${nextContent.trimEnd()}\n\nexport const handle = sequence(evlogHandle as Handle, authHandle);\nexport { handleError };\n`;
|
|
1562
|
+
return nextContent;
|
|
1563
|
+
}
|
|
1564
|
+
function addSvelteLocalsType(content) {
|
|
1565
|
+
let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
|
|
1566
|
+
if (nextContent.includes("log: RequestLogger")) return nextContent;
|
|
1567
|
+
if (nextContent.includes("// interface Locals {}")) return nextContent.replace("// interface Locals {}", "interface Locals {\n log: RequestLogger;\n }");
|
|
1568
|
+
return nextContent.replace("namespace App {", "namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
|
|
1569
|
+
}
|
|
1570
|
+
function addTanstackStartRootEvlogSetup(content) {
|
|
1571
|
+
let nextContent = prependMissingImports(content, ["import { createMiddleware } from \"@tanstack/react-start\";", "import { evlogErrorHandler } from \"evlog/nitro/v3\";"]);
|
|
1572
|
+
const middlewareEntry = "createMiddleware().server(evlogErrorHandler)";
|
|
1573
|
+
if (nextContent.includes(`middleware: [${middlewareEntry}]`)) return nextContent;
|
|
1574
|
+
if (nextContent.includes("middleware: [")) return nextContent.replace("middleware: [", `middleware: [${middlewareEntry}, `);
|
|
1575
|
+
if (/server:\s*{/.test(nextContent)) return nextContent.replace(/server:\s*{\n/, `server: {\n middleware: [${middlewareEntry}],\n`);
|
|
1576
|
+
return nextContent.replace("head: () => ({", `server: {\n middleware: [${middlewareEntry}],\n },\n\n head: () => ({`);
|
|
1577
|
+
}
|
|
1578
|
+
function addAstroMiddlewareEvlogSetup(content, serviceName) {
|
|
1579
|
+
let nextContent = prependMissingImports(content, ["import { createRequestLogger, initLogger } from \"evlog\";"]);
|
|
1580
|
+
const initSnippet = `initLogger({\n env: { service: "${serviceName}" },\n});\n\n`;
|
|
1581
|
+
nextContent = insertBeforeOnce(nextContent, "export const onRequest", initSnippet, "initLogger({");
|
|
1582
|
+
if (nextContent.includes("createRequestLogger({")) return nextContent;
|
|
1583
|
+
const contextMarker = "export const onRequest = defineMiddleware(async (context, next) => {";
|
|
1584
|
+
if (nextContent.includes(contextMarker)) {
|
|
1585
|
+
nextContent = insertAfterOnce(nextContent, contextMarker, `\n const url = new URL(context.request.url);\n const log = createRequestLogger({\n method: context.request.method,\n path: url.pathname,\n });\n\n context.locals.log = log;\n`, "const log = createRequestLogger({");
|
|
1586
|
+
return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
|
|
1587
|
+
}
|
|
1588
|
+
const localsMarker = "export const onRequest = defineMiddleware(async ({ request, locals }, next) => {";
|
|
1589
|
+
if (nextContent.includes(localsMarker)) {
|
|
1590
|
+
nextContent = insertAfterOnce(nextContent, localsMarker, `\n const url = new URL(request.url);\n const log = createRequestLogger({\n method: request.method,\n path: url.pathname,\n });\n\n locals.log = log;\n`, "const log = createRequestLogger({");
|
|
1591
|
+
return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
|
|
1592
|
+
}
|
|
1593
|
+
return nextContent;
|
|
1594
|
+
}
|
|
1595
|
+
function addAstroLocalsType(content) {
|
|
1596
|
+
let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
|
|
1597
|
+
if (nextContent.includes("log: RequestLogger")) return nextContent;
|
|
1598
|
+
if (nextContent.includes("interface Locals {")) return nextContent.replace("interface Locals {", "interface Locals {\n log: RequestLogger;");
|
|
1599
|
+
if (nextContent.includes("declare namespace App {")) return nextContent.replace("declare namespace App {", "declare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
|
|
1600
|
+
return `${nextContent.trimEnd()}\n\ndeclare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n}\n`;
|
|
1601
|
+
}
|
|
1602
|
+
function addNextRouteWrappers(content) {
|
|
1603
|
+
let nextContent = prependMissingImports(content, ["import { withEvlog } from \"@/lib/evlog\";"]);
|
|
1604
|
+
if (nextContent.includes("withEvlog(handler)") || nextContent.includes("withEvlog(handleRequest)")) return nextContent;
|
|
1605
|
+
nextContent = nextContent.replace("export { handler as GET, handler as POST };", "export const GET = withEvlog(handler);\nexport const POST = withEvlog(handler);");
|
|
1606
|
+
for (const method of [
|
|
1607
|
+
"GET",
|
|
1608
|
+
"POST",
|
|
1609
|
+
"PUT",
|
|
1610
|
+
"PATCH",
|
|
1611
|
+
"DELETE"
|
|
1612
|
+
]) nextContent = nextContent.replace(`export const ${method} = handleRequest;`, `export const ${method} = withEvlog(handleRequest);`);
|
|
1613
|
+
return nextContent;
|
|
1614
|
+
}
|
|
1615
|
+
function addNextAiEvlogSetup(content) {
|
|
1616
|
+
let nextContent = addNamedImport(content, "@/lib/evlog", ["withEvlog"]);
|
|
1617
|
+
if (!nextContent.includes("withEvlog(async (req: Request)")) {
|
|
1618
|
+
nextContent = nextContent.replace("export async function POST(req: Request) {", "export const POST = withEvlog(async (req: Request) => {");
|
|
1619
|
+
if (nextContent.includes("export const POST = withEvlog(async (req: Request) => {")) nextContent = nextContent.replace(/\n}\s*$/, "\n});\n");
|
|
1620
|
+
}
|
|
1621
|
+
return nextContent;
|
|
1622
|
+
}
|
|
1623
|
+
function addNuxtAiEvlogSetup(content) {
|
|
1624
|
+
return addAiSdkEvlogTelemetry(content, "useLogger(event)");
|
|
1625
|
+
}
|
|
1626
|
+
function addSvelteAiEvlogSetup(content) {
|
|
1627
|
+
return addAiSdkEvlogTelemetry(content.replace("export const POST: RequestHandler = async ({ request }) => {", "export const POST: RequestHandler = async ({ request, locals }) => {"), "locals.log");
|
|
1628
|
+
}
|
|
1629
|
+
function addTanstackStartAiEvlogSetup(content) {
|
|
1630
|
+
return addAiSdkEvlogTelemetry(prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";", "import { useRequest } from \"nitro/context\";"]), "useRequest().context.log as RequestLogger");
|
|
1631
|
+
}
|
|
1632
|
+
function addBackendAiEvlogSetup(content, backend) {
|
|
1633
|
+
if (backend === "hono") return addAiSdkEvlogTelemetry(content, "c.get(\"log\")");
|
|
1634
|
+
if (backend === "express") return addAiSdkEvlogTelemetry(content, "req.log");
|
|
1635
|
+
if (backend === "fastify") return addAiSdkEvlogTelemetry(addNamedImport(content, "evlog/fastify", ["useLogger"]), "useLogger()");
|
|
1636
|
+
return addAiSdkEvlogTelemetry(content, "context.log");
|
|
1637
|
+
}
|
|
1638
|
+
function addNextBetterAuthToRoute(content) {
|
|
1639
|
+
let nextContent = addNamedImport(content, "@/lib/evlog-auth", ["identifyEvlogUser"]);
|
|
1640
|
+
nextContent = nextContent.replace("function handler(req:", "async function handler(req:");
|
|
1641
|
+
for (const marker of [
|
|
1642
|
+
"async function handler(req: NextRequest) {",
|
|
1643
|
+
"async function handleRequest(req: NextRequest) {",
|
|
1644
|
+
"export const POST = withEvlog(async (req: Request) => {"
|
|
1645
|
+
]) nextContent = insertAfterOnce(nextContent, marker, "\n await identifyEvlogUser(req);", "identifyEvlogUser(req)");
|
|
1646
|
+
return nextContent;
|
|
1647
|
+
}
|
|
1648
|
+
function addSvelteBetterAuthEvlogSetup(content, config) {
|
|
1649
|
+
if (!content.includes("authHandle") || content.includes("evlogAuthHandle")) return content;
|
|
1650
|
+
let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
|
|
1651
|
+
if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
|
|
1652
|
+
if (usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" && !nextContent.includes(`@${config.projectName}/env/server`)) nextContent = prependMissingImports(nextContent, [`import { env as localEnv } from "@${config.projectName}/env/server";`]);
|
|
1653
|
+
const authExpression = getAuthExpression(config);
|
|
1654
|
+
const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
|
|
1655
|
+
const authHandleSnippet = usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" ? `const evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tif (building) {\n\t\treturn resolve(event);\n\t}\n\n\tconst authEnv = event.platform?.env ?? localEnv;\n\tconst identifyUser = createAuthMiddleware(createAuth(authEnv) as BetterAuthInstance, ${authOptions});\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n` : `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\nconst evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n`;
|
|
1656
|
+
nextContent = insertAfterOnce(nextContent, "const { handle: evlogHandle, handleError } = createEvlogHooks();\n\n", authHandleSnippet, "evlogAuthHandle");
|
|
1657
|
+
return nextContent.replace("sequence(evlogHandle as Handle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)").replace("sequence(evlogHandle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)");
|
|
1658
|
+
}
|
|
1659
|
+
function addAstroBetterAuthEvlogSetup(content, config) {
|
|
1660
|
+
if (content.includes("createAuthMiddleware(")) return content;
|
|
1661
|
+
let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
|
|
1662
|
+
if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
|
|
1663
|
+
const authExpression = getAuthExpression(config);
|
|
1664
|
+
const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
|
|
1665
|
+
const usesFactory = usesCreateAuthFactory(config);
|
|
1666
|
+
if (!usesFactory) nextContent = insertBeforeOnce(nextContent, "export const onRequest", `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\n`, "const identifyUser = createAuthMiddleware(");
|
|
1667
|
+
for (const marker of ["context.locals.log = log;", "locals.log = log;"]) {
|
|
1668
|
+
if (!nextContent.includes(marker)) continue;
|
|
1669
|
+
const requestExpression = marker.startsWith("context") ? "context.request" : "request";
|
|
1670
|
+
const identifySnippet = usesFactory ? `\n\n const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n await identifyUser(log, ${requestExpression}.headers, url.pathname);` : `\n\n await identifyUser(log, ${requestExpression}.headers, url.pathname);`;
|
|
1671
|
+
return insertAfterOnce(nextContent, marker, identifySnippet, "identifyUser(log");
|
|
1672
|
+
}
|
|
1673
|
+
return nextContent;
|
|
1674
|
+
}
|
|
1675
|
+
function getNextEvlogFile(serviceName) {
|
|
1676
|
+
return `import { createEvlog } from "evlog/next";
|
|
1677
|
+
import { createInstrumentation } from "evlog/next/instrumentation";
|
|
1678
|
+
|
|
1679
|
+
export const { withEvlog, useLogger, log, createError } = createEvlog({
|
|
1680
|
+
service: "${serviceName}",
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
export const { register, onRequestError } = createInstrumentation({
|
|
1684
|
+
service: "${serviceName}",
|
|
1685
|
+
});
|
|
1686
|
+
`;
|
|
1687
|
+
}
|
|
1688
|
+
function getNextInstrumentationFile() {
|
|
1689
|
+
return `import { defineNodeInstrumentation } from "evlog/next/instrumentation";
|
|
1690
|
+
|
|
1691
|
+
export const { register, onRequestError } = defineNodeInstrumentation(() => import("./src/lib/evlog"));
|
|
1692
|
+
`;
|
|
1693
|
+
}
|
|
1694
|
+
function getNextProxyFile() {
|
|
1695
|
+
return `import { evlogMiddleware } from "evlog/next";
|
|
1696
|
+
|
|
1697
|
+
export const proxy = evlogMiddleware();
|
|
1698
|
+
|
|
1699
|
+
export const config = {
|
|
1700
|
+
matcher: ["/api/:path*"],
|
|
1701
|
+
};
|
|
1702
|
+
`;
|
|
1703
|
+
}
|
|
1704
|
+
function getNextEvlogAuthFile(config) {
|
|
1705
|
+
if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
|
|
1706
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1707
|
+
import { useLogger } from "@/lib/evlog";
|
|
1708
|
+
|
|
1709
|
+
export async function identifyEvlogUser(request: Request) {
|
|
1710
|
+
const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1711
|
+
exclude: ["/api/auth/**"],
|
|
1712
|
+
maskEmail: true,
|
|
1713
|
+
});
|
|
1714
|
+
await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
|
|
1715
|
+
}
|
|
1716
|
+
`;
|
|
1717
|
+
return `${getAuthImportLine(config)}
|
|
1718
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1719
|
+
import { useLogger } from "@/lib/evlog";
|
|
1720
|
+
|
|
1721
|
+
const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1722
|
+
exclude: ["/api/auth/**"],
|
|
1723
|
+
maskEmail: true,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
export async function identifyEvlogUser(request: Request) {
|
|
1727
|
+
await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
|
|
1728
|
+
}
|
|
1729
|
+
`;
|
|
1730
|
+
}
|
|
1731
|
+
function getNitroEvlogAuthPluginFile(config) {
|
|
1732
|
+
if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
|
|
1733
|
+
import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
|
|
1734
|
+
|
|
1735
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
1736
|
+
nitroApp.hooks.hook("request", async (event) => {
|
|
1737
|
+
const identify = createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1738
|
+
exclude: ["/api/auth/**"],
|
|
1739
|
+
maskEmail: true,
|
|
1740
|
+
});
|
|
1741
|
+
await identify(event);
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
`;
|
|
1745
|
+
return `${getAuthImportLine(config)}
|
|
1746
|
+
import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
|
|
1747
|
+
|
|
1748
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
1749
|
+
nitroApp.hooks.hook(
|
|
1750
|
+
"request",
|
|
1751
|
+
createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1752
|
+
exclude: ["/api/auth/**"],
|
|
1753
|
+
maskEmail: true,
|
|
1754
|
+
}),
|
|
1755
|
+
);
|
|
1756
|
+
});
|
|
1757
|
+
`;
|
|
1758
|
+
}
|
|
1759
|
+
function getNuxtEvlogAuthMiddlewareFile(config) {
|
|
1760
|
+
if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
|
|
1761
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1762
|
+
|
|
1763
|
+
export default defineEventHandler(async (event) => {
|
|
1764
|
+
if (!event.context.log) return;
|
|
1765
|
+
const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1766
|
+
exclude: ["/api/auth/**"],
|
|
1767
|
+
maskEmail: true,
|
|
1768
|
+
});
|
|
1769
|
+
await identify(event.context.log, event.headers, event.path);
|
|
1770
|
+
});
|
|
1771
|
+
`;
|
|
1772
|
+
return `${getAuthImportLine(config)}
|
|
1773
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1774
|
+
|
|
1775
|
+
const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1776
|
+
exclude: ["/api/auth/**"],
|
|
1777
|
+
maskEmail: true,
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
export default defineEventHandler(async (event) => {
|
|
1781
|
+
if (!event.context.log) return;
|
|
1782
|
+
await identify(event.context.log, event.headers, event.path);
|
|
1783
|
+
});
|
|
1784
|
+
`;
|
|
1785
|
+
}
|
|
1786
|
+
function getTanstackNitroConfigFile(serviceName) {
|
|
1787
|
+
return `import { defineConfig } from "nitro";
|
|
1788
|
+
import evlog from "evlog/nitro/v3";
|
|
1789
|
+
|
|
1790
|
+
export default defineConfig({
|
|
1791
|
+
experimental: {
|
|
1792
|
+
asyncContext: true,
|
|
1793
|
+
},
|
|
1794
|
+
modules: [
|
|
1795
|
+
evlog({
|
|
1796
|
+
env: { service: "${serviceName}" },
|
|
1797
|
+
}),
|
|
1798
|
+
],
|
|
1799
|
+
});
|
|
1800
|
+
`;
|
|
1801
|
+
}
|
|
1802
|
+
function getAstroMiddlewareFile(serviceName) {
|
|
1803
|
+
return `import { defineMiddleware } from "astro:middleware";
|
|
1804
|
+
import { createRequestLogger, initLogger } from "evlog";
|
|
1805
|
+
|
|
1806
|
+
initLogger({
|
|
1807
|
+
env: { service: "${serviceName}" },
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
|
|
1811
|
+
const url = new URL(request.url);
|
|
1812
|
+
const log = createRequestLogger({
|
|
1813
|
+
method: request.method,
|
|
1814
|
+
path: url.pathname,
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
locals.log = log;
|
|
1818
|
+
|
|
1819
|
+
try {
|
|
1820
|
+
const response = await next();
|
|
1821
|
+
log.emit();
|
|
1822
|
+
return response;
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
log.error(error instanceof Error ? error : new Error(String(error)));
|
|
1825
|
+
log.emit();
|
|
1826
|
+
throw error;
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
`;
|
|
1830
|
+
}
|
|
1831
|
+
function getAstroEnvFile() {
|
|
1832
|
+
return `/// <reference types="astro/client" />
|
|
1833
|
+
|
|
1834
|
+
import type { RequestLogger } from "evlog";
|
|
1835
|
+
|
|
1836
|
+
declare namespace App {
|
|
1837
|
+
interface Locals {
|
|
1838
|
+
log: RequestLogger;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
`;
|
|
1842
|
+
}
|
|
1843
|
+
async function setupNextEvlog(config, serviceName) {
|
|
1844
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1845
|
+
const evlogPath = path.join(webDir, "src/lib/evlog.ts");
|
|
1846
|
+
if (!await fs.pathExists(evlogPath)) await writeFileIfChanged(evlogPath, getNextEvlogFile(serviceName));
|
|
1847
|
+
const identifyWebAuth = shouldIdentifyWebAuth(config);
|
|
1848
|
+
if (identifyWebAuth) {
|
|
1849
|
+
const evlogAuthPath = path.join(webDir, "src/lib/evlog-auth.ts");
|
|
1850
|
+
if (!await fs.pathExists(evlogAuthPath)) await writeFileIfChanged(evlogAuthPath, getNextEvlogAuthFile(config));
|
|
1851
|
+
}
|
|
1852
|
+
const instrumentationPath = path.join(webDir, "instrumentation.ts");
|
|
1853
|
+
if (!await fs.pathExists(instrumentationPath)) await writeFileIfChanged(instrumentationPath, getNextInstrumentationFile());
|
|
1854
|
+
const proxyPath = path.join(webDir, "src/proxy.ts");
|
|
1855
|
+
const rootProxyPath = path.join(webDir, "proxy.ts");
|
|
1856
|
+
if (!await fs.pathExists(proxyPath) && !await fs.pathExists(rootProxyPath)) await writeFileIfChanged(proxyPath, getNextProxyFile());
|
|
1857
|
+
const updateNextApiRoute = (content) => {
|
|
1858
|
+
let nextContent = addNextRouteWrappers(content);
|
|
1859
|
+
if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
|
|
1860
|
+
return nextContent;
|
|
1861
|
+
};
|
|
1862
|
+
await updateFileIfExists(path.join(webDir, "src/app/api/trpc/[trpc]/route.ts"), updateNextApiRoute);
|
|
1863
|
+
await updateFileIfExists(path.join(webDir, "src/app/api/rpc/[[...rest]]/route.ts"), updateNextApiRoute);
|
|
1864
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/app/api/ai/route.ts"), (content) => {
|
|
1865
|
+
let nextContent = addNextAiEvlogSetup(content);
|
|
1866
|
+
if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
|
|
1867
|
+
return nextContent;
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
async function setupNuxtEvlog(config, serviceName) {
|
|
1871
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1872
|
+
await updateFileIfExists(path.join(webDir, "nuxt.config.ts"), (content) => addNuxtEvlogSetup(content, serviceName));
|
|
1873
|
+
if (shouldIdentifyWebAuth(config)) {
|
|
1874
|
+
const oldAuthPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
|
|
1875
|
+
if (await fs.pathExists(oldAuthPluginPath)) {
|
|
1876
|
+
if ((await fs.readFile(oldAuthPluginPath, "utf-8")).includes("evlog/better-auth")) await fs.remove(oldAuthPluginPath);
|
|
1877
|
+
}
|
|
1878
|
+
const authMiddlewarePath = path.join(webDir, "server/middleware/evlog-auth.ts");
|
|
1879
|
+
if (!await fs.pathExists(authMiddlewarePath)) await writeFileIfChanged(authMiddlewarePath, getNuxtEvlogAuthMiddlewareFile(config));
|
|
1880
|
+
}
|
|
1881
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "server/api/ai.post.ts"), addNuxtAiEvlogSetup);
|
|
1882
|
+
}
|
|
1883
|
+
async function setupSvelteEvlog(config, serviceName) {
|
|
1884
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1885
|
+
await updateFileIfExists(path.join(webDir, "vite.config.ts"), (content) => addSvelteViteEvlogSetup(content, serviceName));
|
|
1886
|
+
const hooksPath = path.join(webDir, "src/hooks.server.ts");
|
|
1887
|
+
if (await fs.pathExists(hooksPath)) await updateFileIfExists(hooksPath, addSvelteHooksEvlogSetup);
|
|
1888
|
+
else await writeFileIfChanged(hooksPath, `import { createEvlogHooks } from "evlog/sveltekit";
|
|
1889
|
+
|
|
1890
|
+
export const { handle, handleError } = createEvlogHooks();
|
|
1891
|
+
`);
|
|
1892
|
+
await updateFileIfExists(path.join(webDir, "src/app.d.ts"), addSvelteLocalsType);
|
|
1893
|
+
if (shouldIdentifyWebAuth(config)) await updateFileIfExists(path.join(webDir, "src/hooks.server.ts"), (content) => addSvelteBetterAuthEvlogSetup(content, config));
|
|
1894
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/+server.ts"), addSvelteAiEvlogSetup);
|
|
1895
|
+
}
|
|
1896
|
+
async function setupTanstackStartEvlog(config, serviceName) {
|
|
1897
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1898
|
+
const nitroConfigPath = path.join(webDir, "nitro.config.ts");
|
|
1899
|
+
if (!await fs.pathExists(nitroConfigPath)) await writeFileIfChanged(nitroConfigPath, getTanstackNitroConfigFile(serviceName));
|
|
1900
|
+
await updateFileIfExists(path.join(webDir, "src/routes/__root.tsx"), addTanstackStartRootEvlogSetup);
|
|
1901
|
+
if (shouldIdentifyWebAuth(config)) {
|
|
1902
|
+
const authPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
|
|
1903
|
+
if (!await fs.pathExists(authPluginPath)) await writeFileIfChanged(authPluginPath, getNitroEvlogAuthPluginFile(config));
|
|
1904
|
+
}
|
|
1905
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/$.ts"), addTanstackStartAiEvlogSetup);
|
|
1906
|
+
}
|
|
1907
|
+
async function setupAstroEvlog(config, serviceName) {
|
|
1908
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1909
|
+
const middlewarePath = path.join(webDir, "src/middleware.ts");
|
|
1910
|
+
if (!await fs.pathExists(middlewarePath)) await writeFileIfChanged(middlewarePath, getAstroMiddlewareFile(serviceName));
|
|
1911
|
+
else await updateFileIfExists(middlewarePath, (content) => addAstroMiddlewareEvlogSetup(content, serviceName));
|
|
1912
|
+
const envPath = path.join(webDir, "src/env.d.ts");
|
|
1913
|
+
if (!await fs.pathExists(envPath)) await writeFileIfChanged(envPath, getAstroEnvFile());
|
|
1914
|
+
else await updateFileIfExists(envPath, addAstroLocalsType);
|
|
1915
|
+
if (shouldIdentifyWebAuth(config)) await updateFileIfExists(middlewarePath, (content) => addAstroBetterAuthEvlogSetup(content, config));
|
|
1916
|
+
}
|
|
1917
|
+
async function setupEvlogWeb(config) {
|
|
1918
|
+
const frontend = getEvlogWebFrontend(config.frontend);
|
|
1919
|
+
if (!frontend) return;
|
|
1920
|
+
const serviceName = `${config.projectName}-web`;
|
|
1921
|
+
if (frontend === "next") await setupNextEvlog(config, serviceName);
|
|
1922
|
+
else if (frontend === "nuxt") await setupNuxtEvlog(config, serviceName);
|
|
1923
|
+
else if (frontend === "svelte") await setupSvelteEvlog(config, serviceName);
|
|
1924
|
+
else if (frontend === "tanstack-start") await setupTanstackStartEvlog(config, serviceName);
|
|
1925
|
+
else if (frontend === "astro") await setupAstroEvlog(config, serviceName);
|
|
1926
|
+
}
|
|
1927
|
+
async function setupEvlog(config) {
|
|
1928
|
+
return Result.tryPromise({
|
|
1929
|
+
try: async () => {
|
|
1930
|
+
if (isEvlogBackend(config.backend)) {
|
|
1931
|
+
const serverIndexPath = path.join(config.projectDir, "apps/server/src/index.ts");
|
|
1932
|
+
if (await fs.pathExists(serverIndexPath)) {
|
|
1933
|
+
const content = await fs.readFile(serverIndexPath, "utf-8");
|
|
1934
|
+
let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`);
|
|
1935
|
+
if (config.auth === "better-auth") nextContent = addEvlogBetterAuthServerSetup(nextContent, config.backend, getAuthExpression(config));
|
|
1936
|
+
if (config.examples.includes("ai")) nextContent = addBackendAiEvlogSetup(nextContent, config.backend);
|
|
1937
|
+
if (nextContent !== content) await fs.writeFile(serverIndexPath, nextContent);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
await setupEvlogWeb(config);
|
|
1941
|
+
},
|
|
1942
|
+
catch: (error) => new AddonSetupError({
|
|
1943
|
+
addon: "evlog",
|
|
1944
|
+
message: `Failed to set up evlog: ${error instanceof Error ? error.message : String(error)}`,
|
|
1945
|
+
cause: error
|
|
1946
|
+
})
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
//#endregion
|
|
1950
|
+
//#region src/prompts/navigable-group.ts
|
|
1951
|
+
/**
|
|
1952
|
+
* Navigable group - a group of prompts that allows going back
|
|
1953
|
+
*/
|
|
1954
|
+
/**
|
|
1955
|
+
* Define a group of prompts that supports going back to previous prompts.
|
|
1956
|
+
* Returns a result object with all the values, or handles cancel/go-back navigation.
|
|
1957
|
+
*/
|
|
1958
|
+
async function navigableGroup(prompts, opts) {
|
|
1959
|
+
const results = {};
|
|
1960
|
+
const promptNames = Object.keys(prompts);
|
|
1961
|
+
let currentIndex = 0;
|
|
1962
|
+
let goingBack = false;
|
|
1963
|
+
while (currentIndex < promptNames.length) {
|
|
1964
|
+
const name = promptNames[currentIndex];
|
|
1965
|
+
const prompt = prompts[name];
|
|
1966
|
+
setIsFirstPrompt$1(currentIndex === 0);
|
|
1967
|
+
setLastPromptShownUI(false);
|
|
1968
|
+
const result = await prompt({ results })?.catch((e) => {
|
|
1969
|
+
throw e;
|
|
1970
|
+
});
|
|
1971
|
+
if (isGoBack(result)) {
|
|
1972
|
+
goingBack = true;
|
|
1973
|
+
if (currentIndex > 0) {
|
|
1974
|
+
const prevName = promptNames[currentIndex - 1];
|
|
1975
|
+
delete results[prevName];
|
|
1976
|
+
currentIndex--;
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
goingBack = false;
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
if (isCancel$1(result)) {
|
|
1983
|
+
if (typeof opts?.onCancel === "function") {
|
|
1984
|
+
results[name] = "canceled";
|
|
1985
|
+
opts.onCancel({ results });
|
|
1986
|
+
}
|
|
1987
|
+
setIsFirstPrompt$1(false);
|
|
1988
|
+
return results;
|
|
1989
|
+
}
|
|
1990
|
+
if (goingBack && !didLastPromptShowUI()) {
|
|
1991
|
+
if (currentIndex > 0) {
|
|
1992
|
+
const prevName = promptNames[currentIndex - 1];
|
|
1993
|
+
delete results[prevName];
|
|
1994
|
+
currentIndex--;
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
goingBack = false;
|
|
1999
|
+
results[name] = result;
|
|
2000
|
+
currentIndex++;
|
|
2001
|
+
}
|
|
2002
|
+
setIsFirstPrompt$1(false);
|
|
2003
|
+
return results;
|
|
2004
|
+
}
|
|
2005
|
+
//#endregion
|
|
1384
2006
|
//#region src/utils/external-commands.ts
|
|
1385
2007
|
function shouldSkipExternalCommands() {
|
|
1386
2008
|
return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
|
|
@@ -1484,64 +2106,143 @@ function getPackageRunnerPrefix(packageManager) {
|
|
|
1484
2106
|
const TEMPLATES$2 = {
|
|
1485
2107
|
"next-mdx": {
|
|
1486
2108
|
label: "Next.js: Fumadocs MDX",
|
|
1487
|
-
hint: "
|
|
2109
|
+
hint: "recommended",
|
|
1488
2110
|
value: "+next+fuma-docs-mdx"
|
|
1489
2111
|
},
|
|
1490
2112
|
"next-mdx-static": {
|
|
1491
|
-
label: "Next.js: Fumadocs MDX
|
|
1492
|
-
hint: "Static export template with MDX support",
|
|
2113
|
+
label: "Next.js Static: Fumadocs MDX",
|
|
1493
2114
|
value: "+next+fuma-docs-mdx+static"
|
|
1494
2115
|
},
|
|
1495
2116
|
waku: {
|
|
1496
|
-
label: "Waku:
|
|
1497
|
-
hint: "Template using Waku with content collections",
|
|
2117
|
+
label: "Waku: Fumadocs MDX",
|
|
1498
2118
|
value: "waku"
|
|
1499
2119
|
},
|
|
1500
2120
|
"react-router": {
|
|
1501
|
-
label: "React Router: MDX
|
|
1502
|
-
hint: "Template for React Router with MDX remote",
|
|
2121
|
+
label: "React Router: Fumadocs MDX (not RSC)",
|
|
1503
2122
|
value: "react-router"
|
|
1504
2123
|
},
|
|
1505
2124
|
"react-router-spa": {
|
|
1506
|
-
label: "React Router:
|
|
1507
|
-
hint: "
|
|
2125
|
+
label: "React Router SPA: Fumadocs MDX (not RSC)",
|
|
2126
|
+
hint: "SPA mode allows you to host the site statically, compatible with a CDN.",
|
|
1508
2127
|
value: "react-router-spa"
|
|
1509
2128
|
},
|
|
1510
2129
|
"tanstack-start": {
|
|
1511
|
-
label: "Tanstack Start: MDX
|
|
1512
|
-
hint: "Template for Tanstack Start with MDX remote",
|
|
2130
|
+
label: "Tanstack Start: Fumadocs MDX (not RSC)",
|
|
1513
2131
|
value: "tanstack-start"
|
|
1514
2132
|
},
|
|
1515
2133
|
"tanstack-start-spa": {
|
|
1516
|
-
label: "Tanstack Start:
|
|
1517
|
-
hint: "
|
|
2134
|
+
label: "Tanstack Start SPA: Fumadocs MDX (not RSC)",
|
|
2135
|
+
hint: "SPA mode allows you to host the site statically, compatible with a CDN.",
|
|
1518
2136
|
value: "tanstack-start-spa"
|
|
1519
2137
|
}
|
|
1520
2138
|
};
|
|
1521
2139
|
const DEFAULT_TEMPLATE$2 = "next-mdx";
|
|
1522
2140
|
const DEFAULT_DEV_PORT$1 = 4e3;
|
|
2141
|
+
function aiChatDisabledForTemplate(template) {
|
|
2142
|
+
return template === "next-mdx-static" || template.endsWith("-spa");
|
|
2143
|
+
}
|
|
1523
2144
|
async function setupFumadocs(config) {
|
|
1524
2145
|
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1525
2146
|
const { packageManager, projectDir } = config;
|
|
1526
2147
|
cliLog.info("Setting up Fumadocs...");
|
|
1527
2148
|
const configuredOptions = config.addonOptions?.fumadocs;
|
|
1528
2149
|
let template = configuredOptions?.template;
|
|
1529
|
-
|
|
2150
|
+
let search = configuredOptions?.search;
|
|
2151
|
+
let ogImage = configuredOptions?.ogImage;
|
|
2152
|
+
let aiChat = configuredOptions?.aiChat;
|
|
2153
|
+
if (isSilent()) template = template ?? DEFAULT_TEMPLATE$2;
|
|
1530
2154
|
else {
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2155
|
+
const promptResult = await Result.tryPromise({
|
|
2156
|
+
try: () => navigableGroup({
|
|
2157
|
+
template: async () => {
|
|
2158
|
+
if (template !== void 0) return template;
|
|
2159
|
+
return navigableSelect({
|
|
2160
|
+
message: "Choose a template",
|
|
2161
|
+
options: Object.entries(TEMPLATES$2).map(([key, t]) => ({
|
|
2162
|
+
value: key,
|
|
2163
|
+
label: t.label,
|
|
2164
|
+
hint: "hint" in t ? t.hint : void 0
|
|
2165
|
+
})),
|
|
2166
|
+
initialValue: DEFAULT_TEMPLATE$2
|
|
2167
|
+
});
|
|
2168
|
+
},
|
|
2169
|
+
search: async () => {
|
|
2170
|
+
if (search !== void 0) return search;
|
|
2171
|
+
return navigableSelect({
|
|
2172
|
+
message: "Choose a search solution?",
|
|
2173
|
+
options: [{
|
|
2174
|
+
value: "orama",
|
|
2175
|
+
label: "Default",
|
|
2176
|
+
hint: "local search powered by Orama, recommended"
|
|
2177
|
+
}, {
|
|
2178
|
+
value: "orama-cloud",
|
|
2179
|
+
label: "Orama Cloud",
|
|
2180
|
+
hint: "3rd party search solution, signup needed"
|
|
2181
|
+
}],
|
|
2182
|
+
initialValue: "orama"
|
|
2183
|
+
});
|
|
2184
|
+
},
|
|
2185
|
+
ogImage: async ({ results }) => {
|
|
2186
|
+
if (ogImage !== void 0) return ogImage;
|
|
2187
|
+
if (!(results.template ?? template ?? DEFAULT_TEMPLATE$2).startsWith("next-")) return "skip";
|
|
2188
|
+
return navigableSelect({
|
|
2189
|
+
message: "Configure Open Graph Image generation?",
|
|
2190
|
+
options: [{
|
|
2191
|
+
value: "next-og",
|
|
2192
|
+
label: "next/og",
|
|
2193
|
+
hint: "Next.js built-in solution"
|
|
2194
|
+
}, {
|
|
2195
|
+
value: "takumi",
|
|
2196
|
+
label: "Takumi",
|
|
2197
|
+
hint: "Output WebP format, framework-agnostic"
|
|
2198
|
+
}],
|
|
2199
|
+
initialValue: "next-og"
|
|
2200
|
+
});
|
|
2201
|
+
},
|
|
2202
|
+
aiChat: async ({ results }) => {
|
|
2203
|
+
if (aiChat !== void 0) return aiChat;
|
|
2204
|
+
if (aiChatDisabledForTemplate(results.template ?? template ?? DEFAULT_TEMPLATE$2)) return "none";
|
|
2205
|
+
return navigableSelect({
|
|
2206
|
+
message: "Configure AI Chat?",
|
|
2207
|
+
options: [
|
|
2208
|
+
{
|
|
2209
|
+
value: "none",
|
|
2210
|
+
label: "No"
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
value: "openrouter",
|
|
2214
|
+
label: "AI SDK",
|
|
2215
|
+
hint: "default to OpenRouter"
|
|
2216
|
+
},
|
|
2217
|
+
{
|
|
2218
|
+
value: "inkeep",
|
|
2219
|
+
label: "Inkeep AI",
|
|
2220
|
+
hint: "API key required"
|
|
2221
|
+
}
|
|
2222
|
+
],
|
|
2223
|
+
initialValue: "none"
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
}),
|
|
2227
|
+
catch: (e) => new AddonSetupError({
|
|
2228
|
+
addon: "fumadocs",
|
|
2229
|
+
message: `Failed to run Fumadocs prompts: ${e instanceof Error ? e.message : String(e)}`,
|
|
2230
|
+
cause: e
|
|
2231
|
+
})
|
|
1539
2232
|
});
|
|
1540
|
-
if (
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2233
|
+
if (promptResult.isErr()) return Result.err(promptResult.error);
|
|
2234
|
+
const results = promptResult.value;
|
|
2235
|
+
if (results.template === void 0 || results.search === void 0 || results.ogImage === void 0 || results.aiChat === void 0) return userCancelled("Operation cancelled");
|
|
2236
|
+
template = results.template;
|
|
2237
|
+
search = results.search;
|
|
2238
|
+
ogImage = results.ogImage === "skip" ? void 0 : results.ogImage;
|
|
2239
|
+
aiChat = results.aiChat === "none" ? void 0 : results.aiChat;
|
|
2240
|
+
}
|
|
2241
|
+
if (!template) return userCancelled("Operation cancelled");
|
|
1544
2242
|
const isNextTemplate = template.startsWith("next-");
|
|
2243
|
+
if (!isNextTemplate) ogImage = void 0;
|
|
2244
|
+
if (aiChatDisabledForTemplate(template)) aiChat = void 0;
|
|
2245
|
+
const templateArg = TEMPLATES$2[template].value;
|
|
1545
2246
|
const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT$1;
|
|
1546
2247
|
const options = [
|
|
1547
2248
|
`--template ${templateArg}`,
|
|
@@ -1549,7 +2250,10 @@ async function setupFumadocs(config) {
|
|
|
1549
2250
|
"--no-git"
|
|
1550
2251
|
];
|
|
1551
2252
|
if (isNextTemplate) options.push("--src");
|
|
1552
|
-
if (config.addons.includes("biome")) options.push("--linter biome");
|
|
2253
|
+
if (config.addons.includes("biome") || config.addons.includes("ultracite")) options.push("--linter biome");
|
|
2254
|
+
if (search) options.push(`--search ${search}`);
|
|
2255
|
+
if (ogImage) options.push(`--og-image ${ogImage}`);
|
|
2256
|
+
if (aiChat) options.push(`--ai-chat ${aiChat}`);
|
|
1553
2257
|
const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs ${options.join(" ")}`);
|
|
1554
2258
|
const appsDir = path.join(projectDir, "apps");
|
|
1555
2259
|
await fs.ensureDir(appsDir);
|
|
@@ -1812,67 +2516,84 @@ async function setupMcp(config) {
|
|
|
1812
2516
|
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
1813
2517
|
const { packageManager, projectDir } = config;
|
|
1814
2518
|
cliLog.info("Setting up MCP servers...");
|
|
1815
|
-
|
|
1816
|
-
if (!scope) if (isSilent()) scope = DEFAULT_SCOPE$1;
|
|
1817
|
-
else {
|
|
1818
|
-
const selectedScope = await select({
|
|
1819
|
-
message: "Where should MCP servers be installed?",
|
|
1820
|
-
options: [{
|
|
1821
|
-
value: "project",
|
|
1822
|
-
label: "Project",
|
|
1823
|
-
hint: "Writes to project config files (recommended for teams)"
|
|
1824
|
-
}, {
|
|
1825
|
-
value: "global",
|
|
1826
|
-
label: "Global",
|
|
1827
|
-
hint: "Writes to user-level config files (personal machine)"
|
|
1828
|
-
}],
|
|
1829
|
-
initialValue: DEFAULT_SCOPE$1
|
|
1830
|
-
});
|
|
1831
|
-
if (isCancel(selectedScope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
1832
|
-
scope = selectedScope;
|
|
1833
|
-
}
|
|
1834
|
-
const recommendedServers = getRecommendedMcpServers(config, scope);
|
|
1835
|
-
if (recommendedServers.length === 0) return Result.ok(void 0);
|
|
1836
|
-
const allServersByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
|
|
1837
|
-
const serverOptions = recommendedServers.map((s) => ({
|
|
1838
|
-
value: s.key,
|
|
1839
|
-
label: s.label,
|
|
1840
|
-
hint: s.target
|
|
1841
|
-
}));
|
|
2519
|
+
const configuredScope = config.addonOptions?.mcp?.scope;
|
|
1842
2520
|
const configuredServerKeys = config.addonOptions?.mcp?.servers;
|
|
1843
|
-
const availableServerKeys = new Set(allServersByKey.keys());
|
|
1844
|
-
let selectedServerKeys = configuredServerKeys?.filter((serverKey) => availableServerKeys.has(serverKey)) ?? [];
|
|
1845
|
-
if (selectedServerKeys.length === 0 && configuredServerKeys === void 0) if (isSilent()) selectedServerKeys = serverOptions.map((o) => o.value);
|
|
1846
|
-
else {
|
|
1847
|
-
const promptedServerKeys = await multiselect({
|
|
1848
|
-
message: "Select MCP servers to install",
|
|
1849
|
-
options: serverOptions,
|
|
1850
|
-
required: false,
|
|
1851
|
-
initialValues: serverOptions.map((o) => o.value)
|
|
1852
|
-
});
|
|
1853
|
-
if (isCancel(promptedServerKeys)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
1854
|
-
selectedServerKeys = [...promptedServerKeys];
|
|
1855
|
-
}
|
|
1856
|
-
if (selectedServerKeys.length === 0) return Result.ok(void 0);
|
|
1857
|
-
const agentOptions = filterAgentsForScope(scope).map((a) => ({
|
|
1858
|
-
value: a.value,
|
|
1859
|
-
label: a.label
|
|
1860
|
-
}));
|
|
1861
|
-
const defaultAgents = uniqueValues$1(DEFAULT_AGENTS$2.filter((agent) => agentOptions.some((option) => option.value === agent)));
|
|
1862
2521
|
const configuredAgents = config.addonOptions?.mcp?.agents;
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2522
|
+
const allServersByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
|
|
2523
|
+
const availableServerKeys = new Set(allServersByKey.keys());
|
|
2524
|
+
let scope;
|
|
2525
|
+
let selectedServerKeys;
|
|
2526
|
+
let selectedAgents;
|
|
2527
|
+
if (isSilent()) {
|
|
2528
|
+
scope = configuredScope ?? DEFAULT_SCOPE$1;
|
|
2529
|
+
const recommendedServers = getRecommendedMcpServers(config, scope);
|
|
2530
|
+
if (recommendedServers.length === 0) return Result.ok(void 0);
|
|
2531
|
+
const serverOptions = recommendedServers.map((s) => s.key);
|
|
2532
|
+
selectedServerKeys = configuredServerKeys?.filter((k) => availableServerKeys.has(k)) ?? serverOptions;
|
|
2533
|
+
if (selectedServerKeys.length === 0) return Result.ok(void 0);
|
|
2534
|
+
const agentOptions = filterAgentsForScope(scope);
|
|
2535
|
+
const defaultAgents = uniqueValues$1(DEFAULT_AGENTS$2.filter((a) => agentOptions.some((o) => o.value === a)));
|
|
2536
|
+
selectedAgents = configuredAgents?.filter((a) => agentOptions.some((o) => o.value === a)) ?? defaultAgents;
|
|
2537
|
+
if (selectedAgents.length === 0) return Result.ok(void 0);
|
|
2538
|
+
} else {
|
|
2539
|
+
const results = await navigableGroup({
|
|
2540
|
+
scope: async () => {
|
|
2541
|
+
if (configuredScope !== void 0) return configuredScope;
|
|
2542
|
+
return navigableSelect({
|
|
2543
|
+
message: "Where should MCP servers be installed?",
|
|
2544
|
+
options: [{
|
|
2545
|
+
value: "project",
|
|
2546
|
+
label: "Project",
|
|
2547
|
+
hint: "Writes to project config files (recommended for teams)"
|
|
2548
|
+
}, {
|
|
2549
|
+
value: "global",
|
|
2550
|
+
label: "Global",
|
|
2551
|
+
hint: "Writes to user-level config files (personal machine)"
|
|
2552
|
+
}],
|
|
2553
|
+
initialValue: DEFAULT_SCOPE$1
|
|
2554
|
+
});
|
|
2555
|
+
},
|
|
2556
|
+
servers: async ({ results: r }) => {
|
|
2557
|
+
const recommended = getRecommendedMcpServers(config, r.scope ?? configuredScope ?? DEFAULT_SCOPE$1);
|
|
2558
|
+
if (recommended.length === 0) return [];
|
|
2559
|
+
const options = recommended.map((s) => ({
|
|
2560
|
+
value: s.key,
|
|
2561
|
+
label: s.label,
|
|
2562
|
+
hint: s.target
|
|
2563
|
+
}));
|
|
2564
|
+
if (configuredServerKeys !== void 0) return configuredServerKeys.filter((k) => availableServerKeys.has(k));
|
|
2565
|
+
return navigableMultiselect({
|
|
2566
|
+
message: "Select MCP servers to install",
|
|
2567
|
+
options,
|
|
2568
|
+
required: false,
|
|
2569
|
+
initialValues: options.map((o) => o.value)
|
|
2570
|
+
});
|
|
2571
|
+
},
|
|
2572
|
+
agents: async ({ results: r }) => {
|
|
2573
|
+
const currentScope = r.scope ?? configuredScope ?? DEFAULT_SCOPE$1;
|
|
2574
|
+
const currentServers = r.servers;
|
|
2575
|
+
if (currentServers !== void 0 && currentServers.length === 0) return [];
|
|
2576
|
+
const agentOpts = filterAgentsForScope(currentScope);
|
|
2577
|
+
if (agentOpts.length === 0) return [];
|
|
2578
|
+
const defaults = uniqueValues$1(DEFAULT_AGENTS$2.filter((a) => agentOpts.some((o) => o.value === a)));
|
|
2579
|
+
if (configuredAgents !== void 0) return configuredAgents.filter((a) => agentOpts.some((o) => o.value === a));
|
|
2580
|
+
return navigableMultiselect({
|
|
2581
|
+
message: "Select agents to install MCP servers to",
|
|
2582
|
+
options: agentOpts.map((a) => ({
|
|
2583
|
+
value: a.value,
|
|
2584
|
+
label: a.label
|
|
2585
|
+
})),
|
|
2586
|
+
required: false,
|
|
2587
|
+
initialValues: defaults
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
1871
2590
|
});
|
|
1872
|
-
if (
|
|
1873
|
-
|
|
2591
|
+
if (results.scope === void 0 || results.servers === void 0 || results.agents === void 0) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
2592
|
+
scope = results.scope;
|
|
2593
|
+
selectedServerKeys = results.servers;
|
|
2594
|
+
selectedAgents = results.agents;
|
|
2595
|
+
if (selectedServerKeys.length === 0 || selectedAgents.length === 0) return Result.ok(void 0);
|
|
1874
2596
|
}
|
|
1875
|
-
if (selectedAgents.length === 0) return Result.ok(void 0);
|
|
1876
2597
|
const selectedServers = [];
|
|
1877
2598
|
for (const key of selectedServerKeys) {
|
|
1878
2599
|
const server = allServersByKey.get(key);
|
|
@@ -1994,7 +2715,8 @@ const SKILL_SOURCES = {
|
|
|
1994
2715
|
"elysiajs/skills": { label: "ElysiaJS" },
|
|
1995
2716
|
"waynesutton/convexskills": { label: "Convex" },
|
|
1996
2717
|
"msmps/opentui-skill": { label: "OpenTUI Platform" },
|
|
1997
|
-
"haydenbleasel/ultracite": { label: "Ultracite" }
|
|
2718
|
+
"haydenbleasel/ultracite": { label: "Ultracite" },
|
|
2719
|
+
"https://www.evlog.dev": { label: "evlog" }
|
|
1998
2720
|
};
|
|
1999
2721
|
const AVAILABLE_AGENTS = [
|
|
2000
2722
|
{
|
|
@@ -2134,6 +2856,7 @@ function getRecommendedSourceKeys(config) {
|
|
|
2134
2856
|
if (backend === "convex") sources.push("waynesutton/convexskills");
|
|
2135
2857
|
if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
|
|
2136
2858
|
if (addons.includes("ultracite")) sources.push("haydenbleasel/ultracite");
|
|
2859
|
+
if (addons.includes("evlog")) sources.push("https://www.evlog.dev");
|
|
2137
2860
|
return sources;
|
|
2138
2861
|
}
|
|
2139
2862
|
const CURATED_SKILLS_BY_SOURCE = {
|
|
@@ -2180,7 +2903,6 @@ const CURATED_SKILLS_BY_SOURCE = {
|
|
|
2180
2903
|
"building-native-ui",
|
|
2181
2904
|
"native-data-fetching",
|
|
2182
2905
|
"expo-deployment",
|
|
2183
|
-
"upgrading-expo",
|
|
2184
2906
|
"expo-cicd-workflows"
|
|
2185
2907
|
];
|
|
2186
2908
|
if (config.frontend.includes("native-uniwind")) skills.push("expo-tailwind-setup");
|
|
@@ -2205,7 +2927,8 @@ const CURATED_SKILLS_BY_SOURCE = {
|
|
|
2205
2927
|
"convex-security-check"
|
|
2206
2928
|
],
|
|
2207
2929
|
"msmps/opentui-skill": () => ["opentui"],
|
|
2208
|
-
"haydenbleasel/ultracite": () => ["ultracite"]
|
|
2930
|
+
"haydenbleasel/ultracite": () => ["ultracite"],
|
|
2931
|
+
"https://www.evlog.dev": () => ["review-logging-patterns", "analyze-logs"]
|
|
2209
2932
|
};
|
|
2210
2933
|
function getCuratedSkillNamesForSourceKey(sourceKey, config) {
|
|
2211
2934
|
return CURATED_SKILLS_BY_SOURCE[sourceKey](config);
|
|
@@ -2236,55 +2959,64 @@ async function setupSkills(config) {
|
|
|
2236
2959
|
}));
|
|
2237
2960
|
});
|
|
2238
2961
|
if (skillOptions.length === 0) return Result.ok(void 0);
|
|
2239
|
-
|
|
2240
|
-
if (!scope) if (isSilent()) scope = DEFAULT_SCOPE;
|
|
2241
|
-
else {
|
|
2242
|
-
const selectedScope = await select({
|
|
2243
|
-
message: "Where should skills be installed?",
|
|
2244
|
-
options: [{
|
|
2245
|
-
value: "project",
|
|
2246
|
-
label: "Project",
|
|
2247
|
-
hint: "Writes to project config files (recommended for teams)"
|
|
2248
|
-
}, {
|
|
2249
|
-
value: "global",
|
|
2250
|
-
label: "Global",
|
|
2251
|
-
hint: "Writes to user-level config files (personal machine)"
|
|
2252
|
-
}],
|
|
2253
|
-
initialValue: DEFAULT_SCOPE
|
|
2254
|
-
});
|
|
2255
|
-
if (isCancel(selectedScope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
2256
|
-
scope = selectedScope;
|
|
2257
|
-
}
|
|
2258
|
-
const allSkillValues = skillOptions.map((opt) => opt.value);
|
|
2962
|
+
const configuredScope = skillsOptions?.scope;
|
|
2259
2963
|
const configuredSelections = skillsOptions?.selections;
|
|
2260
|
-
let selectedSkills;
|
|
2261
|
-
if (configuredSelections !== void 0) selectedSkills = configuredSelections.flatMap((selection) => selection.skills.map((skill) => `${selection.source}::${skill}`));
|
|
2262
|
-
else if (isSilent()) selectedSkills = allSkillValues;
|
|
2263
|
-
else {
|
|
2264
|
-
const promptedSkills = await multiselect({
|
|
2265
|
-
message: "Select skills to install",
|
|
2266
|
-
options: skillOptions,
|
|
2267
|
-
required: false,
|
|
2268
|
-
initialValues: allSkillValues
|
|
2269
|
-
});
|
|
2270
|
-
if (isCancel(promptedSkills)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
2271
|
-
selectedSkills = promptedSkills;
|
|
2272
|
-
}
|
|
2273
|
-
if (selectedSkills.length === 0) return Result.ok(void 0);
|
|
2274
2964
|
const configuredAgents = skillsOptions?.agents;
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2965
|
+
const allSkillValues = skillOptions.map((opt) => opt.value);
|
|
2966
|
+
let scope;
|
|
2967
|
+
let selectedSkills;
|
|
2968
|
+
let selectedAgents;
|
|
2969
|
+
if (isSilent()) {
|
|
2970
|
+
scope = configuredScope ?? DEFAULT_SCOPE;
|
|
2971
|
+
selectedSkills = configuredSelections !== void 0 ? configuredSelections.flatMap((selection) => selection.skills.map((skill) => `${selection.source}::${skill}`)) : allSkillValues;
|
|
2972
|
+
if (selectedSkills.length === 0) return Result.ok(void 0);
|
|
2973
|
+
selectedAgents = configuredAgents ? [...configuredAgents] : [...DEFAULT_AGENTS$1];
|
|
2974
|
+
if (selectedAgents.length === 0) return Result.ok(void 0);
|
|
2975
|
+
} else {
|
|
2976
|
+
const results = await navigableGroup({
|
|
2977
|
+
scope: async () => {
|
|
2978
|
+
if (configuredScope !== void 0) return configuredScope;
|
|
2979
|
+
return navigableSelect({
|
|
2980
|
+
message: "Where should skills be installed?",
|
|
2981
|
+
options: [{
|
|
2982
|
+
value: "project",
|
|
2983
|
+
label: "Project",
|
|
2984
|
+
hint: "Writes to project config files (recommended for teams)"
|
|
2985
|
+
}, {
|
|
2986
|
+
value: "global",
|
|
2987
|
+
label: "Global",
|
|
2988
|
+
hint: "Writes to user-level config files (personal machine)"
|
|
2989
|
+
}],
|
|
2990
|
+
initialValue: DEFAULT_SCOPE
|
|
2991
|
+
});
|
|
2992
|
+
},
|
|
2993
|
+
skills: async () => {
|
|
2994
|
+
if (configuredSelections !== void 0) return configuredSelections.flatMap((selection) => selection.skills.map((skill) => `${selection.source}::${skill}`));
|
|
2995
|
+
return navigableMultiselect({
|
|
2996
|
+
message: "Select skills to install",
|
|
2997
|
+
options: skillOptions,
|
|
2998
|
+
required: false,
|
|
2999
|
+
initialValues: allSkillValues
|
|
3000
|
+
});
|
|
3001
|
+
},
|
|
3002
|
+
agents: async ({ results: r }) => {
|
|
3003
|
+
const pickedSkills = r.skills;
|
|
3004
|
+
if (pickedSkills !== void 0 && pickedSkills.length === 0) return [];
|
|
3005
|
+
if (configuredAgents !== void 0) return [...configuredAgents];
|
|
3006
|
+
return navigableMultiselect({
|
|
3007
|
+
message: "Select agents to install skills to",
|
|
3008
|
+
options: AVAILABLE_AGENTS,
|
|
3009
|
+
required: false,
|
|
3010
|
+
initialValues: [...DEFAULT_AGENTS$1]
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
2283
3013
|
});
|
|
2284
|
-
if (
|
|
2285
|
-
|
|
3014
|
+
if (results.scope === void 0 || results.skills === void 0 || results.agents === void 0) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
|
|
3015
|
+
scope = results.scope;
|
|
3016
|
+
selectedSkills = results.skills;
|
|
3017
|
+
selectedAgents = results.agents;
|
|
3018
|
+
if (selectedSkills.length === 0 || selectedAgents.length === 0) return Result.ok(void 0);
|
|
2286
3019
|
}
|
|
2287
|
-
if (selectedAgents.length === 0) return Result.ok(void 0);
|
|
2288
3020
|
const skillsBySource = {};
|
|
2289
3021
|
for (const skillKey of selectedSkills) {
|
|
2290
3022
|
const [source, skillName] = skillKey.split("::");
|
|
@@ -2474,7 +3206,8 @@ async function setupTui(config) {
|
|
|
2474
3206
|
cliLog.info("Setting up OpenTUI...");
|
|
2475
3207
|
let template = resolveTuiTemplate(config);
|
|
2476
3208
|
if (!template) {
|
|
2477
|
-
|
|
3209
|
+
setIsFirstPrompt(true);
|
|
3210
|
+
const selectedTemplate = await navigableSelect({
|
|
2478
3211
|
message: "Choose a template",
|
|
2479
3212
|
options: Object.entries(TEMPLATES$1).map(([key, templateOption]) => ({
|
|
2480
3213
|
value: key,
|
|
@@ -2483,7 +3216,7 @@ async function setupTui(config) {
|
|
|
2483
3216
|
})),
|
|
2484
3217
|
initialValue: DEFAULT_TEMPLATE$1
|
|
2485
3218
|
});
|
|
2486
|
-
if (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
|
|
3219
|
+
if (isCancel$1(selectedTemplate)) return userCancelled("Operation cancelled");
|
|
2487
3220
|
template = selectedTemplate;
|
|
2488
3221
|
}
|
|
2489
3222
|
const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
|
|
@@ -2563,42 +3296,40 @@ async function postProcessTuiWorkspace(tuiDir) {
|
|
|
2563
3296
|
//#endregion
|
|
2564
3297
|
//#region src/helpers/addons/ultracite-setup.ts
|
|
2565
3298
|
const LINTERS = {
|
|
2566
|
-
biome: {
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
},
|
|
2570
|
-
eslint: {
|
|
2571
|
-
label: "ESLint",
|
|
2572
|
-
hint: "Traditional JavaScript linter"
|
|
2573
|
-
},
|
|
2574
|
-
oxlint: {
|
|
2575
|
-
label: "Oxlint",
|
|
2576
|
-
hint: "Oxidation compiler linter"
|
|
2577
|
-
}
|
|
2578
|
-
};
|
|
2579
|
-
const EDITORS = {
|
|
2580
|
-
vscode: { label: "VS Code" },
|
|
2581
|
-
cursor: { label: "Cursor" },
|
|
2582
|
-
windsurf: { label: "Windsurf" },
|
|
2583
|
-
antigravity: { label: "Antigravity" },
|
|
2584
|
-
kiro: { label: "Kiro" },
|
|
2585
|
-
trae: { label: "Trae" },
|
|
2586
|
-
void: { label: "Void" },
|
|
2587
|
-
zed: { label: "Zed" }
|
|
3299
|
+
biome: { label: "Biome (Recommended)" },
|
|
3300
|
+
eslint: { label: "ESLint + Prettier + Stylelint" },
|
|
3301
|
+
oxlint: { label: "Oxlint + Oxfmt" }
|
|
2588
3302
|
};
|
|
2589
3303
|
const AGENTS = {
|
|
2590
|
-
|
|
3304
|
+
universal: { label: "Universal (AGENTS.md — covers all agents)" },
|
|
3305
|
+
claude: { label: "Claude Code" },
|
|
2591
3306
|
codex: { label: "Codex" },
|
|
2592
3307
|
jules: { label: "Jules" },
|
|
3308
|
+
replit: { label: "Replit Agent" },
|
|
3309
|
+
devin: { label: "Devin" },
|
|
3310
|
+
lovable: { label: "Lovable" },
|
|
3311
|
+
zencoder: { label: "Zencoder" },
|
|
3312
|
+
ona: { label: "Ona" },
|
|
3313
|
+
openclaw: { label: "OpenClaw" },
|
|
3314
|
+
continue: { label: "Continue" },
|
|
3315
|
+
"snowflake-cortex": { label: "Snowflake Cortex" },
|
|
3316
|
+
deepagents: { label: "Deepagents" },
|
|
3317
|
+
qoder: { label: "Qoder" },
|
|
3318
|
+
"kimi-cli": { label: "Kimi CLI" },
|
|
3319
|
+
mcpjam: { label: "MCPJam" },
|
|
3320
|
+
mux: { label: "Mux" },
|
|
3321
|
+
pi: { label: "Pi" },
|
|
3322
|
+
adal: { label: "AdaL" },
|
|
2593
3323
|
copilot: { label: "GitHub Copilot" },
|
|
2594
3324
|
cline: { label: "Cline" },
|
|
2595
|
-
amp: { label: "
|
|
3325
|
+
amp: { label: "AMP" },
|
|
2596
3326
|
aider: { label: "Aider" },
|
|
2597
3327
|
"firebase-studio": { label: "Firebase Studio" },
|
|
2598
|
-
"open-hands": { label: "
|
|
3328
|
+
"open-hands": { label: "OpenHands" },
|
|
2599
3329
|
gemini: { label: "Gemini" },
|
|
2600
3330
|
junie: { label: "Junie" },
|
|
2601
|
-
augmentcode: { label: "
|
|
3331
|
+
augmentcode: { label: "Augment Code" },
|
|
3332
|
+
bob: { label: "IBM Bob" },
|
|
2602
3333
|
"kilo-code": { label: "Kilo Code" },
|
|
2603
3334
|
goose: { label: "Goose" },
|
|
2604
3335
|
"roo-code": { label: "Roo Code" },
|
|
@@ -2606,21 +3337,23 @@ const AGENTS = {
|
|
|
2606
3337
|
droid: { label: "Droid" },
|
|
2607
3338
|
opencode: { label: "OpenCode" },
|
|
2608
3339
|
crush: { label: "Crush" },
|
|
2609
|
-
qwen: { label: "Qwen" },
|
|
3340
|
+
qwen: { label: "Qwen Code" },
|
|
2610
3341
|
"amazon-q-cli": { label: "Amazon Q CLI" },
|
|
2611
3342
|
firebender: { label: "Firebender" },
|
|
2612
3343
|
"cursor-cli": { label: "Cursor CLI" },
|
|
2613
3344
|
"mistral-vibe": { label: "Mistral Vibe" },
|
|
2614
|
-
vercel: { label: "Vercel" }
|
|
3345
|
+
vercel: { label: "Vercel Agent" }
|
|
2615
3346
|
};
|
|
2616
3347
|
const HOOKS = {
|
|
2617
3348
|
cursor: { label: "Cursor" },
|
|
2618
3349
|
windsurf: { label: "Windsurf" },
|
|
2619
|
-
|
|
3350
|
+
codebuddy: { label: "CodeBuddy" },
|
|
3351
|
+
claude: { label: "Claude Code" },
|
|
3352
|
+
copilot: { label: "GitHub Copilot" }
|
|
2620
3353
|
};
|
|
2621
3354
|
const DEFAULT_LINTER = "biome";
|
|
2622
|
-
const DEFAULT_EDITORS = ["vscode"
|
|
2623
|
-
const DEFAULT_AGENTS = ["
|
|
3355
|
+
const DEFAULT_EDITORS = ["vscode"];
|
|
3356
|
+
const DEFAULT_AGENTS = ["universal"];
|
|
2624
3357
|
const DEFAULT_HOOKS = [];
|
|
2625
3358
|
function getFrameworksFromFrontend(frontend) {
|
|
2626
3359
|
const frameworkMap = {
|
|
@@ -2633,7 +3366,8 @@ function getFrameworksFromFrontend(frontend) {
|
|
|
2633
3366
|
"native-uniwind": "react",
|
|
2634
3367
|
"native-unistyles": "react",
|
|
2635
3368
|
svelte: "svelte",
|
|
2636
|
-
solid: "solid"
|
|
3369
|
+
solid: "solid",
|
|
3370
|
+
astro: "astro"
|
|
2637
3371
|
};
|
|
2638
3372
|
const frameworks = /* @__PURE__ */ new Set();
|
|
2639
3373
|
for (const f of frontend) if (f !== "none" && frameworkMap[f]) frameworks.add(frameworkMap[f]);
|
|
@@ -2651,10 +3385,7 @@ function buildUltraciteInitArgs({ packageManager, linter, frameworks, editors, a
|
|
|
2651
3385
|
if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
|
|
2652
3386
|
if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
|
|
2653
3387
|
if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
|
|
2654
|
-
if (gitHooks.length > 0)
|
|
2655
|
-
const integrations = gitHooks.includes("husky") ? [...new Set([...gitHooks, "lint-staged"])] : gitHooks;
|
|
2656
|
-
ultraciteArgs.push("--integrations", ...integrations);
|
|
2657
|
-
}
|
|
3388
|
+
if (gitHooks.length > 0) ultraciteArgs.push("--integrations", ...gitHooks);
|
|
2658
3389
|
return [
|
|
2659
3390
|
...getPackageRunnerPrefix(packageManager),
|
|
2660
3391
|
"ultracite@latest",
|
|
@@ -2678,67 +3409,60 @@ async function setupUltracite(config, gitHooks) {
|
|
|
2678
3409
|
agents = agents ?? [...DEFAULT_AGENTS];
|
|
2679
3410
|
hooks = hooks ?? [...DEFAULT_HOOKS];
|
|
2680
3411
|
} else {
|
|
2681
|
-
const
|
|
2682
|
-
|
|
2683
|
-
return
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
}),
|
|
2693
|
-
editors: () => multiselect({
|
|
2694
|
-
message: "Choose editors",
|
|
2695
|
-
required: false,
|
|
2696
|
-
options: Object.entries(EDITORS).map(([key, editor]) => ({
|
|
2697
|
-
value: key,
|
|
2698
|
-
label: editor.label
|
|
2699
|
-
})),
|
|
2700
|
-
initialValues: editors ?? [...DEFAULT_EDITORS]
|
|
2701
|
-
}),
|
|
2702
|
-
agents: () => multiselect({
|
|
2703
|
-
message: "Choose agents",
|
|
2704
|
-
required: false,
|
|
2705
|
-
options: Object.entries(AGENTS).map(([key, agent]) => ({
|
|
2706
|
-
value: key,
|
|
2707
|
-
label: agent.label
|
|
2708
|
-
})),
|
|
2709
|
-
initialValues: agents ?? [...DEFAULT_AGENTS]
|
|
2710
|
-
}),
|
|
2711
|
-
hooks: () => multiselect({
|
|
2712
|
-
message: "Choose hooks",
|
|
2713
|
-
required: false,
|
|
2714
|
-
options: Object.entries(HOOKS).map(([key, hook]) => ({
|
|
2715
|
-
value: key,
|
|
2716
|
-
label: hook.label
|
|
2717
|
-
})),
|
|
2718
|
-
initialValues: hooks ?? [...DEFAULT_HOOKS]
|
|
2719
|
-
})
|
|
2720
|
-
}, { onCancel: () => {
|
|
2721
|
-
throw new UserCancelledError({ message: "Operation cancelled" });
|
|
2722
|
-
} });
|
|
3412
|
+
const results = await navigableGroup({
|
|
3413
|
+
linter: async () => {
|
|
3414
|
+
if (linter !== void 0) return linter;
|
|
3415
|
+
return navigableSelect({
|
|
3416
|
+
message: "Which linter do you want to use?",
|
|
3417
|
+
options: Object.entries(LINTERS).map(([key, linterOption]) => ({
|
|
3418
|
+
value: key,
|
|
3419
|
+
label: linterOption.label
|
|
3420
|
+
})),
|
|
3421
|
+
initialValue: linter ?? DEFAULT_LINTER
|
|
3422
|
+
});
|
|
2723
3423
|
},
|
|
2724
|
-
|
|
2725
|
-
if (
|
|
2726
|
-
return
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3424
|
+
editors: async () => {
|
|
3425
|
+
if (editors !== void 0) return editors;
|
|
3426
|
+
return navigableMultiselect({
|
|
3427
|
+
message: "Which editors do you want to configure (recommended)?",
|
|
3428
|
+
required: false,
|
|
3429
|
+
options: [{
|
|
3430
|
+
value: "vscode",
|
|
3431
|
+
label: "VSCode / Cursor / Windsurf"
|
|
3432
|
+
}, {
|
|
3433
|
+
value: "zed",
|
|
3434
|
+
label: "Zed"
|
|
3435
|
+
}]
|
|
3436
|
+
});
|
|
3437
|
+
},
|
|
3438
|
+
agents: async () => {
|
|
3439
|
+
if (agents !== void 0) return agents;
|
|
3440
|
+
return navigableMultiselect({
|
|
3441
|
+
message: "Which agent files do you want to add (optional)?",
|
|
3442
|
+
required: false,
|
|
3443
|
+
options: Object.entries(AGENTS).map(([key, agent]) => ({
|
|
3444
|
+
value: key,
|
|
3445
|
+
label: agent.label
|
|
3446
|
+
}))
|
|
3447
|
+
});
|
|
3448
|
+
},
|
|
3449
|
+
hooks: async () => {
|
|
3450
|
+
if (hooks !== void 0) return hooks;
|
|
3451
|
+
return navigableMultiselect({
|
|
3452
|
+
message: "Which agent hooks do you want to enable (optional)?",
|
|
3453
|
+
required: false,
|
|
3454
|
+
options: Object.entries(HOOKS).map(([key, hook]) => ({
|
|
3455
|
+
value: key,
|
|
3456
|
+
label: hook.label
|
|
3457
|
+
}))
|
|
2730
3458
|
});
|
|
2731
3459
|
}
|
|
2732
3460
|
});
|
|
2733
|
-
if (
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
linter = groupResult.value.linter;
|
|
2739
|
-
editors = groupResult.value.editors;
|
|
2740
|
-
agents = groupResult.value.agents;
|
|
2741
|
-
hooks = groupResult.value.hooks;
|
|
3461
|
+
if (results.linter === void 0 || results.editors === void 0 || results.agents === void 0 || results.hooks === void 0) return userCancelled("Operation cancelled");
|
|
3462
|
+
linter = results.linter;
|
|
3463
|
+
editors = results.editors;
|
|
3464
|
+
agents = results.agents;
|
|
3465
|
+
hooks = results.hooks;
|
|
2742
3466
|
}
|
|
2743
3467
|
const frameworks = getFrameworksFromFrontend(frontend);
|
|
2744
3468
|
const args = buildUltraciteInitArgs({
|
|
@@ -2809,7 +3533,8 @@ async function setupWxt(config) {
|
|
|
2809
3533
|
let template = configuredOptions?.template;
|
|
2810
3534
|
if (!template) if (isSilent()) template = DEFAULT_TEMPLATE;
|
|
2811
3535
|
else {
|
|
2812
|
-
|
|
3536
|
+
setIsFirstPrompt(true);
|
|
3537
|
+
const selectedTemplate = await navigableSelect({
|
|
2813
3538
|
message: "Choose a template",
|
|
2814
3539
|
options: Object.entries(TEMPLATES).map(([key, templateOption]) => ({
|
|
2815
3540
|
value: key,
|
|
@@ -2818,7 +3543,7 @@ async function setupWxt(config) {
|
|
|
2818
3543
|
})),
|
|
2819
3544
|
initialValue: DEFAULT_TEMPLATE
|
|
2820
3545
|
});
|
|
2821
|
-
if (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
|
|
3546
|
+
if (isCancel$1(selectedTemplate)) return userCancelled("Operation cancelled");
|
|
2822
3547
|
template = selectedTemplate;
|
|
2823
3548
|
}
|
|
2824
3549
|
const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT;
|
|
@@ -2926,6 +3651,7 @@ async function setupAddons(config) {
|
|
|
2926
3651
|
if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
|
|
2927
3652
|
if (addons.includes("skills")) await runSetup(() => setupSkills(config));
|
|
2928
3653
|
if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
|
|
3654
|
+
if (addons.includes("evlog")) await runSetup(() => setupEvlog(config));
|
|
2929
3655
|
}
|
|
2930
3656
|
async function setupBiome(projectDir) {
|
|
2931
3657
|
await addPackageDependency({
|
|
@@ -3089,7 +3815,7 @@ async function addHandlerInternal(input) {
|
|
|
3089
3815
|
} else if (isSilent()) return Result.err(new CLIError({ message: "Addons are required in silent mode. Provide them via add() or add-json." }));
|
|
3090
3816
|
else {
|
|
3091
3817
|
const promptResult = await Result.tryPromise({
|
|
3092
|
-
try: () => getAddonsToAdd(existingConfig
|
|
3818
|
+
try: () => getAddonsToAdd(existingConfig),
|
|
3093
3819
|
catch: (e) => {
|
|
3094
3820
|
if (UserCancelledError.is(e)) return e;
|
|
3095
3821
|
return new CLIError({
|
|
@@ -3113,6 +3839,8 @@ async function addHandlerInternal(input) {
|
|
|
3113
3839
|
}
|
|
3114
3840
|
addonsToAdd = selectedAddons;
|
|
3115
3841
|
}
|
|
3842
|
+
const addonsValidationResult = validateAddonsAgainstConfig(addonsToAdd, existingConfig);
|
|
3843
|
+
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
3116
3844
|
if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
|
|
3117
3845
|
const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
|
|
3118
3846
|
const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
|
|
@@ -3140,7 +3868,12 @@ async function addHandlerInternal(input) {
|
|
|
3140
3868
|
};
|
|
3141
3869
|
if (!isSilent()) log.info(pc.dim("Installing addon files..."));
|
|
3142
3870
|
const vfs = new VirtualFileSystem();
|
|
3143
|
-
for (const pkgPath of [
|
|
3871
|
+
for (const pkgPath of [
|
|
3872
|
+
"package.json",
|
|
3873
|
+
"apps/web/package.json",
|
|
3874
|
+
"apps/server/package.json",
|
|
3875
|
+
"apps/native/package.json"
|
|
3876
|
+
]) {
|
|
3144
3877
|
const fullPath = path.join(projectDir, pkgPath);
|
|
3145
3878
|
if (await fs.pathExists(fullPath)) {
|
|
3146
3879
|
const content = await fs.readFile(fullPath, "utf-8");
|
|
@@ -3295,6 +4028,7 @@ const FULLSTACK_FRONTENDS = [
|
|
|
3295
4028
|
"next",
|
|
3296
4029
|
"tanstack-start",
|
|
3297
4030
|
"nuxt",
|
|
4031
|
+
"svelte",
|
|
3298
4032
|
"astro"
|
|
3299
4033
|
];
|
|
3300
4034
|
async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
@@ -3648,62 +4382,6 @@ async function getinstallChoice(install) {
|
|
|
3648
4382
|
return response;
|
|
3649
4383
|
}
|
|
3650
4384
|
//#endregion
|
|
3651
|
-
//#region src/prompts/navigable-group.ts
|
|
3652
|
-
/**
|
|
3653
|
-
* Navigable group - a group of prompts that allows going back
|
|
3654
|
-
*/
|
|
3655
|
-
/**
|
|
3656
|
-
* Define a group of prompts that supports going back to previous prompts.
|
|
3657
|
-
* Returns a result object with all the values, or handles cancel/go-back navigation.
|
|
3658
|
-
*/
|
|
3659
|
-
async function navigableGroup(prompts, opts) {
|
|
3660
|
-
const results = {};
|
|
3661
|
-
const promptNames = Object.keys(prompts);
|
|
3662
|
-
let currentIndex = 0;
|
|
3663
|
-
let goingBack = false;
|
|
3664
|
-
while (currentIndex < promptNames.length) {
|
|
3665
|
-
const name = promptNames[currentIndex];
|
|
3666
|
-
const prompt = prompts[name];
|
|
3667
|
-
setIsFirstPrompt$1(currentIndex === 0);
|
|
3668
|
-
setLastPromptShownUI(false);
|
|
3669
|
-
const result = await prompt({ results })?.catch((e) => {
|
|
3670
|
-
throw e;
|
|
3671
|
-
});
|
|
3672
|
-
if (isGoBack(result)) {
|
|
3673
|
-
goingBack = true;
|
|
3674
|
-
if (currentIndex > 0) {
|
|
3675
|
-
const prevName = promptNames[currentIndex - 1];
|
|
3676
|
-
delete results[prevName];
|
|
3677
|
-
currentIndex--;
|
|
3678
|
-
continue;
|
|
3679
|
-
}
|
|
3680
|
-
goingBack = false;
|
|
3681
|
-
continue;
|
|
3682
|
-
}
|
|
3683
|
-
if (isCancel$1(result)) {
|
|
3684
|
-
if (typeof opts?.onCancel === "function") {
|
|
3685
|
-
results[name] = "canceled";
|
|
3686
|
-
opts.onCancel({ results });
|
|
3687
|
-
}
|
|
3688
|
-
setIsFirstPrompt$1(false);
|
|
3689
|
-
return results;
|
|
3690
|
-
}
|
|
3691
|
-
if (goingBack && !didLastPromptShowUI()) {
|
|
3692
|
-
if (currentIndex > 0) {
|
|
3693
|
-
const prevName = promptNames[currentIndex - 1];
|
|
3694
|
-
delete results[prevName];
|
|
3695
|
-
currentIndex--;
|
|
3696
|
-
continue;
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
goingBack = false;
|
|
3700
|
-
results[name] = result;
|
|
3701
|
-
currentIndex++;
|
|
3702
|
-
}
|
|
3703
|
-
setIsFirstPrompt$1(false);
|
|
3704
|
-
return results;
|
|
3705
|
-
}
|
|
3706
|
-
//#endregion
|
|
3707
4385
|
//#region src/utils/config-validation.ts
|
|
3708
4386
|
function validationErr(message) {
|
|
3709
4387
|
return Result.err(new ValidationError({ message }));
|
|
@@ -3887,7 +4565,7 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
3887
4565
|
if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
|
|
3888
4566
|
if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") yield* validationErr(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
|
|
3889
4567
|
if (config.addons && config.addons.length > 0) {
|
|
3890
|
-
yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
|
4568
|
+
yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
3891
4569
|
config.addons = [...new Set(config.addons)];
|
|
3892
4570
|
}
|
|
3893
4571
|
yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
|
|
@@ -3901,7 +4579,7 @@ function validateConfigForProgrammaticUse(config) {
|
|
|
3901
4579
|
if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
|
|
3902
4580
|
yield* validateApiFrontendCompatibility(config.api, config.frontend);
|
|
3903
4581
|
yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
|
|
3904
|
-
if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
|
4582
|
+
if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
3905
4583
|
yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
|
|
3906
4584
|
return Result.ok(void 0);
|
|
3907
4585
|
});
|
|
@@ -4096,7 +4774,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
4096
4774
|
api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
|
|
4097
4775
|
auth: ({ results }) => getAuthChoice(flags.auth, results.backend, results.frontend),
|
|
4098
4776
|
payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
|
|
4099
|
-
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth),
|
|
4777
|
+
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime),
|
|
4100
4778
|
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
4101
4779
|
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
4102
4780
|
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup),
|
|
@@ -6076,7 +6754,10 @@ async function displayPostInstallInstructions(config) {
|
|
|
6076
6754
|
output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
|
|
6077
6755
|
if (api === "orpc") output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
|
|
6078
6756
|
}
|
|
6079
|
-
if (isBackendSelf && api === "orpc")
|
|
6757
|
+
if (isBackendSelf && api === "orpc") {
|
|
6758
|
+
const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
|
|
6759
|
+
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}${rpcPath}/api-reference\n`;
|
|
6760
|
+
}
|
|
6080
6761
|
if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
|
6081
6762
|
if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
|
6082
6763
|
}
|
|
@@ -6104,7 +6785,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
6104
6785
|
}
|
|
6105
6786
|
function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
|
|
6106
6787
|
const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
|
|
6107
|
-
const
|
|
6788
|
+
const selfBackendPort = frontend.includes("svelte") ? "5173" : frontend.includes("astro") ? "4321" : "3001";
|
|
6789
|
+
const exampleUrl = isConvex ? "https://<YOUR_CONVEX_URL>" : isBackendSelf ? `http://<YOUR_LOCAL_IP>:${selfBackendPort}` : "http://<YOUR_LOCAL_IP>:3000";
|
|
6108
6790
|
const envFileName = ".env";
|
|
6109
6791
|
const ipNote = isConvex ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address";
|
|
6110
6792
|
let instructions = `${pc.yellow("NOTE:")} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
|
|
@@ -6251,8 +6933,8 @@ function getPolarInstructions(backend) {
|
|
|
6251
6933
|
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
|
|
6252
6934
|
const instructions = [];
|
|
6253
6935
|
const isBackendSelf = backend === "self";
|
|
6254
|
-
if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare") instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${
|
|
6255
|
-
else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${
|
|
6936
|
+
if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
6937
|
+
else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
6256
6938
|
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`}`);
|
|
6257
6939
|
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
6258
6940
|
}
|
|
@@ -6532,6 +7214,10 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
6532
7214
|
...config,
|
|
6533
7215
|
dbSetupOptions: effectiveDbSetupOptions
|
|
6534
7216
|
};
|
|
7217
|
+
if (!input.yolo) {
|
|
7218
|
+
const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
7219
|
+
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
7220
|
+
}
|
|
6535
7221
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
6536
7222
|
if (input.dryRun) {
|
|
6537
7223
|
const elapsedTimeMs = Date.now() - startTime;
|