@gurulu/cli 0.4.3 → 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.
@@ -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");
@@ -48,9 +49,19 @@ const skad_1 = require("./commands/skad");
48
49
  const errors_1 = require("./commands/errors");
49
50
  const replay_1 = require("./commands/replay");
50
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");
51
55
  (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
52
56
  .scriptName('gurulu')
53
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
+ })
54
65
  .command('init', 'Set up Gurulu analytics in your project', (y) => y
55
66
  .option('site-id', { type: 'string', describe: 'Site ID' })
56
67
  .option('token', { type: 'string', describe: 'Site token' })
@@ -232,6 +243,10 @@ const conversion_paths_1 = require("./commands/conversion-paths");
232
243
  .option('skip-env', { type: 'boolean', describe: 'Skip .env file merge' })
233
244
  .option('yes', { type: 'boolean', alias: 'y', describe: 'Non-interactive (assume yes)' })
234
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)' })
235
250
  .option('verify', { type: 'boolean', default: false, describe: 'Live smoke test after install (requires playwright-core)' })
236
251
  .option('skip-intent', { type: 'boolean', describe: 'Skip install-time intent discovery' })
237
252
  .option('intent-dry-run', { type: 'boolean', describe: 'Show intent proposal without pre-seeding' })
@@ -268,6 +283,7 @@ const conversion_paths_1 = require("./commands/conversion-paths");
268
283
  skipEnv: args['skip-env'],
269
284
  yes: args.yes,
270
285
  ingestUrl: args['ingest-url'],
286
+ scriptSrc: args['script-src'],
271
287
  verify: args['skip-verify'] ? false : args.verify,
272
288
  profile: args.profile,
273
289
  skipIntent: args['skip-intent'],
@@ -277,6 +293,20 @@ const conversion_paths_1 = require("./commands/conversion-paths");
277
293
  autoProperties: args['auto-properties'],
278
294
  });
279
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
+ }))
280
310
  .command('warehouse <action>', 'Warehouse exports (BigQuery)', (y) => y
281
311
  .positional('action', { type: 'string', describe: 'Action: export' })
282
312
  .option('tenant', { type: 'string', describe: 'Tenant id (forward-compat)' })
@@ -465,10 +495,10 @@ const conversion_paths_1 = require("./commands/conversion-paths");
465
495
  json: args.json,
466
496
  profile: args.profile,
467
497
  }))
468
- .command('identity <action> [sub]', 'Identity state + writes (decay stats | transfers list | cdc-sources list | identify | alias | merge)', (y) => y
498
+ .command('identity <action> [sub]', 'Identity state + writes (decay stats | transfers list | cdc-sources list | identify | alias | merge | bulk)', (y) => y
469
499
  .positional('action', {
470
500
  type: 'string',
471
- describe: 'decay | transfers | cdc-sources | identify | alias | merge',
501
+ describe: 'decay | transfers | cdc-sources | identify | alias | merge | bulk',
472
502
  })
473
503
  .positional('sub', { type: 'string', describe: 'Subaction (stats, list)' })
474
504
  .option('direction', { type: 'string', describe: 'outbound | inbound | all' })
@@ -484,6 +514,10 @@ const conversion_paths_1 = require("./commands/conversion-paths");
484
514
  .option('new-user-id', { type: 'string', describe: 'New canonical user id (alias)' })
485
515
  .option('canonical-id', { type: 'string', describe: 'Winner canonical profile id (merge)' })
486
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)' })
487
521
  .option('yes', { type: 'boolean', alias: 'y', describe: 'Skip confirmation' })
488
522
  .option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, identity_1.identityCommand)({
489
523
  action: args.action,
@@ -500,6 +534,9 @@ const conversion_paths_1 = require("./commands/conversion-paths");
500
534
  newUserId: args['new-user-id'],
501
535
  canonicalId: args['canonical-id'],
502
536
  duplicateId: args['duplicate-id'],
537
+ file: args.file,
538
+ format: args.format,
539
+ resumeFrom: args['resume-from'],
503
540
  yes: args.yes,
504
541
  json: args.json,
505
542
  profile: args.profile,
@@ -731,6 +768,32 @@ const conversion_paths_1 = require("./commands/conversion-paths");
731
768
  format: args.format,
732
769
  json: args.json,
733
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,
734
797
  }))
735
798
  .demandCommand(1, 'Run gurulu --help for available commands')
736
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gurulu/cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Gurulu.io CLI — setup analytics in seconds",
5
5
  "bin": {
6
6
  "gurulu": "bin/gurulu.js"
@@ -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/3: Prisma migrate deploy
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/3: Prisma migrate deploy');
43
+ console.log('\n[bootstrap] Step 1/2: Prisma migrate deploy');
43
44
  await retry(async () => {
44
- try {
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/3: SQL hooks
49
+ // Step 2/2: SQL hooks
61
50
  if (process.env.POSTGRES_AUTO_MIGRATE !== 'false') {
62
- console.log('\n[bootstrap] Step 2/3: SQL hooks');
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') {