@gurulu/cli 0.4.1 → 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.
Files changed (36) hide show
  1. package/dist/commands/attribution.d.ts +22 -0
  2. package/dist/commands/attribution.js +111 -0
  3. package/dist/commands/conversion-paths.d.ts +19 -0
  4. package/dist/commands/conversion-paths.js +55 -0
  5. package/dist/commands/errors.d.ts +27 -0
  6. package/dist/commands/errors.js +121 -0
  7. package/dist/commands/identity.d.ts +13 -0
  8. package/dist/commands/identity.js +85 -1
  9. package/dist/commands/init.js +1 -1
  10. package/dist/commands/install.d.ts +5 -0
  11. package/dist/commands/install.js +186 -9
  12. package/dist/commands/releases.d.ts +17 -0
  13. package/dist/commands/releases.js +54 -0
  14. package/dist/commands/replay.d.ts +18 -0
  15. package/dist/commands/replay.js +64 -0
  16. package/dist/commands/skad.d.ts +18 -0
  17. package/dist/commands/skad.js +53 -0
  18. package/dist/frameworks/detect.d.ts +1 -1
  19. package/dist/frameworks/detect.js +60 -0
  20. package/dist/index.js +162 -2
  21. package/package.json +1 -1
  22. package/scripts/gurulu-agentic-install.lib.cjs +32 -4
  23. package/scripts/gurulu-agentic-install.mjs +60 -5
  24. package/scripts/patches/auto-instrument/ast-helper.cjs +158 -10
  25. package/scripts/patches/auto-instrument/astro.cjs +12 -6
  26. package/scripts/patches/auto-instrument/express.cjs +23 -8
  27. package/scripts/patches/auto-instrument/fastify.cjs +7 -3
  28. package/scripts/patches/auto-instrument/hono.cjs +20 -9
  29. package/scripts/patches/auto-instrument/nestjs.cjs +7 -3
  30. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +27 -9
  31. package/scripts/patches/auto-instrument/nextjs-pages.cjs +23 -10
  32. package/scripts/patches/auto-instrument/remix.cjs +7 -3
  33. package/scripts/patches/auto-instrument/sdk-helper-map.cjs +241 -0
  34. package/scripts/patches/auto-instrument/sveltekit.cjs +7 -3
  35. package/scripts/patches/auto-instrument/vue.cjs +7 -3
  36. package/scripts/patches/index.cjs +6 -0
@@ -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 repoRoot = path.resolve(args.path || process.cwd());
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
- console.log((0, ui_1.dim)(' │ ') + 'I have Gurulu analytics installed. Site ID: ' + (0, ui_1.cyan)(siteId));
580
- console.log((0, ui_1.dim)(' │ ') + 'Analyze my codebase and add gurulu.track() calls to');
581
- console.log((0, ui_1.dim)(' │ ') + 'important user actions: signups, purchases, form submits,');
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
- intentRecord.analyzed = true; // Mark as analyzed so auto-instrument proceeds
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
- if (!intentRecord.analyzed || !intentRecord.accepted || intentRecord.accepted.events === 0) {
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',