switchman-dev 0.1.4 → 0.1.6

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,210 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { dirname, join } from 'path';
4
+ import { randomUUID } from 'crypto';
5
+ import readline from 'readline/promises';
6
+
7
+ export const DEFAULT_TELEMETRY_HOST = 'https://us.i.posthog.com';
8
+
9
+ function normalizeBoolean(value) {
10
+ if (typeof value === 'boolean') return value;
11
+ if (typeof value !== 'string') return null;
12
+ const lowered = value.trim().toLowerCase();
13
+ if (['1', 'true', 'yes', 'y', 'on'].includes(lowered)) return true;
14
+ if (['0', 'false', 'no', 'n', 'off'].includes(lowered)) return false;
15
+ return null;
16
+ }
17
+
18
+ export function getTelemetryConfigPath(homeDir = homedir()) {
19
+ return join(homeDir, '.switchman', 'config.json');
20
+ }
21
+
22
+ export function getTelemetryRuntimeConfig(env = process.env) {
23
+ return {
24
+ apiKey: env.SWITCHMAN_TELEMETRY_API_KEY || env.POSTHOG_API_KEY || null,
25
+ host: env.SWITCHMAN_TELEMETRY_HOST || env.POSTHOG_HOST || DEFAULT_TELEMETRY_HOST,
26
+ disabled: normalizeBoolean(env.SWITCHMAN_TELEMETRY_DISABLED) === true,
27
+ };
28
+ }
29
+
30
+ export function loadTelemetryConfig(homeDir = homedir()) {
31
+ const configPath = getTelemetryConfigPath(homeDir);
32
+ if (!existsSync(configPath)) {
33
+ return {
34
+ telemetry_enabled: null,
35
+ telemetry_install_id: null,
36
+ telemetry_prompted_at: null,
37
+ };
38
+ }
39
+
40
+ try {
41
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
42
+ return {
43
+ telemetry_enabled: typeof parsed?.telemetry_enabled === 'boolean' ? parsed.telemetry_enabled : null,
44
+ telemetry_install_id: typeof parsed?.telemetry_install_id === 'string' ? parsed.telemetry_install_id : null,
45
+ telemetry_prompted_at: typeof parsed?.telemetry_prompted_at === 'string' ? parsed.telemetry_prompted_at : null,
46
+ };
47
+ } catch {
48
+ return {
49
+ telemetry_enabled: null,
50
+ telemetry_install_id: null,
51
+ telemetry_prompted_at: null,
52
+ };
53
+ }
54
+ }
55
+
56
+ export function writeTelemetryConfig(homeDir = homedir(), config = {}) {
57
+ const configPath = getTelemetryConfigPath(homeDir);
58
+ mkdirSync(dirname(configPath), { recursive: true });
59
+ const normalized = {
60
+ telemetry_enabled: typeof config.telemetry_enabled === 'boolean' ? config.telemetry_enabled : null,
61
+ telemetry_install_id: typeof config.telemetry_install_id === 'string' ? config.telemetry_install_id : (config.telemetry_enabled ? randomUUID() : null),
62
+ telemetry_prompted_at: typeof config.telemetry_prompted_at === 'string' ? config.telemetry_prompted_at : null,
63
+ };
64
+ writeFileSync(configPath, `${JSON.stringify(normalized, null, 2)}\n`);
65
+ return { path: configPath, config: normalized };
66
+ }
67
+
68
+ export function enableTelemetry(homeDir = homedir()) {
69
+ const current = loadTelemetryConfig(homeDir);
70
+ return writeTelemetryConfig(homeDir, {
71
+ ...current,
72
+ telemetry_enabled: true,
73
+ telemetry_install_id: current.telemetry_install_id || randomUUID(),
74
+ telemetry_prompted_at: new Date().toISOString(),
75
+ });
76
+ }
77
+
78
+ export function disableTelemetry(homeDir = homedir()) {
79
+ const current = loadTelemetryConfig(homeDir);
80
+ return writeTelemetryConfig(homeDir, {
81
+ ...current,
82
+ telemetry_enabled: false,
83
+ telemetry_install_id: current.telemetry_install_id || randomUUID(),
84
+ telemetry_prompted_at: new Date().toISOString(),
85
+ });
86
+ }
87
+
88
+ export async function maybePromptForTelemetry({ homeDir = homedir(), stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
89
+ const runtime = getTelemetryRuntimeConfig(env);
90
+ if (!runtime.apiKey || runtime.disabled) {
91
+ return { prompted: false, enabled: false, available: Boolean(runtime.apiKey) && !runtime.disabled };
92
+ }
93
+
94
+ const current = loadTelemetryConfig(homeDir);
95
+ if (typeof current.telemetry_enabled === 'boolean') {
96
+ return { prompted: false, enabled: current.telemetry_enabled, available: true };
97
+ }
98
+
99
+ if (!stdin?.isTTY || !stdout?.isTTY) {
100
+ return { prompted: false, enabled: false, available: true };
101
+ }
102
+
103
+ const rl = readline.createInterface({ input: stdin, output: stdout });
104
+ try {
105
+ stdout.write('\nHelp improve Switchman?\n');
106
+ stdout.write('If you opt in, Switchman will send anonymous usage events like setup success,\n');
107
+ stdout.write('verify-setup pass, status --watch, queue usage, and gate outcomes.\n');
108
+ stdout.write('No code, prompts, file contents, repo names, or secrets are collected.\n\n');
109
+ const answer = await rl.question('Enable telemetry? [y/N] ');
110
+ const enabled = ['y', 'yes'].includes(String(answer || '').trim().toLowerCase());
111
+ if (enabled) {
112
+ enableTelemetry(homeDir);
113
+ } else {
114
+ disableTelemetry(homeDir);
115
+ }
116
+ return { prompted: true, enabled, available: true };
117
+ } finally {
118
+ rl.close();
119
+ }
120
+ }
121
+
122
+ export async function captureTelemetryEvent(event, properties = {}, {
123
+ homeDir = homedir(),
124
+ env = process.env,
125
+ timeoutMs = 1500,
126
+ } = {}) {
127
+ const result = await sendTelemetryEvent(event, properties, {
128
+ homeDir,
129
+ env,
130
+ timeoutMs,
131
+ });
132
+ return result.ok;
133
+ }
134
+
135
+ export async function sendTelemetryEvent(event, properties = {}, {
136
+ homeDir = homedir(),
137
+ env = process.env,
138
+ timeoutMs = 1500,
139
+ } = {}) {
140
+ const runtime = getTelemetryRuntimeConfig(env);
141
+ if (!runtime.apiKey) {
142
+ return {
143
+ ok: false,
144
+ reason: 'not_configured',
145
+ status: null,
146
+ destination: runtime.host,
147
+ };
148
+ }
149
+ if (runtime.disabled) {
150
+ return {
151
+ ok: false,
152
+ reason: 'disabled_by_env',
153
+ status: null,
154
+ destination: runtime.host,
155
+ };
156
+ }
157
+ if (typeof fetch !== 'function') {
158
+ return {
159
+ ok: false,
160
+ reason: 'fetch_unavailable',
161
+ status: null,
162
+ destination: runtime.host,
163
+ };
164
+ }
165
+
166
+ const config = loadTelemetryConfig(homeDir);
167
+ if (config.telemetry_enabled !== true || !config.telemetry_install_id) {
168
+ return {
169
+ ok: false,
170
+ reason: 'not_enabled',
171
+ status: null,
172
+ destination: runtime.host,
173
+ };
174
+ }
175
+
176
+ try {
177
+ const controller = new AbortController();
178
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
179
+ const response = await fetch(`${runtime.host.replace(/\/$/, '')}/capture/`, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({
183
+ api_key: runtime.apiKey,
184
+ event,
185
+ distinct_id: config.telemetry_install_id,
186
+ properties: {
187
+ source: 'switchman-cli',
188
+ ...properties,
189
+ },
190
+ timestamp: new Date().toISOString(),
191
+ }),
192
+ signal: controller.signal,
193
+ });
194
+ clearTimeout(timer);
195
+ return {
196
+ ok: response.ok,
197
+ reason: response.ok ? null : 'http_error',
198
+ status: response.status,
199
+ destination: runtime.host,
200
+ };
201
+ } catch (err) {
202
+ return {
203
+ ok: false,
204
+ reason: err?.name === 'AbortError' ? 'timeout' : 'network_error',
205
+ status: null,
206
+ destination: runtime.host,
207
+ error: String(err?.message || err),
208
+ };
209
+ }
210
+ }