@gurulu/cli 0.4.5 → 0.4.7
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/chat.d.ts +1 -0
- package/dist/commands/chat.js +1 -0
- package/dist/commands/doctor.js +57 -15
- package/dist/commands/events.js +28 -6
- package/dist/commands/insights.js +13 -1
- package/dist/commands/setup.d.ts +21 -0
- package/dist/commands/setup.js +67 -0
- package/dist/commands/whoami.js +4 -1
- package/dist/frameworks/detect.js +62 -48
- package/dist/index.js +29 -6
- 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/chat.d.ts
CHANGED
package/dist/commands/chat.js
CHANGED
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
|
}
|
|
@@ -248,11 +248,33 @@ async function verifyCmd(args) {
|
|
|
248
248
|
console.log(`Unique event types: ${data.unique_types ?? 'N/A'}`);
|
|
249
249
|
}
|
|
250
250
|
else {
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
251
|
+
// Phase 33 P33-WA — three-state empty output instead of one generic
|
|
252
|
+
// "no telemetry yet" message. Helps the user tell apart "definition
|
|
253
|
+
// exists but never matched" vs "definition missing entirely" vs "no
|
|
254
|
+
// events at all on this site". Definition lookup is best-effort
|
|
255
|
+
// (extra HTTP call) so a 4xx falls through to the legacy message.
|
|
254
256
|
if (filterEvent) {
|
|
255
|
-
|
|
257
|
+
let definitionExists = null;
|
|
258
|
+
try {
|
|
259
|
+
const defs = await (0, api_client_1.cliApiJson)(`/api/cli/events/definitions?siteId=${encodeURIComponent(args.site)}`, { profile: args.profile });
|
|
260
|
+
definitionExists = Array.isArray(defs.definitions)
|
|
261
|
+
&& defs.definitions.some((d) => d.eventName === filterEvent);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// best-effort — fall through to legacy message
|
|
265
|
+
}
|
|
266
|
+
if (definitionExists === true) {
|
|
267
|
+
(0, ui_1.info)(`✓ "${filterEvent}" is registered (catalog has it) but no telemetry has matched in the last 24h.`);
|
|
268
|
+
(0, ui_1.info)(` Likely cause: SDK hasn't fired this event yet. Check your client code and run verify again.`);
|
|
269
|
+
}
|
|
270
|
+
else if (definitionExists === false) {
|
|
271
|
+
(0, ui_1.info)(`⚠ "${filterEvent}" is NOT registered yet (no CustomEventDefinition row).`);
|
|
272
|
+
(0, ui_1.info)(` Phase 32 auto-registers on first sight, but you can also run:`);
|
|
273
|
+
(0, ui_1.info)(` gurulu events define --site ${args.site} --event-name ${filterEvent} --display-name "..."`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
(0, ui_1.info)(`No telemetry yet for "${filterEvent}". Track it from your app and run verify again.`);
|
|
277
|
+
}
|
|
256
278
|
}
|
|
257
279
|
else {
|
|
258
280
|
(0, ui_1.info)('No telemetry data found for this site in the last 24h.');
|
|
@@ -20,6 +20,18 @@ async function insightsCommand(args) {
|
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
function formatHighlight(h) {
|
|
24
|
+
if (typeof h === 'string')
|
|
25
|
+
return h;
|
|
26
|
+
if (h && typeof h === 'object') {
|
|
27
|
+
const o = h;
|
|
28
|
+
const arrow = o.trend === 'up' ? '↑' : o.trend === 'down' ? '↓' : '→';
|
|
29
|
+
const value = typeof o.value === 'number' ? o.value.toLocaleString() : String(o.value ?? '');
|
|
30
|
+
const delta = typeof o.delta === 'number' ? ` (${o.delta >= 0 ? '+' : ''}${o.delta.toFixed(1)}% ${arrow})` : '';
|
|
31
|
+
return `${o.label ?? 'metric'}: ${value}${delta}`;
|
|
32
|
+
}
|
|
33
|
+
return String(h);
|
|
34
|
+
}
|
|
23
35
|
async function todayCmd(args) {
|
|
24
36
|
try {
|
|
25
37
|
const body = await (0, api_client_1.cliApiJson)('/api/cli/insights', {
|
|
@@ -35,7 +47,7 @@ async function todayCmd(args) {
|
|
|
35
47
|
if (Array.isArray(i.highlights)) {
|
|
36
48
|
process.stdout.write(`Highlights:\n`);
|
|
37
49
|
for (const h of i.highlights)
|
|
38
|
-
process.stdout.write(` - ${h}\n`);
|
|
50
|
+
process.stdout.write(` - ${formatHighlight(h)}\n`);
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
catch (err) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2026-05-05 DF-5 — `gurulu setup --vertical saas` zero-friction lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Tek komutta:
|
|
5
|
+
* - vertical event taxonomy'sini define et (idempotent)
|
|
6
|
+
* - default funnel'ları kur
|
|
7
|
+
* - acquisition + revenue + retention event'lerine goal bağla
|
|
8
|
+
*
|
|
9
|
+
* 23 ayrı komut yerine 1.
|
|
10
|
+
*/
|
|
11
|
+
export interface SetupArgs {
|
|
12
|
+
vertical?: string;
|
|
13
|
+
site?: string;
|
|
14
|
+
includeFunnels?: boolean;
|
|
15
|
+
includeGoals?: boolean;
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
json?: boolean;
|
|
18
|
+
yes?: boolean;
|
|
19
|
+
profile?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function setupCommand(args: SetupArgs): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 2026-05-05 DF-5 — `gurulu setup --vertical saas` zero-friction lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Tek komutta:
|
|
6
|
+
* - vertical event taxonomy'sini define et (idempotent)
|
|
7
|
+
* - default funnel'ları kur
|
|
8
|
+
* - acquisition + revenue + retention event'lerine goal bağla
|
|
9
|
+
*
|
|
10
|
+
* 23 ayrı komut yerine 1.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.setupCommand = setupCommand;
|
|
14
|
+
const api_client_1 = require("../api-client");
|
|
15
|
+
const ui_1 = require("../utils/ui");
|
|
16
|
+
async function setupCommand(args) {
|
|
17
|
+
const vertical = (args.vertical || '').toLowerCase().trim();
|
|
18
|
+
if (!vertical) {
|
|
19
|
+
(0, ui_1.error)('--vertical required (e.g. saas, ecommerce, marketplace, fintech, healthcare, education, media, igaming, generic)');
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!args.site) {
|
|
24
|
+
(0, ui_1.error)('--site required');
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const path = '/api/cli/setup/apply-template' + (args.dryRun ? '?dry_run=1' : '');
|
|
29
|
+
const body = {
|
|
30
|
+
siteId: args.site,
|
|
31
|
+
vertical,
|
|
32
|
+
includeFunnels: args.includeFunnels !== false,
|
|
33
|
+
includeGoals: args.includeGoals !== false,
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const res = await (0, api_client_1.cliApiJson)(path, {
|
|
37
|
+
profile: args.profile,
|
|
38
|
+
method: 'POST',
|
|
39
|
+
json: body,
|
|
40
|
+
});
|
|
41
|
+
if (args.json) {
|
|
42
|
+
process.stdout.write(JSON.stringify(res, null, 2) + '\n');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if ('dryRun' in res && res.dryRun) {
|
|
46
|
+
const a = res.after;
|
|
47
|
+
(0, ui_1.info)(`Dry-run preview — ${a.vertical} lifecycle taxonomy:`);
|
|
48
|
+
process.stdout.write(`\n Events (${a.events.length}): ${a.events.join(', ')}\n`);
|
|
49
|
+
process.stdout.write(` Funnels (${a.funnels.length}): ${a.funnels.join(', ')}\n`);
|
|
50
|
+
process.stdout.write(` Goals (${a.goals.length}): ${a.goals.join(', ')}\n\n`);
|
|
51
|
+
(0, ui_1.info)('Re-run without --dry-run to apply.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const r = res;
|
|
55
|
+
(0, ui_1.success)(`Applied ${r.vertical} lifecycle taxonomy to site ${args.site}`);
|
|
56
|
+
process.stdout.write(` Events: ${r.events.created.length} created, ${r.events.skipped.length} skipped\n`);
|
|
57
|
+
process.stdout.write(` Funnels: ${r.funnels.created.length} created, ${r.funnels.skipped.length} skipped\n`);
|
|
58
|
+
process.stdout.write(` Goals: ${r.goals.created.length} created, ${r.goals.skipped.length} skipped\n`);
|
|
59
|
+
if (r.events.created.length) {
|
|
60
|
+
process.stdout.write(`\n New events: ${r.events.created.join(', ')}\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
(0, ui_1.error)(`Setup failed: ${err?.message ?? String(err)}`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
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
|
}
|
|
@@ -232,26 +232,33 @@ export function initGurulu() {
|
|
|
232
232
|
case 'express':
|
|
233
233
|
return {
|
|
234
234
|
file: 'src/gurulu.ts',
|
|
235
|
-
code: `// Gurulu.io Server Analytics
|
|
235
|
+
code: `// Gurulu.io Server Analytics — Phase 32 P32-A2 fix
|
|
236
|
+
// Use the @gurulu/node SDK so retry, identity propagation, and the
|
|
237
|
+
// correct ingest endpoint (/api/ingest/v1/server) are handled for you.
|
|
238
|
+
// npm install @gurulu/node
|
|
239
|
+
import { Gurulu } from '@gurulu/node';
|
|
236
240
|
import type { Request, Response, NextFunction } from 'express';
|
|
237
241
|
|
|
238
|
-
const
|
|
239
|
-
|
|
242
|
+
export const gurulu = new Gurulu({
|
|
243
|
+
siteId: '${siteId}',
|
|
244
|
+
token: '${token}',
|
|
245
|
+
endpoint: 'https://gurulu.io',
|
|
246
|
+
});
|
|
240
247
|
|
|
241
248
|
export function guruluMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
// Identity propagation: read the anonymous_id the browser SDK set in a
|
|
250
|
+
// cookie / header so server-side events stitch onto the same user.
|
|
251
|
+
const anonymousId = (req.headers['x-gurulu-anonymous-id'] as string)
|
|
252
|
+
|| (req.cookies?._gurulu_id as string)
|
|
253
|
+
|| undefined;
|
|
254
|
+
gurulu.track({
|
|
255
|
+
eventName: 'pageview',
|
|
256
|
+
anonymousId,
|
|
257
|
+
properties: {
|
|
249
258
|
url: req.originalUrl,
|
|
250
259
|
referrer: req.headers.referer || '',
|
|
251
260
|
user_agent: req.headers['user-agent'] || '',
|
|
252
|
-
|
|
253
|
-
timestamp: new Date().toISOString(),
|
|
254
|
-
}),
|
|
261
|
+
},
|
|
255
262
|
}).catch(() => {});
|
|
256
263
|
next();
|
|
257
264
|
}`,
|
|
@@ -260,26 +267,28 @@ export function guruluMiddleware(req: Request, res: Response, next: NextFunction
|
|
|
260
267
|
case 'fastify':
|
|
261
268
|
return {
|
|
262
269
|
file: 'src/gurulu.ts',
|
|
263
|
-
code: `// Gurulu.io Server Analytics — Fastify plugin
|
|
270
|
+
code: `// Gurulu.io Server Analytics — Fastify plugin (Phase 32 P32-A2 fix)
|
|
271
|
+
// npm install @gurulu/node
|
|
272
|
+
import { Gurulu } from '@gurulu/node';
|
|
264
273
|
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
265
274
|
|
|
266
|
-
const
|
|
267
|
-
|
|
275
|
+
export const gurulu = new Gurulu({
|
|
276
|
+
siteId: '${siteId}',
|
|
277
|
+
token: '${token}',
|
|
278
|
+
endpoint: 'https://gurulu.io',
|
|
279
|
+
});
|
|
268
280
|
|
|
269
281
|
export async function guruluPlugin(fastify: FastifyInstance) {
|
|
270
282
|
fastify.addHook('onRequest', async (req: FastifyRequest, _reply: FastifyReply) => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
event: 'pageview',
|
|
283
|
+
const anonymousId = (req.headers['x-gurulu-anonymous-id'] as string) || undefined;
|
|
284
|
+
gurulu.track({
|
|
285
|
+
eventName: 'pageview',
|
|
286
|
+
anonymousId,
|
|
287
|
+
properties: {
|
|
277
288
|
url: req.url,
|
|
278
289
|
referrer: (req.headers.referer as string) || '',
|
|
279
290
|
user_agent: (req.headers['user-agent'] as string) || '',
|
|
280
|
-
|
|
281
|
-
timestamp: new Date().toISOString(),
|
|
282
|
-
}),
|
|
291
|
+
},
|
|
283
292
|
}).catch(() => {});
|
|
284
293
|
});
|
|
285
294
|
}`,
|
|
@@ -288,24 +297,27 @@ export async function guruluPlugin(fastify: FastifyInstance) {
|
|
|
288
297
|
case 'hono':
|
|
289
298
|
return {
|
|
290
299
|
file: 'src/gurulu.ts',
|
|
291
|
-
code: `// Gurulu.io Server Analytics — Hono middleware
|
|
300
|
+
code: `// Gurulu.io Server Analytics — Hono middleware (Phase 32 P32-A2 fix)
|
|
301
|
+
// npm install @gurulu/node
|
|
302
|
+
import { Gurulu } from '@gurulu/node';
|
|
292
303
|
import type { MiddlewareHandler } from 'hono';
|
|
293
304
|
|
|
294
|
-
const
|
|
295
|
-
|
|
305
|
+
export const gurulu = new Gurulu({
|
|
306
|
+
siteId: '${siteId}',
|
|
307
|
+
token: '${token}',
|
|
308
|
+
endpoint: 'https://gurulu.io',
|
|
309
|
+
});
|
|
296
310
|
|
|
297
311
|
export const guruluMiddleware: MiddlewareHandler = async (c, next) => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
event: 'pageview',
|
|
312
|
+
const anonymousId = c.req.header('x-gurulu-anonymous-id') || undefined;
|
|
313
|
+
gurulu.track({
|
|
314
|
+
eventName: 'pageview',
|
|
315
|
+
anonymousId,
|
|
316
|
+
properties: {
|
|
304
317
|
url: c.req.url,
|
|
305
318
|
referrer: c.req.header('referer') || '',
|
|
306
319
|
user_agent: c.req.header('user-agent') || '',
|
|
307
|
-
|
|
308
|
-
}),
|
|
320
|
+
},
|
|
309
321
|
}).catch(() => {});
|
|
310
322
|
await next();
|
|
311
323
|
};`,
|
|
@@ -314,28 +326,30 @@ export const guruluMiddleware: MiddlewareHandler = async (c, next) => {
|
|
|
314
326
|
case 'nestjs':
|
|
315
327
|
return {
|
|
316
328
|
file: 'src/gurulu.middleware.ts',
|
|
317
|
-
code: `// Gurulu.io Server Analytics Middleware
|
|
329
|
+
code: `// Gurulu.io Server Analytics Middleware (Phase 32 P32-A2 fix)
|
|
330
|
+
// npm install @gurulu/node
|
|
318
331
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
319
332
|
import { Request, Response, NextFunction } from 'express';
|
|
333
|
+
import { Gurulu } from '@gurulu/node';
|
|
320
334
|
|
|
321
|
-
const
|
|
322
|
-
|
|
335
|
+
const gurulu = new Gurulu({
|
|
336
|
+
siteId: '${siteId}',
|
|
337
|
+
token: '${token}',
|
|
338
|
+
endpoint: 'https://gurulu.io',
|
|
339
|
+
});
|
|
323
340
|
|
|
324
341
|
@Injectable()
|
|
325
342
|
export class GuruluMiddleware implements NestMiddleware {
|
|
326
343
|
use(req: Request, res: Response, next: NextFunction) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
event: 'pageview',
|
|
344
|
+
const anonymousId = (req.headers['x-gurulu-anonymous-id'] as string) || undefined;
|
|
345
|
+
gurulu.track({
|
|
346
|
+
eventName: 'pageview',
|
|
347
|
+
anonymousId,
|
|
348
|
+
properties: {
|
|
333
349
|
url: req.originalUrl,
|
|
334
350
|
referrer: req.headers.referer || '',
|
|
335
351
|
user_agent: req.headers['user-agent'] || '',
|
|
336
|
-
|
|
337
|
-
timestamp: new Date().toISOString(),
|
|
338
|
-
}),
|
|
352
|
+
},
|
|
339
353
|
}).catch(() => {});
|
|
340
354
|
next();
|
|
341
355
|
}
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,8 @@ const funnels_1 = require("./commands/funnels");
|
|
|
38
38
|
const heatmap_1 = require("./commands/heatmap");
|
|
39
39
|
// Gurulu Chat — NL → SQL analytics
|
|
40
40
|
const chat_1 = require("./commands/chat");
|
|
41
|
+
// 2026-05-05 DF-5 — zero-friction lifecycle setup
|
|
42
|
+
const setup_1 = require("./commands/setup");
|
|
41
43
|
// Error tracking — source map upload
|
|
42
44
|
const sourcemap_1 = require("./commands/sourcemap");
|
|
43
45
|
// Phase 21 — database connect
|
|
@@ -99,7 +101,7 @@ const secrets_1 = require("./commands/secrets");
|
|
|
99
101
|
.option('key', { type: 'string', describe: 'Secret key (gsk_live_... or gsk_test_...) — legacy manual flow' })
|
|
100
102
|
.option('email', { type: 'string', describe: 'Email address (legacy flow)' })
|
|
101
103
|
.option('secret-key', { type: 'string', describe: 'Secret key (legacy flow alias for --key)' })
|
|
102
|
-
.option('
|
|
104
|
+
.option('browser', { type: 'boolean', default: true, describe: 'Auto-open the browser (use --no-browser to disable)' })
|
|
103
105
|
.option('no-interactive', { type: 'boolean', describe: 'Non-interactive mode (legacy flow)' })
|
|
104
106
|
.option('keychain', { type: 'boolean', describe: 'Store secret in macOS Keychain if available' })
|
|
105
107
|
.option('api-base', { type: 'string', describe: 'Override API base URL' }), (args) => (0, auth_1.authCommand)({
|
|
@@ -110,7 +112,7 @@ const secrets_1 = require("./commands/secrets");
|
|
|
110
112
|
noInteractive: args['no-interactive'],
|
|
111
113
|
useKeychain: args.keychain,
|
|
112
114
|
apiBase: args['api-base'],
|
|
113
|
-
noBrowser: args
|
|
115
|
+
noBrowser: args.browser === false ? true : undefined,
|
|
114
116
|
}))
|
|
115
117
|
.command('logout', 'Remove a stored profile', (y) => y
|
|
116
118
|
.option('all', { type: 'boolean', describe: 'Remove all profiles' })
|
|
@@ -619,24 +621,45 @@ const secrets_1 = require("./commands/secrets");
|
|
|
619
621
|
format: args.format,
|
|
620
622
|
json: args.json,
|
|
621
623
|
profile: args.profile,
|
|
624
|
+
}))
|
|
625
|
+
// ── 2026-05-05 DF-5 — zero-friction lifecycle setup ──────────────────
|
|
626
|
+
.command('setup', 'Apply a vertical lifecycle taxonomy (events + funnels + goals) in one shot', (y) => y
|
|
627
|
+
.option('vertical', {
|
|
628
|
+
type: 'string',
|
|
629
|
+
describe: 'saas | ecommerce | marketplace | fintech | healthcare | education | media | igaming | generic',
|
|
630
|
+
})
|
|
631
|
+
.option('site', { type: 'string', describe: 'Site ID' })
|
|
632
|
+
.option('include-funnels', { type: 'boolean', default: true, describe: 'Also create funnels' })
|
|
633
|
+
.option('include-goals', { type: 'boolean', default: true, describe: 'Also create goals' })
|
|
634
|
+
.option('dry-run', { type: 'boolean', describe: 'Preview without writing' })
|
|
635
|
+
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, setup_1.setupCommand)({
|
|
636
|
+
vertical: args.vertical,
|
|
637
|
+
site: args.site,
|
|
638
|
+
includeFunnels: args['include-funnels'],
|
|
639
|
+
includeGoals: args['include-goals'],
|
|
640
|
+
dryRun: args['dry-run'],
|
|
641
|
+
json: args.json,
|
|
642
|
+
profile: args.profile,
|
|
622
643
|
}))
|
|
623
644
|
// ── Gurulu Chat — NL → SQL analytics ─────────────────────────────────
|
|
624
645
|
.command('chat [question]', 'Ask analytics questions in natural language (NL → SQL)', (y) => y
|
|
625
646
|
.positional('question', { type: 'string', describe: 'Question to ask (omit for REPL mode)' })
|
|
626
647
|
.option('json', { type: 'boolean', describe: 'Machine-readable JSON output' })
|
|
627
648
|
.option('show-sql', { type: 'boolean', describe: 'Also print the generated SQL' })
|
|
628
|
-
.option('context', { type: 'string', describe: 'Additional context for the query' })
|
|
649
|
+
.option('context', { type: 'string', describe: 'Additional context for the query' })
|
|
650
|
+
.option('site', { type: 'string', describe: 'Site ID (overrides profile default)' }), (args) => (0, chat_1.chatCommand)({
|
|
629
651
|
question: args.question,
|
|
630
652
|
json: args.json,
|
|
631
653
|
showSql: args['show-sql'],
|
|
632
654
|
context: args.context,
|
|
633
655
|
profile: args.profile,
|
|
656
|
+
site: args.site,
|
|
634
657
|
}))
|
|
635
658
|
// ── Error tracking — source map upload ────────────────────────────────
|
|
636
659
|
.command('sourcemap <action>', 'Upload source maps for error deobfuscation (upload). Supports web/server JS .map and native dSYM/ProGuard.', (y) => y
|
|
637
660
|
.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.' })
|
|
661
|
+
.option('release', { type: 'string', describe: 'Release version (e.g. v1.0.0). Web/server only — alias of --app-version.' })
|
|
662
|
+
.option('app-version', { type: 'string', describe: 'App version (e.g. 1.4.2). Required for native uploads.' })
|
|
640
663
|
.option('dir', { type: 'string', describe: 'Directory containing .map files (web/server)' })
|
|
641
664
|
.option('file', { type: 'string', describe: 'Path to dSYM zip (iOS) or mapping.txt (Android)' })
|
|
642
665
|
.option('bundle-id', { type: 'string', describe: 'iOS bundleId or Android applicationId (native only)' })
|
|
@@ -645,7 +668,7 @@ const secrets_1 = require("./commands/secrets");
|
|
|
645
668
|
.option('json', { type: 'boolean', describe: 'JSON output' }), (args) => (0, sourcemap_1.sourcemapCommand)({
|
|
646
669
|
action: args.action,
|
|
647
670
|
release: args.release,
|
|
648
|
-
version: args
|
|
671
|
+
version: args['app-version'],
|
|
649
672
|
dir: args.dir,
|
|
650
673
|
file: args.file,
|
|
651
674
|
bundleId: args['bundle-id'],
|