create-better-t-stack 3.20.2 → 3.21.0-pr892.a7e1b0f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,7 +58,7 @@ Options:
58
58
  --auth Include authentication
59
59
  --no-auth Exclude authentication
60
60
  --frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-bare, native-uniwind, native-unistyles, none)
61
- --addons <types...> Additional addons (pwa, tauri, starlight, biome, lefthook, husky, turborepo, fumadocs, ultracite, oxlint, none)
61
+ --addons <types...> Additional addons (pwa, tauri, starlight, fumadocs, biome, lefthook, husky, turborepo, ultracite, oxlint, ruler, opentui, wxt, skills, mcp, none)
62
62
  --examples <types...> Examples to include (todo, ai, none)
63
63
  --git Initialize git repository
64
64
  --no-git Skip git initialization
@@ -74,6 +74,14 @@ Options:
74
74
  -h, --help Display help
75
75
  ```
76
76
 
77
+ Additional commands:
78
+
79
+ ```bash
80
+ create-better-t-stack add [options] # Add addons to an existing project
81
+ create-better-t-stack history [options] # Show or clear local scaffold history
82
+ create-better-t-stack mcp # Start MCP server over stdio
83
+ ```
84
+
77
85
  ## Telemetry
78
86
 
79
87
  This CLI collects anonymous usage data to help improve the tool. The data collected includes:
package/dist/cli.mjs CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { l as createBtsCli } from "./src-BqpeME-D.mjs";
2
+ import { h as startMcpServer, l as createBtsCli } from "./src-Dtzva6-_.mjs";
3
3
 
4
4
  //#region src/cli.ts
5
- createBtsCli().run();
5
+ if (process.argv[2] === "mcp") startMcpServer().catch((error) => {
6
+ console.error("Failed to start MCP server:", error);
7
+ process.exit(1);
8
+ });
9
+ else createBtsCli().run();
6
10
 
7
11
  //#endregion
8
12
  export { };
package/dist/index.d.mts CHANGED
@@ -287,6 +287,7 @@ declare const router: {
287
287
  clear: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
288
288
  json: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
289
289
  }, z.core.$strip>, _orpc_server0.Schema<void, void>, _orpc_server0.MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
290
+ mcp: _orpc_server0.Procedure<_orpc_server0.MergedInitialContext<Record<never, never>, Record<never, never>, Record<never, never>>, Record<never, never>, _orpc_server0.Schema<unknown, unknown>, _orpc_server0.Schema<void, void>, _orpc_server0.MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
290
291
  };
291
292
  declare function createBtsCli(): trpc_cli0.TrpcCli;
292
293
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { _ as DatabaseSetupError, a as VirtualFileSystem, b as UserCancelledError, c as create, d as docs, f as generate, g as CompatibilityError, h as CLIError, i as TEMPLATE_COUNT, l as createBtsCli, m as sponsors, n as GeneratorError, o as add, p as router, r as Result, s as builder, t as EMBEDDED_TEMPLATES, u as createVirtual, v as DirectoryConflictError, x as ValidationError, y as ProjectCreationError } from "./src-BqpeME-D.mjs";
2
+ import { S as ValidationError, _ as CompatibilityError, a as VirtualFileSystem, b as ProjectCreationError, c as create, d as docs, f as generate, g as CLIError, i as TEMPLATE_COUNT, l as createBtsCli, m as sponsors, n as GeneratorError, o as add, p as router, r as Result, s as builder, t as EMBEDDED_TEMPLATES, u as createVirtual, v as DatabaseSetupError, x as UserCancelledError, y as DirectoryConflictError } from "./src-Dtzva6-_.mjs";
3
3
 
4
4
  export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, Result, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, router, sponsors };
@@ -20,6 +20,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
20
20
  import { applyEdits, modify, parse } from "jsonc-parser";
21
21
  import os$1 from "node:os";
22
22
  import { format } from "oxfmt";
23
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
25
+ import * as z$1 from "zod/v4";
23
26
 
24
27
  //#region src/utils/get-package-manager.ts
25
28
  const getUserPkgManager = () => {
@@ -199,7 +202,7 @@ function getLatestCLIVersionResult() {
199
202
  });
200
203
  }
201
204
  function getLatestCLIVersion() {
202
- return getLatestCLIVersionResult().unwrapOr("1.0.0");
205
+ return getLatestCLIVersionResult().unwrapOr("1.0.0") ?? "1.0.0";
203
206
  }
204
207
 
205
208
  //#endregion
@@ -1336,20 +1339,33 @@ const TEMPLATES$2 = {
1336
1339
  value: "tanstack-start-spa"
1337
1340
  }
1338
1341
  };
1339
- async function setupFumadocs(config) {
1340
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
1342
+ async function setupFumadocs(config, context = {}) {
1343
+ const emit = context.collectExternalReport;
1344
+ if (shouldSkipExternalCommands()) {
1345
+ emit?.({
1346
+ addon: "fumadocs",
1347
+ status: "skipped",
1348
+ warning: "Skipped because BTS_SKIP_EXTERNAL_COMMANDS or BTS_TEST_MODE is enabled."
1349
+ });
1350
+ return Result.ok(void 0);
1351
+ }
1341
1352
  const { packageManager, projectDir } = config;
1353
+ const isInteractive = context.interactive ?? true;
1342
1354
  log.info("Setting up Fumadocs...");
1343
- const template = await select({
1344
- message: "Choose a template",
1345
- options: Object.entries(TEMPLATES$2).map(([key, template]) => ({
1346
- value: key,
1347
- label: template.label,
1348
- hint: template.hint
1349
- })),
1350
- initialValue: "next-mdx"
1351
- });
1352
- if (isCancel(template)) return userCancelled("Operation cancelled");
1355
+ let template = context.addonOptions?.fumadocs?.template ?? "next-mdx";
1356
+ if (isInteractive) {
1357
+ const selectedTemplate = await select({
1358
+ message: "Choose a template",
1359
+ options: Object.entries(TEMPLATES$2).map(([key, template]) => ({
1360
+ value: key,
1361
+ label: template.label,
1362
+ hint: template.hint
1363
+ })),
1364
+ initialValue: template
1365
+ });
1366
+ if (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
1367
+ template = selectedTemplate;
1368
+ }
1353
1369
  const templateArg = TEMPLATES$2[template].value;
1354
1370
  const isNextTemplate = template.startsWith("next-");
1355
1371
  const options = [
@@ -1372,12 +1388,14 @@ async function setupFumadocs(config) {
1372
1388
  })`${args}`;
1373
1389
  const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
1374
1390
  const packageJsonPath = path.join(fumadocsDir, "package.json");
1375
- if (await fs.pathExists(packageJsonPath)) {
1376
- const packageJson = await fs.readJson(packageJsonPath);
1377
- packageJson.name = "fumadocs";
1378
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
1379
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1380
- }
1391
+ if (!await fs.pathExists(packageJsonPath)) throw new AddonSetupError({
1392
+ addon: "fumadocs",
1393
+ message: "Fumadocs generator did not create apps/fumadocs/package.json. Upstream template shape may have changed."
1394
+ });
1395
+ const packageJson = await fs.readJson(packageJsonPath);
1396
+ packageJson.name = "fumadocs";
1397
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
1398
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
1381
1399
  },
1382
1400
  catch: (e) => new AddonSetupError({
1383
1401
  addon: "fumadocs",
@@ -1387,9 +1405,24 @@ async function setupFumadocs(config) {
1387
1405
  });
1388
1406
  if (result.isErr()) {
1389
1407
  s.stop("Failed to set up Fumadocs");
1408
+ emit?.({
1409
+ addon: "fumadocs",
1410
+ status: "failed",
1411
+ selectedOptions: { template },
1412
+ commands: [args.join(" ")],
1413
+ postChecks: ["apps/fumadocs/package.json exists", "apps/fumadocs/package.json scripts.dev uses --port=4000"],
1414
+ error: result.error.message
1415
+ });
1390
1416
  return result;
1391
1417
  }
1392
1418
  s.stop("Fumadocs setup complete!");
1419
+ emit?.({
1420
+ addon: "fumadocs",
1421
+ status: "success",
1422
+ selectedOptions: { template },
1423
+ commands: [args.join(" ")],
1424
+ postChecks: ["apps/fumadocs/package.json exists", "apps/fumadocs/package.json scripts.dev uses --port=4000"]
1425
+ });
1393
1426
  return Result.ok(void 0);
1394
1427
  }
1395
1428
 
@@ -1554,24 +1587,37 @@ function getRecommendedMcpServers(config) {
1554
1587
  function filterAgentsForScope(scope) {
1555
1588
  return MCP_AGENTS.filter((a) => a.scope === "both" || a.scope === scope);
1556
1589
  }
1557
- async function setupMcp(config) {
1558
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
1590
+ async function setupMcp(config, context = {}) {
1591
+ const emit = context.collectExternalReport;
1592
+ if (shouldSkipExternalCommands()) {
1593
+ emit?.({
1594
+ addon: "mcp",
1595
+ status: "skipped",
1596
+ warning: "Skipped because BTS_SKIP_EXTERNAL_COMMANDS or BTS_TEST_MODE is enabled."
1597
+ });
1598
+ return Result.ok(void 0);
1599
+ }
1559
1600
  const { packageManager, projectDir } = config;
1601
+ const isInteractive = context.interactive ?? true;
1560
1602
  log.info("Setting up MCP servers...");
1561
- const scope = await select({
1562
- message: "Where should MCP servers be installed?",
1563
- options: [{
1564
- value: "project",
1565
- label: "Project",
1566
- hint: "Writes to project config files (recommended for teams)"
1567
- }, {
1568
- value: "global",
1569
- label: "Global",
1570
- hint: "Writes to user-level config files (personal machine)"
1571
- }],
1572
- initialValue: "project"
1573
- });
1574
- if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1603
+ let scope = context.addonOptions?.mcp?.scope ?? "project";
1604
+ if (isInteractive) {
1605
+ const selectedScope = await select({
1606
+ message: "Where should MCP servers be installed?",
1607
+ options: [{
1608
+ value: "project",
1609
+ label: "Project",
1610
+ hint: "Writes to project config files (recommended for teams)"
1611
+ }, {
1612
+ value: "global",
1613
+ label: "Global",
1614
+ hint: "Writes to user-level config files (personal machine)"
1615
+ }],
1616
+ initialValue: scope
1617
+ });
1618
+ if (isCancel(selectedScope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1619
+ scope = selectedScope;
1620
+ }
1575
1621
  const recommendedServers = getRecommendedMcpServers(config);
1576
1622
  if (recommendedServers.length === 0) return Result.ok(void 0);
1577
1623
  const serverOptions = recommendedServers.map((s) => ({
@@ -1579,37 +1625,77 @@ async function setupMcp(config) {
1579
1625
  label: s.label,
1580
1626
  hint: s.target
1581
1627
  }));
1582
- const selectedServerKeys = await multiselect({
1583
- message: "Select MCP servers to install",
1584
- options: serverOptions,
1585
- required: false,
1586
- initialValues: serverOptions.map((o) => o.value)
1587
- });
1588
- if (isCancel(selectedServerKeys)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1589
- if (selectedServerKeys.length === 0) return Result.ok(void 0);
1628
+ let selectedServerKeys = context.addonOptions?.mcp?.serverKeys?.length === 0 ? [] : context.addonOptions?.mcp?.serverKeys ?? serverOptions.map((o) => o.value);
1629
+ if (isInteractive) {
1630
+ const selectedServersResult = await multiselect({
1631
+ message: "Select MCP servers to install",
1632
+ options: serverOptions,
1633
+ required: false,
1634
+ initialValues: selectedServerKeys
1635
+ });
1636
+ if (isCancel(selectedServersResult)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1637
+ selectedServerKeys = selectedServersResult;
1638
+ }
1639
+ if (selectedServerKeys.length === 0) {
1640
+ emit?.({
1641
+ addon: "mcp",
1642
+ status: "warning",
1643
+ selectedOptions: { scope },
1644
+ warning: "No MCP servers were selected for installation."
1645
+ });
1646
+ return Result.ok(void 0);
1647
+ }
1590
1648
  const agentOptions = filterAgentsForScope(scope).map((a) => ({
1591
1649
  value: a.value,
1592
1650
  label: a.label
1593
1651
  }));
1594
- const selectedAgents = await multiselect({
1595
- message: "Select agents to install MCP servers to",
1596
- options: agentOptions,
1597
- required: false,
1598
- initialValues: uniqueValues$1([
1599
- "cursor",
1600
- "claude-code",
1601
- "vscode"
1602
- ].filter((a) => agentOptions.some((o) => o.value === a)))
1603
- });
1604
- if (isCancel(selectedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1605
- if (selectedAgents.length === 0) return Result.ok(void 0);
1652
+ const defaultAgents = uniqueValues$1([
1653
+ "cursor",
1654
+ "claude-code",
1655
+ "vscode"
1656
+ ].filter((a) => agentOptions.some((o) => o.value === a)));
1657
+ let selectedAgents = uniqueValues$1((context.addonOptions?.mcp?.agents ?? defaultAgents).filter((agent) => agentOptions.some((option) => option.value === agent)));
1658
+ if (isInteractive) {
1659
+ const selectedAgentsResult = await multiselect({
1660
+ message: "Select agents to install MCP servers to",
1661
+ options: agentOptions,
1662
+ required: false,
1663
+ initialValues: selectedAgents
1664
+ });
1665
+ if (isCancel(selectedAgentsResult)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
1666
+ selectedAgents = selectedAgentsResult;
1667
+ }
1668
+ if (selectedAgents.length === 0) {
1669
+ emit?.({
1670
+ addon: "mcp",
1671
+ status: "warning",
1672
+ selectedOptions: {
1673
+ scope,
1674
+ selectedServerKeys
1675
+ },
1676
+ warning: "No agents were selected for MCP server installation."
1677
+ });
1678
+ return Result.ok(void 0);
1679
+ }
1606
1680
  const serversByKey = new Map(recommendedServers.map((s) => [s.key, s]));
1607
1681
  const selectedServers = [];
1608
1682
  for (const key of selectedServerKeys) {
1609
1683
  const server = serversByKey.get(key);
1610
1684
  if (server) selectedServers.push(server);
1611
1685
  }
1612
- if (selectedServers.length === 0) return Result.ok(void 0);
1686
+ if (selectedServers.length === 0) {
1687
+ emit?.({
1688
+ addon: "mcp",
1689
+ status: "warning",
1690
+ selectedOptions: {
1691
+ scope,
1692
+ selectedServerKeys,
1693
+ selectedAgents
1694
+ },
1695
+ warning: "No matching recommended MCP servers were found for the selected keys."
1696
+ });
1697
+ return Result.ok(void 0);
1698
+ }
1613
1699
  const installSpinner = spinner();
1614
1700
  installSpinner.start("Installing MCP servers...");
1615
1701
  const runner = getPackageRunnerPrefix(packageManager);
@@ -1630,7 +1716,8 @@ async function setupMcp(config) {
1630
1716
  ...globalFlags,
1631
1717
  "-y"
1632
1718
  ];
1633
- if ((await Result.tryPromise({
1719
+ const postChecks = [`MCP agent configs updated with server '${server.name}'`];
1720
+ const installResult = await Result.tryPromise({
1634
1721
  try: async () => {
1635
1722
  await $({
1636
1723
  cwd: projectDir,
@@ -1642,7 +1729,36 @@ async function setupMcp(config) {
1642
1729
  message: `Failed to install MCP server '${server.name}': ${e instanceof Error ? e.message : String(e)}`,
1643
1730
  cause: e
1644
1731
  })
1645
- })).isErr()) log.warn(pc.yellow(`Warning: Could not install MCP server '${server.name}'`));
1732
+ });
1733
+ if (installResult.isErr()) {
1734
+ log.warn(pc.yellow(`Warning: Could not install MCP server '${server.name}'`));
1735
+ emit?.({
1736
+ addon: "mcp",
1737
+ status: "warning",
1738
+ selectedOptions: {
1739
+ scope,
1740
+ server: server.key,
1741
+ serverName: server.name,
1742
+ agents: selectedAgents
1743
+ },
1744
+ commands: [args.join(" ")],
1745
+ postChecks,
1746
+ warning: installResult.error.message
1747
+ });
1748
+ continue;
1749
+ }
1750
+ emit?.({
1751
+ addon: "mcp",
1752
+ status: "success",
1753
+ selectedOptions: {
1754
+ scope,
1755
+ server: server.key,
1756
+ serverName: server.name,
1757
+ agents: selectedAgents
1758
+ },
1759
+ commands: [args.join(" ")],
1760
+ postChecks
1761
+ });
1646
1762
  }
1647
1763
  installSpinner.stop("MCP servers installed");
1648
1764
  return Result.ok(void 0);
@@ -1994,9 +2110,18 @@ function getCuratedSkillNamesForSourceKey(sourceKey, config) {
1994
2110
  function uniqueValues(values) {
1995
2111
  return Array.from(new Set(values));
1996
2112
  }
1997
- async function setupSkills(config) {
1998
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2113
+ async function setupSkills(config, context = {}) {
2114
+ const emit = context.collectExternalReport;
2115
+ if (shouldSkipExternalCommands()) {
2116
+ emit?.({
2117
+ addon: "skills",
2118
+ status: "skipped",
2119
+ warning: "Skipped because BTS_SKIP_EXTERNAL_COMMANDS or BTS_TEST_MODE is enabled."
2120
+ });
2121
+ return Result.ok(void 0);
2122
+ }
1999
2123
  const { packageManager, projectDir } = config;
2124
+ const isInteractive = context.interactive ?? true;
2000
2125
  const btsConfig = await readBtsConfig(projectDir);
2001
2126
  const fullConfig = btsConfig ? {
2002
2127
  ...config,
@@ -2013,40 +2138,73 @@ async function setupSkills(config) {
2013
2138
  }));
2014
2139
  });
2015
2140
  if (skillOptions.length === 0) return Result.ok(void 0);
2016
- const scope = await select({
2017
- message: "Where should skills be installed?",
2018
- options: [{
2019
- value: "project",
2020
- label: "Project",
2021
- hint: "Writes to project config files (recommended for teams)"
2022
- }, {
2023
- value: "global",
2024
- label: "Global",
2025
- hint: "Writes to user-level config files (personal machine)"
2026
- }],
2027
- initialValue: "project"
2028
- });
2029
- if (isCancel(scope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2030
- const selectedSkills = await multiselect({
2031
- message: "Select skills to install",
2032
- options: skillOptions,
2033
- required: false,
2034
- initialValues: skillOptions.map((opt) => opt.value)
2035
- });
2036
- if (isCancel(selectedSkills)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2037
- if (selectedSkills.length === 0) return Result.ok(void 0);
2038
- const selectedAgents = await multiselect({
2039
- message: "Select agents to install skills to",
2040
- options: AVAILABLE_AGENTS,
2041
- required: false,
2042
- initialValues: [
2043
- "cursor",
2044
- "claude-code",
2045
- "github-copilot"
2046
- ]
2047
- });
2048
- if (isCancel(selectedAgents)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2049
- if (selectedAgents.length === 0) return Result.ok(void 0);
2141
+ let scope = context.addonOptions?.skills?.scope ?? "project";
2142
+ if (isInteractive) {
2143
+ const selectedScope = await select({
2144
+ message: "Where should skills be installed?",
2145
+ options: [{
2146
+ value: "project",
2147
+ label: "Project",
2148
+ hint: "Writes to project config files (recommended for teams)"
2149
+ }, {
2150
+ value: "global",
2151
+ label: "Global",
2152
+ hint: "Writes to user-level config files (personal machine)"
2153
+ }],
2154
+ initialValue: scope
2155
+ });
2156
+ if (isCancel(selectedScope)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2157
+ scope = selectedScope;
2158
+ }
2159
+ const allSkillValues = skillOptions.map((opt) => opt.value);
2160
+ let selectedSkills = context.addonOptions?.skills?.skillKeys?.length === 0 ? [] : context.addonOptions?.skills?.skillKeys ?? allSkillValues;
2161
+ if (isInteractive) {
2162
+ const selectedSkillsResult = await multiselect({
2163
+ message: "Select skills to install",
2164
+ options: skillOptions,
2165
+ required: false,
2166
+ initialValues: allSkillValues
2167
+ });
2168
+ if (isCancel(selectedSkillsResult)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2169
+ selectedSkills = selectedSkillsResult;
2170
+ }
2171
+ if (selectedSkills.length === 0) {
2172
+ emit?.({
2173
+ addon: "skills",
2174
+ status: "warning",
2175
+ selectedOptions: { scope },
2176
+ warning: "No skills were selected for installation."
2177
+ });
2178
+ return Result.ok(void 0);
2179
+ }
2180
+ let selectedAgents = context.addonOptions?.skills?.agents ?? [
2181
+ "cursor",
2182
+ "claude-code",
2183
+ "github-copilot"
2184
+ ];
2185
+ selectedAgents = selectedAgents.filter((agent) => AVAILABLE_AGENTS.some((option) => option.value === agent));
2186
+ if (isInteractive) {
2187
+ const selectedAgentsResult = await multiselect({
2188
+ message: "Select agents to install skills to",
2189
+ options: AVAILABLE_AGENTS,
2190
+ required: false,
2191
+ initialValues: selectedAgents
2192
+ });
2193
+ if (isCancel(selectedAgentsResult)) return Result.err(new UserCancelledError({ message: "Operation cancelled" }));
2194
+ selectedAgents = selectedAgentsResult;
2195
+ }
2196
+ if (selectedAgents.length === 0) {
2197
+ emit?.({
2198
+ addon: "skills",
2199
+ status: "warning",
2200
+ selectedOptions: {
2201
+ scope,
2202
+ selectedSkills
2203
+ },
2204
+ warning: "No agents were selected for skills installation."
2205
+ });
2206
+ return Result.ok(void 0);
2207
+ }
2050
2208
  const skillsBySource = {};
2051
2209
  for (const skillKey of selectedSkills) {
2052
2210
  const [source, skillName] = skillKey.split("::");
@@ -2059,7 +2217,7 @@ async function setupSkills(config) {
2059
2217
  const globalFlag = scope === "global" ? "-g" : "";
2060
2218
  for (const [source, skills] of Object.entries(skillsBySource)) {
2061
2219
  const skillFlags = skills.map((s) => `-s ${s}`).join(" ");
2062
- if ((await Result.tryPromise({
2220
+ const installResult = await Result.tryPromise({
2063
2221
  try: async () => {
2064
2222
  const args = getPackageExecutionArgs(packageManager, `skills@latest add ${source} ${globalFlag} ${skillFlags} ${agentFlags} -y`);
2065
2223
  await $({
@@ -2072,7 +2230,32 @@ async function setupSkills(config) {
2072
2230
  message: `Failed to install skills from ${source}: ${e instanceof Error ? e.message : String(e)}`,
2073
2231
  cause: e
2074
2232
  })
2075
- })).isErr()) log.warn(pc.yellow(`Warning: Could not install skills from ${source}`));
2233
+ });
2234
+ if (installResult.isErr()) {
2235
+ log.warn(pc.yellow(`Warning: Could not install skills from ${source}`));
2236
+ emit?.({
2237
+ addon: "skills",
2238
+ status: "warning",
2239
+ selectedOptions: {
2240
+ scope,
2241
+ source,
2242
+ skills,
2243
+ agents: selectedAgents
2244
+ },
2245
+ warning: installResult.error.message
2246
+ });
2247
+ continue;
2248
+ }
2249
+ emit?.({
2250
+ addon: "skills",
2251
+ status: "success",
2252
+ selectedOptions: {
2253
+ scope,
2254
+ source,
2255
+ skills,
2256
+ agents: selectedAgents
2257
+ }
2258
+ });
2076
2259
  }
2077
2260
  installSpinner.stop("Skills installed");
2078
2261
  return Result.ok(void 0);
@@ -2080,8 +2263,16 @@ async function setupSkills(config) {
2080
2263
 
2081
2264
  //#endregion
2082
2265
  //#region src/helpers/addons/starlight-setup.ts
2083
- async function setupStarlight(config) {
2084
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2266
+ async function setupStarlight(config, context = {}) {
2267
+ const emit = context.collectExternalReport;
2268
+ if (shouldSkipExternalCommands()) {
2269
+ emit?.({
2270
+ addon: "starlight",
2271
+ status: "skipped",
2272
+ warning: "Skipped because BTS_SKIP_EXTERNAL_COMMANDS or BTS_TEST_MODE is enabled."
2273
+ });
2274
+ return Result.ok(void 0);
2275
+ }
2085
2276
  const { packageManager, projectDir } = config;
2086
2277
  const s = spinner();
2087
2278
  s.start("Setting up Starlight docs...");
@@ -2112,20 +2303,48 @@ async function setupStarlight(config) {
2112
2303
  });
2113
2304
  if (result.isErr()) {
2114
2305
  s.stop("Failed to set up Starlight docs");
2306
+ emit?.({
2307
+ addon: "starlight",
2308
+ status: "failed",
2309
+ commands: [args.join(" ")],
2310
+ postChecks: ["apps/docs exists"],
2311
+ error: result.error.message
2312
+ });
2115
2313
  return result;
2116
2314
  }
2117
2315
  s.stop("Starlight docs setup successfully!");
2316
+ emit?.({
2317
+ addon: "starlight",
2318
+ status: "success",
2319
+ commands: [args.join(" ")],
2320
+ postChecks: ["apps/docs exists"]
2321
+ });
2118
2322
  return Result.ok(void 0);
2119
2323
  }
2120
2324
 
2121
2325
  //#endregion
2122
2326
  //#region src/helpers/addons/tauri-setup.ts
2123
- async function setupTauri(config) {
2124
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2327
+ async function setupTauri(config, context = {}) {
2328
+ const emit = context.collectExternalReport;
2329
+ if (shouldSkipExternalCommands()) {
2330
+ emit?.({
2331
+ addon: "tauri",
2332
+ status: "skipped",
2333
+ warning: "Skipped because BTS_SKIP_EXTERNAL_COMMANDS or BTS_TEST_MODE is enabled."
2334
+ });
2335
+ return Result.ok(void 0);
2336
+ }
2125
2337
  const { packageManager, frontend, projectDir } = config;
2126
2338
  const s = spinner();
2127
2339
  const clientPackageDir = path.join(projectDir, "apps/web");
2128
- if (!await fs.pathExists(clientPackageDir)) return Result.ok(void 0);
2340
+ if (!await fs.pathExists(clientPackageDir)) {
2341
+ emit?.({
2342
+ addon: "tauri",
2343
+ status: "skipped",
2344
+ warning: "Skipped because apps/web does not exist in this project."
2345
+ });
2346
+ return Result.ok(void 0);
2347
+ }
2129
2348
  s.start("Setting up Tauri desktop app support...");
2130
2349
  const hasReactRouter = frontend.includes("react-router");
2131
2350
  const hasNuxt = frontend.includes("nuxt");
@@ -2159,9 +2378,22 @@ async function setupTauri(config) {
2159
2378
  });
2160
2379
  if (result.isErr()) {
2161
2380
  s.stop("Failed to set up Tauri");
2381
+ emit?.({
2382
+ addon: "tauri",
2383
+ status: "failed",
2384
+ commands: [[...prefix, ...tauriArgs].join(" ")],
2385
+ postChecks: ["apps/web/src-tauri exists"],
2386
+ error: result.error.message
2387
+ });
2162
2388
  return result;
2163
2389
  }
2164
2390
  s.stop("Tauri desktop app support configured successfully!");
2391
+ emit?.({
2392
+ addon: "tauri",
2393
+ status: "success",
2394
+ commands: [[...prefix, ...tauriArgs].join(" ")],
2395
+ postChecks: ["apps/web/src-tauri exists"]
2396
+ });
2165
2397
  return Result.ok(void 0);
2166
2398
  }
2167
2399
 
@@ -2438,20 +2670,33 @@ const TEMPLATES = {
2438
2670
  hint: "Svelte template"
2439
2671
  }
2440
2672
  };
2441
- async function setupWxt(config) {
2442
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2673
+ async function setupWxt(config, context = {}) {
2674
+ const emit = context.collectExternalReport;
2675
+ if (shouldSkipExternalCommands()) {
2676
+ emit?.({
2677
+ addon: "wxt",
2678
+ status: "skipped",
2679
+ warning: "Skipped because BTS_SKIP_EXTERNAL_COMMANDS or BTS_TEST_MODE is enabled."
2680
+ });
2681
+ return Result.ok(void 0);
2682
+ }
2443
2683
  const { packageManager, projectDir } = config;
2684
+ const isInteractive = context.interactive ?? true;
2444
2685
  log.info("Setting up WXT...");
2445
- const template = await select({
2446
- message: "Choose a template",
2447
- options: Object.entries(TEMPLATES).map(([key, template]) => ({
2448
- value: key,
2449
- label: template.label,
2450
- hint: template.hint
2451
- })),
2452
- initialValue: "react"
2453
- });
2454
- if (isCancel(template)) return userCancelled("Operation cancelled");
2686
+ let template = context.addonOptions?.wxt?.template ?? "react";
2687
+ if (isInteractive) {
2688
+ const selectedTemplate = await select({
2689
+ message: "Choose a template",
2690
+ options: Object.entries(TEMPLATES).map(([key, template]) => ({
2691
+ value: key,
2692
+ label: template.label,
2693
+ hint: template.hint
2694
+ })),
2695
+ initialValue: template
2696
+ });
2697
+ if (isCancel(selectedTemplate)) return userCancelled("Operation cancelled");
2698
+ template = selectedTemplate;
2699
+ }
2455
2700
  const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
2456
2701
  const appsDir = path.join(projectDir, "apps");
2457
2702
  const ensureDirResult = await Result.tryPromise({
@@ -2483,26 +2728,56 @@ async function setupWxt(config) {
2483
2728
  });
2484
2729
  if (initResult.isErr()) {
2485
2730
  log.error(pc.red("Failed to set up WXT"));
2731
+ emit?.({
2732
+ addon: "wxt",
2733
+ status: "failed",
2734
+ selectedOptions: { template },
2735
+ commands: [args.join(" ")],
2736
+ postChecks: ["apps/extension/package.json exists", "apps/extension/package.json scripts.dev uses --port 5555"],
2737
+ error: initResult.error.message
2738
+ });
2486
2739
  return initResult;
2487
2740
  }
2488
2741
  const extensionDir = path.join(projectDir, "apps", "extension");
2489
2742
  const packageJsonPath = path.join(extensionDir, "package.json");
2490
- if ((await Result.tryPromise({
2743
+ const updatePackageResult = await Result.tryPromise({
2491
2744
  try: async () => {
2492
- if (await fs.pathExists(packageJsonPath)) {
2493
- const packageJson = await fs.readJson(packageJsonPath);
2494
- packageJson.name = "extension";
2495
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port 5555`;
2496
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2497
- }
2745
+ if (!await fs.pathExists(packageJsonPath)) throw new AddonSetupError({
2746
+ addon: "wxt",
2747
+ message: "WXT generator did not create apps/extension/package.json. Upstream template shape may have changed."
2748
+ });
2749
+ const packageJson = await fs.readJson(packageJsonPath);
2750
+ packageJson.name = "extension";
2751
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port 5555`;
2752
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2498
2753
  },
2499
2754
  catch: (e) => new AddonSetupError({
2500
2755
  addon: "wxt",
2501
2756
  message: `Failed to update package.json: ${e instanceof Error ? e.message : String(e)}`,
2502
2757
  cause: e
2503
2758
  })
2504
- })).isErr()) log.warn(pc.yellow("WXT setup completed but failed to update package.json"));
2759
+ });
2760
+ if (updatePackageResult.isErr()) {
2761
+ log.warn(pc.yellow("WXT setup completed but failed to update package.json"));
2762
+ s.stop("WXT setup completed with warnings");
2763
+ emit?.({
2764
+ addon: "wxt",
2765
+ status: "warning",
2766
+ selectedOptions: { template },
2767
+ commands: [args.join(" ")],
2768
+ postChecks: ["apps/extension/package.json exists", "apps/extension/package.json scripts.dev uses --port 5555"],
2769
+ warning: updatePackageResult.error.message
2770
+ });
2771
+ return Result.ok(void 0);
2772
+ }
2505
2773
  s.stop("WXT setup complete!");
2774
+ emit?.({
2775
+ addon: "wxt",
2776
+ status: "success",
2777
+ selectedOptions: { template },
2778
+ commands: [args.join(" ")],
2779
+ postChecks: ["apps/extension/package.json exists", "apps/extension/package.json scripts.dev uses --port 5555"]
2780
+ });
2506
2781
  return Result.ok(void 0);
2507
2782
  }
2508
2783
 
@@ -2526,14 +2801,14 @@ async function runAddonStep(addon, step) {
2526
2801
  });
2527
2802
  if (result.isErr()) consola.error(pc.red(result.error.message));
2528
2803
  }
2529
- async function setupAddons(config) {
2804
+ async function setupAddons(config, context = {}) {
2530
2805
  const { addons, frontend, projectDir } = config;
2531
2806
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
2532
2807
  const hasNuxtFrontend = frontend.includes("nuxt");
2533
2808
  const hasSvelteFrontend = frontend.includes("svelte");
2534
2809
  const hasSolidFrontend = frontend.includes("solid");
2535
2810
  const hasNextFrontend = frontend.includes("next");
2536
- if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await runSetup(() => setupTauri(config));
2811
+ if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await runSetup(() => setupTauri(config, context));
2537
2812
  const hasUltracite = addons.includes("ultracite");
2538
2813
  const hasBiome = addons.includes("biome");
2539
2814
  const hasHusky = addons.includes("husky");
@@ -2555,13 +2830,13 @@ async function setupAddons(config) {
2555
2830
  if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
2556
2831
  }
2557
2832
  }
2558
- if (addons.includes("starlight")) await runSetup(() => setupStarlight(config));
2559
- if (addons.includes("fumadocs")) await runSetup(() => setupFumadocs(config));
2833
+ if (addons.includes("starlight")) await runSetup(() => setupStarlight(config, context));
2834
+ if (addons.includes("fumadocs")) await runSetup(() => setupFumadocs(config, context));
2560
2835
  if (addons.includes("opentui")) await runSetup(() => setupTui(config));
2561
- if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
2836
+ if (addons.includes("wxt")) await runSetup(() => setupWxt(config, context));
2562
2837
  if (addons.includes("ruler")) await runSetup(() => setupRuler(config));
2563
- if (addons.includes("skills")) await runSetup(() => setupSkills(config));
2564
- if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
2838
+ if (addons.includes("skills")) await runSetup(() => setupSkills(config, context));
2839
+ if (addons.includes("mcp")) await runSetup(() => setupMcp(config, context));
2565
2840
  }
2566
2841
  async function setupBiome(projectDir) {
2567
2842
  await addPackageDependency({
@@ -3543,7 +3818,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
3543
3818
 
3544
3819
  //#endregion
3545
3820
  //#region src/prompts/project-name.ts
3546
- function isPathWithinCwd$1(targetPath) {
3821
+ function isPathWithinCwd$2(targetPath) {
3547
3822
  const resolved = path.resolve(targetPath);
3548
3823
  const rel = path.relative(process.cwd(), resolved);
3549
3824
  return !rel.startsWith("..") && !path.isAbsolute(rel);
@@ -3557,7 +3832,7 @@ async function getProjectName(initialName) {
3557
3832
  if (initialName) {
3558
3833
  if (initialName === ".") return initialName;
3559
3834
  if (!validateDirectoryName(path.basename(initialName))) {
3560
- if (isPathWithinCwd$1(path.resolve(process.cwd(), initialName))) return initialName;
3835
+ if (isPathWithinCwd$2(path.resolve(process.cwd(), initialName))) return initialName;
3561
3836
  consola.error(pc.red("Project path must be within current directory"));
3562
3837
  }
3563
3838
  }
@@ -3580,7 +3855,7 @@ async function getProjectName(initialName) {
3580
3855
  const validationError = validateDirectoryName(path.basename(nameToUse));
3581
3856
  if (validationError) return validationError;
3582
3857
  if (nameToUse !== ".") {
3583
- if (!isPathWithinCwd$1(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
3858
+ if (!isPathWithinCwd$2(path.resolve(process.cwd(), nameToUse))) return "Project path must be within current directory";
3584
3859
  }
3585
3860
  }
3586
3861
  });
@@ -3601,7 +3876,7 @@ async function getProjectName(initialName) {
3601
3876
  */
3602
3877
  function isTelemetryEnabled() {
3603
3878
  const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
3604
- const BTS_TELEMETRY = "1";
3879
+ const BTS_TELEMETRY = "0";
3605
3880
  if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
3606
3881
  if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
3607
3882
  return true;
@@ -3609,17 +3884,7 @@ function isTelemetryEnabled() {
3609
3884
 
3610
3885
  //#endregion
3611
3886
  //#region src/utils/analytics.ts
3612
- const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
3613
- async function sendConvexEvent(payload) {
3614
- await Result.tryPromise({
3615
- try: () => fetch(CONVEX_INGEST_URL, {
3616
- method: "POST",
3617
- headers: { "Content-Type": "application/json" },
3618
- body: JSON.stringify(payload)
3619
- }),
3620
- catch: () => void 0
3621
- });
3622
- }
3887
+ async function sendConvexEvent(payload) {}
3623
3888
  async function trackProjectCreation(config, disableAnalytics = false) {
3624
3889
  if (!isTelemetryEnabled() || disableAnalytics) return;
3625
3890
  const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
@@ -4205,6 +4470,19 @@ async function formatProject(projectDir) {
4205
4470
  });
4206
4471
  }
4207
4472
 
4473
+ //#endregion
4474
+ //#region src/helpers/addons/external-manifest.ts
4475
+ async function writeExternalAddonManifest(projectDir, reports) {
4476
+ if (reports.length === 0) return;
4477
+ const manifest = {
4478
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4479
+ reports
4480
+ };
4481
+ const btsDir = path.join(projectDir, ".bts");
4482
+ await fs.ensureDir(btsDir);
4483
+ await fs.writeJson(path.join(btsDir, "external-manifest.json"), manifest, { spaces: 2 });
4484
+ }
4485
+
4208
4486
  //#endregion
4209
4487
  //#region src/utils/env-utils.ts
4210
4488
  async function addEnvVariablesToFile(envPath, variables) {
@@ -5647,6 +5925,7 @@ async function createProject(options, cliInput = {}) {
5647
5925
  return Result.gen(async function* () {
5648
5926
  const projectDir = options.projectDir;
5649
5927
  const isConvex = options.backend === "convex";
5928
+ const externalReports = [];
5650
5929
  yield* Result.await(Result.tryPromise({
5651
5930
  try: () => fs.ensureDir(projectDir),
5652
5931
  catch: (e) => new ProjectCreationError({
@@ -5678,11 +5957,32 @@ async function createProject(options, cliInput = {}) {
5678
5957
  cause: e
5679
5958
  })
5680
5959
  }));
5681
- if (options.addons.length > 0 && options.addons[0] !== "none") yield* Result.await(Result.tryPromise({
5682
- try: () => setupAddons(options),
5960
+ if (options.addons.length > 0 && options.addons[0] !== "none") {
5961
+ const baseSetupContext = cliInput.addonSetupContext ?? {};
5962
+ const setupContext = {
5963
+ ...baseSetupContext,
5964
+ collectExternalReport: (report) => {
5965
+ externalReports.push(report);
5966
+ baseSetupContext.collectExternalReport?.(report);
5967
+ }
5968
+ };
5969
+ yield* Result.await(Result.tryPromise({
5970
+ try: () => setupAddons(options, setupContext),
5971
+ catch: (e) => new ProjectCreationError({
5972
+ phase: "addons-setup",
5973
+ message: `Failed to setup addons: ${e instanceof Error ? e.message : String(e)}`,
5974
+ cause: e
5975
+ })
5976
+ }));
5977
+ }
5978
+ yield* Result.await(Result.tryPromise({
5979
+ try: async () => {
5980
+ await writeExternalAddonManifest(projectDir, externalReports);
5981
+ cliInput.onExternalAddonReports?.(externalReports);
5982
+ },
5683
5983
  catch: (e) => new ProjectCreationError({
5684
- phase: "addons-setup",
5685
- message: `Failed to setup addons: ${e instanceof Error ? e.message : String(e)}`,
5984
+ phase: "addons-manifest",
5985
+ message: `Failed to write external addon manifest: ${e instanceof Error ? e.message : String(e)}`,
5686
5986
  cause: e
5687
5987
  })
5688
5988
  }));
@@ -5917,7 +6217,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
5917
6217
  });
5918
6218
  });
5919
6219
  }
5920
- function isPathWithinCwd(targetPath) {
6220
+ function isPathWithinCwd$1(targetPath) {
5921
6221
  const resolved = path.resolve(targetPath);
5922
6222
  const rel = path.relative(process.cwd(), resolved);
5923
6223
  return !rel.startsWith("..") && !path.isAbsolute(rel);
@@ -5931,7 +6231,7 @@ async function resolveProjectNameForSilent(input) {
5931
6231
  message: validationResult.error.message,
5932
6232
  cause: validationResult.error
5933
6233
  }));
5934
- if (!isPathWithinCwd(candidate)) return Result.err(new CLIError({ message: "Project path must be within current directory" }));
6234
+ if (!isPathWithinCwd$1(candidate)) return Result.err(new CLIError({ message: "Project path must be within current directory" }));
5935
6235
  return Result.ok(candidate);
5936
6236
  }
5937
6237
  async function handleDirectoryConflictResult(currentPathInput, strategy) {
@@ -5985,6 +6285,385 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
5985
6285
  }
5986
6286
  }
5987
6287
 
6288
+ //#endregion
6289
+ //#region src/mcp/planning.ts
6290
+ const EXTERNAL_ADDON_KEYS = new Set([
6291
+ "fumadocs",
6292
+ "starlight",
6293
+ "wxt",
6294
+ "tauri",
6295
+ "mcp",
6296
+ "skills"
6297
+ ]);
6298
+ function getPreferredTemplate(options, addon) {
6299
+ if (addon === "fumadocs") return options?.fumadocs?.template;
6300
+ if (addon === "wxt") return options?.wxt?.template;
6301
+ }
6302
+ function toResultError(message) {
6303
+ return new CLIError({ message });
6304
+ }
6305
+ function isPathWithinCwd(candidatePath) {
6306
+ const resolvedCwd = path.resolve(process.cwd());
6307
+ const resolvedPath = path.resolve(process.cwd(), candidatePath);
6308
+ const relative = path.relative(resolvedCwd, resolvedPath);
6309
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
6310
+ }
6311
+ function buildPlannedExternalSteps(config, addonOptions) {
6312
+ return config.addons.filter((addon) => EXTERNAL_ADDON_KEYS.has(addon)).map((addon) => {
6313
+ const preferredTemplate = getPreferredTemplate(addonOptions, addon);
6314
+ return {
6315
+ addon,
6316
+ status: "planned",
6317
+ selectedOptions: preferredTemplate ? { template: preferredTemplate } : void 0
6318
+ };
6319
+ });
6320
+ }
6321
+ function resolveScaffoldPlan(input) {
6322
+ return Result.gen(function* () {
6323
+ const configInput = input.config ?? {};
6324
+ const defaultConfig = getDefaultConfig();
6325
+ const requestedTarget = input.directory ?? input.projectName ?? defaultConfig.relativePath;
6326
+ if (!isPathWithinCwd(requestedTarget)) yield* Result.err(toResultError("Project path must be within current directory"));
6327
+ const processedInput = {
6328
+ ...configInput,
6329
+ projectName: requestedTarget,
6330
+ template: configInput.template,
6331
+ yes: false,
6332
+ yolo: false,
6333
+ install: configInput.install ?? defaultConfig.install,
6334
+ git: configInput.git ?? defaultConfig.git,
6335
+ packageManager: configInput.packageManager ?? defaultConfig.packageManager
6336
+ };
6337
+ let mergedInput = processedInput;
6338
+ if (configInput.template && configInput.template !== "none") {
6339
+ const templateConfig = getTemplateConfig(configInput.template);
6340
+ if (templateConfig) mergedInput = {
6341
+ ...templateConfig,
6342
+ ...processedInput,
6343
+ template: configInput.template
6344
+ };
6345
+ }
6346
+ const providedFlags = getProvidedFlags(mergedInput);
6347
+ const projectNameBase = path.basename(path.resolve(process.cwd(), requestedTarget));
6348
+ const flagConfig = yield* processAndValidateFlags({
6349
+ ...mergedInput,
6350
+ projectDirectory: requestedTarget
6351
+ }, providedFlags, projectNameBase).mapError((error) => toResultError(error.message));
6352
+ const resolvedProjectDir = path.resolve(process.cwd(), requestedTarget);
6353
+ const normalizedConfig = {
6354
+ ...defaultConfig,
6355
+ ...flagConfig,
6356
+ projectName: projectNameBase,
6357
+ projectDir: resolvedProjectDir,
6358
+ relativePath: requestedTarget
6359
+ };
6360
+ yield* validateConfigCompatibility(normalizedConfig, providedFlags, mergedInput).mapError((error) => toResultError(error.message));
6361
+ const warnings = [];
6362
+ const plannedExternalSteps = buildPlannedExternalSteps(normalizedConfig, input.addonOptions);
6363
+ if (plannedExternalSteps.length > 0) warnings.push("This configuration includes addons that execute external generators. The project can complete with partial_success if upstream generators fail.");
6364
+ return Result.ok({
6365
+ config: normalizedConfig,
6366
+ reproducibleCommand: generateReproducibleCommand(normalizedConfig),
6367
+ plannedExternalSteps,
6368
+ warnings
6369
+ });
6370
+ });
6371
+ }
6372
+ function computeExternalStatusFromReports(reports) {
6373
+ return reports.some((report) => report.status === "warning" || report.status === "failed") ? "partial_success" : "success";
6374
+ }
6375
+
6376
+ //#endregion
6377
+ //#region src/mcp/tools/create-stack.ts
6378
+ function collectReportWarnings(reports) {
6379
+ return reports.flatMap((report) => {
6380
+ if (report.warning) return [report.warning];
6381
+ if (report.error) return [report.error];
6382
+ return [];
6383
+ });
6384
+ }
6385
+ async function resolveDirectoryConflict(currentPathInput, strategy) {
6386
+ const currentPath = path.resolve(process.cwd(), currentPathInput);
6387
+ if (!await fs.pathExists(currentPath)) return Result.ok({
6388
+ finalPathInput: currentPathInput,
6389
+ shouldClearDirectory: false
6390
+ });
6391
+ if (!((await fs.readdir(currentPath)).length > 0)) return Result.ok({
6392
+ finalPathInput: currentPathInput,
6393
+ shouldClearDirectory: false
6394
+ });
6395
+ switch (strategy) {
6396
+ case "overwrite": return Result.ok({
6397
+ finalPathInput: currentPathInput,
6398
+ shouldClearDirectory: true
6399
+ });
6400
+ case "merge": return Result.ok({
6401
+ finalPathInput: currentPathInput,
6402
+ shouldClearDirectory: false
6403
+ });
6404
+ case "increment": {
6405
+ let counter = 1;
6406
+ const baseName = currentPathInput;
6407
+ let finalPathInput = `${baseName}-${counter}`;
6408
+ while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
6409
+ counter += 1;
6410
+ finalPathInput = `${baseName}-${counter}`;
6411
+ }
6412
+ return Result.ok({
6413
+ finalPathInput,
6414
+ shouldClearDirectory: false
6415
+ });
6416
+ }
6417
+ case "error": return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
6418
+ default: return Result.err(new CLIError({ message: `Unknown directory conflict strategy: ${strategy}` }));
6419
+ }
6420
+ }
6421
+ async function createStack(input) {
6422
+ const planResult = resolveScaffoldPlan(input);
6423
+ if (planResult.isErr()) return {
6424
+ status: "failed",
6425
+ externalStepReports: [],
6426
+ warnings: [],
6427
+ errors: [planResult.error.message]
6428
+ };
6429
+ const strategy = input.directoryConflict ?? "error";
6430
+ const planned = planResult.value;
6431
+ const conflictResult = await resolveDirectoryConflict(planned.config.relativePath, strategy);
6432
+ if (conflictResult.isErr()) return {
6433
+ status: "failed",
6434
+ externalStepReports: [],
6435
+ warnings: planned.warnings,
6436
+ errors: [conflictResult.error.message]
6437
+ };
6438
+ const dirSetupResult = await Result.tryPromise({
6439
+ try: async () => setupProjectDirectory(conflictResult.value.finalPathInput, conflictResult.value.shouldClearDirectory),
6440
+ catch: (e) => new CLIError({
6441
+ message: e instanceof Error ? e.message : String(e),
6442
+ cause: e
6443
+ })
6444
+ });
6445
+ if (dirSetupResult.isErr()) return {
6446
+ status: "failed",
6447
+ externalStepReports: [],
6448
+ warnings: planned.warnings,
6449
+ errors: [dirSetupResult.error.message]
6450
+ };
6451
+ const { finalBaseName, finalResolvedPath } = dirSetupResult.value;
6452
+ const finalConfig = {
6453
+ ...planned.config,
6454
+ projectName: finalBaseName,
6455
+ projectDir: finalResolvedPath,
6456
+ relativePath: conflictResult.value.finalPathInput
6457
+ };
6458
+ const reports = [];
6459
+ const addonOptions = input.addonOptions;
6460
+ const projectResult = await runWithContextAsync({ silent: true }, () => createProject(finalConfig, {
6461
+ manualDb: input.config?.manualDb ?? false,
6462
+ addonSetupContext: {
6463
+ interactive: false,
6464
+ addonOptions
6465
+ },
6466
+ onExternalAddonReports: (newReports) => reports.push(...newReports)
6467
+ }));
6468
+ if (projectResult.isErr()) return {
6469
+ status: "failed",
6470
+ externalStepReports: reports,
6471
+ warnings: [...planned.warnings, ...collectReportWarnings(reports)],
6472
+ errors: [projectResult.error.message]
6473
+ };
6474
+ const reproducibleCommand = generateReproducibleCommand(finalConfig);
6475
+ const status = computeExternalStatusFromReports(reports);
6476
+ const warnings = [...planned.warnings, ...collectReportWarnings(reports)];
6477
+ return {
6478
+ status,
6479
+ projectDirectory: finalConfig.projectDir,
6480
+ relativePath: finalConfig.relativePath,
6481
+ reproducibleCommand,
6482
+ externalStepReports: reports,
6483
+ warnings,
6484
+ errors: []
6485
+ };
6486
+ }
6487
+
6488
+ //#endregion
6489
+ //#region src/mcp/tools/plan-stack.ts
6490
+ function planStack(input) {
6491
+ const result = resolveScaffoldPlan({
6492
+ projectName: input.projectName,
6493
+ directory: input.directory,
6494
+ config: input.config,
6495
+ addonOptions: input.addonOptions
6496
+ });
6497
+ if (result.isErr()) return {
6498
+ success: false,
6499
+ warnings: [],
6500
+ errors: [result.error.message]
6501
+ };
6502
+ const plan = result.value;
6503
+ return {
6504
+ success: true,
6505
+ normalizedConfig: plan.config,
6506
+ reproducibleCommand: plan.reproducibleCommand,
6507
+ plannedExternalSteps: plan.plannedExternalSteps,
6508
+ warnings: plan.warnings,
6509
+ errors: []
6510
+ };
6511
+ }
6512
+
6513
+ //#endregion
6514
+ //#region src/mcp/server.ts
6515
+ const AddonOptionsSchema = z$1.object({
6516
+ fumadocs: z$1.object({ template: z$1.enum([
6517
+ "next-mdx",
6518
+ "next-mdx-static",
6519
+ "waku",
6520
+ "react-router",
6521
+ "react-router-spa",
6522
+ "tanstack-start",
6523
+ "tanstack-start-spa"
6524
+ ]).optional() }).optional(),
6525
+ wxt: z$1.object({ template: z$1.enum([
6526
+ "vanilla",
6527
+ "vue",
6528
+ "react",
6529
+ "solid",
6530
+ "svelte"
6531
+ ]).optional() }).optional(),
6532
+ mcp: z$1.object({
6533
+ scope: z$1.enum(["project", "global"]).optional(),
6534
+ agents: z$1.array(z$1.string()).optional(),
6535
+ serverKeys: z$1.array(z$1.string()).optional()
6536
+ }).optional(),
6537
+ skills: z$1.object({
6538
+ scope: z$1.enum(["project", "global"]).optional(),
6539
+ agents: z$1.array(z$1.string()).optional(),
6540
+ skillKeys: z$1.array(z$1.string()).optional()
6541
+ }).optional()
6542
+ });
6543
+ const SharedInputSchema = {
6544
+ projectName: z$1.string().optional(),
6545
+ directory: z$1.string().optional(),
6546
+ config: z$1.unknown().optional(),
6547
+ addonOptions: AddonOptionsSchema.optional()
6548
+ };
6549
+ function validateConfig(input) {
6550
+ const parsed = types_exports.CreateInputSchema.partial().safeParse(input ?? {});
6551
+ if (!parsed.success) {
6552
+ const issue = parsed.error.issues[0];
6553
+ throw new Error(`Invalid config: ${issue?.message ?? "unknown error"}`);
6554
+ }
6555
+ return parsed.data;
6556
+ }
6557
+ async function startMcpServer() {
6558
+ const server = new McpServer({
6559
+ name: "create-better-t-stack",
6560
+ version: getLatestCLIVersion()
6561
+ });
6562
+ server.registerTool("plan_stack", {
6563
+ description: "Validate and normalize a Better-T-Stack configuration without writing files. Returns reproducible command and planned external steps.",
6564
+ inputSchema: SharedInputSchema,
6565
+ outputSchema: {
6566
+ success: z$1.boolean(),
6567
+ normalizedConfig: z$1.record(z$1.string(), z$1.unknown()).optional(),
6568
+ reproducibleCommand: z$1.string().optional(),
6569
+ plannedExternalSteps: z$1.array(z$1.object({
6570
+ addon: z$1.string(),
6571
+ status: z$1.literal("planned"),
6572
+ selectedOptions: z$1.record(z$1.string(), z$1.unknown()).optional()
6573
+ })).optional(),
6574
+ warnings: z$1.array(z$1.string()),
6575
+ errors: z$1.array(z$1.string())
6576
+ }
6577
+ }, async (args) => {
6578
+ try {
6579
+ const result = planStack({
6580
+ projectName: args.projectName,
6581
+ directory: args.directory,
6582
+ config: validateConfig(args.config),
6583
+ addonOptions: args.addonOptions
6584
+ });
6585
+ return {
6586
+ content: [{
6587
+ type: "text",
6588
+ text: JSON.stringify(result, null, 2)
6589
+ }],
6590
+ structuredContent: result
6591
+ };
6592
+ } catch (error) {
6593
+ const result = {
6594
+ success: false,
6595
+ warnings: [],
6596
+ errors: [error instanceof Error ? error.message : String(error)]
6597
+ };
6598
+ return {
6599
+ content: [{
6600
+ type: "text",
6601
+ text: JSON.stringify(result, null, 2)
6602
+ }],
6603
+ structuredContent: result
6604
+ };
6605
+ }
6606
+ });
6607
+ server.registerTool("create_stack", {
6608
+ description: "Scaffold a Better-T-Stack project on disk. Returns success, partial_success, or failed along with external step reports.",
6609
+ inputSchema: {
6610
+ ...SharedInputSchema,
6611
+ directoryConflict: z$1.enum([
6612
+ "merge",
6613
+ "overwrite",
6614
+ "increment",
6615
+ "error"
6616
+ ]).optional()
6617
+ },
6618
+ outputSchema: {
6619
+ status: z$1.enum([
6620
+ "success",
6621
+ "partial_success",
6622
+ "failed"
6623
+ ]),
6624
+ projectDirectory: z$1.string().optional(),
6625
+ relativePath: z$1.string().optional(),
6626
+ reproducibleCommand: z$1.string().optional(),
6627
+ externalStepReports: z$1.array(z$1.record(z$1.string(), z$1.unknown())),
6628
+ warnings: z$1.array(z$1.string()),
6629
+ errors: z$1.array(z$1.string())
6630
+ }
6631
+ }, async (args) => {
6632
+ try {
6633
+ const result = await createStack({
6634
+ projectName: args.projectName,
6635
+ directory: args.directory,
6636
+ config: validateConfig(args.config),
6637
+ addonOptions: args.addonOptions,
6638
+ directoryConflict: args.directoryConflict
6639
+ });
6640
+ return {
6641
+ content: [{
6642
+ type: "text",
6643
+ text: JSON.stringify(result, null, 2)
6644
+ }],
6645
+ structuredContent: result
6646
+ };
6647
+ } catch (error) {
6648
+ const result = {
6649
+ status: "failed",
6650
+ externalStepReports: [],
6651
+ warnings: [],
6652
+ errors: [error instanceof Error ? error.message : String(error)]
6653
+ };
6654
+ return {
6655
+ content: [{
6656
+ type: "text",
6657
+ text: JSON.stringify(result, null, 2)
6658
+ }],
6659
+ structuredContent: result
6660
+ };
6661
+ }
6662
+ });
6663
+ const transport = new StdioServerTransport();
6664
+ await server.connect(transport);
6665
+ }
6666
+
5988
6667
  //#endregion
5989
6668
  //#region src/index.ts
5990
6669
  const router = os.router({
@@ -6042,6 +6721,9 @@ const router = os.router({
6042
6721
  json: z.boolean().optional().default(false).describe("Output as JSON")
6043
6722
  })).handler(async ({ input }) => {
6044
6723
  await historyHandler(input);
6724
+ }),
6725
+ mcp: os.meta({ description: "Start Better-T-Stack MCP server over stdio" }).handler(async () => {
6726
+ await startMcpServer();
6045
6727
  })
6046
6728
  });
6047
6729
  const caller = createRouterClient(router, { context: {} });
@@ -6184,4 +6866,4 @@ async function add(options = {}) {
6184
6866
  }
6185
6867
 
6186
6868
  //#endregion
6187
- export { DatabaseSetupError as _, VirtualFileSystem$1 as a, UserCancelledError as b, create as c, docs as d, generate$1 as f, CompatibilityError as g, CLIError as h, TEMPLATE_COUNT as i, createBtsCli as l, sponsors as m, GeneratorError$1 as n, add as o, router as p, Result$1 as r, builder as s, EMBEDDED_TEMPLATES$1 as t, createVirtual as u, DirectoryConflictError as v, ValidationError as x, ProjectCreationError as y };
6869
+ export { ValidationError as S, CompatibilityError as _, VirtualFileSystem$1 as a, ProjectCreationError as b, create as c, docs as d, generate$1 as f, CLIError as g, startMcpServer as h, TEMPLATE_COUNT as i, createBtsCli as l, sponsors as m, GeneratorError$1 as n, add as o, router as p, Result$1 as r, builder as s, EMBEDDED_TEMPLATES$1 as t, createVirtual as u, DatabaseSetupError as v, UserCancelledError as x, DirectoryConflictError as y };
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { EMBEDDED_TEMPLATES, GeneratorError, GeneratorOptions, Result, TEMPLATE_COUNT, VirtualDirectory, VirtualFile, VirtualFileSystem, VirtualFileTree, VirtualNode, generate } from "@better-t-stack/template-generator";
2
+ import { Result } from "better-result";
3
+ import { EMBEDDED_TEMPLATES, GeneratorError, GeneratorOptions, TEMPLATE_COUNT, VirtualDirectory, VirtualFile, VirtualFileSystem, VirtualFileTree, VirtualNode, generate } from "@better-t-stack/template-generator";
3
4
  import { API, Addons, Auth, Backend, Database, DatabaseSetup, Examples, Frontend, ORM, PackageManager, Payments, ProjectConfig, Runtime, ServerDeploy, WebDeploy } from "@better-t-stack/types";
4
5
  export { type API, type Addons, type Auth, type Backend, type Database, type DatabaseSetup, EMBEDDED_TEMPLATES, type Examples, type Frontend, GeneratorError, type GeneratorOptions, type ORM, type PackageManager, type Payments, type ProjectConfig, Result, type Runtime, type ServerDeploy, TEMPLATE_COUNT, type VirtualDirectory, type VirtualFile, VirtualFileSystem, type VirtualFileTree, type VirtualNode, type WebDeploy, generate };
package/dist/virtual.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { EMBEDDED_TEMPLATES, GeneratorError, Result, TEMPLATE_COUNT, VirtualFileSystem, generate } from "@better-t-stack/template-generator";
2
+ import { Result } from "better-result";
3
+ import { EMBEDDED_TEMPLATES, GeneratorError, TEMPLATE_COUNT, VirtualFileSystem, generate } from "@better-t-stack/template-generator";
3
4
 
4
5
  export { EMBEDDED_TEMPLATES, GeneratorError, Result, TEMPLATE_COUNT, VirtualFileSystem, generate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.20.2",
3
+ "version": "3.21.0-pr892.a7e1b0f",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "keywords": [
6
6
  "better-auth",
@@ -70,10 +70,11 @@
70
70
  "prepublishOnly": "npm run build"
71
71
  },
72
72
  "dependencies": {
73
- "@better-t-stack/template-generator": "^3.20.2",
74
- "@better-t-stack/types": "^3.20.2",
73
+ "@better-t-stack/template-generator": "3.21.0-pr892.a7e1b0f",
74
+ "@better-t-stack/types": "3.21.0-pr892.a7e1b0f",
75
75
  "@clack/core": "^1.0.0",
76
76
  "@clack/prompts": "^1.0.0",
77
+ "@modelcontextprotocol/sdk": "^1.26.0",
77
78
  "@orpc/server": "^1.13.4",
78
79
  "better-result": "^2.7.0",
79
80
  "consola": "^3.4.2",