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.
@@ -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, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
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. Support for SvelteKit will be added in a future update.");
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. Support for SvelteKit will be added in a future update.");
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(frontend, existingAddons = [], auth) {
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, existingAddons, auth);
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: "Recommended template with MDX support",
2109
+ hint: "recommended",
1488
2110
  value: "+next+fuma-docs-mdx"
1489
2111
  },
1490
2112
  "next-mdx-static": {
1491
- label: "Next.js: Fumadocs MDX (Static)",
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: Content Collections",
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 Remote",
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: SPA",
1507
- hint: "Template for React Router SPA",
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 Remote",
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: SPA",
1517
- hint: "Template for Tanstack Start SPA",
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
- if (!template) if (isSilent()) template = DEFAULT_TEMPLATE$2;
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 selectedTemplate = await select({
1532
- message: "Choose a template",
1533
- options: Object.entries(TEMPLATES$2).map(([key, templateOption]) => ({
1534
- value: key,
1535
- label: templateOption.label,
1536
- hint: templateOption.hint
1537
- })),
1538
- initialValue: DEFAULT_TEMPLATE$2
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 (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
1541
- template = selectedTemplate;
1542
- }
1543
- const templateArg = TEMPLATES$2[template].value;
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
- let scope = config.addonOptions?.mcp?.scope;
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
- let selectedAgents = configuredAgents?.filter((agent) => agentOptions.some((option) => option.value === agent)) ?? [];
1864
- if (selectedAgents.length === 0 && configuredAgents === void 0) if (isSilent()) selectedAgents = defaultAgents;
1865
- else {
1866
- const promptedAgents = await multiselect({
1867
- message: "Select agents to install MCP servers to",
1868
- options: agentOptions,
1869
- required: false,
1870
- initialValues: defaultAgents
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 (isCancel(promptedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1873
- selectedAgents = [...promptedAgents];
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
- let scope = skillsOptions?.scope;
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
- let selectedAgents = configuredAgents ? [...configuredAgents] : [];
2276
- if (selectedAgents.length === 0 && configuredAgents === void 0) if (isSilent()) selectedAgents = [...DEFAULT_AGENTS$1];
2277
- else {
2278
- const promptedAgents = await multiselect({
2279
- message: "Select agents to install skills to",
2280
- options: AVAILABLE_AGENTS,
2281
- required: false,
2282
- initialValues: [...DEFAULT_AGENTS$1]
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 (isCancel(promptedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2285
- selectedAgents = [...promptedAgents];
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
- const selectedTemplate = await select({
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
- label: "Biome",
2568
- hint: "Fast formatter and linter"
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
- claude: { label: "Claude" },
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: "Amp" },
3325
+ amp: { label: "AMP" },
2596
3326
  aider: { label: "Aider" },
2597
3327
  "firebase-studio": { label: "Firebase Studio" },
2598
- "open-hands": { label: "Open Hands" },
3328
+ "open-hands": { label: "OpenHands" },
2599
3329
  gemini: { label: "Gemini" },
2600
3330
  junie: { label: "Junie" },
2601
- augmentcode: { label: "AugmentCode" },
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
- claude: { label: "Claude" }
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", "cursor"];
2623
- const DEFAULT_AGENTS = ["claude", "codex"];
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 groupResult = await Result.tryPromise({
2682
- try: async () => {
2683
- return await group({
2684
- linter: () => select({
2685
- message: "Choose linter/formatter",
2686
- options: Object.entries(LINTERS).map(([key, linterOption]) => ({
2687
- value: key,
2688
- label: linterOption.label,
2689
- hint: linterOption.hint
2690
- })),
2691
- initialValue: linter ?? DEFAULT_LINTER
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
- catch: (e) => {
2725
- if (e instanceof UserCancelledError) return e;
2726
- return new AddonSetupError({
2727
- addon: "ultracite",
2728
- message: `Failed to get user preferences: ${e instanceof Error ? e.message : String(e)}`,
2729
- cause: e
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 (groupResult.isErr()) {
2734
- if (UserCancelledError.is(groupResult.error)) return userCancelled(groupResult.error.message);
2735
- cliLog.error(pc.red("Failed to set up Ultracite"));
2736
- return groupResult;
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
- const selectedTemplate = await select({
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.frontend, existingConfig.addons, existingConfig.auth),
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 ["package.json", "apps/web/package.json"]) {
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") output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}/api/rpc/api-reference\n`;
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 exampleUrl = isConvex ? "https://<YOUR_CONVEX_URL>" : isBackendSelf ? "http://<YOUR_LOCAL_IP>:3001" : "http://<YOUR_LOCAL_IP>:3000";
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: ${`cd apps/web && ${runCmd} alchemy dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/web && ${runCmd} destroy`}`);
6255
- else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`cd apps/server && ${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/server && ${runCmd} destroy`}`);
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;