create-better-t-stack 3.40.5 → 3.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -128,6 +128,9 @@ This CLI collects anonymous usage data to help improve the tool. The data collec
128
128
  - CLI version
129
129
  - Node.js version
130
130
  - Platform (OS)
131
+ - How the CLI was driven (prompts, flags, `--yes`, JSON, the programmatic API, or the MCP server)
132
+
133
+ Separately, a small number of anonymous diagnostic events are sent for failures (stage and error class, never full messages or paths), cancelled prompts, other commands such as `add` and `history`, slow runs, and MCP tool usage. See the [analytics documentation](https://better-t-stack.dev/docs/analytics) for the exact list.
131
134
 
132
135
  **Telemetry is enabled by default in published versions** to help us understand usage patterns and improve the tool.
133
136
 
@@ -143,6 +146,8 @@ BTS_TELEMETRY_DISABLED=1 npx create-better-t-stack
143
146
  export BTS_TELEMETRY_DISABLED=1
144
147
  ```
145
148
 
149
+ The CLI also honors the cross-tool `DO_NOT_TRACK=1` convention (https://consoledonottrack.com), and `--disable-analytics` skips telemetry for a single run.
150
+
146
151
  ## Examples
147
152
 
148
153
  Create a project with default configuration:
package/dist/cli.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as types_exports, y as getLatestCLIVersion } from "./src-E3yd8mxg.mjs";
2
+ import { A as setProcessMode, S as getLatestCLIVersion, b as scrubReason, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as durationBucket, x as types_exports, y as reportDiagnostic } from "./src-NAkPN6DN.mjs";
3
3
  import z from "zod";
4
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { McpServer } from "@modelcontextprotocol/server";
5
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
6
6
  //#region src/mcp.ts
7
7
  const ToolResponseSchema = z.object({
8
8
  ok: z.boolean(),
@@ -58,6 +58,44 @@ function formatToolError(cause) {
58
58
  isError: true
59
59
  };
60
60
  }
61
+ const reportedSessions = /* @__PURE__ */ new WeakSet();
62
+ /** One session event per connection, sent when the client identity is first known. */
63
+ function reportMcpSession(server) {
64
+ if (reportedSessions.has(server)) return;
65
+ reportedSessions.add(server);
66
+ const client = server.server.getClientVersion();
67
+ reportDiagnostic("mcp_session", {
68
+ client: client?.name ?? "unknown",
69
+ clientVersion: client?.version ?? "unknown"
70
+ });
71
+ }
72
+ /**
73
+ * Times every tool call and reports the outcome. Diagnostics are fire-and-forget here
74
+ * because the server process is long-lived and must not add latency to tool results.
75
+ */
76
+ function instrumentTool(server, tool, handler, isOptedOut) {
77
+ return async (...args) => {
78
+ if (isOptedOut?.(...args)) return handler(...args);
79
+ reportMcpSession(server);
80
+ const startTime = Date.now();
81
+ const result = await handler(...args);
82
+ const ok = !("isError" in result);
83
+ reportDiagnostic("mcp_tool", {
84
+ tool,
85
+ ok,
86
+ duration: durationBucket(Date.now() - startTime)
87
+ });
88
+ if (!ok) reportDiagnostic("mcp_tool_error", {
89
+ tool,
90
+ error: "ToolError",
91
+ reason: scrubReason(result.structuredContent.error)
92
+ });
93
+ return result;
94
+ };
95
+ }
96
+ function isCreateOptedOut(input) {
97
+ return input.disableAnalytics === true;
98
+ }
61
99
  function getProjectToolAnnotations() {
62
100
  return {
63
101
  destructiveHint: true,
@@ -128,10 +166,14 @@ function getStackGuidance() {
128
166
  };
129
167
  }
130
168
  function createBtsMcpServer() {
169
+ setProcessMode("mcp");
131
170
  const server = new McpServer({
132
171
  name: "create-better-t-stack",
133
172
  version: getLatestCLIVersion()
134
- }, { capabilities: { logging: {} } });
173
+ }, { cacheHints: { "tools/list": {
174
+ ttlMs: 3600 * 1e3,
175
+ cacheScope: "public"
176
+ } } });
135
177
  server.registerTool("bts_get_stack_guidance", {
136
178
  title: "Get Better T Stack MCP Guidance",
137
179
  description: "Read MCP-specific guidance for choosing valid Better T Stack configurations. Use this before planning when user intent is ambiguous. This explains the full explicit config required by MCP project creation, plus important field semantics and ambiguity rules.",
@@ -143,13 +185,13 @@ function createBtsMcpServer() {
143
185
  idempotentHint: true,
144
186
  openWorldHint: false
145
187
  }
146
- }, async () => {
188
+ }, instrumentTool(server, "bts_get_stack_guidance", async () => {
147
189
  try {
148
190
  return formatToolSuccess(getStackGuidance());
149
191
  } catch (error) {
150
192
  return formatToolError(error);
151
193
  }
152
- });
194
+ }));
153
195
  server.registerTool("bts_get_schema", {
154
196
  title: "Get Better T Stack Schemas",
155
197
  description: "Inspect Better T Stack CLI and input schemas so agents can plan valid create/add requests. Use this together with bts_get_stack_guidance before creating a project if any part of the request is ambiguous.",
@@ -162,13 +204,13 @@ function createBtsMcpServer() {
162
204
  idempotentHint: true,
163
205
  openWorldHint: false
164
206
  }
165
- }, async ({ name }) => {
207
+ }, instrumentTool(server, "bts_get_schema", async ({ name }) => {
166
208
  try {
167
209
  return formatToolSuccess(getSchemaResult(name ?? "all"));
168
210
  } catch (error) {
169
211
  return formatToolError(error);
170
212
  }
171
- });
213
+ }));
172
214
  server.registerTool("bts_plan_project", {
173
215
  title: "Plan Better T Stack Project",
174
216
  description: "Validate and preview a Better T Stack project creation without writing files or provisioning resources. Always use this before bts_create_project. This tool requires an explicit full stack config rather than a partial payload with inferred defaults.",
@@ -181,7 +223,7 @@ function createBtsMcpServer() {
181
223
  idempotentHint: true,
182
224
  openWorldHint: false
183
225
  }
184
- }, async (input) => {
226
+ }, instrumentTool(server, "bts_plan_project", async (input) => {
185
227
  try {
186
228
  const result = await create(input.projectName, {
187
229
  ...input,
@@ -200,7 +242,7 @@ function createBtsMcpServer() {
200
242
  } catch (error) {
201
243
  return formatToolError(error);
202
244
  }
203
- });
245
+ }, isCreateOptedOut));
204
246
  server.registerTool("bts_create_project", {
205
247
  title: "Create Better T Stack Project",
206
248
  description: "Create a Better T Stack project on disk using the same silent programmatic flow as the CLI JSON API. Call this only after bts_plan_project succeeds and the plan clearly matches the user's intent. This tool requires an explicit full stack config.",
@@ -210,19 +252,19 @@ function createBtsMcpServer() {
210
252
  title: "Create Better T Stack Project",
211
253
  ...getProjectToolAnnotations()
212
254
  }
213
- }, async (input) => {
255
+ }, instrumentTool(server, "bts_create_project", async (input) => {
214
256
  try {
215
257
  if (input.install) return formatToolError(getMcpInstallTimeoutMessage(input.packageManager));
216
258
  const result = await create(input.projectName, {
217
259
  ...input,
218
- disableAnalytics: true
260
+ disableAnalytics: input.disableAnalytics ?? false
219
261
  });
220
262
  if (result.isErr()) return formatToolError(result.error);
221
263
  return formatToolSuccess(result.value);
222
264
  } catch (error) {
223
265
  return formatToolError(error);
224
266
  }
225
- });
267
+ }, isCreateOptedOut));
226
268
  server.registerTool("bts_plan_addons", {
227
269
  title: "Plan Better T Stack Project Additions",
228
270
  description: "Validate and preview addon installation or workspace package scaffolding for an existing Better T Stack project without writing files. Always use this before bts_add_addons.",
@@ -235,7 +277,7 @@ function createBtsMcpServer() {
235
277
  idempotentHint: true,
236
278
  openWorldHint: false
237
279
  }
238
- }, async (input) => {
280
+ }, instrumentTool(server, "bts_plan_addons", async (input) => {
239
281
  try {
240
282
  const result = await add({
241
283
  ...input,
@@ -246,7 +288,7 @@ function createBtsMcpServer() {
246
288
  } catch (error) {
247
289
  return formatToolError(error);
248
290
  }
249
- });
291
+ }));
250
292
  server.registerTool("bts_add_addons", {
251
293
  title: "Apply Better T Stack Project Additions",
252
294
  description: "Install addons or scaffold a workspace package in an existing Better T Stack project using the same silent flow as add-json. Call this only after bts_plan_addons succeeds and the planned changes match the user's intent.",
@@ -258,7 +300,7 @@ function createBtsMcpServer() {
258
300
  idempotentHint: false,
259
301
  openWorldHint: true
260
302
  }
261
- }, async (input) => {
303
+ }, instrumentTool(server, "bts_add_addons", async (input) => {
262
304
  try {
263
305
  const result = await add(input);
264
306
  if (!result?.success) return formatToolError(result?.error ?? "Failed to update project");
@@ -266,13 +308,11 @@ function createBtsMcpServer() {
266
308
  } catch (error) {
267
309
  return formatToolError(error);
268
310
  }
269
- });
311
+ }));
270
312
  return server;
271
313
  }
272
- async function startBtsMcpServer() {
273
- const server = createBtsMcpServer();
274
- const transport = new StdioServerTransport();
275
- await server.connect(transport);
314
+ function startBtsMcpServer() {
315
+ return serveStdio(() => createBtsMcpServer());
276
316
  }
277
317
  //#endregion
278
318
  //#region src/cli.ts
package/dist/index.d.mts CHANGED
@@ -12,9 +12,12 @@ declare const UserCancelledError_base: import("better-result").TaggedErrorClass<
12
12
  */
13
13
  declare class UserCancelledError extends UserCancelledError_base<{
14
14
  message: string;
15
+ /** Name of the prompt that was open when the user cancelled, when known. */
16
+ prompt?: string;
15
17
  }> {
16
18
  constructor(args?: {
17
19
  message?: string;
20
+ prompt?: string;
18
21
  });
19
22
  }
20
23
  declare const CLIError_base: import("better-result").TaggedErrorClass<"CLIError">;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { C as DirectoryConflictError, E as ValidationError, S as DatabaseSetupError, T as UserCancelledError, _ as ProjectLauncherSchema, a as TEMPLATE_COUNT, b as CLIError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as ProjectCreationError, x as CompatibilityError } from "./src-E3yd8mxg.mjs";
2
+ import { C as CLIError, D as ProjectCreationError, E as DirectoryConflictError, O as UserCancelledError, T as DatabaseSetupError, _ as ProjectLauncherSchema, a as TEMPLATE_COUNT, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, k as ValidationError, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as CompatibilityError } from "./src-NAkPN6DN.mjs";
3
3
  export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, ProjectLauncherSchema, Result, SchemaNameSchema, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, getSchemaResult, router, sponsors };
@@ -234,6 +234,14 @@ const ADDON_COMPATIBILITY = {
234
234
  //#endregion
235
235
  //#region src/utils/context.ts
236
236
  const cliStorage = new AsyncLocalStorage();
237
+ /** Process-wide mode for hosts that own the whole process, such as the MCP server. */
238
+ let processMode;
239
+ function setProcessMode(mode) {
240
+ processMode = mode;
241
+ }
242
+ function getProcessMode() {
243
+ return processMode;
244
+ }
237
245
  function defaultContext() {
238
246
  return {
239
247
  navigation: {
@@ -241,7 +249,9 @@ function defaultContext() {
241
249
  lastPromptShownUI: false
242
250
  },
243
251
  silent: false,
244
- verbose: false
252
+ verbose: false,
253
+ promptShown: false,
254
+ analyticsDisabled: false
245
255
  };
246
256
  }
247
257
  function getContext() {
@@ -264,6 +274,20 @@ function didLastPromptShowUI() {
264
274
  function getPromptProgress() {
265
275
  return getContext().navigation.promptProgress;
266
276
  }
277
+ function markPromptShown() {
278
+ const ctx = tryGetContext();
279
+ if (ctx) ctx.promptShown = true;
280
+ }
281
+ /**
282
+ * How this run is being driven. Hosts that know (json, api, mcp) set it up front;
283
+ * otherwise it is derived from whether prompts were rendered.
284
+ */
285
+ function resolveInvocationMode(yes) {
286
+ const ctx = getContext();
287
+ if (ctx.mode) return ctx.mode;
288
+ if (yes) return "yes";
289
+ return ctx.promptShown ? "interactive" : "flags";
290
+ }
267
291
  function setIsFirstPrompt$1(value) {
268
292
  const ctx = tryGetContext();
269
293
  if (ctx) ctx.navigation.isFirstPrompt = value;
@@ -276,19 +300,24 @@ function setPromptProgress(value) {
276
300
  const ctx = tryGetContext();
277
301
  if (ctx) ctx.navigation.promptProgress = value;
278
302
  }
279
- async function runWithContextAsync(options, fn) {
280
- const ctx = {
303
+ function createContext(options) {
304
+ return {
281
305
  navigation: {
282
306
  isFirstPrompt: false,
283
307
  lastPromptShownUI: false
284
308
  },
285
309
  silent: options.silent ?? false,
286
310
  verbose: options.verbose ?? false,
311
+ mode: options.mode ?? processMode,
312
+ promptShown: false,
313
+ analyticsDisabled: options.analyticsDisabled ?? false,
287
314
  projectDir: options.projectDir,
288
315
  projectName: options.projectName,
289
316
  packageManager: options.packageManager
290
317
  };
291
- return cliStorage.run(ctx, fn);
318
+ }
319
+ async function runWithContextAsync(options, fn) {
320
+ return cliStorage.run(createContext(options), fn);
292
321
  }
293
322
  //#endregion
294
323
  //#region src/utils/terminal-output.ts
@@ -348,7 +377,10 @@ const cliConsola = {
348
377
  */
349
378
  var UserCancelledError = class extends TaggedError("UserCancelledError") {
350
379
  constructor(args) {
351
- super({ message: args?.message ?? "Operation cancelled" });
380
+ super({
381
+ message: args?.message ?? "Operation cancelled",
382
+ prompt: args?.prompt
383
+ });
352
384
  }
353
385
  };
354
386
  /**
@@ -633,34 +665,36 @@ function formatHistoryEntry(entry, index) {
633
665
  const details = rows.map(({ label, value }) => `${pc.dim(label.padEnd(labelWidth))} ${value}`).join("\n");
634
666
  return `${pc.cyan(pc.bold(`${index + 1}. ${entry.projectName}`))}\n${details}\n${pc.dim("Recreate")}\n${pc.cyan(entry.reproducibleCommand)}`;
635
667
  }
668
+ /** Resolves to whether the command did what was asked; failures are reported to the user here. */
636
669
  async function historyHandler(input) {
637
670
  if (input.clear) {
638
671
  const clearResult = await clearHistory();
639
672
  if (clearResult.isErr()) {
640
673
  log.warn(pc.yellow(clearResult.error.message));
641
- return;
674
+ return false;
642
675
  }
643
676
  log.success(pc.green("Project history cleared."));
644
- return;
677
+ return true;
645
678
  }
646
679
  const historyResult = await getHistory(input.limit);
647
680
  if (historyResult.isErr()) {
648
681
  log.warn(pc.yellow(historyResult.error.message));
649
- return;
682
+ return false;
650
683
  }
651
684
  const entries = historyResult.value;
652
685
  if (input.json) {
653
686
  console.log(JSON.stringify(entries, null, 2));
654
- return;
687
+ return true;
655
688
  }
656
689
  renderTitle();
657
690
  intro(pc.magenta(`Project history · ${entries.length}`));
658
691
  if (entries.length === 0) {
659
692
  outro(`${pc.dim("No saved projects yet · create one with")} ${pc.cyan("create-better-t-stack my-app")}`);
660
- return;
693
+ return true;
661
694
  }
662
695
  log.message(entries.map(formatHistoryEntry).join("\n\n"));
663
696
  outro(pc.dim("Run a command above to recreate that project"));
697
+ return true;
664
698
  }
665
699
  //#endregion
666
700
  //#region src/utils/open-url.ts
@@ -1704,6 +1738,125 @@ async function updateBtsConfig(projectDir, updates) {
1704
1738
  } catch {}
1705
1739
  }
1706
1740
  //#endregion
1741
+ //#region src/utils/telemetry.ts
1742
+ /**
1743
+ * Returns true if telemetry/analytics should be enabled, false otherwise.
1744
+ *
1745
+ * - DO_NOT_TRACK=1 (the cross-tool convention from consoledonottrack.com) disables analytics.
1746
+ * - If BTS_TELEMETRY_DISABLED is present and "1", disables analytics.
1747
+ * - Otherwise, BTS_TELEMETRY: "0" disables, "1" enables (default: enabled).
1748
+ */
1749
+ function isTelemetryEnabled() {
1750
+ const DO_NOT_TRACK = process.env.DO_NOT_TRACK;
1751
+ const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
1752
+ if (DO_NOT_TRACK === "1" || DO_NOT_TRACK?.toLowerCase() === "true") return false;
1753
+ if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
1754
+ return true;
1755
+ }
1756
+ //#endregion
1757
+ //#region src/utils/diagnostics.ts
1758
+ /**
1759
+ * Diagnostic events go to the self-hosted Umami instance (a separate "CLI" website),
1760
+ * not to the Convex project dataset. They cover what the project-creation event
1761
+ * cannot: failures, cancellations, non-create commands, slow stages, and MCP usage.
1762
+ * Everything is a no-op until UMAMI_CLI_WEBSITE_ID is baked in at build time.
1763
+ */
1764
+ const UMAMI_HOST_URL = "https://umami.amanv.cloud";
1765
+ const UMAMI_CLI_WEBSITE_ID = "e658611d-dbcc-4d3a-bc4e-182b9f5b0d5d";
1766
+ const SEND_TIMEOUT_MS$1 = 3e3;
1767
+ const MAX_STRING_LENGTH = 500;
1768
+ const MAX_REASON_LENGTH = 160;
1769
+ const DURATION_BUCKETS = [
1770
+ [1e3, "<1s"],
1771
+ [5e3, "1-5s"],
1772
+ [15e3, "5-15s"],
1773
+ [6e4, "15-60s"],
1774
+ [3e5, "1-5m"]
1775
+ ];
1776
+ function durationBucket(elapsedMs) {
1777
+ for (const [limit, label] of DURATION_BUCKETS) if (elapsedMs < limit) return label;
1778
+ return ">5m";
1779
+ }
1780
+ /** Class name of the failure, which is stable and never carries user content. */
1781
+ function errorClass(cause) {
1782
+ if (cause instanceof Error) return cause.name || cause.constructor.name || "Error";
1783
+ return "UnknownError";
1784
+ }
1785
+ /** Coarse stage a create/add failure belongs to, derived from the tagged error types. */
1786
+ function failureStage(cause) {
1787
+ if (ProjectCreationError.is(cause)) return cause.phase;
1788
+ if (DatabaseSetupError.is(cause)) return "database-setup";
1789
+ if (AddonSetupError.is(cause)) return "addons-setup";
1790
+ if (DirectoryConflictError.is(cause)) return "directory";
1791
+ if (ValidationError.is(cause) || CompatibilityError.is(cause)) return "validate";
1792
+ if (CLIError.is(cause)) return "config";
1793
+ return "unknown";
1794
+ }
1795
+ /**
1796
+ * First line of an error message with anything that could identify a machine or
1797
+ * project replaced by placeholders: file paths, URLs, emails, and quoted names.
1798
+ */
1799
+ function scrubReason(cause) {
1800
+ const scrubbed = ((cause instanceof Error ? cause.message : String(cause)).split(/\r?\n/, 1)[0] ?? "").replaceAll(/(["'`])(?:(?!\1).)*\1/g, "<name>").replaceAll(/https?:\/\/[^\s)\]"'>]+/gi, "<url>").replaceAll(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "<email>").replaceAll(/(?:[a-zA-Z]:)?(?:[\\/][\w.@+ -]+)+[\\/][\w.@+-]+(?:[ \w.@+-]*\.\w+)?/g, "<path>").replaceAll(/\S*[\\/]\S*/g, "<path>").replaceAll(/\s+/g, " ").trim();
1801
+ return scrubbed.length > MAX_REASON_LENGTH ? scrubbed.slice(0, MAX_REASON_LENGTH) : scrubbed;
1802
+ }
1803
+ function clampValue(value) {
1804
+ const text = String(value);
1805
+ return text.length > MAX_STRING_LENGTH ? text.slice(0, MAX_STRING_LENGTH) : value;
1806
+ }
1807
+ function isDiagnosticsEnabled() {
1808
+ return Boolean(UMAMI_HOST_URL) && Boolean(UMAMI_CLI_WEBSITE_ID) && isTelemetryEnabled() && !getContext().analyticsDisabled;
1809
+ }
1810
+ function eventUrl(name, data) {
1811
+ if (name.startsWith("mcp_")) return "/mcp";
1812
+ const command = data.command;
1813
+ return command === void 0 ? `/${name}` : `/${String(command)}`;
1814
+ }
1815
+ function buildDiagnosticPayload(name, data) {
1816
+ const eventData = {};
1817
+ for (const [key, value] of Object.entries(data)) if (value !== void 0) eventData[key] = clampValue(value);
1818
+ return {
1819
+ type: "event",
1820
+ payload: {
1821
+ website: UMAMI_CLI_WEBSITE_ID,
1822
+ hostname: "cli",
1823
+ url: eventUrl(name, eventData),
1824
+ title: name,
1825
+ name,
1826
+ data: eventData
1827
+ }
1828
+ };
1829
+ }
1830
+ function diagnosticUserAgent() {
1831
+ return `Mozilla/5.0 (compatible; create-better-t-stack/${getLatestCLIVersion()})`;
1832
+ }
1833
+ async function reportDiagnostic(name, data) {
1834
+ if (!isDiagnosticsEnabled()) return;
1835
+ await Result.tryPromise({
1836
+ try: () => fetch(`${UMAMI_HOST_URL}/api/send`, {
1837
+ method: "POST",
1838
+ headers: {
1839
+ "Content-Type": "application/json",
1840
+ "User-Agent": diagnosticUserAgent()
1841
+ },
1842
+ body: JSON.stringify(buildDiagnosticPayload(name, data)),
1843
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS$1),
1844
+ keepalive: true
1845
+ }),
1846
+ catch: () => void 0
1847
+ });
1848
+ }
1849
+ /** Reports a stage only when it crossed the slow threshold, so the event is a signal. */
1850
+ async function reportSlowStage(command, stage, elapsedMs, packageManager) {
1851
+ if (elapsedMs < 6e4) return;
1852
+ await reportDiagnostic("cli_slow", {
1853
+ command,
1854
+ stage,
1855
+ duration: durationBucket(elapsedMs),
1856
+ packageManager
1857
+ });
1858
+ }
1859
+ //#endregion
1707
1860
  //#region src/utils/input-hardening.ts
1708
1861
  function hasControlCharacters(value) {
1709
1862
  for (const char of value) {
@@ -2561,6 +2714,7 @@ async function navigableGroup(prompts, opts) {
2561
2714
  setIsFirstPrompt$1(currentIndex === 0);
2562
2715
  setLastPromptShownUI(false);
2563
2716
  const presetResult = opts?.preselected?.[name];
2717
+ if (presetResult === void 0) markPromptShown();
2564
2718
  const result = presetResult !== void 0 ? presetResult : await prompt({
2565
2719
  results,
2566
2720
  previousAnswer: previousAnswers[name]
@@ -2577,7 +2731,10 @@ async function navigableGroup(prompts, opts) {
2577
2731
  if (isCancel$1(result)) {
2578
2732
  if (opts?.onCancel) {
2579
2733
  results[name] = "canceled";
2580
- opts.onCancel({ results });
2734
+ opts.onCancel({
2735
+ results,
2736
+ prompt: String(name)
2737
+ });
2581
2738
  }
2582
2739
  return results;
2583
2740
  }
@@ -4759,9 +4916,14 @@ function updateViteConfigImportsForVitePlus(vfs) {
4759
4916
  }
4760
4917
  }
4761
4918
  async function addHandler(input, options = {}) {
4762
- const { silent = false } = options;
4763
- return runWithContextAsync({ silent }, async () => {
4919
+ const { silent = false, mode } = options;
4920
+ return runWithContextAsync({
4921
+ silent,
4922
+ mode
4923
+ }, async () => {
4924
+ const startTime = Date.now();
4764
4925
  const result = await addHandlerInternal(input);
4926
+ await reportAddOutcome(input, result, Date.now() - startTime);
4765
4927
  if (result.isOk()) return result.value;
4766
4928
  const error = result.error;
4767
4929
  if (UserCancelledError.is(error)) {
@@ -4783,6 +4945,42 @@ async function addHandler(input, options = {}) {
4783
4945
  process.exit(1);
4784
4946
  });
4785
4947
  }
4948
+ async function reportAddOutcome(input, result, elapsedMs) {
4949
+ const mode = resolveInvocationMode(false);
4950
+ const duration = durationBucket(elapsedMs);
4951
+ if (result.isOk()) {
4952
+ await reportDiagnostic("cli_command", {
4953
+ command: "add",
4954
+ mode,
4955
+ ok: true,
4956
+ duration
4957
+ });
4958
+ return;
4959
+ }
4960
+ const error = result.error;
4961
+ if (UserCancelledError.is(error)) {
4962
+ await reportDiagnostic("cli_cancelled", {
4963
+ command: "add",
4964
+ mode,
4965
+ prompt: error.prompt ?? "unknown"
4966
+ });
4967
+ return;
4968
+ }
4969
+ await reportDiagnostic("cli_command", {
4970
+ command: "add",
4971
+ mode,
4972
+ ok: false,
4973
+ duration
4974
+ });
4975
+ await reportDiagnostic("cli_failed", {
4976
+ command: "add",
4977
+ mode,
4978
+ stage: failureStage(error),
4979
+ error: errorClass(error),
4980
+ reason: scrubReason(error),
4981
+ packageManager: input.packageManager
4982
+ });
4983
+ }
4786
4984
  async function addHandlerInternal(input) {
4787
4985
  const projectDir = input.projectDir || process.cwd();
4788
4986
  const hardeningResult = validateAgentSafePathInput(projectDir, "projectDir");
@@ -4795,7 +4993,7 @@ async function addHandlerInternal(input) {
4795
4993
  intro(pc.magenta("Add to your project"));
4796
4994
  }
4797
4995
  const existingConfig = await detectProjectConfig(projectDir);
4798
- if (!existingConfig) return Result.err(new CLIError({ message: `No Better-T-Stack project found in ${projectDir}. Make sure bts.jsonc exists.` }));
4996
+ if (!existingConfig) return Result.err(new CLIError({ message: `No Better-T-Stack project found in "${projectDir}". Make sure bts.jsonc exists.` }));
4799
4997
  if (!isSilent()) log.info(pc.dim(`Detected project: ${existingConfig.projectName}`));
4800
4998
  let addonsToAdd;
4801
4999
  if (input.addons && input.addons.length > 0) {
@@ -6080,8 +6278,11 @@ async function gatherConfig(flags, projectName, projectDir, relativePath, option
6080
6278
  ]
6081
6279
  }
6082
6280
  ],
6083
- onCancel: () => {
6084
- throw new UserCancelledError({ message: "Operation cancelled" });
6281
+ onCancel: ({ prompt }) => {
6282
+ throw new UserCancelledError({
6283
+ message: "Operation cancelled",
6284
+ prompt
6285
+ });
6085
6286
  }
6086
6287
  });
6087
6288
  return {
@@ -6151,6 +6352,7 @@ async function getProjectName(initialName) {
6151
6352
  counter++;
6152
6353
  }
6153
6354
  while (!isValid) {
6355
+ markPromptShown();
6154
6356
  const response = await text({
6155
6357
  message: "Where should we create your project?",
6156
6358
  placeholder: defaultName,
@@ -6165,40 +6367,34 @@ async function getProjectName(initialName) {
6165
6367
  }
6166
6368
  }
6167
6369
  });
6168
- if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
6370
+ if (isCancel(response)) throw new UserCancelledError({
6371
+ message: "Operation cancelled.",
6372
+ prompt: "projectName"
6373
+ });
6169
6374
  projectPath = response || defaultName;
6170
6375
  isValid = true;
6171
6376
  }
6172
6377
  return projectPath;
6173
6378
  }
6174
6379
  //#endregion
6175
- //#region src/utils/telemetry.ts
6176
- /**
6177
- * Returns true if telemetry/analytics should be enabled, false otherwise.
6178
- *
6179
- * - If BTS_TELEMETRY_DISABLED is present and "1", disables analytics.
6180
- * - Otherwise, BTS_TELEMETRY: "0" disables, "1" enables (default: enabled).
6181
- */
6182
- function isTelemetryEnabled() {
6183
- const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
6184
- if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
6185
- return true;
6186
- }
6187
- //#endregion
6188
6380
  //#region src/utils/analytics.ts
6189
6381
  const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
6382
+ const SEND_TIMEOUT_MS = 3e3;
6190
6383
  async function sendConvexEvent(payload) {
6191
6384
  await Result.tryPromise({
6192
6385
  try: () => fetch(CONVEX_INGEST_URL, {
6193
6386
  method: "POST",
6194
6387
  headers: { "Content-Type": "application/json" },
6195
- body: JSON.stringify(payload)
6388
+ body: JSON.stringify(payload),
6389
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
6390
+ keepalive: true
6196
6391
  }),
6197
6392
  catch: () => void 0
6198
6393
  });
6199
6394
  }
6200
- function buildAnalyticsEvent(config) {
6395
+ function buildAnalyticsEvent(config, mode) {
6201
6396
  return types_exports.AnalyticsEventSchema.parse({
6397
+ mode,
6202
6398
  database: config.database,
6203
6399
  orm: config.orm,
6204
6400
  backend: config.backend,
@@ -6220,10 +6416,10 @@ function buildAnalyticsEvent(config) {
6220
6416
  platform: process.platform
6221
6417
  });
6222
6418
  }
6223
- async function trackProjectCreation(config, disableAnalytics = false) {
6419
+ async function trackProjectCreation(config, disableAnalytics = false, mode) {
6224
6420
  if (!isTelemetryEnabled() || disableAnalytics) return;
6225
6421
  await Result.tryPromise({
6226
- try: () => sendConvexEvent(buildAnalyticsEvent(config)),
6422
+ try: () => sendConvexEvent(buildAnalyticsEvent(config, mode)),
6227
6423
  catch: () => void 0
6228
6424
  });
6229
6425
  }
@@ -8809,10 +9005,14 @@ async function createProject(options, cliInput) {
8809
9005
  }));
8810
9006
  yield* Result.await(formatProject(projectDir));
8811
9007
  if (!isSilent()) log.success("Project scaffolded");
8812
- if (options.install) yield* Result.await(installDependencies({
8813
- projectDir,
8814
- packageManager: options.packageManager
8815
- }));
9008
+ if (options.install) {
9009
+ const installStartTime = Date.now();
9010
+ yield* Result.await(installDependencies({
9011
+ projectDir,
9012
+ packageManager: options.packageManager
9013
+ }));
9014
+ await reportSlowStage("create", "install", Date.now() - installStartTime, options.packageManager);
9015
+ }
8816
9016
  yield* Result.await(initializeGit(projectDir, options.git));
8817
9017
  if (!isSilent()) await displayPostInstallInstructions({
8818
9018
  ...options,
@@ -8875,17 +9075,49 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
8875
9075
  };
8876
9076
  }
8877
9077
  async function executeCreateProjectHandler(input, options) {
8878
- const { silent = false } = options;
8879
- return runWithContextAsync({ silent }, async () => {
9078
+ const { silent = false, mode } = options;
9079
+ return runWithContextAsync({
9080
+ silent,
9081
+ mode,
9082
+ analyticsDisabled: input.disableAnalytics
9083
+ }, async () => {
8880
9084
  const startTime = Date.now();
8881
9085
  const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
9086
+ const result = await createProjectHandlerInternal(input, startTime, timeScaffolded);
9087
+ await reportCreateOutcome(input, result, Date.now() - startTime);
8882
9088
  return {
8883
- result: await createProjectHandlerInternal(input, startTime, timeScaffolded),
9089
+ result,
8884
9090
  startTime,
8885
9091
  timeScaffolded
8886
9092
  };
8887
9093
  });
8888
9094
  }
9095
+ /** Diagnostics for what the success-only project event cannot show; awaited so it beats process.exit. */
9096
+ async function reportCreateOutcome(input, result, elapsedMs) {
9097
+ const mode = resolveInvocationMode(input.yes);
9098
+ if (result.isOk()) {
9099
+ if (!input.dryRun) await reportSlowStage("create", "create", elapsedMs, input.packageManager ?? "unknown");
9100
+ return;
9101
+ }
9102
+ const error = result.error;
9103
+ if (UserCancelledError.is(error)) {
9104
+ await reportDiagnostic("cli_cancelled", {
9105
+ command: "create",
9106
+ mode,
9107
+ prompt: error.prompt ?? "unknown"
9108
+ });
9109
+ return;
9110
+ }
9111
+ await reportDiagnostic("cli_failed", {
9112
+ command: "create",
9113
+ mode,
9114
+ stage: failureStage(error),
9115
+ error: errorClass(error),
9116
+ reason: scrubReason(error),
9117
+ packageManager: input.packageManager,
9118
+ backend: input.backend
9119
+ });
9120
+ }
8889
9121
  async function createProjectHandlerResult(input, options = {}) {
8890
9122
  return (await executeCreateProjectHandler(input, options)).result;
8891
9123
  }
@@ -9058,7 +9290,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
9058
9290
  dbSetupOptions: effectiveDbSetupOptions,
9059
9291
  packageManagerVersion: localRequirements.packageManagerVersion
9060
9292
  }));
9061
- await trackProjectCreation(config, input.disableAnalytics);
9293
+ await trackProjectCreation(config, input.disableAnalytics, resolveInvocationMode(input.yes));
9062
9294
  const historyResult = await addToHistory(config, reproducibleCommand);
9063
9295
  if (historyResult.isErr() && !isSilent()) {
9064
9296
  log.warn(pc.yellow(historyResult.error.message));
@@ -9259,15 +9491,18 @@ const router = t.router({
9259
9491
  description: "Create a project from a raw JSON payload (agent-friendly)",
9260
9492
  jsonInput: "always"
9261
9493
  }).input(types_exports.CreateInputSchema).mutation(async ({ input }) => {
9262
- const result = await createProjectHandler(input, { silent: true });
9494
+ const result = await createProjectHandler(input, {
9495
+ silent: true,
9496
+ mode: "json"
9497
+ });
9263
9498
  if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
9264
9499
  if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
9265
9500
  return result;
9266
9501
  }),
9267
9502
  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)),
9268
- sponsors: t.procedure.meta({ description: "Show Better-T-Stack sponsors" }).mutation(() => showSponsorsCommand()),
9269
- docs: t.procedure.meta({ description: "Open Better-T-Stack documentation" }).mutation(() => openDocsCommand()),
9270
- builder: t.procedure.meta({ description: "Open the web-based stack builder" }).mutation(() => openBuilderCommand()),
9503
+ sponsors: t.procedure.meta({ description: "Show Better-T-Stack sponsors" }).mutation(() => trackCommand("sponsors", () => showSponsorsCommand())),
9504
+ docs: t.procedure.meta({ description: "Open Better-T-Stack documentation" }).mutation(() => trackCommand("docs", () => openDocsCommand())),
9505
+ builder: t.procedure.meta({ description: "Open the web-based stack builder" }).mutation(() => trackCommand("builder", () => openBuilderCommand())),
9271
9506
  add: t.procedure.meta({ description: "Add addons or a workspace package to an existing Better-T-Stack project" }).input(z.object({
9272
9507
  addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
9273
9508
  package: types_exports.WorkspacePackageNameSchema.optional(),
@@ -9282,7 +9517,10 @@ const router = t.router({
9282
9517
  description: "Add addons or a workspace package from a raw JSON payload (agent-friendly)",
9283
9518
  jsonInput: "always"
9284
9519
  }).input(types_exports.AddInputSchema).mutation(async ({ input }) => {
9285
- const result = await addHandler(input, { silent: true });
9520
+ const result = await addHandler(input, {
9521
+ silent: true,
9522
+ mode: "json"
9523
+ });
9286
9524
  if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
9287
9525
  if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
9288
9526
  return result;
@@ -9292,9 +9530,29 @@ const router = t.router({
9292
9530
  clear: z.boolean().optional().default(false).describe("Clear all history"),
9293
9531
  json: z.boolean().optional().default(false).describe("Output as JSON")
9294
9532
  })).mutation(async ({ input }) => {
9295
- await historyHandler(input);
9533
+ await trackCommand("history", () => historyHandler(input), (ok) => ok);
9296
9534
  })
9297
9535
  });
9536
+ /** Usage diagnostics for commands that have no project event of their own. */
9537
+ async function trackCommand(command, run, isOk = () => true) {
9538
+ const startTime = Date.now();
9539
+ try {
9540
+ const value = await run();
9541
+ await reportDiagnostic("cli_command", {
9542
+ command,
9543
+ ok: isOk(value),
9544
+ duration: durationBucket(Date.now() - startTime)
9545
+ });
9546
+ return value;
9547
+ } catch (cause) {
9548
+ await reportDiagnostic("cli_command", {
9549
+ command,
9550
+ ok: false,
9551
+ duration: durationBucket(Date.now() - startTime)
9552
+ });
9553
+ throw cause;
9554
+ }
9555
+ }
9298
9556
  function createBtsCli() {
9299
9557
  return createCli({
9300
9558
  router,
@@ -9352,7 +9610,10 @@ async function create(projectName, options) {
9352
9610
  };
9353
9611
  return Result.tryPromise({
9354
9612
  try: async () => {
9355
- const result = await createProjectHandlerResult(input, { silent: true });
9613
+ const result = await createProjectHandlerResult(input, {
9614
+ silent: true,
9615
+ mode: getProcessMode() ?? "api"
9616
+ });
9356
9617
  if (result.isErr()) throw result.error;
9357
9618
  return result.value;
9358
9619
  },
@@ -9467,7 +9728,10 @@ async function add(options = {}) {
9467
9728
  projectDir: "",
9468
9729
  error: formatInputValidationError("add", parsedInput.error)
9469
9730
  };
9470
- return await addHandler(parsedInput.data, { silent: true }) ?? {
9731
+ return await addHandler(parsedInput.data, {
9732
+ silent: true,
9733
+ mode: getProcessMode() ?? "api"
9734
+ }) ?? {
9471
9735
  success: false,
9472
9736
  addedAddons: [],
9473
9737
  projectDir: parsedInput.data.projectDir ?? "",
@@ -9475,4 +9739,4 @@ async function add(options = {}) {
9475
9739
  };
9476
9740
  }
9477
9741
  //#endregion
9478
- export { DirectoryConflictError as C, ValidationError as E, DatabaseSetupError as S, UserCancelledError as T, ProjectLauncherSchema as _, TEMPLATE_COUNT as a, CLIError 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, types_exports as v, ProjectCreationError as w, CompatibilityError as x, getLatestCLIVersion as y };
9742
+ export { setProcessMode as A, CLIError as C, ProjectCreationError as D, DirectoryConflictError as E, UserCancelledError as O, getLatestCLIVersion as S, DatabaseSetupError as T, ProjectLauncherSchema as _, TEMPLATE_COUNT as a, scrubReason as b, builder as c, createVirtual as d, docs as f, sponsors as g, router as h, SchemaNameSchema as i, ValidationError as k, 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, durationBucket as v, CompatibilityError as w, types_exports as x, reportDiagnostic as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.40.5",
3
+ "version": "3.41.0",
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",
@@ -69,11 +69,11 @@
69
69
  "prepublishOnly": "npm run build"
70
70
  },
71
71
  "dependencies": {
72
- "@better-t-stack/template-generator": "^3.40.5",
73
- "@better-t-stack/types": "^3.40.5",
72
+ "@better-t-stack/template-generator": "^3.41.0",
73
+ "@better-t-stack/types": "^3.41.0",
74
74
  "@clack/core": "^1.4.3",
75
75
  "@clack/prompts": "^1.7.0",
76
- "@modelcontextprotocol/sdk": "1.30.0",
76
+ "@modelcontextprotocol/server": "2.0.0",
77
77
  "@trpc/server": "^11.18.0",
78
78
  "better-result": "^3.0.0",
79
79
  "consola": "^3.4.2",
@@ -93,6 +93,7 @@
93
93
  "zod": "^4.4.3"
94
94
  },
95
95
  "devDependencies": {
96
+ "@modelcontextprotocol/client": "2.0.0",
96
97
  "@types/bun": "^1.3.14",
97
98
  "@types/fs-extra": "^11.0.4",
98
99
  "@types/node": "^26.1.2",