@gurulu/cli 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/attribution.d.ts +22 -0
- package/dist/commands/attribution.js +111 -0
- package/dist/commands/conversion-paths.d.ts +19 -0
- package/dist/commands/conversion-paths.js +55 -0
- package/dist/commands/errors.d.ts +27 -0
- package/dist/commands/errors.js +121 -0
- package/dist/commands/identity.d.ts +13 -0
- package/dist/commands/identity.js +85 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/install.d.ts +5 -0
- package/dist/commands/install.js +186 -9
- package/dist/commands/releases.d.ts +17 -0
- package/dist/commands/releases.js +54 -0
- package/dist/commands/replay.d.ts +18 -0
- package/dist/commands/replay.js +64 -0
- package/dist/commands/skad.d.ts +18 -0
- package/dist/commands/skad.js +53 -0
- package/dist/frameworks/detect.d.ts +1 -1
- package/dist/frameworks/detect.js +60 -0
- package/dist/index.js +162 -2
- package/package.json +1 -1
- package/scripts/gurulu-agentic-install.lib.cjs +32 -4
- package/scripts/gurulu-agentic-install.mjs +10 -2
package/dist/commands/install.js
CHANGED
|
@@ -48,6 +48,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
48
48
|
};
|
|
49
49
|
})();
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.resolveWorkspaceRoot = resolveWorkspaceRoot;
|
|
51
52
|
exports.resolveScriptsDir = resolveScriptsDir;
|
|
52
53
|
exports.detectPackageManager = detectPackageManager;
|
|
53
54
|
exports.packageInstallArgs = packageInstallArgs;
|
|
@@ -68,6 +69,90 @@ const config_1 = require("../config");
|
|
|
68
69
|
const api_client_1 = require("../api-client");
|
|
69
70
|
const install_intent_proposal_1 = require("../install-intent-proposal");
|
|
70
71
|
// ---------------------------------------------------------------------------
|
|
72
|
+
// Sprint E1.5 — Workspace resolution.
|
|
73
|
+
//
|
|
74
|
+
// Many fresh repos are npm/pnpm/yarn workspaces (Turborepo, Nx, Bun, etc).
|
|
75
|
+
// Running `gurulu install` from the repo root is ambiguous: should we patch
|
|
76
|
+
// the `apps/web/` Next.js app, the `apps/admin/` dashboard, or one of the
|
|
77
|
+
// `packages/*` libraries? When package.json declares >=2 workspace globs we
|
|
78
|
+
// either (a) honour `--workspace=<path>` if set, or (b) prompt the user
|
|
79
|
+
// interactively. The resolved path becomes `repoRoot` for the rest of the
|
|
80
|
+
// install flow.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function expandWorkspaceGlobs(repoRoot, globs) {
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const g of globs) {
|
|
85
|
+
// Accept only the simple `dir/*` and bare `dir` forms — anything fancier
|
|
86
|
+
// (negation, `**`, alternation) falls through to the manual prompt.
|
|
87
|
+
if (g.includes('**') || g.startsWith('!'))
|
|
88
|
+
continue;
|
|
89
|
+
if (g.endsWith('/*')) {
|
|
90
|
+
const parent = path.join(repoRoot, g.slice(0, -2));
|
|
91
|
+
if (!fs.existsSync(parent))
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
for (const entry of fs.readdirSync(parent)) {
|
|
95
|
+
const abs = path.join(parent, entry);
|
|
96
|
+
if (fs.statSync(abs).isDirectory() &&
|
|
97
|
+
fs.existsSync(path.join(abs, 'package.json'))) {
|
|
98
|
+
out.push(abs);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* ignore */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const abs = path.join(repoRoot, g);
|
|
108
|
+
if (fs.existsSync(abs) && fs.existsSync(path.join(abs, 'package.json'))) {
|
|
109
|
+
out.push(abs);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
async function resolveWorkspaceRoot(repoRoot, args, deps) {
|
|
116
|
+
const pkgPath = path.join(repoRoot, 'package.json');
|
|
117
|
+
if (!fs.existsSync(pkgPath))
|
|
118
|
+
return repoRoot;
|
|
119
|
+
let pkg;
|
|
120
|
+
try {
|
|
121
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return repoRoot;
|
|
125
|
+
}
|
|
126
|
+
let globs = null;
|
|
127
|
+
if (Array.isArray(pkg.workspaces))
|
|
128
|
+
globs = pkg.workspaces;
|
|
129
|
+
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages))
|
|
130
|
+
globs = pkg.workspaces.packages;
|
|
131
|
+
if (!globs || globs.length === 0)
|
|
132
|
+
return repoRoot;
|
|
133
|
+
const candidates = expandWorkspaceGlobs(repoRoot, globs);
|
|
134
|
+
if (candidates.length <= 1)
|
|
135
|
+
return repoRoot;
|
|
136
|
+
if (args.workspace) {
|
|
137
|
+
const target = path.resolve(repoRoot, args.workspace);
|
|
138
|
+
if (candidates.some((c) => path.resolve(c) === target))
|
|
139
|
+
return target;
|
|
140
|
+
log(deps, 'warn', `--workspace=${args.workspace} not found among workspace packages.`);
|
|
141
|
+
}
|
|
142
|
+
if (args.yes) {
|
|
143
|
+
log(deps, 'warn', `Multiple workspaces detected (${candidates.length}); pass --workspace=<path> next time.`);
|
|
144
|
+
return repoRoot;
|
|
145
|
+
}
|
|
146
|
+
log(deps, 'info', 'Multiple workspaces detected:');
|
|
147
|
+
candidates.forEach((c, i) => log(deps, 'info', ` [${i + 1}] ${path.relative(repoRoot, c)}`));
|
|
148
|
+
const answer = (await deps.prompt(' Select workspace [1]: ')).trim() || '1';
|
|
149
|
+
const idx = parseInt(answer, 10) - 1;
|
|
150
|
+
if (Number.isFinite(idx) && idx >= 0 && idx < candidates.length) {
|
|
151
|
+
return candidates[idx];
|
|
152
|
+
}
|
|
153
|
+
return repoRoot;
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
71
156
|
// Script resolution
|
|
72
157
|
// ---------------------------------------------------------------------------
|
|
73
158
|
/**
|
|
@@ -140,6 +225,38 @@ function mergeEnvFile(repoRoot, framework, vars) {
|
|
|
140
225
|
return { file: filename, added, skipped };
|
|
141
226
|
}
|
|
142
227
|
// ---------------------------------------------------------------------------
|
|
228
|
+
// Sprint E2.2 — Fetch canonical install prompt from server endpoint.
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
async function fetchInstallPrompt(opts) {
|
|
231
|
+
const fallback = `I have Gurulu analytics installed. Site ID: ${opts.siteId}\n` +
|
|
232
|
+
`Analyze my codebase and add gurulu.track() calls to\n` +
|
|
233
|
+
`important user actions: signups, purchases, form submits,\n` +
|
|
234
|
+
`button clicks, and key conversions. Use the Gurulu MCP\n` +
|
|
235
|
+
`server (@gurulu/mcp-server) for live event verification.\n` +
|
|
236
|
+
`Docs: https://gurulu.io/docs/quick-start`;
|
|
237
|
+
if (!opts.authToken || !opts.siteId)
|
|
238
|
+
return fallback;
|
|
239
|
+
try {
|
|
240
|
+
const url = new URL('/api/cli/install/prompt', opts.ingestUrl);
|
|
241
|
+
url.searchParams.set('siteId', opts.siteId);
|
|
242
|
+
if (opts.framework)
|
|
243
|
+
url.searchParams.set('framework', opts.framework);
|
|
244
|
+
const res = await globalThis.fetch(url.toString(), {
|
|
245
|
+
headers: { authorization: `Bearer ${opts.authToken}` },
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok)
|
|
248
|
+
return fallback;
|
|
249
|
+
const body = await res.json();
|
|
250
|
+
if (body && typeof body.prompt === 'string' && body.prompt.length > 0) {
|
|
251
|
+
return body.prompt;
|
|
252
|
+
}
|
|
253
|
+
return fallback;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return fallback;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
143
260
|
// Default dependency implementations (real spawn + real fetch)
|
|
144
261
|
// ---------------------------------------------------------------------------
|
|
145
262
|
function createDefaultDeps(scriptsDir) {
|
|
@@ -251,7 +368,11 @@ function getEnvPrefix(framework) {
|
|
|
251
368
|
return ''; // Express, Fastify, NestJS don't need prefix
|
|
252
369
|
}
|
|
253
370
|
async function runInstallFlow(args, deps, scriptsDir) {
|
|
254
|
-
const
|
|
371
|
+
const initialRoot = path.resolve(args.path || process.cwd());
|
|
372
|
+
// Sprint E1.5 — workspace resolution. When the project is a multi-package
|
|
373
|
+
// workspace, narrow `repoRoot` to the chosen workspace package so scan,
|
|
374
|
+
// patches, .env merge, and npm install all target the right place.
|
|
375
|
+
const repoRoot = await resolveWorkspaceRoot(initialRoot, args, deps);
|
|
255
376
|
const summary = {
|
|
256
377
|
scan: null,
|
|
257
378
|
framework: null,
|
|
@@ -390,12 +511,27 @@ async function runInstallFlow(args, deps, scriptsDir) {
|
|
|
390
511
|
}
|
|
391
512
|
}
|
|
392
513
|
const applyRes = await deps.runNode(applyArgs);
|
|
514
|
+
// Sprint E1.1 — exit code 5 means the agentic-install script auto-rolled
|
|
515
|
+
// back due to an auto-instrument failure. Mark the summary as rolled back
|
|
516
|
+
// and skip the npm install / .env merge / ingest ping blocks so the user
|
|
517
|
+
// isn't left with a half-installed setup.
|
|
518
|
+
if (applyRes.code === 5) {
|
|
519
|
+
summary.rolledBack = true;
|
|
520
|
+
summary.partiallyInstalled = false;
|
|
521
|
+
log(deps, 'warn', '⚠ Install rolled back (auto-instrument failed; script tag reverted).');
|
|
522
|
+
return summary;
|
|
523
|
+
}
|
|
393
524
|
if (applyRes.code !== 0) {
|
|
394
525
|
const msg = `Apply failed (exit ${applyRes.code}): ${applyRes.stderr.trim()}`;
|
|
395
526
|
summary.errors.push(msg);
|
|
396
527
|
log(deps, 'error', msg);
|
|
397
528
|
return summary;
|
|
398
529
|
}
|
|
530
|
+
// Sprint E1.7 — record components that were successfully installed for
|
|
531
|
+
// partial-install diagnostics in the summary.
|
|
532
|
+
summary.installedComponents = ['script-tag', 'patch-log'];
|
|
533
|
+
if (args.autoInstrument)
|
|
534
|
+
summary.installedComponents.push('auto-instrument');
|
|
399
535
|
// Parse the machine-readable auto-instrument result line if present.
|
|
400
536
|
if (args.autoInstrument) {
|
|
401
537
|
const lines = (applyRes.stdout || '').split('\n');
|
|
@@ -427,6 +563,29 @@ async function runInstallFlow(args, deps, scriptsDir) {
|
|
|
427
563
|
}
|
|
428
564
|
}
|
|
429
565
|
log(deps, 'success', 'Patches applied.');
|
|
566
|
+
// Sprint E5.2 — wire a `postbuild` script for Next.js projects so the
|
|
567
|
+
// CLI's sourcemap uploader runs automatically after every `next build`.
|
|
568
|
+
// We only touch package.json when the framework is Next.js AND the user
|
|
569
|
+
// hasn't already defined a `postbuild` (idempotent).
|
|
570
|
+
if (detectedFw && detectedFw.startsWith('nextjs')) {
|
|
571
|
+
try {
|
|
572
|
+
const pkgPath = path.join(repoRoot, 'package.json');
|
|
573
|
+
if (fs.existsSync(pkgPath)) {
|
|
574
|
+
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
575
|
+
const pkgJson = JSON.parse(raw);
|
|
576
|
+
pkgJson.scripts = pkgJson.scripts || {};
|
|
577
|
+
if (!pkgJson.scripts.postbuild) {
|
|
578
|
+
pkgJson.scripts.postbuild =
|
|
579
|
+
'gurulu sourcemap upload --release ${npm_package_version} --dir .next/static/chunks';
|
|
580
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
|
|
581
|
+
log(deps, 'info', 'Added `postbuild` script for sourcemap upload.');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
log(deps, 'warn', `Could not add postbuild script: ${err.message}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
430
589
|
}
|
|
431
590
|
else {
|
|
432
591
|
if (args.autoInstrument) {
|
|
@@ -575,13 +734,21 @@ async function runInstallFlow(args, deps, scriptsDir) {
|
|
|
575
734
|
console.log('');
|
|
576
735
|
log(deps, 'info', ` ${(0, ui_1.dim)('Or paste this prompt into your AI assistant:')}`);
|
|
577
736
|
console.log('');
|
|
737
|
+
// Sprint E2.2 — fetch the canonical install prompt from the server so
|
|
738
|
+
// edits land in `src/lib/cli/install-prompt.ts` (the single source of
|
|
739
|
+
// truth) instead of being duplicated in three places. We fall back to a
|
|
740
|
+
// tiny inline body when the endpoint is unreachable or unauthenticated
|
|
741
|
+
// so legacy installs still see something useful.
|
|
742
|
+
const promptText = await fetchInstallPrompt({
|
|
743
|
+
siteId,
|
|
744
|
+
ingestUrl: args.ingestUrl || process.env.GURULU_INGEST_URL || 'https://gurulu.io',
|
|
745
|
+
authToken: args.authToken,
|
|
746
|
+
framework: summary.framework || undefined,
|
|
747
|
+
});
|
|
578
748
|
console.log((0, ui_1.dim)(' ┌─────────────────────────────────────────────'));
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
console.log((0, ui_1.dim)(' │ ') + 'button clicks, and key conversions. Use the Gurulu MCP');
|
|
583
|
-
console.log((0, ui_1.dim)(' │ ') + 'server (@gurulu/mcp-server) for live event verification.');
|
|
584
|
-
console.log((0, ui_1.dim)(' │ ') + 'Docs: https://gurulu.io/docs/quick-start');
|
|
749
|
+
for (const line of promptText.split('\n')) {
|
|
750
|
+
console.log((0, ui_1.dim)(' │ ') + line);
|
|
751
|
+
}
|
|
585
752
|
console.log((0, ui_1.dim)(' └─────────────────────────────────────────────'));
|
|
586
753
|
console.log('');
|
|
587
754
|
// Goals & funnels management
|
|
@@ -726,7 +893,13 @@ async function runAuthenticatedInstallFlow(args, authDeps, installDeps, scriptsD
|
|
|
726
893
|
installDeps.log?.warn(`Intent analyzer failed: ${err.message}`);
|
|
727
894
|
installDeps.log?.warn('⚠ Falling back to built-in generic event set.');
|
|
728
895
|
intentRecord.error = `analyze_failed:${err.message}`;
|
|
729
|
-
|
|
896
|
+
// Sprint E1.2 — distinguish "analyzer ran" from "fallback was used".
|
|
897
|
+
// `analyzed=false` so callers can detect that the LLM/heuristic
|
|
898
|
+
// analyzer never produced a real result; `analyze_status='fallback'`
|
|
899
|
+
// tells them the install still proceeded with a baked-in event set
|
|
900
|
+
// instead of failing outright.
|
|
901
|
+
intentRecord.analyzed = false;
|
|
902
|
+
intentRecord.analyze_status = 'fallback';
|
|
730
903
|
// Client-side fallback: produce a minimal generic intent so
|
|
731
904
|
// auto-instrument and pre-seed still work even when the API is down.
|
|
732
905
|
intent = {
|
|
@@ -819,7 +992,11 @@ async function runAuthenticatedInstallFlow(args, authDeps, installDeps, scriptsD
|
|
|
819
992
|
let autoInstrumentEnabled = !!args.autoInstrument;
|
|
820
993
|
let intentResultPath;
|
|
821
994
|
if (autoInstrumentEnabled) {
|
|
822
|
-
|
|
995
|
+
// Sprint E1.2 — accept either `analyzed=true` OR a fallback intent so
|
|
996
|
+
// auto-instrument still proceeds when the analyzer crashed and we fell
|
|
997
|
+
// back to the built-in generic event set.
|
|
998
|
+
const intentUsable = intentRecord.analyzed || intentRecord.analyze_status === 'fallback';
|
|
999
|
+
if (!intentUsable || !intentRecord.accepted || intentRecord.accepted.events === 0) {
|
|
823
1000
|
installDeps.log?.warn('Auto-instrument requested but no accepted events from intent discovery; disabling.');
|
|
824
1001
|
autoInstrumentEnabled = false;
|
|
825
1002
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprint E SE-B — `gurulu releases list`.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of MCP `gurulu.releases.list`. Returns recent releases with
|
|
5
|
+
* crash-free session %, adoption, and regression deltas.
|
|
6
|
+
*/
|
|
7
|
+
export interface ReleasesArgs {
|
|
8
|
+
action?: string;
|
|
9
|
+
site?: string;
|
|
10
|
+
range?: string;
|
|
11
|
+
environment?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
format?: string;
|
|
14
|
+
json?: boolean;
|
|
15
|
+
profile?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function releasesCommand(args: ReleasesArgs): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sprint E SE-B — `gurulu releases list`.
|
|
4
|
+
*
|
|
5
|
+
* Mirror of MCP `gurulu.releases.list`. Returns recent releases with
|
|
6
|
+
* crash-free session %, adoption, and regression deltas.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.releasesCommand = releasesCommand;
|
|
10
|
+
const api_client_1 = require("../api-client");
|
|
11
|
+
const ui_1 = require("../utils/ui");
|
|
12
|
+
async function releasesCommand(args) {
|
|
13
|
+
const action = args.action || 'list';
|
|
14
|
+
switch (action) {
|
|
15
|
+
case 'list':
|
|
16
|
+
return listCmd(args);
|
|
17
|
+
default:
|
|
18
|
+
(0, ui_1.error)(`Unknown releases action: ${action}`);
|
|
19
|
+
(0, ui_1.info)('Usage: gurulu releases list --site=<id> [--limit=20] [--range=7d|30d]');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function listCmd(args) {
|
|
24
|
+
if (!args.site) {
|
|
25
|
+
(0, ui_1.error)('Usage: gurulu releases list --site=<id> [--limit=20]');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const usp = new URLSearchParams();
|
|
29
|
+
usp.set('site', args.site);
|
|
30
|
+
if (args.range)
|
|
31
|
+
usp.set('range', args.range);
|
|
32
|
+
if (args.environment)
|
|
33
|
+
usp.set('environment', args.environment);
|
|
34
|
+
if (args.limit)
|
|
35
|
+
usp.set('limit', String(args.limit));
|
|
36
|
+
const body = await (0, api_client_1.cliApiJson)(`/api/cli/releases?${usp.toString()}`, {
|
|
37
|
+
profile: args.profile,
|
|
38
|
+
});
|
|
39
|
+
if (args.format === 'table') {
|
|
40
|
+
const rows = body.releases || [];
|
|
41
|
+
process.stdout.write(['VERSION', 'SESSIONS', 'CRASH_FREE_%', 'USERS', 'ERRORS'].join('\t') + '\n');
|
|
42
|
+
for (const r of rows) {
|
|
43
|
+
process.stdout.write([
|
|
44
|
+
r.version ?? '-',
|
|
45
|
+
r.totalSessions ?? 0,
|
|
46
|
+
r.crashFreeSessionRate?.toFixed?.(2) ?? r.crashFreeSessionRate ?? '-',
|
|
47
|
+
r.totalUsers ?? 0,
|
|
48
|
+
r.errorCount ?? 0,
|
|
49
|
+
].join('\t') + '\n');
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprint E SE-B — `gurulu replay list|get`.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of MCP `gurulu.replay.list` / `.get`. Hits new `/api/cli/replay/*`
|
|
5
|
+
* proxy endpoints with CLI-auth.
|
|
6
|
+
*/
|
|
7
|
+
export interface ReplayArgs {
|
|
8
|
+
action?: string;
|
|
9
|
+
site?: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
range?: string;
|
|
12
|
+
hasError?: boolean;
|
|
13
|
+
limit?: number;
|
|
14
|
+
format?: string;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
profile?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function replayCommand(args: ReplayArgs): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sprint E SE-B — `gurulu replay list|get`.
|
|
4
|
+
*
|
|
5
|
+
* Mirror of MCP `gurulu.replay.list` / `.get`. Hits new `/api/cli/replay/*`
|
|
6
|
+
* proxy endpoints with CLI-auth.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.replayCommand = replayCommand;
|
|
10
|
+
const api_client_1 = require("../api-client");
|
|
11
|
+
const ui_1 = require("../utils/ui");
|
|
12
|
+
async function replayCommand(args) {
|
|
13
|
+
const action = args.action || 'list';
|
|
14
|
+
switch (action) {
|
|
15
|
+
case 'list':
|
|
16
|
+
return listCmd(args);
|
|
17
|
+
case 'get':
|
|
18
|
+
return getCmd(args);
|
|
19
|
+
default:
|
|
20
|
+
(0, ui_1.error)(`Unknown replay action: ${action}`);
|
|
21
|
+
(0, ui_1.info)('Usage: gurulu replay [list|get] --site=<id> [--session-id=<id>]');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function listCmd(args) {
|
|
26
|
+
if (!args.site) {
|
|
27
|
+
(0, ui_1.error)('Usage: gurulu replay list --site=<id> [--range=7d|30d] [--limit=20]');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const usp = new URLSearchParams();
|
|
31
|
+
usp.set('site', args.site);
|
|
32
|
+
if (args.range)
|
|
33
|
+
usp.set('range', args.range);
|
|
34
|
+
if (args.hasError !== undefined)
|
|
35
|
+
usp.set('hasError', String(args.hasError));
|
|
36
|
+
if (args.limit)
|
|
37
|
+
usp.set('limit', String(args.limit));
|
|
38
|
+
const body = await (0, api_client_1.cliApiJson)(`/api/cli/replay?${usp.toString()}`, {
|
|
39
|
+
profile: args.profile,
|
|
40
|
+
});
|
|
41
|
+
if (args.format === 'table') {
|
|
42
|
+
const rows = body.sessions || [];
|
|
43
|
+
process.stdout.write(['SESSION', 'STARTED', 'DURATION', 'ERRORS', 'SEGMENTS'].join('\t') + '\n');
|
|
44
|
+
for (const s of rows) {
|
|
45
|
+
process.stdout.write([
|
|
46
|
+
s.sessionId ?? s.id ?? '-',
|
|
47
|
+
String(s.startedAt ?? '-'),
|
|
48
|
+
String(s.duration ?? 0),
|
|
49
|
+
s.hasError ? 'yes' : 'no',
|
|
50
|
+
String(s.segments ?? 0),
|
|
51
|
+
].join('\t') + '\n');
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
56
|
+
}
|
|
57
|
+
async function getCmd(args) {
|
|
58
|
+
if (!args.site || !args.sessionId) {
|
|
59
|
+
(0, ui_1.error)('Usage: gurulu replay get --site=<id> --session-id=<id>');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const body = await (0, api_client_1.cliApiJson)(`/api/cli/replay/${encodeURIComponent(args.sessionId)}?site=${encodeURIComponent(args.site)}`, { profile: args.profile });
|
|
63
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
64
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprint E SE-B — `gurulu skad postbacks`.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of MCP `gurulu.skad.list_postbacks`. Backend already exists at
|
|
5
|
+
* `/api/cli/skad` (Sprint C C9).
|
|
6
|
+
*/
|
|
7
|
+
export interface SkadArgs {
|
|
8
|
+
action?: string;
|
|
9
|
+
site?: string;
|
|
10
|
+
from?: string;
|
|
11
|
+
to?: string;
|
|
12
|
+
goal?: string;
|
|
13
|
+
limit?: number;
|
|
14
|
+
format?: string;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
profile?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function skadCommand(args: SkadArgs): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sprint E SE-B — `gurulu skad postbacks`.
|
|
4
|
+
*
|
|
5
|
+
* Mirror of MCP `gurulu.skad.list_postbacks`. Backend already exists at
|
|
6
|
+
* `/api/cli/skad` (Sprint C C9).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.skadCommand = skadCommand;
|
|
10
|
+
const api_client_1 = require("../api-client");
|
|
11
|
+
const ui_1 = require("../utils/ui");
|
|
12
|
+
async function skadCommand(args) {
|
|
13
|
+
const action = args.action || '';
|
|
14
|
+
switch (action) {
|
|
15
|
+
case 'postbacks':
|
|
16
|
+
return postbacksCmd(args);
|
|
17
|
+
default:
|
|
18
|
+
(0, ui_1.error)(`Unknown skad action: ${action}`);
|
|
19
|
+
(0, ui_1.info)('Usage: gurulu skad postbacks --site=<id> [--from=...] [--goal=$purchase] [--limit=50]');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function postbacksCmd(args) {
|
|
24
|
+
const usp = new URLSearchParams();
|
|
25
|
+
if (args.site)
|
|
26
|
+
usp.set('site', args.site);
|
|
27
|
+
if (args.from)
|
|
28
|
+
usp.set('from', args.from);
|
|
29
|
+
if (args.to)
|
|
30
|
+
usp.set('to', args.to);
|
|
31
|
+
if (args.goal)
|
|
32
|
+
usp.set('goal', args.goal);
|
|
33
|
+
if (args.limit)
|
|
34
|
+
usp.set('limit', String(args.limit));
|
|
35
|
+
const path = `/api/cli/skad${usp.toString() ? `?${usp.toString()}` : ''}`;
|
|
36
|
+
const body = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
|
|
37
|
+
if (args.format === 'table') {
|
|
38
|
+
const rows = body.postbacks || [];
|
|
39
|
+
process.stdout.write(['RECEIVED', 'NETWORK', 'GOAL', 'CV', 'WIN', 'VERIFIED'].join('\t') + '\n');
|
|
40
|
+
for (const p of rows) {
|
|
41
|
+
process.stdout.write([
|
|
42
|
+
String(p.received_at ?? '-'),
|
|
43
|
+
p.ad_network_id ?? '-',
|
|
44
|
+
p.resolved_goal ?? '-',
|
|
45
|
+
String(p.conversion_value ?? '-'),
|
|
46
|
+
p.did_win ? 'yes' : 'no',
|
|
47
|
+
p.signature_verified ? 'yes' : 'no',
|
|
48
|
+
].join('\t') + '\n');
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
53
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Framework = 'nextjs-app' | 'nextjs-pages' | 'react-vite' | 'react-cra' | 'vue3' | 'nuxt3' | 'svelte' | 'sveltekit' | 'astro' | 'express' | 'nestjs' | 'html' | 'react-native' | 'ios-swift' | 'android-kotlin' | 'flutter' | 'unknown';
|
|
1
|
+
export type Framework = 'nextjs-app' | 'nextjs-pages' | 'react-vite' | 'react-cra' | 'vue3' | 'nuxt3' | 'svelte' | 'sveltekit' | 'astro' | 'express' | 'fastify' | 'hono' | 'nestjs' | 'html' | 'react-native' | 'ios-swift' | 'android-kotlin' | 'flutter' | 'unknown';
|
|
2
2
|
export declare function detectFramework(projectDir: string): Framework;
|
|
3
3
|
export declare function getSetupSnippet(framework: Framework, siteId: string, token: string): {
|
|
4
4
|
file: string;
|
|
@@ -75,6 +75,10 @@ function detectFramework(projectDir) {
|
|
|
75
75
|
return 'react-vite';
|
|
76
76
|
if (deps['react-scripts'])
|
|
77
77
|
return 'react-cra';
|
|
78
|
+
if (deps['fastify'])
|
|
79
|
+
return 'fastify';
|
|
80
|
+
if (deps['hono'])
|
|
81
|
+
return 'hono';
|
|
78
82
|
if (deps['express'])
|
|
79
83
|
return 'express';
|
|
80
84
|
return 'unknown';
|
|
@@ -253,6 +257,60 @@ export function guruluMiddleware(req: Request, res: Response, next: NextFunction
|
|
|
253
257
|
}`,
|
|
254
258
|
instruction: 'Add app.use(guruluMiddleware) in your Express app',
|
|
255
259
|
};
|
|
260
|
+
case 'fastify':
|
|
261
|
+
return {
|
|
262
|
+
file: 'src/gurulu.ts',
|
|
263
|
+
code: `// Gurulu.io Server Analytics — Fastify plugin
|
|
264
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
265
|
+
|
|
266
|
+
const SITE_ID = '${siteId}';
|
|
267
|
+
const TOKEN = '${token}';
|
|
268
|
+
|
|
269
|
+
export async function guruluPlugin(fastify: FastifyInstance) {
|
|
270
|
+
fastify.addHook('onRequest', async (req: FastifyRequest, _reply: FastifyReply) => {
|
|
271
|
+
fetch('https://ingest.gurulu.io/api/events', {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
site_id: SITE_ID,
|
|
276
|
+
event: 'pageview',
|
|
277
|
+
url: req.url,
|
|
278
|
+
referrer: (req.headers.referer as string) || '',
|
|
279
|
+
user_agent: (req.headers['user-agent'] as string) || '',
|
|
280
|
+
ip: req.ip,
|
|
281
|
+
timestamp: new Date().toISOString(),
|
|
282
|
+
}),
|
|
283
|
+
}).catch(() => {});
|
|
284
|
+
});
|
|
285
|
+
}`,
|
|
286
|
+
instruction: 'Register with fastify.register(guruluPlugin) in your Fastify app',
|
|
287
|
+
};
|
|
288
|
+
case 'hono':
|
|
289
|
+
return {
|
|
290
|
+
file: 'src/gurulu.ts',
|
|
291
|
+
code: `// Gurulu.io Server Analytics — Hono middleware
|
|
292
|
+
import type { MiddlewareHandler } from 'hono';
|
|
293
|
+
|
|
294
|
+
const SITE_ID = '${siteId}';
|
|
295
|
+
const TOKEN = '${token}';
|
|
296
|
+
|
|
297
|
+
export const guruluMiddleware: MiddlewareHandler = async (c, next) => {
|
|
298
|
+
fetch('https://ingest.gurulu.io/api/events', {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
|
|
301
|
+
body: JSON.stringify({
|
|
302
|
+
site_id: SITE_ID,
|
|
303
|
+
event: 'pageview',
|
|
304
|
+
url: c.req.url,
|
|
305
|
+
referrer: c.req.header('referer') || '',
|
|
306
|
+
user_agent: c.req.header('user-agent') || '',
|
|
307
|
+
timestamp: new Date().toISOString(),
|
|
308
|
+
}),
|
|
309
|
+
}).catch(() => {});
|
|
310
|
+
await next();
|
|
311
|
+
};`,
|
|
312
|
+
instruction: 'Add app.use(guruluMiddleware) in your Hono app',
|
|
313
|
+
};
|
|
256
314
|
case 'nestjs':
|
|
257
315
|
return {
|
|
258
316
|
file: 'src/gurulu.middleware.ts',
|
|
@@ -372,6 +430,8 @@ function getFrameworkDisplayName(fw) {
|
|
|
372
430
|
'sveltekit': 'SvelteKit',
|
|
373
431
|
'astro': 'Astro',
|
|
374
432
|
'express': 'Express',
|
|
433
|
+
'fastify': 'Fastify',
|
|
434
|
+
'hono': 'Hono',
|
|
375
435
|
'nestjs': 'NestJS',
|
|
376
436
|
'html': 'HTML',
|
|
377
437
|
'react-native': 'React Native',
|