@gurulu/cli 0.1.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.
Files changed (77) hide show
  1. package/README.md +66 -0
  2. package/bin/gurulu.js +2 -0
  3. package/dist/api-client.d.ts +27 -0
  4. package/dist/api-client.js +150 -0
  5. package/dist/commands/add-server.d.ts +9 -0
  6. package/dist/commands/add-server.js +155 -0
  7. package/dist/commands/alerts.d.ts +22 -0
  8. package/dist/commands/alerts.js +281 -0
  9. package/dist/commands/api-keys.d.ts +20 -0
  10. package/dist/commands/api-keys.js +130 -0
  11. package/dist/commands/audiences.d.ts +16 -0
  12. package/dist/commands/audiences.js +180 -0
  13. package/dist/commands/audit.d.ts +20 -0
  14. package/dist/commands/audit.js +130 -0
  15. package/dist/commands/auth.d.ts +20 -0
  16. package/dist/commands/auth.js +214 -0
  17. package/dist/commands/chat.d.ts +18 -0
  18. package/dist/commands/chat.js +117 -0
  19. package/dist/commands/config.d.ts +10 -0
  20. package/dist/commands/config.js +92 -0
  21. package/dist/commands/db.d.ts +25 -0
  22. package/dist/commands/db.js +322 -0
  23. package/dist/commands/destinations.d.ts +20 -0
  24. package/dist/commands/destinations.js +191 -0
  25. package/dist/commands/doctor.d.ts +7 -0
  26. package/dist/commands/doctor.js +318 -0
  27. package/dist/commands/events.d.ts +27 -0
  28. package/dist/commands/events.js +147 -0
  29. package/dist/commands/experiments.d.ts +18 -0
  30. package/dist/commands/experiments.js +233 -0
  31. package/dist/commands/identity.d.ts +13 -0
  32. package/dist/commands/identity.js +107 -0
  33. package/dist/commands/init.d.ts +11 -0
  34. package/dist/commands/init.js +215 -0
  35. package/dist/commands/insights.d.ts +10 -0
  36. package/dist/commands/insights.js +65 -0
  37. package/dist/commands/install.d.ts +233 -0
  38. package/dist/commands/install.js +920 -0
  39. package/dist/commands/login.d.ts +20 -0
  40. package/dist/commands/login.js +170 -0
  41. package/dist/commands/logout.d.ts +10 -0
  42. package/dist/commands/logout.js +41 -0
  43. package/dist/commands/playground.d.ts +11 -0
  44. package/dist/commands/playground.js +47 -0
  45. package/dist/commands/sites.d.ts +18 -0
  46. package/dist/commands/sites.js +139 -0
  47. package/dist/commands/sourcemap.d.ts +21 -0
  48. package/dist/commands/sourcemap.js +137 -0
  49. package/dist/commands/status.d.ts +7 -0
  50. package/dist/commands/status.js +136 -0
  51. package/dist/commands/warehouse.d.ts +20 -0
  52. package/dist/commands/warehouse.js +65 -0
  53. package/dist/commands/warehouses.d.ts +17 -0
  54. package/dist/commands/warehouses.js +182 -0
  55. package/dist/commands/whoami.d.ts +9 -0
  56. package/dist/commands/whoami.js +47 -0
  57. package/dist/config.d.ts +75 -0
  58. package/dist/config.js +329 -0
  59. package/dist/frameworks/detect.d.ts +8 -0
  60. package/dist/frameworks/detect.js +362 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.js +429 -0
  63. package/dist/install-intent-proposal.d.ts +99 -0
  64. package/dist/install-intent-proposal.js +202 -0
  65. package/dist/utils/api.d.ts +20 -0
  66. package/dist/utils/api.js +47 -0
  67. package/dist/utils/config.d.ts +13 -0
  68. package/dist/utils/config.js +30 -0
  69. package/dist/utils/confirm.d.ts +17 -0
  70. package/dist/utils/confirm.js +40 -0
  71. package/dist/utils/dry-run.d.ts +20 -0
  72. package/dist/utils/dry-run.js +67 -0
  73. package/dist/utils/from-file.d.ts +9 -0
  74. package/dist/utils/from-file.js +72 -0
  75. package/dist/utils/ui.d.ts +14 -0
  76. package/dist/utils/ui.js +59 -0
  77. package/package.json +26 -0
@@ -0,0 +1,920 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 16 A2 — `gurulu install` user-facing auto-install command.
4
+ *
5
+ * Runs the full bootstrap loop against a target repo:
6
+ * 1. scan (scripts/gurulu-scan.mjs)
7
+ * 2. confirm (interactive, skipped with --yes)
8
+ * 3. agentic plan (scripts/gurulu-agentic-install.mjs --dry-run)
9
+ * 4. apply (scripts/gurulu-agentic-install.mjs --apply)
10
+ * 5. npm install (pm detected from lockfile)
11
+ * 6. .env merge (.env.local for Next.js, .env otherwise)
12
+ * 7. ingest ping (POST <ingestUrl>/api/ingest/v1/health)
13
+ *
14
+ * The command is designed so the flow orchestration (`runInstallFlow`) can
15
+ * be unit-tested with injected deps — no real spawns or fetches required.
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.resolveScriptsDir = resolveScriptsDir;
52
+ exports.detectPackageManager = detectPackageManager;
53
+ exports.packageInstallArgs = packageInstallArgs;
54
+ exports.mergeEnvFile = mergeEnvFile;
55
+ exports.createDefaultDeps = createDefaultDeps;
56
+ exports.runInstallFlow = runInstallFlow;
57
+ exports.repoHashOf = repoHashOf;
58
+ exports.pickSite = pickSite;
59
+ exports.emitInstallTelemetry = emitInstallTelemetry;
60
+ exports.runAuthenticatedInstallFlow = runAuthenticatedInstallFlow;
61
+ exports.installCommand = installCommand;
62
+ const fs = __importStar(require("fs"));
63
+ const path = __importStar(require("path"));
64
+ const crypto = __importStar(require("crypto"));
65
+ const child_process_1 = require("child_process");
66
+ const ui_1 = require("../utils/ui");
67
+ const config_1 = require("../config");
68
+ const api_client_1 = require("../api-client");
69
+ const install_intent_proposal_1 = require("../install-intent-proposal");
70
+ // ---------------------------------------------------------------------------
71
+ // Script resolution
72
+ // ---------------------------------------------------------------------------
73
+ /**
74
+ * Locate the repo scripts/ directory (works when installed from source tree
75
+ * or as a bundled package; falls back to GURULU_SCRIPTS_DIR env override).
76
+ */
77
+ function resolveScriptsDir(startDir = __dirname) {
78
+ if (process.env.GURULU_SCRIPTS_DIR && fs.existsSync(process.env.GURULU_SCRIPTS_DIR)) {
79
+ return process.env.GURULU_SCRIPTS_DIR;
80
+ }
81
+ let dir = startDir;
82
+ for (let i = 0; i < 8; i++) {
83
+ const candidate = path.join(dir, 'scripts', 'gurulu-scan.mjs');
84
+ if (fs.existsSync(candidate))
85
+ return path.join(dir, 'scripts');
86
+ const parent = path.dirname(dir);
87
+ if (parent === dir)
88
+ break;
89
+ dir = parent;
90
+ }
91
+ // Default to monorepo root guess (4 levels up from packages/cli/dist/commands)
92
+ return path.resolve(startDir, '..', '..', '..', '..', 'scripts');
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Package-manager detection
96
+ // ---------------------------------------------------------------------------
97
+ function detectPackageManager(repoRoot) {
98
+ if (fs.existsSync(path.join(repoRoot, 'pnpm-lock.yaml')))
99
+ return 'pnpm';
100
+ if (fs.existsSync(path.join(repoRoot, 'yarn.lock')))
101
+ return 'yarn';
102
+ return 'npm';
103
+ }
104
+ function packageInstallArgs(pm) {
105
+ if (pm === 'pnpm')
106
+ return ['add', '@gurulu/tracker-web'];
107
+ if (pm === 'yarn')
108
+ return ['add', '@gurulu/tracker-web'];
109
+ return ['install', '@gurulu/tracker-web'];
110
+ }
111
+ function mergeEnvFile(repoRoot, framework, vars) {
112
+ const isNext = framework.startsWith('nextjs') || framework === 'nextjs';
113
+ const filename = isNext ? '.env.local' : '.env';
114
+ const filePath = path.join(repoRoot, filename);
115
+ let existing = '';
116
+ if (fs.existsSync(filePath)) {
117
+ existing = fs.readFileSync(filePath, 'utf8');
118
+ }
119
+ const existingKeys = new Set(existing
120
+ .split('\n')
121
+ .map((l) => l.trim())
122
+ .filter((l) => l && !l.startsWith('#'))
123
+ .map((l) => l.split('=')[0].trim()));
124
+ const added = [];
125
+ const skipped = [];
126
+ const linesToAppend = [];
127
+ for (const [key, value] of Object.entries(vars)) {
128
+ if (existingKeys.has(key)) {
129
+ skipped.push(key);
130
+ continue;
131
+ }
132
+ linesToAppend.push(`${key}=${value}`);
133
+ added.push(key);
134
+ }
135
+ if (linesToAppend.length > 0) {
136
+ const separator = existing && !existing.endsWith('\n') ? '\n' : '';
137
+ const header = existing ? '' : '# Gurulu.io Analytics\n';
138
+ fs.appendFileSync(filePath, `${separator}${header}${linesToAppend.join('\n')}\n`);
139
+ }
140
+ return { file: filename, added, skipped };
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // Default dependency implementations (real spawn + real fetch)
144
+ // ---------------------------------------------------------------------------
145
+ function createDefaultDeps(scriptsDir) {
146
+ const runNode = (args, opts = {}) => new Promise((resolve) => {
147
+ const child = (0, child_process_1.spawn)('node', args, {
148
+ cwd: opts.cwd,
149
+ stdio: ['pipe', 'pipe', 'pipe'],
150
+ });
151
+ let stdout = '';
152
+ let stderr = '';
153
+ child.stdout.on('data', (c) => (stdout += c.toString()));
154
+ child.stderr.on('data', (c) => (stderr += c.toString()));
155
+ child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
156
+ if (opts.input) {
157
+ child.stdin.write(opts.input);
158
+ child.stdin.end();
159
+ }
160
+ });
161
+ const runCmd = (cmd, args, opts = {}) => new Promise((resolve) => {
162
+ const child = (0, child_process_1.spawn)(cmd, args, {
163
+ cwd: opts.cwd,
164
+ stdio: ['ignore', 'pipe', 'pipe'],
165
+ shell: process.platform === 'win32',
166
+ });
167
+ let stdout = '';
168
+ let stderr = '';
169
+ child.stdout.on('data', (c) => (stdout += c.toString()));
170
+ child.stderr.on('data', (c) => (stderr += c.toString()));
171
+ child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
172
+ child.on('error', (err) => resolve({ code: 1, stdout, stderr: String(err) }));
173
+ });
174
+ const fetchJson = async (url, body) => {
175
+ try {
176
+ // Node 18+ has global fetch
177
+ const res = await globalThis.fetch(url, {
178
+ method: 'POST',
179
+ headers: { 'content-type': 'application/json' },
180
+ body: JSON.stringify(body),
181
+ });
182
+ return { ok: !!res.ok, status: res.status };
183
+ }
184
+ catch {
185
+ return { ok: false, status: 0 };
186
+ }
187
+ };
188
+ const verify = async (params) => {
189
+ const verifierScript = path.join(params.scriptsDir, 'gurulu-verify-install.mjs');
190
+ const args = [
191
+ verifierScript,
192
+ params.repoRoot,
193
+ '--framework',
194
+ params.framework,
195
+ '--site-id',
196
+ params.siteId,
197
+ '--tenant-id',
198
+ params.tenantId,
199
+ '--ingest-url',
200
+ params.ingestUrl,
201
+ ];
202
+ const res = await runNode(args);
203
+ // Parse the final VERIFY_RESULT <json> line if present.
204
+ const lines = (res.stdout || '').trim().split('\n');
205
+ let parsed = { ok: res.code === 0, reason: null };
206
+ for (let i = lines.length - 1; i >= 0; i--) {
207
+ const line = lines[i];
208
+ if (line.startsWith('VERIFY_RESULT ')) {
209
+ try {
210
+ parsed = JSON.parse(line.slice('VERIFY_RESULT '.length));
211
+ }
212
+ catch {
213
+ // fall through
214
+ }
215
+ break;
216
+ }
217
+ }
218
+ if (res.code !== 0 && parsed.ok)
219
+ parsed.ok = false;
220
+ return parsed;
221
+ };
222
+ return {
223
+ runNode,
224
+ runCmd,
225
+ fetchJson,
226
+ prompt: ui_1.prompt,
227
+ verify,
228
+ log: { info: ui_1.info, warn: ui_1.warn, error: ui_1.error, step: ui_1.step, success: ui_1.success },
229
+ };
230
+ }
231
+ // ---------------------------------------------------------------------------
232
+ // Orchestration
233
+ // ---------------------------------------------------------------------------
234
+ function log(deps, level, msg) {
235
+ const l = deps.log;
236
+ if (!l)
237
+ return;
238
+ l[level](msg);
239
+ }
240
+ async function runInstallFlow(args, deps, scriptsDir) {
241
+ const repoRoot = path.resolve(args.path || process.cwd());
242
+ const summary = {
243
+ scan: null,
244
+ framework: null,
245
+ planDiff: '',
246
+ filesChanged: 0,
247
+ packageManager: null,
248
+ packageInstalled: false,
249
+ envKeysWritten: [],
250
+ ingestOk: null,
251
+ dryRun: !!args.dryRun,
252
+ verify: null,
253
+ rolledBack: false,
254
+ errors: [],
255
+ };
256
+ if (!args.siteId) {
257
+ const msg = 'Missing --site-id. Provide --site-id or run without --yes to enter interactively.';
258
+ summary.errors.push(msg);
259
+ log(deps, 'error', msg);
260
+ return summary;
261
+ }
262
+ if (!args.tenantId) {
263
+ const msg = 'Missing --tenant-id. Provide --tenant-id or run without --yes to enter interactively.';
264
+ summary.errors.push(msg);
265
+ log(deps, 'error', msg);
266
+ return summary;
267
+ }
268
+ const scanScript = path.join(scriptsDir, 'gurulu-scan.mjs');
269
+ const agenticScript = path.join(scriptsDir, 'gurulu-agentic-install.mjs');
270
+ // ---- 1. Scan ---------------------------------------------------------
271
+ log(deps, 'step', `Scanning ${(0, ui_1.cyan)(repoRoot)}...`);
272
+ const scanRes = await deps.runNode([scanScript, repoRoot, '--quiet']);
273
+ if (scanRes.code !== 0) {
274
+ const msg = `Scan failed (exit ${scanRes.code}): ${scanRes.stderr.trim()}`;
275
+ summary.errors.push(msg);
276
+ log(deps, 'error', msg);
277
+ return summary;
278
+ }
279
+ try {
280
+ summary.scan = JSON.parse(scanRes.stdout);
281
+ }
282
+ catch (err) {
283
+ const msg = `Scan produced invalid JSON: ${err.message}`;
284
+ summary.errors.push(msg);
285
+ log(deps, 'error', msg);
286
+ return summary;
287
+ }
288
+ const detectedFw = args.framework || summary.scan.framework?.name || 'auto';
289
+ summary.framework = detectedFw;
290
+ log(deps, 'info', `Framework: ${(0, ui_1.bold)(detectedFw)} · ORM: ${summary.scan.orm?.name || 'none'} · Auth: ${summary.scan.auth?.name || 'none'} · Routes: ${summary.scan.routes?.length ?? 0}`);
291
+ // ---- 2. Confirm ------------------------------------------------------
292
+ if (!args.yes) {
293
+ const answer = await deps.prompt(' Continue with install? (Y/n): ');
294
+ if (answer && answer.toLowerCase().startsWith('n')) {
295
+ log(deps, 'info', 'Aborted by user.');
296
+ return summary;
297
+ }
298
+ }
299
+ // ---- 3. Agentic dry-run (plan / diff) --------------------------------
300
+ log(deps, 'step', 'Generating patch plan (dry-run)...');
301
+ const planArgs = [
302
+ agenticScript,
303
+ repoRoot,
304
+ '--dry-run',
305
+ '--site-id',
306
+ args.siteId,
307
+ '--tenant-id',
308
+ args.tenantId,
309
+ ];
310
+ if (args.framework) {
311
+ planArgs.push('--framework', args.framework);
312
+ }
313
+ // Phase 18.7 B — include auto-instrument diff in the dry-run plan so the
314
+ // consent prompt / preview shows both script-tag + track-call changes.
315
+ if (args.autoInstrument) {
316
+ planArgs.push('--auto-instrument');
317
+ if (args.autoProperties) {
318
+ planArgs.push('--auto-properties');
319
+ }
320
+ if (args.intentResultPath) {
321
+ planArgs.push('--intent-result', args.intentResultPath);
322
+ }
323
+ }
324
+ const planRes = await deps.runNode(planArgs);
325
+ summary.planDiff = planRes.stdout;
326
+ if (planRes.code !== 0) {
327
+ const msg = `Planner failed (exit ${planRes.code}): ${planRes.stderr.trim()}`;
328
+ summary.errors.push(msg);
329
+ log(deps, 'error', msg);
330
+ return summary;
331
+ }
332
+ if (planRes.stdout.trim()) {
333
+ log(deps, 'info', 'Proposed changes:');
334
+ for (const line of planRes.stdout.split('\n').slice(0, 40)) {
335
+ if (line)
336
+ log(deps, 'info', ` ${(0, ui_1.dim)(line)}`);
337
+ }
338
+ }
339
+ // crude file-count: `+++ b/<path>` lines in unified diff
340
+ summary.filesChanged = (planRes.stdout.match(/^\+\+\+ /gm) || []).length;
341
+ // ---- 4. Apply (skipped on --dry-run) ---------------------------------
342
+ if (!args.dryRun) {
343
+ log(deps, 'step', 'Applying patches...');
344
+ const applyArgs = [
345
+ agenticScript,
346
+ repoRoot,
347
+ '--apply',
348
+ '--site-id',
349
+ args.siteId,
350
+ '--tenant-id',
351
+ args.tenantId,
352
+ ];
353
+ if (args.framework) {
354
+ applyArgs.push('--framework', args.framework);
355
+ }
356
+ // Phase 18.7 B — forward auto-instrument flag + intent result path.
357
+ if (args.autoInstrument) {
358
+ applyArgs.push('--auto-instrument');
359
+ if (args.autoProperties) {
360
+ applyArgs.push('--auto-properties');
361
+ }
362
+ if (args.intentResultPath) {
363
+ applyArgs.push('--intent-result', args.intentResultPath);
364
+ }
365
+ }
366
+ const applyRes = await deps.runNode(applyArgs);
367
+ if (applyRes.code !== 0) {
368
+ const msg = `Apply failed (exit ${applyRes.code}): ${applyRes.stderr.trim()}`;
369
+ summary.errors.push(msg);
370
+ log(deps, 'error', msg);
371
+ return summary;
372
+ }
373
+ // Parse the machine-readable auto-instrument result line if present.
374
+ if (args.autoInstrument) {
375
+ const lines = (applyRes.stdout || '').split('\n');
376
+ for (let i = lines.length - 1; i >= 0; i--) {
377
+ if (lines[i].startsWith('AUTO_INSTRUMENT_RESULT ')) {
378
+ try {
379
+ const parsed = JSON.parse(lines[i].slice('AUTO_INSTRUMENT_RESULT '.length));
380
+ summary.instrumentation = {
381
+ enabled: true,
382
+ filesModified: parsed.filesModified || 0,
383
+ eventsInstrumented: parsed.eventsInstrumented || 0,
384
+ eventsSkipped: parsed.eventsSkipped || 0,
385
+ notes: parsed.notes || [],
386
+ };
387
+ }
388
+ catch {
389
+ // fall through — telemetry will show enabled:true with zeros.
390
+ }
391
+ break;
392
+ }
393
+ }
394
+ if (!summary.instrumentation) {
395
+ summary.instrumentation = {
396
+ enabled: true,
397
+ filesModified: 0,
398
+ eventsInstrumented: 0,
399
+ eventsSkipped: 0,
400
+ };
401
+ }
402
+ }
403
+ log(deps, 'success', 'Patches applied.');
404
+ }
405
+ else {
406
+ log(deps, 'info', 'Dry-run: skipping apply, npm install, env merge, and ingest test.');
407
+ }
408
+ // ---- 5. npm install --------------------------------------------------
409
+ const pm = detectPackageManager(repoRoot);
410
+ summary.packageManager = pm;
411
+ if (args.skipNpm || args.dryRun) {
412
+ log(deps, 'info', `Skipping package install (${pm} add @gurulu/tracker-web)`);
413
+ }
414
+ else {
415
+ log(deps, 'step', `Installing @gurulu/tracker-web with ${(0, ui_1.bold)(pm)}...`);
416
+ const pmRes = await deps.runCmd(pm, packageInstallArgs(pm), { cwd: repoRoot });
417
+ if (pmRes.code !== 0) {
418
+ const msg = `${pm} install failed (exit ${pmRes.code}): ${pmRes.stderr.trim()}`;
419
+ summary.errors.push(msg);
420
+ log(deps, 'warn', msg);
421
+ }
422
+ else {
423
+ summary.packageInstalled = true;
424
+ log(deps, 'success', '@gurulu/tracker-web installed.');
425
+ }
426
+ }
427
+ // ---- 6. .env merge ---------------------------------------------------
428
+ const ingestUrl = args.ingestUrl || process.env.GURULU_INGEST_URL || 'https://api.gurulu.io';
429
+ const envVars = {
430
+ NEXT_PUBLIC_GURULU_SITE_ID: args.siteId,
431
+ NEXT_PUBLIC_GURULU_TENANT_ID: args.tenantId,
432
+ NEXT_PUBLIC_GURULU_INGEST_URL: ingestUrl,
433
+ };
434
+ if (args.skipEnv || args.dryRun) {
435
+ log(deps, 'info', 'Skipping .env merge.');
436
+ }
437
+ else {
438
+ const mergeRes = mergeEnvFile(repoRoot, detectedFw, envVars);
439
+ summary.envKeysWritten = mergeRes.added;
440
+ if (mergeRes.added.length > 0) {
441
+ log(deps, 'success', `Wrote ${mergeRes.added.length} key(s) to ${mergeRes.file}`);
442
+ }
443
+ if (mergeRes.skipped.length > 0) {
444
+ log(deps, 'warn', `Skipped existing key(s): ${mergeRes.skipped.join(', ')}`);
445
+ }
446
+ }
447
+ // ---- 7. Ingest ping --------------------------------------------------
448
+ if (!args.dryRun) {
449
+ const pingUrl = `${ingestUrl.replace(/\/$/, '')}/api/ingest/v1/health`;
450
+ log(deps, 'step', `Pinging ingest (${pingUrl})...`);
451
+ const pingRes = await deps.fetchJson(pingUrl, {
452
+ siteId: args.siteId,
453
+ tenantId: args.tenantId,
454
+ source: 'gurulu-cli-install',
455
+ });
456
+ summary.ingestOk = pingRes.ok;
457
+ if (pingRes.ok) {
458
+ log(deps, 'success', `Ingest reachable (status ${pingRes.status}).`);
459
+ }
460
+ else {
461
+ log(deps, 'warn', `Ingest ping failed (status ${pingRes.status}). Verify GURULU_INGEST_URL.`);
462
+ }
463
+ }
464
+ // ---- 8. Live smoke verify (Phase 16 A3) ------------------------------
465
+ if (args.verify && !args.dryRun) {
466
+ if (!deps.verify) {
467
+ log(deps, 'warn', 'Verify flag set but no verifier dep configured; skipping.');
468
+ }
469
+ else {
470
+ log(deps, 'step', 'Running live smoke verification...');
471
+ try {
472
+ const verifyRes = await deps.verify({
473
+ repoRoot,
474
+ framework: detectedFw,
475
+ siteId: args.siteId,
476
+ tenantId: args.tenantId,
477
+ ingestUrl,
478
+ scriptsDir,
479
+ });
480
+ summary.verify = verifyRes;
481
+ if (verifyRes.ok) {
482
+ log(deps, 'success', 'Install verified: $page_view captured.');
483
+ }
484
+ else {
485
+ const reason = verifyRes.reason || 'unknown';
486
+ const msg = `Verification failed: ${reason}. Rolling back patches...`;
487
+ summary.errors.push(msg);
488
+ log(deps, 'error', msg);
489
+ // Auto-rollback
490
+ const rollbackArgs = [agenticScript, repoRoot, '--rollback'];
491
+ const rbRes = await deps.runNode(rollbackArgs);
492
+ if (rbRes.code === 0) {
493
+ summary.rolledBack = true;
494
+ log(deps, 'warn', 'Patches rolled back.');
495
+ }
496
+ else {
497
+ log(deps, 'error', `Rollback failed (exit ${rbRes.code}): ${rbRes.stderr.trim()}`);
498
+ }
499
+ }
500
+ }
501
+ catch (err) {
502
+ const msg = `Verify threw: ${err.message}`;
503
+ summary.errors.push(msg);
504
+ log(deps, 'error', msg);
505
+ }
506
+ }
507
+ }
508
+ // ---- Final summary ---------------------------------------------------
509
+ log(deps, 'info', '');
510
+ log(deps, 'info', (0, ui_1.bold)(' Install summary'));
511
+ log(deps, 'step', `Framework: ${summary.framework}`);
512
+ log(deps, 'step', `Files changed: ${summary.filesChanged}`);
513
+ log(deps, 'step', `Package manager: ${summary.packageManager}`);
514
+ log(deps, 'step', `Package installed: ${summary.packageInstalled ? 'yes' : 'no'}`);
515
+ log(deps, 'step', `.env keys written: ${summary.envKeysWritten.join(', ') || 'none'}`);
516
+ log(deps, 'step', `Ingest test: ${summary.ingestOk === null ? 'skipped' : summary.ingestOk ? 'ok' : 'failed'}`);
517
+ if (summary.verify) {
518
+ log(deps, 'step', `Live verify: ${summary.verify.ok ? 'verified' : `failed (${summary.verify.reason || 'unknown'})`}`);
519
+ if (summary.rolledBack) {
520
+ log(deps, 'step', 'Patches: rolled back');
521
+ }
522
+ }
523
+ if (summary.instrumentation && summary.instrumentation.enabled) {
524
+ log(deps, 'step', `Auto-instrument: ${summary.instrumentation.filesModified} file(s), ` +
525
+ `${summary.instrumentation.eventsInstrumented} event(s), ` +
526
+ `${summary.instrumentation.eventsSkipped} skipped`);
527
+ }
528
+ log(deps, 'info', '');
529
+ return summary;
530
+ }
531
+ function repoHashOf(repoRoot, remoteUrl = null) {
532
+ const h = crypto.createHash('sha256');
533
+ h.update(path.resolve(repoRoot));
534
+ if (remoteUrl)
535
+ h.update('\n' + remoteUrl);
536
+ return h.digest('hex');
537
+ }
538
+ function pickSite(sites, target) {
539
+ if (!target)
540
+ return sites.length === 1 ? sites[0] : null;
541
+ return (sites.find((s) => s.id === target) ||
542
+ sites.find((s) => s.name === target) ||
543
+ sites.find((s) => s.id.startsWith(target)) ||
544
+ null);
545
+ }
546
+ /** Send a telemetry POST. Never throws — quota errors are surfaced via `ok`. */
547
+ async function emitInstallTelemetry(deps, payload) {
548
+ try {
549
+ const res = await deps.postVerify({
550
+ source: 'cli',
551
+ ...payload,
552
+ });
553
+ if (res.status === 402)
554
+ return { ok: false, quotaExceeded: true };
555
+ return { ok: !!res.ok, quotaExceeded: false };
556
+ }
557
+ catch {
558
+ return { ok: false, quotaExceeded: false };
559
+ }
560
+ }
561
+ async function runAuthenticatedInstallFlow(args, authDeps, installDeps, scriptsDir) {
562
+ // Site selection
563
+ let selected = pickSite(authDeps.sites, args.site);
564
+ if (!selected) {
565
+ if (authDeps.sites.length === 0) {
566
+ const msg = 'No sites found. Run `gurulu sites create --domain ...` first.';
567
+ installDeps.log?.error(msg);
568
+ return {
569
+ scan: null,
570
+ framework: null,
571
+ planDiff: '',
572
+ filesChanged: 0,
573
+ packageManager: null,
574
+ packageInstalled: false,
575
+ envKeysWritten: [],
576
+ ingestOk: null,
577
+ dryRun: !!args.dryRun,
578
+ verify: null,
579
+ rolledBack: false,
580
+ errors: [msg],
581
+ };
582
+ }
583
+ if (args.yes) {
584
+ // Non-interactive, multiple sites: pick the first.
585
+ selected = authDeps.sites[0];
586
+ }
587
+ else if (authDeps.promptSelect) {
588
+ const labels = authDeps.sites.map((s) => `${s.name || s.id} (${s.domain})`);
589
+ const idx = await authDeps.promptSelect(' Select site: ', labels);
590
+ selected = authDeps.sites[idx] || authDeps.sites[0];
591
+ }
592
+ else {
593
+ selected = authDeps.sites[0];
594
+ }
595
+ }
596
+ const filled = {
597
+ ...args,
598
+ siteId: selected.id,
599
+ tenantId: authDeps.tenantId,
600
+ };
601
+ // ---- Phase 18.6 — Intent discovery (pre-scan pass) ------------------
602
+ // Runs BEFORE the core flow so the user sees the proposal first. We use
603
+ // the scanner in --quiet mode, feed signals to the injected analyzer,
604
+ // and render a proposal. On --skip-intent or missing deps we fall
605
+ // straight through to the Phase 18.5 authenticated flow.
606
+ const intentRecord = {
607
+ skipped: !!args.skipIntent,
608
+ dryRun: !!args.intentDryRun,
609
+ analyzed: false,
610
+ preSeeded: false,
611
+ };
612
+ if (!args.skipIntent && authDeps.intent) {
613
+ const repoRoot = path.resolve(args.path || process.cwd());
614
+ const scanScript = path.join(scriptsDir, 'gurulu-scan.mjs');
615
+ installDeps.log?.step('Scanning for install-time intent signals...');
616
+ const scanRes = await installDeps.runNode([scanScript, repoRoot, '--quiet']);
617
+ if (scanRes.code !== 0) {
618
+ installDeps.log?.warn(`Intent scan failed (exit ${scanRes.code}); skipping intent discovery.`);
619
+ intentRecord.error = `scan_failed:${scanRes.code}`;
620
+ }
621
+ else {
622
+ let signals = null;
623
+ try {
624
+ signals = JSON.parse(scanRes.stdout);
625
+ }
626
+ catch (err) {
627
+ installDeps.log?.warn(`Intent scan produced invalid JSON: ${err.message}; skipping.`);
628
+ intentRecord.error = 'scan_invalid_json';
629
+ }
630
+ if (signals) {
631
+ let intent = null;
632
+ try {
633
+ intent = await authDeps.intent.analyze(signals);
634
+ intentRecord.analyzed = true;
635
+ intentRecord.vertical = intent.vertical;
636
+ }
637
+ catch (err) {
638
+ installDeps.log?.warn(`Intent analyzer failed: ${err.message}`);
639
+ intentRecord.error = `analyze_failed:${err.message}`;
640
+ }
641
+ if (intent) {
642
+ const io = authDeps.intent.io || {
643
+ print: (l) => console.log(l),
644
+ prompt: installDeps.prompt,
645
+ isPiped: !process.stdout.isTTY,
646
+ };
647
+ const nonInteractive = !!args.yes || io.isPiped || !!args.intentDryRun;
648
+ const decision = await (0, install_intent_proposal_1.runProposal)({ intent, io, nonInteractive });
649
+ intentRecord.accepted = {
650
+ events: decision.accepted.events.length,
651
+ funnels: decision.accepted.funnels.length,
652
+ };
653
+ // Phase 18.7 B — stash the raw ProposedEvent list for the
654
+ // auto-instrument bridge below. Not part of the persisted summary
655
+ // shape — marked with an underscore so TypeScript narrowing sees
656
+ // the public `accepted` counts only.
657
+ intentRecord._acceptedEvents = decision.accepted.events;
658
+ if (decision.quit) {
659
+ installDeps.log?.info('Intent proposal dismissed by user.');
660
+ }
661
+ else if (args.intentDryRun) {
662
+ installDeps.log?.info(`Intent dry-run: would pre-seed ${decision.accepted.events.length} events and ${decision.accepted.funnels.length} funnels.`);
663
+ }
664
+ else if (decision.accepted.events.length === 0 && decision.accepted.funnels.length === 0) {
665
+ installDeps.log?.info('No events or funnels accepted; skipping pre-seed.');
666
+ }
667
+ else {
668
+ const res = await authDeps.intent.preSeed({
669
+ siteId: selected.id,
670
+ intent,
671
+ accepted: decision.accepted,
672
+ rejected: decision.rejected,
673
+ });
674
+ if (res.quotaExceeded) {
675
+ const msg = 'Install quota exceeded during pre-seed. Upgrade at https://gurulu.io/settings/billing';
676
+ installDeps.log?.error(msg);
677
+ intentRecord.error = 'quota_exceeded';
678
+ }
679
+ else if (!res.ok) {
680
+ installDeps.log?.warn(`Pre-seed failed: ${res.error || 'unknown error'}`);
681
+ intentRecord.error = res.error || 'pre_seed_failed';
682
+ }
683
+ else {
684
+ intentRecord.preSeeded = true;
685
+ intentRecord.proposalId = res.proposalId;
686
+ intentRecord.created = res.created;
687
+ installDeps.log?.success(`Pre-seeded ${res.created?.goals || 0} goals, ${res.created?.funnels || 0} funnels, ${res.created?.milestones || 0} milestones.`);
688
+ }
689
+ }
690
+ }
691
+ }
692
+ }
693
+ }
694
+ else if (args.skipIntent) {
695
+ installDeps.log?.info('Intent discovery skipped (--skip-intent).');
696
+ }
697
+ // ---- Phase 18.7 B — Auto-instrumentation consent + intent-result file
698
+ // When --auto-instrument is set AND an intent with accepted events exists,
699
+ // serialize the decision to a tmp file so the agentic-install script can
700
+ // read it during --apply. We also show a prominent consent prompt listing
701
+ // the route files that will be touched (skippable with --yes).
702
+ let autoInstrumentEnabled = !!args.autoInstrument;
703
+ let intentResultPath;
704
+ if (autoInstrumentEnabled) {
705
+ if (!intentRecord.analyzed || !intentRecord.accepted || intentRecord.accepted.events === 0) {
706
+ installDeps.log?.warn('Auto-instrument requested but no accepted events from intent discovery; disabling.');
707
+ autoInstrumentEnabled = false;
708
+ }
709
+ else {
710
+ // Persist an `{ accepted: { events } }` envelope matching the
711
+ // agentic-install script's reader shape. We resolve the raw ProposedEvent
712
+ // list from the proposal IO if available — otherwise we fall back to
713
+ // writing an empty envelope and rely on scan-driven route detection.
714
+ const tmpDir = process.env.TMPDIR || '/tmp';
715
+ const uuid = crypto.randomBytes(8).toString('hex');
716
+ intentResultPath = path.join(tmpDir, `gurulu-intent-${uuid}.json`);
717
+ try {
718
+ fs.writeFileSync(intentResultPath, JSON.stringify({
719
+ accepted: {
720
+ events: intentRecord._acceptedEvents || [],
721
+ },
722
+ }, null, 2), 'utf8');
723
+ }
724
+ catch (err) {
725
+ installDeps.log?.warn(`Failed to persist intent result: ${err.message}; disabling auto-instrument.`);
726
+ autoInstrumentEnabled = false;
727
+ intentResultPath = undefined;
728
+ }
729
+ // Consent prompt — unless --yes or --dry-run.
730
+ if (autoInstrumentEnabled && !args.yes && !args.dryRun) {
731
+ installDeps.log?.warn(`Auto-instrumentation will modify ${intentRecord.accepted.events} route file(s) to insert gurulu.track() calls.`);
732
+ installDeps.log?.info(' Changes are backed up to .gurulu-backup/ and can be rolled back with `gurulu install --rollback`.');
733
+ const answer = await installDeps.prompt(' Proceed with auto-instrumentation? [y/N]: ');
734
+ if (!answer || !/^y/i.test(answer.trim())) {
735
+ installDeps.log?.info('Auto-instrumentation declined.');
736
+ autoInstrumentEnabled = false;
737
+ intentResultPath = undefined;
738
+ }
739
+ }
740
+ }
741
+ }
742
+ const runArgs = {
743
+ ...filled,
744
+ autoInstrument: autoInstrumentEnabled,
745
+ intentResultPath,
746
+ };
747
+ // Run the core flow.
748
+ const summary = await runInstallFlow(runArgs, installDeps, scriptsDir);
749
+ summary.intent = intentRecord;
750
+ if (autoInstrumentEnabled && !summary.instrumentation) {
751
+ summary.instrumentation = {
752
+ enabled: true,
753
+ filesModified: 0,
754
+ eventsInstrumented: 0,
755
+ eventsSkipped: 0,
756
+ };
757
+ }
758
+ // Telemetry after each terminal state.
759
+ const cliVersion = (() => {
760
+ try {
761
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
762
+ return require('../../package.json').version || '0.0.0';
763
+ }
764
+ catch {
765
+ return '0.0.0';
766
+ }
767
+ })();
768
+ const rh = repoHashOf(path.resolve(args.path || process.cwd()));
769
+ const fw = summary.framework || 'unknown';
770
+ if (args.dryRun) {
771
+ await emitInstallTelemetry(authDeps, {
772
+ siteId: selected.id,
773
+ status: 'dry-run',
774
+ framework: fw,
775
+ cliVersion,
776
+ repoHash: rh,
777
+ });
778
+ }
779
+ else if (summary.errors.length > 0) {
780
+ await emitInstallTelemetry(authDeps, {
781
+ siteId: selected.id,
782
+ status: 'failed',
783
+ framework: fw,
784
+ cliVersion,
785
+ repoHash: rh,
786
+ errorMessage: summary.errors[0] || null,
787
+ });
788
+ }
789
+ else {
790
+ const telemetry = await emitInstallTelemetry(authDeps, {
791
+ siteId: selected.id,
792
+ status: summary.verify?.ok ? 'verified' : 'applied',
793
+ framework: fw,
794
+ cliVersion,
795
+ repoHash: rh,
796
+ ...(summary.instrumentation
797
+ ? {
798
+ instrumentation: {
799
+ filesModified: summary.instrumentation.filesModified,
800
+ eventsInstrumented: summary.instrumentation.eventsInstrumented,
801
+ eventsSkipped: summary.instrumentation.eventsSkipped,
802
+ },
803
+ }
804
+ : {}),
805
+ });
806
+ if (telemetry.quotaExceeded) {
807
+ const msg = 'Install quota exceeded for the current plan. Upgrade at https://gurulu.io/settings/billing';
808
+ installDeps.log?.error(msg);
809
+ summary.errors.push(msg);
810
+ }
811
+ if (summary.rolledBack) {
812
+ await emitInstallTelemetry(authDeps, {
813
+ siteId: selected.id,
814
+ status: 'rolled-back',
815
+ framework: fw,
816
+ cliVersion,
817
+ repoHash: rh,
818
+ });
819
+ }
820
+ }
821
+ return summary;
822
+ }
823
+ async function installCommand(args) {
824
+ const scriptsDir = resolveScriptsDir();
825
+ const deps = createDefaultDeps(scriptsDir);
826
+ // Legacy unauthenticated path: --site-id passed explicitly and no active profile.
827
+ const profile = await (0, config_1.getActiveProfile)({ profile: args.profile });
828
+ const legacyMode = !!args.siteId && !profile;
829
+ if (legacyMode) {
830
+ (0, ui_1.warn)('Legacy unauthenticated install. Run `gurulu login` for the full experience.');
831
+ let siteId = args.siteId || process.env.GURULU_SITE_ID;
832
+ let tenantId = args.tenantId || process.env.GURULU_TENANT_ID;
833
+ if (!args.yes) {
834
+ if (!siteId)
835
+ siteId = await deps.prompt(' Site ID: ');
836
+ if (!tenantId)
837
+ tenantId = await deps.prompt(' Tenant ID: ');
838
+ }
839
+ const filled = { ...args, siteId, tenantId };
840
+ const summary = await runInstallFlow(filled, deps, scriptsDir);
841
+ if (summary.errors.length > 0)
842
+ process.exit(1);
843
+ return;
844
+ }
845
+ if (!profile) {
846
+ (0, ui_1.error)('Not logged in. Run `gurulu login` first, or provide --site-id for legacy mode.');
847
+ process.exit(1);
848
+ }
849
+ // Authenticated: fetch /me, pick site, run flow, emit telemetry.
850
+ let me;
851
+ try {
852
+ const res = await (0, api_client_1.cliApi)('/api/cli/me', { preloadedProfile: profile });
853
+ me = await res.json();
854
+ }
855
+ catch (err) {
856
+ (0, ui_1.error)(`Failed to load account: ${err.message}`);
857
+ process.exit(1);
858
+ }
859
+ const sites = (me.sites || []).map((s) => ({
860
+ id: s.id,
861
+ name: s.name,
862
+ domain: s.domain,
863
+ publishableKey: s.publishableKey,
864
+ }));
865
+ const authDeps = {
866
+ profile,
867
+ sites,
868
+ tenantId: me.tenant?.id || '',
869
+ postVerify: async (body) => {
870
+ const res = await (0, api_client_1.cliApi)('/api/cli/install/verify', {
871
+ method: 'POST',
872
+ preloadedProfile: profile,
873
+ body: JSON.stringify(body),
874
+ noExitOnError: true,
875
+ });
876
+ return { ok: res.ok, status: res.status };
877
+ },
878
+ intent: {
879
+ analyze: async (signals) => {
880
+ const res = await (0, api_client_1.cliApi)('/api/cli/install/analyze-intent', {
881
+ method: 'POST',
882
+ preloadedProfile: profile,
883
+ body: JSON.stringify({ signals }),
884
+ noExitOnError: true,
885
+ });
886
+ const text = await res.text();
887
+ const parsed = text ? JSON.parse(text) : {};
888
+ if (!res.ok) {
889
+ throw new Error((parsed && parsed.message) || `HTTP ${res.status}`);
890
+ }
891
+ return parsed.intent;
892
+ },
893
+ preSeed: async (body) => {
894
+ const res = await (0, api_client_1.cliApi)('/api/cli/install/pre-seed', {
895
+ method: 'POST',
896
+ preloadedProfile: profile,
897
+ body: JSON.stringify(body),
898
+ noExitOnError: true,
899
+ });
900
+ if (res.status === 402 || res.status === 429) {
901
+ return { ok: false, quotaExceeded: true };
902
+ }
903
+ const text = await res.text();
904
+ const parsed = text ? JSON.parse(text) : {};
905
+ if (!res.ok) {
906
+ return { ok: false, error: (parsed && parsed.message) || `HTTP ${res.status}` };
907
+ }
908
+ return {
909
+ ok: true,
910
+ proposalId: parsed.proposalId,
911
+ created: parsed.created,
912
+ skipped: parsed.skipped,
913
+ };
914
+ },
915
+ },
916
+ };
917
+ const summary = await runAuthenticatedInstallFlow(args, authDeps, deps, scriptsDir);
918
+ if (summary.errors.length > 0)
919
+ process.exit(1);
920
+ }