@gurulu/cli 0.4.6 → 0.4.7
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/chat.d.ts +1 -0
- package/dist/commands/chat.js +1 -0
- package/dist/commands/events.js +26 -4
- package/dist/commands/insights.js +13 -1
- package/dist/commands/setup.d.ts +21 -0
- package/dist/commands/setup.js +67 -0
- package/dist/frameworks/detect.js +62 -48
- package/dist/index.js +24 -1
- package/package.json +1 -1
package/dist/commands/chat.d.ts
CHANGED
package/dist/commands/chat.js
CHANGED
package/dist/commands/events.js
CHANGED
|
@@ -248,11 +248,33 @@ async function verifyCmd(args) {
|
|
|
248
248
|
console.log(`Unique event types: ${data.unique_types ?? 'N/A'}`);
|
|
249
249
|
}
|
|
250
250
|
else {
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
251
|
+
// Phase 33 P33-WA — three-state empty output instead of one generic
|
|
252
|
+
// "no telemetry yet" message. Helps the user tell apart "definition
|
|
253
|
+
// exists but never matched" vs "definition missing entirely" vs "no
|
|
254
|
+
// events at all on this site". Definition lookup is best-effort
|
|
255
|
+
// (extra HTTP call) so a 4xx falls through to the legacy message.
|
|
254
256
|
if (filterEvent) {
|
|
255
|
-
|
|
257
|
+
let definitionExists = null;
|
|
258
|
+
try {
|
|
259
|
+
const defs = await (0, api_client_1.cliApiJson)(`/api/cli/events/definitions?siteId=${encodeURIComponent(args.site)}`, { profile: args.profile });
|
|
260
|
+
definitionExists = Array.isArray(defs.definitions)
|
|
261
|
+
&& defs.definitions.some((d) => d.eventName === filterEvent);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// best-effort — fall through to legacy message
|
|
265
|
+
}
|
|
266
|
+
if (definitionExists === true) {
|
|
267
|
+
(0, ui_1.info)(`✓ "${filterEvent}" is registered (catalog has it) but no telemetry has matched in the last 24h.`);
|
|
268
|
+
(0, ui_1.info)(` Likely cause: SDK hasn't fired this event yet. Check your client code and run verify again.`);
|
|
269
|
+
}
|
|
270
|
+
else if (definitionExists === false) {
|
|
271
|
+
(0, ui_1.info)(`⚠ "${filterEvent}" is NOT registered yet (no CustomEventDefinition row).`);
|
|
272
|
+
(0, ui_1.info)(` Phase 32 auto-registers on first sight, but you can also run:`);
|
|
273
|
+
(0, ui_1.info)(` gurulu events define --site ${args.site} --event-name ${filterEvent} --display-name "..."`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
(0, ui_1.info)(`No telemetry yet for "${filterEvent}". Track it from your app and run verify again.`);
|
|
277
|
+
}
|
|
256
278
|
}
|
|
257
279
|
else {
|
|
258
280
|
(0, ui_1.info)('No telemetry data found for this site in the last 24h.');
|
|
@@ -20,6 +20,18 @@ async function insightsCommand(args) {
|
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
function formatHighlight(h) {
|
|
24
|
+
if (typeof h === 'string')
|
|
25
|
+
return h;
|
|
26
|
+
if (h && typeof h === 'object') {
|
|
27
|
+
const o = h;
|
|
28
|
+
const arrow = o.trend === 'up' ? '↑' : o.trend === 'down' ? '↓' : '→';
|
|
29
|
+
const value = typeof o.value === 'number' ? o.value.toLocaleString() : String(o.value ?? '');
|
|
30
|
+
const delta = typeof o.delta === 'number' ? ` (${o.delta >= 0 ? '+' : ''}${o.delta.toFixed(1)}% ${arrow})` : '';
|
|
31
|
+
return `${o.label ?? 'metric'}: ${value}${delta}`;
|
|
32
|
+
}
|
|
33
|
+
return String(h);
|
|
34
|
+
}
|
|
23
35
|
async function todayCmd(args) {
|
|
24
36
|
try {
|
|
25
37
|
const body = await (0, api_client_1.cliApiJson)('/api/cli/insights', {
|
|
@@ -35,7 +47,7 @@ async function todayCmd(args) {
|
|
|
35
47
|
if (Array.isArray(i.highlights)) {
|
|
36
48
|
process.stdout.write(`Highlights:\n`);
|
|
37
49
|
for (const h of i.highlights)
|
|
38
|
-
process.stdout.write(` - ${h}\n`);
|
|
50
|
+
process.stdout.write(` - ${formatHighlight(h)}\n`);
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
catch (err) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2026-05-05 DF-5 — `gurulu setup --vertical saas` zero-friction lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Tek komutta:
|
|
5
|
+
* - vertical event taxonomy'sini define et (idempotent)
|
|
6
|
+
* - default funnel'ları kur
|
|
7
|
+
* - acquisition + revenue + retention event'lerine goal bağla
|
|
8
|
+
*
|
|
9
|
+
* 23 ayrı komut yerine 1.
|
|
10
|
+
*/
|
|
11
|
+
export interface SetupArgs {
|
|
12
|
+
vertical?: string;
|
|
13
|
+
site?: string;
|
|
14
|
+
includeFunnels?: boolean;
|
|
15
|
+
includeGoals?: boolean;
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
json?: boolean;
|
|
18
|
+
yes?: boolean;
|
|
19
|
+
profile?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function setupCommand(args: SetupArgs): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 2026-05-05 DF-5 — `gurulu setup --vertical saas` zero-friction lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Tek komutta:
|
|
6
|
+
* - vertical event taxonomy'sini define et (idempotent)
|
|
7
|
+
* - default funnel'ları kur
|
|
8
|
+
* - acquisition + revenue + retention event'lerine goal bağla
|
|
9
|
+
*
|
|
10
|
+
* 23 ayrı komut yerine 1.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.setupCommand = setupCommand;
|
|
14
|
+
const api_client_1 = require("../api-client");
|
|
15
|
+
const ui_1 = require("../utils/ui");
|
|
16
|
+
async function setupCommand(args) {
|
|
17
|
+
const vertical = (args.vertical || '').toLowerCase().trim();
|
|
18
|
+
if (!vertical) {
|
|
19
|
+
(0, ui_1.error)('--vertical required (e.g. saas, ecommerce, marketplace, fintech, healthcare, education, media, igaming, generic)');
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!args.site) {
|
|
24
|
+
(0, ui_1.error)('--site required');
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const path = '/api/cli/setup/apply-template' + (args.dryRun ? '?dry_run=1' : '');
|
|
29
|
+
const body = {
|
|
30
|
+
siteId: args.site,
|
|
31
|
+
vertical,
|
|
32
|
+
includeFunnels: args.includeFunnels !== false,
|
|
33
|
+
includeGoals: args.includeGoals !== false,
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const res = await (0, api_client_1.cliApiJson)(path, {
|
|
37
|
+
profile: args.profile,
|
|
38
|
+
method: 'POST',
|
|
39
|
+
json: body,
|
|
40
|
+
});
|
|
41
|
+
if (args.json) {
|
|
42
|
+
process.stdout.write(JSON.stringify(res, null, 2) + '\n');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if ('dryRun' in res && res.dryRun) {
|
|
46
|
+
const a = res.after;
|
|
47
|
+
(0, ui_1.info)(`Dry-run preview — ${a.vertical} lifecycle taxonomy:`);
|
|
48
|
+
process.stdout.write(`\n Events (${a.events.length}): ${a.events.join(', ')}\n`);
|
|
49
|
+
process.stdout.write(` Funnels (${a.funnels.length}): ${a.funnels.join(', ')}\n`);
|
|
50
|
+
process.stdout.write(` Goals (${a.goals.length}): ${a.goals.join(', ')}\n\n`);
|
|
51
|
+
(0, ui_1.info)('Re-run without --dry-run to apply.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const r = res;
|
|
55
|
+
(0, ui_1.success)(`Applied ${r.vertical} lifecycle taxonomy to site ${args.site}`);
|
|
56
|
+
process.stdout.write(` Events: ${r.events.created.length} created, ${r.events.skipped.length} skipped\n`);
|
|
57
|
+
process.stdout.write(` Funnels: ${r.funnels.created.length} created, ${r.funnels.skipped.length} skipped\n`);
|
|
58
|
+
process.stdout.write(` Goals: ${r.goals.created.length} created, ${r.goals.skipped.length} skipped\n`);
|
|
59
|
+
if (r.events.created.length) {
|
|
60
|
+
process.stdout.write(`\n New events: ${r.events.created.join(', ')}\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
(0, ui_1.error)(`Setup failed: ${err?.message ?? String(err)}`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -232,26 +232,33 @@ export function initGurulu() {
|
|
|
232
232
|
case 'express':
|
|
233
233
|
return {
|
|
234
234
|
file: 'src/gurulu.ts',
|
|
235
|
-
code: `// Gurulu.io Server Analytics
|
|
235
|
+
code: `// Gurulu.io Server Analytics — Phase 32 P32-A2 fix
|
|
236
|
+
// Use the @gurulu/node SDK so retry, identity propagation, and the
|
|
237
|
+
// correct ingest endpoint (/api/ingest/v1/server) are handled for you.
|
|
238
|
+
// npm install @gurulu/node
|
|
239
|
+
import { Gurulu } from '@gurulu/node';
|
|
236
240
|
import type { Request, Response, NextFunction } from 'express';
|
|
237
241
|
|
|
238
|
-
const
|
|
239
|
-
|
|
242
|
+
export const gurulu = new Gurulu({
|
|
243
|
+
siteId: '${siteId}',
|
|
244
|
+
token: '${token}',
|
|
245
|
+
endpoint: 'https://gurulu.io',
|
|
246
|
+
});
|
|
240
247
|
|
|
241
248
|
export function guruluMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
// Identity propagation: read the anonymous_id the browser SDK set in a
|
|
250
|
+
// cookie / header so server-side events stitch onto the same user.
|
|
251
|
+
const anonymousId = (req.headers['x-gurulu-anonymous-id'] as string)
|
|
252
|
+
|| (req.cookies?._gurulu_id as string)
|
|
253
|
+
|| undefined;
|
|
254
|
+
gurulu.track({
|
|
255
|
+
eventName: 'pageview',
|
|
256
|
+
anonymousId,
|
|
257
|
+
properties: {
|
|
249
258
|
url: req.originalUrl,
|
|
250
259
|
referrer: req.headers.referer || '',
|
|
251
260
|
user_agent: req.headers['user-agent'] || '',
|
|
252
|
-
|
|
253
|
-
timestamp: new Date().toISOString(),
|
|
254
|
-
}),
|
|
261
|
+
},
|
|
255
262
|
}).catch(() => {});
|
|
256
263
|
next();
|
|
257
264
|
}`,
|
|
@@ -260,26 +267,28 @@ export function guruluMiddleware(req: Request, res: Response, next: NextFunction
|
|
|
260
267
|
case 'fastify':
|
|
261
268
|
return {
|
|
262
269
|
file: 'src/gurulu.ts',
|
|
263
|
-
code: `// Gurulu.io Server Analytics — Fastify plugin
|
|
270
|
+
code: `// Gurulu.io Server Analytics — Fastify plugin (Phase 32 P32-A2 fix)
|
|
271
|
+
// npm install @gurulu/node
|
|
272
|
+
import { Gurulu } from '@gurulu/node';
|
|
264
273
|
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
265
274
|
|
|
266
|
-
const
|
|
267
|
-
|
|
275
|
+
export const gurulu = new Gurulu({
|
|
276
|
+
siteId: '${siteId}',
|
|
277
|
+
token: '${token}',
|
|
278
|
+
endpoint: 'https://gurulu.io',
|
|
279
|
+
});
|
|
268
280
|
|
|
269
281
|
export async function guruluPlugin(fastify: FastifyInstance) {
|
|
270
282
|
fastify.addHook('onRequest', async (req: FastifyRequest, _reply: FastifyReply) => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
event: 'pageview',
|
|
283
|
+
const anonymousId = (req.headers['x-gurulu-anonymous-id'] as string) || undefined;
|
|
284
|
+
gurulu.track({
|
|
285
|
+
eventName: 'pageview',
|
|
286
|
+
anonymousId,
|
|
287
|
+
properties: {
|
|
277
288
|
url: req.url,
|
|
278
289
|
referrer: (req.headers.referer as string) || '',
|
|
279
290
|
user_agent: (req.headers['user-agent'] as string) || '',
|
|
280
|
-
|
|
281
|
-
timestamp: new Date().toISOString(),
|
|
282
|
-
}),
|
|
291
|
+
},
|
|
283
292
|
}).catch(() => {});
|
|
284
293
|
});
|
|
285
294
|
}`,
|
|
@@ -288,24 +297,27 @@ export async function guruluPlugin(fastify: FastifyInstance) {
|
|
|
288
297
|
case 'hono':
|
|
289
298
|
return {
|
|
290
299
|
file: 'src/gurulu.ts',
|
|
291
|
-
code: `// Gurulu.io Server Analytics — Hono middleware
|
|
300
|
+
code: `// Gurulu.io Server Analytics — Hono middleware (Phase 32 P32-A2 fix)
|
|
301
|
+
// npm install @gurulu/node
|
|
302
|
+
import { Gurulu } from '@gurulu/node';
|
|
292
303
|
import type { MiddlewareHandler } from 'hono';
|
|
293
304
|
|
|
294
|
-
const
|
|
295
|
-
|
|
305
|
+
export const gurulu = new Gurulu({
|
|
306
|
+
siteId: '${siteId}',
|
|
307
|
+
token: '${token}',
|
|
308
|
+
endpoint: 'https://gurulu.io',
|
|
309
|
+
});
|
|
296
310
|
|
|
297
311
|
export const guruluMiddleware: MiddlewareHandler = async (c, next) => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
event: 'pageview',
|
|
312
|
+
const anonymousId = c.req.header('x-gurulu-anonymous-id') || undefined;
|
|
313
|
+
gurulu.track({
|
|
314
|
+
eventName: 'pageview',
|
|
315
|
+
anonymousId,
|
|
316
|
+
properties: {
|
|
304
317
|
url: c.req.url,
|
|
305
318
|
referrer: c.req.header('referer') || '',
|
|
306
319
|
user_agent: c.req.header('user-agent') || '',
|
|
307
|
-
|
|
308
|
-
}),
|
|
320
|
+
},
|
|
309
321
|
}).catch(() => {});
|
|
310
322
|
await next();
|
|
311
323
|
};`,
|
|
@@ -314,28 +326,30 @@ export const guruluMiddleware: MiddlewareHandler = async (c, next) => {
|
|
|
314
326
|
case 'nestjs':
|
|
315
327
|
return {
|
|
316
328
|
file: 'src/gurulu.middleware.ts',
|
|
317
|
-
code: `// Gurulu.io Server Analytics Middleware
|
|
329
|
+
code: `// Gurulu.io Server Analytics Middleware (Phase 32 P32-A2 fix)
|
|
330
|
+
// npm install @gurulu/node
|
|
318
331
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
319
332
|
import { Request, Response, NextFunction } from 'express';
|
|
333
|
+
import { Gurulu } from '@gurulu/node';
|
|
320
334
|
|
|
321
|
-
const
|
|
322
|
-
|
|
335
|
+
const gurulu = new Gurulu({
|
|
336
|
+
siteId: '${siteId}',
|
|
337
|
+
token: '${token}',
|
|
338
|
+
endpoint: 'https://gurulu.io',
|
|
339
|
+
});
|
|
323
340
|
|
|
324
341
|
@Injectable()
|
|
325
342
|
export class GuruluMiddleware implements NestMiddleware {
|
|
326
343
|
use(req: Request, res: Response, next: NextFunction) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
event: 'pageview',
|
|
344
|
+
const anonymousId = (req.headers['x-gurulu-anonymous-id'] as string) || undefined;
|
|
345
|
+
gurulu.track({
|
|
346
|
+
eventName: 'pageview',
|
|
347
|
+
anonymousId,
|
|
348
|
+
properties: {
|
|
333
349
|
url: req.originalUrl,
|
|
334
350
|
referrer: req.headers.referer || '',
|
|
335
351
|
user_agent: req.headers['user-agent'] || '',
|
|
336
|
-
|
|
337
|
-
timestamp: new Date().toISOString(),
|
|
338
|
-
}),
|
|
352
|
+
},
|
|
339
353
|
}).catch(() => {});
|
|
340
354
|
next();
|
|
341
355
|
}
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,8 @@ const funnels_1 = require("./commands/funnels");
|
|
|
38
38
|
const heatmap_1 = require("./commands/heatmap");
|
|
39
39
|
// Gurulu Chat — NL → SQL analytics
|
|
40
40
|
const chat_1 = require("./commands/chat");
|
|
41
|
+
// 2026-05-05 DF-5 — zero-friction lifecycle setup
|
|
42
|
+
const setup_1 = require("./commands/setup");
|
|
41
43
|
// Error tracking — source map upload
|
|
42
44
|
const sourcemap_1 = require("./commands/sourcemap");
|
|
43
45
|
// Phase 21 — database connect
|
|
@@ -619,18 +621,39 @@ const secrets_1 = require("./commands/secrets");
|
|
|
619
621
|
format: args.format,
|
|
620
622
|
json: args.json,
|
|
621
623
|
profile: args.profile,
|
|
624
|
+
}))
|
|
625
|
+
// ── 2026-05-05 DF-5 — zero-friction lifecycle setup ──────────────────
|
|
626
|
+
.command('setup', 'Apply a vertical lifecycle taxonomy (events + funnels + goals) in one shot', (y) => y
|
|
627
|
+
.option('vertical', {
|
|
628
|
+
type: 'string',
|
|
629
|
+
describe: 'saas | ecommerce | marketplace | fintech | healthcare | education | media | igaming | generic',
|
|
630
|
+
})
|
|
631
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
632
|
+
.option('include-funnels', { type: 'boolean', default: true, describe: 'Also create funnels' })
|
|
633
|
+
.option('include-goals', { type: 'boolean', default: true, describe: 'Also create goals' })
|
|
634
|
+
.option('dry-run', { type: 'boolean', describe: 'Preview without writing' })
|
|
635
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, setup_1.setupCommand)({
|
|
636
|
+
vertical: args.vertical,
|
|
637
|
+
site: args.site,
|
|
638
|
+
includeFunnels: args['include-funnels'],
|
|
639
|
+
includeGoals: args['include-goals'],
|
|
640
|
+
dryRun: args['dry-run'],
|
|
641
|
+
json: args.json,
|
|
642
|
+
profile: args.profile,
|
|
622
643
|
}))
|
|
623
644
|
// ── Gurulu Chat — NL → SQL analytics ─────────────────────────────────
|
|
624
645
|
.command('chat [question]', 'Ask analytics questions in natural language (NL → SQL)', (y) => y
|
|
625
646
|
.positional('question', { type: 'string', describe: 'Question to ask (omit for REPL mode)' })
|
|
626
647
|
.option('json', { type: 'boolean', describe: 'Machine-readable JSON output' })
|
|
627
648
|
.option('show-sql', { type: 'boolean', describe: 'Also print the generated SQL' })
|
|
628
|
-
.option('context', { type: 'string', describe: 'Additional context for the query' })
|
|
649
|
+
.option('context', { type: 'string', describe: 'Additional context for the query' })
|
|
650
|
+
.option('site', { type: 'string', describe: 'Site ID (overrides profile default)' }), (args) => (0, chat_1.chatCommand)({
|
|
629
651
|
question: args.question,
|
|
630
652
|
json: args.json,
|
|
631
653
|
showSql: args['show-sql'],
|
|
632
654
|
context: args.context,
|
|
633
655
|
profile: args.profile,
|
|
656
|
+
site: args.site,
|
|
634
657
|
}))
|
|
635
658
|
// ── Error tracking — source map upload ────────────────────────────────
|
|
636
659
|
.command('sourcemap <action>', 'Upload source maps for error deobfuscation (upload). Supports web/server JS .map and native dSYM/ProGuard.', (y) => y
|