@gurulu/cli 0.4.3 → 0.4.5

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.
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * CLI@0.4.4 — `gurulu secrets` — list & rotate keys in the tenant credential vault.
4
+ *
5
+ * Subcommands:
6
+ * gurulu secrets list (key names only — never values)
7
+ * gurulu secrets rotate --key API_TOKEN (issues new value, keeps old in 24h grace window)
8
+ *
9
+ * TODO(Sprint K): backend endpoints below are not yet live. The CLI command
10
+ * surface is implemented now so customers can wire automation.
11
+ * - GET /api/cli/secrets/list -> { secrets: [{ key, lastRotatedAt, rotationStatus }] }
12
+ * - POST /api/cli/secrets/rotate { key } -> { key, newRevealedOnce: string, graceUntil: ISO }
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.secretsCommand = secretsCommand;
16
+ const config_1 = require("../config");
17
+ const api_client_1 = require("../api-client");
18
+ const ui_1 = require("../utils/ui");
19
+ const redact_1 = require("../utils/redact");
20
+ async function secretsCommand(args) {
21
+ // Defensive — never echo args wholesale in any future debug path.
22
+ if (process.env.GURULU_DEBUG === '1') {
23
+ console.log(`[debug] secrets args: ${JSON.stringify((0, redact_1.redactSensitiveArgs)({ ...args }))}`);
24
+ }
25
+ switch (args.action) {
26
+ case 'list':
27
+ return secretsList(args);
28
+ case 'rotate':
29
+ return secretsRotate(args);
30
+ default:
31
+ (0, ui_1.error)(`Unknown action: ${args.action}. Use: list, rotate`);
32
+ process.exit(1);
33
+ }
34
+ }
35
+ async function loadProfileOrExit(args) {
36
+ try {
37
+ return await (0, config_1.loadActiveProfile)({ profile: args.profile });
38
+ }
39
+ catch {
40
+ (0, ui_1.error)('Not authenticated. Run "gurulu login" first.');
41
+ process.exit(1);
42
+ }
43
+ }
44
+ function emitFallbackNotice(json) {
45
+ if (json)
46
+ return;
47
+ (0, ui_1.info)((0, ui_1.dim)('Note: backend endpoint not yet live (Sprint K candidate). Command-shape is stable.'));
48
+ }
49
+ // ── list ───────────────────────────────────────────────────────────────────
50
+ async function secretsList(args) {
51
+ const profile = await loadProfileOrExit(args);
52
+ try {
53
+ const res = await (0, api_client_1.cliApi)('/api/cli/secrets/list', { preloadedProfile: profile });
54
+ const data = await res.json().catch(() => ({}));
55
+ if (res.status === 404) {
56
+ emitFallbackNotice(args.json);
57
+ if (args.json)
58
+ console.log(JSON.stringify({ ok: false, pending: true, secrets: [] }));
59
+ else
60
+ (0, ui_1.info)('Pending: would list tenant secrets (key names only).');
61
+ return;
62
+ }
63
+ if (!res.ok) {
64
+ const msg = data.message || data.error || `HTTP ${res.status}`;
65
+ if (args.json)
66
+ console.log(JSON.stringify({ ok: false, error: msg }));
67
+ else
68
+ (0, ui_1.error)(msg);
69
+ process.exit(1);
70
+ }
71
+ if (args.json) {
72
+ console.log(JSON.stringify(data, null, 2));
73
+ return;
74
+ }
75
+ const secrets = data.secrets || [];
76
+ if (secrets.length === 0) {
77
+ (0, ui_1.info)('No secrets in this tenant vault.');
78
+ return;
79
+ }
80
+ console.log((0, ui_1.bold)('Secrets (values are never exposed):'));
81
+ for (const s of secrets) {
82
+ const rotated = s.lastRotatedAt ? (0, ui_1.dim)(` rotated ${s.lastRotatedAt}`) : '';
83
+ const status = s.rotationStatus ? (0, ui_1.dim)(` [${s.rotationStatus}]`) : '';
84
+ (0, ui_1.step)(`${s.key}${rotated}${status}`);
85
+ }
86
+ }
87
+ catch (err) {
88
+ if (args.json)
89
+ console.log(JSON.stringify({ ok: false, error: err.message }));
90
+ else
91
+ (0, ui_1.error)(`Failed: ${err.message}`);
92
+ process.exit(1);
93
+ }
94
+ }
95
+ // ── rotate ─────────────────────────────────────────────────────────────────
96
+ async function secretsRotate(args) {
97
+ if (!args.key) {
98
+ (0, ui_1.error)('Usage: gurulu secrets rotate --key <KEY_NAME>');
99
+ process.exit(1);
100
+ }
101
+ const profile = await loadProfileOrExit(args);
102
+ try {
103
+ const res = await (0, api_client_1.cliApi)('/api/cli/secrets/rotate', {
104
+ method: 'POST',
105
+ body: JSON.stringify({ key: args.key }),
106
+ preloadedProfile: profile,
107
+ });
108
+ const data = await res.json().catch(() => ({}));
109
+ if (res.status === 404) {
110
+ emitFallbackNotice(args.json);
111
+ if (args.json)
112
+ console.log(JSON.stringify({ ok: false, pending: true, key: args.key }));
113
+ else
114
+ (0, ui_1.info)(`Pending: would rotate ${args.key} (24h grace window).`);
115
+ return;
116
+ }
117
+ if (!res.ok) {
118
+ const msg = data.message || data.error || `HTTP ${res.status}`;
119
+ if (args.json)
120
+ console.log(JSON.stringify({ ok: false, error: msg }));
121
+ else
122
+ (0, ui_1.error)(msg);
123
+ process.exit(1);
124
+ }
125
+ if (args.json) {
126
+ console.log(JSON.stringify(data, null, 2));
127
+ return;
128
+ }
129
+ (0, ui_1.success)(`Rotated ${args.key}`);
130
+ if (data.newRevealedOnce) {
131
+ (0, ui_1.warn)('New value (shown ONCE — store it now, you cannot retrieve it again):');
132
+ console.log(` ${(0, ui_1.bold)(data.newRevealedOnce)}`);
133
+ }
134
+ if (data.graceUntil) {
135
+ (0, ui_1.info)(`Old value remains valid until: ${data.graceUntil}`);
136
+ }
137
+ }
138
+ catch (err) {
139
+ if (args.json)
140
+ console.log(JSON.stringify({ ok: false, error: err.message }));
141
+ else
142
+ (0, ui_1.error)(`Failed: ${err.message}`);
143
+ process.exit(1);
144
+ }
145
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * FA-1 P1-4 — `gurulu upgrade` command.
3
+ *
4
+ * Bumps installed @gurulu/* packages to the latest version published on npm.
5
+ * Reads current versions from `npm ls --depth=0 --json` so it works regardless
6
+ * of which package manager is in use, then dispatches to the same `pm`
7
+ * detection install.ts uses (npm/pnpm/yarn/bun) for the actual upgrade.
8
+ *
9
+ * Usage:
10
+ * gurulu upgrade # default: bump @gurulu/web
11
+ * gurulu upgrade --package=@gurulu/cli
12
+ * gurulu upgrade --all # bump @gurulu/cli + @gurulu/node + @gurulu/web
13
+ * gurulu upgrade --dry-run # show plan without executing
14
+ */
15
+ export interface UpgradeArgs {
16
+ package?: string;
17
+ all?: boolean;
18
+ dryRun?: boolean;
19
+ path?: string;
20
+ }
21
+ export declare function upgradeCommand(args: UpgradeArgs): Promise<void>;
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ /**
3
+ * FA-1 P1-4 — `gurulu upgrade` command.
4
+ *
5
+ * Bumps installed @gurulu/* packages to the latest version published on npm.
6
+ * Reads current versions from `npm ls --depth=0 --json` so it works regardless
7
+ * of which package manager is in use, then dispatches to the same `pm`
8
+ * detection install.ts uses (npm/pnpm/yarn/bun) for the actual upgrade.
9
+ *
10
+ * Usage:
11
+ * gurulu upgrade # default: bump @gurulu/web
12
+ * gurulu upgrade --package=@gurulu/cli
13
+ * gurulu upgrade --all # bump @gurulu/cli + @gurulu/node + @gurulu/web
14
+ * gurulu upgrade --dry-run # show plan without executing
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.upgradeCommand = upgradeCommand;
51
+ const path = __importStar(require("path"));
52
+ const child_process_1 = require("child_process");
53
+ const ui_1 = require("../utils/ui");
54
+ const install_1 = require("./install");
55
+ const DEFAULT_PACKAGES = ['@gurulu/cli', '@gurulu/node', '@gurulu/web'];
56
+ function runCmd(cmd, args, opts = {}) {
57
+ return new Promise((resolve) => {
58
+ const child = (0, child_process_1.spawn)(cmd, args, {
59
+ cwd: opts.cwd,
60
+ stdio: ['ignore', 'pipe', 'pipe'],
61
+ shell: process.platform === 'win32',
62
+ });
63
+ let stdout = '';
64
+ let stderr = '';
65
+ child.stdout.on('data', (c) => (stdout += c.toString()));
66
+ child.stderr.on('data', (c) => (stderr += c.toString()));
67
+ child.on('close', (code) => resolve({ code: code ?? 0, stdout, stderr }));
68
+ child.on('error', (err) => resolve({ code: 1, stdout, stderr: String(err) }));
69
+ });
70
+ }
71
+ async function getCurrentVersion(repoRoot, pkg) {
72
+ // `npm ls --depth=0 --json` works in any project regardless of which PM
73
+ // wrote the lockfile (npm reads node_modules directly). When the package
74
+ // is missing from node_modules the JSON is still valid; we just see no
75
+ // entry under `dependencies`.
76
+ const res = await runCmd('npm', ['ls', pkg, '--depth=0', '--json'], { cwd: repoRoot });
77
+ if (!res.stdout)
78
+ return null;
79
+ try {
80
+ const parsed = JSON.parse(res.stdout);
81
+ const dep = parsed.dependencies && parsed.dependencies[pkg];
82
+ if (dep && typeof dep.version === 'string')
83
+ return dep.version;
84
+ return null;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ async function getLatestVersion(pkg) {
91
+ const res = await runCmd('npm', ['view', pkg, 'version']);
92
+ if (res.code !== 0)
93
+ return null;
94
+ const v = res.stdout.trim();
95
+ return v || null;
96
+ }
97
+ function pmUpgradeArgs(pm, pkg) {
98
+ if (pm === 'pnpm')
99
+ return ['update', '--latest', pkg];
100
+ if (pm === 'yarn')
101
+ return ['upgrade', `${pkg}@latest`];
102
+ if (pm === 'bun')
103
+ return ['update', pkg];
104
+ return ['install', `${pkg}@latest`];
105
+ }
106
+ function compareSemver(a, b) {
107
+ // Returns -1 if a < b, 0 if equal, 1 if a > b. Strips any leading `v` and
108
+ // pre-release suffixes ("1.2.3-rc.1" → ["1","2","3"]).
109
+ const norm = (s) => s
110
+ .replace(/^v/, '')
111
+ .split('-')[0]
112
+ .split('.')
113
+ .map((n) => parseInt(n, 10) || 0);
114
+ const aa = norm(a);
115
+ const bb = norm(b);
116
+ for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
117
+ const x = aa[i] || 0;
118
+ const y = bb[i] || 0;
119
+ if (x < y)
120
+ return -1;
121
+ if (x > y)
122
+ return 1;
123
+ }
124
+ return 0;
125
+ }
126
+ async function upgradeCommand(args) {
127
+ const repoRoot = path.resolve(args.path || process.cwd());
128
+ const targets = args.all
129
+ ? DEFAULT_PACKAGES
130
+ : [args.package || '@gurulu/web'];
131
+ (0, ui_1.info)(`${(0, ui_1.bold)('gurulu upgrade')} — checking ${targets.length} package(s) in ${(0, ui_1.cyan)(repoRoot)}`);
132
+ const pm = (0, install_1.detectPackageManager)(repoRoot);
133
+ (0, ui_1.step)(`Package manager: ${(0, ui_1.bold)(pm)}`);
134
+ const plans = [];
135
+ for (const pkg of targets) {
136
+ const [current, latest] = await Promise.all([
137
+ getCurrentVersion(repoRoot, pkg),
138
+ getLatestVersion(pkg),
139
+ ]);
140
+ plans.push({ pkg, current, latest });
141
+ }
142
+ const toBump = [];
143
+ for (const p of plans) {
144
+ if (!p.latest) {
145
+ (0, ui_1.warn)(`${p.pkg}: could not resolve latest version (npm view failed).`);
146
+ continue;
147
+ }
148
+ if (!p.current) {
149
+ (0, ui_1.info)(`${p.pkg}: not installed — skipping (run \`gurulu install\` first).`);
150
+ continue;
151
+ }
152
+ const cmp = compareSemver(p.current, p.latest);
153
+ if (cmp >= 0) {
154
+ (0, ui_1.success)(`${p.pkg}: up-to-date (${p.current})`);
155
+ continue;
156
+ }
157
+ (0, ui_1.info)(`${p.pkg}: ${(0, ui_1.dim)(p.current)} → ${(0, ui_1.cyan)(p.latest)}`);
158
+ toBump.push(p);
159
+ }
160
+ if (toBump.length === 0) {
161
+ (0, ui_1.success)('All packages up-to-date.');
162
+ return;
163
+ }
164
+ if (args.dryRun) {
165
+ (0, ui_1.info)(`Dry-run: would upgrade ${toBump.length} package(s).`);
166
+ return;
167
+ }
168
+ let failures = 0;
169
+ for (const p of toBump) {
170
+ (0, ui_1.step)(`Upgrading ${p.pkg} via ${pm}...`);
171
+ const res = await runCmd(pm, pmUpgradeArgs(pm, p.pkg), { cwd: repoRoot });
172
+ if (res.code !== 0) {
173
+ failures++;
174
+ (0, ui_1.error)(`${pm} upgrade failed for ${p.pkg} (exit ${res.code}): ${res.stderr.trim()}`);
175
+ }
176
+ else {
177
+ (0, ui_1.success)(`${p.pkg} upgraded.`);
178
+ }
179
+ }
180
+ if (failures > 0) {
181
+ process.exit(1);
182
+ }
183
+ }
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");
@@ -33,6 +34,8 @@ const audit_1 = require("./commands/audit");
33
34
  // Goals & Funnels CRUD
34
35
  const goals_1 = require("./commands/goals");
35
36
  const funnels_1 = require("./commands/funnels");
37
+ // Sprint J-Heat HM10 — heatmap read surface
38
+ const heatmap_1 = require("./commands/heatmap");
36
39
  // Gurulu Chat — NL → SQL analytics
37
40
  const chat_1 = require("./commands/chat");
38
41
  // Error tracking — source map upload
@@ -48,9 +51,19 @@ const skad_1 = require("./commands/skad");
48
51
  const errors_1 = require("./commands/errors");
49
52
  const replay_1 = require("./commands/replay");
50
53
  const conversion_paths_1 = require("./commands/conversion-paths");
54
+ // CLI@0.4.4 — consent + secrets management
55
+ const consent_1 = require("./commands/consent");
56
+ const secrets_1 = require("./commands/secrets");
51
57
  (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
52
58
  .scriptName('gurulu')
53
59
  .option('profile', { type: 'string', describe: 'Use a specific profile (default: personal)' })
60
+ // FA-1 P0-5 — GDPR consent. Also honored via env vars: GURULU_TELEMETRY=off
61
+ // (or 0/false/no) and DO_NOT_TRACK=1, plus persisted choice in
62
+ // ~/.gurulu/config.json (`telemetry: false`).
63
+ .option('no-telemetry', {
64
+ type: 'boolean',
65
+ describe: 'Disable anonymous install telemetry for this run',
66
+ })
54
67
  .command('init', 'Set up Gurulu analytics in your project', (y) => y
55
68
  .option('site-id', { type: 'string', describe: 'Site ID' })
56
69
  .option('token', { type: 'string', describe: 'Site token' })
@@ -232,6 +245,10 @@ const conversion_paths_1 = require("./commands/conversion-paths");
232
245
  .option('skip-env', { type: 'boolean', describe: 'Skip .env file merge' })
233
246
  .option('yes', { type: 'boolean', alias: 'y', describe: 'Non-interactive (assume yes)' })
234
247
  .option('ingest-url', { type: 'string', describe: 'Override ingest base URL' })
248
+ // FA-1 P0-2 — self-hosted tracker tag override. When set, install.ts
249
+ // forwards this to the agentic-install script so the rendered script
250
+ // tag points at the customer's own asset host instead of gurulu.io/t.js.
251
+ .option('script-src', { type: 'string', describe: 'Override tracker tag <script src> URL (self-hosted)' })
235
252
  .option('verify', { type: 'boolean', default: false, describe: 'Live smoke test after install (requires playwright-core)' })
236
253
  .option('skip-intent', { type: 'boolean', describe: 'Skip install-time intent discovery' })
237
254
  .option('intent-dry-run', { type: 'boolean', describe: 'Show intent proposal without pre-seeding' })
@@ -268,6 +285,7 @@ const conversion_paths_1 = require("./commands/conversion-paths");
268
285
  skipEnv: args['skip-env'],
269
286
  yes: args.yes,
270
287
  ingestUrl: args['ingest-url'],
288
+ scriptSrc: args['script-src'],
271
289
  verify: args['skip-verify'] ? false : args.verify,
272
290
  profile: args.profile,
273
291
  skipIntent: args['skip-intent'],
@@ -277,6 +295,20 @@ const conversion_paths_1 = require("./commands/conversion-paths");
277
295
  autoProperties: args['auto-properties'],
278
296
  });
279
297
  })
298
+ .command(
299
+ // FA-1 P1-4 — `gurulu upgrade` bumps installed @gurulu/* packages to the
300
+ // latest version published on npm. Defaults to @gurulu/web; `--all` bumps
301
+ // cli + node + web in one pass.
302
+ 'upgrade [path]', 'Upgrade installed Gurulu packages to the latest npm version', (y) => y
303
+ .positional('path', { type: 'string', describe: 'Target project path (default: cwd)' })
304
+ .option('package', { type: 'string', describe: 'Package to upgrade (default: @gurulu/web)' })
305
+ .option('all', { type: 'boolean', describe: 'Upgrade @gurulu/cli + @gurulu/node + @gurulu/web' })
306
+ .option('dry-run', { type: 'boolean', describe: 'Show what would be upgraded' }), (args) => (0, upgrade_1.upgradeCommand)({
307
+ path: args.path,
308
+ package: args.package,
309
+ all: args.all,
310
+ dryRun: args['dry-run'],
311
+ }))
280
312
  .command('warehouse <action>', 'Warehouse exports (BigQuery)', (y) => y
281
313
  .positional('action', { type: 'string', describe: 'Action: export' })
282
314
  .option('tenant', { type: 'string', describe: 'Tenant id (forward-compat)' })
@@ -293,8 +325,11 @@ const conversion_paths_1 = require("./commands/conversion-paths");
293
325
  });
294
326
  })
295
327
  // ── Phase 19.5 W2 — read-surface subcommands ─────────────────────────
296
- .command('audiences <action> [target]', 'Manage audiences (list, show, create, update, delete)', (y) => y
297
- .positional('action', { type: 'string', describe: 'list | show | create | update | delete' })
328
+ .command('audiences <action> [target]', 'Manage audiences (list, show, create, update, delete, export)', (y) => y
329
+ .positional('action', {
330
+ type: 'string',
331
+ describe: 'list | show | create | update | delete | export',
332
+ })
298
333
  .positional('target', { type: 'string', describe: 'Audience name or id' })
299
334
  .option('site', { type: 'string', describe: 'Site ID' })
300
335
  .option('id', { type: 'string', describe: 'Audience ID (for update/delete)' })
@@ -304,6 +339,15 @@ const conversion_paths_1 = require("./commands/conversion-paths");
304
339
  .option('from-file', { type: 'string', describe: 'Load payload from JSON file' })
305
340
  .option('dry-run', { type: 'boolean', describe: 'Show what would be done' })
306
341
  .option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
342
+ // AU5 — export options
343
+ .option('format', {
344
+ type: 'string',
345
+ describe: 'Export format: csv | json (default csv)',
346
+ })
347
+ .option('output', {
348
+ type: 'string',
349
+ describe: 'Write export body to this file (default stdout)',
350
+ })
307
351
  .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, audiences_1.audiencesCommand)({
308
352
  action: args.action,
309
353
  target: args.target,
@@ -315,6 +359,8 @@ const conversion_paths_1 = require("./commands/conversion-paths");
315
359
  fromFile: args['from-file'],
316
360
  dryRun: args['dry-run'],
317
361
  yes: args.yes,
362
+ format: args.format,
363
+ output: args.output,
318
364
  json: args.json,
319
365
  profile: args.profile,
320
366
  }))
@@ -370,6 +416,10 @@ const conversion_paths_1 = require("./commands/conversion-paths");
370
416
  }))
371
417
  .command('insights <action>', 'View daily insights (today, history, weekly)', (y) => y
372
418
  .positional('action', { type: 'string', describe: 'today | history | weekly' })
419
+ // `--site` is accepted but ignored: insights are computed at tenant
420
+ // scope (across all sites). Accept it so the flag is consistent with
421
+ // the rest of the CLI and doesn't reject scripts that pass it.
422
+ .option('site', { type: 'string', describe: 'Site ID (accepted for consistency, currently tenant-scoped)' })
373
423
  .option('days', { type: 'number', describe: 'History window in days (1..30)' })
374
424
  .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, insights_1.insightsCommand)({
375
425
  action: args.action,
@@ -465,10 +515,44 @@ const conversion_paths_1 = require("./commands/conversion-paths");
465
515
  json: args.json,
466
516
  profile: args.profile,
467
517
  }))
468
- .command('identity <action> [sub]', 'Identity state + writes (decay stats | transfers list | cdc-sources list | identify | alias | merge)', (y) => y
518
+ .command('heatmap <action>', 'Inspect click + scroll heatmaps (list, export)', (y) => y
519
+ .positional('action', { type: 'string', describe: 'list | export' })
520
+ .option('site', { type: 'string', describe: 'Site name or id' })
521
+ .option('page', { type: 'string', describe: 'Page URL (for export)' })
522
+ .option('page-id', {
523
+ type: 'string',
524
+ describe: 'Virtual-page id (HM6, optional)',
525
+ })
526
+ .option('type', {
527
+ type: 'string',
528
+ choices: ['click', 'scroll'],
529
+ describe: 'Heatmap type for export (default click)',
530
+ })
531
+ .option('range', {
532
+ type: 'string',
533
+ choices: ['7d', '30d'],
534
+ describe: 'Lookback window (default 7d)',
535
+ })
536
+ .option('viewport', {
537
+ type: 'string',
538
+ choices: ['mobile', 'tablet', 'desktop'],
539
+ describe: 'Filter to a viewport bucket (HM9)',
540
+ })
541
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, heatmap_1.heatmapCommand)({
542
+ action: args.action,
543
+ site: args.site,
544
+ page: args.page,
545
+ pageId: args['page-id'],
546
+ type: args.type,
547
+ range: args.range,
548
+ viewport: args.viewport,
549
+ json: args.json,
550
+ profile: args.profile,
551
+ }))
552
+ .command('identity <action> [sub]', 'Identity state + writes (decay stats | transfers list | cdc-sources list | identify | alias | merge | bulk)', (y) => y
469
553
  .positional('action', {
470
554
  type: 'string',
471
- describe: 'decay | transfers | cdc-sources | identify | alias | merge',
555
+ describe: 'decay | transfers | cdc-sources | identify | alias | merge | bulk',
472
556
  })
473
557
  .positional('sub', { type: 'string', describe: 'Subaction (stats, list)' })
474
558
  .option('direction', { type: 'string', describe: 'outbound | inbound | all' })
@@ -484,6 +568,10 @@ const conversion_paths_1 = require("./commands/conversion-paths");
484
568
  .option('new-user-id', { type: 'string', describe: 'New canonical user id (alias)' })
485
569
  .option('canonical-id', { type: 'string', describe: 'Winner canonical profile id (merge)' })
486
570
  .option('duplicate-id', { type: 'string', describe: 'Loser canonical profile id (merge)' })
571
+ // bulk
572
+ .option('file', { type: 'string', describe: 'Input file path (bulk)' })
573
+ .option('format', { type: 'string', describe: 'csv | json (bulk; auto-detected from extension)' })
574
+ .option('resume-from', { type: 'number', describe: 'Skip first N records (bulk)' })
487
575
  .option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
488
576
  .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, identity_1.identityCommand)({
489
577
  action: args.action,
@@ -500,6 +588,9 @@ const conversion_paths_1 = require("./commands/conversion-paths");
500
588
  newUserId: args['new-user-id'],
501
589
  canonicalId: args['canonical-id'],
502
590
  duplicateId: args['duplicate-id'],
591
+ file: args.file,
592
+ format: args.format,
593
+ resumeFrom: args['resume-from'],
503
594
  yes: args.yes,
504
595
  json: args.json,
505
596
  profile: args.profile,
@@ -731,6 +822,32 @@ const conversion_paths_1 = require("./commands/conversion-paths");
731
822
  format: args.format,
732
823
  json: args.json,
733
824
  profile: args.profile,
825
+ }))
826
+ // ── CLI@0.4.4 — consent (per-user GDPR scopes) ───────────────────────
827
+ .command('consent <action>', 'Manage per-user consent (set, get, revoke, check)', (y) => y
828
+ .positional('action', { type: 'string', describe: 'set | get | revoke | check' })
829
+ .option('user-id', { type: 'string', describe: 'User ID' })
830
+ .option('scope', { type: 'string', describe: 'Consent scope (e.g. marketing_external, analytics)' })
831
+ .option('state', { type: 'string', choices: ['granted', 'denied'], describe: 'Consent state (for set)' })
832
+ .option('destination', { type: 'string', describe: 'Destination key (for check, e.g. capi)' })
833
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, consent_1.consentCommand)({
834
+ action: args.action,
835
+ userId: args['user-id'],
836
+ scope: args.scope,
837
+ state: args.state,
838
+ destination: args.destination,
839
+ json: args.json,
840
+ profile: args.profile,
841
+ }))
842
+ // ── CLI@0.4.4 — secrets vault (list, rotate) ─────────────────────────
843
+ .command('secrets <action>', 'Tenant credential vault (list, rotate)', (y) => y
844
+ .positional('action', { type: 'string', describe: 'list | rotate' })
845
+ .option('key', { type: 'string', describe: 'Secret key name (for rotate)' })
846
+ .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, secrets_1.secretsCommand)({
847
+ action: args.action,
848
+ key: args.key,
849
+ json: args.json,
850
+ profile: args.profile,
734
851
  }))
735
852
  .demandCommand(1, 'Run gurulu --help for available commands')
736
853
  .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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gurulu/cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Gurulu.io CLI — setup analytics in seconds",
5
5
  "bin": {
6
6
  "gurulu": "bin/gurulu.js"