@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/index.js CHANGED
@@ -41,6 +41,13 @@ const sourcemap_1 = require("./commands/sourcemap");
41
41
  const db_1 = require("./commands/db");
42
42
  // Sprint B Group VII B14 — live event stream
43
43
  const watch_1 = require("./commands/watch");
44
+ // Sprint E SE-A / SE-B — analytics + observability CLI surface
45
+ const attribution_1 = require("./commands/attribution");
46
+ const releases_1 = require("./commands/releases");
47
+ const skad_1 = require("./commands/skad");
48
+ const errors_1 = require("./commands/errors");
49
+ const replay_1 = require("./commands/replay");
50
+ const conversion_paths_1 = require("./commands/conversion-paths");
44
51
  (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
45
52
  .scriptName('gurulu')
46
53
  .option('profile', { type: 'string', describe: 'Use a specific profile (default: personal)' })
@@ -458,18 +465,42 @@ const watch_1 = require("./commands/watch");
458
465
  json: args.json,
459
466
  profile: args.profile,
460
467
  }))
461
- .command('identity <action> [sub]', 'View identity state (decay stats, transfers list, cdc-sources list)', (y) => y
462
- .positional('action', { type: 'string', describe: 'decay | transfers | cdc-sources' })
468
+ .command('identity <action> [sub]', 'Identity state + writes (decay stats | transfers list | cdc-sources list | identify | alias | merge)', (y) => y
469
+ .positional('action', {
470
+ type: 'string',
471
+ describe: 'decay | transfers | cdc-sources | identify | alias | merge',
472
+ })
463
473
  .positional('sub', { type: 'string', describe: 'Subaction (stats, list)' })
464
474
  .option('direction', { type: 'string', describe: 'outbound | inbound | all' })
465
475
  .option('status', { type: 'string', describe: 'Filter by transfer status' })
466
476
  .option('limit', { type: 'number', describe: 'Max rows' })
477
+ // identify / alias / merge
478
+ .option('site', { type: 'string', describe: 'Site ID (write actions)' })
479
+ .option('user-id', { type: 'string', describe: 'User ID (identify)' })
480
+ .option('email', { type: 'string', describe: 'Email (identify)' })
481
+ .option('phone', { type: 'string', describe: 'Phone (identify)' })
482
+ .option('traits', { type: 'string', describe: 'Traits as JSON object (identify)' })
483
+ .option('previous-user-id', { type: 'string', describe: 'Anonymous/old user id (alias)' })
484
+ .option('new-user-id', { type: 'string', describe: 'New canonical user id (alias)' })
485
+ .option('canonical-id', { type: 'string', describe: 'Winner canonical profile id (merge)' })
486
+ .option('duplicate-id', { type: 'string', describe: 'Loser canonical profile id (merge)' })
487
+ .option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
467
488
  .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, identity_1.identityCommand)({
468
489
  action: args.action,
469
490
  sub: args.sub,
470
491
  direction: args.direction,
471
492
  status: args.status,
472
493
  limit: args.limit,
494
+ site: args.site,
495
+ userId: args['user-id'],
496
+ email: args.email,
497
+ phone: args.phone,
498
+ traits: args.traits,
499
+ previousUserId: args['previous-user-id'],
500
+ newUserId: args['new-user-id'],
501
+ canonicalId: args['canonical-id'],
502
+ duplicateId: args['duplicate-id'],
503
+ yes: args.yes,
473
504
  json: args.json,
474
505
  profile: args.profile,
475
506
  }))
@@ -571,6 +602,135 @@ const watch_1 = require("./commands/watch");
571
602
  tail: args.tail,
572
603
  json: args.json,
573
604
  profile: args.profile,
605
+ }))
606
+ // ── Sprint E SE-A/B — analytics + observability CLI surface ──────────
607
+ .command('attribution <action>', 'Attribution analytics (report, compare, channels)', (y) => y
608
+ .positional('action', { type: 'string', describe: 'report | compare | channels' })
609
+ .option('site', { type: 'string', describe: 'Site ID' })
610
+ .option('from', { type: 'string', describe: 'ISO date (inclusive)' })
611
+ .option('to', { type: 'string', describe: 'ISO date (inclusive)' })
612
+ .option('range', { type: 'string', describe: '7d | 30d | 90d shorthand' })
613
+ .option('model', { type: 'string', describe: 'last-touch | first-touch | linear | time-decay | position-based | data-driven' })
614
+ .option('baseline-model', { type: 'string', describe: 'Baseline model id (compare)' })
615
+ .option('variant-model', { type: 'string', describe: 'Variant model id (compare)' })
616
+ .option('conversion-event', { type: 'string', describe: 'Conversion event (default $purchase)' })
617
+ .option('format', { type: 'string', describe: 'json | table' })
618
+ .option('json', { type: 'boolean', describe: 'JSON output (default)' }), (args) => (0, attribution_1.attributionCommand)({
619
+ action: args.action,
620
+ site: args.site,
621
+ from: args.from,
622
+ to: args.to,
623
+ range: args.range,
624
+ model: args.model,
625
+ baselineModel: args['baseline-model'],
626
+ variantModel: args['variant-model'],
627
+ conversionEvent: args['conversion-event'],
628
+ format: args.format,
629
+ json: args.json,
630
+ profile: args.profile,
631
+ }))
632
+ .command('releases <action>', 'Release health (list)', (y) => y
633
+ .positional('action', { type: 'string', describe: 'list' })
634
+ .option('site', { type: 'string', describe: 'Site ID' })
635
+ .option('range', { type: 'string', describe: '24h | 7d | 30d (default 7d)' })
636
+ .option('environment', { type: 'string', describe: 'Optional environment filter' })
637
+ .option('limit', { type: 'number', describe: 'Max rows (default 20)' })
638
+ .option('format', { type: 'string', describe: 'json | table' })
639
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, releases_1.releasesCommand)({
640
+ action: args.action,
641
+ site: args.site,
642
+ range: args.range,
643
+ environment: args.environment,
644
+ limit: args.limit,
645
+ format: args.format,
646
+ json: args.json,
647
+ profile: args.profile,
648
+ }))
649
+ .command('skad <action>', 'Apple SKAdNetwork postbacks (postbacks)', (y) => y
650
+ .positional('action', { type: 'string', describe: 'postbacks' })
651
+ .option('site', { type: 'string', describe: 'Site ID' })
652
+ .option('from', { type: 'string', describe: 'ISO ts or 7d/24h' })
653
+ .option('to', { type: 'string', describe: 'ISO ts (default now)' })
654
+ .option('goal', { type: 'string', describe: 'Filter by resolved goal' })
655
+ .option('limit', { type: 'number', describe: 'Max rows (1..500)' })
656
+ .option('format', { type: 'string', describe: 'json | table' })
657
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, skad_1.skadCommand)({
658
+ action: args.action,
659
+ site: args.site,
660
+ from: args.from,
661
+ to: args.to,
662
+ goal: args.goal,
663
+ limit: args.limit,
664
+ format: args.format,
665
+ json: args.json,
666
+ profile: args.profile,
667
+ }))
668
+ .command('errors <action>', 'Error tracking (list, detail, resolve, mute)', (y) => y
669
+ .positional('action', { type: 'string', describe: 'list | detail | resolve | mute | upload-sourcemap | upload-native-symbols' })
670
+ .option('site', { type: 'string', describe: 'Site ID' })
671
+ .option('fingerprint', { type: 'string', describe: 'Error group fingerprint' })
672
+ .option('level', { type: 'string', describe: 'error | warning | info' })
673
+ .option('resolved', { type: 'string', describe: 'true | false' })
674
+ .option('environment', { type: 'string', describe: 'Environment filter' })
675
+ .option('release-id', { type: 'string', describe: 'Filter to a release' })
676
+ .option('limit', { type: 'number', describe: 'Max rows' })
677
+ .option('duration', { type: 'string', describe: 'Mute duration (e.g. 24h, 7d)' })
678
+ .option('format', { type: 'string', describe: 'json | table' })
679
+ .option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
680
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, errors_1.errorsCommand)({
681
+ action: args.action,
682
+ site: args.site,
683
+ fingerprint: args.fingerprint,
684
+ level: args.level,
685
+ resolved: args.resolved,
686
+ environment: args.environment,
687
+ releaseId: args['release-id'],
688
+ limit: args.limit,
689
+ duration: args.duration,
690
+ format: args.format,
691
+ yes: args.yes,
692
+ json: args.json,
693
+ profile: args.profile,
694
+ }))
695
+ .command('replay <action>', 'Session replay (list, get)', (y) => y
696
+ .positional('action', { type: 'string', describe: 'list | get' })
697
+ .option('site', { type: 'string', describe: 'Site ID' })
698
+ .option('session-id', { type: 'string', describe: 'Session id (for get)' })
699
+ .option('range', { type: 'string', describe: '7d | 30d (default 7d)' })
700
+ .option('has-error', { type: 'boolean', describe: 'Filter to sessions with errors' })
701
+ .option('limit', { type: 'number', describe: 'Max rows' })
702
+ .option('format', { type: 'string', describe: 'json | table' })
703
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, replay_1.replayCommand)({
704
+ action: args.action,
705
+ site: args.site,
706
+ sessionId: args['session-id'],
707
+ range: args.range,
708
+ hasError: args['has-error'],
709
+ limit: args.limit,
710
+ format: args.format,
711
+ json: args.json,
712
+ profile: args.profile,
713
+ }))
714
+ .command('conversion-paths <action>', 'Conversion path analytics (list)', (y) => y
715
+ .positional('action', { type: 'string', describe: 'list' })
716
+ .option('site', { type: 'string', describe: 'Site ID' })
717
+ .option('from', { type: 'string', describe: 'ISO date' })
718
+ .option('to', { type: 'string', describe: 'ISO date' })
719
+ .option('range', { type: 'string', describe: '7d | 30d | 90d' })
720
+ .option('conversion-event', { type: 'string', describe: 'Conversion event' })
721
+ .option('limit', { type: 'number', describe: 'Max rows' })
722
+ .option('format', { type: 'string', describe: 'json | table' })
723
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, conversion_paths_1.conversionPathsCommand)({
724
+ action: args.action,
725
+ site: args.site,
726
+ from: args.from,
727
+ to: args.to,
728
+ range: args.range,
729
+ conversionEvent: args['conversion-event'],
730
+ limit: args.limit,
731
+ format: args.format,
732
+ json: args.json,
733
+ profile: args.profile,
574
734
  }))
575
735
  .demandCommand(1, 'Run gurulu --help for available commands')
576
736
  .strict()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gurulu/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Gurulu.io CLI — setup analytics in seconds",
5
5
  "bin": {
6
6
  "gurulu": "bin/gurulu.js"
@@ -380,16 +380,44 @@ function generateServerMap(scan /*, opts*/) {
380
380
  };
381
381
  }
382
382
 
383
+ /**
384
+ * Sprint E1.3b — singularize plural table/model names to canonical singular.
385
+ * The scan layer normalises mutations as `{ op: '<model>.<operation>' }` so we
386
+ * derive the operation from `m.op` when `m.operation` isn't present, and
387
+ * collapse common irregular plurals to their singular form so e.g. raw-SQL
388
+ * `INSERT INTO orders` and a Drizzle `pgTable('orders', ...)` both fold into
389
+ * the same `order` row in db-map.
390
+ */
391
+ const SINGULARIZE_IRREGULARS = {
392
+ users: 'user',
393
+ orders: 'order',
394
+ items: 'item',
395
+ };
396
+ function singularizeModelName(raw) {
397
+ const lower = String(raw || '').toLowerCase();
398
+ if (!lower) return lower;
399
+ if (Object.prototype.hasOwnProperty.call(SINGULARIZE_IRREGULARS, lower)) {
400
+ return SINGULARIZE_IRREGULARS[lower];
401
+ }
402
+ return lower;
403
+ }
404
+
383
405
  /** db-map.json — table → event emitter mapping from detected mutations. */
384
406
  function generateDbMap(scan, opts) {
385
407
  const byModel = new Map();
386
408
  for (const m of scan.mutations || []) {
387
- const name = String(m.model || '').trim();
388
- if (!name) continue;
389
- const op = cdcOp(m.operation);
409
+ const rawName = String(m.model || '').trim();
410
+ if (!rawName) continue;
411
+ const name = singularizeModelName(rawName);
412
+ // Sprint E1.3b — `scan.mutations` from gurulu-scan.lib.cjs normalises the
413
+ // operation as `m.op = "<model>.<operation>"` and drops `m.operation`.
414
+ // Derive the operation verb from whichever field is populated.
415
+ const operation =
416
+ m.operation || (typeof m.op === 'string' ? m.op.split('.').pop() : null);
417
+ const op = cdcOp(operation);
390
418
  if (!op) continue;
391
419
  if (!byModel.has(name)) byModel.set(name, new Set());
392
- const key = `${op}::${inferEventName(name, m.operation)}`;
420
+ const key = `${op}::${inferEventName(name, operation)}`;
393
421
  byModel.get(name).add(key);
394
422
  }
395
423
 
@@ -278,10 +278,13 @@ async function runPatchMode(repoRoot, args) {
278
278
  process.exit(1);
279
279
  }
280
280
  const framework = args.framework || 'auto';
281
- const { patcher, detection, error } = patches.resolvePatcher(repoRoot, framework);
281
+ const { patcher, detection, error, unsupportedFramework } = patches.resolvePatcher(repoRoot, framework);
282
282
  if (error) {
283
283
  process.stderr.write(`Error: ${error}\n`);
284
- process.exit(1);
284
+ // Sprint E1.6 — exit 6 specifically for mobile / not-yet-supported
285
+ // frameworks so the wrapping CLI can surface docs links instead of a
286
+ // generic install failure.
287
+ process.exit(unsupportedFramework ? 6 : 1);
285
288
  }
286
289
  if (!patcher) {
287
290
  process.stderr.write('No supported framework detected in ' + repoRoot + '\n');
@@ -412,6 +415,11 @@ async function runPatchMode(repoRoot, args) {
412
415
  `Auto-instrument failed (${reason}); rollback threw: ${err.stack || err.message || err}\n`,
413
416
  );
414
417
  }
418
+ // Sprint E1.1 — exit code 5 signals "patches rolled back" so install.ts
419
+ // can skip the npm-install / .env-merge / ingest-ping steps and surface
420
+ // the rollback to the user. The wrapping CLI converts this into a
421
+ // partially-installed state with `summary.rolledBack=true`.
422
+ process.exit(5);
415
423
  }
416
424
  if (args.autoInstrument) {
417
425
  const intentPath = args['intent-result'];