@gurulu/cli 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +6 -0
- package/dist/api-client.js +25 -0
- package/dist/commands/auth.js +41 -6
- package/dist/commands/doctor.js +57 -15
- package/dist/commands/events.js +2 -2
- package/dist/commands/whoami.js +4 -1
- package/dist/index.js +5 -5
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface CliApiOptions extends RequestInit {
|
|
|
12
12
|
preloadedProfile?: ActiveProfile;
|
|
13
13
|
/** Disable process.exit for tests. */
|
|
14
14
|
noExitOnError?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Surface raw error bodies on 5xx instead of the generic "temporarily
|
|
17
|
+
* unavailable" message. Caller should pass true when --json / --show-sql
|
|
18
|
+
* / any verbose flag is set. GURULU_DEBUG=1 also forces verbose globally.
|
|
19
|
+
*/
|
|
20
|
+
verbose?: boolean;
|
|
15
21
|
}
|
|
16
22
|
export declare class CliApiError extends Error {
|
|
17
23
|
status: number;
|
package/dist/api-client.js
CHANGED
|
@@ -129,6 +129,31 @@ async function cliApi(path, init = {}) {
|
|
|
129
129
|
fatal(`Rate limited by Gurulu${retry ? ` — try again in ${retry}s` : ''}. Try again shortly.`, init);
|
|
130
130
|
}
|
|
131
131
|
if (res.status >= 500) {
|
|
132
|
+
const verbose = init.verbose === true || process.env.GURULU_DEBUG === '1';
|
|
133
|
+
if (verbose) {
|
|
134
|
+
let detail = '';
|
|
135
|
+
try {
|
|
136
|
+
const body = await res.clone().json();
|
|
137
|
+
const msg = body?.error || body?.message;
|
|
138
|
+
if (msg) {
|
|
139
|
+
detail = ` ${typeof msg === 'string' ? msg : JSON.stringify(msg)}`;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
detail = ` ${JSON.stringify(body)}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
try {
|
|
147
|
+
const txt = await res.clone().text();
|
|
148
|
+
if (txt)
|
|
149
|
+
detail = ` ${txt.slice(0, 500)}`;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
/* ignore */
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
fatal(`Gurulu is temporarily unavailable (HTTP ${res.status}).${detail}`, init);
|
|
156
|
+
}
|
|
132
157
|
fatal(`Gurulu is temporarily unavailable (HTTP ${res.status}).`, init);
|
|
133
158
|
}
|
|
134
159
|
return res;
|
package/dist/commands/auth.js
CHANGED
|
@@ -54,22 +54,38 @@ const login_1 = require("./login");
|
|
|
54
54
|
const config_1 = require("../config");
|
|
55
55
|
const api_client_1 = require("../api-client");
|
|
56
56
|
const ui_1 = require("../utils/ui");
|
|
57
|
+
function isHeadless() {
|
|
58
|
+
if (!process.stdout.isTTY)
|
|
59
|
+
return true;
|
|
60
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION)
|
|
61
|
+
return true;
|
|
62
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
|
|
63
|
+
return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
57
66
|
function tryOpenBrowser(url) {
|
|
58
67
|
const platform = process.platform;
|
|
59
68
|
const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
|
|
69
|
+
// On Windows the URL goes as a positional after an empty title arg to `start`.
|
|
70
|
+
const args = platform === 'win32' ? ['', url] : [url];
|
|
60
71
|
try {
|
|
61
|
-
const child = (0, child_process_1.spawn)(cmd,
|
|
72
|
+
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
62
73
|
detached: true,
|
|
63
74
|
stdio: 'ignore',
|
|
64
75
|
shell: platform === 'win32',
|
|
65
76
|
});
|
|
77
|
+
let errored = false;
|
|
66
78
|
child.on('error', () => {
|
|
67
|
-
|
|
79
|
+
errored = true;
|
|
68
80
|
});
|
|
69
81
|
child.unref();
|
|
82
|
+
// spawn() returns synchronously; if the binary is missing on PATH we'll
|
|
83
|
+
// catch the synchronous throw below. The async 'error' handler covers
|
|
84
|
+
// post-fork failures but we can't await it here without blocking the flow.
|
|
85
|
+
return !errored;
|
|
70
86
|
}
|
|
71
87
|
catch {
|
|
72
|
-
|
|
88
|
+
return false;
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
function sleep(ms) {
|
|
@@ -121,8 +137,17 @@ async function authCommand(args) {
|
|
|
121
137
|
console.log(` ${(0, ui_1.dim)('Visit:')} ${verifyUrl}`);
|
|
122
138
|
console.log(` ${(0, ui_1.dim)('Device:')} CLI — ${deviceLabel}`);
|
|
123
139
|
console.log('');
|
|
124
|
-
if (
|
|
125
|
-
|
|
140
|
+
if (args.noBrowser) {
|
|
141
|
+
(0, ui_1.info)(`Open this URL manually: ${verifyUrl}`);
|
|
142
|
+
}
|
|
143
|
+
else if (isHeadless()) {
|
|
144
|
+
(0, ui_1.info)(`Headless/SSH session detected — open this URL on another device: ${verifyUrl}`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const opened = tryOpenBrowser(verifyUrl);
|
|
148
|
+
if (!opened) {
|
|
149
|
+
(0, ui_1.info)(`Could not auto-open the browser. Open this URL manually: ${verifyUrl}`);
|
|
150
|
+
}
|
|
126
151
|
}
|
|
127
152
|
const expiresAtMs = new Date(expiresAt).getTime();
|
|
128
153
|
const interval = Math.max(1000, pollIntervalMs || 2000);
|
|
@@ -195,16 +220,26 @@ async function authCommand(args) {
|
|
|
195
220
|
process.exit(1);
|
|
196
221
|
}
|
|
197
222
|
const fullKey = approved.secretKey.fullKey;
|
|
223
|
+
const userRequestedKeychain = args.useKeychain === true;
|
|
224
|
+
const keychainAvailable = (0, config_1.isKeychainAvailable)();
|
|
225
|
+
if (userRequestedKeychain && !keychainAvailable) {
|
|
226
|
+
(0, ui_1.warn)('Keychain requested via --keychain but the optional `keytar` module is not installed. ' +
|
|
227
|
+
'Falling back to plaintext storage in ~/.gurulu/config.json. ' +
|
|
228
|
+
'Install `keytar` (`npm i -g keytar`) or unset --keychain to silence this warning.');
|
|
229
|
+
}
|
|
198
230
|
const { storedInKeychain } = await (0, config_1.saveProfile)(profileName, {
|
|
199
231
|
email: `cli-${deviceLabel}@gurulu.local`,
|
|
200
232
|
secret_key: fullKey,
|
|
201
233
|
api_base: apiBase,
|
|
202
|
-
}, { useKeychain: args.useKeychain ??
|
|
234
|
+
}, { useKeychain: args.useKeychain ?? keychainAvailable });
|
|
203
235
|
const tail = fullKey.slice(-6);
|
|
204
236
|
(0, ui_1.success)(`Device paired. Key …${tail} saved to profile "${profileName}".`);
|
|
205
237
|
if (storedInKeychain) {
|
|
206
238
|
(0, ui_1.info)(` Stored in macOS Keychain (${(0, ui_1.dim)('io.gurulu.cli')})`);
|
|
207
239
|
}
|
|
240
|
+
else if (userRequestedKeychain) {
|
|
241
|
+
(0, ui_1.info)(` Stored in plaintext at ~/.gurulu/config.json (keychain unavailable).`);
|
|
242
|
+
}
|
|
208
243
|
if (approved.secretKey.expiresAt) {
|
|
209
244
|
(0, ui_1.info)(` Expires: ${approved.secretKey.expiresAt}`);
|
|
210
245
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -23,6 +23,9 @@ async function doctorCommand(args) {
|
|
|
23
23
|
const checks = [];
|
|
24
24
|
// 1. Framework detection
|
|
25
25
|
const framework = (0, detect_1.detectFramework)(projectDir);
|
|
26
|
+
// If the user explicitly passed --site, the cwd-derived checks should not
|
|
27
|
+
// imply that the *production site* is broken — they're informational only.
|
|
28
|
+
const isLocalProjectDetected = framework !== 'unknown';
|
|
26
29
|
checks.push({
|
|
27
30
|
name: 'Framework Detection',
|
|
28
31
|
status: framework !== 'unknown' ? 'pass' : 'warn',
|
|
@@ -30,6 +33,7 @@ async function doctorCommand(args) {
|
|
|
30
33
|
? `Detected ${(0, detect_1.getFrameworkDisplayName)(framework)}`
|
|
31
34
|
: 'Could not detect framework',
|
|
32
35
|
fix: framework === 'unknown' ? 'Run "gurulu init --framework <name>" to specify manually' : undefined,
|
|
36
|
+
section: 'local',
|
|
33
37
|
});
|
|
34
38
|
// 2. SDK installed
|
|
35
39
|
const pkgPath = path_1.default.join(projectDir, 'package.json');
|
|
@@ -78,18 +82,35 @@ async function doctorCommand(args) {
|
|
|
78
82
|
: hasAndroidSDK ? 'Android SDK found in build.gradle'
|
|
79
83
|
: setupFiles.length > 0 ? `Setup file found: ${setupFiles[0]}`
|
|
80
84
|
: 'No client SDK detected';
|
|
85
|
+
// When --site was passed but cwd is not a Gurulu project, downgrade local
|
|
86
|
+
// SDK failures to "skip" so they don't masquerade as production-site issues.
|
|
87
|
+
const localChecksAreInformational = !!siteId && !isLocalProjectDetected;
|
|
81
88
|
checks.push({
|
|
82
89
|
name: 'Client SDK',
|
|
83
|
-
status: hasSetup ? 'pass' : 'fail',
|
|
84
|
-
message:
|
|
85
|
-
|
|
90
|
+
status: hasSetup ? 'pass' : localChecksAreInformational ? 'skip' : 'fail',
|
|
91
|
+
message: hasSetup
|
|
92
|
+
? sdkLabel
|
|
93
|
+
: localChecksAreInformational
|
|
94
|
+
? 'No client SDK detected in this directory (informational — site checks below)'
|
|
95
|
+
: sdkLabel,
|
|
96
|
+
fix: !hasSetup && !localChecksAreInformational
|
|
97
|
+
? 'Run "gurulu init" to set up analytics'
|
|
98
|
+
: undefined,
|
|
99
|
+
section: 'local',
|
|
86
100
|
});
|
|
87
101
|
// 3. Server SDK
|
|
88
102
|
checks.push({
|
|
89
103
|
name: 'Server SDK',
|
|
90
|
-
status: hasNodeSDK ? 'pass' : 'warn',
|
|
91
|
-
message: hasNodeSDK
|
|
92
|
-
|
|
104
|
+
status: hasNodeSDK ? 'pass' : localChecksAreInformational ? 'skip' : 'warn',
|
|
105
|
+
message: hasNodeSDK
|
|
106
|
+
? '@gurulu/node installed'
|
|
107
|
+
: localChecksAreInformational
|
|
108
|
+
? 'Server SDK check skipped (no local project detected)'
|
|
109
|
+
: 'Server SDK not installed (optional)',
|
|
110
|
+
fix: !hasNodeSDK && !localChecksAreInformational
|
|
111
|
+
? 'Run "gurulu add-server" for server-side tracking'
|
|
112
|
+
: undefined,
|
|
113
|
+
section: 'local',
|
|
93
114
|
});
|
|
94
115
|
// 4. Site ID configured
|
|
95
116
|
checks.push({
|
|
@@ -263,6 +284,7 @@ async function doctorCommand(args) {
|
|
|
263
284
|
status: envHasSiteId ? 'pass' : 'warn',
|
|
264
285
|
message: envHasSiteId ? 'Site ID found in env file' : 'No GURULU_SITE_ID in env files',
|
|
265
286
|
fix: !envHasSiteId ? 'Run "gurulu init" to auto-configure .env.local' : undefined,
|
|
287
|
+
section: 'local',
|
|
266
288
|
});
|
|
267
289
|
}
|
|
268
290
|
// Output
|
|
@@ -281,16 +303,36 @@ async function doctorCommand(args) {
|
|
|
281
303
|
};
|
|
282
304
|
let failCount = 0;
|
|
283
305
|
let warnCount = 0;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
306
|
+
const localChecks = checks.filter((c) => c.section === 'local');
|
|
307
|
+
const siteChecks = checks.filter((c) => c.section !== 'local');
|
|
308
|
+
const printGroup = (title, items, note) => {
|
|
309
|
+
if (items.length === 0)
|
|
310
|
+
return;
|
|
311
|
+
console.log(` ${(0, ui_1.bold)(title)}`);
|
|
312
|
+
if (note)
|
|
313
|
+
console.log(` ${(0, ui_1.dim)(note)}`);
|
|
314
|
+
for (const check of items) {
|
|
315
|
+
const icon = icons[check.status];
|
|
316
|
+
console.log(` ${icon} ${(0, ui_1.bold)(check.name)}: ${check.message}`);
|
|
317
|
+
if (check.fix) {
|
|
318
|
+
console.log(` ${(0, ui_1.dim)('\u2192')} ${(0, ui_1.dim)(check.fix)}`);
|
|
319
|
+
}
|
|
320
|
+
if (check.status === 'fail')
|
|
321
|
+
failCount++;
|
|
322
|
+
if (check.status === 'warn')
|
|
323
|
+
warnCount++;
|
|
289
324
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
325
|
+
console.log('');
|
|
326
|
+
};
|
|
327
|
+
if (localChecks.length > 0) {
|
|
328
|
+
const localNote = siteId && !isLocalProjectDetected
|
|
329
|
+
? `(Local project not detected \u2014 running site-only checks for ${siteId}.)`
|
|
330
|
+
: undefined;
|
|
331
|
+
printGroup('Local Project', localChecks, localNote);
|
|
332
|
+
}
|
|
333
|
+
if (siteChecks.length > 0) {
|
|
334
|
+
const siteTitle = siteId ? `Site (${siteId})` : 'Site';
|
|
335
|
+
printGroup(siteTitle, siteChecks);
|
|
294
336
|
}
|
|
295
337
|
console.log('');
|
|
296
338
|
if (failCount === 0 && warnCount === 0) {
|
package/dist/commands/events.js
CHANGED
|
@@ -76,10 +76,10 @@ async function listCmd(args) {
|
|
|
76
76
|
process.stdout.write(['TS', 'EVENT', 'SITE', 'URL'].join('\t') + '\n');
|
|
77
77
|
for (const e of events) {
|
|
78
78
|
process.stdout.write([
|
|
79
|
-
String(e.event_ts || '-'),
|
|
79
|
+
String(e.timestamp || e.event_ts || '-'),
|
|
80
80
|
String(e.event_name || '-'),
|
|
81
81
|
String(e.site_id || '-'),
|
|
82
|
-
String(e.url || '-').slice(0, 60),
|
|
82
|
+
String(e.page_url || e.url || '-').slice(0, 60),
|
|
83
83
|
].join('\t') + '\n');
|
|
84
84
|
}
|
|
85
85
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -31,7 +31,10 @@ async function whoamiCommand(args) {
|
|
|
31
31
|
const sites = (me.sites || []).length;
|
|
32
32
|
const keys = (me.apiKeys || []).length;
|
|
33
33
|
const installs = me.quota?.installsThisMonth ?? 0;
|
|
34
|
-
const
|
|
34
|
+
const rawLimit = me.quota?.installsLimit;
|
|
35
|
+
const limit = rawLimit === -1 || rawLimit === null || rawLimit === undefined
|
|
36
|
+
? 'unlimited'
|
|
37
|
+
: rawLimit;
|
|
35
38
|
process.stdout.write(`${me.user?.email} · ${me.tenant?.name} · ${me.tenant?.plan} plan · ${me.tenant?.id}\n`);
|
|
36
39
|
process.stdout.write(`Sites: ${sites} | API keys: ${keys} | Installs this month: ${installs}/${limit}\n`);
|
|
37
40
|
}
|
package/dist/index.js
CHANGED
|
@@ -99,7 +99,7 @@ const secrets_1 = require("./commands/secrets");
|
|
|
99
99
|
.option('key', { type: 'string', describe: 'Secret key (gsk_live_... or gsk_test_...) — legacy manual flow' })
|
|
100
100
|
.option('email', { type: 'string', describe: 'Email address (legacy flow)' })
|
|
101
101
|
.option('secret-key', { type: 'string', describe: 'Secret key (legacy flow alias for --key)' })
|
|
102
|
-
.option('
|
|
102
|
+
.option('browser', { type: 'boolean', default: true, describe: 'Auto-open the browser (use --no-browser to disable)' })
|
|
103
103
|
.option('no-interactive', { type: 'boolean', describe: 'Non-interactive mode (legacy flow)' })
|
|
104
104
|
.option('keychain', { type: 'boolean', describe: 'Store secret in macOS Keychain if available' })
|
|
105
105
|
.option('api-base', { type: 'string', describe: 'Override API base URL' }), (args) => (0, auth_1.authCommand)({
|
|
@@ -110,7 +110,7 @@ const secrets_1 = require("./commands/secrets");
|
|
|
110
110
|
noInteractive: args['no-interactive'],
|
|
111
111
|
useKeychain: args.keychain,
|
|
112
112
|
apiBase: args['api-base'],
|
|
113
|
-
noBrowser: args
|
|
113
|
+
noBrowser: args.browser === false ? true : undefined,
|
|
114
114
|
}))
|
|
115
115
|
.command('logout', 'Remove a stored profile', (y) => y
|
|
116
116
|
.option('all', { type: 'boolean', describe: 'Remove all profiles' })
|
|
@@ -635,8 +635,8 @@ const secrets_1 = require("./commands/secrets");
|
|
|
635
635
|
// ── Error tracking — source map upload ────────────────────────────────
|
|
636
636
|
.command('sourcemap <action>', 'Upload source maps for error deobfuscation (upload). Supports web/server JS .map and native dSYM/ProGuard.', (y) => y
|
|
637
637
|
.positional('action', { type: 'string', describe: 'upload' })
|
|
638
|
-
.option('release', { type: 'string', describe: 'Release version (e.g. v1.0.0). Web/server only — alias of --version.' })
|
|
639
|
-
.option('version', { type: 'string', describe: 'App version (e.g. 1.4.2). Required for native uploads.' })
|
|
638
|
+
.option('release', { type: 'string', describe: 'Release version (e.g. v1.0.0). Web/server only — alias of --app-version.' })
|
|
639
|
+
.option('app-version', { type: 'string', describe: 'App version (e.g. 1.4.2). Required for native uploads.' })
|
|
640
640
|
.option('dir', { type: 'string', describe: 'Directory containing .map files (web/server)' })
|
|
641
641
|
.option('file', { type: 'string', describe: 'Path to dSYM zip (iOS) or mapping.txt (Android)' })
|
|
642
642
|
.option('bundle-id', { type: 'string', describe: 'iOS bundleId or Android applicationId (native only)' })
|
|
@@ -645,7 +645,7 @@ const secrets_1 = require("./commands/secrets");
|
|
|
645
645
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, sourcemap_1.sourcemapCommand)({
|
|
646
646
|
action: args.action,
|
|
647
647
|
release: args.release,
|
|
648
|
-
version: args
|
|
648
|
+
version: args['app-version'],
|
|
649
649
|
dir: args.dir,
|
|
650
650
|
file: args.file,
|
|
651
651
|
bundleId: args['bundle-id'],
|