@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.
@@ -14,5 +14,6 @@ export interface ChatArgs {
14
14
  showSql?: boolean;
15
15
  profile?: string;
16
16
  context?: string;
17
+ site?: string;
17
18
  }
18
19
  export declare function chatCommand(args: ChatArgs): Promise<void>;
@@ -54,6 +54,7 @@ async function askQuestion(question, args) {
54
54
  json: {
55
55
  question,
56
56
  context: args.context,
57
+ ...(args.site ? { siteId: args.site } : {}),
57
58
  },
58
59
  });
59
60
  }
@@ -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
- // Empty: no telemetry yet for the requested filter / definition. Without
252
- // this branch the report printed only its header users couldn't tell
253
- // success from "no data found".
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
- (0, ui_1.info)(`No telemetry yet for "${filterEvent}". Track it from your app and run verify again.`);
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 SITE_ID = '${siteId}';
239
- const TOKEN = '${token}';
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
- // Track server-side pageview
243
- fetch('https://ingest.gurulu.io/api/events', {
244
- method: 'POST',
245
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
246
- body: JSON.stringify({
247
- site_id: SITE_ID,
248
- event: 'pageview',
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
- ip: req.ip,
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 SITE_ID = '${siteId}';
267
- const TOKEN = '${token}';
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
- 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',
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
- ip: req.ip,
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 SITE_ID = '${siteId}';
295
- const TOKEN = '${token}';
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
- 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',
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
- timestamp: new Date().toISOString(),
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 SITE_ID = '${siteId}';
322
- const TOKEN = '${token}';
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
- fetch('https://ingest.gurulu.io/api/events', {
328
- method: 'POST',
329
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOKEN },
330
- body: JSON.stringify({
331
- site_id: SITE_ID,
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
- ip: req.ip,
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' }), (args) => (0, chat_1.chatCommand)({
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gurulu/cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Gurulu.io CLI — setup analytics in seconds",
5
5
  "bin": {
6
6
  "gurulu": "bin/gurulu.js"