create-kubojs 0.1.0 → 0.2.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.
@@ -22,6 +22,7 @@ import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt,
22
22
  import { applyEdits, modify, parse } from "jsonc-parser";
23
23
  import os from "node:os";
24
24
  import { format } from "oxfmt";
25
+ import { Generator, configSchema } from "@tanstack/router-generator";
25
26
  //#region src/utils/get-package-manager.ts
26
27
  const getUserPkgManager = () => {
27
28
  const userAgent = process.env.npm_config_user_agent;
@@ -42,9 +43,11 @@ const DEFAULT_CONFIG_BASE = {
42
43
  orm: "drizzle",
43
44
  auth: "better-auth",
44
45
  payments: "none",
45
- observability: "none",
46
+ observability: ["getmonitor"],
47
+ communication: "none",
46
48
  addons: ["turborepo"],
47
49
  examples: [],
50
+ testing: [],
48
51
  git: true,
49
52
  install: true,
50
53
  dbSetup: "none",
@@ -61,7 +64,8 @@ function getDefaultConfig() {
61
64
  packageManager: getUserPkgManager(),
62
65
  frontend: [...DEFAULT_CONFIG_BASE.frontend],
63
66
  addons: [...DEFAULT_CONFIG_BASE.addons],
64
- examples: [...DEFAULT_CONFIG_BASE.examples]
67
+ examples: [...DEFAULT_CONFIG_BASE.examples],
68
+ testing: [...DEFAULT_CONFIG_BASE.testing]
65
69
  };
66
70
  }
67
71
  const DEFAULT_CONFIG = getDefaultConfig();
@@ -383,9 +387,11 @@ async function addToHistory(config, reproducibleCommand) {
383
387
  auth: config.auth,
384
388
  payments: config.payments,
385
389
  observability: config.observability,
390
+ communication: config.communication,
386
391
  api: config.api,
387
392
  addons: config.addons,
388
393
  examples: config.examples,
394
+ testing: config.testing,
389
395
  dbSetup: config.dbSetup,
390
396
  packageManager: config.packageManager
391
397
  },
@@ -516,169 +522,6 @@ async function openUrl(url) {
516
522
  await $({ stdio: "ignore" })`xdg-open ${url}`;
517
523
  }
518
524
  //#endregion
519
- //#region src/utils/sponsors.ts
520
- const SPONSORS_JSON_URL = "https://sponsors.kubojs.dev/sponsors.json";
521
- const GITHUB_SPONSOR_URL = "https://github.com/sponsors/AmanVarshney01";
522
- const nullableString = z.string().nullish().transform((value) => value ?? void 0);
523
- const nullableNumber = z.number().nullish().transform((value) => value ?? void 0);
524
- const sponsorSchema = z.object({
525
- name: nullableString,
526
- githubId: z.string(),
527
- avatarUrl: z.string(),
528
- websiteUrl: nullableString,
529
- githubUrl: z.string(),
530
- tierName: nullableString,
531
- sinceWhen: z.string(),
532
- transactionCount: z.number(),
533
- totalProcessedAmount: nullableNumber,
534
- formattedAmount: nullableString
535
- });
536
- const sponsorSummarySchema = z.object({
537
- total_sponsors: z.number(),
538
- total_lifetime_amount: z.number(),
539
- total_current_monthly: z.number(),
540
- special_sponsors: z.number(),
541
- current_sponsors: z.number(),
542
- past_sponsors: z.number(),
543
- backers: z.number(),
544
- top_sponsor: z.object({
545
- name: z.string(),
546
- amount: z.number()
547
- }).nullish().transform((value) => value ?? void 0)
548
- });
549
- const sponsorEntrySchema = z.object({
550
- generated_at: z.string(),
551
- summary: sponsorSummarySchema,
552
- specialSponsors: z.array(sponsorSchema),
553
- sponsors: z.array(sponsorSchema),
554
- pastSponsors: z.array(sponsorSchema),
555
- backers: z.array(sponsorSchema)
556
- });
557
- async function fetchSponsors(url = SPONSORS_JSON_URL) {
558
- return fetchSponsorsData({
559
- url,
560
- withSpinner: true
561
- });
562
- }
563
- async function fetchSponsorsQuietly({ url = SPONSORS_JSON_URL, timeoutMs = 1500 } = {}) {
564
- return fetchSponsorsData({
565
- url,
566
- withSpinner: false,
567
- timeoutMs
568
- });
569
- }
570
- function displaySponsors(sponsors) {
571
- const { total_sponsors } = sponsors.summary;
572
- if (total_sponsors === 0) {
573
- log.info("No sponsors found. You can be the first one! ✨");
574
- outro(pc.cyan(`Visit ${GITHUB_SPONSOR_URL} to become a sponsor.`));
575
- return;
576
- }
577
- displaySponsorsBox(sponsors);
578
- if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`));
579
- outro(pc.magenta(`Visit ${GITHUB_SPONSOR_URL} to become a sponsor.`));
580
- }
581
- function displaySponsorsBox(sponsors) {
582
- if (sponsors.specialSponsors.length === 0) return;
583
- let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
584
- sponsors.specialSponsors.forEach((sponsor, idx) => {
585
- const displayName = sponsor.name ?? sponsor.githubId;
586
- const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
587
- output += `${pc.green(`• ${displayName}`)}${tier}\n`;
588
- output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
589
- const website = sponsor.websiteUrl ?? sponsor.githubUrl;
590
- if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
591
- if (idx < sponsors.specialSponsors.length - 1) output += "\n";
592
- });
593
- cliConsola.box(output);
594
- }
595
- function formatPostInstallSpecialSponsorsSection(sponsors) {
596
- if (sponsors.specialSponsors.length === 0) return "";
597
- const wrappedSponsorLines = wrapSponsorTokens(sponsors.specialSponsors.map((sponsor) => {
598
- return `• ${sponsor.name ?? sponsor.githubId}`;
599
- }), getPostInstallSponsorLineWidth());
600
- let output = `${pc.bold("Special sponsors")}\n`;
601
- wrappedSponsorLines.forEach((line) => {
602
- output += `${line}\n`;
603
- });
604
- return output.trimEnd();
605
- }
606
- function getPostInstallSponsorLineWidth() {
607
- const terminalWidth = process.stdout.columns;
608
- if (!terminalWidth || terminalWidth <= 0) return 72;
609
- const availableWidth = Math.max(8, terminalWidth - 24);
610
- return Math.min(72, availableWidth);
611
- }
612
- function wrapSponsorTokens(tokens, maxLineWidth) {
613
- const lines = [];
614
- const separator = " ";
615
- let currentLine = "";
616
- tokens.forEach((token) => {
617
- const candidateLine = currentLine ? `${currentLine}${separator}${token}` : token;
618
- if (candidateLine.length <= maxLineWidth) {
619
- currentLine = candidateLine;
620
- return;
621
- }
622
- if (currentLine) {
623
- lines.push(currentLine);
624
- currentLine = token;
625
- return;
626
- }
627
- lines.push(token);
628
- });
629
- if (currentLine) lines.push(currentLine);
630
- return lines;
631
- }
632
- async function fetchSponsorsData({ url = SPONSORS_JSON_URL, withSpinner = false, timeoutMs }) {
633
- const s = withSpinner ? spinner() : null;
634
- if (s) s.start("Fetching sponsors…");
635
- const controller = timeoutMs ? new AbortController() : null;
636
- const timeout = timeoutMs ? setTimeout(() => {
637
- controller?.abort();
638
- }, timeoutMs) : null;
639
- try {
640
- const response = await fetch(url, controller ? { signal: controller.signal } : void 0);
641
- if (!response.ok) {
642
- const message = `Failed to fetch sponsors: ${response.statusText || String(response.status)}`;
643
- if (s) s.stop(pc.red(message));
644
- return Result.err(new CLIError({ message }));
645
- }
646
- const rawSponsors = await response.json();
647
- const parseResult = sponsorEntrySchema.safeParse(rawSponsors);
648
- if (!parseResult.success) {
649
- const message = `Failed to fetch sponsors: invalid response format at "${parseResult.error.issues[0]?.path?.join(".") || "unknown"}"`;
650
- if (s) s.stop(pc.red(message));
651
- return Result.err(new CLIError({
652
- message,
653
- cause: parseResult.error
654
- }));
655
- }
656
- if (s) s.stop("Sponsors fetched successfully!");
657
- return Result.ok(parseResult.data);
658
- } catch (error) {
659
- const normalizedError = normalizeSponsorFetchError(error);
660
- if (s) s.stop(pc.red(normalizedError.message));
661
- return Result.err(normalizedError);
662
- } finally {
663
- if (timeout) clearTimeout(timeout);
664
- }
665
- }
666
- function normalizeSponsorFetchError(error) {
667
- if (error instanceof Error && error.name === "AbortError") return new CLIError({
668
- message: "Failed to fetch sponsors: request timed out",
669
- cause: error
670
- });
671
- if (CLIError.is(error)) return error;
672
- if (error instanceof Error) return new CLIError({
673
- message: error.message.startsWith("Failed to fetch sponsors:") ? error.message : `Failed to fetch sponsors: ${error.message}`,
674
- cause: error
675
- });
676
- return new CLIError({
677
- message: `Failed to fetch sponsors: ${String(error)}`,
678
- cause: error
679
- });
680
- }
681
- //#endregion
682
525
  //#region src/commands/meta.ts
683
526
  const DOCS_URL = "https://kubojs.dev/docs";
684
527
  const BUILDER_URL = "https://kubojs.dev/new";
@@ -689,17 +532,6 @@ async function openExternalUrl(url, successMessage) {
689
532
  })).isOk()) log.success(pc.blue(successMessage));
690
533
  else log.message(`Please visit ${url}`);
691
534
  }
692
- async function showSponsorsCommand() {
693
- renderTitle();
694
- intro(pc.magenta("I dont know Sponsors"));
695
- const sponsorsResult = await fetchSponsors();
696
- if (sponsorsResult.isErr()) {
697
- displayError(sponsorsResult.error);
698
- process.exit(1);
699
- return;
700
- }
701
- displaySponsors(sponsorsResult.value);
702
- }
703
535
  async function openDocsCommand() {
704
536
  await openExternalUrl(DOCS_URL, "Opened docs in your default browser.");
705
537
  }
@@ -730,7 +562,35 @@ const TASK_RUNNER_ADDONS$1 = [
730
562
  "nx",
731
563
  "vite-plus"
732
564
  ];
565
+ /** Mutually exclusive code-quality linters (one slot). */
566
+ const LINTER_ADDONS = [
567
+ "biome",
568
+ "oxlint",
569
+ "ultracite"
570
+ ];
733
571
  const STATIC_DESKTOP_ADDONS = ["tauri", "electrobun"];
572
+ function isLinterAddon(addon) {
573
+ return LINTER_ADDONS.includes(addon);
574
+ }
575
+ /**
576
+ * When adding a code-quality linter, drop sibling linters so only one remains.
577
+ * Last-selected linter wins. Task runners stay validation-only (reject on dual).
578
+ */
579
+ function mergeAddonsExclusive(existingAddons, addonsToAdd) {
580
+ const removed = /* @__PURE__ */ new Set();
581
+ let result = [...existingAddons];
582
+ for (const addon of addonsToAdd) {
583
+ if (LINTER_ADDONS.includes(addon)) {
584
+ for (const current of result) if (LINTER_ADDONS.includes(current) && current !== addon) removed.add(current);
585
+ result = result.filter((current) => !LINTER_ADDONS.includes(current) || current === addon);
586
+ }
587
+ if (!result.includes(addon) && addon !== "none") result.push(addon);
588
+ }
589
+ return {
590
+ updatedAddons: result,
591
+ removedAddons: [...removed]
592
+ };
593
+ }
734
594
  const TAURI_STATIC_EXPORT_FRONTENDS = ["next", "tanstack-start"];
735
595
  const CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS = [
736
596
  "nuxt",
@@ -753,6 +613,13 @@ function validationErr$1(message) {
753
613
  function isWebFrontend(value) {
754
614
  return WEB_FRAMEWORKS.includes(value);
755
615
  }
616
+ function isPlaywrightAllowed(frontends = []) {
617
+ return frontends.some((frontend) => isWebFrontend(frontend));
618
+ }
619
+ function validateTestingAgainstFrontends(testing = [], frontends = []) {
620
+ if (testing.includes("playwright") && !isPlaywrightAllowed(frontends)) return validationErr$1("playwright testing requires a web frontend");
621
+ return Result.ok(void 0);
622
+ }
756
623
  function splitFrontends(values = []) {
757
624
  return {
758
625
  web: values.filter((f) => isWebFrontend(f)),
@@ -942,6 +809,7 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
942
809
  }
943
810
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
944
811
  if (addons.filter((addon) => TASK_RUNNER_ADDONS$1.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo', 'nx', and 'vite-plus' addons. Choose one task runner.");
812
+ if ([...new Set(addons.filter((addon) => LINTER_ADDONS.includes(addon)))].length > 1) return validationErr$1("Cannot combine 'biome', 'oxlint', and 'ultracite' addons. Choose one code-quality linter.");
945
813
  for (const addon of addons) {
946
814
  if (addon === "none") continue;
947
815
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
@@ -952,6 +820,11 @@ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backe
952
820
  function validateAddonsAgainstConfig(addons = [], config) {
953
821
  return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
954
822
  }
823
+ function validateCommunicationCompatibility(communication, backend) {
824
+ if (!communication || communication === "none") return Result.ok(void 0);
825
+ if ((communication === "resend" || communication === "notifique") && backend === "none") return validationErr$1(`${communication === "notifique" ? "Notifique" : "Resend"} communication requires a server backend. Please choose a backend or use '--communication none'.`);
826
+ return Result.ok(void 0);
827
+ }
955
828
  function validatePaymentsCompatibility(payments, auth, backend, frontends = [], orm, database) {
956
829
  if (!payments || payments === "none") return Result.ok(void 0);
957
830
  if (payments === "abacatepay") {
@@ -1297,7 +1170,7 @@ function getAddonDisplay(addon) {
1297
1170
  break;
1298
1171
  case "mcp":
1299
1172
  label = "MCP";
1300
- hint = "Install MCP servers, including I dont know, via add-mcp";
1173
+ hint = "Install MCP servers, including kubojs, via add-mcp";
1301
1174
  break;
1302
1175
  case "evlog":
1303
1176
  label = "evlog";
@@ -1363,6 +1236,11 @@ function validateAddonSelection(selected) {
1363
1236
  "nx",
1364
1237
  "vite-plus"
1365
1238
  ].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
1239
+ if ((selected?.filter((addon) => [
1240
+ "biome",
1241
+ "oxlint",
1242
+ "ultracite"
1243
+ ].includes(addon)) ?? []).length > 1) return "Choose Biome, Oxlint, or Ultracite as your code-quality tool, not more than one.";
1366
1244
  }
1367
1245
  async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
1368
1246
  if (addons !== void 0) return addons;
@@ -1414,14 +1292,34 @@ async function getAddonsToAdd(config) {
1414
1292
  return response;
1415
1293
  }
1416
1294
  //#endregion
1417
- //#region src/utils/bts-config.ts
1418
- const BTS_CONFIG_FILE = "bts.jsonc";
1295
+ //#region src/utils/input-hardening.ts
1296
+ function hasControlCharacters(value) {
1297
+ for (const char of value) {
1298
+ const charCode = char.charCodeAt(0);
1299
+ if (charCode < 32 || charCode === 127) return true;
1300
+ }
1301
+ return false;
1302
+ }
1303
+ function hardeningError(field, value, message) {
1304
+ return Result.err(new ValidationError({
1305
+ field,
1306
+ value,
1307
+ message
1308
+ }));
1309
+ }
1310
+ function validateAgentSafePathInput(value, field) {
1311
+ if (hasControlCharacters(value)) return hardeningError(field, value, `Invalid ${field}: control characters are not allowed.`);
1312
+ return Result.ok(void 0);
1313
+ }
1314
+ //#endregion
1315
+ //#region src/utils/kubojs-config.ts
1316
+ const KUBOJS_CONFIG_FILE = "kubojs.jsonrc";
1419
1317
  /**
1420
- * Reads the BTS configuration file from the project directory.
1318
+ * Reads the kubojs configuration file from the project directory.
1421
1319
  */
1422
- async function readBtsConfig(projectDir) {
1320
+ async function readKubojsConfig(projectDir) {
1423
1321
  try {
1424
- const configPath = path.join(projectDir, BTS_CONFIG_FILE);
1322
+ const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1425
1323
  if (!await fs.pathExists(configPath)) return null;
1426
1324
  return parse(await fs.readFile(configPath, "utf-8"));
1427
1325
  } catch {
@@ -1429,11 +1327,11 @@ async function readBtsConfig(projectDir) {
1429
1327
  }
1430
1328
  }
1431
1329
  /**
1432
- * Updates specific fields in the BTS configuration file.
1330
+ * Updates specific fields in the kubojs configuration file.
1433
1331
  */
1434
- async function updateBtsConfig(projectDir, updates) {
1332
+ async function updateKubojsConfig(projectDir, updates) {
1435
1333
  try {
1436
- const configPath = path.join(projectDir, BTS_CONFIG_FILE);
1334
+ const configPath = path.join(projectDir, KUBOJS_CONFIG_FILE);
1437
1335
  if (!await fs.pathExists(configPath)) return;
1438
1336
  let content = await fs.readFile(configPath, "utf-8");
1439
1337
  for (const [key, value] of Object.entries(updates)) {
@@ -1444,26 +1342,6 @@ async function updateBtsConfig(projectDir, updates) {
1444
1342
  } catch {}
1445
1343
  }
1446
1344
  //#endregion
1447
- //#region src/utils/input-hardening.ts
1448
- function hasControlCharacters(value) {
1449
- for (const char of value) {
1450
- const charCode = char.charCodeAt(0);
1451
- if (charCode < 32 || charCode === 127) return true;
1452
- }
1453
- return false;
1454
- }
1455
- function hardeningError(field, value, message) {
1456
- return Result.err(new ValidationError({
1457
- field,
1458
- value,
1459
- message
1460
- }));
1461
- }
1462
- function validateAgentSafePathInput(value, field) {
1463
- if (hasControlCharacters(value)) return hardeningError(field, value, `Invalid ${field}: control characters are not allowed.`);
1464
- return Result.ok(void 0);
1465
- }
1466
- //#endregion
1467
1345
  //#region src/utils/add-package-deps.ts
1468
1346
  const addPackageDependency = async (opts) => {
1469
1347
  const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
@@ -2318,7 +2196,7 @@ async function setupFumadocs(config) {
2318
2196
  "--no-git"
2319
2197
  ];
2320
2198
  if (isNextTemplate) options.push("--src");
2321
- const persistedConfig = await readBtsConfig(projectDir);
2199
+ const persistedConfig = await readKubojsConfig(projectDir);
2322
2200
  const linter = getFumadocsLinter(getFumadocsAddonContext(config.addons, persistedConfig?.addons));
2323
2201
  if (linter) options.push(`--linter ${linter}`);
2324
2202
  if (search) options.push(`--search ${search}`);
@@ -2450,7 +2328,7 @@ function getAllMcpServers(config) {
2450
2328
  return [
2451
2329
  {
2452
2330
  key: "kubojs",
2453
- label: "I dont know",
2331
+ label: "kubojs",
2454
2332
  name: "kubojs",
2455
2333
  target: getPackageExecutionCommand(config.packageManager, "kubojs@latest mcp")
2456
2334
  },
@@ -3001,11 +2879,11 @@ function uniqueValues(values) {
3001
2879
  async function setupSkills(config) {
3002
2880
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
3003
2881
  const { packageManager, projectDir } = config;
3004
- const btsConfig = await readBtsConfig(projectDir);
3005
- const fullConfig = btsConfig ? {
2882
+ const kubojsConfig = await readKubojsConfig(projectDir);
2883
+ const fullConfig = kubojsConfig ? {
3006
2884
  ...config,
3007
- addons: btsConfig.addons ?? config.addons,
3008
- addonOptions: btsConfig.addonOptions ?? config.addonOptions
2885
+ addons: kubojsConfig.addons ?? config.addons,
2886
+ addonOptions: kubojsConfig.addonOptions ?? config.addonOptions
3009
2887
  } : config;
3010
2888
  const recommendedSourceKeys = getRecommendedSourceKeys(fullConfig);
3011
2889
  const skillsOptions = fullConfig.addonOptions?.skills;
@@ -3694,14 +3572,14 @@ async function runAddonStep(addon, step) {
3694
3572
  if (result.isErr()) cliConsola.error(pc.red(result.error.message));
3695
3573
  }
3696
3574
  async function setupAddons(config) {
3697
- const { addons, frontend, projectDir } = config;
3575
+ const { addons, testing, frontend, projectDir } = config;
3698
3576
  const hasWebFrontend = frontend.some((value) => types_exports.desktopWebFrontends.includes(value));
3699
3577
  if (addons.includes("tauri") && hasWebFrontend) await runSetup(() => setupTauri(config));
3700
3578
  const hasUltracite = addons.includes("ultracite");
3701
- const hasBiome = addons.includes("biome");
3579
+ const hasOxlint = !hasUltracite && addons.includes("oxlint");
3580
+ const hasBiome = !hasUltracite && !hasOxlint && addons.includes("biome");
3702
3581
  const hasHusky = addons.includes("husky");
3703
3582
  const hasLefthook = addons.includes("lefthook");
3704
- const hasOxlint = addons.includes("oxlint");
3705
3583
  const hasVitePlus = addons.includes("vite-plus");
3706
3584
  if (hasUltracite) {
3707
3585
  const gitHooks = [];
@@ -3710,7 +3588,7 @@ async function setupAddons(config) {
3710
3588
  await runSetup(() => setupUltracite(config, gitHooks));
3711
3589
  } else {
3712
3590
  if (hasBiome) await runAddonStep("biome", () => setupBiome(projectDir));
3713
- if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
3591
+ else if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
3714
3592
  if (hasHusky || hasLefthook) {
3715
3593
  let linter;
3716
3594
  if (hasOxlint) linter = "oxlint";
@@ -3727,6 +3605,8 @@ async function setupAddons(config) {
3727
3605
  if (addons.includes("skills")) await runSetup(() => setupSkills(config));
3728
3606
  if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
3729
3607
  if (addons.includes("evlog")) await runSetup(() => setupEvlog(config));
3608
+ if (testing.includes("vitest")) await runAddonStep("vitest", () => setupVitest(projectDir));
3609
+ if (testing.includes("playwright")) await runAddonStep("playwright", () => setupPlaywright(projectDir));
3730
3610
  }
3731
3611
  async function setupBiome(projectDir) {
3732
3612
  await addPackageDependency({
@@ -3769,31 +3649,127 @@ async function setupLefthook(projectDir) {
3769
3649
  });
3770
3650
  }
3771
3651
  //#endregion
3652
+ //#region src/helpers/testing/testing-setup.ts
3653
+ async function updateScripts(projectDir, scripts) {
3654
+ const packageJsonPath = path.join(projectDir, "package.json");
3655
+ if (!await fs.pathExists(packageJsonPath)) return;
3656
+ const packageJson = await fs.readJson(packageJsonPath);
3657
+ packageJson.scripts = {
3658
+ ...packageJson.scripts,
3659
+ ...scripts
3660
+ };
3661
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3662
+ }
3663
+ async function setupTesting(projectDir, testing) {
3664
+ if (testing.includes("vitest")) {
3665
+ await addPackageDependency({
3666
+ devDependencies: ["vitest"],
3667
+ projectDir
3668
+ });
3669
+ await updateScripts(projectDir, {
3670
+ test: "vitest run",
3671
+ "test:watch": "vitest"
3672
+ });
3673
+ }
3674
+ if (testing.includes("playwright")) {
3675
+ await addPackageDependency({
3676
+ devDependencies: ["@playwright/test"],
3677
+ projectDir
3678
+ });
3679
+ await updateScripts(projectDir, { "test:e2e": "playwright test" });
3680
+ }
3681
+ }
3682
+ //#endregion
3683
+ //#region src/utils/config-processing.ts
3684
+ function processArrayOption(options) {
3685
+ if (!options || options.length === 0) return [];
3686
+ if (options.includes("none")) return [];
3687
+ return options.filter((item) => item !== "none");
3688
+ }
3689
+ function deriveProjectName(projectName, projectDirectory) {
3690
+ if (projectName) return projectName;
3691
+ if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
3692
+ return "";
3693
+ }
3694
+ function processFlags(options, projectName) {
3695
+ const config = {};
3696
+ if (options.api) config.api = options.api;
3697
+ if (options.addonOptions) config.addonOptions = options.addonOptions;
3698
+ if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
3699
+ if (options.backend) config.backend = options.backend;
3700
+ if (options.database) config.database = options.database;
3701
+ if (options.orm) config.orm = options.orm;
3702
+ if (options.auth !== void 0) config.auth = options.auth;
3703
+ if (options.payments !== void 0) config.payments = options.payments;
3704
+ if (options.observability !== void 0) config.observability = normalizeObservability(options.observability);
3705
+ if (options.disableObservability) config.observability = [];
3706
+ if (options.communication !== void 0) config.communication = options.communication;
3707
+ if (options.git !== void 0) config.git = options.git;
3708
+ if (options.install !== void 0) config.install = options.install;
3709
+ if (options.runtime) config.runtime = options.runtime;
3710
+ if (options.dbSetup) config.dbSetup = options.dbSetup;
3711
+ if (options.packageManager) config.packageManager = options.packageManager;
3712
+ if (options.webDeploy) config.webDeploy = options.webDeploy;
3713
+ if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
3714
+ const derivedName = deriveProjectName(projectName, options.projectDirectory);
3715
+ if (derivedName) config.projectName = projectName || derivedName;
3716
+ if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
3717
+ if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
3718
+ if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
3719
+ if (options.testing && options.testing.length > 0) config.testing = processArrayOption(options.testing);
3720
+ return config;
3721
+ }
3722
+ function normalizeObservability(value) {
3723
+ if (value === "none" || value === void 0) return [];
3724
+ return [...new Set((Array.isArray(value) ? value : [value]).filter((item) => item === "getmonitor" || item === "himetrica"))];
3725
+ }
3726
+ function getProvidedFlags(options) {
3727
+ return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
3728
+ }
3729
+ function validateNoneExclusivity(options, optionName) {
3730
+ if (!options || options.length === 0) return Result.ok(void 0);
3731
+ if (options.includes("none") && options.length > 1) return Result.err(new ValidationError({ message: `Cannot combine 'none' with other ${optionName}.` }));
3732
+ return Result.ok(void 0);
3733
+ }
3734
+ function validateArrayOptions(options) {
3735
+ if (options.disableObservability && options.observability !== void 0) return Result.err(new ValidationError({ message: "Cannot combine '--disable-observability' with '--observability'." }));
3736
+ const frontendResult = validateNoneExclusivity(options.frontend, "frontend options");
3737
+ if (frontendResult.isErr()) return frontendResult;
3738
+ const addonsResult = validateNoneExclusivity(options.addons, "addons");
3739
+ if (addonsResult.isErr()) return addonsResult;
3740
+ const examplesResult = validateNoneExclusivity(options.examples, "examples");
3741
+ if (examplesResult.isErr()) return examplesResult;
3742
+ const testingResult = validateNoneExclusivity(options.testing, "testing");
3743
+ if (testingResult.isErr()) return testingResult;
3744
+ return Result.ok(void 0);
3745
+ }
3746
+ //#endregion
3772
3747
  //#region src/helpers/core/detect-project-config.ts
3773
3748
  async function detectProjectConfig(projectDir) {
3774
3749
  const result = await Result.tryPromise({
3775
3750
  try: async () => {
3776
- const btsConfig = await readBtsConfig(projectDir);
3777
- if (btsConfig) return {
3751
+ const kubojsConfig = await readKubojsConfig(projectDir);
3752
+ if (kubojsConfig) return {
3778
3753
  projectDir,
3779
3754
  projectName: path.basename(projectDir),
3780
- addonOptions: btsConfig.addonOptions,
3781
- dbSetupOptions: btsConfig.dbSetupOptions,
3782
- database: btsConfig.database,
3783
- orm: btsConfig.orm,
3784
- backend: btsConfig.backend,
3785
- runtime: btsConfig.runtime,
3786
- frontend: btsConfig.frontend,
3787
- addons: btsConfig.addons,
3788
- examples: btsConfig.examples,
3789
- auth: btsConfig.auth,
3790
- payments: btsConfig.payments,
3791
- observability: btsConfig.observability,
3792
- packageManager: btsConfig.packageManager,
3793
- dbSetup: btsConfig.dbSetup,
3794
- api: btsConfig.api,
3795
- webDeploy: btsConfig.webDeploy,
3796
- serverDeploy: btsConfig.serverDeploy
3755
+ addonOptions: kubojsConfig.addonOptions,
3756
+ dbSetupOptions: kubojsConfig.dbSetupOptions,
3757
+ database: kubojsConfig.database,
3758
+ orm: kubojsConfig.orm,
3759
+ backend: kubojsConfig.backend,
3760
+ runtime: kubojsConfig.runtime,
3761
+ frontend: kubojsConfig.frontend,
3762
+ addons: kubojsConfig.addons,
3763
+ examples: kubojsConfig.examples,
3764
+ auth: kubojsConfig.auth,
3765
+ payments: kubojsConfig.payments,
3766
+ observability: normalizeObservability(kubojsConfig.observability),
3767
+ communication: kubojsConfig.communication ?? "none",
3768
+ packageManager: kubojsConfig.packageManager,
3769
+ dbSetup: kubojsConfig.dbSetup,
3770
+ api: kubojsConfig.api,
3771
+ webDeploy: kubojsConfig.webDeploy,
3772
+ serverDeploy: kubojsConfig.serverDeploy
3797
3773
  };
3798
3774
  return null;
3799
3775
  },
@@ -3883,6 +3859,41 @@ function refreshLefthookTemplate(vfs, config) {
3883
3859
  if (!template) return;
3884
3860
  vfs.writeFile("lefthook.yml", processTemplateString(template, config));
3885
3861
  }
3862
+ /** Remove files/deps left by a replaced exclusive linter. */
3863
+ async function cleanupRemovedLinters(projectDir, removedAddons) {
3864
+ const removedLinters = removedAddons.filter(isLinterAddon);
3865
+ if (removedLinters.length === 0) return;
3866
+ const packageJsonPath = path.join(projectDir, "package.json");
3867
+ if (await fs.pathExists(packageJsonPath)) {
3868
+ const packageJson = await fs.readJson(packageJsonPath);
3869
+ packageJson.devDependencies = packageJson.devDependencies ?? {};
3870
+ for (const removed of removedLinters) {
3871
+ if (removed === "biome" || removed === "ultracite") {
3872
+ delete packageJson.devDependencies["@biomejs/biome"];
3873
+ delete packageJson.devDependencies.ultracite;
3874
+ }
3875
+ if (removed === "oxlint" || removed === "ultracite") {
3876
+ delete packageJson.devDependencies.oxlint;
3877
+ delete packageJson.devDependencies.oxfmt;
3878
+ }
3879
+ }
3880
+ if (typeof packageJson.scripts?.check === "string" && removedLinters.some((linter) => {
3881
+ if (linter === "biome") return packageJson.scripts.check.includes("biome");
3882
+ if (linter === "oxlint") return packageJson.scripts.check.includes("oxlint");
3883
+ if (linter === "ultracite") return packageJson.scripts.check.includes("ultracite");
3884
+ return false;
3885
+ })) delete packageJson.scripts.check;
3886
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3887
+ }
3888
+ for (const removed of removedLinters) {
3889
+ if (removed === "biome") await fs.remove(path.join(projectDir, "biome.json"));
3890
+ if (removed === "oxlint") {
3891
+ await fs.remove(path.join(projectDir, ".oxlintrc.json"));
3892
+ await fs.remove(path.join(projectDir, ".oxfmtrc.json"));
3893
+ await fs.remove(path.join(projectDir, ".oxfmtrc.jsonc"));
3894
+ }
3895
+ }
3896
+ }
3886
3897
  function updateViteConfigImportsForVitePlus(vfs) {
3887
3898
  for (const viteConfigPath of ["apps/web/vite.config.ts"]) {
3888
3899
  const content = vfs.readFile(viteConfigPath);
@@ -3925,10 +3936,10 @@ async function addHandlerInternal(input) {
3925
3936
  }));
3926
3937
  if (!isSilent()) {
3927
3938
  renderTitle();
3928
- intro(pc.magenta("Add addons to your I dont know project"));
3939
+ intro(pc.magenta("Add addons to your kubojs project"));
3929
3940
  }
3930
3941
  const existingConfig = await detectProjectConfig(projectDir);
3931
- if (!existingConfig) return Result.err(new CLIError({ message: `No I dont know project found in ${projectDir}. Make sure bts.jsonc exists.` }));
3942
+ if (!existingConfig) return Result.err(new CLIError({ message: `No kubojs project found in ${projectDir}. Make sure kubojs.jsonrc exists.` }));
3932
3943
  if (!isSilent()) log.info(pc.dim(`Detected project: ${existingConfig.projectName}`));
3933
3944
  let addonsToAdd;
3934
3945
  if (input.addons && input.addons.length > 0) {
@@ -3968,10 +3979,13 @@ async function addHandlerInternal(input) {
3968
3979
  }
3969
3980
  addonsToAdd = selectedAddons;
3970
3981
  }
3971
- const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3982
+ const { updatedAddons, removedAddons } = mergeAddonsExclusive(existingConfig.addons, addonsToAdd);
3972
3983
  const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
3973
3984
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
3974
- if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
3985
+ if (!isSilent()) {
3986
+ log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
3987
+ if (removedAddons.length > 0) log.info(pc.dim(`Replacing exclusive addons: ${removedAddons.join(", ")}`));
3988
+ }
3975
3989
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
3976
3990
  const config = {
3977
3991
  projectName: existingConfig.projectName,
@@ -3988,6 +4002,7 @@ async function addHandlerInternal(input) {
3988
4002
  auth: existingConfig.auth,
3989
4003
  payments: existingConfig.payments,
3990
4004
  observability: existingConfig.observability,
4005
+ communication: existingConfig.communication ?? "none",
3991
4006
  git: false,
3992
4007
  packageManager: input.packageManager || existingConfig.packageManager,
3993
4008
  install: input.install ?? false,
@@ -4047,6 +4062,7 @@ async function addHandlerInternal(input) {
4047
4062
  const writeResult = await writeTree(tree, projectDir);
4048
4063
  if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
4049
4064
  if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
4065
+ await cleanupRemovedLinters(projectDir, removedAddons);
4050
4066
  const setupResult = await Result.tryPromise({
4051
4067
  try: () => setupAddons({
4052
4068
  ...config,
@@ -4061,7 +4077,15 @@ async function addHandlerInternal(input) {
4061
4077
  }
4062
4078
  });
4063
4079
  if (setupResult.isErr()) return Result.err(setupResult.error);
4064
- await updateBtsConfig(projectDir, {
4080
+ const testingSetupResult = await Result.tryPromise({
4081
+ try: () => setupTesting(projectDir, updatedTesting),
4082
+ catch: (e) => new CLIError({
4083
+ message: e instanceof Error ? e.message : String(e),
4084
+ cause: e
4085
+ })
4086
+ });
4087
+ if (testingSetupResult.isErr()) return Result.err(testingSetupResult.error);
4088
+ await updateKubojsConfig(projectDir, {
4065
4089
  addons: updatedAddons,
4066
4090
  addonOptions: updatedConfig.addonOptions
4067
4091
  });
@@ -4222,6 +4246,36 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
4222
4246
  return response;
4223
4247
  }
4224
4248
  //#endregion
4249
+ //#region src/prompts/communication.ts
4250
+ async function getCommunicationChoice(communication, backend, previousValue) {
4251
+ if (communication !== void 0) return communication;
4252
+ if (backend === "none") return "none";
4253
+ const options = [
4254
+ {
4255
+ value: "none",
4256
+ label: "None",
4257
+ hint: "No email/communication provider"
4258
+ },
4259
+ {
4260
+ value: "resend",
4261
+ label: "Resend",
4262
+ hint: "Transactional email for developers"
4263
+ },
4264
+ {
4265
+ value: "notifique",
4266
+ label: "Notifique",
4267
+ hint: "Omnichannel BR messaging API"
4268
+ }
4269
+ ];
4270
+ const response = await navigableSelect({
4271
+ message: "Select communication provider",
4272
+ options,
4273
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.communication)
4274
+ });
4275
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4276
+ return response;
4277
+ }
4278
+ //#endregion
4225
4279
  //#region src/prompts/database.ts
4226
4280
  async function getDatabaseChoice(database, backend, runtime, previousValue) {
4227
4281
  if (backend === "convex" || backend === "none") return "none";
@@ -4544,18 +4598,19 @@ async function getinstallChoice(install, previousValue) {
4544
4598
  const options = [{
4545
4599
  value: "getmonitor",
4546
4600
  label: "GetMonitor",
4547
- hint: "Uptime monitoring, alerts, and hosted status pages"
4601
+ hint: "Recommended — JS/TS error tracking for browser and server"
4548
4602
  }, {
4549
- value: "none",
4550
- label: "None",
4551
- hint: "Skip observability setup guidance"
4603
+ value: "himetrica",
4604
+ label: "Himetrica",
4605
+ hint: "Product analytics, errors, and Web Vitals for web apps"
4552
4606
  }];
4553
4607
  async function getObservabilityChoice(observability, previousValue) {
4554
4608
  if (observability !== void 0) return observability;
4555
- const response = await navigableSelect({
4556
- message: "Select observability provider",
4609
+ const response = await navigableMultiselect({
4610
+ message: "Select observability providers",
4557
4611
  options,
4558
- initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.observability)
4612
+ required: false,
4613
+ initialValues: previousValue ?? DEFAULT_CONFIG.observability
4559
4614
  });
4560
4615
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4561
4616
  return response;
@@ -4676,6 +4731,7 @@ function validateBackendNoneConstraints(config, providedFlags) {
4676
4731
  if (has("api") && config.api !== "none") return validationErr("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
4677
4732
  if (has("auth") && config.auth !== "none") return validationErr("Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.");
4678
4733
  if (has("payments") && config.payments !== "none") return validationErr("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
4734
+ if (has("communication") && config.communication !== "none") return validationErr("Backend 'none' requires '--communication none'. Please remove the --communication flag or set it to 'none'.");
4679
4735
  if (has("dbSetup") && config.dbSetup !== "none") return validationErr("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
4680
4736
  if (has("serverDeploy") && config.serverDeploy !== "none") return validationErr("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
4681
4737
  return Result.ok(void 0);
@@ -4737,6 +4793,7 @@ function validateFullConfig(config, providedFlags, options) {
4737
4793
  yield* validateSelfBackendConstraints(config, providedFlags);
4738
4794
  yield* validateBackendConstraints(config, providedFlags, options);
4739
4795
  yield* validateFrontendConstraints(config, providedFlags);
4796
+ yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
4740
4797
  yield* validateApiConstraints(config, options);
4741
4798
  yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
4742
4799
  yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
@@ -4753,6 +4810,7 @@ function validateFullConfig(config, providedFlags, options) {
4753
4810
  }
4754
4811
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4755
4812
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? [], config.orm, config.database);
4813
+ yield* validateCommunicationCompatibility(config.communication, config.backend);
4756
4814
  return Result.ok(void 0);
4757
4815
  });
4758
4816
  }
@@ -4761,7 +4819,9 @@ function validateConfigForProgrammaticUse(config) {
4761
4819
  yield* validateDatabaseOrmAuth(config);
4762
4820
  if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
4763
4821
  yield* validateApiFrontendCompatibility(config.api, config.frontend);
4822
+ yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
4764
4823
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend, config.orm, config.database);
4824
+ yield* validateCommunicationCompatibility(config.communication, config.backend);
4765
4825
  if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
4766
4826
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4767
4827
  return Result.ok(void 0);
@@ -4958,6 +5018,29 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
4958
5018
  return response;
4959
5019
  }
4960
5020
  //#endregion
5021
+ //#region src/prompts/testing.ts
5022
+ async function getTestingChoice(testing, frontends, previousValue) {
5023
+ if (testing !== void 0) return testing;
5024
+ const options = [{
5025
+ value: "vitest",
5026
+ label: "Vitest",
5027
+ hint: "Fast unit test runner"
5028
+ }];
5029
+ if (isPlaywrightAllowed(frontends ?? [])) options.push({
5030
+ value: "playwright",
5031
+ label: "Playwright",
5032
+ hint: "End-to-end browser testing"
5033
+ });
5034
+ const response = await navigableMultiselect({
5035
+ message: "Select testing tools",
5036
+ options,
5037
+ required: false,
5038
+ initialValues: (previousValue ?? DEFAULT_CONFIG.testing)?.filter((t) => options.some((o) => o.value === t))
5039
+ });
5040
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5041
+ return response;
5042
+ }
5043
+ //#endregion
4961
5044
  //#region src/prompts/web-deploy.ts
4962
5045
  function hasWebFrontend(frontends) {
4963
5046
  return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
@@ -5027,8 +5110,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5027
5110
  auth: flags.auth ?? DEFAULT_CONFIG.auth,
5028
5111
  payments: flags.payments ?? DEFAULT_CONFIG.payments,
5029
5112
  observability: flags.observability ?? DEFAULT_CONFIG.observability,
5113
+ communication: flags.communication ?? DEFAULT_CONFIG.communication,
5030
5114
  addons: flags.addons ?? [...DEFAULT_CONFIG.addons],
5031
5115
  examples: flags.examples ?? [...DEFAULT_CONFIG.examples],
5116
+ testing: flags.testing ?? [...DEFAULT_CONFIG.testing],
5032
5117
  git: flags.git ?? DEFAULT_CONFIG.git,
5033
5118
  packageManager: flags.packageManager ?? DEFAULT_CONFIG.packageManager,
5034
5119
  install: flags.install ?? DEFAULT_CONFIG.install,
@@ -5047,6 +5132,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5047
5132
  auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
5048
5133
  payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
5049
5134
  observability: ({ previousAnswer }) => getObservabilityChoice(flags.observability, previousAnswer),
5135
+ communication: ({ results, previousAnswer }) => getCommunicationChoice(flags.communication, results.backend, previousAnswer),
5136
+ testing: ({ results, previousAnswer }) => getTestingChoice(flags.testing, results.frontend, previousAnswer),
5050
5137
  addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
5051
5138
  examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
5052
5139
  dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
@@ -5072,8 +5159,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5072
5159
  auth: result.auth,
5073
5160
  payments: result.payments,
5074
5161
  observability: result.observability,
5162
+ communication: result.communication,
5075
5163
  addons: result.addons,
5076
5164
  examples: result.examples,
5165
+ testing: result.testing,
5077
5166
  git: result.git,
5078
5167
  packageManager: result.packageManager,
5079
5168
  install: result.install,
@@ -5180,7 +5269,8 @@ function displayConfig(config) {
5180
5269
  if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
5181
5270
  if (config.auth !== void 0) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
5182
5271
  if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
5183
- if (config.observability !== void 0) configDisplay.push(`${pc.blue("Observability:")} ${String(config.observability)}`);
5272
+ if (config.communication !== void 0) configDisplay.push(`${pc.blue("Communication:")} ${String(config.communication)}`);
5273
+ if (config.observability !== void 0) configDisplay.push(`${pc.blue("Observability:")} ${config.observability.join(", ") || "none"}`);
5184
5274
  if (config.addons !== void 0) {
5185
5275
  const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
5186
5276
  const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
@@ -5191,6 +5281,11 @@ function displayConfig(config) {
5191
5281
  const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
5192
5282
  configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
5193
5283
  }
5284
+ if (config.testing !== void 0) {
5285
+ const testing = Array.isArray(config.testing) ? config.testing : [config.testing];
5286
+ const testingText = testing.length > 0 && testing[0] !== void 0 ? testing.join(", ") : "none";
5287
+ configDisplay.push(`${pc.blue("Testing:")} ${testingText}`);
5288
+ }
5194
5289
  if (config.git !== void 0) {
5195
5290
  const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
5196
5291
  configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
@@ -5333,7 +5428,8 @@ const TEMPLATE_PRESETS = {
5333
5428
  api: "orpc",
5334
5429
  auth: "better-auth",
5335
5430
  payments: "none",
5336
- observability: "none",
5431
+ observability: ["getmonitor"],
5432
+ communication: "none",
5337
5433
  addons: ["turborepo"],
5338
5434
  examples: ["todo"],
5339
5435
  dbSetup: "mongodb-atlas",
@@ -5349,7 +5445,8 @@ const TEMPLATE_PRESETS = {
5349
5445
  api: "trpc",
5350
5446
  auth: "better-auth",
5351
5447
  payments: "none",
5352
- observability: "none",
5448
+ observability: ["getmonitor"],
5449
+ communication: "none",
5353
5450
  addons: ["turborepo"],
5354
5451
  examples: ["todo"],
5355
5452
  dbSetup: "none",
@@ -5365,7 +5462,8 @@ const TEMPLATE_PRESETS = {
5365
5462
  api: "trpc",
5366
5463
  auth: "better-auth",
5367
5464
  payments: "none",
5368
- observability: "none",
5465
+ observability: ["getmonitor"],
5466
+ communication: "none",
5369
5467
  addons: ["biome", "turborepo"],
5370
5468
  examples: ["none"],
5371
5469
  dbSetup: "none",
@@ -5381,7 +5479,8 @@ const TEMPLATE_PRESETS = {
5381
5479
  api: "none",
5382
5480
  auth: "none",
5383
5481
  payments: "none",
5384
- observability: "none",
5482
+ observability: [],
5483
+ communication: "none",
5385
5484
  addons: ["none"],
5386
5485
  examples: ["none"],
5387
5486
  dbSetup: "none",
@@ -5406,60 +5505,6 @@ function getTemplateDescription(template) {
5406
5505
  }[template] || "";
5407
5506
  }
5408
5507
  //#endregion
5409
- //#region src/utils/config-processing.ts
5410
- function processArrayOption(options) {
5411
- if (!options || options.length === 0) return [];
5412
- if (options.includes("none")) return [];
5413
- return options.filter((item) => item !== "none");
5414
- }
5415
- function deriveProjectName(projectName, projectDirectory) {
5416
- if (projectName) return projectName;
5417
- if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
5418
- return "";
5419
- }
5420
- function processFlags(options, projectName) {
5421
- const config = {};
5422
- if (options.api) config.api = options.api;
5423
- if (options.addonOptions) config.addonOptions = options.addonOptions;
5424
- if (options.dbSetupOptions) config.dbSetupOptions = options.dbSetupOptions;
5425
- if (options.backend) config.backend = options.backend;
5426
- if (options.database) config.database = options.database;
5427
- if (options.orm) config.orm = options.orm;
5428
- if (options.auth !== void 0) config.auth = options.auth;
5429
- if (options.payments !== void 0) config.payments = options.payments;
5430
- if (options.observability !== void 0) config.observability = options.observability;
5431
- if (options.git !== void 0) config.git = options.git;
5432
- if (options.install !== void 0) config.install = options.install;
5433
- if (options.runtime) config.runtime = options.runtime;
5434
- if (options.dbSetup) config.dbSetup = options.dbSetup;
5435
- if (options.packageManager) config.packageManager = options.packageManager;
5436
- if (options.webDeploy) config.webDeploy = options.webDeploy;
5437
- if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
5438
- const derivedName = deriveProjectName(projectName, options.projectDirectory);
5439
- if (derivedName) config.projectName = projectName || derivedName;
5440
- if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
5441
- if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
5442
- if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
5443
- return config;
5444
- }
5445
- function getProvidedFlags(options) {
5446
- return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
5447
- }
5448
- function validateNoneExclusivity(options, optionName) {
5449
- if (!options || options.length === 0) return Result.ok(void 0);
5450
- if (options.includes("none") && options.length > 1) return Result.err(new ValidationError({ message: `Cannot combine 'none' with other ${optionName}.` }));
5451
- return Result.ok(void 0);
5452
- }
5453
- function validateArrayOptions(options) {
5454
- const frontendResult = validateNoneExclusivity(options.frontend, "frontend options");
5455
- if (frontendResult.isErr()) return frontendResult;
5456
- const addonsResult = validateNoneExclusivity(options.addons, "addons");
5457
- if (addonsResult.isErr()) return addonsResult;
5458
- const examplesResult = validateNoneExclusivity(options.examples, "examples");
5459
- if (examplesResult.isErr()) return examplesResult;
5460
- return Result.ok(void 0);
5461
- }
5462
- //#endregion
5463
5508
  //#region src/validation.ts
5464
5509
  const CORE_STACK_FLAGS = new Set([
5465
5510
  "database",
@@ -5469,10 +5514,13 @@ const CORE_STACK_FLAGS = new Set([
5469
5514
  "frontend",
5470
5515
  "addons",
5471
5516
  "examples",
5517
+ "testing",
5472
5518
  "auth",
5473
5519
  "dbSetup",
5474
5520
  "payments",
5475
5521
  "observability",
5522
+ "disableObservability",
5523
+ "communication",
5476
5524
  "api",
5477
5525
  "webDeploy",
5478
5526
  "serverDeploy"
@@ -5525,6 +5573,49 @@ const formatOptions = {
5525
5573
  experimentalSortPackageJson: true,
5526
5574
  experimentalSortImports: { order: "asc" }
5527
5575
  };
5576
+ /** Files that must not be rewritten by post-scaffold formatters. */
5577
+ const SKIP_FILE_NAMES = new Set([
5578
+ "kubojs.jsonrc",
5579
+ "package-lock.json",
5580
+ "pnpm-lock.yaml",
5581
+ "yarn.lock",
5582
+ "bun.lock",
5583
+ "bun.lockb",
5584
+ "Cargo.lock",
5585
+ "composer.lock"
5586
+ ]);
5587
+ const SKIP_EXTENSIONS = new Set([
5588
+ ".png",
5589
+ ".jpg",
5590
+ ".jpeg",
5591
+ ".gif",
5592
+ ".webp",
5593
+ ".ico",
5594
+ ".svg",
5595
+ ".woff",
5596
+ ".woff2",
5597
+ ".ttf",
5598
+ ".eot",
5599
+ ".mp4",
5600
+ ".mp3",
5601
+ ".wasm",
5602
+ ".zip",
5603
+ ".gz",
5604
+ ".tgz",
5605
+ ".br",
5606
+ ".pdf",
5607
+ ".db",
5608
+ ".sqlite"
5609
+ ]);
5610
+ function shouldSkipFile(fileName) {
5611
+ if (SKIP_FILE_NAMES.has(fileName)) return true;
5612
+ const ext = path.extname(fileName).toLowerCase();
5613
+ return SKIP_EXTENSIONS.has(ext);
5614
+ }
5615
+ function shouldUseBiomeFormatter(addons) {
5616
+ if (!addons) return false;
5617
+ return addons.includes("biome") || addons.includes("ultracite");
5618
+ }
5528
5619
  async function formatCode(filePath, content) {
5529
5620
  const result = await Result.tryPromise({
5530
5621
  try: async () => {
@@ -5536,25 +5627,61 @@ async function formatCode(filePath, content) {
5536
5627
  });
5537
5628
  return result.isOk() ? result.value : null;
5538
5629
  }
5539
- async function formatProject(projectDir) {
5630
+ async function formatProjectWithOxfmt(projectDir) {
5631
+ async function formatDirectory(dir) {
5632
+ const entries = await fs.readdir(dir, { withFileTypes: true });
5633
+ await Promise.all(entries.map(async (entry) => {
5634
+ const fullPath = path.join(dir, entry.name);
5635
+ if (entry.isDirectory()) {
5636
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name === ".turbo") return;
5637
+ await formatDirectory(fullPath);
5638
+ } else if (entry.isFile()) {
5639
+ if (shouldSkipFile(entry.name)) return;
5640
+ await Result.tryPromise({
5641
+ try: async () => {
5642
+ const content = await fs.readFile(fullPath, "utf-8");
5643
+ const formatted = await formatCode(fullPath, content);
5644
+ if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
5645
+ },
5646
+ catch: () => void 0
5647
+ });
5648
+ }
5649
+ }));
5650
+ }
5651
+ await formatDirectory(projectDir);
5652
+ }
5653
+ /**
5654
+ * Format with Biome so scaffold matches `biome check` (source of truth).
5655
+ * Uses bunx so it works before local install. Best-effort: failures are ignored
5656
+ * so offline CI/tests still scaffold.
5657
+ */
5658
+ async function formatProjectWithBiome(projectDir) {
5659
+ if (shouldSkipExternalCommands()) return;
5660
+ await Result.tryPromise({
5661
+ try: async () => {
5662
+ await $({
5663
+ cwd: projectDir,
5664
+ reject: false,
5665
+ stdio: "pipe"
5666
+ })`bunx --bun @biomejs/biome@2.5.8 check --write .`;
5667
+ },
5668
+ catch: () => void 0
5669
+ });
5670
+ }
5671
+ /**
5672
+ * Post-scaffold format.
5673
+ * - biome / ultracite → Biome `check --write` (not oxfmt)
5674
+ * - oxlint / none → oxfmt
5675
+ * Always skips kubojs.jsonrc, lockfiles, and binary extensions (oxfmt path).
5676
+ */
5677
+ async function formatProject(projectDir, options = {}) {
5540
5678
  return Result.tryPromise({
5541
5679
  try: async () => {
5542
- async function formatDirectory(dir) {
5543
- const entries = await fs.readdir(dir, { withFileTypes: true });
5544
- await Promise.all(entries.map(async (entry) => {
5545
- const fullPath = path.join(dir, entry.name);
5546
- if (entry.isDirectory()) await formatDirectory(fullPath);
5547
- else if (entry.isFile()) await Result.tryPromise({
5548
- try: async () => {
5549
- const content = await fs.readFile(fullPath, "utf-8");
5550
- const formatted = await formatCode(fullPath, content);
5551
- if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
5552
- },
5553
- catch: () => void 0
5554
- });
5555
- }));
5680
+ if (shouldUseBiomeFormatter(options.addons)) {
5681
+ await formatProjectWithBiome(projectDir);
5682
+ return;
5556
5683
  }
5557
- await formatDirectory(projectDir);
5684
+ await formatProjectWithOxfmt(projectDir);
5558
5685
  },
5559
5686
  catch: (e) => new ProjectCreationError({
5560
5687
  phase: "formatting",
@@ -5564,6 +5691,57 @@ async function formatProject(projectDir) {
5564
5691
  });
5565
5692
  }
5566
5693
  //#endregion
5694
+ //#region src/utils/generate-route-tree.ts
5695
+ const TANSTACK_ROUTE_TREE_FRONTENDS = [
5696
+ "tanstack-router",
5697
+ "tanstack-start",
5698
+ "solid"
5699
+ ];
5700
+ function resolveRouteTreeTarget(frontend) {
5701
+ if (frontend.includes("solid")) return "solid";
5702
+ if (frontend.includes("tanstack-router") || frontend.includes("tanstack-start")) return "react";
5703
+ return null;
5704
+ }
5705
+ function needsRouteTree(frontend) {
5706
+ return frontend.some((value) => TANSTACK_ROUTE_TREE_FRONTENDS.includes(value));
5707
+ }
5708
+ /**
5709
+ * Generate apps/web/src/routeTree.gen.ts from the scaffolded file routes.
5710
+ * TanStack recommends committing this file (runtime source, not a build artifact).
5711
+ * Running at create-time makes `check-types` pass without a prior vite build.
5712
+ */
5713
+ async function generateRouteTreeIfNeeded(projectDir, config) {
5714
+ if (!needsRouteTree(config.frontend)) return Result.ok(void 0);
5715
+ const target = resolveRouteTreeTarget(config.frontend);
5716
+ if (!target) return Result.ok(void 0);
5717
+ const webAppDir = path.join(projectDir, "apps/web");
5718
+ const routesDir = path.join(webAppDir, "src/routes");
5719
+ if (!await fs.pathExists(routesDir)) return Result.ok(void 0);
5720
+ return Result.tryPromise({
5721
+ try: async () => {
5722
+ await new Generator({
5723
+ config: configSchema.parse({
5724
+ target,
5725
+ routesDirectory: path.join(webAppDir, "src/routes"),
5726
+ generatedRouteTree: path.join(webAppDir, "src/routeTree.gen.ts"),
5727
+ tmpDir: path.join(webAppDir, ".tanstack/tmp"),
5728
+ autoCodeSplitting: true,
5729
+ disableLogging: true,
5730
+ enableRouteTreeFormatting: true,
5731
+ quoteStyle: "single",
5732
+ semicolons: false
5733
+ }),
5734
+ root: webAppDir
5735
+ }).run();
5736
+ },
5737
+ catch: (e) => new ProjectCreationError({
5738
+ phase: "route-tree-generation",
5739
+ message: `Failed to generate TanStack route tree: ${e instanceof Error ? e.message : String(e)}`,
5740
+ cause: e
5741
+ })
5742
+ });
5743
+ }
5744
+ //#endregion
5567
5745
  //#region src/utils/env-utils.ts
5568
5746
  async function addEnvVariablesToFile(envPath, variables) {
5569
5747
  let content = "";
@@ -5974,7 +6152,7 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
5974
6152
  function displayManualSetupInstructions$2(target) {
5975
6153
  cliLog.info(`Manual Neon PostgreSQL Setup Instructions:
5976
6154
 
5977
- 1. Get Neon with I dont know referral: https://get.neon.com/sbA3tIe
6155
+ 1. Get Neon with kubojs referral: https://get.neon.com/sbA3tIe
5978
6156
  2. Create a new project from the dashboard
5979
6157
  3. Get your connection string
5980
6158
  4. Add the database URL to the .env file in ${target}/.env
@@ -6053,7 +6231,7 @@ async function setupNeonPostgres(config, cliInput) {
6053
6231
  displayManualSetupInstructions$2(target);
6054
6232
  return Result.ok(void 0);
6055
6233
  }
6056
- cliLog.info(`Get Neon with I dont know referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
6234
+ cliLog.info(`Get Neon with kubojs referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
6057
6235
  return Result.ok(void 0);
6058
6236
  }
6059
6237
  const suggestedProjectName = path.basename(projectDir);
@@ -6095,7 +6273,7 @@ async function setupNeonPostgres(config, cliInput) {
6095
6273
  return envResult;
6096
6274
  }
6097
6275
  finalSpinner.stop("Neon database configured!");
6098
- cliLog.info(`Get Neon with I dont know referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
6276
+ cliLog.info(`Get Neon with kubojs referral: ${pc.cyan("https://get.neon.com/sbA3tIe")}`);
6099
6277
  return Result.ok(void 0);
6100
6278
  }
6101
6279
  //#endregion
@@ -6196,7 +6374,7 @@ const AVAILABLE_REGIONS = [
6196
6374
  label: "US West (N. California)"
6197
6375
  }
6198
6376
  ];
6199
- const CREATE_DB_USER_AGENT = "aman/kubojs";
6377
+ const CREATE_DB_USER_AGENT = "kubojs/kubojs";
6200
6378
  async function setupWithCreateDb(serverDir, packageManager, regionId) {
6201
6379
  cliLog.info("Starting Prisma Postgres setup with create-db.");
6202
6380
  let selectedRegion = regionId;
@@ -6976,7 +7154,7 @@ function getDesktopStaticBuildNote(frontend) {
6976
7154
  return `${pc.yellow("NOTE:")} Desktop builds package static web assets.\n ${staticBuildFrontends.get(staticBuildFrontend)} needs a static/export web build before desktop packaging will work.`;
6977
7155
  }
6978
7156
  async function displayPostInstallInstructions(config) {
6979
- const { api, database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability } = config;
7157
+ const { api, database, relativePath, packageManager, depsInstalled, orm, addons, testing, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability, communication } = config;
6980
7158
  const isConvex = backend === "convex";
6981
7159
  const isBackendSelf = backend === "self";
6982
7160
  const runCmd = packageManager === "npm" ? "npm run" : packageManager === "pnpm" ? "pnpm run" : "bun run";
@@ -6996,22 +7174,22 @@ async function displayPostInstallInstructions(config) {
6996
7174
  const nativeInstructions = (frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles")) && backend !== "none" ? getNativeInstructions(isConvex, isBackendSelf, frontend || [], runCmd) : "";
6997
7175
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
6998
7176
  const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
7177
+ const playwrightInstructions = testing?.includes("playwright") ? getPlaywrightInstructions(runCmd) : "";
6999
7178
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
7000
7179
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
7001
- const getMonitorInstructions = observability === "getmonitor" ? getGetMonitorInstructions() : "";
7180
+ const getMonitorInstructions = observability.includes("getmonitor") ? getGetMonitorInstructions() : "";
7181
+ const resendInstructions = communication === "resend" ? getResendInstructions() : "";
7182
+ const notifiqueInstructions = communication === "notifique" ? getNotifiqueInstructions() : "";
7002
7183
  const hasWeb = frontend?.some((f) => types_exports.desktopWebFrontends.includes(f));
7003
7184
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
7004
- const hasReactRouter = frontend?.includes("react-router");
7005
- const hasSvelte = frontend?.includes("svelte");
7006
- const hasAstro = frontend?.includes("astro");
7007
- const webPort = hasReactRouter || hasSvelte ? "5173" : hasAstro ? "4321" : "3001";
7185
+ const webPort = (0, types_exports.getWebPort)(frontend ?? []);
7008
7186
  const betterAuthConvexInstructions = isConvex && config.auth === "better-auth" ? getBetterAuthConvexInstructions(hasWeb ?? false, webPort, packageManager, runCmd) : "";
7009
7187
  const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
7010
7188
  const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
7011
7189
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
7012
7190
  let stepCounter = 2;
7013
7191
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
7014
- if (database === "sqlite" && dbSetup !== "d1") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local SQLite database)")}\n`;
7192
+ if (database === "sqlite" && dbSetup === "turso") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local Turso/libsql replica; requires Turso CLI)")}\n`;
7015
7193
  if (isConvex) {
7016
7194
  output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(" (this will guide you through Convex project setup)")}\n`;
7017
7195
  output += `${pc.cyan(`${stepCounter++}.`)} Copy environment variables from\n${pc.white(" packages/backend/.env.local")} to ${pc.white("apps/*/.env")}\n`;
@@ -7050,16 +7228,16 @@ async function displayPostInstallInstructions(config) {
7050
7228
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
7051
7229
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
7052
7230
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
7231
+ if (playwrightInstructions) output += `\n${playwrightInstructions.trim()}\n`;
7053
7232
  if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
7054
7233
  if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
7055
7234
  if (getMonitorInstructions) output += `\n${getMonitorInstructions.trim()}\n`;
7235
+ if (resendInstructions) output += `\n${resendInstructions.trim()}\n`;
7236
+ if (notifiqueInstructions) output += `\n${notifiqueInstructions.trim()}\n`;
7056
7237
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
7057
7238
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
7058
7239
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
7059
- const sponsorsResult = await fetchSponsorsQuietly();
7060
- const specialSponsorsSection = sponsorsResult.isOk() ? formatPostInstallSpecialSponsorsSection(sponsorsResult.value) : "";
7061
- if (specialSponsorsSection) output += `\n${specialSponsorsSection.trim()}\n`;
7062
- output += `\n${pc.bold("Like I dont know?")} Please consider giving us a star\n on GitHub:\n`;
7240
+ output += `\n${pc.bold("Like kubojs?")} Please consider giving us a star\n on GitHub:\n`;
7063
7241
  output += pc.cyan("https://github.com/albuquerquesz/kubo");
7064
7242
  cliConsola.box(output);
7065
7243
  }
@@ -7077,6 +7255,9 @@ function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
7077
7255
  function getHuskyInstructions(runCmd) {
7078
7256
  return `${pc.bold("Git hooks with Husky:")}\n${pc.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
7079
7257
  }
7258
+ function getPlaywrightInstructions(runCmd) {
7259
+ return `${pc.bold("End-to-end tests with Playwright:")}\n${pc.cyan("•")} Install browsers: npx playwright install\n${pc.cyan("•")} Run e2e tests: ${`${runCmd} test:e2e`}\n`;
7260
+ }
7080
7261
  function getLintingInstructions(runCmd) {
7081
7262
  return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
7082
7263
  }
@@ -7144,7 +7325,13 @@ function getNoOrmWarning() {
7144
7325
  return `\n${pc.yellow("WARNING:")} Database selected without an ORM. Features requiring\n database access (e.g., examples, auth) need manual setup.`;
7145
7326
  }
7146
7327
  function getGetMonitorInstructions() {
7147
- return `${pc.bold("GetMonitor observability:")}\n${pc.cyan("•")} Deploy first, then create an HTTP monitor for a stable public health endpoint\n${pc.cyan("•")} Add an alert integration and test delivery\n${pc.cyan("•")} Create a status page and add the monitor as a component\n${pc.dim(" https://getmonitor.io/docs/getting-started/introduction/")}`;
7328
+ return `${pc.bold("GetMonitor observability:")}\n${pc.cyan("•")} Keys are optional on first run — the app starts without them; capture stays idle until configured\n${pc.cyan("•")} When ready, set GETMONITOR_API_KEY (public gm_xxx) in the web and/or server .env\n${pc.cyan("•")} Ingestion host is fixed (http://ingest.getmonitor.io) — no apiHost env var\n${pc.cyan("•")} Optional: set GETMONITOR_AUTH_TOKEN for Next/Nuxt source-map upload on production builds\n${pc.cyan("•")} Browser/Node capture is automatic; React trees include GetMonitorErrorBoundary\n${pc.cyan("•")} Opt out next time with --disable-observability\n${pc.dim(" https://github.com/get-monitor/getmonitor-js")}`;
7329
+ }
7330
+ function getResendInstructions() {
7331
+ return `${pc.bold("Resend communication:")}\n${pc.cyan("•")} Keys are optional on first run — sendEmail throws only when called without RESEND_API_KEY\n${pc.cyan("•")} Set RESEND_API_KEY (and optional RESEND_FROM_EMAIL) in the server .env\n${pc.cyan("•")} Import sendEmail from packages/email (e.g. @project/email)\n${pc.cyan("•")} Replace onboarding@resend.dev with a verified domain sender for production\n${pc.dim(" https://resend.com/docs/send-with-nodejs")}`;
7332
+ }
7333
+ function getNotifiqueInstructions() {
7334
+ return `${pc.bold("Notifique communication:")}\n${pc.cyan("•")} Keys are optional on first run — helpers throw only when called without NOTIFIQUE_API_KEY\n${pc.cyan("•")} Set NOTIFIQUE_API_KEY (sk_live_… / sk_test_…) in the server .env\n${pc.cyan("•")} Import sendSms / sendWhatsAppText / sendEmail from packages/notifique\n${pc.cyan("•")} Auth is Bearer only — do not send x-workspace-id\n${pc.dim(" https://docs.notifique.dev/skill.md")}`;
7148
7335
  }
7149
7336
  function getBunWebNativeWarning() {
7150
7337
  return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
@@ -7275,6 +7462,7 @@ async function createProject(options, cliInput = {}) {
7275
7462
  message: e.message,
7276
7463
  cause: e
7277
7464
  }))));
7465
+ yield* Result.await(generateRouteTreeIfNeeded(projectDir, options));
7278
7466
  yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager));
7279
7467
  if (!isConvex && options.database !== "none") yield* Result.await(Result.tryPromise({
7280
7468
  try: () => setupDatabase(options, cliInput),
@@ -7284,7 +7472,9 @@ async function createProject(options, cliInput = {}) {
7284
7472
  cause: e
7285
7473
  })
7286
7474
  }));
7287
- if (options.addons.length > 0 && options.addons[0] !== "none") yield* Result.await(Result.tryPromise({
7475
+ const hasAddons = options.addons.some((addon) => addon !== "none");
7476
+ const hasTesting = options.testing.some((tool) => tool !== "none");
7477
+ if (hasAddons) yield* Result.await(Result.tryPromise({
7288
7478
  try: () => setupAddons(options),
7289
7479
  catch: (e) => new ProjectCreationError({
7290
7480
  phase: "addons-setup",
@@ -7292,7 +7482,15 @@ async function createProject(options, cliInput = {}) {
7292
7482
  cause: e
7293
7483
  })
7294
7484
  }));
7295
- yield* Result.await(formatProject(projectDir));
7485
+ if (hasTesting) yield* Result.await(Result.tryPromise({
7486
+ try: () => setupTesting(projectDir, options.testing),
7487
+ catch: (e) => new ProjectCreationError({
7488
+ phase: "testing-setup",
7489
+ message: String(e),
7490
+ cause: e
7491
+ })
7492
+ }));
7493
+ yield* Result.await(formatProject(projectDir, { addons: options.addons }));
7296
7494
  if (!isSilent()) log.success("Project template successfully scaffolded!");
7297
7495
  if (options.install) yield* Result.await(installDependencies({
7298
7496
  projectDir,
@@ -7306,21 +7504,40 @@ async function createProject(options, cliInput = {}) {
7306
7504
  return Result.ok(projectDir);
7307
7505
  });
7308
7506
  }
7309
- async function setPackageManagerVersion(projectDir, packageManager) {
7310
- const pkgJsonPath = path.join(projectDir, "package.json");
7311
- if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
7507
+ /**
7508
+ * Corepack/Turbo require `name@x.y.z`. Placeholders like `@latest` are invalid.
7509
+ */
7510
+ function isValidPackageManagerField(value) {
7511
+ return /^(bun|npm|pnpm|yarn)@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
7512
+ }
7513
+ function normalizePackageManagerVersion(version) {
7514
+ const cleaned = version.replace(/^v/i, "").trim();
7515
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(cleaned)) return null;
7516
+ return cleaned;
7517
+ }
7518
+ async function resolveInstalledPackageManagerVersion(packageManager) {
7519
+ if (packageManager === "bun" && typeof process.versions.bun === "string" && process.versions.bun.length > 0) return normalizePackageManagerVersion(process.versions.bun);
7312
7520
  const versionResult = await Result.tryPromise({
7313
7521
  try: async () => {
7314
7522
  const { stdout } = await $({ cwd: os.tmpdir() })`${packageManager} -v`;
7315
- return stdout.trim();
7523
+ return normalizePackageManagerVersion(stdout);
7316
7524
  },
7317
7525
  catch: () => null
7318
7526
  });
7527
+ return versionResult.isOk() ? versionResult.value : null;
7528
+ }
7529
+ async function setPackageManagerVersion(projectDir, packageManager) {
7530
+ const pkgJsonPath = path.join(projectDir, "package.json");
7531
+ if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
7532
+ const version = await resolveInstalledPackageManagerVersion(packageManager);
7319
7533
  return Result.tryPromise({
7320
7534
  try: async () => {
7321
7535
  const pkgJson = await fs.readJson(pkgJsonPath);
7322
- if (versionResult.isOk() && versionResult.value) pkgJson.packageManager = `${packageManager}@${versionResult.value}`;
7323
- else delete pkgJson.packageManager;
7536
+ if (version) {
7537
+ const packageManagerField = `${packageManager}@${version}`;
7538
+ if (isValidPackageManagerField(packageManagerField)) pkgJson.packageManager = packageManagerField;
7539
+ else delete pkgJson.packageManager;
7540
+ } else delete pkgJson.packageManager;
7324
7541
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
7325
7542
  },
7326
7543
  catch: (e) => new ProjectCreationError({
@@ -7349,9 +7566,11 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
7349
7566
  frontend: [],
7350
7567
  addons: [],
7351
7568
  examples: [],
7569
+ testing: [],
7352
7570
  auth: "none",
7353
7571
  payments: "none",
7354
- observability: "none",
7572
+ observability: [],
7573
+ communication: "none",
7355
7574
  git: false,
7356
7575
  packageManager: "npm",
7357
7576
  install: false,
@@ -7389,7 +7608,7 @@ async function createProjectHandler(input, options = {}) {
7389
7608
  async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7390
7609
  return Result.gen(async function* () {
7391
7610
  if (!isSilent() && input.renderTitle !== false) renderTitle();
7392
- if (!isSilent()) intro(pc.magenta("Creating a new I dont know project"));
7611
+ if (!isSilent()) intro(pc.magenta("Creating a new kubojs project"));
7393
7612
  if (!isSilent() && input.yolo) cliConsola.fatal("YOLO mode enabled - skipping checks. Things may break!");
7394
7613
  let currentPathInput;
7395
7614
  if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
@@ -7523,6 +7742,8 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7523
7742
  if (!input.yolo) {
7524
7743
  const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
7525
7744
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
7745
+ const testingValidationResult = validateTestingAgainstFrontends(config.testing, config.frontend);
7746
+ if (testingValidationResult.isErr()) return Result.err(new CLIError({ message: testingValidationResult.error.message }));
7526
7747
  }
7527
7748
  const reproducibleCommand = generateReproducibleCommand(config);
7528
7749
  if (input.dryRun) {
@@ -7655,12 +7876,14 @@ const SchemaNameSchema = z.enum([
7655
7876
  "frontend",
7656
7877
  "addons",
7657
7878
  "examples",
7879
+ "testing",
7658
7880
  "packageManager",
7659
7881
  "databaseSetup",
7660
7882
  "api",
7661
7883
  "auth",
7662
7884
  "payments",
7663
7885
  "observability",
7886
+ "communication",
7664
7887
  "webDeploy",
7665
7888
  "serverDeploy",
7666
7889
  "directoryConflict",
@@ -7670,8 +7893,8 @@ const SchemaNameSchema = z.enum([
7670
7893
  "createInput",
7671
7894
  "addInput",
7672
7895
  "projectConfig",
7673
- "betterTStackConfig",
7674
- "betterTStackConfigFile",
7896
+ "kubojsConfig",
7897
+ "kubojsConfigFile",
7675
7898
  "initResult"
7676
7899
  ]).default("all");
7677
7900
  const t = initTRPC.meta().create();
@@ -7693,7 +7916,7 @@ function getSchemaResult(name) {
7693
7916
  }
7694
7917
  const router = t.router({
7695
7918
  create: t.procedure.meta({
7696
- description: "Create a new I dont know project",
7919
+ description: "Create a new kubojs project",
7697
7920
  default: true,
7698
7921
  negateBooleans: true
7699
7922
  }).input(z.tuple([types_exports.ProjectNameSchema.optional(), z.object({
@@ -7707,9 +7930,12 @@ const router = t.router({
7707
7930
  auth: types_exports.AuthSchema.optional(),
7708
7931
  payments: types_exports.PaymentsSchema.optional(),
7709
7932
  observability: types_exports.ObservabilitySchema.optional(),
7933
+ disableObservability: z.boolean().optional().describe("Disable all observability providers"),
7934
+ communication: types_exports.CommunicationSchema.optional(),
7710
7935
  frontend: z.array(types_exports.FrontendSchema).optional(),
7711
7936
  addons: z.array(types_exports.AddonsSchema).optional(),
7712
7937
  examples: z.array(types_exports.ExamplesSchema).optional(),
7938
+ testing: z.array(types_exports.TestingSchema).optional(),
7713
7939
  git: z.boolean().optional(),
7714
7940
  packageManager: types_exports.PackageManagerSchema.optional(),
7715
7941
  install: z.boolean().optional(),
@@ -7742,11 +7968,11 @@ const router = t.router({
7742
7968
  return result;
7743
7969
  }),
7744
7970
  schema: t.procedure.meta({ description: "Show runtime CLI and input schemas as JSON" }).input(z.object({ name: SchemaNameSchema.describe("Schema name to inspect") })).query(({ input }) => getSchemaResult(input.name)),
7745
- sponsors: t.procedure.meta({ description: "Show I dont know sponsors" }).mutation(() => showSponsorsCommand()),
7746
- docs: t.procedure.meta({ description: "Open I dont know documentation" }).mutation(() => openDocsCommand()),
7971
+ docs: t.procedure.meta({ description: "Open kubojs documentation" }).mutation(() => openDocsCommand()),
7747
7972
  builder: t.procedure.meta({ description: "Open the web-based stack builder" }).mutation(() => openBuilderCommand()),
7748
- add: t.procedure.meta({ description: "Add addons to an existing I dont know project" }).input(z.object({
7973
+ add: t.procedure.meta({ description: "Add addons to an existing kubojs project" }).input(z.object({
7749
7974
  addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
7975
+ testing: z.array(types_exports.TestingSchema).optional().describe("Testing tools to add"),
7750
7976
  install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
7751
7977
  packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
7752
7978
  projectDir: z.string().optional().describe("Project directory (defaults to current)")
@@ -7778,7 +8004,7 @@ function createBtsCli() {
7778
8004
  });
7779
8005
  }
7780
8006
  /**
7781
- * Programmatic API to create a new I dont know project.
8007
+ * Programmatic API to create a new kubojs project.
7782
8008
  * Returns a Result type - no console output, no interactive prompts.
7783
8009
  *
7784
8010
  * @example
@@ -7829,9 +8055,6 @@ async function create(projectName, options) {
7829
8055
  }
7830
8056
  });
7831
8057
  }
7832
- async function sponsors() {
7833
- return showSponsorsCommand();
7834
- }
7835
8058
  async function docs() {
7836
8059
  return openDocsCommand();
7837
8060
  }
@@ -7875,9 +8098,11 @@ async function createVirtual(options) {
7875
8098
  frontend: options.frontend || ["tanstack-router"],
7876
8099
  addons: options.addons || [],
7877
8100
  examples: options.examples || [],
8101
+ testing: options.testing || [],
7878
8102
  auth: options.auth || "none",
7879
8103
  payments: options.payments || "none",
7880
- observability: options.observability || "none",
8104
+ observability: options.disableObservability ? [] : options.observability || [],
8105
+ communication: options.communication || "none",
7881
8106
  git: options.git ?? false,
7882
8107
  packageManager: options.packageManager || "bun",
7883
8108
  install: false,
@@ -7894,10 +8119,13 @@ async function createVirtual(options) {
7894
8119
  "frontend",
7895
8120
  "addons",
7896
8121
  "examples",
8122
+ "testing",
7897
8123
  "auth",
7898
8124
  "dbSetup",
7899
8125
  "payments",
7900
8126
  "observability",
8127
+ "disableObservability",
8128
+ "communication",
7901
8129
  "api",
7902
8130
  "webDeploy",
7903
8131
  "serverDeploy"
@@ -7913,7 +8141,7 @@ async function createVirtual(options) {
7913
8141
  });
7914
8142
  }
7915
8143
  /**
7916
- * Programmatic API to add addons to an existing I dont know project.
8144
+ * Programmatic API to add addons to an existing kubojs project.
7917
8145
  *
7918
8146
  * @example
7919
8147
  * ```typescript
@@ -7933,4 +8161,4 @@ async function add(options = {}) {
7933
8161
  return addHandler(options, { silent: true });
7934
8162
  }
7935
8163
  //#endregion
7936
- export { ProjectCreationError as C, DirectoryConflictError as S, ValidationError as T, types_exports as _, TEMPLATE_COUNT as a, CompatibilityError as b, builder as c, createVirtual as d, docs as f, sponsors as g, router as h, SchemaNameSchema as i, create as l, getSchemaResult as m, GeneratorError$1 as n, VirtualFileSystem$1 as o, generate$1 as p, Result$1 as r, add as s, EMBEDDED_TEMPLATES$1 as t, createBtsCli as u, getLatestCLIVersion as v, UserCancelledError as w, DatabaseSetupError as x, CLIError as y };
8164
+ export { UserCancelledError as C, ProjectCreationError as S, getLatestCLIVersion as _, TEMPLATE_COUNT as a, DatabaseSetupError as b, builder as c, createVirtual as d, docs as f, types_exports as g, router as h, SchemaNameSchema as i, create as l, getSchemaResult as m, GeneratorError$1 as n, VirtualFileSystem$1 as o, generate$1 as p, Result$1 as r, add as s, EMBEDDED_TEMPLATES$1 as t, createBtsCli as u, CLIError as v, ValidationError as w, DirectoryConflictError as x, CompatibilityError as y };