gekto 0.0.13 → 0.0.15
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/agents/agentWebSocket.js +170 -0
- package/dist/agents/gektoPersistent.js +3 -1
- package/dist/agents/gektoTools.js +14 -0
- package/dist/detectClaude.js +14 -0
- package/dist/inspectRepo.js +185 -0
- package/dist/onboarding.js +339 -0
- package/dist/portUtils.js +40 -0
- package/dist/posthog.js +53 -0
- package/dist/proxy.js +96 -132
- package/dist/terminal.js +5 -0
- package/dist/widget/gekto-widget.iife.js +279 -271
- package/package.json +2 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { detectClaude } from './detectClaude.js';
|
|
4
|
+
import { inspectRepo, describeStack, DEV_TEST_APP_DIR } from './inspectRepo.js';
|
|
5
|
+
import { isPortInUse, waitForPort, startDevServer } from './portUtils.js';
|
|
6
|
+
import { getPostHog, getDistinctId, initDistinctId } from './posthog.js';
|
|
7
|
+
const c = {
|
|
8
|
+
reset: '\x1b[0m',
|
|
9
|
+
bold: '\x1b[1m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
green: '\x1b[32m',
|
|
12
|
+
white: '\x1b[37m',
|
|
13
|
+
};
|
|
14
|
+
const FALLBACK_STACK = {
|
|
15
|
+
language: 'unknown',
|
|
16
|
+
hasWebUI: true,
|
|
17
|
+
port: 3000,
|
|
18
|
+
};
|
|
19
|
+
async function saveLeadToSheetDB(data) {
|
|
20
|
+
try {
|
|
21
|
+
await fetch('https://sheetdb.io/api/v1/hxn1hd5nzjxhd?sheet=Leads', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
data: {
|
|
26
|
+
email: data.email,
|
|
27
|
+
is_using_claude: data.isUsingClaude ? 'Yes' : 'No',
|
|
28
|
+
source: 'tui_onboarding',
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Silently fail - don't block onboarding
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function printStack(stack) {
|
|
38
|
+
const row = (label, value) => p.log.info(`${c.dim}${label.padEnd(11)}${c.reset} ${value ?? '(none)'}`);
|
|
39
|
+
row('Language:', stack.language);
|
|
40
|
+
row('Framework:', stack.framework);
|
|
41
|
+
row('Bundler:', stack.bundler);
|
|
42
|
+
row('Runtime:', stack.runtime);
|
|
43
|
+
row('Package mgr:', stack.packageManager);
|
|
44
|
+
row('Web UI:', stack.hasWebUI ? 'yes' : 'no');
|
|
45
|
+
row('Port:', stack.hasWebUI ? stack.port : null);
|
|
46
|
+
row('Dev cmd:', stack.hasWebUI ? stack.devCommand : null);
|
|
47
|
+
}
|
|
48
|
+
function cap(s) {
|
|
49
|
+
if (!s)
|
|
50
|
+
return '';
|
|
51
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
52
|
+
}
|
|
53
|
+
function describeProject(stack) {
|
|
54
|
+
if (stack.hasWebUI) {
|
|
55
|
+
const what = stack.framework ? `${cap(stack.framework)} app` : `${cap(stack.language)} app`;
|
|
56
|
+
const portPart = stack.port ? ` that runs on port ${stack.port}` : '';
|
|
57
|
+
return `Looks like it's a ${what}${portPart}. Gekto can run on top of your app.`;
|
|
58
|
+
}
|
|
59
|
+
const langPart = cap(stack.language);
|
|
60
|
+
const fwPart = stack.framework ? ` and ${cap(stack.framework)} framework` : '';
|
|
61
|
+
return `Hmm, I suggest this is a backend app that uses ${langPart}${fwPart}. There is no visual layer, only data — Gekto will run its widget standalone.`;
|
|
62
|
+
}
|
|
63
|
+
export async function runOnboarding(opts = {}) {
|
|
64
|
+
const dev = opts.dev ?? false;
|
|
65
|
+
const proxyPort = opts.defaultProxyPort ?? 3200;
|
|
66
|
+
let stack = { ...FALLBACK_STACK };
|
|
67
|
+
console.clear();
|
|
68
|
+
// Claude Code check — must come before anything else
|
|
69
|
+
const claudeSpinner = p.spinner();
|
|
70
|
+
claudeSpinner.start('Looking for installed Claude Code...');
|
|
71
|
+
const [claude] = await Promise.all([
|
|
72
|
+
opts.fakeNoClaude ? Promise.resolve({ available: false }) : detectClaude(),
|
|
73
|
+
new Promise(resolve => setTimeout(resolve, 1000)),
|
|
74
|
+
]);
|
|
75
|
+
if (!claude.available) {
|
|
76
|
+
claudeSpinner.stop('Claude Code not found on PATH');
|
|
77
|
+
p.log.warn('Gekto requires the Claude Code CLI to run.');
|
|
78
|
+
p.log.info(`Install it: ${c.white}npm install -g @anthropic-ai/claude-code${c.reset}`);
|
|
79
|
+
p.log.info(`Docs: ${c.white}https://docs.claude.com/en/docs/claude-code${c.reset}`);
|
|
80
|
+
p.cancel('Install Claude Code, then run gekto again.');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
claudeSpinner.stop(`Claude Code v${claude.version} detected`);
|
|
84
|
+
const fs = await import('fs');
|
|
85
|
+
const path = await import('path');
|
|
86
|
+
const STORE_PATH = path.join(process.cwd(), 'gekto-store.json');
|
|
87
|
+
const loadSettings = () => {
|
|
88
|
+
if (dev)
|
|
89
|
+
return undefined;
|
|
90
|
+
try {
|
|
91
|
+
if (fs.existsSync(STORE_PATH)) {
|
|
92
|
+
const store = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
|
|
93
|
+
const settings = store.data?.settings;
|
|
94
|
+
if (settings?.stack && typeof settings.stack.hasWebUI === 'boolean')
|
|
95
|
+
return settings;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
const saveSettings = (settings) => {
|
|
102
|
+
if (dev)
|
|
103
|
+
return;
|
|
104
|
+
let store = { version: 1, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), data: {} };
|
|
105
|
+
try {
|
|
106
|
+
if (fs.existsSync(STORE_PATH)) {
|
|
107
|
+
store = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
store.data.settings = settings;
|
|
112
|
+
store.updatedAt = new Date().toISOString();
|
|
113
|
+
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
|
|
114
|
+
};
|
|
115
|
+
const existingSettings = opts.force ? undefined : loadSettings();
|
|
116
|
+
if (existingSettings?.onboardingCompleted) {
|
|
117
|
+
initDistinctId(existingSettings.email);
|
|
118
|
+
console.log(`${c.dim}Loaded settings from gekto-store.json${c.reset}`);
|
|
119
|
+
return { stack: existingSettings.stack, proxyPort: existingSettings.proxyPort };
|
|
120
|
+
}
|
|
121
|
+
p.intro(`${c.green}${c.bold}create-gekto${c.reset}${dev ? ` ${c.dim}(dev mode — not persisting)${c.reset}` : ''}`);
|
|
122
|
+
// Optional: agent-based repo inspection (requires explicit consent)
|
|
123
|
+
let inspection = null;
|
|
124
|
+
const inspectAnswer = await p.confirm({
|
|
125
|
+
message: 'Inspect this repo with Claude to detect your tech stack? (read-only)',
|
|
126
|
+
initialValue: true,
|
|
127
|
+
});
|
|
128
|
+
if (p.isCancel(inspectAnswer)) {
|
|
129
|
+
p.cancel('Setup cancelled.');
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
if (inspectAnswer) {
|
|
133
|
+
const inspectSpinner = p.spinner();
|
|
134
|
+
inspectSpinner.start('Inspecting repository...');
|
|
135
|
+
inspection = await inspectRepo({
|
|
136
|
+
cwd: dev ? DEV_TEST_APP_DIR : process.cwd(),
|
|
137
|
+
onEvent: event => {
|
|
138
|
+
if (event.type === 'tool')
|
|
139
|
+
inspectSpinner.message(event.summary);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (inspection) {
|
|
143
|
+
inspectSpinner.stop(`Detected: ${describeStack(inspection)}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
inspectSpinner.stop('Inspection failed — continuing with manual setup');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// If inspection succeeded, show summary + narrative, then loop: agree / correct / manual.
|
|
150
|
+
let confirmedFromInspection = false;
|
|
151
|
+
let current = inspection;
|
|
152
|
+
while (current) {
|
|
153
|
+
printStack(current);
|
|
154
|
+
p.note(describeProject(current));
|
|
155
|
+
const choice = await p.select({
|
|
156
|
+
message: 'Agree?',
|
|
157
|
+
options: [
|
|
158
|
+
{ label: 'Yes, continue', value: 'yes' },
|
|
159
|
+
{ label: 'No, tell agent where he is wrong', value: 'tell' },
|
|
160
|
+
{ label: 'No, setup manually', value: 'manual' },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
if (p.isCancel(choice)) {
|
|
164
|
+
p.cancel('Setup cancelled.');
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
if (choice === 'yes') {
|
|
168
|
+
stack = current;
|
|
169
|
+
confirmedFromInspection = true;
|
|
170
|
+
inspection = current;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (choice === 'manual') {
|
|
174
|
+
inspection = current;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
// choice === 'tell' — re-inspect with user's correction
|
|
178
|
+
const correction = await p.text({
|
|
179
|
+
message: 'What did Claude get wrong?',
|
|
180
|
+
placeholder: "e.g. it's actually a Vue 3 app, not React",
|
|
181
|
+
});
|
|
182
|
+
if (p.isCancel(correction)) {
|
|
183
|
+
p.cancel('Setup cancelled.');
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
if (!correction.trim())
|
|
187
|
+
continue;
|
|
188
|
+
const refineSpinner = p.spinner();
|
|
189
|
+
refineSpinner.start('Re-inspecting with your correction...');
|
|
190
|
+
const refined = await inspectRepo({
|
|
191
|
+
cwd: dev ? DEV_TEST_APP_DIR : process.cwd(),
|
|
192
|
+
correction,
|
|
193
|
+
onEvent: event => {
|
|
194
|
+
if (event.type === 'tool')
|
|
195
|
+
refineSpinner.message(event.summary);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
if (refined) {
|
|
199
|
+
current = refined;
|
|
200
|
+
refineSpinner.stop(`Updated: ${describeStack(refined)}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
refineSpinner.stop('Re-inspection failed — keeping previous detection');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!confirmedFromInspection) {
|
|
207
|
+
// Manual override — only ask what actually affects behavior: hasWebUI + port.
|
|
208
|
+
// Keep detected lang/framework/etc. as descriptive metadata.
|
|
209
|
+
const hasWebUIAnswer = await p.confirm({
|
|
210
|
+
message: 'Does this project have a web UI Gekto should proxy?',
|
|
211
|
+
initialValue: inspection?.hasWebUI ?? true,
|
|
212
|
+
});
|
|
213
|
+
if (p.isCancel(hasWebUIAnswer)) {
|
|
214
|
+
p.cancel('Setup cancelled.');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
let port;
|
|
218
|
+
if (hasWebUIAnswer) {
|
|
219
|
+
const detectedPort = inspection?.port ? String(inspection.port) : '3000';
|
|
220
|
+
const portAnswer = await p.text({
|
|
221
|
+
message: 'What port is your dev server running on?',
|
|
222
|
+
placeholder: detectedPort,
|
|
223
|
+
defaultValue: detectedPort,
|
|
224
|
+
});
|
|
225
|
+
if (p.isCancel(portAnswer)) {
|
|
226
|
+
p.cancel('Setup cancelled.');
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
port = parseInt(portAnswer, 10);
|
|
230
|
+
}
|
|
231
|
+
stack = {
|
|
232
|
+
language: inspection?.language ?? 'unknown',
|
|
233
|
+
framework: inspection?.framework,
|
|
234
|
+
bundler: inspection?.bundler,
|
|
235
|
+
runtime: inspection?.runtime,
|
|
236
|
+
packageManager: inspection?.packageManager,
|
|
237
|
+
hasWebUI: hasWebUIAnswer,
|
|
238
|
+
port,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const emailAnswer = await p.text({
|
|
242
|
+
message: 'Enter your email for updates',
|
|
243
|
+
placeholder: 'you@example.com',
|
|
244
|
+
});
|
|
245
|
+
if (p.isCancel(emailAnswer)) {
|
|
246
|
+
p.cancel('Setup cancelled.');
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
const email = emailAnswer || '';
|
|
250
|
+
// If the project has a web UI and a port, verify the dev server is up — or offer to start it.
|
|
251
|
+
if (stack.hasWebUI && stack.port) {
|
|
252
|
+
const cwd = dev ? DEV_TEST_APP_DIR : process.cwd();
|
|
253
|
+
const portUsed = await isPortInUse(stack.port);
|
|
254
|
+
if (!portUsed) {
|
|
255
|
+
const options = [];
|
|
256
|
+
if (stack.devCommand) {
|
|
257
|
+
options.push({ label: `Yes, start using "${stack.devCommand}"`, value: 'detected' });
|
|
258
|
+
}
|
|
259
|
+
options.push({ label: 'Yes, write command', value: 'custom' });
|
|
260
|
+
options.push({ label: 'Not now', value: 'skip' });
|
|
261
|
+
const startChoice = await p.select({
|
|
262
|
+
message: `Your app isn't running on port ${stack.port}. Start it?`,
|
|
263
|
+
options,
|
|
264
|
+
});
|
|
265
|
+
if (p.isCancel(startChoice)) {
|
|
266
|
+
p.cancel('Setup cancelled.');
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
let command = null;
|
|
270
|
+
if (startChoice === 'detected') {
|
|
271
|
+
command = stack.devCommand;
|
|
272
|
+
}
|
|
273
|
+
else if (startChoice === 'custom') {
|
|
274
|
+
const cmdDefault = stack.devCommand || 'npm run dev';
|
|
275
|
+
const cmdAnswer = await p.text({
|
|
276
|
+
message: 'Command to start your dev server:',
|
|
277
|
+
placeholder: cmdDefault,
|
|
278
|
+
defaultValue: cmdDefault,
|
|
279
|
+
});
|
|
280
|
+
if (p.isCancel(cmdAnswer)) {
|
|
281
|
+
p.cancel('Setup cancelled.');
|
|
282
|
+
process.exit(0);
|
|
283
|
+
}
|
|
284
|
+
command = (cmdAnswer || cmdDefault).trim();
|
|
285
|
+
}
|
|
286
|
+
if (command) {
|
|
287
|
+
const launchSpinner = p.spinner();
|
|
288
|
+
launchSpinner.start(`Starting "${command}"...`);
|
|
289
|
+
startDevServer(command, cwd);
|
|
290
|
+
const opened = await waitForPort(stack.port, 60_000, elapsed => {
|
|
291
|
+
launchSpinner.message(`Waiting for port ${stack.port}... (${Math.floor(elapsed / 1000)}s)`);
|
|
292
|
+
});
|
|
293
|
+
if (opened) {
|
|
294
|
+
launchSpinner.stop(`Your app is up on port ${stack.port}`);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
launchSpinner.stop(`Timed out waiting for port ${stack.port} — Gekto will start anyway`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const spinner = p.spinner();
|
|
303
|
+
spinner.start(dev ? 'Finishing (dev mode — skipping persistence)...' : 'Preparing Gekto...');
|
|
304
|
+
if (!dev) {
|
|
305
|
+
await saveLeadToSheetDB({
|
|
306
|
+
email,
|
|
307
|
+
isUsingClaude: claude.available,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
saveSettings({ stack, proxyPort, onboardingCompleted: true, email });
|
|
311
|
+
initDistinctId(email);
|
|
312
|
+
getPostHog().capture({
|
|
313
|
+
distinctId: getDistinctId(),
|
|
314
|
+
event: 'onboarding completed',
|
|
315
|
+
properties: {
|
|
316
|
+
language: stack.language,
|
|
317
|
+
framework: stack.framework,
|
|
318
|
+
bundler: stack.bundler,
|
|
319
|
+
runtime: stack.runtime,
|
|
320
|
+
has_web_ui: stack.hasWebUI,
|
|
321
|
+
port: stack.port,
|
|
322
|
+
has_email: Boolean(email),
|
|
323
|
+
dev_mode: dev,
|
|
324
|
+
$set: email ? { email } : undefined,
|
|
325
|
+
$set_once: { initial_language: stack.language, initial_framework: stack.framework },
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
spinner.stop('Ready!');
|
|
329
|
+
p.outro(`${c.green}Starting Gekto...${c.reset}`);
|
|
330
|
+
return { stack, proxyPort };
|
|
331
|
+
}
|
|
332
|
+
// CLI entry — run `bun run src/onboarding.ts [--dev] [--no-claude]`
|
|
333
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
334
|
+
const dev = process.argv.includes('--dev');
|
|
335
|
+
const fakeNoClaude = process.argv.includes('--no-claude');
|
|
336
|
+
const result = await runOnboarding({ dev, fakeNoClaude });
|
|
337
|
+
console.log('\nResult:', result);
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export function isPortInUse(port, host = '127.0.0.1') {
|
|
4
|
+
return new Promise(resolve => {
|
|
5
|
+
const socket = new net.Socket();
|
|
6
|
+
let settled = false;
|
|
7
|
+
const done = (result) => {
|
|
8
|
+
if (settled)
|
|
9
|
+
return;
|
|
10
|
+
settled = true;
|
|
11
|
+
socket.destroy();
|
|
12
|
+
resolve(result);
|
|
13
|
+
};
|
|
14
|
+
socket.setTimeout(500);
|
|
15
|
+
socket.once('connect', () => done(true));
|
|
16
|
+
socket.once('error', () => done(false));
|
|
17
|
+
socket.once('timeout', () => done(false));
|
|
18
|
+
socket.connect(port, host);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export async function waitForPort(port, timeoutMs, onTick) {
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
while (Date.now() - start < timeoutMs) {
|
|
24
|
+
if (await isPortInUse(port))
|
|
25
|
+
return true;
|
|
26
|
+
onTick?.(Date.now() - start);
|
|
27
|
+
await new Promise(r => setTimeout(r, 500));
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
export function startDevServer(command, cwd) {
|
|
32
|
+
const child = spawn(command, [], {
|
|
33
|
+
cwd,
|
|
34
|
+
shell: true,
|
|
35
|
+
stdio: 'ignore',
|
|
36
|
+
detached: false,
|
|
37
|
+
});
|
|
38
|
+
child.on('error', () => { });
|
|
39
|
+
return child;
|
|
40
|
+
}
|
package/dist/posthog.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { PostHog } from 'posthog-node';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
// Public project API key — phc_ keys are safe to ship in client code.
|
|
6
|
+
const DEFAULT_API_KEY = 'phc_s8GT8r8YG23NJqUhpyq2XcNegWyLomZjnsJmmtxOKMf';
|
|
7
|
+
const DEFAULT_HOST = 'https://us.i.posthog.com';
|
|
8
|
+
// Lazy singleton — created on first use
|
|
9
|
+
let _client = null;
|
|
10
|
+
// Distinct ID used for all events — set once per process
|
|
11
|
+
let _distinctId = 'anonymous';
|
|
12
|
+
export function getPostHog() {
|
|
13
|
+
if (!_client) {
|
|
14
|
+
_client = new PostHog(process.env.POSTHOG_API_KEY || DEFAULT_API_KEY, {
|
|
15
|
+
host: process.env.POSTHOG_HOST || DEFAULT_HOST,
|
|
16
|
+
enableExceptionAutocapture: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return _client;
|
|
20
|
+
}
|
|
21
|
+
/** Return the current distinct ID for event capture. */
|
|
22
|
+
export function getDistinctId() {
|
|
23
|
+
return _distinctId;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Set the distinct ID from the user's stored email or a persisted install UUID.
|
|
27
|
+
* Should be called once during startup / after onboarding completes.
|
|
28
|
+
*/
|
|
29
|
+
export function initDistinctId(email) {
|
|
30
|
+
if (email && email.trim()) {
|
|
31
|
+
_distinctId = email.trim();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Fall back to a persisted install UUID stored alongside gekto-store.json
|
|
35
|
+
const installIdPath = join(process.cwd(), '.gekto-install-id');
|
|
36
|
+
if (existsSync(installIdPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const id = readFileSync(installIdPath, 'utf8').trim();
|
|
39
|
+
if (id) {
|
|
40
|
+
_distinctId = id;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { /* ignore */ }
|
|
45
|
+
}
|
|
46
|
+
// First run — generate and persist an install UUID
|
|
47
|
+
const id = randomUUID();
|
|
48
|
+
try {
|
|
49
|
+
writeFileSync(installIdPath, id, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
catch { /* ignore */ }
|
|
52
|
+
_distinctId = id;
|
|
53
|
+
}
|