@tanstack/cli 0.62.5 → 0.63.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.
@@ -0,0 +1,140 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ export const TELEMETRY_NOTICE_VERSION = 1;
7
+ function createDefaultTelemetryConfig() {
8
+ return {
9
+ distinctId: randomUUID(),
10
+ enabled: true,
11
+ noticeVersion: 0,
12
+ };
13
+ }
14
+ function getHomeDirectory() {
15
+ return process.env.HOME || homedir();
16
+ }
17
+ function isTelemetryConfig(value) {
18
+ if (!value || typeof value !== 'object') {
19
+ return false;
20
+ }
21
+ const record = value;
22
+ return (typeof record.distinctId === 'string' &&
23
+ typeof record.enabled === 'boolean' &&
24
+ typeof record.noticeVersion === 'number');
25
+ }
26
+ function getDisabledByEnvironment() {
27
+ if (process.env.DO_NOT_TRACK === '1') {
28
+ return 'env';
29
+ }
30
+ if (process.env.TANSTACK_CLI_TELEMETRY_DISABLED === '1') {
31
+ return 'env';
32
+ }
33
+ if (process.env.CI) {
34
+ return 'ci';
35
+ }
36
+ return undefined;
37
+ }
38
+ export function getTelemetryConfigPath() {
39
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
40
+ if (xdgConfigHome) {
41
+ return join(xdgConfigHome, 'tanstack', 'cli.json');
42
+ }
43
+ const homeDirectory = getHomeDirectory().trim();
44
+ if (homeDirectory) {
45
+ return join(homeDirectory, '.config', 'tanstack', 'cli.json');
46
+ }
47
+ return join(process.cwd(), '.tanstack', 'cli.json');
48
+ }
49
+ async function readTelemetryConfigFile(configPath) {
50
+ if (!existsSync(configPath)) {
51
+ return undefined;
52
+ }
53
+ try {
54
+ const raw = await readFile(configPath, 'utf8');
55
+ const parsed = JSON.parse(raw);
56
+ if (!isTelemetryConfig(parsed)) {
57
+ return undefined;
58
+ }
59
+ return parsed;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ async function writeTelemetryConfigFile(configPath, config) {
66
+ await mkdir(dirname(configPath), { recursive: true });
67
+ await writeFile(configPath, JSON.stringify(config, null, 2));
68
+ }
69
+ async function readOrCreateTelemetryConfig(createIfMissing) {
70
+ const configPath = getTelemetryConfigPath();
71
+ const existingConfig = await readTelemetryConfigFile(configPath);
72
+ if (existingConfig || !createIfMissing) {
73
+ return {
74
+ config: existingConfig,
75
+ configPath,
76
+ };
77
+ }
78
+ const createdConfig = createDefaultTelemetryConfig();
79
+ await writeTelemetryConfigFile(configPath, createdConfig);
80
+ return {
81
+ config: createdConfig,
82
+ configPath,
83
+ };
84
+ }
85
+ export async function getTelemetryStatus(opts) {
86
+ const disabledBy = getDisabledByEnvironment();
87
+ const { config, configPath } = await readOrCreateTelemetryConfig(opts?.createIfMissing ?? !disabledBy);
88
+ if (!config) {
89
+ return {
90
+ configPath,
91
+ disabledBy,
92
+ distinctId: undefined,
93
+ enabled: false,
94
+ noticeVersion: 0,
95
+ };
96
+ }
97
+ if (disabledBy) {
98
+ return {
99
+ configPath,
100
+ disabledBy,
101
+ distinctId: config.distinctId,
102
+ enabled: false,
103
+ noticeVersion: config.noticeVersion,
104
+ };
105
+ }
106
+ if (!config.enabled) {
107
+ return {
108
+ configPath,
109
+ disabledBy: 'config',
110
+ distinctId: config.distinctId,
111
+ enabled: false,
112
+ noticeVersion: config.noticeVersion,
113
+ };
114
+ }
115
+ return {
116
+ configPath,
117
+ distinctId: config.distinctId,
118
+ enabled: true,
119
+ noticeVersion: config.noticeVersion,
120
+ };
121
+ }
122
+ export async function markTelemetryNoticeSeen() {
123
+ const { config, configPath } = await readOrCreateTelemetryConfig(true);
124
+ if (!config || config.noticeVersion >= TELEMETRY_NOTICE_VERSION) {
125
+ return;
126
+ }
127
+ await writeTelemetryConfigFile(configPath, {
128
+ ...config,
129
+ noticeVersion: TELEMETRY_NOTICE_VERSION,
130
+ });
131
+ }
132
+ export async function setTelemetryEnabled(enabled) {
133
+ const { config, configPath } = await readOrCreateTelemetryConfig(true);
134
+ const nextConfig = {
135
+ ...(config || createDefaultTelemetryConfig()),
136
+ enabled,
137
+ };
138
+ await writeTelemetryConfigFile(configPath, nextConfig);
139
+ return nextConfig;
140
+ }
@@ -0,0 +1,195 @@
1
+ import { version as nodeVersion } from 'node:process';
2
+ import { getTelemetryStatus, markTelemetryNoticeSeen, TELEMETRY_NOTICE_VERSION, } from './telemetry-config.js';
3
+ const POSTHOG_API_HOST = 'https://us.i.posthog.com';
4
+ const POSTHOG_CAPTURE_ENDPOINT = `${POSTHOG_API_HOST}/capture/`;
5
+ const POSTHOG_PROJECT_TOKEN = 'phc_xJ2VBahJBzy3BShLhuGpw7EyoSuQtgwXXvhE9BYtHuKQ';
6
+ const TELEMETRY_NOTICE = 'TanStack CLI sends anonymous usage telemetry by default. It never sends project names, paths, raw search text, template URLs, add-on config values, or raw error messages. Disable it with `tanstack telemetry disable` or `TANSTACK_CLI_TELEMETRY_DISABLED=1`.';
7
+ const TELEMETRY_TIMEOUT_MS = 1200;
8
+ let telemetryStatusPromise;
9
+ function getNodeMajorVersion() {
10
+ return Number.parseInt(nodeVersion.replace(/^v/, '').split('.')[0] || '0', 10);
11
+ }
12
+ function cleanProperties(value) {
13
+ if (Array.isArray(value)) {
14
+ return value
15
+ .map((entry) => cleanProperties(entry))
16
+ .filter((entry) => entry !== undefined);
17
+ }
18
+ if (value && typeof value === 'object') {
19
+ const cleanedEntries = Object.entries(value)
20
+ .map(([key, entry]) => [key, cleanProperties(entry)])
21
+ .filter(([, entry]) => entry !== undefined);
22
+ return Object.fromEntries(cleanedEntries);
23
+ }
24
+ if (value === undefined) {
25
+ return undefined;
26
+ }
27
+ return value;
28
+ }
29
+ function getErrorCode(error) {
30
+ if (!error || typeof error !== 'object') {
31
+ return 'unknown_error';
32
+ }
33
+ const message = String(error.message || '').toLowerCase();
34
+ if (message.includes('cancel')) {
35
+ return 'cancelled';
36
+ }
37
+ if (message.includes('invalid')) {
38
+ return 'invalid_input';
39
+ }
40
+ if (message.includes('not found')) {
41
+ return 'not_found';
42
+ }
43
+ if (message.includes('timed out')) {
44
+ return 'timeout';
45
+ }
46
+ if (message.includes('fetch') ||
47
+ message.includes('network') ||
48
+ message.includes('econn')) {
49
+ return 'network_error';
50
+ }
51
+ if (message.includes('permission') || message.includes('eacces')) {
52
+ return 'permission_error';
53
+ }
54
+ return 'unknown_error';
55
+ }
56
+ async function fetchTelemetryStatus() {
57
+ telemetryStatusPromise ?? (telemetryStatusPromise = getTelemetryStatus());
58
+ return telemetryStatusPromise;
59
+ }
60
+ async function postEvent(event, distinctId, properties) {
61
+ const controller = new AbortController();
62
+ const timeout = setTimeout(() => {
63
+ controller.abort();
64
+ }, TELEMETRY_TIMEOUT_MS);
65
+ try {
66
+ await fetch(POSTHOG_CAPTURE_ENDPOINT, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ },
71
+ body: JSON.stringify({
72
+ api_key: POSTHOG_PROJECT_TOKEN,
73
+ distinct_id: distinctId,
74
+ event,
75
+ properties,
76
+ }),
77
+ signal: controller.signal,
78
+ });
79
+ }
80
+ catch {
81
+ // Telemetry must never affect CLI behavior.
82
+ }
83
+ finally {
84
+ clearTimeout(timeout);
85
+ }
86
+ }
87
+ export class TelemetryClient {
88
+ constructor(status) {
89
+ this.commandProperties = {};
90
+ this.pendingSteps = new Map();
91
+ this.completedSteps = [];
92
+ this.disabledBy = status.disabledBy;
93
+ this.distinctId = status.distinctId;
94
+ this.enabled = status.enabled && Boolean(status.distinctId);
95
+ }
96
+ mergeProperties(properties) {
97
+ this.commandProperties = {
98
+ ...this.commandProperties,
99
+ ...properties,
100
+ };
101
+ }
102
+ startStep(info) {
103
+ if (!this.enabled) {
104
+ return;
105
+ }
106
+ this.pendingSteps.set(info.id, {
107
+ startedAt: Date.now(),
108
+ type: info.type,
109
+ });
110
+ }
111
+ finishStep(id) {
112
+ if (!this.enabled) {
113
+ return;
114
+ }
115
+ const step = this.pendingSteps.get(id);
116
+ if (!step) {
117
+ return;
118
+ }
119
+ this.pendingSteps.delete(id);
120
+ this.completedSteps.push({
121
+ durationMs: Math.max(Date.now() - step.startedAt, 0),
122
+ id,
123
+ type: step.type,
124
+ });
125
+ }
126
+ async captureCommandStarted(command, properties) {
127
+ this.mergeProperties(properties);
128
+ if (!this.enabled || !this.distinctId) {
129
+ return;
130
+ }
131
+ void postEvent('command_started', this.distinctId, cleanProperties({
132
+ ...this.baseProperties(),
133
+ ...this.commandProperties,
134
+ command,
135
+ }));
136
+ }
137
+ async captureCommandCompleted(command, durationMs) {
138
+ if (!this.enabled || !this.distinctId) {
139
+ return;
140
+ }
141
+ await postEvent('command_completed', this.distinctId, cleanProperties({
142
+ ...this.baseProperties(),
143
+ ...this.commandProperties,
144
+ command,
145
+ duration_ms: durationMs,
146
+ result: 'success',
147
+ steps: this.completedSteps.map((step) => ({
148
+ duration_ms: step.durationMs,
149
+ id: step.id,
150
+ type: step.type,
151
+ })),
152
+ }));
153
+ }
154
+ async captureCommandFailed(command, durationMs, error) {
155
+ if (!this.enabled || !this.distinctId) {
156
+ return;
157
+ }
158
+ await postEvent('command_failed', this.distinctId, cleanProperties({
159
+ ...this.baseProperties(),
160
+ ...this.commandProperties,
161
+ command,
162
+ duration_ms: durationMs,
163
+ error_code: getErrorCode(error),
164
+ result: 'failed',
165
+ steps: this.completedSteps.map((step) => ({
166
+ duration_ms: step.durationMs,
167
+ id: step.id,
168
+ type: step.type,
169
+ })),
170
+ }));
171
+ }
172
+ baseProperties() {
173
+ return {
174
+ $lib: 'tanstack-cli',
175
+ disabled_by: this.disabledBy,
176
+ node_major: getNodeMajorVersion(),
177
+ os_arch: process.arch,
178
+ os_platform: process.platform,
179
+ };
180
+ }
181
+ }
182
+ export async function createTelemetryClient(opts) {
183
+ const status = await fetchTelemetryStatus();
184
+ if (status.enabled &&
185
+ status.noticeVersion < TELEMETRY_NOTICE_VERSION &&
186
+ !opts?.json) {
187
+ console.error(TELEMETRY_NOTICE);
188
+ await markTelemetryNoticeSeen();
189
+ telemetryStatusPromise = undefined;
190
+ }
191
+ return new TelemetryClient(await fetchTelemetryStatus());
192
+ }
193
+ export function resetTelemetryStateForTests() {
194
+ telemetryStatusPromise = undefined;
195
+ }
@@ -0,0 +1,47 @@
1
+ export interface TelemetryConfig {
2
+ distinctId: string;
3
+ enabled: boolean;
4
+ noticeVersion: number;
5
+ }
6
+ export interface TelemetryStatus {
7
+ configPath: string;
8
+ disabledBy?: 'ci' | 'config' | 'env';
9
+ distinctId?: string;
10
+ enabled: boolean;
11
+ noticeVersion: number;
12
+ }
13
+ export declare const TELEMETRY_NOTICE_VERSION = 1;
14
+ export declare function getTelemetryConfigPath(): string;
15
+ export declare function getTelemetryStatus(opts?: {
16
+ createIfMissing?: boolean;
17
+ }): Promise<{
18
+ configPath: string;
19
+ disabledBy: "ci" | "env" | undefined;
20
+ distinctId: undefined;
21
+ enabled: false;
22
+ noticeVersion: number;
23
+ } | {
24
+ configPath: string;
25
+ disabledBy: "ci" | "env";
26
+ distinctId: string;
27
+ enabled: false;
28
+ noticeVersion: number;
29
+ } | {
30
+ configPath: string;
31
+ disabledBy: "config";
32
+ distinctId: string;
33
+ enabled: false;
34
+ noticeVersion: number;
35
+ } | {
36
+ configPath: string;
37
+ distinctId: string;
38
+ enabled: true;
39
+ noticeVersion: number;
40
+ disabledBy?: undefined;
41
+ }>;
42
+ export declare function markTelemetryNoticeSeen(): Promise<void>;
43
+ export declare function setTelemetryEnabled(enabled: boolean): Promise<{
44
+ enabled: boolean;
45
+ distinctId: string;
46
+ noticeVersion: number;
47
+ }>;
@@ -0,0 +1,24 @@
1
+ import { getTelemetryStatus } from './telemetry-config.js';
2
+ import type { StatusEvent } from '@tanstack/create';
3
+ type TelemetryProperties = Record<string, unknown>;
4
+ export declare class TelemetryClient {
5
+ private commandProperties;
6
+ private pendingSteps;
7
+ private completedSteps;
8
+ private readonly disabledBy?;
9
+ private readonly distinctId?;
10
+ readonly enabled: boolean;
11
+ constructor(status: Awaited<ReturnType<typeof getTelemetryStatus>>);
12
+ mergeProperties(properties: TelemetryProperties): void;
13
+ startStep(info: StatusEvent): void;
14
+ finishStep(id: string): void;
15
+ captureCommandStarted(command: string, properties: TelemetryProperties): Promise<void>;
16
+ captureCommandCompleted(command: string, durationMs: number): Promise<void>;
17
+ captureCommandFailed(command: string, durationMs: number, error: unknown): Promise<void>;
18
+ private baseProperties;
19
+ }
20
+ export declare function createTelemetryClient(opts?: {
21
+ json?: boolean;
22
+ }): Promise<TelemetryClient>;
23
+ export declare function resetTelemetryStateForTests(): void;
24
+ export {};
@@ -14,6 +14,8 @@ export interface CliOptions {
14
14
  templateId?: string;
15
15
  targetDir?: string;
16
16
  interactive?: boolean;
17
+ nonInteractive?: boolean;
18
+ yes?: boolean;
17
19
  devWatch?: string;
18
20
  runDev?: boolean;
19
21
  install?: boolean;
@@ -1,2 +1,3 @@
1
1
  import type { Environment } from '@tanstack/create';
2
- export declare function createUIEnvironment(appName: string, silent: boolean): Environment;
2
+ import type { TelemetryClient } from './telemetry.js';
3
+ export declare function createUIEnvironment(appName: string, silent: boolean, getTelemetry?: () => TelemetryClient | undefined): Environment;
@@ -1,7 +1,7 @@
1
1
  import { cancel, confirm, intro, isCancel, log, outro, spinner, } from '@clack/prompts';
2
2
  import chalk from 'chalk';
3
3
  import { createDefaultEnvironment } from '@tanstack/create';
4
- export function createUIEnvironment(appName, silent) {
4
+ export function createUIEnvironment(appName, silent, getTelemetry) {
5
5
  const defaultEnvironment = createDefaultEnvironment();
6
6
  let newEnvironment = {
7
7
  ...defaultEnvironment,
@@ -46,6 +46,23 @@ export function createUIEnvironment(appName, silent) {
46
46
  },
47
47
  };
48
48
  },
49
+ startStep: (info) => {
50
+ getTelemetry?.()?.startStep(info);
51
+ },
52
+ finishStep: (id, _finalMessage) => {
53
+ getTelemetry?.()?.finishStep(id);
54
+ },
55
+ };
56
+ }
57
+ else {
58
+ newEnvironment = {
59
+ ...newEnvironment,
60
+ startStep: (info) => {
61
+ getTelemetry?.()?.startStep(info);
62
+ },
63
+ finishStep: (id, _finalMessage) => {
64
+ getTelemetry?.()?.finishStep(id);
65
+ },
49
66
  };
50
67
  }
51
68
  return newEnvironment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cli",
3
- "version": "0.62.5",
3
+ "version": "0.63.0",
4
4
  "description": "TanStack CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",