@gurulu/cli 0.4.2 → 0.4.4
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/README.md +24 -0
- package/dist/commands/attribution.d.ts +22 -0
- package/dist/commands/attribution.js +111 -0
- package/dist/commands/consent.d.ts +27 -0
- package/dist/commands/consent.js +233 -0
- package/dist/commands/conversion-paths.d.ts +19 -0
- package/dist/commands/conversion-paths.js +55 -0
- package/dist/commands/db.js +8 -0
- package/dist/commands/errors.d.ts +27 -0
- package/dist/commands/errors.js +121 -0
- package/dist/commands/identity.d.ts +16 -0
- package/dist/commands/identity.js +222 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/install.d.ts +21 -3
- package/dist/commands/install.js +562 -26
- 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/secrets.d.ts +19 -0
- package/dist/commands/secrets.js +145 -0
- package/dist/commands/skad.d.ts +18 -0
- package/dist/commands/skad.js +53 -0
- package/dist/commands/upgrade.d.ts +21 -0
- package/dist/commands/upgrade.js +183 -0
- package/dist/frameworks/detect.d.ts +1 -1
- package/dist/frameworks/detect.js +60 -0
- package/dist/index.js +225 -2
- package/dist/utils/redact.d.ts +14 -0
- package/dist/utils/redact.js +48 -0
- package/package.json +1 -1
- package/scripts/bootstrap-runtime-schema.mjs +7 -25
- package/scripts/gurulu-agentic-install.lib.cjs +32 -4
- package/scripts/gurulu-agentic-install.mjs +10 -2
|
@@ -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',
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const status_1 = require("./commands/status");
|
|
|
18
18
|
const doctor_1 = require("./commands/doctor");
|
|
19
19
|
const add_server_1 = require("./commands/add-server");
|
|
20
20
|
const install_1 = require("./commands/install");
|
|
21
|
+
const upgrade_1 = require("./commands/upgrade");
|
|
21
22
|
const warehouse_1 = require("./commands/warehouse");
|
|
22
23
|
// Phase 19.5 W2 — Read-surface subcommands
|
|
23
24
|
const audiences_1 = require("./commands/audiences");
|
|
@@ -41,9 +42,26 @@ const sourcemap_1 = require("./commands/sourcemap");
|
|
|
41
42
|
const db_1 = require("./commands/db");
|
|
42
43
|
// Sprint B Group VII B14 — live event stream
|
|
43
44
|
const watch_1 = require("./commands/watch");
|
|
45
|
+
// Sprint E SE-A / SE-B — analytics + observability CLI surface
|
|
46
|
+
const attribution_1 = require("./commands/attribution");
|
|
47
|
+
const releases_1 = require("./commands/releases");
|
|
48
|
+
const skad_1 = require("./commands/skad");
|
|
49
|
+
const errors_1 = require("./commands/errors");
|
|
50
|
+
const replay_1 = require("./commands/replay");
|
|
51
|
+
const conversion_paths_1 = require("./commands/conversion-paths");
|
|
52
|
+
// CLI@0.4.4 — consent + secrets management
|
|
53
|
+
const consent_1 = require("./commands/consent");
|
|
54
|
+
const secrets_1 = require("./commands/secrets");
|
|
44
55
|
(0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
45
56
|
.scriptName('gurulu')
|
|
46
57
|
.option('profile', { type: 'string', describe: 'Use a specific profile (default: personal)' })
|
|
58
|
+
// FA-1 P0-5 — GDPR consent. Also honored via env vars: GURULU_TELEMETRY=off
|
|
59
|
+
// (or 0/false/no) and DO_NOT_TRACK=1, plus persisted choice in
|
|
60
|
+
// ~/.gurulu/config.json (`telemetry: false`).
|
|
61
|
+
.option('no-telemetry', {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
describe: 'Disable anonymous install telemetry for this run',
|
|
64
|
+
})
|
|
47
65
|
.command('init', 'Set up Gurulu analytics in your project', (y) => y
|
|
48
66
|
.option('site-id', { type: 'string', describe: 'Site ID' })
|
|
49
67
|
.option('token', { type: 'string', describe: 'Site token' })
|
|
@@ -225,6 +243,10 @@ const watch_1 = require("./commands/watch");
|
|
|
225
243
|
.option('skip-env', { type: 'boolean', describe: 'Skip .env file merge' })
|
|
226
244
|
.option('yes', { type: 'boolean', alias: 'y', describe: 'Non-interactive (assume yes)' })
|
|
227
245
|
.option('ingest-url', { type: 'string', describe: 'Override ingest base URL' })
|
|
246
|
+
// FA-1 P0-2 — self-hosted tracker tag override. When set, install.ts
|
|
247
|
+
// forwards this to the agentic-install script so the rendered script
|
|
248
|
+
// tag points at the customer's own asset host instead of gurulu.io/t.js.
|
|
249
|
+
.option('script-src', { type: 'string', describe: 'Override tracker tag <script src> URL (self-hosted)' })
|
|
228
250
|
.option('verify', { type: 'boolean', default: false, describe: 'Live smoke test after install (requires playwright-core)' })
|
|
229
251
|
.option('skip-intent', { type: 'boolean', describe: 'Skip install-time intent discovery' })
|
|
230
252
|
.option('intent-dry-run', { type: 'boolean', describe: 'Show intent proposal without pre-seeding' })
|
|
@@ -261,6 +283,7 @@ const watch_1 = require("./commands/watch");
|
|
|
261
283
|
skipEnv: args['skip-env'],
|
|
262
284
|
yes: args.yes,
|
|
263
285
|
ingestUrl: args['ingest-url'],
|
|
286
|
+
scriptSrc: args['script-src'],
|
|
264
287
|
verify: args['skip-verify'] ? false : args.verify,
|
|
265
288
|
profile: args.profile,
|
|
266
289
|
skipIntent: args['skip-intent'],
|
|
@@ -270,6 +293,20 @@ const watch_1 = require("./commands/watch");
|
|
|
270
293
|
autoProperties: args['auto-properties'],
|
|
271
294
|
});
|
|
272
295
|
})
|
|
296
|
+
.command(
|
|
297
|
+
// FA-1 P1-4 — `gurulu upgrade` bumps installed @gurulu/* packages to the
|
|
298
|
+
// latest version published on npm. Defaults to @gurulu/web; `--all` bumps
|
|
299
|
+
// cli + node + web in one pass.
|
|
300
|
+
'upgrade [path]', 'Upgrade installed Gurulu packages to the latest npm version', (y) => y
|
|
301
|
+
.positional('path', { type: 'string', describe: 'Target project path (default: cwd)' })
|
|
302
|
+
.option('package', { type: 'string', describe: 'Package to upgrade (default: @gurulu/web)' })
|
|
303
|
+
.option('all', { type: 'boolean', describe: 'Upgrade @gurulu/cli + @gurulu/node + @gurulu/web' })
|
|
304
|
+
.option('dry-run', { type: 'boolean', describe: 'Show what would be upgraded' }), (args) => (0, upgrade_1.upgradeCommand)({
|
|
305
|
+
path: args.path,
|
|
306
|
+
package: args.package,
|
|
307
|
+
all: args.all,
|
|
308
|
+
dryRun: args['dry-run'],
|
|
309
|
+
}))
|
|
273
310
|
.command('warehouse <action>', 'Warehouse exports (BigQuery)', (y) => y
|
|
274
311
|
.positional('action', { type: 'string', describe: 'Action: export' })
|
|
275
312
|
.option('tenant', { type: 'string', describe: 'Tenant id (forward-compat)' })
|
|
@@ -458,18 +495,49 @@ const watch_1 = require("./commands/watch");
|
|
|
458
495
|
json: args.json,
|
|
459
496
|
profile: args.profile,
|
|
460
497
|
}))
|
|
461
|
-
.command('identity <action> [sub]', '
|
|
462
|
-
.positional('action', {
|
|
498
|
+
.command('identity <action> [sub]', 'Identity state + writes (decay stats | transfers list | cdc-sources list | identify | alias | merge | bulk)', (y) => y
|
|
499
|
+
.positional('action', {
|
|
500
|
+
type: 'string',
|
|
501
|
+
describe: 'decay | transfers | cdc-sources | identify | alias | merge | bulk',
|
|
502
|
+
})
|
|
463
503
|
.positional('sub', { type: 'string', describe: 'Subaction (stats, list)' })
|
|
464
504
|
.option('direction', { type: 'string', describe: 'outbound | inbound | all' })
|
|
465
505
|
.option('status', { type: 'string', describe: 'Filter by transfer status' })
|
|
466
506
|
.option('limit', { type: 'number', describe: 'Max rows' })
|
|
507
|
+
// identify / alias / merge
|
|
508
|
+
.option('site', { type: 'string', describe: 'Site ID (write actions)' })
|
|
509
|
+
.option('user-id', { type: 'string', describe: 'User ID (identify)' })
|
|
510
|
+
.option('email', { type: 'string', describe: 'Email (identify)' })
|
|
511
|
+
.option('phone', { type: 'string', describe: 'Phone (identify)' })
|
|
512
|
+
.option('traits', { type: 'string', describe: 'Traits as JSON object (identify)' })
|
|
513
|
+
.option('previous-user-id', { type: 'string', describe: 'Anonymous/old user id (alias)' })
|
|
514
|
+
.option('new-user-id', { type: 'string', describe: 'New canonical user id (alias)' })
|
|
515
|
+
.option('canonical-id', { type: 'string', describe: 'Winner canonical profile id (merge)' })
|
|
516
|
+
.option('duplicate-id', { type: 'string', describe: 'Loser canonical profile id (merge)' })
|
|
517
|
+
// bulk
|
|
518
|
+
.option('file', { type: 'string', describe: 'Input file path (bulk)' })
|
|
519
|
+
.option('format', { type: 'string', describe: 'csv | json (bulk; auto-detected from extension)' })
|
|
520
|
+
.option('resume-from', { type: 'number', describe: 'Skip first N records (bulk)' })
|
|
521
|
+
.option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
|
|
467
522
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, identity_1.identityCommand)({
|
|
468
523
|
action: args.action,
|
|
469
524
|
sub: args.sub,
|
|
470
525
|
direction: args.direction,
|
|
471
526
|
status: args.status,
|
|
472
527
|
limit: args.limit,
|
|
528
|
+
site: args.site,
|
|
529
|
+
userId: args['user-id'],
|
|
530
|
+
email: args.email,
|
|
531
|
+
phone: args.phone,
|
|
532
|
+
traits: args.traits,
|
|
533
|
+
previousUserId: args['previous-user-id'],
|
|
534
|
+
newUserId: args['new-user-id'],
|
|
535
|
+
canonicalId: args['canonical-id'],
|
|
536
|
+
duplicateId: args['duplicate-id'],
|
|
537
|
+
file: args.file,
|
|
538
|
+
format: args.format,
|
|
539
|
+
resumeFrom: args['resume-from'],
|
|
540
|
+
yes: args.yes,
|
|
473
541
|
json: args.json,
|
|
474
542
|
profile: args.profile,
|
|
475
543
|
}))
|
|
@@ -571,6 +639,161 @@ const watch_1 = require("./commands/watch");
|
|
|
571
639
|
tail: args.tail,
|
|
572
640
|
json: args.json,
|
|
573
641
|
profile: args.profile,
|
|
642
|
+
}))
|
|
643
|
+
// ── Sprint E SE-A/B — analytics + observability CLI surface ──────────
|
|
644
|
+
.command('attribution <action>', 'Attribution analytics (report, compare, channels)', (y) => y
|
|
645
|
+
.positional('action', { type: 'string', describe: 'report | compare | channels' })
|
|
646
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
647
|
+
.option('from', { type: 'string', describe: 'ISO date (inclusive)' })
|
|
648
|
+
.option('to', { type: 'string', describe: 'ISO date (inclusive)' })
|
|
649
|
+
.option('range', { type: 'string', describe: '7d | 30d | 90d shorthand' })
|
|
650
|
+
.option('model', { type: 'string', describe: 'last-touch | first-touch | linear | time-decay | position-based | data-driven' })
|
|
651
|
+
.option('baseline-model', { type: 'string', describe: 'Baseline model id (compare)' })
|
|
652
|
+
.option('variant-model', { type: 'string', describe: 'Variant model id (compare)' })
|
|
653
|
+
.option('conversion-event', { type: 'string', describe: 'Conversion event (default $purchase)' })
|
|
654
|
+
.option('format', { type: 'string', describe: 'json | table' })
|
|
655
|
+
.option('json', { type: 'boolean', describe: 'JSON output (default)' }), (args) => (0, attribution_1.attributionCommand)({
|
|
656
|
+
action: args.action,
|
|
657
|
+
site: args.site,
|
|
658
|
+
from: args.from,
|
|
659
|
+
to: args.to,
|
|
660
|
+
range: args.range,
|
|
661
|
+
model: args.model,
|
|
662
|
+
baselineModel: args['baseline-model'],
|
|
663
|
+
variantModel: args['variant-model'],
|
|
664
|
+
conversionEvent: args['conversion-event'],
|
|
665
|
+
format: args.format,
|
|
666
|
+
json: args.json,
|
|
667
|
+
profile: args.profile,
|
|
668
|
+
}))
|
|
669
|
+
.command('releases <action>', 'Release health (list)', (y) => y
|
|
670
|
+
.positional('action', { type: 'string', describe: 'list' })
|
|
671
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
672
|
+
.option('range', { type: 'string', describe: '24h | 7d | 30d (default 7d)' })
|
|
673
|
+
.option('environment', { type: 'string', describe: 'Optional environment filter' })
|
|
674
|
+
.option('limit', { type: 'number', describe: 'Max rows (default 20)' })
|
|
675
|
+
.option('format', { type: 'string', describe: 'json | table' })
|
|
676
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, releases_1.releasesCommand)({
|
|
677
|
+
action: args.action,
|
|
678
|
+
site: args.site,
|
|
679
|
+
range: args.range,
|
|
680
|
+
environment: args.environment,
|
|
681
|
+
limit: args.limit,
|
|
682
|
+
format: args.format,
|
|
683
|
+
json: args.json,
|
|
684
|
+
profile: args.profile,
|
|
685
|
+
}))
|
|
686
|
+
.command('skad <action>', 'Apple SKAdNetwork postbacks (postbacks)', (y) => y
|
|
687
|
+
.positional('action', { type: 'string', describe: 'postbacks' })
|
|
688
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
689
|
+
.option('from', { type: 'string', describe: 'ISO ts or 7d/24h' })
|
|
690
|
+
.option('to', { type: 'string', describe: 'ISO ts (default now)' })
|
|
691
|
+
.option('goal', { type: 'string', describe: 'Filter by resolved goal' })
|
|
692
|
+
.option('limit', { type: 'number', describe: 'Max rows (1..500)' })
|
|
693
|
+
.option('format', { type: 'string', describe: 'json | table' })
|
|
694
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, skad_1.skadCommand)({
|
|
695
|
+
action: args.action,
|
|
696
|
+
site: args.site,
|
|
697
|
+
from: args.from,
|
|
698
|
+
to: args.to,
|
|
699
|
+
goal: args.goal,
|
|
700
|
+
limit: args.limit,
|
|
701
|
+
format: args.format,
|
|
702
|
+
json: args.json,
|
|
703
|
+
profile: args.profile,
|
|
704
|
+
}))
|
|
705
|
+
.command('errors <action>', 'Error tracking (list, detail, resolve, mute)', (y) => y
|
|
706
|
+
.positional('action', { type: 'string', describe: 'list | detail | resolve | mute | upload-sourcemap | upload-native-symbols' })
|
|
707
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
708
|
+
.option('fingerprint', { type: 'string', describe: 'Error group fingerprint' })
|
|
709
|
+
.option('level', { type: 'string', describe: 'error | warning | info' })
|
|
710
|
+
.option('resolved', { type: 'string', describe: 'true | false' })
|
|
711
|
+
.option('environment', { type: 'string', describe: 'Environment filter' })
|
|
712
|
+
.option('release-id', { type: 'string', describe: 'Filter to a release' })
|
|
713
|
+
.option('limit', { type: 'number', describe: 'Max rows' })
|
|
714
|
+
.option('duration', { type: 'string', describe: 'Mute duration (e.g. 24h, 7d)' })
|
|
715
|
+
.option('format', { type: 'string', describe: 'json | table' })
|
|
716
|
+
.option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
|
|
717
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, errors_1.errorsCommand)({
|
|
718
|
+
action: args.action,
|
|
719
|
+
site: args.site,
|
|
720
|
+
fingerprint: args.fingerprint,
|
|
721
|
+
level: args.level,
|
|
722
|
+
resolved: args.resolved,
|
|
723
|
+
environment: args.environment,
|
|
724
|
+
releaseId: args['release-id'],
|
|
725
|
+
limit: args.limit,
|
|
726
|
+
duration: args.duration,
|
|
727
|
+
format: args.format,
|
|
728
|
+
yes: args.yes,
|
|
729
|
+
json: args.json,
|
|
730
|
+
profile: args.profile,
|
|
731
|
+
}))
|
|
732
|
+
.command('replay <action>', 'Session replay (list, get)', (y) => y
|
|
733
|
+
.positional('action', { type: 'string', describe: 'list | get' })
|
|
734
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
735
|
+
.option('session-id', { type: 'string', describe: 'Session id (for get)' })
|
|
736
|
+
.option('range', { type: 'string', describe: '7d | 30d (default 7d)' })
|
|
737
|
+
.option('has-error', { type: 'boolean', describe: 'Filter to sessions with errors' })
|
|
738
|
+
.option('limit', { type: 'number', describe: 'Max rows' })
|
|
739
|
+
.option('format', { type: 'string', describe: 'json | table' })
|
|
740
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, replay_1.replayCommand)({
|
|
741
|
+
action: args.action,
|
|
742
|
+
site: args.site,
|
|
743
|
+
sessionId: args['session-id'],
|
|
744
|
+
range: args.range,
|
|
745
|
+
hasError: args['has-error'],
|
|
746
|
+
limit: args.limit,
|
|
747
|
+
format: args.format,
|
|
748
|
+
json: args.json,
|
|
749
|
+
profile: args.profile,
|
|
750
|
+
}))
|
|
751
|
+
.command('conversion-paths <action>', 'Conversion path analytics (list)', (y) => y
|
|
752
|
+
.positional('action', { type: 'string', describe: 'list' })
|
|
753
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
754
|
+
.option('from', { type: 'string', describe: 'ISO date' })
|
|
755
|
+
.option('to', { type: 'string', describe: 'ISO date' })
|
|
756
|
+
.option('range', { type: 'string', describe: '7d | 30d | 90d' })
|
|
757
|
+
.option('conversion-event', { type: 'string', describe: 'Conversion event' })
|
|
758
|
+
.option('limit', { type: 'number', describe: 'Max rows' })
|
|
759
|
+
.option('format', { type: 'string', describe: 'json | table' })
|
|
760
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, conversion_paths_1.conversionPathsCommand)({
|
|
761
|
+
action: args.action,
|
|
762
|
+
site: args.site,
|
|
763
|
+
from: args.from,
|
|
764
|
+
to: args.to,
|
|
765
|
+
range: args.range,
|
|
766
|
+
conversionEvent: args['conversion-event'],
|
|
767
|
+
limit: args.limit,
|
|
768
|
+
format: args.format,
|
|
769
|
+
json: args.json,
|
|
770
|
+
profile: args.profile,
|
|
771
|
+
}))
|
|
772
|
+
// ── CLI@0.4.4 — consent (per-user GDPR scopes) ───────────────────────
|
|
773
|
+
.command('consent <action>', 'Manage per-user consent (set, get, revoke, check)', (y) => y
|
|
774
|
+
.positional('action', { type: 'string', describe: 'set | get | revoke | check' })
|
|
775
|
+
.option('user-id', { type: 'string', describe: 'User ID' })
|
|
776
|
+
.option('scope', { type: 'string', describe: 'Consent scope (e.g. marketing_external, analytics)' })
|
|
777
|
+
.option('state', { type: 'string', choices: ['granted', 'denied'], describe: 'Consent state (for set)' })
|
|
778
|
+
.option('destination', { type: 'string', describe: 'Destination key (for check, e.g. capi)' })
|
|
779
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, consent_1.consentCommand)({
|
|
780
|
+
action: args.action,
|
|
781
|
+
userId: args['user-id'],
|
|
782
|
+
scope: args.scope,
|
|
783
|
+
state: args.state,
|
|
784
|
+
destination: args.destination,
|
|
785
|
+
json: args.json,
|
|
786
|
+
profile: args.profile,
|
|
787
|
+
}))
|
|
788
|
+
// ── CLI@0.4.4 — secrets vault (list, rotate) ─────────────────────────
|
|
789
|
+
.command('secrets <action>', 'Tenant credential vault (list, rotate)', (y) => y
|
|
790
|
+
.positional('action', { type: 'string', describe: 'list | rotate' })
|
|
791
|
+
.option('key', { type: 'string', describe: 'Secret key name (for rotate)' })
|
|
792
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, secrets_1.secretsCommand)({
|
|
793
|
+
action: args.action,
|
|
794
|
+
key: args.key,
|
|
795
|
+
json: args.json,
|
|
796
|
+
profile: args.profile,
|
|
574
797
|
}))
|
|
575
798
|
.demandCommand(1, 'Run gurulu --help for available commands')
|
|
576
799
|
.strict()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI@0.4.4 — sensitive-arg redaction helper.
|
|
3
|
+
*
|
|
4
|
+
* Used before any debug/log/telemetry path that might serialize CLI args.
|
|
5
|
+
* Mask any key whose name contains password / token / secret / api-key / authorization
|
|
6
|
+
* (case-insensitive). Returns a shallow clone with offending values replaced
|
|
7
|
+
* by `***REDACTED***` (or `undefined` when the original was falsy, so the log
|
|
8
|
+
* does not imply a value was present).
|
|
9
|
+
*/
|
|
10
|
+
export declare function redactSensitiveArgs<T extends Record<string, unknown>>(args: T): T;
|
|
11
|
+
/**
|
|
12
|
+
* Convenience: produce a JSON string with sensitive fields redacted.
|
|
13
|
+
*/
|
|
14
|
+
export declare function safeStringifyArgs(args: Record<string, unknown>): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI@0.4.4 — sensitive-arg redaction helper.
|
|
4
|
+
*
|
|
5
|
+
* Used before any debug/log/telemetry path that might serialize CLI args.
|
|
6
|
+
* Mask any key whose name contains password / token / secret / api-key / authorization
|
|
7
|
+
* (case-insensitive). Returns a shallow clone with offending values replaced
|
|
8
|
+
* by `***REDACTED***` (or `undefined` when the original was falsy, so the log
|
|
9
|
+
* does not imply a value was present).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.redactSensitiveArgs = redactSensitiveArgs;
|
|
13
|
+
exports.safeStringifyArgs = safeStringifyArgs;
|
|
14
|
+
const SENSITIVE_KEY_RX = /(password|passwd|pwd|token|secret|api[_-]?key|authorization|auth[_-]?token|access[_-]?key|private[_-]?key|client[_-]?secret|session[_-]?token)/i;
|
|
15
|
+
function redactSensitiveArgs(args) {
|
|
16
|
+
if (!args || typeof args !== 'object')
|
|
17
|
+
return args;
|
|
18
|
+
const out = Array.isArray(args)
|
|
19
|
+
? [...args]
|
|
20
|
+
: { ...args };
|
|
21
|
+
for (const k of Object.keys(out)) {
|
|
22
|
+
const v = out[k];
|
|
23
|
+
if (SENSITIVE_KEY_RX.test(k)) {
|
|
24
|
+
out[k] = v ? '***REDACTED***' : undefined;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
// Recurse into nested objects (but not class instances / Buffers / Dates).
|
|
28
|
+
if (v &&
|
|
29
|
+
typeof v === 'object' &&
|
|
30
|
+
!Buffer.isBuffer(v) &&
|
|
31
|
+
!(v instanceof Date) &&
|
|
32
|
+
(Object.getPrototypeOf(v) === Object.prototype || Array.isArray(v))) {
|
|
33
|
+
out[k] = redactSensitiveArgs(v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convenience: produce a JSON string with sensitive fields redacted.
|
|
40
|
+
*/
|
|
41
|
+
function safeStringifyArgs(args) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(redactSensitiveArgs(args));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return '[unserializable]';
|
|
47
|
+
}
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -37,29 +37,18 @@ async function retry(fn, label) {
|
|
|
37
37
|
async function main() {
|
|
38
38
|
console.log('[bootstrap] Starting runtime schema bootstrap...');
|
|
39
39
|
|
|
40
|
-
// Step 1/
|
|
40
|
+
// Step 1/2: Prisma migrate deploy — see /scripts/bootstrap-runtime-schema.mjs
|
|
41
|
+
// for the FA-11 #3 / Sprint I-ops Faz 2 rationale.
|
|
41
42
|
if (process.env.POSTGRES_AUTO_MIGRATE !== 'false') {
|
|
42
|
-
console.log('\n[bootstrap] Step 1/
|
|
43
|
+
console.log('\n[bootstrap] Step 1/2: Prisma migrate deploy');
|
|
43
44
|
await retry(async () => {
|
|
44
|
-
|
|
45
|
-
exec('npx prisma migrate deploy');
|
|
46
|
-
} catch (err) {
|
|
47
|
-
if (
|
|
48
|
-
process.env.PRISMA_MIGRATE_FALLBACK_DB_PUSH !== 'false' &&
|
|
49
|
-
err.message?.includes('P3005')
|
|
50
|
-
) {
|
|
51
|
-
console.log('[bootstrap] P3005 detected, falling back to db push...');
|
|
52
|
-
exec('npx prisma db push --accept-data-loss');
|
|
53
|
-
} else {
|
|
54
|
-
throw err;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
45
|
+
exec('npx prisma migrate deploy');
|
|
57
46
|
}, 'migrate deploy');
|
|
58
47
|
}
|
|
59
48
|
|
|
60
|
-
// Step 2/
|
|
49
|
+
// Step 2/2: SQL hooks
|
|
61
50
|
if (process.env.POSTGRES_AUTO_MIGRATE !== 'false') {
|
|
62
|
-
console.log('\n[bootstrap] Step 2/
|
|
51
|
+
console.log('\n[bootstrap] Step 2/2: SQL hooks');
|
|
63
52
|
const hooksDir = join(process.cwd(), 'prisma', 'sql-hooks');
|
|
64
53
|
|
|
65
54
|
if (existsSync(hooksDir)) {
|
|
@@ -77,14 +66,7 @@ async function main() {
|
|
|
77
66
|
console.log('[bootstrap] No sql-hooks directory found, skipping.');
|
|
78
67
|
}
|
|
79
68
|
}
|
|
80
|
-
|
|
81
|
-
// Step 3/3: db push (catch drift)
|
|
82
|
-
if (process.env.POSTGRES_AUTO_MIGRATE !== 'false') {
|
|
83
|
-
console.log('\n[bootstrap] Step 3/3: Prisma db push (drift catch)');
|
|
84
|
-
await retry(async () => {
|
|
85
|
-
exec('npx prisma db push --accept-data-loss');
|
|
86
|
-
}, 'db push');
|
|
87
|
-
}
|
|
69
|
+
// Step 3/3 (db push drift catch) removed — drift is now a hard fail.
|
|
88
70
|
|
|
89
71
|
// ClickHouse migration
|
|
90
72
|
if (process.env.CLICKHOUSE_AUTO_MIGRATE !== 'false') {
|
|
@@ -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
|
|
388
|
-
if (!
|
|
389
|
-
const
|
|
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,
|
|
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
|
-
|
|
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'];
|