@gurulu/cli 0.4.7 → 1.0.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.
- package/LICENSE +92 -0
- package/README.md +35 -106
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +25410 -0
- package/dist/commands/auth.d.ts +23 -20
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts +20 -6
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/init.d.ts +25 -11
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/pull.d.ts +13 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/push.d.ts +40 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/validate.d.ts +36 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24985 -876
- package/dist/lib/api.d.ts +139 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/codegen.d.ts +4 -0
- package/dist/lib/codegen.d.ts.map +1 -0
- package/dist/lib/config.d.ts +43 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/package.json +40 -20
- package/bin/gurulu.js +0 -2
- package/dist/api-client.d.ts +0 -33
- package/dist/api-client.js +0 -175
- package/dist/commands/add-server.d.ts +0 -9
- package/dist/commands/add-server.js +0 -162
- package/dist/commands/alerts.d.ts +0 -27
- package/dist/commands/alerts.js +0 -309
- package/dist/commands/api-keys.d.ts +0 -20
- package/dist/commands/api-keys.js +0 -130
- package/dist/commands/attribution.d.ts +0 -22
- package/dist/commands/attribution.js +0 -111
- package/dist/commands/audiences.d.ts +0 -23
- package/dist/commands/audiences.js +0 -243
- package/dist/commands/audit.d.ts +0 -20
- package/dist/commands/audit.js +0 -130
- package/dist/commands/auth.js +0 -249
- package/dist/commands/chat.d.ts +0 -19
- package/dist/commands/chat.js +0 -118
- package/dist/commands/config.d.ts +0 -10
- package/dist/commands/config.js +0 -92
- package/dist/commands/consent.d.ts +0 -27
- package/dist/commands/consent.js +0 -233
- package/dist/commands/conversion-paths.d.ts +0 -19
- package/dist/commands/conversion-paths.js +0 -55
- package/dist/commands/db.d.ts +0 -25
- package/dist/commands/db.js +0 -330
- package/dist/commands/destinations.d.ts +0 -20
- package/dist/commands/destinations.js +0 -191
- package/dist/commands/doctor.js +0 -360
- package/dist/commands/errors.d.ts +0 -27
- package/dist/commands/errors.js +0 -121
- package/dist/commands/events.d.ts +0 -33
- package/dist/commands/events.js +0 -371
- package/dist/commands/experiments.d.ts +0 -22
- package/dist/commands/experiments.js +0 -264
- package/dist/commands/funnels.d.ts +0 -17
- package/dist/commands/funnels.js +0 -203
- package/dist/commands/goals.d.ts +0 -18
- package/dist/commands/goals.js +0 -214
- package/dist/commands/heatmap.d.ts +0 -27
- package/dist/commands/heatmap.js +0 -112
- package/dist/commands/identity.d.ts +0 -29
- package/dist/commands/identity.js +0 -328
- package/dist/commands/init.js +0 -215
- package/dist/commands/insights.d.ts +0 -10
- package/dist/commands/insights.js +0 -77
- package/dist/commands/install.d.ts +0 -259
- package/dist/commands/install.js +0 -1590
- package/dist/commands/login.d.ts +0 -20
- package/dist/commands/login.js +0 -170
- package/dist/commands/logout.d.ts +0 -10
- package/dist/commands/logout.js +0 -41
- package/dist/commands/playground.d.ts +0 -11
- package/dist/commands/playground.js +0 -47
- package/dist/commands/releases.d.ts +0 -17
- package/dist/commands/releases.js +0 -54
- package/dist/commands/replay.d.ts +0 -18
- package/dist/commands/replay.js +0 -64
- package/dist/commands/secrets.d.ts +0 -19
- package/dist/commands/secrets.js +0 -145
- package/dist/commands/setup.d.ts +0 -21
- package/dist/commands/setup.js +0 -67
- package/dist/commands/sites.d.ts +0 -18
- package/dist/commands/sites.js +0 -139
- package/dist/commands/skad.d.ts +0 -18
- package/dist/commands/skad.js +0 -53
- package/dist/commands/sourcemap.d.ts +0 -33
- package/dist/commands/sourcemap.js +0 -204
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -136
- package/dist/commands/upgrade.d.ts +0 -21
- package/dist/commands/upgrade.js +0 -183
- package/dist/commands/warehouse.d.ts +0 -20
- package/dist/commands/warehouse.js +0 -65
- package/dist/commands/warehouses.d.ts +0 -17
- package/dist/commands/warehouses.js +0 -182
- package/dist/commands/watch.d.ts +0 -45
- package/dist/commands/watch.js +0 -258
- package/dist/commands/whoami.d.ts +0 -9
- package/dist/commands/whoami.js +0 -50
- package/dist/config.d.ts +0 -75
- package/dist/config.js +0 -329
- package/dist/frameworks/detect.d.ts +0 -8
- package/dist/frameworks/detect.js +0 -458
- package/dist/install-intent-proposal.d.ts +0 -99
- package/dist/install-intent-proposal.js +0 -202
- package/dist/utils/api.d.ts +0 -20
- package/dist/utils/api.js +0 -47
- package/dist/utils/config.d.ts +0 -13
- package/dist/utils/config.js +0 -30
- package/dist/utils/confirm.d.ts +0 -17
- package/dist/utils/confirm.js +0 -40
- package/dist/utils/dry-run.d.ts +0 -20
- package/dist/utils/dry-run.js +0 -67
- package/dist/utils/from-file.d.ts +0 -9
- package/dist/utils/from-file.js +0 -72
- package/dist/utils/redact.d.ts +0 -14
- package/dist/utils/redact.js +0 -48
- package/dist/utils/ui.d.ts +0 -14
- package/dist/utils/ui.js +0 -59
- package/scripts/.gitkeep +0 -0
- package/scripts/README-gurulu-agentic-install.md +0 -114
- package/scripts/README-gurulu-scan.md +0 -98
- package/scripts/audit-cli-scopes.mjs +0 -204
- package/scripts/backfill-tenant-id.mjs +0 -172
- package/scripts/backfill-tenant-links.ts +0 -252
- package/scripts/backup-clickhouse.sh +0 -27
- package/scripts/backup-postgres.sh +0 -19
- package/scripts/bootstrap-runtime-schema.mjs +0 -87
- package/scripts/bootstrap-stripe.mjs +0 -158
- package/scripts/gurulu-agentic-install.lib.cjs +0 -762
- package/scripts/gurulu-agentic-install.mjs +0 -623
- package/scripts/gurulu-scan.lib.cjs +0 -1509
- package/scripts/gurulu-scan.mjs +0 -91
- package/scripts/gurulu-verify-install.lib.cjs +0 -334
- package/scripts/gurulu-verify-install.mjs +0 -59
- package/scripts/init-ssl.sh +0 -26
- package/scripts/migrate-flow-graph-enums.sh +0 -86
- package/scripts/monitor-disk.sh +0 -24
- package/scripts/patches/astro.patch.cjs +0 -74
- package/scripts/patches/auto-instrument/ast-helper.cjs +0 -480
- package/scripts/patches/auto-instrument/astro.cjs +0 -273
- package/scripts/patches/auto-instrument/express.cjs +0 -383
- package/scripts/patches/auto-instrument/fastify.cjs +0 -262
- package/scripts/patches/auto-instrument/hono.cjs +0 -392
- package/scripts/patches/auto-instrument/index.cjs +0 -80
- package/scripts/patches/auto-instrument/nestjs.cjs +0 -286
- package/scripts/patches/auto-instrument/nextjs-app-router.cjs +0 -345
- package/scripts/patches/auto-instrument/nextjs-pages.cjs +0 -361
- package/scripts/patches/auto-instrument/remix.cjs +0 -168
- package/scripts/patches/auto-instrument/sdk-helper-map.cjs +0 -241
- package/scripts/patches/auto-instrument/singleton-helper.cjs +0 -193
- package/scripts/patches/auto-instrument/sveltekit.cjs +0 -161
- package/scripts/patches/auto-instrument/vite-react.cjs +0 -37
- package/scripts/patches/auto-instrument/vue.cjs +0 -196
- package/scripts/patches/express.patch.cjs +0 -99
- package/scripts/patches/fastify.patch.cjs +0 -108
- package/scripts/patches/index.cjs +0 -300
- package/scripts/patches/nestjs.patch.cjs +0 -112
- package/scripts/patches/nextjs-app-router.patch.cjs +0 -97
- package/scripts/patches/nextjs-pages.patch.cjs +0 -97
- package/scripts/patches/remix.patch.cjs +0 -75
- package/scripts/patches/sveltekit.patch.cjs +0 -72
- package/scripts/patches/vite-react.patch.cjs +0 -73
- package/scripts/patches/vue.patch.cjs +0 -82
- package/scripts/renew-ssl.sh +0 -14
- package/scripts/resolve-migration.sh +0 -23
- package/scripts/seed-cli-dev-keys.mjs +0 -130
- package/scripts/seed-test-data.mjs +0 -391
- package/scripts/spike-browserless.ts +0 -65
- package/scripts/tenant-pivot-consistency-check.mjs +0 -205
- package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +0 -258
- package/scripts/tenant-pivot-phase-3-cleanup.mjs +0 -98
- package/scripts/test-identity-resolution.ts +0 -804
- package/scripts/validate-gurulu-schemas.mjs +0 -79
package/dist/commands/install.js
DELETED
|
@@ -1,1590 +0,0 @@
|
|
|
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.resolveWorkspaceRoot = resolveWorkspaceRoot;
|
|
52
|
-
exports.resolveScriptsDir = resolveScriptsDir;
|
|
53
|
-
exports.detectPackageManager = detectPackageManager;
|
|
54
|
-
exports.packageInstallArgs = packageInstallArgs;
|
|
55
|
-
exports.parseEnvFile = parseEnvFile;
|
|
56
|
-
exports.mergeEnvFile = mergeEnvFile;
|
|
57
|
-
exports.createDefaultDeps = createDefaultDeps;
|
|
58
|
-
exports.runInstallFlow = runInstallFlow;
|
|
59
|
-
exports.isTelemetryDisabled = isTelemetryDisabled;
|
|
60
|
-
exports.maybePromptTelemetryConsent = maybePromptTelemetryConsent;
|
|
61
|
-
exports.repoHashOf = repoHashOf;
|
|
62
|
-
exports.pickSite = pickSite;
|
|
63
|
-
exports.emitInstallTelemetry = emitInstallTelemetry;
|
|
64
|
-
exports.detectCiEnv = detectCiEnv;
|
|
65
|
-
exports.runAuthenticatedInstallFlow = runAuthenticatedInstallFlow;
|
|
66
|
-
exports.installCommand = installCommand;
|
|
67
|
-
const fs = __importStar(require("fs"));
|
|
68
|
-
const os = __importStar(require("os"));
|
|
69
|
-
const path = __importStar(require("path"));
|
|
70
|
-
const crypto = __importStar(require("crypto"));
|
|
71
|
-
const child_process_1 = require("child_process");
|
|
72
|
-
const ui_1 = require("../utils/ui");
|
|
73
|
-
const config_1 = require("../config");
|
|
74
|
-
const api_client_1 = require("../api-client");
|
|
75
|
-
const install_intent_proposal_1 = require("../install-intent-proposal");
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Sprint E1.5 — Workspace resolution.
|
|
78
|
-
//
|
|
79
|
-
// Many fresh repos are npm/pnpm/yarn workspaces (Turborepo, Nx, Bun, etc).
|
|
80
|
-
// Running `gurulu install` from the repo root is ambiguous: should we patch
|
|
81
|
-
// the `apps/web/` Next.js app, the `apps/admin/` dashboard, or one of the
|
|
82
|
-
// `packages/*` libraries? When package.json declares >=2 workspace globs we
|
|
83
|
-
// either (a) honour `--workspace=<path>` if set, or (b) prompt the user
|
|
84
|
-
// interactively. The resolved path becomes `repoRoot` for the rest of the
|
|
85
|
-
// install flow.
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
function walkForPackageJson(root, depth, out) {
|
|
88
|
-
// FA-1 P1-7 — recursive walker for `**/*` and `apps/**` style globs used by
|
|
89
|
-
// Turborepo and Yarn berry monorepos. We cap depth to avoid descending into
|
|
90
|
-
// node_modules / .git / huge build trees, and we only collect directories
|
|
91
|
-
// that actually contain a package.json.
|
|
92
|
-
if (depth < 0)
|
|
93
|
-
return;
|
|
94
|
-
let entries;
|
|
95
|
-
try {
|
|
96
|
-
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
for (const e of entries) {
|
|
102
|
-
if (!e.isDirectory())
|
|
103
|
-
continue;
|
|
104
|
-
if (e.name === 'node_modules' || e.name === '.git' || e.name.startsWith('.'))
|
|
105
|
-
continue;
|
|
106
|
-
const abs = path.join(root, e.name);
|
|
107
|
-
if (fs.existsSync(path.join(abs, 'package.json')))
|
|
108
|
-
out.push(abs);
|
|
109
|
-
walkForPackageJson(abs, depth - 1, out);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function expandWorkspaceGlobs(repoRoot, globs) {
|
|
113
|
-
// FA-1 P1-7 — workspace glob support now covers:
|
|
114
|
-
// - bare `dir` (single workspace)
|
|
115
|
-
// - `dir/*` (immediate children — npm/pnpm/yarn classic)
|
|
116
|
-
// - `dir/**`, `dir/**/*` (Turborepo / Yarn berry recursive)
|
|
117
|
-
// - `!packages/excluded` (negation filter, applied after expansion)
|
|
118
|
-
// - `workspace:*` protocol (Yarn berry — skipped, not a glob)
|
|
119
|
-
const positives = [];
|
|
120
|
-
const negatives = [];
|
|
121
|
-
for (const raw of globs) {
|
|
122
|
-
if (!raw || typeof raw !== 'string')
|
|
123
|
-
continue;
|
|
124
|
-
// Yarn berry sometimes lists `workspace:*` protocol entries — skip; they
|
|
125
|
-
// are dependency declarations, not workspace path globs.
|
|
126
|
-
if (raw.startsWith('workspace:'))
|
|
127
|
-
continue;
|
|
128
|
-
if (raw.startsWith('!')) {
|
|
129
|
-
negatives.push(raw.slice(1));
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
positives.push(raw);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const collected = new Set();
|
|
136
|
-
for (const g of positives) {
|
|
137
|
-
// `dir/**`, `dir/**/*` — recursive walk up to 4 levels deep.
|
|
138
|
-
if (g.endsWith('/**') || g.endsWith('/**/*') || g === '**' || g === '**/*') {
|
|
139
|
-
const root = g === '**' || g === '**/*'
|
|
140
|
-
? repoRoot
|
|
141
|
-
: path.join(repoRoot, g.replace(/\/\*\*(\/\*)?$/, ''));
|
|
142
|
-
if (!fs.existsSync(root))
|
|
143
|
-
continue;
|
|
144
|
-
const acc = [];
|
|
145
|
-
walkForPackageJson(root, 4, acc);
|
|
146
|
-
for (const a of acc)
|
|
147
|
-
collected.add(a);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (g.endsWith('/*')) {
|
|
151
|
-
const parent = path.join(repoRoot, g.slice(0, -2));
|
|
152
|
-
if (!fs.existsSync(parent))
|
|
153
|
-
continue;
|
|
154
|
-
try {
|
|
155
|
-
for (const entry of fs.readdirSync(parent)) {
|
|
156
|
-
const abs = path.join(parent, entry);
|
|
157
|
-
if (fs.statSync(abs).isDirectory() &&
|
|
158
|
-
fs.existsSync(path.join(abs, 'package.json'))) {
|
|
159
|
-
collected.add(abs);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
/* ignore */
|
|
165
|
-
}
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// Bare directory.
|
|
169
|
-
const abs = path.join(repoRoot, g);
|
|
170
|
-
if (fs.existsSync(abs) && fs.existsSync(path.join(abs, 'package.json'))) {
|
|
171
|
-
collected.add(abs);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
// Apply negations as simple suffix-match against the relative path.
|
|
175
|
-
if (negatives.length > 0) {
|
|
176
|
-
for (const dir of Array.from(collected)) {
|
|
177
|
-
const rel = path.relative(repoRoot, dir);
|
|
178
|
-
for (const n of negatives) {
|
|
179
|
-
const negResolved = n.replace(/\/\*+$/, '');
|
|
180
|
-
if (rel === negResolved || rel.startsWith(negResolved + path.sep)) {
|
|
181
|
-
collected.delete(dir);
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return Array.from(collected);
|
|
188
|
-
}
|
|
189
|
-
async function resolveWorkspaceRoot(repoRoot, args, deps) {
|
|
190
|
-
const pkgPath = path.join(repoRoot, 'package.json');
|
|
191
|
-
if (!fs.existsSync(pkgPath))
|
|
192
|
-
return repoRoot;
|
|
193
|
-
let pkg;
|
|
194
|
-
try {
|
|
195
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return repoRoot;
|
|
199
|
-
}
|
|
200
|
-
let globs = null;
|
|
201
|
-
if (Array.isArray(pkg.workspaces))
|
|
202
|
-
globs = pkg.workspaces;
|
|
203
|
-
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages))
|
|
204
|
-
globs = pkg.workspaces.packages;
|
|
205
|
-
if (!globs || globs.length === 0)
|
|
206
|
-
return repoRoot;
|
|
207
|
-
const candidates = expandWorkspaceGlobs(repoRoot, globs);
|
|
208
|
-
if (candidates.length <= 1)
|
|
209
|
-
return repoRoot;
|
|
210
|
-
if (args.workspace) {
|
|
211
|
-
const target = path.resolve(repoRoot, args.workspace);
|
|
212
|
-
if (candidates.some((c) => path.resolve(c) === target))
|
|
213
|
-
return target;
|
|
214
|
-
log(deps, 'warn', `--workspace=${args.workspace} not found among workspace packages.`);
|
|
215
|
-
}
|
|
216
|
-
if (args.yes) {
|
|
217
|
-
log(deps, 'warn', `Multiple workspaces detected (${candidates.length}); pass --workspace=<path> next time.`);
|
|
218
|
-
return repoRoot;
|
|
219
|
-
}
|
|
220
|
-
log(deps, 'info', 'Multiple workspaces detected:');
|
|
221
|
-
candidates.forEach((c, i) => log(deps, 'info', ` [${i + 1}] ${path.relative(repoRoot, c)}`));
|
|
222
|
-
const answer = (await deps.prompt(' Select workspace [1]: ')).trim() || '1';
|
|
223
|
-
const idx = parseInt(answer, 10) - 1;
|
|
224
|
-
if (Number.isFinite(idx) && idx >= 0 && idx < candidates.length) {
|
|
225
|
-
return candidates[idx];
|
|
226
|
-
}
|
|
227
|
-
return repoRoot;
|
|
228
|
-
}
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// Script resolution
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
/**
|
|
233
|
-
* Locate the repo scripts/ directory (works when installed from source tree
|
|
234
|
-
* or as a bundled package; falls back to GURULU_SCRIPTS_DIR env override).
|
|
235
|
-
*/
|
|
236
|
-
function resolveScriptsDir(startDir = __dirname) {
|
|
237
|
-
if (process.env.GURULU_SCRIPTS_DIR && fs.existsSync(process.env.GURULU_SCRIPTS_DIR)) {
|
|
238
|
-
return process.env.GURULU_SCRIPTS_DIR;
|
|
239
|
-
}
|
|
240
|
-
let dir = startDir;
|
|
241
|
-
for (let i = 0; i < 8; i++) {
|
|
242
|
-
const candidate = path.join(dir, 'scripts', 'gurulu-scan.mjs');
|
|
243
|
-
if (fs.existsSync(candidate))
|
|
244
|
-
return path.join(dir, 'scripts');
|
|
245
|
-
const parent = path.dirname(dir);
|
|
246
|
-
if (parent === dir)
|
|
247
|
-
break;
|
|
248
|
-
dir = parent;
|
|
249
|
-
}
|
|
250
|
-
// Default to monorepo root guess (4 levels up from packages/cli/dist/commands)
|
|
251
|
-
return path.resolve(startDir, '..', '..', '..', '..', 'scripts');
|
|
252
|
-
}
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
// Package-manager detection
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
function detectPackageManager(repoRoot) {
|
|
257
|
-
// FA-1 P1-6 — Bun support. Bun ships either `bun.lockb` (binary) or
|
|
258
|
-
// `bun.lock` (text, newer Bun releases). Detect both before falling back
|
|
259
|
-
// to pnpm/yarn/npm so monorepos using Bun don't get installed with the
|
|
260
|
-
// wrong package manager.
|
|
261
|
-
if (fs.existsSync(path.join(repoRoot, 'bun.lockb')) ||
|
|
262
|
-
fs.existsSync(path.join(repoRoot, 'bun.lock'))) {
|
|
263
|
-
return 'bun';
|
|
264
|
-
}
|
|
265
|
-
if (fs.existsSync(path.join(repoRoot, 'pnpm-lock.yaml')))
|
|
266
|
-
return 'pnpm';
|
|
267
|
-
if (fs.existsSync(path.join(repoRoot, 'yarn.lock')))
|
|
268
|
-
return 'yarn';
|
|
269
|
-
return 'npm';
|
|
270
|
-
}
|
|
271
|
-
function packageInstallArgs(pm) {
|
|
272
|
-
if (pm === 'pnpm')
|
|
273
|
-
return ['add', '@gurulu/web'];
|
|
274
|
-
if (pm === 'yarn')
|
|
275
|
-
return ['add', '@gurulu/web'];
|
|
276
|
-
if (pm === 'bun')
|
|
277
|
-
return ['add', '@gurulu/web'];
|
|
278
|
-
return ['install', '@gurulu/web'];
|
|
279
|
-
}
|
|
280
|
-
// FA-1 P1-8 — full dotenv-compatible parser. The previous one-line
|
|
281
|
-
// `KEY=value` split mis-handled (a) quoted values with `=` inside,
|
|
282
|
-
// (b) multi-line values inside double-quotes (escape `\n`), and (c) the
|
|
283
|
-
// `export KEY=value` form some teams use. This parser walks the file
|
|
284
|
-
// character-by-character, tracking whether we're inside a quoted value, so
|
|
285
|
-
// the existing-keys set actually contains every declared key — preventing
|
|
286
|
-
// duplicate writes that would override the user's values.
|
|
287
|
-
function parseEnvFile(text) {
|
|
288
|
-
const keys = new Set();
|
|
289
|
-
let i = 0;
|
|
290
|
-
const n = text.length;
|
|
291
|
-
while (i < n) {
|
|
292
|
-
// Skip leading whitespace + newlines.
|
|
293
|
-
while (i < n && (text[i] === ' ' || text[i] === '\t' || text[i] === '\r' || text[i] === '\n')) {
|
|
294
|
-
i++;
|
|
295
|
-
}
|
|
296
|
-
if (i >= n)
|
|
297
|
-
break;
|
|
298
|
-
// Comment line.
|
|
299
|
-
if (text[i] === '#') {
|
|
300
|
-
while (i < n && text[i] !== '\n')
|
|
301
|
-
i++;
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
// Optional `export ` prefix.
|
|
305
|
-
if (text.startsWith('export ', i) || text.startsWith('export\t', i)) {
|
|
306
|
-
i += 7;
|
|
307
|
-
while (i < n && (text[i] === ' ' || text[i] === '\t'))
|
|
308
|
-
i++;
|
|
309
|
-
}
|
|
310
|
-
// Key.
|
|
311
|
-
const keyStart = i;
|
|
312
|
-
while (i < n && text[i] !== '=' && text[i] !== '\n' && text[i] !== '#')
|
|
313
|
-
i++;
|
|
314
|
-
const key = text.slice(keyStart, i).trim();
|
|
315
|
-
if (text[i] !== '=') {
|
|
316
|
-
// No `=` on this line — skip to next newline.
|
|
317
|
-
while (i < n && text[i] !== '\n')
|
|
318
|
-
i++;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (key && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
322
|
-
keys.add(key);
|
|
323
|
-
}
|
|
324
|
-
i++; // consume '='
|
|
325
|
-
// Value — handles "..." (multi-line, escape-aware), '...', and bare.
|
|
326
|
-
if (text[i] === '"') {
|
|
327
|
-
i++;
|
|
328
|
-
while (i < n) {
|
|
329
|
-
if (text[i] === '\\' && i + 1 < n) {
|
|
330
|
-
i += 2;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (text[i] === '"') {
|
|
334
|
-
i++;
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
i++;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else if (text[i] === "'") {
|
|
341
|
-
i++;
|
|
342
|
-
while (i < n && text[i] !== "'")
|
|
343
|
-
i++;
|
|
344
|
-
if (i < n)
|
|
345
|
-
i++;
|
|
346
|
-
}
|
|
347
|
-
else if (text[i] === '`') {
|
|
348
|
-
i++;
|
|
349
|
-
while (i < n && text[i] !== '`')
|
|
350
|
-
i++;
|
|
351
|
-
if (i < n)
|
|
352
|
-
i++;
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
while (i < n && text[i] !== '\n' && text[i] !== '#')
|
|
356
|
-
i++;
|
|
357
|
-
}
|
|
358
|
-
// Skip rest of the line (trailing comment / whitespace).
|
|
359
|
-
while (i < n && text[i] !== '\n')
|
|
360
|
-
i++;
|
|
361
|
-
}
|
|
362
|
-
return keys;
|
|
363
|
-
}
|
|
364
|
-
function mergeEnvFile(repoRoot, framework, vars) {
|
|
365
|
-
const isNext = framework.startsWith('nextjs') || framework === 'nextjs';
|
|
366
|
-
const filename = isNext ? '.env.local' : '.env';
|
|
367
|
-
const filePath = path.join(repoRoot, filename);
|
|
368
|
-
let existing = '';
|
|
369
|
-
if (fs.existsSync(filePath)) {
|
|
370
|
-
existing = fs.readFileSync(filePath, 'utf8');
|
|
371
|
-
}
|
|
372
|
-
// FA-1 P1-8 — proper quoted/multiline-aware parse so we don't shadow keys
|
|
373
|
-
// the user already defined inside `KEY="multi\nline"` blocks.
|
|
374
|
-
const existingKeys = parseEnvFile(existing);
|
|
375
|
-
const added = [];
|
|
376
|
-
const skipped = [];
|
|
377
|
-
const linesToAppend = [];
|
|
378
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
379
|
-
if (existingKeys.has(key)) {
|
|
380
|
-
skipped.push(key);
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
// Quote values that contain whitespace, `=`, `#`, or quotes themselves so
|
|
384
|
-
// they round-trip through any standard dotenv parser.
|
|
385
|
-
const needsQuote = /[\s="#\\]/.test(value);
|
|
386
|
-
const serialized = needsQuote
|
|
387
|
-
? `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
|
388
|
-
: value;
|
|
389
|
-
linesToAppend.push(`${key}=${serialized}`);
|
|
390
|
-
added.push(key);
|
|
391
|
-
}
|
|
392
|
-
if (linesToAppend.length > 0) {
|
|
393
|
-
const separator = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
394
|
-
const header = existing ? '' : '# Gurulu.io Analytics\n';
|
|
395
|
-
fs.appendFileSync(filePath, `${separator}${header}${linesToAppend.join('\n')}\n`);
|
|
396
|
-
}
|
|
397
|
-
return { file: filename, added, skipped };
|
|
398
|
-
}
|
|
399
|
-
// ---------------------------------------------------------------------------
|
|
400
|
-
// Sprint E2.2 — Fetch canonical install prompt from server endpoint.
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
|
-
async function fetchInstallPrompt(opts) {
|
|
403
|
-
const fallback = `I have Gurulu analytics installed. Site ID: ${opts.siteId}\n` +
|
|
404
|
-
`Analyze my codebase and add gurulu.track() calls to\n` +
|
|
405
|
-
`important user actions: signups, purchases, form submits,\n` +
|
|
406
|
-
`button clicks, and key conversions. Use the Gurulu MCP\n` +
|
|
407
|
-
`server (@gurulu/mcp-server) for live event verification.\n` +
|
|
408
|
-
`Docs: https://gurulu.io/docs/quick-start`;
|
|
409
|
-
if (!opts.authToken || !opts.siteId)
|
|
410
|
-
return fallback;
|
|
411
|
-
try {
|
|
412
|
-
const url = new URL('/api/cli/install/prompt', opts.ingestUrl);
|
|
413
|
-
url.searchParams.set('siteId', opts.siteId);
|
|
414
|
-
if (opts.framework)
|
|
415
|
-
url.searchParams.set('framework', opts.framework);
|
|
416
|
-
const res = await globalThis.fetch(url.toString(), {
|
|
417
|
-
headers: { authorization: `Bearer ${opts.authToken}` },
|
|
418
|
-
});
|
|
419
|
-
if (!res.ok)
|
|
420
|
-
return fallback;
|
|
421
|
-
const body = await res.json();
|
|
422
|
-
if (body && typeof body.prompt === 'string' && body.prompt.length > 0) {
|
|
423
|
-
return body.prompt;
|
|
424
|
-
}
|
|
425
|
-
return fallback;
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return fallback;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
// ---------------------------------------------------------------------------
|
|
432
|
-
// Default dependency implementations (real spawn + real fetch)
|
|
433
|
-
// ---------------------------------------------------------------------------
|
|
434
|
-
function createDefaultDeps(scriptsDir) {
|
|
435
|
-
const runNode = (args, opts = {}) => new Promise((resolve) => {
|
|
436
|
-
const child = (0, child_process_1.spawn)('node', args, {
|
|
437
|
-
cwd: opts.cwd,
|
|
438
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
439
|
-
});
|
|
440
|
-
let stdout = '';
|
|
441
|
-
let stderr = '';
|
|
442
|
-
child.stdout.on('data', (c) => (stdout += c.toString()));
|
|
443
|
-
child.stderr.on('data', (c) => (stderr += c.toString()));
|
|
444
|
-
child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
445
|
-
if (opts.input) {
|
|
446
|
-
child.stdin.write(opts.input);
|
|
447
|
-
child.stdin.end();
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
const runCmd = (cmd, args, opts = {}) => new Promise((resolve) => {
|
|
451
|
-
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
452
|
-
cwd: opts.cwd,
|
|
453
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
454
|
-
shell: process.platform === 'win32',
|
|
455
|
-
});
|
|
456
|
-
let stdout = '';
|
|
457
|
-
let stderr = '';
|
|
458
|
-
child.stdout.on('data', (c) => (stdout += c.toString()));
|
|
459
|
-
child.stderr.on('data', (c) => (stderr += c.toString()));
|
|
460
|
-
child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
461
|
-
child.on('error', (err) => resolve({ code: 1, stdout, stderr: String(err) }));
|
|
462
|
-
});
|
|
463
|
-
const fetchJson = async (url, body) => {
|
|
464
|
-
try {
|
|
465
|
-
// Node 18+ has global fetch
|
|
466
|
-
const res = await globalThis.fetch(url, {
|
|
467
|
-
method: 'POST',
|
|
468
|
-
headers: { 'content-type': 'application/json' },
|
|
469
|
-
body: JSON.stringify(body),
|
|
470
|
-
});
|
|
471
|
-
return { ok: !!res.ok, status: res.status };
|
|
472
|
-
}
|
|
473
|
-
catch {
|
|
474
|
-
return { ok: false, status: 0 };
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
const verify = async (params) => {
|
|
478
|
-
const verifierScript = path.join(params.scriptsDir, 'gurulu-verify-install.mjs');
|
|
479
|
-
const args = [
|
|
480
|
-
verifierScript,
|
|
481
|
-
params.repoRoot,
|
|
482
|
-
'--framework',
|
|
483
|
-
params.framework,
|
|
484
|
-
'--site-id',
|
|
485
|
-
params.siteId,
|
|
486
|
-
'--tenant-id',
|
|
487
|
-
params.tenantId,
|
|
488
|
-
'--ingest-url',
|
|
489
|
-
params.ingestUrl,
|
|
490
|
-
];
|
|
491
|
-
const res = await runNode(args);
|
|
492
|
-
// Parse the final VERIFY_RESULT <json> line if present.
|
|
493
|
-
const lines = (res.stdout || '').trim().split('\n');
|
|
494
|
-
let parsed = { ok: res.code === 0, reason: null };
|
|
495
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
496
|
-
const line = lines[i];
|
|
497
|
-
if (line.startsWith('VERIFY_RESULT ')) {
|
|
498
|
-
try {
|
|
499
|
-
parsed = JSON.parse(line.slice('VERIFY_RESULT '.length));
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
// fall through
|
|
503
|
-
}
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (res.code !== 0 && parsed.ok)
|
|
508
|
-
parsed.ok = false;
|
|
509
|
-
return parsed;
|
|
510
|
-
};
|
|
511
|
-
return {
|
|
512
|
-
runNode,
|
|
513
|
-
runCmd,
|
|
514
|
-
fetchJson,
|
|
515
|
-
prompt: ui_1.prompt,
|
|
516
|
-
verify,
|
|
517
|
-
log: { info: ui_1.info, warn: ui_1.warn, error: ui_1.error, step: ui_1.step, success: ui_1.success },
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
// ---------------------------------------------------------------------------
|
|
521
|
-
// Orchestration
|
|
522
|
-
// ---------------------------------------------------------------------------
|
|
523
|
-
function log(deps, level, msg) {
|
|
524
|
-
const l = deps.log;
|
|
525
|
-
if (!l)
|
|
526
|
-
return;
|
|
527
|
-
l[level](msg);
|
|
528
|
-
}
|
|
529
|
-
function getEnvPrefix(framework) {
|
|
530
|
-
if (framework.startsWith('nextjs'))
|
|
531
|
-
return 'NEXT_PUBLIC_';
|
|
532
|
-
if (framework === 'vite-react' || framework === 'vite')
|
|
533
|
-
return 'VITE_';
|
|
534
|
-
if (framework === 'nuxt' || framework === 'vue')
|
|
535
|
-
return 'NUXT_PUBLIC_';
|
|
536
|
-
if (framework === 'sveltekit' || framework === 'svelte')
|
|
537
|
-
return 'PUBLIC_';
|
|
538
|
-
if (framework === 'astro')
|
|
539
|
-
return 'PUBLIC_';
|
|
540
|
-
return ''; // Express, Fastify, NestJS don't need prefix
|
|
541
|
-
}
|
|
542
|
-
async function runInstallFlow(args, deps, scriptsDir) {
|
|
543
|
-
const initialRoot = path.resolve(args.path || process.cwd());
|
|
544
|
-
// Sprint E1.5 — workspace resolution. When the project is a multi-package
|
|
545
|
-
// workspace, narrow `repoRoot` to the chosen workspace package so scan,
|
|
546
|
-
// patches, .env merge, and npm install all target the right place.
|
|
547
|
-
const repoRoot = await resolveWorkspaceRoot(initialRoot, args, deps);
|
|
548
|
-
const summary = {
|
|
549
|
-
scan: null,
|
|
550
|
-
framework: null,
|
|
551
|
-
planDiff: '',
|
|
552
|
-
filesChanged: 0,
|
|
553
|
-
packageManager: null,
|
|
554
|
-
packageInstalled: false,
|
|
555
|
-
envKeysWritten: [],
|
|
556
|
-
ingestOk: null,
|
|
557
|
-
dryRun: !!args.dryRun,
|
|
558
|
-
verify: null,
|
|
559
|
-
rolledBack: false,
|
|
560
|
-
errors: [],
|
|
561
|
-
};
|
|
562
|
-
if (!args.siteId) {
|
|
563
|
-
const msg = 'Missing --site-id. Provide --site-id or run without --yes to enter interactively.';
|
|
564
|
-
summary.errors.push(msg);
|
|
565
|
-
log(deps, 'error', msg);
|
|
566
|
-
return summary;
|
|
567
|
-
}
|
|
568
|
-
if (!args.tenantId) {
|
|
569
|
-
const msg = 'Missing --tenant-id. Provide --tenant-id or run without --yes to enter interactively.';
|
|
570
|
-
summary.errors.push(msg);
|
|
571
|
-
log(deps, 'error', msg);
|
|
572
|
-
return summary;
|
|
573
|
-
}
|
|
574
|
-
const scanScript = path.join(scriptsDir, 'gurulu-scan.mjs');
|
|
575
|
-
const agenticScript = path.join(scriptsDir, 'gurulu-agentic-install.mjs');
|
|
576
|
-
// ---- 1. Scan ---------------------------------------------------------
|
|
577
|
-
log(deps, 'step', `Scanning ${(0, ui_1.cyan)(repoRoot)}...`);
|
|
578
|
-
const scanRes = await deps.runNode([scanScript, repoRoot, '--quiet']);
|
|
579
|
-
if (scanRes.code !== 0) {
|
|
580
|
-
const msg = `Scan failed (exit ${scanRes.code}): ${scanRes.stderr.trim()}`;
|
|
581
|
-
summary.errors.push(msg);
|
|
582
|
-
log(deps, 'error', msg);
|
|
583
|
-
return summary;
|
|
584
|
-
}
|
|
585
|
-
try {
|
|
586
|
-
summary.scan = JSON.parse(scanRes.stdout);
|
|
587
|
-
}
|
|
588
|
-
catch (err) {
|
|
589
|
-
const msg = `Scan produced invalid JSON: ${err.message}`;
|
|
590
|
-
summary.errors.push(msg);
|
|
591
|
-
log(deps, 'error', msg);
|
|
592
|
-
return summary;
|
|
593
|
-
}
|
|
594
|
-
const detectedFw = args.framework || summary.scan.framework?.name || 'auto';
|
|
595
|
-
summary.framework = detectedFw;
|
|
596
|
-
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}`);
|
|
597
|
-
// ---- 2. Confirm ------------------------------------------------------
|
|
598
|
-
if (!args.yes) {
|
|
599
|
-
const answer = await deps.prompt(' Continue with install? (Y/n): ');
|
|
600
|
-
if (answer && answer.toLowerCase().startsWith('n')) {
|
|
601
|
-
log(deps, 'info', 'Aborted by user.');
|
|
602
|
-
return summary;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
// ---- 3. Agentic dry-run (plan / diff) --------------------------------
|
|
606
|
-
log(deps, 'step', 'Generating patch plan (dry-run)...');
|
|
607
|
-
const planArgs = [
|
|
608
|
-
agenticScript,
|
|
609
|
-
repoRoot,
|
|
610
|
-
'--dry-run',
|
|
611
|
-
'--site-id',
|
|
612
|
-
args.siteId,
|
|
613
|
-
'--tenant-id',
|
|
614
|
-
args.tenantId,
|
|
615
|
-
];
|
|
616
|
-
if (args.framework) {
|
|
617
|
-
planArgs.push('--framework', args.framework);
|
|
618
|
-
}
|
|
619
|
-
// Sprint A fix A2 — forward the active profile's secret key so the planner
|
|
620
|
-
// can call the LLM property-extraction endpoint while building the diff.
|
|
621
|
-
if (args.authToken) {
|
|
622
|
-
planArgs.push('--token', args.authToken);
|
|
623
|
-
}
|
|
624
|
-
// Phase 18.7 B — include auto-instrument diff in the dry-run plan so the
|
|
625
|
-
// consent prompt / preview shows both script-tag + track-call changes.
|
|
626
|
-
if (args.autoInstrument) {
|
|
627
|
-
planArgs.push('--auto-instrument');
|
|
628
|
-
if (args.autoProperties) {
|
|
629
|
-
planArgs.push('--auto-properties');
|
|
630
|
-
}
|
|
631
|
-
if (args.intentResultPath) {
|
|
632
|
-
planArgs.push('--intent-result', args.intentResultPath);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
// FA-1 P0-2 — forward self-hosted overrides so the dry-run plan, the
|
|
636
|
-
// applied tracker tag, and the LLM endpoint base all agree. Without this
|
|
637
|
-
// self-hosted installs render `<script src="https://gurulu.io/t.js">` even
|
|
638
|
-
// when the user pointed `--ingest-url` at their own host.
|
|
639
|
-
if (args.ingestUrl) {
|
|
640
|
-
planArgs.push('--ingest-url', args.ingestUrl);
|
|
641
|
-
}
|
|
642
|
-
if (args.scriptSrc) {
|
|
643
|
-
planArgs.push('--script-src', args.scriptSrc);
|
|
644
|
-
}
|
|
645
|
-
const planRes = await deps.runNode(planArgs);
|
|
646
|
-
summary.planDiff = planRes.stdout;
|
|
647
|
-
if (planRes.code !== 0) {
|
|
648
|
-
const msg = `Planner failed (exit ${planRes.code}): ${planRes.stderr.trim()}`;
|
|
649
|
-
summary.errors.push(msg);
|
|
650
|
-
log(deps, 'error', msg);
|
|
651
|
-
return summary;
|
|
652
|
-
}
|
|
653
|
-
if (planRes.stdout.trim()) {
|
|
654
|
-
log(deps, 'info', 'Proposed changes:');
|
|
655
|
-
for (const line of planRes.stdout.split('\n').slice(0, 40)) {
|
|
656
|
-
if (line)
|
|
657
|
-
log(deps, 'info', ` ${(0, ui_1.dim)(line)}`);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// crude file-count: `+++ b/<path>` lines in unified diff
|
|
661
|
-
summary.filesChanged = (planRes.stdout.match(/^\+\+\+ /gm) || []).length;
|
|
662
|
-
// ---- 4. Apply (skipped on --dry-run) ---------------------------------
|
|
663
|
-
if (!args.dryRun) {
|
|
664
|
-
log(deps, 'step', 'Applying patches...');
|
|
665
|
-
const applyArgs = [
|
|
666
|
-
agenticScript,
|
|
667
|
-
repoRoot,
|
|
668
|
-
'--apply',
|
|
669
|
-
'--site-id',
|
|
670
|
-
args.siteId,
|
|
671
|
-
'--tenant-id',
|
|
672
|
-
args.tenantId,
|
|
673
|
-
];
|
|
674
|
-
if (args.framework) {
|
|
675
|
-
applyArgs.push('--framework', args.framework);
|
|
676
|
-
}
|
|
677
|
-
// Sprint A fix A2 — forward the active profile's secret key so the
|
|
678
|
-
// agentic-install script's `tryLlmExtraction` can authenticate against
|
|
679
|
-
// the LLM property-extraction endpoint. Without it the request 401s and
|
|
680
|
-
// every injected `gurulu.track(...)` falls back to `// TODO: <prop>`
|
|
681
|
-
// placeholders, breaking the auto-instrument feature.
|
|
682
|
-
if (args.authToken) {
|
|
683
|
-
applyArgs.push('--token', args.authToken);
|
|
684
|
-
}
|
|
685
|
-
// Phase 18.7 B — forward auto-instrument flag + intent result path.
|
|
686
|
-
if (args.autoInstrument) {
|
|
687
|
-
applyArgs.push('--auto-instrument');
|
|
688
|
-
if (args.autoProperties) {
|
|
689
|
-
applyArgs.push('--auto-properties');
|
|
690
|
-
}
|
|
691
|
-
if (args.intentResultPath) {
|
|
692
|
-
applyArgs.push('--intent-result', args.intentResultPath);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
// FA-1 P0-2 — also forward to --apply so the file actually written to
|
|
696
|
-
// disk uses the correct tracker URL.
|
|
697
|
-
if (args.ingestUrl) {
|
|
698
|
-
applyArgs.push('--ingest-url', args.ingestUrl);
|
|
699
|
-
}
|
|
700
|
-
if (args.scriptSrc) {
|
|
701
|
-
applyArgs.push('--script-src', args.scriptSrc);
|
|
702
|
-
}
|
|
703
|
-
const applyRes = await deps.runNode(applyArgs);
|
|
704
|
-
// Sprint E1.1 — exit code 5 means the agentic-install script auto-rolled
|
|
705
|
-
// back due to an auto-instrument failure. Mark the summary as rolled back
|
|
706
|
-
// and skip the npm install / .env merge / ingest ping blocks so the user
|
|
707
|
-
// isn't left with a half-installed setup.
|
|
708
|
-
if (applyRes.code === 5) {
|
|
709
|
-
summary.rolledBack = true;
|
|
710
|
-
summary.partiallyInstalled = false;
|
|
711
|
-
log(deps, 'warn', '⚠ Install rolled back (auto-instrument failed; script tag reverted).');
|
|
712
|
-
return summary;
|
|
713
|
-
}
|
|
714
|
-
if (applyRes.code !== 0) {
|
|
715
|
-
const msg = `Apply failed (exit ${applyRes.code}): ${applyRes.stderr.trim()}`;
|
|
716
|
-
summary.errors.push(msg);
|
|
717
|
-
log(deps, 'error', msg);
|
|
718
|
-
return summary;
|
|
719
|
-
}
|
|
720
|
-
// Sprint E1.7 — record components that were successfully installed for
|
|
721
|
-
// partial-install diagnostics in the summary.
|
|
722
|
-
// CLI@0.4.4 — re-run idempotency: dedupe via Set so a second `gurulu install`
|
|
723
|
-
// pass on the same project does not double-list components. Component IDs
|
|
724
|
-
// hash siteId + framework + path so two different sites in the same repo
|
|
725
|
-
// remain distinct.
|
|
726
|
-
const componentScope = `${args.site || args.siteId || 'unknown'}::${args.framework || 'auto'}::${args.path || process.cwd()}`;
|
|
727
|
-
const existingComponents = new Set(summary.installedComponents || []);
|
|
728
|
-
const addComponent = (id) => {
|
|
729
|
-
const key = `${id}@${componentScope}`;
|
|
730
|
-
if (existingComponents.has(key)) {
|
|
731
|
-
log(deps, 'info', `Skipping ${id} — already installed for this scope`);
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
existingComponents.add(key);
|
|
735
|
-
};
|
|
736
|
-
addComponent('script-tag');
|
|
737
|
-
addComponent('patch-log');
|
|
738
|
-
if (args.autoInstrument)
|
|
739
|
-
addComponent('auto-instrument');
|
|
740
|
-
summary.installedComponents = Array.from(existingComponents);
|
|
741
|
-
// Parse the machine-readable auto-instrument result line if present.
|
|
742
|
-
if (args.autoInstrument) {
|
|
743
|
-
const lines = (applyRes.stdout || '').split('\n');
|
|
744
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
745
|
-
if (lines[i].startsWith('AUTO_INSTRUMENT_RESULT ')) {
|
|
746
|
-
try {
|
|
747
|
-
const parsed = JSON.parse(lines[i].slice('AUTO_INSTRUMENT_RESULT '.length));
|
|
748
|
-
summary.instrumentation = {
|
|
749
|
-
enabled: true,
|
|
750
|
-
filesModified: parsed.filesModified || 0,
|
|
751
|
-
eventsInstrumented: parsed.eventsInstrumented || 0,
|
|
752
|
-
eventsSkipped: parsed.eventsSkipped || 0,
|
|
753
|
-
notes: parsed.notes || [],
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
catch {
|
|
757
|
-
// fall through — telemetry will show enabled:true with zeros.
|
|
758
|
-
}
|
|
759
|
-
break;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
if (!summary.instrumentation) {
|
|
763
|
-
summary.instrumentation = {
|
|
764
|
-
enabled: true,
|
|
765
|
-
filesModified: 0,
|
|
766
|
-
eventsInstrumented: 0,
|
|
767
|
-
eventsSkipped: 0,
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
log(deps, 'success', 'Patches applied.');
|
|
772
|
-
// Sprint E5.2 — wire a `postbuild` script for Next.js projects so the
|
|
773
|
-
// CLI's sourcemap uploader runs automatically after every `next build`.
|
|
774
|
-
// We only touch package.json when the framework is Next.js AND the user
|
|
775
|
-
// hasn't already defined a `postbuild` (idempotent).
|
|
776
|
-
if (detectedFw && detectedFw.startsWith('nextjs')) {
|
|
777
|
-
try {
|
|
778
|
-
const pkgPath = path.join(repoRoot, 'package.json');
|
|
779
|
-
if (fs.existsSync(pkgPath)) {
|
|
780
|
-
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
781
|
-
const pkgJson = JSON.parse(raw);
|
|
782
|
-
pkgJson.scripts = pkgJson.scripts || {};
|
|
783
|
-
if (!pkgJson.scripts.postbuild) {
|
|
784
|
-
pkgJson.scripts.postbuild =
|
|
785
|
-
'gurulu sourcemap upload --release ${npm_package_version} --dir .next/static/chunks';
|
|
786
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
|
|
787
|
-
log(deps, 'info', 'Added `postbuild` script for sourcemap upload.');
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
catch (err) {
|
|
792
|
-
log(deps, 'warn', `Could not add postbuild script: ${err.message}`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
else {
|
|
797
|
-
if (args.autoInstrument) {
|
|
798
|
-
log(deps, 'info', `Auto-instrument: ${summary.instrumentation?.filesModified || 0} route files, ${summary.instrumentation?.eventsInstrumented || 0} events`);
|
|
799
|
-
}
|
|
800
|
-
log(deps, 'info', 'Dry-run: skipping apply, npm install, env merge, and ingest test.');
|
|
801
|
-
}
|
|
802
|
-
// ---- 5. npm install --------------------------------------------------
|
|
803
|
-
const pm = detectPackageManager(repoRoot);
|
|
804
|
-
summary.packageManager = pm;
|
|
805
|
-
if (args.skipNpm || args.dryRun) {
|
|
806
|
-
log(deps, 'info', `Skipping package install (${pm} add @gurulu/web)`);
|
|
807
|
-
}
|
|
808
|
-
else {
|
|
809
|
-
log(deps, 'step', `Installing @gurulu/web with ${(0, ui_1.bold)(pm)}...`);
|
|
810
|
-
const pmRes = await deps.runCmd(pm, packageInstallArgs(pm), { cwd: repoRoot });
|
|
811
|
-
if (pmRes.code !== 0) {
|
|
812
|
-
const msg = `${pm} install failed (exit ${pmRes.code}): ${pmRes.stderr.trim()}`;
|
|
813
|
-
summary.errors.push(msg);
|
|
814
|
-
log(deps, 'warn', msg);
|
|
815
|
-
}
|
|
816
|
-
else {
|
|
817
|
-
summary.packageInstalled = true;
|
|
818
|
-
log(deps, 'success', '@gurulu/web installed.');
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
// ---- 6. .env merge ---------------------------------------------------
|
|
822
|
-
const ingestUrl = args.ingestUrl || process.env.GURULU_INGEST_URL || 'https://gurulu.io';
|
|
823
|
-
const envPrefix = getEnvPrefix(detectedFw);
|
|
824
|
-
const envVars = {
|
|
825
|
-
[`${envPrefix}GURULU_SITE_ID`]: args.siteId,
|
|
826
|
-
[`${envPrefix}GURULU_TENANT_ID`]: args.tenantId,
|
|
827
|
-
[`${envPrefix}GURULU_INGEST_URL`]: ingestUrl,
|
|
828
|
-
};
|
|
829
|
-
if (args.skipEnv || args.dryRun) {
|
|
830
|
-
log(deps, 'info', 'Skipping .env merge.');
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
|
-
const mergeRes = mergeEnvFile(repoRoot, detectedFw, envVars);
|
|
834
|
-
summary.envKeysWritten = mergeRes.added;
|
|
835
|
-
if (mergeRes.added.length > 0) {
|
|
836
|
-
log(deps, 'success', `Wrote ${mergeRes.added.length} key(s) to ${mergeRes.file}`);
|
|
837
|
-
}
|
|
838
|
-
if (mergeRes.skipped.length > 0) {
|
|
839
|
-
log(deps, 'warn', `Skipped existing key(s): ${mergeRes.skipped.join(', ')}`);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
// ---- 7. Ingest ping --------------------------------------------------
|
|
843
|
-
if (!args.dryRun) {
|
|
844
|
-
const pingUrl = `${ingestUrl.replace(/\/$/, '')}/api/ingest/v1/health`;
|
|
845
|
-
log(deps, 'step', `Pinging ingest (${pingUrl})...`);
|
|
846
|
-
const pingRes = await deps.fetchJson(pingUrl, {
|
|
847
|
-
siteId: args.siteId,
|
|
848
|
-
tenantId: args.tenantId,
|
|
849
|
-
source: 'gurulu-cli-install',
|
|
850
|
-
});
|
|
851
|
-
summary.ingestOk = pingRes.ok;
|
|
852
|
-
if (pingRes.ok) {
|
|
853
|
-
log(deps, 'success', `Ingest reachable (status ${pingRes.status}).`);
|
|
854
|
-
}
|
|
855
|
-
else {
|
|
856
|
-
log(deps, 'warn', `Ingest ping failed (status ${pingRes.status}). Verify GURULU_INGEST_URL.`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
// ---- 8. Live smoke verify (Phase 16 A3) ------------------------------
|
|
860
|
-
if (args.verify && !args.dryRun) {
|
|
861
|
-
if (!deps.verify) {
|
|
862
|
-
log(deps, 'warn', 'Verify flag set but no verifier dep configured; skipping.');
|
|
863
|
-
}
|
|
864
|
-
else {
|
|
865
|
-
log(deps, 'step', 'Running live smoke verification...');
|
|
866
|
-
try {
|
|
867
|
-
const verifyRes = await deps.verify({
|
|
868
|
-
repoRoot,
|
|
869
|
-
framework: detectedFw,
|
|
870
|
-
siteId: args.siteId,
|
|
871
|
-
tenantId: args.tenantId,
|
|
872
|
-
ingestUrl,
|
|
873
|
-
scriptsDir,
|
|
874
|
-
});
|
|
875
|
-
summary.verify = verifyRes;
|
|
876
|
-
if (verifyRes.ok) {
|
|
877
|
-
log(deps, 'success', 'Install verified: $page_view captured.');
|
|
878
|
-
}
|
|
879
|
-
else {
|
|
880
|
-
const reason = verifyRes.reason || 'unknown';
|
|
881
|
-
const msg = `Verification failed: ${reason}. Rolling back patches...`;
|
|
882
|
-
summary.errors.push(msg);
|
|
883
|
-
log(deps, 'error', msg);
|
|
884
|
-
// Auto-rollback
|
|
885
|
-
const rollbackArgs = [agenticScript, repoRoot, '--rollback'];
|
|
886
|
-
const rbRes = await deps.runNode(rollbackArgs);
|
|
887
|
-
if (rbRes.code === 0) {
|
|
888
|
-
summary.rolledBack = true;
|
|
889
|
-
log(deps, 'warn', 'Patches rolled back.');
|
|
890
|
-
}
|
|
891
|
-
else {
|
|
892
|
-
log(deps, 'error', `Rollback failed (exit ${rbRes.code}): ${rbRes.stderr.trim()}`);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
catch (err) {
|
|
897
|
-
const msg = `Verify threw: ${err.message}`;
|
|
898
|
-
summary.errors.push(msg);
|
|
899
|
-
log(deps, 'error', msg);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
// ---- Final summary ---------------------------------------------------
|
|
904
|
-
log(deps, 'info', '');
|
|
905
|
-
log(deps, 'info', (0, ui_1.bold)(' Install summary'));
|
|
906
|
-
log(deps, 'step', `Framework: ${summary.framework}`);
|
|
907
|
-
log(deps, 'step', `Files changed: ${summary.filesChanged}`);
|
|
908
|
-
log(deps, 'step', `Package manager: ${summary.packageManager}`);
|
|
909
|
-
log(deps, 'step', `Package installed: ${summary.packageInstalled ? 'yes' : 'no'}`);
|
|
910
|
-
log(deps, 'step', `.env keys written: ${summary.envKeysWritten.join(', ') || 'none'}`);
|
|
911
|
-
log(deps, 'step', `Ingest test: ${summary.ingestOk === null ? 'skipped' : summary.ingestOk ? 'ok' : 'failed'}`);
|
|
912
|
-
if (summary.verify) {
|
|
913
|
-
log(deps, 'step', `Live verify: ${summary.verify.ok ? 'verified' : `failed (${summary.verify.reason || 'unknown'})`}`);
|
|
914
|
-
if (summary.rolledBack) {
|
|
915
|
-
log(deps, 'step', 'Patches: rolled back');
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
if (summary.instrumentation && summary.instrumentation.enabled) {
|
|
919
|
-
log(deps, 'step', `Auto-instrument: ${summary.instrumentation.filesModified} file(s), ` +
|
|
920
|
-
`${summary.instrumentation.eventsInstrumented} event(s), ` +
|
|
921
|
-
`${summary.instrumentation.eventsSkipped} skipped`);
|
|
922
|
-
}
|
|
923
|
-
log(deps, 'info', '');
|
|
924
|
-
// ---- Post-install guidance ---------------------------------------------
|
|
925
|
-
if (!summary.dryRun && summary.errors.length === 0) {
|
|
926
|
-
console.log('');
|
|
927
|
-
log(deps, 'info', '✓ Install complete! Next steps:');
|
|
928
|
-
log(deps, 'info', ` Run ${(0, ui_1.cyan)('gurulu doctor')} to verify setup health`);
|
|
929
|
-
log(deps, 'info', ` Run ${(0, ui_1.cyan)('gurulu events tail')} to watch live events`);
|
|
930
|
-
log(deps, 'info', ` Visit ${(0, ui_1.cyan)('https://gurulu.io/dashboard')} to view analytics`);
|
|
931
|
-
console.log('');
|
|
932
|
-
// AI assistant integration prompt
|
|
933
|
-
const siteId = args.siteId || args.site || '';
|
|
934
|
-
console.log((0, ui_1.dim)(' ─────────────────────────────────────────────────'));
|
|
935
|
-
console.log('');
|
|
936
|
-
log(deps, 'info', `${(0, ui_1.cyan)('AI Assistant Integration')} (Claude Code / Cursor / Codex)`);
|
|
937
|
-
console.log('');
|
|
938
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('MCP Server (real-time analytics in your editor):')}`);
|
|
939
|
-
log(deps, 'info', ` ${(0, ui_1.cyan)('npx @gurulu/mcp-server')} with env GURULU_API_KEY`);
|
|
940
|
-
console.log('');
|
|
941
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('Or paste this prompt into your AI assistant:')}`);
|
|
942
|
-
console.log('');
|
|
943
|
-
// Sprint E2.2 — fetch the canonical install prompt from the server so
|
|
944
|
-
// edits land in `src/lib/cli/install-prompt.ts` (the single source of
|
|
945
|
-
// truth) instead of being duplicated in three places. We fall back to a
|
|
946
|
-
// tiny inline body when the endpoint is unreachable or unauthenticated
|
|
947
|
-
// so legacy installs still see something useful.
|
|
948
|
-
const promptText = await fetchInstallPrompt({
|
|
949
|
-
siteId,
|
|
950
|
-
ingestUrl: args.ingestUrl || process.env.GURULU_INGEST_URL || 'https://gurulu.io',
|
|
951
|
-
authToken: args.authToken,
|
|
952
|
-
framework: summary.framework || undefined,
|
|
953
|
-
});
|
|
954
|
-
console.log((0, ui_1.dim)(' ┌─────────────────────────────────────────────'));
|
|
955
|
-
for (const line of promptText.split('\n')) {
|
|
956
|
-
console.log((0, ui_1.dim)(' │ ') + line);
|
|
957
|
-
}
|
|
958
|
-
console.log((0, ui_1.dim)(' └─────────────────────────────────────────────'));
|
|
959
|
-
console.log('');
|
|
960
|
-
// Goals & funnels management
|
|
961
|
-
log(deps, 'info', `${(0, ui_1.cyan)('Goals & Funnels')}`);
|
|
962
|
-
log(deps, 'info', ` ${(0, ui_1.cyan)('gurulu goals list')} ${(0, ui_1.dim)('List all conversion goals')}`);
|
|
963
|
-
log(deps, 'info', ` ${(0, ui_1.cyan)('gurulu goals create')} ${(0, ui_1.dim)('Create a new goal interactively')}`);
|
|
964
|
-
log(deps, 'info', ` ${(0, ui_1.cyan)('gurulu funnels create')} ${(0, ui_1.dim)('Define a multi-step funnel')}`);
|
|
965
|
-
console.log('');
|
|
966
|
-
// Server-side setup
|
|
967
|
-
log(deps, 'info', `${(0, ui_1.cyan)('Server-Side Tracking')}`);
|
|
968
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('Add server-side event tracking with:')} ${(0, ui_1.cyan)('gurulu add-server')}`);
|
|
969
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('Or manually:')}`);
|
|
970
|
-
log(deps, 'info', ` ${(0, ui_1.cyan)('npm install @gurulu/node')}`);
|
|
971
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('import { Gurulu } from \'@gurulu/node\';')}`);
|
|
972
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('const gurulu = new Gurulu({ siteId, serverApiKey });')}`);
|
|
973
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('gurulu.track(\'order_completed\', { userId, revenue });')}`);
|
|
974
|
-
console.log('');
|
|
975
|
-
// DataLayer Bridge for GTM
|
|
976
|
-
log(deps, 'info', `${(0, ui_1.cyan)('DataLayer Bridge')} ${(0, ui_1.dim)('(for GTM users)')}`);
|
|
977
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('Auto-capture all Google Tag Manager events:')}`);
|
|
978
|
-
log(deps, 'info', ` ${(0, ui_1.cyan)('window.gurulu.loadDataLayerBridge()')}`);
|
|
979
|
-
console.log('');
|
|
980
|
-
// identify() guidance
|
|
981
|
-
log(deps, 'info', `${(0, ui_1.cyan)('User Identification')}`);
|
|
982
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('Web:')} ${(0, ui_1.cyan)('window.gurulu.identify(\'user_123\', { email, plan })')}`);
|
|
983
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('Server:')} ${(0, ui_1.cyan)('gurulu.identify(\'user_123\', { email, plan })')}`);
|
|
984
|
-
log(deps, 'info', ` ${(0, ui_1.dim)('Call on login/signup to link events to known users.')}`);
|
|
985
|
-
console.log('');
|
|
986
|
-
}
|
|
987
|
-
return summary;
|
|
988
|
-
}
|
|
989
|
-
// ---------------------------------------------------------------------------
|
|
990
|
-
// FA-1 P0-5 — Telemetry consent gate (GDPR).
|
|
991
|
-
//
|
|
992
|
-
// `emitInstallTelemetry` previously fired on every install with a repo hash,
|
|
993
|
-
// framework, and CLI version — no consent recorded. We now:
|
|
994
|
-
// 1. Honor opt-out via env (`GURULU_TELEMETRY=off|0`, `DO_NOT_TRACK=1`),
|
|
995
|
-
// CLI flag (`--no-telemetry`), and persisted CLI config
|
|
996
|
-
// (`~/.gurulu/config.json` → `telemetry: false`).
|
|
997
|
-
// 2. Prompt for consent on first interactive install and persist the
|
|
998
|
-
// answer in the same config file.
|
|
999
|
-
// 3. Skip `repoHashOf` computation entirely when telemetry is disabled.
|
|
1000
|
-
// ---------------------------------------------------------------------------
|
|
1001
|
-
function cliConfigPath() {
|
|
1002
|
-
if (process.env.GURULU_CONFIG_HOME) {
|
|
1003
|
-
return path.join(process.env.GURULU_CONFIG_HOME, 'config.json');
|
|
1004
|
-
}
|
|
1005
|
-
return path.join(os.homedir(), '.gurulu', 'config.json');
|
|
1006
|
-
}
|
|
1007
|
-
function readCliConfig() {
|
|
1008
|
-
try {
|
|
1009
|
-
const p = cliConfigPath();
|
|
1010
|
-
if (!fs.existsSync(p))
|
|
1011
|
-
return {};
|
|
1012
|
-
const txt = fs.readFileSync(p, 'utf8');
|
|
1013
|
-
const parsed = JSON.parse(txt);
|
|
1014
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
1015
|
-
}
|
|
1016
|
-
catch {
|
|
1017
|
-
return {};
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
function writeCliConfig(patch) {
|
|
1021
|
-
try {
|
|
1022
|
-
const p = cliConfigPath();
|
|
1023
|
-
const dir = path.dirname(p);
|
|
1024
|
-
if (!fs.existsSync(dir))
|
|
1025
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1026
|
-
const current = readCliConfig();
|
|
1027
|
-
const next = { ...current, ...patch };
|
|
1028
|
-
fs.writeFileSync(p, JSON.stringify(next, null, 2));
|
|
1029
|
-
try {
|
|
1030
|
-
fs.chmodSync(p, 0o600);
|
|
1031
|
-
}
|
|
1032
|
-
catch {
|
|
1033
|
-
/* non-unix */
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
catch {
|
|
1037
|
-
/* best-effort persistence */
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
function isTelemetryDisabled() {
|
|
1041
|
-
const env = (process.env.GURULU_TELEMETRY || '').toLowerCase();
|
|
1042
|
-
if (env === 'off' || env === '0' || env === 'false' || env === 'no')
|
|
1043
|
-
return true;
|
|
1044
|
-
if (process.env.DO_NOT_TRACK === '1')
|
|
1045
|
-
return true;
|
|
1046
|
-
if (process.argv.includes('--no-telemetry'))
|
|
1047
|
-
return true;
|
|
1048
|
-
const cfg = readCliConfig();
|
|
1049
|
-
if (cfg.telemetry === false)
|
|
1050
|
-
return true;
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
|
1053
|
-
/**
|
|
1054
|
-
* First-run consent prompt. Persists the decision so we never re-ask.
|
|
1055
|
-
* No-op when telemetry is already disabled, when a decision is already
|
|
1056
|
-
* recorded, or in non-interactive (`--yes` / `--no-interactive`) mode.
|
|
1057
|
-
*/
|
|
1058
|
-
async function maybePromptTelemetryConsent(ask, args = {}) {
|
|
1059
|
-
if (isTelemetryDisabled())
|
|
1060
|
-
return;
|
|
1061
|
-
const cfg = readCliConfig();
|
|
1062
|
-
if (typeof cfg.telemetry === 'boolean')
|
|
1063
|
-
return; // already decided
|
|
1064
|
-
if (args.yes)
|
|
1065
|
-
return; // non-interactive: don't ask, default-on but unrecorded
|
|
1066
|
-
const answer = await ask(' Send anonymous install telemetry (framework, CLI version, repo hash) to improve Gurulu? [Y/n]: ');
|
|
1067
|
-
const decided = !answer || !/^n/i.test(answer.trim());
|
|
1068
|
-
writeCliConfig({ telemetry: decided, telemetry_decided_at: new Date().toISOString() });
|
|
1069
|
-
}
|
|
1070
|
-
function repoHashOf(repoRoot, remoteUrl = null) {
|
|
1071
|
-
// Consent-gated: when telemetry is off, never compute the hash.
|
|
1072
|
-
if (isTelemetryDisabled())
|
|
1073
|
-
return '';
|
|
1074
|
-
const h = crypto.createHash('sha256');
|
|
1075
|
-
h.update(path.resolve(repoRoot));
|
|
1076
|
-
if (remoteUrl)
|
|
1077
|
-
h.update('\n' + remoteUrl);
|
|
1078
|
-
return h.digest('hex');
|
|
1079
|
-
}
|
|
1080
|
-
function pickSite(sites, target) {
|
|
1081
|
-
if (!target)
|
|
1082
|
-
return sites.length === 1 ? sites[0] : null;
|
|
1083
|
-
return (sites.find((s) => s.id === target) ||
|
|
1084
|
-
sites.find((s) => s.name === target) ||
|
|
1085
|
-
sites.find((s) => s.id.startsWith(target)) ||
|
|
1086
|
-
null);
|
|
1087
|
-
}
|
|
1088
|
-
/** Send a telemetry POST. Never throws — quota errors are surfaced via `ok`. */
|
|
1089
|
-
async function emitInstallTelemetry(deps, payload) {
|
|
1090
|
-
// FA-1 P0-5 — GDPR consent gate. Skip the POST entirely when the user has
|
|
1091
|
-
// opted out via env/flag/config. Returns ok:true so callers don't surface
|
|
1092
|
-
// a fake failure for an intentional opt-out.
|
|
1093
|
-
if (isTelemetryDisabled()) {
|
|
1094
|
-
return { ok: true, quotaExceeded: false };
|
|
1095
|
-
}
|
|
1096
|
-
try {
|
|
1097
|
-
const res = await deps.postVerify({
|
|
1098
|
-
source: 'cli',
|
|
1099
|
-
...payload,
|
|
1100
|
-
});
|
|
1101
|
-
if (res.status === 402)
|
|
1102
|
-
return { ok: false, quotaExceeded: true };
|
|
1103
|
-
return { ok: !!res.ok, quotaExceeded: false };
|
|
1104
|
-
}
|
|
1105
|
-
catch {
|
|
1106
|
-
return { ok: false, quotaExceeded: false };
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
// FA-1 P0-1 — Detect popular CI environments. When a CI is detected and the
|
|
1110
|
-
// caller didn't already pass `--yes`, we force non-interactive mode so any
|
|
1111
|
-
// `prompt()` calls fall through to the documented default instead of blocking
|
|
1112
|
-
// on a closed stdin (which causes the entire install to hang in CI).
|
|
1113
|
-
const CI_ENV_VARS = [
|
|
1114
|
-
'CI',
|
|
1115
|
-
'VERCEL',
|
|
1116
|
-
'GITHUB_ACTIONS',
|
|
1117
|
-
'NETLIFY',
|
|
1118
|
-
'GITLAB_CI',
|
|
1119
|
-
'CIRCLECI',
|
|
1120
|
-
'TRAVIS',
|
|
1121
|
-
'BUILDKITE',
|
|
1122
|
-
];
|
|
1123
|
-
function detectCiEnv(env = process.env) {
|
|
1124
|
-
return CI_ENV_VARS.some((v) => {
|
|
1125
|
-
const raw = env[v];
|
|
1126
|
-
if (!raw)
|
|
1127
|
-
return false;
|
|
1128
|
-
const lower = String(raw).toLowerCase();
|
|
1129
|
-
return lower === '1' || lower === 'true';
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
async function runAuthenticatedInstallFlow(args, authDeps, installDeps, scriptsDir) {
|
|
1133
|
-
// FA-1 P0-1 — CI environments do not have an interactive stdin. Without
|
|
1134
|
-
// this guard every `prompt()` (`Continue with install? [Y/n]`, workspace
|
|
1135
|
-
// picker, telemetry consent, auto-instrument confirm, etc.) blocks on EOF
|
|
1136
|
-
// and the CI job times out instead of failing fast. Default to `--yes` so
|
|
1137
|
-
// every prompt accepts the documented default and the flow can complete.
|
|
1138
|
-
if (!args.yes && detectCiEnv()) {
|
|
1139
|
-
installDeps.log?.info('[install] CI environment detected, defaulting to --yes');
|
|
1140
|
-
args = { ...args, yes: true };
|
|
1141
|
-
}
|
|
1142
|
-
// FA-1 P0-5 — telemetry consent prompt on first interactive install.
|
|
1143
|
-
// Persisted to ~/.gurulu/config.json so we never re-ask.
|
|
1144
|
-
try {
|
|
1145
|
-
const ask = authDeps.prompt || ((q) => installDeps.prompt(q));
|
|
1146
|
-
await maybePromptTelemetryConsent(ask, { yes: args.yes });
|
|
1147
|
-
}
|
|
1148
|
-
catch {
|
|
1149
|
-
/* never block install on consent prompt errors */
|
|
1150
|
-
}
|
|
1151
|
-
// Site selection
|
|
1152
|
-
let selected = pickSite(authDeps.sites, args.site);
|
|
1153
|
-
if (!selected) {
|
|
1154
|
-
if (authDeps.sites.length === 0) {
|
|
1155
|
-
const msg = 'No sites found. Run `gurulu sites create --domain ...` first.';
|
|
1156
|
-
installDeps.log?.error(msg);
|
|
1157
|
-
return {
|
|
1158
|
-
scan: null,
|
|
1159
|
-
framework: null,
|
|
1160
|
-
planDiff: '',
|
|
1161
|
-
filesChanged: 0,
|
|
1162
|
-
packageManager: null,
|
|
1163
|
-
packageInstalled: false,
|
|
1164
|
-
envKeysWritten: [],
|
|
1165
|
-
ingestOk: null,
|
|
1166
|
-
dryRun: !!args.dryRun,
|
|
1167
|
-
verify: null,
|
|
1168
|
-
rolledBack: false,
|
|
1169
|
-
errors: [msg],
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
if (args.yes) {
|
|
1173
|
-
// Non-interactive, multiple sites: pick the first.
|
|
1174
|
-
selected = authDeps.sites[0];
|
|
1175
|
-
}
|
|
1176
|
-
else if (authDeps.promptSelect) {
|
|
1177
|
-
const labels = authDeps.sites.map((s) => `${s.name || s.id} (${s.domain})`);
|
|
1178
|
-
const idx = await authDeps.promptSelect(' Select site: ', labels);
|
|
1179
|
-
selected = authDeps.sites[idx] || authDeps.sites[0];
|
|
1180
|
-
}
|
|
1181
|
-
else {
|
|
1182
|
-
selected = authDeps.sites[0];
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
const filled = {
|
|
1186
|
-
...args,
|
|
1187
|
-
siteId: selected.id,
|
|
1188
|
-
tenantId: authDeps.tenantId,
|
|
1189
|
-
};
|
|
1190
|
-
// ---- Phase 18.6 — Intent discovery (pre-scan pass) ------------------
|
|
1191
|
-
// Runs BEFORE the core flow so the user sees the proposal first. We use
|
|
1192
|
-
// the scanner in --quiet mode, feed signals to the injected analyzer,
|
|
1193
|
-
// and render a proposal. On --skip-intent or missing deps we fall
|
|
1194
|
-
// straight through to the Phase 18.5 authenticated flow.
|
|
1195
|
-
const intentRecord = {
|
|
1196
|
-
skipped: !!args.skipIntent,
|
|
1197
|
-
dryRun: !!args.intentDryRun,
|
|
1198
|
-
analyzed: false,
|
|
1199
|
-
preSeeded: false,
|
|
1200
|
-
};
|
|
1201
|
-
if (!args.skipIntent && authDeps.intent) {
|
|
1202
|
-
const repoRoot = path.resolve(args.path || process.cwd());
|
|
1203
|
-
const scanScript = path.join(scriptsDir, 'gurulu-scan.mjs');
|
|
1204
|
-
installDeps.log?.step('Scanning for install-time intent signals...');
|
|
1205
|
-
const scanRes = await installDeps.runNode([scanScript, repoRoot, '--quiet']);
|
|
1206
|
-
if (scanRes.code !== 0) {
|
|
1207
|
-
installDeps.log?.warn(`Intent scan failed (exit ${scanRes.code}); skipping intent discovery.`);
|
|
1208
|
-
intentRecord.error = `scan_failed:${scanRes.code}`;
|
|
1209
|
-
}
|
|
1210
|
-
else {
|
|
1211
|
-
let signals = null;
|
|
1212
|
-
try {
|
|
1213
|
-
signals = JSON.parse(scanRes.stdout);
|
|
1214
|
-
}
|
|
1215
|
-
catch (err) {
|
|
1216
|
-
installDeps.log?.warn(`Intent scan produced invalid JSON: ${err.message}; skipping.`);
|
|
1217
|
-
intentRecord.error = 'scan_invalid_json';
|
|
1218
|
-
}
|
|
1219
|
-
if (signals) {
|
|
1220
|
-
let intent = null;
|
|
1221
|
-
try {
|
|
1222
|
-
intent = await authDeps.intent.analyze(signals);
|
|
1223
|
-
intentRecord.analyzed = true;
|
|
1224
|
-
intentRecord.vertical = intent.vertical;
|
|
1225
|
-
if (intent.analyzerMode === 'heuristic') {
|
|
1226
|
-
installDeps.log?.warn('⚠ LLM unavailable — using rule-based intent discovery. Results may be less precise.');
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
catch (err) {
|
|
1230
|
-
installDeps.log?.warn(`Intent analyzer failed: ${err.message}`);
|
|
1231
|
-
installDeps.log?.warn('⚠ Falling back to built-in generic event set.');
|
|
1232
|
-
intentRecord.error = `analyze_failed:${err.message}`;
|
|
1233
|
-
// Sprint E1.2 — distinguish "analyzer ran" from "fallback was used".
|
|
1234
|
-
// `analyzed=false` so callers can detect that the LLM/heuristic
|
|
1235
|
-
// analyzer never produced a real result; `analyze_status='fallback'`
|
|
1236
|
-
// tells them the install still proceeded with a baked-in event set
|
|
1237
|
-
// instead of failing outright.
|
|
1238
|
-
intentRecord.analyzed = false;
|
|
1239
|
-
intentRecord.analyze_status = 'fallback';
|
|
1240
|
-
// Client-side fallback: produce a minimal generic intent so
|
|
1241
|
-
// auto-instrument and pre-seed still work even when the API is down.
|
|
1242
|
-
intent = {
|
|
1243
|
-
vertical: 'generic',
|
|
1244
|
-
confidence: 'low',
|
|
1245
|
-
reasoning: 'API unavailable — using built-in generic fallback.',
|
|
1246
|
-
analyzerMode: 'heuristic',
|
|
1247
|
-
alternativeVerticals: [],
|
|
1248
|
-
events: [
|
|
1249
|
-
{ name: '$page_view', category: 'engagement', source: 'inferred:pattern', properties: [], confidence: 1.0, reasoning: 'Baseline page impression.' },
|
|
1250
|
-
{ name: '$session_start', category: 'engagement', source: 'inferred:pattern', properties: [], confidence: 1.0, reasoning: 'Session boundary.' },
|
|
1251
|
-
{ name: '$signup', category: 'acquisition', source: 'inferred:pattern', properties: [{ name: 'method', type: 'string' }], confidence: 0.9, reasoning: 'New account signal.' },
|
|
1252
|
-
{ name: '$login', category: 'engagement', source: 'inferred:pattern', properties: [{ name: 'method', type: 'string' }], confidence: 0.9, reasoning: 'Returning user signal.' },
|
|
1253
|
-
{ name: '$logout', category: 'engagement', source: 'inferred:pattern', properties: [], confidence: 0.9, reasoning: 'Session end.' },
|
|
1254
|
-
{ name: '$first_action', category: 'activation', source: 'inferred:pattern', properties: [], confidence: 0.8, reasoning: 'First meaningful interaction.' },
|
|
1255
|
-
{ name: 'click', category: 'engagement', source: 'inferred:pattern', properties: [{ name: 'selector', type: 'string' }], confidence: 1.0, reasoning: 'User clicks.' },
|
|
1256
|
-
{ name: 'scroll_depth', category: 'engagement', source: 'inferred:pattern', properties: [{ name: 'depth', type: 'number' }], confidence: 1.0, reasoning: 'Scroll engagement.' },
|
|
1257
|
-
{ name: 'form_submit', category: 'engagement', source: 'inferred:pattern', properties: [{ name: 'form_id', type: 'string' }], confidence: 0.9, reasoning: 'Form completion.' },
|
|
1258
|
-
{ name: 'engaged_session', category: 'engagement', source: 'inferred:pattern', properties: [], confidence: 1.0, reasoning: 'Quality session marker.' },
|
|
1259
|
-
{ name: '$search', category: 'engagement', source: 'inferred:pattern', properties: [{ name: 'query', type: 'string' }], confidence: 0.7, reasoning: 'Search intent.' },
|
|
1260
|
-
{ name: '$error', category: 'engagement', source: 'inferred:pattern', properties: [{ name: 'message', type: 'string' }], confidence: 0.8, reasoning: 'Client error tracking.' },
|
|
1261
|
-
],
|
|
1262
|
-
funnels: [
|
|
1263
|
-
{ name: 'Signup Flow', category: 'acquisition', steps: ['$page_view', '$signup', '$login'], reasoning: 'Page view to signup to first login.' },
|
|
1264
|
-
{ name: 'Engagement', category: 'engagement', steps: ['$page_view', 'scroll_depth', 'engaged_session'], reasoning: 'Page view to scroll to engaged session.' },
|
|
1265
|
-
],
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
if (intent) {
|
|
1269
|
-
const io = authDeps.intent.io || {
|
|
1270
|
-
print: (l) => console.log(l),
|
|
1271
|
-
prompt: installDeps.prompt,
|
|
1272
|
-
isPiped: !process.stdout.isTTY,
|
|
1273
|
-
};
|
|
1274
|
-
const nonInteractive = !!args.yes || io.isPiped || !!args.intentDryRun;
|
|
1275
|
-
const decision = await (0, install_intent_proposal_1.runProposal)({ intent, io, nonInteractive });
|
|
1276
|
-
intentRecord.accepted = {
|
|
1277
|
-
events: decision.accepted.events.length,
|
|
1278
|
-
funnels: decision.accepted.funnels.length,
|
|
1279
|
-
};
|
|
1280
|
-
// Phase 18.7 B — stash the raw ProposedEvent list for the
|
|
1281
|
-
// auto-instrument bridge below. Not part of the persisted summary
|
|
1282
|
-
// shape — marked with an underscore so TypeScript narrowing sees
|
|
1283
|
-
// the public `accepted` counts only.
|
|
1284
|
-
intentRecord._acceptedEvents = decision.accepted.events;
|
|
1285
|
-
if (decision.quit) {
|
|
1286
|
-
installDeps.log?.info('Intent proposal dismissed by user.');
|
|
1287
|
-
}
|
|
1288
|
-
else if (args.intentDryRun) {
|
|
1289
|
-
installDeps.log?.info(`Intent dry-run: would pre-seed ${decision.accepted.events.length} events and ${decision.accepted.funnels.length} funnels.`);
|
|
1290
|
-
}
|
|
1291
|
-
else if (decision.accepted.events.length === 0 && decision.accepted.funnels.length === 0) {
|
|
1292
|
-
installDeps.log?.info('No events or funnels accepted; skipping pre-seed.');
|
|
1293
|
-
}
|
|
1294
|
-
else {
|
|
1295
|
-
const res = await authDeps.intent.preSeed({
|
|
1296
|
-
siteId: selected.id,
|
|
1297
|
-
intent,
|
|
1298
|
-
accepted: decision.accepted,
|
|
1299
|
-
rejected: decision.rejected,
|
|
1300
|
-
});
|
|
1301
|
-
if (res.quotaExceeded) {
|
|
1302
|
-
const msg = 'Install quota exceeded during pre-seed. Upgrade at https://gurulu.io/settings/billing';
|
|
1303
|
-
installDeps.log?.error(msg);
|
|
1304
|
-
intentRecord.error = 'quota_exceeded';
|
|
1305
|
-
}
|
|
1306
|
-
else if (!res.ok) {
|
|
1307
|
-
installDeps.log?.warn(`Pre-seed failed: ${res.error || 'unknown error'}`);
|
|
1308
|
-
intentRecord.error = res.error || 'pre_seed_failed';
|
|
1309
|
-
}
|
|
1310
|
-
else {
|
|
1311
|
-
intentRecord.preSeeded = true;
|
|
1312
|
-
intentRecord.proposalId = res.proposalId;
|
|
1313
|
-
intentRecord.created = res.created;
|
|
1314
|
-
installDeps.log?.success(`Pre-seeded ${res.created?.goals || 0} goals, ${res.created?.funnels || 0} funnels, ${res.created?.milestones || 0} milestones.`);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
else if (args.skipIntent) {
|
|
1322
|
-
installDeps.log?.info('Intent discovery skipped (--skip-intent).');
|
|
1323
|
-
}
|
|
1324
|
-
// ---- Phase 18.7 B — Auto-instrumentation consent + intent-result file
|
|
1325
|
-
// When --auto-instrument is set AND an intent with accepted events exists,
|
|
1326
|
-
// serialize the decision to a tmp file so the agentic-install script can
|
|
1327
|
-
// read it during --apply. We also show a prominent consent prompt listing
|
|
1328
|
-
// the route files that will be touched (skippable with --yes).
|
|
1329
|
-
let autoInstrumentEnabled = !!args.autoInstrument;
|
|
1330
|
-
let intentResultPath;
|
|
1331
|
-
if (autoInstrumentEnabled) {
|
|
1332
|
-
// Sprint E1.2 — accept either `analyzed=true` OR a fallback intent so
|
|
1333
|
-
// auto-instrument still proceeds when the analyzer crashed and we fell
|
|
1334
|
-
// back to the built-in generic event set.
|
|
1335
|
-
const intentUsable = intentRecord.analyzed || intentRecord.analyze_status === 'fallback';
|
|
1336
|
-
if (!intentUsable || !intentRecord.accepted || intentRecord.accepted.events === 0) {
|
|
1337
|
-
installDeps.log?.warn('Auto-instrument requested but no accepted events from intent discovery; disabling.');
|
|
1338
|
-
autoInstrumentEnabled = false;
|
|
1339
|
-
}
|
|
1340
|
-
else {
|
|
1341
|
-
// Persist an `{ accepted: { events } }` envelope matching the
|
|
1342
|
-
// agentic-install script's reader shape. We resolve the raw ProposedEvent
|
|
1343
|
-
// list from the proposal IO if available — otherwise we fall back to
|
|
1344
|
-
// writing an empty envelope and rely on scan-driven route detection.
|
|
1345
|
-
const tmpDir = process.env.TMPDIR || '/tmp';
|
|
1346
|
-
const uuid = crypto.randomBytes(8).toString('hex');
|
|
1347
|
-
intentResultPath = path.join(tmpDir, `gurulu-intent-${uuid}.json`);
|
|
1348
|
-
try {
|
|
1349
|
-
fs.writeFileSync(intentResultPath, JSON.stringify({
|
|
1350
|
-
accepted: {
|
|
1351
|
-
events: intentRecord._acceptedEvents || [],
|
|
1352
|
-
},
|
|
1353
|
-
}, null, 2), 'utf8');
|
|
1354
|
-
}
|
|
1355
|
-
catch (err) {
|
|
1356
|
-
installDeps.log?.warn(`Failed to persist intent result: ${err.message}; disabling auto-instrument.`);
|
|
1357
|
-
autoInstrumentEnabled = false;
|
|
1358
|
-
intentResultPath = undefined;
|
|
1359
|
-
}
|
|
1360
|
-
// Consent prompt — unless --yes or --dry-run.
|
|
1361
|
-
if (autoInstrumentEnabled && !args.yes && !args.dryRun) {
|
|
1362
|
-
installDeps.log?.warn(`Auto-instrumentation will modify ${intentRecord.accepted.events} route file(s) to insert gurulu.track() calls.`);
|
|
1363
|
-
installDeps.log?.info(' Changes are backed up to .gurulu-backup/ and can be rolled back with `gurulu install --rollback`.');
|
|
1364
|
-
const answer = await installDeps.prompt(' Proceed with auto-instrumentation? [y/N]: ');
|
|
1365
|
-
if (!answer || !/^y/i.test(answer.trim())) {
|
|
1366
|
-
installDeps.log?.info('Auto-instrumentation declined.');
|
|
1367
|
-
autoInstrumentEnabled = false;
|
|
1368
|
-
intentResultPath = undefined;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
const runArgs = {
|
|
1374
|
-
...filled,
|
|
1375
|
-
autoInstrument: autoInstrumentEnabled,
|
|
1376
|
-
intentResultPath,
|
|
1377
|
-
// Sprint A fix A2 — propagate the active profile secret key so the core
|
|
1378
|
-
// flow can forward it to the agentic-install script as `--token`.
|
|
1379
|
-
authToken: authDeps.profile.secret_key,
|
|
1380
|
-
};
|
|
1381
|
-
// Run the core flow.
|
|
1382
|
-
const summary = await runInstallFlow(runArgs, installDeps, scriptsDir);
|
|
1383
|
-
summary.intent = intentRecord;
|
|
1384
|
-
if (autoInstrumentEnabled && !summary.instrumentation) {
|
|
1385
|
-
summary.instrumentation = {
|
|
1386
|
-
enabled: true,
|
|
1387
|
-
filesModified: 0,
|
|
1388
|
-
eventsInstrumented: 0,
|
|
1389
|
-
eventsSkipped: 0,
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
// Telemetry after each terminal state.
|
|
1393
|
-
const cliVersion = (() => {
|
|
1394
|
-
try {
|
|
1395
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1396
|
-
return require('../../package.json').version || '0.0.0';
|
|
1397
|
-
}
|
|
1398
|
-
catch {
|
|
1399
|
-
return '0.0.0';
|
|
1400
|
-
}
|
|
1401
|
-
})();
|
|
1402
|
-
const rh = repoHashOf(path.resolve(args.path || process.cwd()));
|
|
1403
|
-
const fw = summary.framework || 'unknown';
|
|
1404
|
-
if (args.dryRun) {
|
|
1405
|
-
await emitInstallTelemetry(authDeps, {
|
|
1406
|
-
siteId: selected.id,
|
|
1407
|
-
status: 'dry-run',
|
|
1408
|
-
framework: fw,
|
|
1409
|
-
cliVersion,
|
|
1410
|
-
repoHash: rh,
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
|
-
else if (summary.errors.length > 0) {
|
|
1414
|
-
await emitInstallTelemetry(authDeps, {
|
|
1415
|
-
siteId: selected.id,
|
|
1416
|
-
status: 'failed',
|
|
1417
|
-
framework: fw,
|
|
1418
|
-
cliVersion,
|
|
1419
|
-
repoHash: rh,
|
|
1420
|
-
errorMessage: summary.errors[0] || null,
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
else {
|
|
1424
|
-
const telemetry = await emitInstallTelemetry(authDeps, {
|
|
1425
|
-
siteId: selected.id,
|
|
1426
|
-
status: summary.verify?.ok ? 'verified' : 'applied',
|
|
1427
|
-
framework: fw,
|
|
1428
|
-
cliVersion,
|
|
1429
|
-
repoHash: rh,
|
|
1430
|
-
...(summary.instrumentation
|
|
1431
|
-
? {
|
|
1432
|
-
instrumentation: {
|
|
1433
|
-
filesModified: summary.instrumentation.filesModified,
|
|
1434
|
-
eventsInstrumented: summary.instrumentation.eventsInstrumented,
|
|
1435
|
-
eventsSkipped: summary.instrumentation.eventsSkipped,
|
|
1436
|
-
},
|
|
1437
|
-
}
|
|
1438
|
-
: {}),
|
|
1439
|
-
});
|
|
1440
|
-
if (telemetry.quotaExceeded) {
|
|
1441
|
-
const msg = 'Install quota exceeded for the current plan. Upgrade at https://gurulu.io/settings/billing';
|
|
1442
|
-
installDeps.log?.error(msg);
|
|
1443
|
-
summary.errors.push(msg);
|
|
1444
|
-
}
|
|
1445
|
-
if (summary.rolledBack) {
|
|
1446
|
-
await emitInstallTelemetry(authDeps, {
|
|
1447
|
-
siteId: selected.id,
|
|
1448
|
-
status: 'rolled-back',
|
|
1449
|
-
framework: fw,
|
|
1450
|
-
cliVersion,
|
|
1451
|
-
repoHash: rh,
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
return summary;
|
|
1456
|
-
}
|
|
1457
|
-
async function installCommand(args) {
|
|
1458
|
-
const scriptsDir = resolveScriptsDir();
|
|
1459
|
-
const deps = createDefaultDeps(scriptsDir);
|
|
1460
|
-
// Sprint A fix A3 — make `--verify` opt-in (default false). The verifier
|
|
1461
|
-
// (`scripts/gurulu-verify-install.lib.cjs:312`) hard-`require`s
|
|
1462
|
-
// `playwright-core`, which is NOT a runtime dependency of @gurulu/cli, so a
|
|
1463
|
-
// default-on verify aborts every fresh install with "Verify threw". Treat
|
|
1464
|
-
// verify as strictly opt-in: only `args.verify === true` runs it. We also
|
|
1465
|
-
// patch the CLI option default in `src/index.ts` so the flag matches.
|
|
1466
|
-
args = { ...args, verify: args.verify === true };
|
|
1467
|
-
// FA-1 P0-1 — top-level CI guard so the legacy unauthenticated path also
|
|
1468
|
-
// skips interactive prompts when run from CI. (The authenticated flow
|
|
1469
|
-
// re-checks inside `runAuthenticatedInstallFlow` for unit-test clarity.)
|
|
1470
|
-
if (!args.yes && detectCiEnv()) {
|
|
1471
|
-
console.log('[install] CI environment detected, defaulting to --yes');
|
|
1472
|
-
args = { ...args, yes: true };
|
|
1473
|
-
}
|
|
1474
|
-
// Legacy unauthenticated path: --site-id passed explicitly and no active profile.
|
|
1475
|
-
const profile = await (0, config_1.getActiveProfile)({ profile: args.profile });
|
|
1476
|
-
const legacyMode = !!args.siteId && !profile;
|
|
1477
|
-
if (legacyMode) {
|
|
1478
|
-
(0, ui_1.warn)('Legacy unauthenticated install. Run `gurulu login` for the full experience.');
|
|
1479
|
-
let siteId = args.siteId || process.env.GURULU_SITE_ID;
|
|
1480
|
-
let tenantId = args.tenantId || process.env.GURULU_TENANT_ID;
|
|
1481
|
-
if (!args.yes) {
|
|
1482
|
-
if (!siteId)
|
|
1483
|
-
siteId = await deps.prompt(' Site ID: ');
|
|
1484
|
-
if (!tenantId)
|
|
1485
|
-
tenantId = await deps.prompt(' Tenant ID: ');
|
|
1486
|
-
}
|
|
1487
|
-
// Sprint A fix A2 — legacy callers can supply a token via env so that
|
|
1488
|
-
// auto-instrument property extraction still works without a profile.
|
|
1489
|
-
const filled = {
|
|
1490
|
-
...args,
|
|
1491
|
-
siteId,
|
|
1492
|
-
tenantId,
|
|
1493
|
-
authToken: args.authToken || process.env.GURULU_API_KEY || process.env.GURULU_SECRET_KEY,
|
|
1494
|
-
};
|
|
1495
|
-
const summary = await runInstallFlow(filled, deps, scriptsDir);
|
|
1496
|
-
if (summary.errors.length > 0)
|
|
1497
|
-
process.exit(1);
|
|
1498
|
-
return;
|
|
1499
|
-
}
|
|
1500
|
-
if (!profile) {
|
|
1501
|
-
(0, ui_1.error)('Not logged in. Run `gurulu login` first, or provide --site-id for legacy mode.');
|
|
1502
|
-
process.exit(1);
|
|
1503
|
-
}
|
|
1504
|
-
// Authenticated: fetch /me, pick site, run flow, emit telemetry.
|
|
1505
|
-
let me;
|
|
1506
|
-
try {
|
|
1507
|
-
const res = await (0, api_client_1.cliApi)('/api/cli/me', { preloadedProfile: profile });
|
|
1508
|
-
me = await res.json();
|
|
1509
|
-
}
|
|
1510
|
-
catch (err) {
|
|
1511
|
-
(0, ui_1.error)(`Failed to load account: ${err.message}`);
|
|
1512
|
-
process.exit(1);
|
|
1513
|
-
}
|
|
1514
|
-
const sites = (me.sites || []).map((s) => ({
|
|
1515
|
-
id: s.id,
|
|
1516
|
-
name: s.name,
|
|
1517
|
-
domain: s.domain,
|
|
1518
|
-
publishableKey: s.publishableKey,
|
|
1519
|
-
}));
|
|
1520
|
-
const authDeps = {
|
|
1521
|
-
profile,
|
|
1522
|
-
sites,
|
|
1523
|
-
tenantId: me.tenant?.id || '',
|
|
1524
|
-
postVerify: async (body) => {
|
|
1525
|
-
const res = await (0, api_client_1.cliApi)('/api/cli/install/verify', {
|
|
1526
|
-
method: 'POST',
|
|
1527
|
-
preloadedProfile: profile,
|
|
1528
|
-
body: JSON.stringify(body),
|
|
1529
|
-
noExitOnError: true,
|
|
1530
|
-
});
|
|
1531
|
-
return { ok: res.ok, status: res.status };
|
|
1532
|
-
},
|
|
1533
|
-
intent: {
|
|
1534
|
-
analyze: async (signals) => {
|
|
1535
|
-
// FA-1 P0-3 — bound the LLM round-trip. Without this guard a hung
|
|
1536
|
-
// upstream stalls the whole install (Sprint E1.2 fallback never
|
|
1537
|
-
// fires until the request finally errors out). 30s mirrors the
|
|
1538
|
-
// tryLlmExtraction timeout in scripts/gurulu-agentic-install.mjs.
|
|
1539
|
-
try {
|
|
1540
|
-
const res = await (0, api_client_1.cliApi)('/api/cli/install/analyze-intent', {
|
|
1541
|
-
method: 'POST',
|
|
1542
|
-
preloadedProfile: profile,
|
|
1543
|
-
body: JSON.stringify({ signals }),
|
|
1544
|
-
noExitOnError: true,
|
|
1545
|
-
signal: AbortSignal.timeout(30_000),
|
|
1546
|
-
});
|
|
1547
|
-
const text = await res.text();
|
|
1548
|
-
const parsed = text ? JSON.parse(text) : {};
|
|
1549
|
-
if (!res.ok) {
|
|
1550
|
-
throw new Error((parsed && parsed.message) || `HTTP ${res.status}`);
|
|
1551
|
-
}
|
|
1552
|
-
return parsed.intent;
|
|
1553
|
-
}
|
|
1554
|
-
catch (err) {
|
|
1555
|
-
const e = err;
|
|
1556
|
-
if (e && (e.name === 'AbortError' || e.name === 'TimeoutError')) {
|
|
1557
|
-
console.warn('[install] LLM analyze-intent timeout — falling back to heuristic intent');
|
|
1558
|
-
throw new Error('analyze-intent timeout');
|
|
1559
|
-
}
|
|
1560
|
-
throw err;
|
|
1561
|
-
}
|
|
1562
|
-
},
|
|
1563
|
-
preSeed: async (body) => {
|
|
1564
|
-
const res = await (0, api_client_1.cliApi)('/api/cli/install/pre-seed', {
|
|
1565
|
-
method: 'POST',
|
|
1566
|
-
preloadedProfile: profile,
|
|
1567
|
-
body: JSON.stringify(body),
|
|
1568
|
-
noExitOnError: true,
|
|
1569
|
-
});
|
|
1570
|
-
if (res.status === 402 || res.status === 429) {
|
|
1571
|
-
return { ok: false, quotaExceeded: true };
|
|
1572
|
-
}
|
|
1573
|
-
const text = await res.text();
|
|
1574
|
-
const parsed = text ? JSON.parse(text) : {};
|
|
1575
|
-
if (!res.ok) {
|
|
1576
|
-
return { ok: false, error: (parsed && parsed.message) || `HTTP ${res.status}` };
|
|
1577
|
-
}
|
|
1578
|
-
return {
|
|
1579
|
-
ok: true,
|
|
1580
|
-
proposalId: parsed.proposalId,
|
|
1581
|
-
created: parsed.created,
|
|
1582
|
-
skipped: parsed.skipped,
|
|
1583
|
-
};
|
|
1584
|
-
},
|
|
1585
|
-
},
|
|
1586
|
-
};
|
|
1587
|
-
const summary = await runAuthenticatedInstallFlow(args, authDeps, deps, scriptsDir);
|
|
1588
|
-
if (summary.errors.length > 0)
|
|
1589
|
-
process.exit(1);
|
|
1590
|
-
}
|