create-better-t-stack 3.40.4 → 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-CpGAduw0.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,
@@ -79,7 +117,7 @@ function getStackGuidance() {
79
117
  "For project creation, build a full explicit config before calling bts_plan_project.",
80
118
  "Always call bts_plan_project before bts_create_project.",
81
119
  "Only call bts_create_project after the plan succeeds and matches the user's intent.",
82
- "Use bts_plan_addons before bts_add_addons for existing projects."
120
+ "Use bts_plan_addons before bts_add_addons when adding addons or scaffolding a workspace package in an existing project."
83
121
  ],
84
122
  createContract: {
85
123
  requiresExplicitFields: [
@@ -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,69 +252,67 @@ 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
- title: "Plan Better T Stack Addons",
228
- description: "Validate and preview addon installation for an existing Better T Stack project without writing files. Always use this before bts_add_addons when the addon set or nested options are uncertain.",
269
+ title: "Plan Better T Stack Project Additions",
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.",
229
271
  inputSchema: types_exports.AddInputSchema,
230
272
  outputSchema: ToolResponseSchema,
231
273
  annotations: {
232
- title: "Plan Better T Stack Addons",
274
+ title: "Plan Better T Stack Project Additions",
233
275
  readOnlyHint: true,
234
276
  destructiveHint: false,
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,
242
284
  dryRun: true
243
285
  });
244
- if (!result?.success) return formatToolError(result?.error ?? "Failed to plan addon installation");
286
+ if (!result?.success) return formatToolError(result?.error ?? "Failed to plan project additions");
245
287
  return formatToolSuccess(result);
246
288
  } catch (error) {
247
289
  return formatToolError(error);
248
290
  }
249
- });
291
+ }));
250
292
  server.registerTool("bts_add_addons", {
251
- title: "Add Better T Stack Addons",
252
- description: "Install addons into 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.",
293
+ title: "Apply Better T Stack Project Additions",
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.",
253
295
  inputSchema: types_exports.AddInputSchema,
254
296
  outputSchema: ToolResponseSchema,
255
297
  annotations: {
256
- title: "Add Better T Stack Addons",
298
+ title: "Apply Better T Stack Project Additions",
257
299
  destructiveHint: true,
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
- if (!result?.success) return formatToolError(result?.error ?? "Failed to add addons");
306
+ if (!result?.success) return formatToolError(result?.error ?? "Failed to update project");
265
307
  return formatToolSuccess(result);
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">;
@@ -153,6 +156,7 @@ interface AddResult {
153
156
  projectDir: string;
154
157
  dryRun?: boolean;
155
158
  plannedFileCount?: number;
159
+ addedPackage?: string;
156
160
  error?: string;
157
161
  }
158
162
  //#endregion
@@ -349,6 +353,7 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
349
353
  add: import("@trpc/server").TRPCMutationProcedure<{
350
354
  input: {
351
355
  addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
356
+ package?: string | undefined;
352
357
  install?: boolean | undefined;
353
358
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
354
359
  projectDir?: string | undefined;
@@ -360,6 +365,7 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
360
365
  addJson: import("@trpc/server").TRPCMutationProcedure<{
361
366
  input: {
362
367
  addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
368
+ package?: string | undefined;
363
369
  addonOptions?: {
364
370
  wxt?: {
365
371
  template: "svelte" | "solid" | "vanilla" | "vue" | "react";
@@ -473,9 +479,9 @@ declare function builder(): Promise<void>;
473
479
  * ```
474
480
  */
475
481
  declare function createVirtual(options: Partial<Omit<types_d_exports.ProjectConfig, "projectDir" | "relativePath">>): Promise<Result$1<VirtualFileTree$1, GeneratorError$1>>;
476
- type AddOptions = Pick<types_d_exports.AddInput, "addons" | "addonOptions" | "install" | "packageManager" | "projectDir" | "dryRun">;
482
+ type AddOptions = Pick<types_d_exports.AddInput, "addons" | "addonOptions" | "package" | "install" | "packageManager" | "projectDir" | "dryRun">;
477
483
  /**
478
- * Programmatic API to add addons to an existing Better-T-Stack project.
484
+ * Programmatic API to add addons or a workspace package to an existing Better-T-Stack project.
479
485
  *
480
486
  * @example
481
487
  * ```typescript
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-CpGAduw0.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
  }
@@ -4653,6 +4810,68 @@ const TASK_RUNNER_ADDONS = [
4653
4810
  "nx",
4654
4811
  "vite-plus"
4655
4812
  ];
4813
+ const fileExistsErrorSchema = z.object({ code: z.literal("EEXIST") });
4814
+ const configPackageScopeSchema = z.object({ name: z.string().regex(/^@[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\/config$/) }).transform(({ name }) => name.slice(0, -7));
4815
+ const rootTypescriptVersionSchema = z.object({ devDependencies: z.object({ typescript: z.string().min(1) }) }).transform(({ devDependencies }) => devDependencies.typescript);
4816
+ function isFileExistsError(cause) {
4817
+ return fileExistsErrorSchema.safeParse(cause).success;
4818
+ }
4819
+ async function reserveWorkspacePackage(projectDir, packageName) {
4820
+ const packageDir = path.join(projectDir, "packages", packageName);
4821
+ return Result.tryPromise({
4822
+ try: async () => {
4823
+ await fs.mkdir(packageDir);
4824
+ return packageDir;
4825
+ },
4826
+ catch: (cause) => new CLIError({
4827
+ message: isFileExistsError(cause) ? `Workspace package already exists: packages/${packageName}` : `Failed to reserve workspace package: packages/${packageName}`,
4828
+ cause
4829
+ })
4830
+ });
4831
+ }
4832
+ async function addWorkspacePackage(vfs, projectDir, packageName, packageManager) {
4833
+ const packageDir = path.join(projectDir, "packages", packageName);
4834
+ if (await fs.pathExists(packageDir)) return Result.err(new CLIError({ message: `Workspace package already exists: packages/${packageName}` }));
4835
+ const configPackagePath = path.join(projectDir, "packages", "config", "package.json");
4836
+ const packageScopeResult = await Result.tryPromise({
4837
+ try: async () => configPackageScopeSchema.parse(await fs.readJson(configPackagePath)),
4838
+ catch: (cause) => new CLIError({
4839
+ message: "Cannot determine the workspace package scope. Expected packages/config/package.json to have a name like @my-app/config.",
4840
+ cause
4841
+ })
4842
+ });
4843
+ if (packageScopeResult.isErr()) return Result.err(packageScopeResult.error);
4844
+ const typescriptVersionResult = await Result.tryPromise({
4845
+ try: async () => rootTypescriptVersionSchema.parse(await fs.readJson(path.join(projectDir, "package.json"))),
4846
+ catch: (cause) => new CLIError({
4847
+ message: "Cannot determine the TypeScript version. Expected package.json to declare devDependencies.typescript.",
4848
+ cause
4849
+ })
4850
+ });
4851
+ if (typescriptVersionResult.isErr()) return Result.err(typescriptVersionResult.error);
4852
+ const packageScope = packageScopeResult.value;
4853
+ const fullPackageName = `${packageScope}/${packageName}`;
4854
+ if (fullPackageName.length > 214) return Result.err(new CLIError({ message: "Workspace package name must not exceed 214 characters including its scope." }));
4855
+ const packagePath = `packages/${packageName}`;
4856
+ vfs.writeFile(`${packagePath}/package.json`, `${JSON.stringify({
4857
+ name: fullPackageName,
4858
+ version: "0.0.0",
4859
+ private: true,
4860
+ type: "module",
4861
+ exports: { ".": "./src/index.ts" },
4862
+ scripts: { "check-types": "tsc --noEmit" },
4863
+ devDependencies: {
4864
+ [`${packageScope}/config`]: packageManager === "npm" ? "*" : "workspace:*",
4865
+ typescript: typescriptVersionResult.value
4866
+ }
4867
+ }, null, 2)}\n`);
4868
+ vfs.writeFile(`${packagePath}/tsconfig.json`, `${JSON.stringify({
4869
+ extends: `${packageScope}/config/tsconfig.base.json`,
4870
+ include: ["src/**/*.ts"]
4871
+ }, null, 2)}\n`);
4872
+ vfs.writeFile(`${packagePath}/src/index.ts`, "export {};\n");
4873
+ return Result.ok(void 0);
4874
+ }
4656
4875
  function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
4657
4876
  if (!existingAddonOptions && !nextAddonOptions) return;
4658
4877
  const mergeOption = (existing, next) => {
@@ -4697,9 +4916,14 @@ function updateViteConfigImportsForVitePlus(vfs) {
4697
4916
  }
4698
4917
  }
4699
4918
  async function addHandler(input, options = {}) {
4700
- const { silent = false } = options;
4701
- return runWithContextAsync({ silent }, async () => {
4919
+ const { silent = false, mode } = options;
4920
+ return runWithContextAsync({
4921
+ silent,
4922
+ mode
4923
+ }, async () => {
4924
+ const startTime = Date.now();
4702
4925
  const result = await addHandlerInternal(input);
4926
+ await reportAddOutcome(input, result, Date.now() - startTime);
4703
4927
  if (result.isOk()) return result.value;
4704
4928
  const error = result.error;
4705
4929
  if (UserCancelledError.is(error)) {
@@ -4721,6 +4945,42 @@ async function addHandler(input, options = {}) {
4721
4945
  process.exit(1);
4722
4946
  });
4723
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
+ }
4724
4984
  async function addHandlerInternal(input) {
4725
4985
  const projectDir = input.projectDir || process.cwd();
4726
4986
  const hardeningResult = validateAgentSafePathInput(projectDir, "projectDir");
@@ -4733,12 +4993,12 @@ async function addHandlerInternal(input) {
4733
4993
  intro(pc.magenta("Add to your project"));
4734
4994
  }
4735
4995
  const existingConfig = await detectProjectConfig(projectDir);
4736
- 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.` }));
4737
4997
  if (!isSilent()) log.info(pc.dim(`Detected project: ${existingConfig.projectName}`));
4738
4998
  let addonsToAdd;
4739
4999
  if (input.addons && input.addons.length > 0) {
4740
5000
  addonsToAdd = input.addons.filter((addon) => addon !== "none" && !existingConfig.addons.includes(addon));
4741
- if (addonsToAdd.length === 0) {
5001
+ if (addonsToAdd.length === 0 && !input.package) {
4742
5002
  if (!isSilent()) {
4743
5003
  log.warn(pc.yellow("Nothing to add — those addons are already installed"));
4744
5004
  outro(pc.dim("Project unchanged"));
@@ -4749,7 +5009,8 @@ async function addHandlerInternal(input) {
4749
5009
  projectDir
4750
5010
  });
4751
5011
  }
4752
- } else if (isSilent()) return Result.err(new CLIError({ message: "Addons are required in silent mode. Provide them via add() or add-json." }));
5012
+ } else if (input.package) addonsToAdd = [];
5013
+ else if (isSilent()) return Result.err(new CLIError({ message: "Addons or a package are required in silent mode." }));
4753
5014
  else {
4754
5015
  const promptResult = await Result.tryPromise({
4755
5016
  try: () => getAddonsToAdd(existingConfig),
@@ -4776,7 +5037,10 @@ async function addHandlerInternal(input) {
4776
5037
  const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
4777
5038
  const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
4778
5039
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
4779
- if (!isSilent()) log.info(`${pc.dim("Adding")} ${pc.cyan(formatConfigValue(addonsToAdd))}`);
5040
+ if (!isSilent()) {
5041
+ const additions = [addonsToAdd.length > 0 ? formatConfigValue(addonsToAdd) : void 0, input.package ? `package ${input.package}` : void 0].filter(Boolean);
5042
+ log.info(`${pc.dim("Adding")} ${pc.cyan(additions.join(" and "))}`);
5043
+ }
4780
5044
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
4781
5045
  const config = {
4782
5046
  projectName: existingConfig.projectName,
@@ -4804,33 +5068,41 @@ async function addHandlerInternal(input) {
4804
5068
  ...config,
4805
5069
  addons: updatedAddons
4806
5070
  };
4807
- const requirementsResult = await checkLocalRequirements(updatedConfig);
4808
- if (requirementsResult.isErr()) return Result.err(requirementsResult.error);
4809
- if (!isSilent()) for (const warning of requirementsResult.value.warnings) log.warn(pc.yellow(warning));
4810
- if (!isSilent()) log.info(pc.dim("Preparing addon files…"));
5071
+ if (addonsToAdd.length > 0) {
5072
+ const requirementsResult = await checkLocalRequirements(updatedConfig);
5073
+ if (requirementsResult.isErr()) return Result.err(requirementsResult.error);
5074
+ if (!isSilent()) for (const warning of requirementsResult.value.warnings) log.warn(pc.yellow(warning));
5075
+ }
5076
+ if (!isSilent()) log.info(pc.dim("Preparing files…"));
4811
5077
  const vfs = new VirtualFileSystem();
4812
- for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
4813
- const fullPath = path.join(projectDir, pkgPath);
4814
- if (await fs.pathExists(fullPath)) {
4815
- const content = await fs.readFile(fullPath, "utf-8");
4816
- vfs.writeFile(pkgPath, content);
5078
+ if (input.package) {
5079
+ const packageResult = await addWorkspacePackage(vfs, projectDir, input.package, existingConfig.packageManager);
5080
+ if (packageResult.isErr()) return Result.err(packageResult.error);
5081
+ }
5082
+ if (addonsToAdd.length > 0) {
5083
+ for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
5084
+ const fullPath = path.join(projectDir, pkgPath);
5085
+ if (await fs.pathExists(fullPath)) {
5086
+ const content = await fs.readFile(fullPath, "utf-8");
5087
+ vfs.writeFile(pkgPath, content);
5088
+ }
4817
5089
  }
4818
- }
4819
- for (const filePath of ADD_TEXT_FILE_PATHS) {
4820
- const fullPath = path.join(projectDir, filePath);
4821
- if (await fs.pathExists(fullPath)) {
4822
- const content = await fs.readFile(fullPath, "utf-8");
4823
- vfs.writeFile(filePath, content);
5090
+ for (const filePath of ADD_TEXT_FILE_PATHS) {
5091
+ const fullPath = path.join(projectDir, filePath);
5092
+ if (await fs.pathExists(fullPath)) {
5093
+ const content = await fs.readFile(fullPath, "utf-8");
5094
+ vfs.writeFile(filePath, content);
5095
+ }
4824
5096
  }
5097
+ await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
5098
+ processAddonsDeps(vfs, config);
5099
+ if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
5100
+ if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
5101
+ if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
5102
+ if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
5103
+ if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
5104
+ if (shouldRefreshLefthook(addonsToAdd, updatedAddons)) refreshLefthookTemplate(vfs, updatedConfig);
4825
5105
  }
4826
- await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
4827
- processAddonsDeps(vfs, config);
4828
- if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
4829
- if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
4830
- if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
4831
- if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
4832
- if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
4833
- if (shouldRefreshLefthook(addonsToAdd, updatedAddons)) refreshLefthookTemplate(vfs, updatedConfig);
4834
5106
  const tree = {
4835
5107
  root: vfs.toTree(""),
4836
5108
  fileCount: vfs.getFileCount(),
@@ -4840,7 +5112,7 @@ async function addHandlerInternal(input) {
4840
5112
  if (input.dryRun) {
4841
5113
  if (!isSilent()) {
4842
5114
  log.success(pc.green("Dry run passed · no files written"));
4843
- log.message(pc.dim(`${vfs.getFileCount()} addon files planned`));
5115
+ log.message(pc.dim(`${vfs.getFileCount()} files planned`));
4844
5116
  outro(pc.dim("Project unchanged"));
4845
5117
  }
4846
5118
  return Result.ok({
@@ -4848,17 +5120,41 @@ async function addHandlerInternal(input) {
4848
5120
  addedAddons: addonsToAdd,
4849
5121
  projectDir,
4850
5122
  dryRun: true,
4851
- plannedFileCount: vfs.getFileCount()
5123
+ plannedFileCount: vfs.getFileCount(),
5124
+ addedPackage: input.package
4852
5125
  });
4853
5126
  }
5127
+ let reservedPackageDir;
5128
+ if (input.package) {
5129
+ const reservationResult = await reserveWorkspacePackage(projectDir, input.package);
5130
+ if (reservationResult.isErr()) return Result.err(reservationResult.error);
5131
+ reservedPackageDir = reservationResult.value;
5132
+ }
4854
5133
  const writeResult = await writeTree(tree, projectDir);
4855
- if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
4856
- if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
5134
+ if (writeResult.isErr()) {
5135
+ if (reservedPackageDir && input.package) {
5136
+ const cleanupResult = await Result.tryPromise({
5137
+ try: () => fs.remove(reservedPackageDir),
5138
+ catch: (cause) => new CLIError({
5139
+ message: `Failed to clean up incomplete workspace package: packages/${input.package}`,
5140
+ cause
5141
+ })
5142
+ });
5143
+ if (cleanupResult.isErr()) return Result.err(new CLIError({
5144
+ message: `Failed to write files: ${writeResult.error.message}. ${cleanupResult.error.message}`,
5145
+ cause: cleanupResult.error
5146
+ }));
5147
+ }
5148
+ return Result.err(new CLIError({ message: `Failed to write files: ${writeResult.error.message}` }));
5149
+ }
5150
+ if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} files`));
4857
5151
  const setupResult = await Result.tryPromise({
4858
- try: () => setupAddons({
4859
- ...config,
4860
- addons: getSetupAddons(addonsToAdd, updatedAddons)
4861
- }),
5152
+ try: async () => {
5153
+ if (addonsToAdd.length > 0) await setupAddons({
5154
+ ...config,
5155
+ addons: getSetupAddons(addonsToAdd, updatedAddons)
5156
+ });
5157
+ },
4862
5158
  catch: (cause) => {
4863
5159
  if (UserCancelledError.is(cause)) return cause;
4864
5160
  return new CLIError({
@@ -4868,7 +5164,7 @@ async function addHandlerInternal(input) {
4868
5164
  }
4869
5165
  });
4870
5166
  if (setupResult.isErr()) return Result.err(setupResult.error);
4871
- await updateBtsConfig(projectDir, {
5167
+ if (addonsToAdd.length > 0) await updateBtsConfig(projectDir, {
4872
5168
  addons: updatedAddons,
4873
5169
  addonOptions: updatedConfig.addonOptions
4874
5170
  });
@@ -4877,7 +5173,8 @@ async function addHandlerInternal(input) {
4877
5173
  packageManager: config.packageManager
4878
5174
  });
4879
5175
  if (!isSilent()) {
4880
- log.success(pc.green(`Added ${formatConfigValue(addonsToAdd)}`));
5176
+ const additions = [addonsToAdd.length > 0 ? formatConfigValue(addonsToAdd) : void 0, input.package ? `package ${input.package}` : void 0].filter(Boolean);
5177
+ log.success(pc.green(`Added ${additions.join(" and ")}`));
4881
5178
  if (!input.install) {
4882
5179
  const installCommand = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
4883
5180
  log.message(`${pc.dim("Next step")}\n${pc.cyan(installCommand)}`);
@@ -4888,7 +5185,8 @@ async function addHandlerInternal(input) {
4888
5185
  success: true,
4889
5186
  addedAddons: addonsToAdd,
4890
5187
  projectDir,
4891
- plannedFileCount: vfs.getFileCount()
5188
+ plannedFileCount: vfs.getFileCount(),
5189
+ addedPackage: input.package
4892
5190
  });
4893
5191
  }
4894
5192
  //#endregion
@@ -5980,8 +6278,11 @@ async function gatherConfig(flags, projectName, projectDir, relativePath, option
5980
6278
  ]
5981
6279
  }
5982
6280
  ],
5983
- onCancel: () => {
5984
- throw new UserCancelledError({ message: "Operation cancelled" });
6281
+ onCancel: ({ prompt }) => {
6282
+ throw new UserCancelledError({
6283
+ message: "Operation cancelled",
6284
+ prompt
6285
+ });
5985
6286
  }
5986
6287
  });
5987
6288
  return {
@@ -6051,6 +6352,7 @@ async function getProjectName(initialName) {
6051
6352
  counter++;
6052
6353
  }
6053
6354
  while (!isValid) {
6355
+ markPromptShown();
6054
6356
  const response = await text({
6055
6357
  message: "Where should we create your project?",
6056
6358
  placeholder: defaultName,
@@ -6065,40 +6367,34 @@ async function getProjectName(initialName) {
6065
6367
  }
6066
6368
  }
6067
6369
  });
6068
- if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
6370
+ if (isCancel(response)) throw new UserCancelledError({
6371
+ message: "Operation cancelled.",
6372
+ prompt: "projectName"
6373
+ });
6069
6374
  projectPath = response || defaultName;
6070
6375
  isValid = true;
6071
6376
  }
6072
6377
  return projectPath;
6073
6378
  }
6074
6379
  //#endregion
6075
- //#region src/utils/telemetry.ts
6076
- /**
6077
- * Returns true if telemetry/analytics should be enabled, false otherwise.
6078
- *
6079
- * - If BTS_TELEMETRY_DISABLED is present and "1", disables analytics.
6080
- * - Otherwise, BTS_TELEMETRY: "0" disables, "1" enables (default: enabled).
6081
- */
6082
- function isTelemetryEnabled() {
6083
- const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
6084
- if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
6085
- return true;
6086
- }
6087
- //#endregion
6088
6380
  //#region src/utils/analytics.ts
6089
6381
  const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
6382
+ const SEND_TIMEOUT_MS = 3e3;
6090
6383
  async function sendConvexEvent(payload) {
6091
6384
  await Result.tryPromise({
6092
6385
  try: () => fetch(CONVEX_INGEST_URL, {
6093
6386
  method: "POST",
6094
6387
  headers: { "Content-Type": "application/json" },
6095
- body: JSON.stringify(payload)
6388
+ body: JSON.stringify(payload),
6389
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
6390
+ keepalive: true
6096
6391
  }),
6097
6392
  catch: () => void 0
6098
6393
  });
6099
6394
  }
6100
- function buildAnalyticsEvent(config) {
6395
+ function buildAnalyticsEvent(config, mode) {
6101
6396
  return types_exports.AnalyticsEventSchema.parse({
6397
+ mode,
6102
6398
  database: config.database,
6103
6399
  orm: config.orm,
6104
6400
  backend: config.backend,
@@ -6120,10 +6416,10 @@ function buildAnalyticsEvent(config) {
6120
6416
  platform: process.platform
6121
6417
  });
6122
6418
  }
6123
- async function trackProjectCreation(config, disableAnalytics = false) {
6419
+ async function trackProjectCreation(config, disableAnalytics = false, mode) {
6124
6420
  if (!isTelemetryEnabled() || disableAnalytics) return;
6125
6421
  await Result.tryPromise({
6126
- try: () => sendConvexEvent(buildAnalyticsEvent(config)),
6422
+ try: () => sendConvexEvent(buildAnalyticsEvent(config, mode)),
6127
6423
  catch: () => void 0
6128
6424
  });
6129
6425
  }
@@ -8709,10 +9005,14 @@ async function createProject(options, cliInput) {
8709
9005
  }));
8710
9006
  yield* Result.await(formatProject(projectDir));
8711
9007
  if (!isSilent()) log.success("Project scaffolded");
8712
- if (options.install) yield* Result.await(installDependencies({
8713
- projectDir,
8714
- packageManager: options.packageManager
8715
- }));
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
+ }
8716
9016
  yield* Result.await(initializeGit(projectDir, options.git));
8717
9017
  if (!isSilent()) await displayPostInstallInstructions({
8718
9018
  ...options,
@@ -8775,17 +9075,49 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
8775
9075
  };
8776
9076
  }
8777
9077
  async function executeCreateProjectHandler(input, options) {
8778
- const { silent = false } = options;
8779
- return runWithContextAsync({ silent }, async () => {
9078
+ const { silent = false, mode } = options;
9079
+ return runWithContextAsync({
9080
+ silent,
9081
+ mode,
9082
+ analyticsDisabled: input.disableAnalytics
9083
+ }, async () => {
8780
9084
  const startTime = Date.now();
8781
9085
  const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
9086
+ const result = await createProjectHandlerInternal(input, startTime, timeScaffolded);
9087
+ await reportCreateOutcome(input, result, Date.now() - startTime);
8782
9088
  return {
8783
- result: await createProjectHandlerInternal(input, startTime, timeScaffolded),
9089
+ result,
8784
9090
  startTime,
8785
9091
  timeScaffolded
8786
9092
  };
8787
9093
  });
8788
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
+ }
8789
9121
  async function createProjectHandlerResult(input, options = {}) {
8790
9122
  return (await executeCreateProjectHandler(input, options)).result;
8791
9123
  }
@@ -8958,7 +9290,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
8958
9290
  dbSetupOptions: effectiveDbSetupOptions,
8959
9291
  packageManagerVersion: localRequirements.packageManagerVersion
8960
9292
  }));
8961
- await trackProjectCreation(config, input.disableAnalytics);
9293
+ await trackProjectCreation(config, input.disableAnalytics, resolveInvocationMode(input.yes));
8962
9294
  const historyResult = await addToHistory(config, reproducibleCommand);
8963
9295
  if (historyResult.isErr() && !isSilent()) {
8964
9296
  log.warn(pc.yellow(historyResult.error.message));
@@ -9159,17 +9491,21 @@ const router = t.router({
9159
9491
  description: "Create a project from a raw JSON payload (agent-friendly)",
9160
9492
  jsonInput: "always"
9161
9493
  }).input(types_exports.CreateInputSchema).mutation(async ({ input }) => {
9162
- const result = await createProjectHandler(input, { silent: true });
9494
+ const result = await createProjectHandler(input, {
9495
+ silent: true,
9496
+ mode: "json"
9497
+ });
9163
9498
  if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
9164
9499
  if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
9165
9500
  return result;
9166
9501
  }),
9167
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)),
9168
- sponsors: t.procedure.meta({ description: "Show Better-T-Stack sponsors" }).mutation(() => showSponsorsCommand()),
9169
- docs: t.procedure.meta({ description: "Open Better-T-Stack documentation" }).mutation(() => openDocsCommand()),
9170
- builder: t.procedure.meta({ description: "Open the web-based stack builder" }).mutation(() => openBuilderCommand()),
9171
- add: t.procedure.meta({ description: "Add addons to an existing Better-T-Stack project" }).input(z.object({
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())),
9506
+ add: t.procedure.meta({ description: "Add addons or a workspace package to an existing Better-T-Stack project" }).input(z.object({
9172
9507
  addons: z.array(types_exports.AddonsSchema).optional().describe("Addons to add"),
9508
+ package: types_exports.WorkspacePackageNameSchema.optional(),
9173
9509
  install: z.boolean().optional().default(false).describe("Install dependencies after adding"),
9174
9510
  packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
9175
9511
  projectDir: z.string().optional().describe("Project directory (defaults to current)"),
@@ -9178,10 +9514,13 @@ const router = t.router({
9178
9514
  await addHandler(input);
9179
9515
  }),
9180
9516
  addJson: t.procedure.meta({
9181
- description: "Add addons from a raw JSON payload (agent-friendly)",
9517
+ description: "Add addons or a workspace package from a raw JSON payload (agent-friendly)",
9182
9518
  jsonInput: "always"
9183
9519
  }).input(types_exports.AddInputSchema).mutation(async ({ input }) => {
9184
- const result = await addHandler(input, { silent: true });
9520
+ const result = await addHandler(input, {
9521
+ silent: true,
9522
+ mode: "json"
9523
+ });
9185
9524
  if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
9186
9525
  if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
9187
9526
  return result;
@@ -9191,9 +9530,29 @@ const router = t.router({
9191
9530
  clear: z.boolean().optional().default(false).describe("Clear all history"),
9192
9531
  json: z.boolean().optional().default(false).describe("Output as JSON")
9193
9532
  })).mutation(async ({ input }) => {
9194
- await historyHandler(input);
9533
+ await trackCommand("history", () => historyHandler(input), (ok) => ok);
9195
9534
  })
9196
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
+ }
9197
9556
  function createBtsCli() {
9198
9557
  return createCli({
9199
9558
  router,
@@ -9251,7 +9610,10 @@ async function create(projectName, options) {
9251
9610
  };
9252
9611
  return Result.tryPromise({
9253
9612
  try: async () => {
9254
- const result = await createProjectHandlerResult(input, { silent: true });
9613
+ const result = await createProjectHandlerResult(input, {
9614
+ silent: true,
9615
+ mode: getProcessMode() ?? "api"
9616
+ });
9255
9617
  if (result.isErr()) throw result.error;
9256
9618
  return result.value;
9257
9619
  },
@@ -9342,7 +9704,7 @@ async function createVirtual(options) {
9342
9704
  });
9343
9705
  }
9344
9706
  /**
9345
- * Programmatic API to add addons to an existing Better-T-Stack project.
9707
+ * Programmatic API to add addons or a workspace package to an existing Better-T-Stack project.
9346
9708
  *
9347
9709
  * @example
9348
9710
  * ```typescript
@@ -9366,7 +9728,10 @@ async function add(options = {}) {
9366
9728
  projectDir: "",
9367
9729
  error: formatInputValidationError("add", parsedInput.error)
9368
9730
  };
9369
- return await addHandler(parsedInput.data, { silent: true }) ?? {
9731
+ return await addHandler(parsedInput.data, {
9732
+ silent: true,
9733
+ mode: getProcessMode() ?? "api"
9734
+ }) ?? {
9370
9735
  success: false,
9371
9736
  addedAddons: [],
9372
9737
  projectDir: parsedInput.data.projectDir ?? "",
@@ -9374,4 +9739,4 @@ async function add(options = {}) {
9374
9739
  };
9375
9740
  }
9376
9741
  //#endregion
9377
- 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.4",
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.4",
73
- "@better-t-stack/types": "^3.40.4",
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",