@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.
- package/README.md +24 -0
- package/dist/commands/consent.d.ts +27 -0
- package/dist/commands/consent.js +233 -0
- package/dist/commands/db.js +8 -0
- package/dist/commands/identity.d.ts +3 -0
- package/dist/commands/identity.js +138 -1
- package/dist/commands/install.d.ts +16 -3
- package/dist/commands/install.js +389 -30
- package/dist/commands/secrets.d.ts +19 -0
- package/dist/commands/secrets.js +145 -0
- package/dist/commands/upgrade.d.ts +21 -0
- package/dist/commands/upgrade.js +183 -0
- package/dist/index.js +65 -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
|
@@ -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
|
@@ -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') {
|