@tanstack/cli 0.62.5 → 0.63.1

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/dist/cli.js CHANGED
@@ -6,6 +6,8 @@ import chalk from 'chalk';
6
6
  import semver from 'semver';
7
7
  import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, devAddOn, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
8
8
  import { LIBRARY_GROUPS, fetchDocContent, fetchLibraries, fetchPartners, searchTanStackDocs, } from './discovery.js';
9
+ import { getTelemetryStatus, setTelemetryEnabled, } from './telemetry-config.js';
10
+ import { createTelemetryClient } from './telemetry.js';
9
11
  import { promptForAddOns, promptForCreateOptions } from './options.js';
10
12
  import { normalizeOptions, validateDevWatchOptions, validateLegacyCreateFlags, } from './command-line.js';
11
13
  import { createUIEnvironment } from './ui-environment.js';
@@ -14,8 +16,127 @@ import { DevWatchManager } from './dev-watch.js';
14
16
  const packageJsonPath = new URL('../package.json', import.meta.url);
15
17
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
16
18
  const VERSION = packageJson.version;
19
+ function isLocalPath(value) {
20
+ return (value.startsWith('./') ||
21
+ value.startsWith('../') ||
22
+ value.startsWith('/') ||
23
+ /^[a-zA-Z]:[\\/]/.test(value));
24
+ }
25
+ function isRemoteUrl(value) {
26
+ return /^https?:\/\//i.test(value) || /^file:\/\//i.test(value);
27
+ }
28
+ function sanitizeId(value) {
29
+ const normalized = value.trim().toLowerCase();
30
+ if (!normalized) {
31
+ return undefined;
32
+ }
33
+ return normalized.replace(/[^a-z0-9._:/-]/g, '-');
34
+ }
35
+ function sanitizeIdList(values) {
36
+ return Array.from(new Set(values
37
+ .map((value) => sanitizeId(value))
38
+ .filter((value) => Boolean(value))));
39
+ }
40
+ function getStarterTelemetryProperties(value) {
41
+ if (!value) {
42
+ return {};
43
+ }
44
+ if (isRemoteUrl(value)) {
45
+ return {
46
+ starter_kind: 'remote_url',
47
+ };
48
+ }
49
+ if (isLocalPath(value)) {
50
+ return {
51
+ starter_kind: 'local_path',
52
+ };
53
+ }
54
+ return {
55
+ starter_id: sanitizeId(value),
56
+ starter_kind: 'built_in',
57
+ };
58
+ }
59
+ function getLengthBucket(value) {
60
+ const length = value.trim().length;
61
+ if (length === 0) {
62
+ return 'empty';
63
+ }
64
+ if (length <= 10) {
65
+ return '1_10';
66
+ }
67
+ if (length <= 25) {
68
+ return '11_25';
69
+ }
70
+ if (length <= 50) {
71
+ return '26_50';
72
+ }
73
+ return '51_plus';
74
+ }
75
+ function getCreateCommandVariant(options) {
76
+ if (options.listAddOns) {
77
+ return 'list_add_ons';
78
+ }
79
+ if (options.addonDetails) {
80
+ return 'addon_details';
81
+ }
82
+ if (options.devWatch) {
83
+ return 'dev_watch';
84
+ }
85
+ return 'scaffold';
86
+ }
87
+ function getCreateTelemetryProperties(projectName, options) {
88
+ const addOnIds = Array.isArray(options.addOns)
89
+ ? sanitizeIdList(options.addOns)
90
+ : undefined;
91
+ return {
92
+ ...getStarterTelemetryProperties(options.starter || options.templateId || options.template),
93
+ add_on_count: addOnIds?.length,
94
+ add_on_ids: addOnIds,
95
+ addon_details_id: options.addonDetails
96
+ ? sanitizeId(options.addonDetails)
97
+ : undefined,
98
+ command_variant: getCreateCommandVariant(options),
99
+ deployment: options.deployment ? sanitizeId(options.deployment) : undefined,
100
+ examples: options.examples,
101
+ framework: options.framework ? sanitizeId(options.framework) : undefined,
102
+ git: options.git,
103
+ install: options.install !== false,
104
+ interactive: !!options.interactive,
105
+ json: !!options.json,
106
+ non_interactive: !!options.nonInteractive || !!options.yes,
107
+ package_manager: options.packageManager,
108
+ project_name_provided: Boolean(projectName),
109
+ router_only: !!options.routerOnly,
110
+ target_dir_flag: Boolean(options.targetDir),
111
+ toolchain: typeof options.toolchain === 'string' ? sanitizeId(options.toolchain) : undefined,
112
+ yes: !!options.yes,
113
+ };
114
+ }
115
+ function getResolvedCreateTelemetryProperties(finalOptions, cliOptions) {
116
+ const includeExamples = finalOptions.includeExamples !== false;
117
+ const addOnIds = sanitizeIdList(finalOptions.chosenAddOns.map((addOn) => addOn.id));
118
+ const deployment = finalOptions.chosenAddOns.find((addOn) => addOn.type === 'deployment');
119
+ const toolchain = finalOptions.chosenAddOns.find((addOn) => addOn.type === 'toolchain');
120
+ return {
121
+ ...getStarterTelemetryProperties(finalOptions.starter?.id || cliOptions.starter || cliOptions.templateId || cliOptions.template),
122
+ add_on_count: addOnIds.length,
123
+ add_on_ids: addOnIds,
124
+ deployment: deployment ? sanitizeId(deployment.id) : undefined,
125
+ examples: includeExamples,
126
+ framework: sanitizeId(finalOptions.framework.id),
127
+ git: finalOptions.git,
128
+ install: finalOptions.install !== false,
129
+ package_manager: finalOptions.packageManager,
130
+ router_only: !!cliOptions.routerOnly,
131
+ toolchain: toolchain ? sanitizeId(toolchain.id) : undefined,
132
+ };
133
+ }
134
+ function formatErrorMessage(error) {
135
+ return error instanceof Error ? error.message : 'An unknown error occurred';
136
+ }
17
137
  export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, defaultRouterOnly = false, }) {
18
- const environment = createUIEnvironment(appName, false);
138
+ let currentTelemetry;
139
+ const environment = createUIEnvironment(appName, false, () => currentTelemetry);
19
140
  const program = new Command();
20
141
  async function confirmTargetDirectorySafety(targetDir, forced) {
21
142
  if (forced) {
@@ -56,22 +177,18 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
56
177
  // Validate dev watch options
57
178
  const validation = validateDevWatchOptions({ ...options, projectName });
58
179
  if (!validation.valid) {
59
- console.error(validation.error);
60
- process.exit(1);
180
+ throw new Error(validation.error);
61
181
  }
62
182
  // Enter dev watch mode
63
183
  if (!projectName && !options.targetDir) {
64
- console.error('Project name/target directory is required for dev watch mode');
65
- process.exit(1);
184
+ throw new Error('Project name/target directory is required for dev watch mode');
66
185
  }
67
186
  if (!options.framework) {
68
- console.error('Failed to detect framework');
69
- process.exit(1);
187
+ throw new Error('Failed to detect framework');
70
188
  }
71
189
  const framework = getFrameworkByName(options.framework);
72
190
  if (!framework) {
73
- console.error('Failed to detect framework');
74
- process.exit(1);
191
+ throw new Error('Failed to detect framework');
75
192
  }
76
193
  // First, create the app normally using the standard flow
77
194
  const normalizedOpts = await normalizeOptions({
@@ -82,6 +199,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
82
199
  if (!normalizedOpts) {
83
200
  throw new Error('Failed to normalize options');
84
201
  }
202
+ currentTelemetry?.mergeProperties(getResolvedCreateTelemetryProperties(normalizedOpts, options));
85
203
  normalizedOpts.targetDir =
86
204
  options.targetDir || resolve(process.cwd(), projectName);
87
205
  // Create the initial app with minimal output for dev watch mode
@@ -90,7 +208,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
90
208
  if (normalizedOpts.install !== false) {
91
209
  console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
92
210
  }
93
- const silentEnvironment = createUIEnvironment(appName, true);
211
+ const silentEnvironment = createUIEnvironment(appName, true, () => currentTelemetry);
94
212
  await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
95
213
  await createApp(silentEnvironment, normalizedOpts);
96
214
  console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
@@ -152,197 +270,238 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
152
270
  }
153
271
  return parsed;
154
272
  }
273
+ async function runWithTelemetry(command, opts, action) {
274
+ const telemetry = await createTelemetryClient({ json: opts.json });
275
+ const startedAt = Date.now();
276
+ currentTelemetry = telemetry;
277
+ telemetry.captureCommandStarted(command, {
278
+ ...opts.properties,
279
+ cli_version: VERSION,
280
+ });
281
+ try {
282
+ const result = await action(telemetry);
283
+ await telemetry.captureCommandCompleted(command, Date.now() - startedAt);
284
+ return result;
285
+ }
286
+ catch (error) {
287
+ await telemetry.captureCommandFailed(command, Date.now() - startedAt, error);
288
+ throw error;
289
+ }
290
+ finally {
291
+ currentTelemetry = undefined;
292
+ }
293
+ }
155
294
  program
156
295
  .name(name)
157
296
  .description(`${appName} CLI`)
158
297
  .version(VERSION, '-v, --version', 'output the current version');
159
298
  // Helper to create the create command action handler
160
299
  async function handleCreate(projectName, options) {
161
- const legacyCreateFlags = validateLegacyCreateFlags(options);
162
- if (legacyCreateFlags.error) {
163
- log.error(legacyCreateFlags.error);
164
- process.exit(1);
165
- }
166
- for (const warning of legacyCreateFlags.warnings) {
167
- log.warn(warning);
168
- }
169
- if (options.listAddOns) {
170
- const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode);
171
- const visibleAddOns = addOns.filter((a) => !forcedAddOns.includes(a.id));
172
- if (options.json) {
173
- printJson(visibleAddOns.map((addOn) => ({
174
- id: addOn.id,
175
- name: addOn.name,
176
- description: addOn.description,
177
- type: addOn.type,
178
- category: addOn.category,
179
- phase: addOn.phase,
180
- modes: addOn.modes,
181
- link: addOn.link,
182
- warning: addOn.warning,
183
- exclusive: addOn.exclusive,
184
- dependsOn: addOn.dependsOn,
185
- options: addOn.options,
186
- })));
187
- return;
188
- }
189
- let hasConfigurableAddOns = false;
190
- for (const addOn of visibleAddOns) {
191
- const hasOptions = addOn.options && Object.keys(addOn.options).length > 0;
192
- const optionMarker = hasOptions ? '*' : ' ';
193
- if (hasOptions)
194
- hasConfigurableAddOns = true;
195
- console.log(`${optionMarker} ${chalk.bold(addOn.id)}: ${addOn.description}`);
196
- }
197
- if (hasConfigurableAddOns) {
198
- console.log('\n* = has configuration options');
199
- }
200
- return;
201
- }
202
- if (options.addonDetails) {
203
- const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode);
204
- const addOn = addOns.find((a) => a.id === options.addonDetails) ??
205
- addOns.find((a) => a.id.toLowerCase() === options.addonDetails.toLowerCase());
206
- if (!addOn) {
207
- console.error(`Add-on '${options.addonDetails}' not found`);
208
- process.exit(1);
209
- }
210
- if (options.json) {
211
- const files = await addOn.getFiles();
212
- printJson({
213
- id: addOn.id,
214
- name: addOn.name,
215
- description: addOn.description,
216
- type: addOn.type,
217
- category: addOn.category,
218
- phase: addOn.phase,
219
- modes: addOn.modes,
220
- link: addOn.link,
221
- warning: addOn.warning,
222
- exclusive: addOn.exclusive,
223
- dependsOn: addOn.dependsOn,
224
- options: addOn.options,
225
- routes: addOn.routes,
226
- packageAdditions: addOn.packageAdditions,
227
- shadcnComponents: addOn.shadcnComponents,
228
- integrations: addOn.integrations,
229
- readme: addOn.readme,
230
- files,
231
- author: addOn.author,
232
- version: addOn.version,
233
- license: addOn.license,
234
- });
235
- return;
236
- }
237
- console.log(`${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`);
238
- console.log(`${chalk.bold('ID:')} ${addOn.id}`);
239
- console.log(`${chalk.bold('Description:')} ${addOn.description}`);
240
- console.log(`${chalk.bold('Type:')} ${addOn.type}`);
241
- console.log(`${chalk.bold('Phase:')} ${addOn.phase}`);
242
- console.log(`${chalk.bold('Supported Modes:')} ${addOn.modes.join(', ')}`);
243
- if (addOn.link) {
244
- console.log(`${chalk.bold('Link:')} ${chalk.blue(addOn.link)}`);
245
- }
246
- if (addOn.dependsOn && addOn.dependsOn.length > 0) {
247
- console.log(`${chalk.bold('Dependencies:')} ${addOn.dependsOn.join(', ')}`);
248
- }
249
- if (addOn.options && Object.keys(addOn.options).length > 0) {
250
- console.log(`\n${chalk.bold.yellow('Configuration Options:')}`);
251
- for (const [optionName, option] of Object.entries(addOn.options)) {
252
- if ('type' in option) {
253
- const opt = option;
254
- console.log(` ${chalk.bold(optionName)}:`);
255
- console.log(` Label: ${opt.label}`);
256
- if (opt.description) {
257
- console.log(` Description: ${opt.description}`);
258
- }
259
- console.log(` Type: ${opt.type}`);
260
- console.log(` Default: ${opt.default}`);
261
- if (opt.type === 'select' && opt.options) {
262
- console.log(` Available values:`);
263
- for (const choice of opt.options) {
264
- console.log(` - ${choice.value}: ${choice.label}`);
300
+ try {
301
+ await runWithTelemetry('create', {
302
+ json: options.json,
303
+ properties: getCreateTelemetryProperties(projectName, options),
304
+ }, async (telemetry) => {
305
+ const legacyCreateFlags = validateLegacyCreateFlags(options);
306
+ if (legacyCreateFlags.error) {
307
+ throw new Error(legacyCreateFlags.error);
308
+ }
309
+ for (const warning of legacyCreateFlags.warnings) {
310
+ log.warn(warning);
311
+ }
312
+ if (options.listAddOns) {
313
+ const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode);
314
+ const visibleAddOns = addOns.filter((a) => !forcedAddOns.includes(a.id));
315
+ telemetry.mergeProperties({
316
+ result_count: visibleAddOns.length,
317
+ });
318
+ if (options.json) {
319
+ printJson(visibleAddOns.map((addOn) => ({
320
+ id: addOn.id,
321
+ name: addOn.name,
322
+ description: addOn.description,
323
+ type: addOn.type,
324
+ category: addOn.category,
325
+ phase: addOn.phase,
326
+ modes: addOn.modes,
327
+ link: addOn.link,
328
+ warning: addOn.warning,
329
+ exclusive: addOn.exclusive,
330
+ dependsOn: addOn.dependsOn,
331
+ options: addOn.options,
332
+ })));
333
+ return;
334
+ }
335
+ let hasConfigurableAddOns = false;
336
+ for (const addOn of visibleAddOns) {
337
+ const hasOptions = addOn.options && Object.keys(addOn.options).length > 0;
338
+ const optionMarker = hasOptions ? '*' : ' ';
339
+ if (hasOptions)
340
+ hasConfigurableAddOns = true;
341
+ console.log(`${optionMarker} ${chalk.bold(addOn.id)}: ${addOn.description}`);
342
+ }
343
+ if (hasConfigurableAddOns) {
344
+ console.log('\n* = has configuration options');
345
+ }
346
+ return;
347
+ }
348
+ if (options.addonDetails) {
349
+ const addOns = await getAllAddOns(getFrameworkByName(options.framework || defaultFramework || 'React'), defaultMode);
350
+ const addOn = addOns.find((a) => a.id === options.addonDetails) ??
351
+ addOns.find((a) => a.id.toLowerCase() === options.addonDetails.toLowerCase());
352
+ if (!addOn) {
353
+ throw new Error(`Add-on '${options.addonDetails}' not found`);
354
+ }
355
+ telemetry.mergeProperties({
356
+ add_on_file_count: (await addOn.getFiles()).length,
357
+ });
358
+ if (options.json) {
359
+ const files = await addOn.getFiles();
360
+ printJson({
361
+ id: addOn.id,
362
+ name: addOn.name,
363
+ description: addOn.description,
364
+ type: addOn.type,
365
+ category: addOn.category,
366
+ phase: addOn.phase,
367
+ modes: addOn.modes,
368
+ link: addOn.link,
369
+ warning: addOn.warning,
370
+ exclusive: addOn.exclusive,
371
+ dependsOn: addOn.dependsOn,
372
+ options: addOn.options,
373
+ routes: addOn.routes,
374
+ packageAdditions: addOn.packageAdditions,
375
+ shadcnComponents: addOn.shadcnComponents,
376
+ integrations: addOn.integrations,
377
+ readme: addOn.readme,
378
+ files,
379
+ author: addOn.author,
380
+ version: addOn.version,
381
+ license: addOn.license,
382
+ });
383
+ return;
384
+ }
385
+ console.log(`${chalk.bold.cyan('Add-on Details:')} ${chalk.bold(addOn.name)}`);
386
+ console.log(`${chalk.bold('ID:')} ${addOn.id}`);
387
+ console.log(`${chalk.bold('Description:')} ${addOn.description}`);
388
+ console.log(`${chalk.bold('Type:')} ${addOn.type}`);
389
+ console.log(`${chalk.bold('Phase:')} ${addOn.phase}`);
390
+ console.log(`${chalk.bold('Supported Modes:')} ${addOn.modes.join(', ')}`);
391
+ if (addOn.link) {
392
+ console.log(`${chalk.bold('Link:')} ${chalk.blue(addOn.link)}`);
393
+ }
394
+ if (addOn.dependsOn && addOn.dependsOn.length > 0) {
395
+ console.log(`${chalk.bold('Dependencies:')} ${addOn.dependsOn.join(', ')}`);
396
+ }
397
+ if (addOn.options && Object.keys(addOn.options).length > 0) {
398
+ console.log(`\n${chalk.bold.yellow('Configuration Options:')}`);
399
+ for (const [optionName, option] of Object.entries(addOn.options)) {
400
+ if ('type' in option) {
401
+ const opt = option;
402
+ console.log(` ${chalk.bold(optionName)}:`);
403
+ console.log(` Label: ${opt.label}`);
404
+ if (opt.description) {
405
+ console.log(` Description: ${opt.description}`);
406
+ }
407
+ console.log(` Type: ${opt.type}`);
408
+ console.log(` Default: ${opt.default}`);
409
+ if (opt.type === 'select' && opt.options) {
410
+ console.log(` Available values:`);
411
+ for (const choice of opt.options) {
412
+ console.log(` - ${choice.value}: ${choice.label}`);
413
+ }
414
+ }
265
415
  }
266
416
  }
267
417
  }
418
+ else {
419
+ console.log(`\n${chalk.gray('No configuration options available')}`);
420
+ }
421
+ if (addOn.routes && addOn.routes.length > 0) {
422
+ console.log(`\n${chalk.bold.green('Routes:')}`);
423
+ for (const route of addOn.routes) {
424
+ console.log(` ${chalk.bold(route.url)} (${route.name})`);
425
+ console.log(` File: ${route.path}`);
426
+ }
427
+ }
428
+ return;
268
429
  }
269
- }
270
- else {
271
- console.log(`\n${chalk.gray('No configuration options available')}`);
272
- }
273
- if (addOn.routes && addOn.routes.length > 0) {
274
- console.log(`\n${chalk.bold.green('Routes:')}`);
275
- for (const route of addOn.routes) {
276
- console.log(` ${chalk.bold(route.url)} (${route.name})`);
277
- console.log(` File: ${route.path}`);
430
+ if (options.devWatch) {
431
+ await startDevWatchMode(projectName, options);
432
+ return;
278
433
  }
279
- }
280
- return;
281
- }
282
- if (options.devWatch) {
283
- await startDevWatchMode(projectName, options);
284
- return;
285
- }
286
- try {
287
- const cliOptions = {
288
- projectName,
289
- ...options,
290
- };
291
- if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
292
- cliOptions.routerOnly = true;
293
- }
294
- if (cliOptions.routerOnly !== true &&
295
- cliOptions.template &&
296
- ['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(cliOptions.template.toLowerCase()) &&
297
- cliOptions.template.toLowerCase() !== 'file-router') {
298
- cliOptions.routerOnly = true;
299
- }
300
- cliOptions.framework = getFrameworkByName(options.framework || defaultFramework || 'React').id;
301
- let finalOptions;
302
- if (cliOptions.interactive || cliOptions.addOns === true) {
303
- cliOptions.addOns = true;
304
- }
305
- else {
306
- finalOptions = await normalizeOptions(cliOptions, forcedAddOns, { forcedDeployment });
307
- }
308
- if (finalOptions) {
309
- intro(`Creating a new ${appName} app in ${projectName}...`);
310
- }
311
- else {
312
- intro(`Let's configure your ${appName} application`);
313
- finalOptions = await promptForCreateOptions(cliOptions, {
314
- forcedAddOns,
315
- showDeploymentOptions,
316
- });
317
- }
318
- if (!finalOptions) {
319
- throw new Error('No options were provided');
320
- }
321
- ;
322
- finalOptions.routerOnly =
323
- !!cliOptions.routerOnly;
324
- // Determine target directory:
325
- // 1. Use --target-dir if provided
326
- // 2. Use targetDir from normalizeOptions if set (handles "." case)
327
- // 3. If original projectName was ".", use current directory
328
- // 4. Otherwise, use project name as subdirectory
329
- if (options.targetDir) {
330
- finalOptions.targetDir = options.targetDir;
331
- }
332
- else if (finalOptions.targetDir) {
333
- // Keep the targetDir from normalizeOptions (handles "." case)
334
- }
335
- else if (projectName === '.') {
336
- finalOptions.targetDir = resolve(process.cwd());
337
- }
338
- else {
339
- finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName);
340
- }
341
- await confirmTargetDirectorySafety(finalOptions.targetDir, options.force);
342
- await createApp(environment, finalOptions);
434
+ const cliOptions = {
435
+ projectName,
436
+ ...options,
437
+ };
438
+ if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
439
+ cliOptions.routerOnly = true;
440
+ }
441
+ if (cliOptions.routerOnly !== true &&
442
+ cliOptions.template &&
443
+ ['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(cliOptions.template.toLowerCase()) &&
444
+ cliOptions.template.toLowerCase() !== 'file-router') {
445
+ cliOptions.routerOnly = true;
446
+ }
447
+ cliOptions.framework = getFrameworkByName(options.framework || defaultFramework || 'React').id;
448
+ const nonInteractive = !!cliOptions.nonInteractive || !!cliOptions.yes;
449
+ if (cliOptions.interactive && nonInteractive) {
450
+ throw new Error('Cannot combine --interactive with --non-interactive/--yes.');
451
+ }
452
+ const addOnsFlagPassed = process.argv.includes('--add-ons');
453
+ const wantsInteractiveMode = !nonInteractive &&
454
+ (cliOptions.interactive ||
455
+ (cliOptions.addOns === true && addOnsFlagPassed));
456
+ let finalOptions;
457
+ if (wantsInteractiveMode) {
458
+ cliOptions.addOns = true;
459
+ }
460
+ else {
461
+ finalOptions = await normalizeOptions(cliOptions, forcedAddOns, { forcedDeployment });
462
+ }
463
+ if (nonInteractive) {
464
+ if (cliOptions.addOns === true) {
465
+ throw new Error('When using --non-interactive/--yes, pass explicit add-ons via --add-ons <ids>.');
466
+ }
467
+ }
468
+ if (finalOptions) {
469
+ intro(`Creating a new ${appName} app in ${projectName}...`);
470
+ }
471
+ else {
472
+ if (nonInteractive) {
473
+ throw new Error('Project name is required in non-interactive mode. Pass [project-name] or --target-dir.');
474
+ }
475
+ intro(`Let's configure your ${appName} application`);
476
+ finalOptions = await promptForCreateOptions(cliOptions, {
477
+ forcedAddOns,
478
+ showDeploymentOptions,
479
+ });
480
+ }
481
+ if (!finalOptions) {
482
+ throw new Error('No options were provided');
483
+ }
484
+ telemetry.mergeProperties(getResolvedCreateTelemetryProperties(finalOptions, cliOptions));
485
+ finalOptions.routerOnly =
486
+ !!cliOptions.routerOnly;
487
+ if (options.targetDir) {
488
+ finalOptions.targetDir = options.targetDir;
489
+ }
490
+ else if (finalOptions.targetDir) {
491
+ // Keep the normalized target dir.
492
+ }
493
+ else if (projectName === '.') {
494
+ finalOptions.targetDir = resolve(process.cwd());
495
+ }
496
+ else {
497
+ finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName);
498
+ }
499
+ await confirmTargetDirectorySafety(finalOptions.targetDir, options.force);
500
+ await createApp(environment, finalOptions);
501
+ });
343
502
  }
344
503
  catch (error) {
345
- log.error(error instanceof Error ? error.message : 'An unknown error occurred');
504
+ log.error(formatErrorMessage(error));
346
505
  process.exit(1);
347
506
  }
348
507
  }
@@ -398,6 +557,8 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
398
557
  }
399
558
  cmd
400
559
  .option('--interactive', 'interactive mode', false)
560
+ .option('--non-interactive', 'skip prompts and use defaults', false)
561
+ .option('-y, --yes', 'accept defaults and skip prompts', false)
401
562
  .option('--add-ons [...add-ons]', 'pick from a list of available add-ons (comma separated list)', (value) => {
402
563
  let addOns = !!value;
403
564
  if (typeof value === 'string') {
@@ -428,21 +589,36 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
428
589
  .description('Create a sandbox app and watch built-in framework templates/add-ons');
429
590
  configureCreateCommand(devCommand);
430
591
  devCommand.action(async (projectName, options) => {
431
- const frameworkName = options.framework || defaultFramework || 'React';
432
- const framework = getFrameworkByName(frameworkName);
433
- if (!framework) {
434
- console.error(`Unknown framework: ${frameworkName}`);
592
+ try {
593
+ await runWithTelemetry('dev', {
594
+ properties: {
595
+ framework: options.framework
596
+ ? sanitizeId(options.framework)
597
+ : sanitizeId(defaultFramework || 'react'),
598
+ install: options.install !== false,
599
+ run_dev: true,
600
+ },
601
+ }, async () => {
602
+ const frameworkName = options.framework || defaultFramework || 'React';
603
+ const framework = getFrameworkByName(frameworkName);
604
+ if (!framework) {
605
+ throw new Error(`Unknown framework: ${frameworkName}`);
606
+ }
607
+ const watchPath = resolveBuiltInDevWatchPath(framework.id);
608
+ const devOptions = {
609
+ ...options,
610
+ framework: framework.name,
611
+ devWatch: watchPath,
612
+ runDev: true,
613
+ install: options.install ?? true,
614
+ };
615
+ await startDevWatchMode(projectName, devOptions);
616
+ });
617
+ }
618
+ catch (error) {
619
+ log.error(formatErrorMessage(error));
435
620
  process.exit(1);
436
621
  }
437
- const watchPath = resolveBuiltInDevWatchPath(framework.id);
438
- const devOptions = {
439
- ...options,
440
- framework: framework.name,
441
- devWatch: watchPath,
442
- runDev: true,
443
- install: options.install ?? true,
444
- };
445
- await startDevWatchMode(projectName, devOptions);
446
622
  });
447
623
  // === LIBRARIES SUBCOMMAND ===
448
624
  program
@@ -452,41 +628,52 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
452
628
  .option('--json', 'output JSON for automation', false)
453
629
  .action(async (options) => {
454
630
  try {
455
- const data = await fetchLibraries();
456
- let libraries = data.libraries;
457
- if (options.group &&
458
- Object.prototype.hasOwnProperty.call(data.groups, options.group)) {
459
- const groupIds = data.groups[options.group];
460
- libraries = libraries.filter((lib) => groupIds.includes(lib.id));
461
- }
462
- const groupName = options.group
463
- ? data.groupNames[options.group] || options.group
464
- : 'All Libraries';
465
- const payload = {
466
- group: groupName,
467
- count: libraries.length,
468
- libraries: libraries.map((lib) => ({
469
- id: lib.id,
470
- name: lib.name,
471
- tagline: lib.tagline,
472
- description: lib.description,
473
- frameworks: lib.frameworks,
474
- latestVersion: lib.latestVersion,
475
- docsUrl: lib.docsUrl,
476
- githubUrl: lib.githubUrl,
477
- })),
478
- };
479
- if (options.json) {
480
- printJson(payload);
481
- return;
482
- }
483
- console.log(chalk.bold(groupName));
484
- for (const lib of payload.libraries) {
485
- console.log(`${chalk.bold(lib.id)} (${lib.latestVersion}) - ${lib.tagline}`);
486
- }
631
+ await runWithTelemetry('libraries', {
632
+ json: options.json,
633
+ properties: {
634
+ group: options.group ? sanitizeId(options.group) : undefined,
635
+ json: options.json,
636
+ },
637
+ }, async (telemetry) => {
638
+ const data = await fetchLibraries();
639
+ let libraries = data.libraries;
640
+ if (options.group &&
641
+ Object.prototype.hasOwnProperty.call(data.groups, options.group)) {
642
+ const groupIds = data.groups[options.group];
643
+ libraries = libraries.filter((lib) => groupIds.includes(lib.id));
644
+ }
645
+ const groupName = options.group
646
+ ? data.groupNames[options.group] || options.group
647
+ : 'All Libraries';
648
+ const payload = {
649
+ group: groupName,
650
+ count: libraries.length,
651
+ libraries: libraries.map((lib) => ({
652
+ id: lib.id,
653
+ name: lib.name,
654
+ tagline: lib.tagline,
655
+ description: lib.description,
656
+ frameworks: lib.frameworks,
657
+ latestVersion: lib.latestVersion,
658
+ docsUrl: lib.docsUrl,
659
+ githubUrl: lib.githubUrl,
660
+ })),
661
+ };
662
+ telemetry.mergeProperties({
663
+ result_count: payload.count,
664
+ });
665
+ if (options.json) {
666
+ printJson(payload);
667
+ return;
668
+ }
669
+ console.log(chalk.bold(groupName));
670
+ for (const lib of payload.libraries) {
671
+ console.log(`${chalk.bold(lib.id)} (${lib.latestVersion}) - ${lib.tagline}`);
672
+ }
673
+ });
487
674
  }
488
675
  catch (error) {
489
- log.error(error instanceof Error ? error.message : String(error));
676
+ log.error(formatErrorMessage(error));
490
677
  process.exit(1);
491
678
  }
492
679
  });
@@ -500,56 +687,69 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
500
687
  .option('--json', 'output JSON for automation', false)
501
688
  .action(async (libraryId, path, options) => {
502
689
  try {
503
- const data = await fetchLibraries();
504
- const library = data.libraries.find((l) => l.id === libraryId);
505
- if (!library) {
506
- throw new Error(`Library "${libraryId}" not found. Use \`tanstack libraries\` to see available libraries.`);
507
- }
508
- if (options.docsVersion !== 'latest' &&
509
- !library.availableVersions.includes(options.docsVersion)) {
510
- throw new Error(`Version "${options.docsVersion}" not found for ${library.name}. Available: ${library.availableVersions.join(', ')}`);
511
- }
512
- const branch = options.docsVersion === 'latest' ||
513
- options.docsVersion === library.latestVersion
514
- ? library.latestBranch || 'main'
515
- : options.docsVersion;
516
- const docsRoot = library.docsRoot || 'docs';
517
- const filePath = `${docsRoot}/${path}.md`;
518
- const content = await fetchDocContent(library.repo, branch, filePath);
519
- if (!content) {
520
- throw new Error(`Document not found: ${library.name} / ${path} (version: ${options.docsVersion})`);
521
- }
522
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
523
- let title = path.split('/').pop() || 'Untitled';
524
- let docContent = content;
525
- if (frontmatterMatch && frontmatterMatch[1]) {
526
- const frontmatter = frontmatterMatch[1];
527
- const titleMatch = frontmatter.match(/title:\s*['"]?([^'"\n]+)['"]?/);
528
- if (titleMatch && titleMatch[1]) {
529
- title = titleMatch[1];
530
- }
531
- docContent = content.slice(frontmatterMatch[0].length).trim();
532
- }
533
- const payload = {
534
- title,
535
- content: docContent,
536
- url: `https://tanstack.com/${libraryId}/${options.docsVersion}/docs/${path}`,
537
- library: library.name,
538
- version: options.docsVersion === 'latest'
539
- ? library.latestVersion
540
- : options.docsVersion,
541
- };
542
- if (options.json) {
543
- printJson(payload);
544
- return;
545
- }
546
- console.log(chalk.bold(payload.title));
547
- console.log(chalk.blue(payload.url));
548
- console.log('');
549
- console.log(payload.content);
690
+ await runWithTelemetry('doc', {
691
+ json: options.json,
692
+ properties: {
693
+ doc_path_depth: path.split('/').filter(Boolean).length,
694
+ docs_version: sanitizeId(options.docsVersion),
695
+ json: options.json,
696
+ library: sanitizeId(libraryId),
697
+ },
698
+ }, async (telemetry) => {
699
+ const data = await fetchLibraries();
700
+ const library = data.libraries.find((l) => l.id === libraryId);
701
+ if (!library) {
702
+ throw new Error(`Library "${libraryId}" not found. Use \`tanstack libraries\` to see available libraries.`);
703
+ }
704
+ if (options.docsVersion !== 'latest' &&
705
+ !library.availableVersions.includes(options.docsVersion)) {
706
+ throw new Error(`Version "${options.docsVersion}" not found for ${library.name}. Available: ${library.availableVersions.join(', ')}`);
707
+ }
708
+ const branch = options.docsVersion === 'latest' ||
709
+ options.docsVersion === library.latestVersion
710
+ ? library.latestBranch || 'main'
711
+ : options.docsVersion;
712
+ const docsRoot = library.docsRoot || 'docs';
713
+ const filePath = `${docsRoot}/${path}.md`;
714
+ const content = await fetchDocContent(library.repo, branch, filePath);
715
+ if (!content) {
716
+ throw new Error(`Document not found: ${library.name} / ${path} (version: ${options.docsVersion})`);
717
+ }
718
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
719
+ let title = path.split('/').pop() || 'Untitled';
720
+ let docContent = content;
721
+ if (frontmatterMatch && frontmatterMatch[1]) {
722
+ const frontmatter = frontmatterMatch[1];
723
+ const titleMatch = frontmatter.match(/title:\s*['"]?([^'"\n]+)['"]?/);
724
+ if (titleMatch && titleMatch[1]) {
725
+ title = titleMatch[1];
726
+ }
727
+ docContent = content.slice(frontmatterMatch[0].length).trim();
728
+ }
729
+ const payload = {
730
+ title,
731
+ content: docContent,
732
+ url: `https://tanstack.com/${libraryId}/${options.docsVersion}/docs/${path}`,
733
+ library: library.name,
734
+ version: options.docsVersion === 'latest'
735
+ ? library.latestVersion
736
+ : options.docsVersion,
737
+ };
738
+ telemetry.mergeProperties({
739
+ content_length_bucket: getLengthBucket(docContent),
740
+ });
741
+ if (options.json) {
742
+ printJson(payload);
743
+ return;
744
+ }
745
+ console.log(chalk.bold(payload.title));
746
+ console.log(chalk.blue(payload.url));
747
+ console.log('');
748
+ console.log(payload.content);
749
+ });
550
750
  }
551
751
  catch (error) {
552
- log.error(error instanceof Error ? error.message : String(error));
752
+ log.error(formatErrorMessage(error));
553
753
  process.exit(1);
554
754
  }
555
755
  });
@@ -564,22 +764,39 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
564
764
  .option('--json', 'output JSON for automation', false)
565
765
  .action(async (query, options) => {
566
766
  try {
567
- const payload = await searchTanStackDocs({
568
- query,
569
- library: options.library,
570
- framework: options.framework,
571
- limit: options.limit,
767
+ await runWithTelemetry('search-docs', {
768
+ json: options.json,
769
+ properties: {
770
+ framework: options.framework
771
+ ? sanitizeId(options.framework)
772
+ : undefined,
773
+ has_query: query.trim().length > 0,
774
+ json: options.json,
775
+ library: options.library ? sanitizeId(options.library) : undefined,
776
+ limit: options.limit,
777
+ query_length_bucket: getLengthBucket(query),
778
+ },
779
+ }, async (telemetry) => {
780
+ const payload = await searchTanStackDocs({
781
+ query,
782
+ library: options.library,
783
+ framework: options.framework,
784
+ limit: options.limit,
785
+ });
786
+ telemetry.mergeProperties({
787
+ result_count: payload.totalHits,
788
+ });
789
+ if (options.json) {
790
+ printJson(payload);
791
+ return;
792
+ }
793
+ for (const result of payload.results) {
794
+ console.log(`${chalk.bold(result.title)} [${result.library}]\n${chalk.blue(result.url)}\n${result.snippet}\n`);
795
+ }
572
796
  });
573
- if (options.json) {
574
- printJson(payload);
575
- return;
576
- }
577
- for (const result of payload.results) {
578
- console.log(`${chalk.bold(result.title)} [${result.library}]\n${chalk.blue(result.url)}\n${result.snippet}\n`);
579
- }
580
797
  }
581
798
  catch (error) {
582
- log.error(error instanceof Error ? error.message : String(error));
799
+ log.error(formatErrorMessage(error));
583
800
  process.exit(1);
584
801
  }
585
802
  });
@@ -592,48 +809,63 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
592
809
  .option('--json', 'output JSON for automation', false)
593
810
  .action(async (options) => {
594
811
  try {
595
- const data = await fetchPartners();
596
- let resolvedCategory;
597
- if (options.category) {
598
- const normalized = options.category.toLowerCase().trim();
599
- resolvedCategory = categoryAliases[normalized] || normalized;
600
- if (!data.categories.includes(resolvedCategory)) {
601
- resolvedCategory = undefined;
602
- }
603
- }
604
- const library = options.library?.toLowerCase().trim();
605
- const partners = data.partners
606
- .filter((partner) => resolvedCategory ? partner.category === resolvedCategory : true)
607
- .filter((partner) => library ? partner.libraries.some((l) => l === library) : true)
608
- .map((partner) => ({
609
- id: partner.id,
610
- name: partner.name,
611
- tagline: partner.tagline,
612
- description: partner.description,
613
- category: partner.category,
614
- categoryLabel: partner.categoryLabel,
615
- url: partner.url,
616
- libraries: partner.libraries,
617
- }));
618
- const payload = {
619
- query: {
620
- category: options.category,
621
- categoryResolved: resolvedCategory,
622
- library: options.library,
812
+ await runWithTelemetry('ecosystem', {
813
+ json: options.json,
814
+ properties: {
815
+ category: options.category ? sanitizeId(options.category) : undefined,
816
+ json: options.json,
817
+ library: options.library ? sanitizeId(options.library) : undefined,
623
818
  },
624
- count: partners.length,
625
- partners,
626
- };
627
- if (options.json) {
628
- printJson(payload);
629
- return;
630
- }
631
- for (const partner of partners) {
632
- console.log(`${chalk.bold(partner.name)} [${partner.category}] - ${partner.description}\n${chalk.blue(partner.url)}`);
633
- }
819
+ }, async (telemetry) => {
820
+ const data = await fetchPartners();
821
+ let resolvedCategory;
822
+ if (options.category) {
823
+ const normalized = options.category.toLowerCase().trim();
824
+ resolvedCategory = categoryAliases[normalized] || normalized;
825
+ if (!data.categories.includes(resolvedCategory)) {
826
+ resolvedCategory = undefined;
827
+ }
828
+ }
829
+ const library = options.library?.toLowerCase().trim();
830
+ const partners = data.partners
831
+ .filter((partner) => resolvedCategory ? partner.category === resolvedCategory : true)
832
+ .filter((partner) => library ? partner.libraries.some((l) => l === library) : true)
833
+ .map((partner) => ({
834
+ id: partner.id,
835
+ name: partner.name,
836
+ tagline: partner.tagline,
837
+ description: partner.description,
838
+ category: partner.category,
839
+ categoryLabel: partner.categoryLabel,
840
+ url: partner.url,
841
+ libraries: partner.libraries,
842
+ }));
843
+ const payload = {
844
+ query: {
845
+ category: options.category,
846
+ categoryResolved: resolvedCategory,
847
+ library: options.library,
848
+ },
849
+ count: partners.length,
850
+ partners,
851
+ };
852
+ telemetry.mergeProperties({
853
+ category_resolved: resolvedCategory
854
+ ? sanitizeId(resolvedCategory)
855
+ : undefined,
856
+ result_count: payload.count,
857
+ });
858
+ if (options.json) {
859
+ printJson(payload);
860
+ return;
861
+ }
862
+ for (const partner of partners) {
863
+ console.log(`${chalk.bold(partner.name)} [${partner.category}] - ${partner.description}\n${chalk.blue(partner.url)}`);
864
+ }
865
+ });
634
866
  }
635
867
  catch (error) {
636
- log.error(error instanceof Error ? error.message : String(error));
868
+ log.error(formatErrorMessage(error));
637
869
  process.exit(1);
638
870
  }
639
871
  });
@@ -642,90 +874,158 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
642
874
  .command('pin-versions')
643
875
  .description('Pin versions of the TanStack libraries')
644
876
  .action(async () => {
645
- if (!fs.existsSync('package.json')) {
646
- console.error('package.json not found');
647
- return;
648
- }
649
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
650
- const packages = {
651
- '@tanstack/react-router': '',
652
- '@tanstack/router-generator': '',
653
- '@tanstack/react-router-devtools': '',
654
- '@tanstack/react-start': '',
655
- '@tanstack/react-start-config': '',
656
- '@tanstack/router-plugin': '',
657
- '@tanstack/react-start-client': '',
658
- '@tanstack/react-start-plugin': '1.115.0',
659
- '@tanstack/react-start-server': '',
660
- '@tanstack/start-server-core': '1.115.0',
661
- };
662
- function sortObject(obj) {
663
- return Object.keys(obj)
664
- .sort()
665
- .reduce((acc, key) => {
666
- acc[key] = obj[key];
667
- return acc;
668
- }, {});
669
- }
670
- if (!packageJson.dependencies['@tanstack/react-start']) {
671
- console.error('@tanstack/react-start not found in dependencies');
672
- return;
673
- }
674
- let changed = 0;
675
- const startVersion = packageJson.dependencies['@tanstack/react-start'].replace(/^\^/, '');
676
- for (const pkg of Object.keys(packages)) {
677
- if (!packageJson.dependencies[pkg]) {
678
- packageJson.dependencies[pkg] = packages[pkg].length
679
- ? semver.maxSatisfying([startVersion, packages[pkg]], `^${packages[pkg]}`)
680
- : startVersion;
681
- changed++;
682
- }
683
- else {
684
- if (packageJson.dependencies[pkg].startsWith('^')) {
685
- packageJson.dependencies[pkg] = packageJson.dependencies[pkg].replace(/^\^/, '');
686
- changed++;
877
+ try {
878
+ await runWithTelemetry('pin-versions', {}, async (telemetry) => {
879
+ if (!fs.existsSync('package.json')) {
880
+ throw new Error('package.json not found');
687
881
  }
688
- }
689
- }
690
- packageJson.dependencies = sortObject(packageJson.dependencies);
691
- if (changed > 0) {
692
- fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
693
- console.log(`${changed} packages updated.
882
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
883
+ const packages = {
884
+ '@tanstack/react-router': '',
885
+ '@tanstack/router-generator': '',
886
+ '@tanstack/react-router-devtools': '',
887
+ '@tanstack/react-start': '',
888
+ '@tanstack/react-start-config': '',
889
+ '@tanstack/router-plugin': '',
890
+ '@tanstack/react-start-client': '',
891
+ '@tanstack/react-start-plugin': '1.115.0',
892
+ '@tanstack/react-start-server': '',
893
+ '@tanstack/start-server-core': '1.115.0',
894
+ };
895
+ function sortObject(obj) {
896
+ return Object.keys(obj)
897
+ .sort()
898
+ .reduce((acc, key) => {
899
+ acc[key] = obj[key];
900
+ return acc;
901
+ }, {});
902
+ }
903
+ if (!packageJson.dependencies['@tanstack/react-start']) {
904
+ throw new Error('@tanstack/react-start not found in dependencies');
905
+ }
906
+ let changed = 0;
907
+ const startVersion = packageJson.dependencies['@tanstack/react-start'].replace(/^\^/, '');
908
+ for (const pkg of Object.keys(packages)) {
909
+ if (!packageJson.dependencies[pkg]) {
910
+ packageJson.dependencies[pkg] = packages[pkg].length
911
+ ? semver.maxSatisfying([startVersion, packages[pkg]], `^${packages[pkg]}`)
912
+ : startVersion;
913
+ changed++;
914
+ }
915
+ else {
916
+ if (packageJson.dependencies[pkg].startsWith('^')) {
917
+ packageJson.dependencies[pkg] = packageJson.dependencies[pkg].replace(/^\^/, '');
918
+ changed++;
919
+ }
920
+ }
921
+ }
922
+ telemetry.mergeProperties({
923
+ changed_count: changed,
924
+ });
925
+ packageJson.dependencies = sortObject(packageJson.dependencies);
926
+ if (changed > 0) {
927
+ fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
928
+ console.log(`${changed} packages updated.
694
929
 
695
930
  Remove your node_modules directory and package lock file and re-install.`);
931
+ }
932
+ else {
933
+ console.log('No changes needed. The relevant TanStack packages are already pinned.');
934
+ }
935
+ });
696
936
  }
697
- else {
698
- console.log('No changes needed. The relevant TanStack packages are already pinned.');
937
+ catch (error) {
938
+ log.error(formatErrorMessage(error));
939
+ process.exit(1);
699
940
  }
700
941
  });
942
+ const telemetryCommand = program.command('telemetry');
943
+ telemetryCommand
944
+ .command('status')
945
+ .description('Show anonymous telemetry status')
946
+ .option('--json', 'output JSON for automation', false)
947
+ .action(async (options) => {
948
+ const status = await getTelemetryStatus({ createIfMissing: true });
949
+ const payload = {
950
+ configPath: status.configPath,
951
+ disabledBy: status.disabledBy,
952
+ distinctId: status.distinctId,
953
+ enabled: status.enabled,
954
+ noticeVersion: status.noticeVersion,
955
+ };
956
+ if (options.json) {
957
+ printJson(payload);
958
+ return;
959
+ }
960
+ console.log(`Telemetry ${status.enabled ? 'enabled' : 'disabled'}`);
961
+ console.log(`Config: ${status.configPath}`);
962
+ if (status.disabledBy) {
963
+ console.log(`Disabled by: ${status.disabledBy}`);
964
+ }
965
+ });
966
+ telemetryCommand
967
+ .command('enable')
968
+ .description('Enable anonymous telemetry')
969
+ .action(async () => {
970
+ await setTelemetryEnabled(true);
971
+ console.log('Anonymous telemetry enabled');
972
+ });
973
+ telemetryCommand
974
+ .command('disable')
975
+ .description('Disable anonymous telemetry')
976
+ .action(async () => {
977
+ await setTelemetryEnabled(false);
978
+ console.log('Anonymous telemetry disabled');
979
+ });
701
980
  // === ADD SUBCOMMAND ===
702
981
  program
703
982
  .command('add')
704
983
  .argument('[add-on...]', 'Name of the add-ons (or add-ons separated by spaces or commas)')
705
984
  .option('--forced', 'Force the add-on to be added', false)
706
985
  .action(async (addOns, options) => {
707
- const parsedAddOns = [];
708
- for (const addOn of addOns) {
709
- if (addOn.includes(',') || addOn.includes(' ')) {
710
- parsedAddOns.push(...addOn.split(/[\s,]+/).map((addon) => addon.trim()));
711
- }
712
- else {
713
- parsedAddOns.push(addOn.trim());
714
- }
715
- }
716
- if (parsedAddOns.length < 1) {
717
- const selectedAddOns = await promptForAddOns();
718
- if (selectedAddOns.length) {
719
- await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
986
+ try {
987
+ await runWithTelemetry('add', {
988
+ properties: {
989
+ forced: options.forced,
990
+ },
991
+ }, async (telemetry) => {
992
+ const parsedAddOns = [];
993
+ for (const addOn of addOns) {
994
+ if (addOn.includes(',') || addOn.includes(' ')) {
995
+ parsedAddOns.push(...addOn.split(/[\s,]+/).map((addon) => addon.trim()));
996
+ }
997
+ else {
998
+ parsedAddOns.push(addOn.trim());
999
+ }
1000
+ }
1001
+ if (parsedAddOns.length < 1) {
1002
+ const selectedAddOns = await promptForAddOns();
1003
+ telemetry.mergeProperties({
1004
+ add_on_count: selectedAddOns.length,
1005
+ add_on_ids: sanitizeIdList(selectedAddOns),
1006
+ prompted: true,
1007
+ });
1008
+ if (selectedAddOns.length) {
1009
+ await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
1010
+ forced: options.forced,
1011
+ });
1012
+ }
1013
+ return;
1014
+ }
1015
+ telemetry.mergeProperties({
1016
+ add_on_count: parsedAddOns.length,
1017
+ add_on_ids: sanitizeIdList(parsedAddOns),
1018
+ prompted: false,
1019
+ });
1020
+ await addToApp(environment, parsedAddOns, resolve(process.cwd()), {
720
1021
  forced: options.forced,
721
1022
  });
722
- }
723
- }
724
- else {
725
- await addToApp(environment, parsedAddOns, resolve(process.cwd()), {
726
- forced: options.forced,
727
1023
  });
728
1024
  }
1025
+ catch (error) {
1026
+ log.error(formatErrorMessage(error));
1027
+ process.exit(1);
1028
+ }
729
1029
  });
730
1030
  // === ADD-ON SUBCOMMAND ===
731
1031
  const addOnCommand = program.command('add-on');
@@ -733,19 +1033,43 @@ Remove your node_modules directory and package lock file and re-install.`);
733
1033
  .command('init')
734
1034
  .description('Initialize an add-on from the current project')
735
1035
  .action(async () => {
736
- await initAddOn(environment);
1036
+ try {
1037
+ await runWithTelemetry('add-on:init', {}, async () => {
1038
+ await initAddOn(environment);
1039
+ });
1040
+ }
1041
+ catch (error) {
1042
+ log.error(formatErrorMessage(error));
1043
+ process.exit(1);
1044
+ }
737
1045
  });
738
1046
  addOnCommand
739
1047
  .command('compile')
740
1048
  .description('Update add-on from the current project')
741
1049
  .action(async () => {
742
- await compileAddOn(environment);
1050
+ try {
1051
+ await runWithTelemetry('add-on:compile', {}, async () => {
1052
+ await compileAddOn(environment);
1053
+ });
1054
+ }
1055
+ catch (error) {
1056
+ log.error(formatErrorMessage(error));
1057
+ process.exit(1);
1058
+ }
743
1059
  });
744
1060
  addOnCommand
745
1061
  .command('dev')
746
1062
  .description('Watch project files and continuously refresh .add-on and add-on.json')
747
1063
  .action(async () => {
748
- await devAddOn(environment);
1064
+ try {
1065
+ await runWithTelemetry('add-on:dev', {}, async () => {
1066
+ await devAddOn(environment);
1067
+ });
1068
+ }
1069
+ catch (error) {
1070
+ log.error(formatErrorMessage(error));
1071
+ process.exit(1);
1072
+ }
749
1073
  });
750
1074
  // === TEMPLATE SUBCOMMAND ===
751
1075
  const templateCommand = program.command('template');
@@ -753,13 +1077,29 @@ Remove your node_modules directory and package lock file and re-install.`);
753
1077
  .command('init')
754
1078
  .description('Initialize a project template from the current project')
755
1079
  .action(async () => {
756
- await initStarter(environment);
1080
+ try {
1081
+ await runWithTelemetry('template:init', {}, async () => {
1082
+ await initStarter(environment);
1083
+ });
1084
+ }
1085
+ catch (error) {
1086
+ log.error(formatErrorMessage(error));
1087
+ process.exit(1);
1088
+ }
757
1089
  });
758
1090
  templateCommand
759
1091
  .command('compile')
760
1092
  .description('Compile the template JSON file for the current project')
761
1093
  .action(async () => {
762
- await compileStarter(environment);
1094
+ try {
1095
+ await runWithTelemetry('template:compile', {}, async () => {
1096
+ await compileStarter(environment);
1097
+ });
1098
+ }
1099
+ catch (error) {
1100
+ log.error(formatErrorMessage(error));
1101
+ process.exit(1);
1102
+ }
763
1103
  });
764
1104
  // Legacy alias for template command
765
1105
  const starterCommand = program.command('starter');
@@ -767,13 +1107,29 @@ Remove your node_modules directory and package lock file and re-install.`);
767
1107
  .command('init')
768
1108
  .description('Deprecated alias: initialize a project template')
769
1109
  .action(async () => {
770
- await initStarter(environment);
1110
+ try {
1111
+ await runWithTelemetry('starter:init', {}, async () => {
1112
+ await initStarter(environment);
1113
+ });
1114
+ }
1115
+ catch (error) {
1116
+ log.error(formatErrorMessage(error));
1117
+ process.exit(1);
1118
+ }
771
1119
  });
772
1120
  starterCommand
773
1121
  .command('compile')
774
1122
  .description('Deprecated alias: compile the template JSON file')
775
1123
  .action(async () => {
776
- await compileStarter(environment);
1124
+ try {
1125
+ await runWithTelemetry('starter:compile', {}, async () => {
1126
+ await compileStarter(environment);
1127
+ });
1128
+ }
1129
+ catch (error) {
1130
+ log.error(formatErrorMessage(error));
1131
+ process.exit(1);
1132
+ }
777
1133
  });
778
1134
  // === LEGACY AUTO-CREATE MODE ===
779
1135
  // For backward compatibility with cli-aliases (create-tsrouter-app, etc.)