@gurulu/cli 0.1.0

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.
Files changed (77) hide show
  1. package/README.md +66 -0
  2. package/bin/gurulu.js +2 -0
  3. package/dist/api-client.d.ts +27 -0
  4. package/dist/api-client.js +150 -0
  5. package/dist/commands/add-server.d.ts +9 -0
  6. package/dist/commands/add-server.js +155 -0
  7. package/dist/commands/alerts.d.ts +22 -0
  8. package/dist/commands/alerts.js +281 -0
  9. package/dist/commands/api-keys.d.ts +20 -0
  10. package/dist/commands/api-keys.js +130 -0
  11. package/dist/commands/audiences.d.ts +16 -0
  12. package/dist/commands/audiences.js +180 -0
  13. package/dist/commands/audit.d.ts +20 -0
  14. package/dist/commands/audit.js +130 -0
  15. package/dist/commands/auth.d.ts +20 -0
  16. package/dist/commands/auth.js +214 -0
  17. package/dist/commands/chat.d.ts +18 -0
  18. package/dist/commands/chat.js +117 -0
  19. package/dist/commands/config.d.ts +10 -0
  20. package/dist/commands/config.js +92 -0
  21. package/dist/commands/db.d.ts +25 -0
  22. package/dist/commands/db.js +322 -0
  23. package/dist/commands/destinations.d.ts +20 -0
  24. package/dist/commands/destinations.js +191 -0
  25. package/dist/commands/doctor.d.ts +7 -0
  26. package/dist/commands/doctor.js +318 -0
  27. package/dist/commands/events.d.ts +27 -0
  28. package/dist/commands/events.js +147 -0
  29. package/dist/commands/experiments.d.ts +18 -0
  30. package/dist/commands/experiments.js +233 -0
  31. package/dist/commands/identity.d.ts +13 -0
  32. package/dist/commands/identity.js +107 -0
  33. package/dist/commands/init.d.ts +11 -0
  34. package/dist/commands/init.js +215 -0
  35. package/dist/commands/insights.d.ts +10 -0
  36. package/dist/commands/insights.js +65 -0
  37. package/dist/commands/install.d.ts +233 -0
  38. package/dist/commands/install.js +920 -0
  39. package/dist/commands/login.d.ts +20 -0
  40. package/dist/commands/login.js +170 -0
  41. package/dist/commands/logout.d.ts +10 -0
  42. package/dist/commands/logout.js +41 -0
  43. package/dist/commands/playground.d.ts +11 -0
  44. package/dist/commands/playground.js +47 -0
  45. package/dist/commands/sites.d.ts +18 -0
  46. package/dist/commands/sites.js +139 -0
  47. package/dist/commands/sourcemap.d.ts +21 -0
  48. package/dist/commands/sourcemap.js +137 -0
  49. package/dist/commands/status.d.ts +7 -0
  50. package/dist/commands/status.js +136 -0
  51. package/dist/commands/warehouse.d.ts +20 -0
  52. package/dist/commands/warehouse.js +65 -0
  53. package/dist/commands/warehouses.d.ts +17 -0
  54. package/dist/commands/warehouses.js +182 -0
  55. package/dist/commands/whoami.d.ts +9 -0
  56. package/dist/commands/whoami.js +47 -0
  57. package/dist/config.d.ts +75 -0
  58. package/dist/config.js +329 -0
  59. package/dist/frameworks/detect.d.ts +8 -0
  60. package/dist/frameworks/detect.js +362 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.js +429 -0
  63. package/dist/install-intent-proposal.d.ts +99 -0
  64. package/dist/install-intent-proposal.js +202 -0
  65. package/dist/utils/api.d.ts +20 -0
  66. package/dist/utils/api.js +47 -0
  67. package/dist/utils/config.d.ts +13 -0
  68. package/dist/utils/config.js +30 -0
  69. package/dist/utils/confirm.d.ts +17 -0
  70. package/dist/utils/confirm.js +40 -0
  71. package/dist/utils/dry-run.d.ts +20 -0
  72. package/dist/utils/dry-run.js +67 -0
  73. package/dist/utils/from-file.d.ts +9 -0
  74. package/dist/utils/from-file.js +72 -0
  75. package/dist/utils/ui.d.ts +14 -0
  76. package/dist/utils/ui.js +59 -0
  77. package/package.json +26 -0
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.doctorCommand = doctorCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const config_1 = require("../config");
10
+ const api_client_1 = require("../api-client");
11
+ const detect_1 = require("../frameworks/detect");
12
+ const ui_1 = require("../utils/ui");
13
+ async function doctorCommand(args) {
14
+ let profile;
15
+ try {
16
+ profile = await (0, config_1.loadActiveProfile)({ profile: args.profile });
17
+ }
18
+ catch {
19
+ profile = null;
20
+ }
21
+ const siteId = args.site;
22
+ const projectDir = process.cwd();
23
+ const checks = [];
24
+ // 1. Framework detection
25
+ const framework = (0, detect_1.detectFramework)(projectDir);
26
+ checks.push({
27
+ name: 'Framework Detection',
28
+ status: framework !== 'unknown' ? 'pass' : 'warn',
29
+ message: framework !== 'unknown'
30
+ ? `Detected ${(0, detect_1.getFrameworkDisplayName)(framework)}`
31
+ : 'Could not detect framework',
32
+ fix: framework === 'unknown' ? 'Run "gurulu init --framework <name>" to specify manually' : undefined,
33
+ });
34
+ // 2. SDK installed
35
+ const pkgPath = path_1.default.join(projectDir, 'package.json');
36
+ let hasWebSDK = false;
37
+ let hasNodeSDK = false;
38
+ let hasReactNativeSDK = false;
39
+ if (fs_1.default.existsSync(pkgPath)) {
40
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
41
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
42
+ hasWebSDK = !!allDeps['@gurulu/web-sdk'];
43
+ hasNodeSDK = !!allDeps['@gurulu/node'];
44
+ hasReactNativeSDK = !!allDeps['@gurulu/react-native'];
45
+ }
46
+ // Mobile SDK detection
47
+ let hasFlutterSDK = false;
48
+ const pubspecPath = path_1.default.join(projectDir, 'pubspec.yaml');
49
+ if (fs_1.default.existsSync(pubspecPath)) {
50
+ const pubspec = fs_1.default.readFileSync(pubspecPath, 'utf-8');
51
+ hasFlutterSDK = pubspec.includes('gurulu_flutter');
52
+ }
53
+ let hasIOSSDK = false;
54
+ const swiftPkgPath = path_1.default.join(projectDir, 'Package.swift');
55
+ if (fs_1.default.existsSync(swiftPkgPath)) {
56
+ const swiftPkg = fs_1.default.readFileSync(swiftPkgPath, 'utf-8');
57
+ hasIOSSDK = swiftPkg.includes('gurulu-ios-sdk');
58
+ }
59
+ let hasAndroidSDK = false;
60
+ const gradleKtsPath = path_1.default.join(projectDir, 'build.gradle.kts');
61
+ const gradlePath = path_1.default.join(projectDir, 'build.gradle');
62
+ for (const gPath of [gradleKtsPath, gradlePath]) {
63
+ if (fs_1.default.existsSync(gPath)) {
64
+ const gradle = fs_1.default.readFileSync(gPath, 'utf-8');
65
+ if (gradle.includes('gurulu') || gradle.includes('jitpack')) {
66
+ hasAndroidSDK = true;
67
+ break;
68
+ }
69
+ }
70
+ }
71
+ const hasMobileSDK = hasReactNativeSDK || hasFlutterSDK || hasIOSSDK || hasAndroidSDK;
72
+ const setupFiles = findSetupFiles(projectDir);
73
+ const hasSetup = setupFiles.length > 0 || hasWebSDK || hasMobileSDK;
74
+ const sdkLabel = hasWebSDK ? 'Web SDK installed via npm'
75
+ : hasReactNativeSDK ? 'React Native SDK installed via npm'
76
+ : hasFlutterSDK ? 'Flutter SDK found in pubspec.yaml'
77
+ : hasIOSSDK ? 'iOS SDK found in Package.swift'
78
+ : hasAndroidSDK ? 'Android SDK found in build.gradle'
79
+ : setupFiles.length > 0 ? `Setup file found: ${setupFiles[0]}`
80
+ : 'No client SDK detected';
81
+ checks.push({
82
+ name: 'Client SDK',
83
+ status: hasSetup ? 'pass' : 'fail',
84
+ message: sdkLabel,
85
+ fix: !hasSetup ? 'Run "gurulu init" to set up analytics' : undefined,
86
+ });
87
+ // 3. Server SDK
88
+ checks.push({
89
+ name: 'Server SDK',
90
+ status: hasNodeSDK ? 'pass' : 'warn',
91
+ message: hasNodeSDK ? '@gurulu/node installed' : 'Server SDK not installed (optional)',
92
+ fix: !hasNodeSDK ? 'Run "gurulu add-server" for server-side tracking' : undefined,
93
+ });
94
+ // 4. Site ID configured
95
+ checks.push({
96
+ name: 'Site ID',
97
+ status: siteId ? 'pass' : 'fail',
98
+ message: siteId ? `Site ID: ${siteId}` : 'No site ID configured',
99
+ fix: !siteId ? 'Run "gurulu login" to authenticate and select a site' : undefined,
100
+ });
101
+ // 5. Authentication
102
+ checks.push({
103
+ name: 'Authentication',
104
+ status: profile ? 'pass' : 'fail',
105
+ message: profile ? 'Authenticated' : 'Not authenticated',
106
+ fix: !profile ? 'Run "gurulu login" with your Personal API Key' : undefined,
107
+ });
108
+ // 6. Token validation (if we have a profile and siteId)
109
+ if (profile && siteId) {
110
+ try {
111
+ const data = await (0, api_client_1.cliApiJson)(`/api/cli/sites/${encodeURIComponent(siteId)}`, { preloadedProfile: profile });
112
+ const hasToken = !!data.site?.publishableKey;
113
+ checks.push({
114
+ name: 'Token Valid',
115
+ status: hasToken ? 'pass' : 'fail',
116
+ message: hasToken ? 'Site token is valid' : 'Token could not be verified',
117
+ });
118
+ }
119
+ catch (err) {
120
+ checks.push({
121
+ name: 'Token Valid',
122
+ status: 'fail',
123
+ message: `Token validation failed: ${err.message}`,
124
+ fix: 'Check your API key and site ID, or run "gurulu login" again',
125
+ });
126
+ }
127
+ }
128
+ else {
129
+ checks.push({
130
+ name: 'Token Valid',
131
+ status: 'skip',
132
+ message: 'Skipped (authentication required)',
133
+ });
134
+ }
135
+ // 7. Ingest connection + 8. Events flowing — via /api/cli/sites/:id/health
136
+ if (profile && siteId) {
137
+ try {
138
+ const health = await (0, api_client_1.cliApiJson)(`/api/cli/sites/${encodeURIComponent(siteId)}/health`, {
139
+ preloadedProfile: profile,
140
+ });
141
+ checks.push({
142
+ name: 'Ingest Connection',
143
+ status: health.checks.ingest === 'ok' ? 'pass' : 'fail',
144
+ message: health.checks.ingest === 'ok'
145
+ ? 'Ingest endpoint is healthy'
146
+ : 'Ingest check failed — site may be archived or missing a valid token',
147
+ fix: health.checks.ingest !== 'ok'
148
+ ? 'Verify site exists and has a valid publishable key'
149
+ : undefined,
150
+ });
151
+ const chOk = health.checks.clickhouse === 'ok';
152
+ const pgOk = health.checks.postgres === 'ok';
153
+ const allOk = chOk && pgOk;
154
+ checks.push({
155
+ name: 'Events Flowing',
156
+ status: allOk ? 'pass' : chOk || pgOk ? 'warn' : 'fail',
157
+ message: allOk
158
+ ? 'ClickHouse and Postgres are reachable'
159
+ : `Backend health: postgres=${health.checks.postgres}, clickhouse=${health.checks.clickhouse}`,
160
+ fix: !allOk ? 'Check server logs or contact support' : undefined,
161
+ });
162
+ }
163
+ catch (err) {
164
+ checks.push({
165
+ name: 'Ingest Connection',
166
+ status: 'fail',
167
+ message: `Health check failed: ${err.message}`,
168
+ fix: 'Check your API key and site ID, or run "gurulu login" again',
169
+ });
170
+ checks.push({
171
+ name: 'Events Flowing',
172
+ status: 'skip',
173
+ message: 'Skipped (health check unavailable)',
174
+ });
175
+ }
176
+ }
177
+ else if (profile && !siteId) {
178
+ // Try to resolve a site automatically
179
+ try {
180
+ const sitesData = await (0, api_client_1.cliApiJson)('/api/cli/sites', { preloadedProfile: profile });
181
+ if (sitesData.sites?.length) {
182
+ const resolvedId = sitesData.sites[0].id;
183
+ const health = await (0, api_client_1.cliApiJson)(`/api/cli/sites/${encodeURIComponent(resolvedId)}/health`, {
184
+ preloadedProfile: profile,
185
+ });
186
+ checks.push({
187
+ name: 'Ingest Connection',
188
+ status: health.checks.ingest === 'ok' ? 'pass' : 'fail',
189
+ message: health.checks.ingest === 'ok'
190
+ ? `Ingest healthy (auto-resolved site ${resolvedId})`
191
+ : 'Ingest check failed',
192
+ fix: health.checks.ingest !== 'ok'
193
+ ? 'Verify site exists and has a valid publishable key'
194
+ : undefined,
195
+ });
196
+ const chOk = health.checks.clickhouse === 'ok';
197
+ const pgOk = health.checks.postgres === 'ok';
198
+ const allOk = chOk && pgOk;
199
+ checks.push({
200
+ name: 'Events Flowing',
201
+ status: allOk ? 'pass' : chOk || pgOk ? 'warn' : 'fail',
202
+ message: allOk
203
+ ? 'ClickHouse and Postgres are reachable'
204
+ : `Backend health: postgres=${health.checks.postgres}, clickhouse=${health.checks.clickhouse}`,
205
+ fix: !allOk ? 'Check server logs or contact support' : undefined,
206
+ });
207
+ }
208
+ else {
209
+ checks.push({
210
+ name: 'Ingest Connection',
211
+ status: 'skip',
212
+ message: 'No sites found — create one with "gurulu init"',
213
+ });
214
+ checks.push({
215
+ name: 'Events Flowing',
216
+ status: 'skip',
217
+ message: 'Skipped (no sites)',
218
+ });
219
+ }
220
+ }
221
+ catch {
222
+ checks.push({
223
+ name: 'Ingest Connection',
224
+ status: 'skip',
225
+ message: 'Could not resolve site automatically',
226
+ fix: 'Pass --site <id> or run "gurulu init"',
227
+ });
228
+ checks.push({
229
+ name: 'Events Flowing',
230
+ status: 'skip',
231
+ message: 'Skipped (no site resolved)',
232
+ });
233
+ }
234
+ }
235
+ else {
236
+ checks.push({
237
+ name: 'Ingest Connection',
238
+ status: 'skip',
239
+ message: 'Skipped (authentication required)',
240
+ });
241
+ checks.push({
242
+ name: 'Events Flowing',
243
+ status: 'skip',
244
+ message: 'Skipped (authentication required)',
245
+ });
246
+ }
247
+ // 9. Env file check
248
+ const envFiles = ['.env.local', '.env', '.env.development'];
249
+ let envHasSiteId = false;
250
+ for (const envFile of envFiles) {
251
+ const envPath = path_1.default.join(projectDir, envFile);
252
+ if (fs_1.default.existsSync(envPath)) {
253
+ const content = fs_1.default.readFileSync(envPath, 'utf-8');
254
+ if (content.includes('GURULU_SITE_ID')) {
255
+ envHasSiteId = true;
256
+ break;
257
+ }
258
+ }
259
+ }
260
+ if (framework !== 'html' && framework !== 'unknown') {
261
+ checks.push({
262
+ name: 'Env Config',
263
+ status: envHasSiteId ? 'pass' : 'warn',
264
+ message: envHasSiteId ? 'Site ID found in env file' : 'No GURULU_SITE_ID in env files',
265
+ fix: !envHasSiteId ? 'Run "gurulu init" to auto-configure .env.local' : undefined,
266
+ });
267
+ }
268
+ // Output
269
+ if (args.json) {
270
+ console.log(JSON.stringify({ framework, checks }, null, 2));
271
+ return;
272
+ }
273
+ (0, ui_1.banner)();
274
+ console.log((0, ui_1.bold)(' Diagnostics'));
275
+ console.log('');
276
+ const icons = {
277
+ pass: (0, ui_1.green)('\u2713'),
278
+ fail: (0, ui_1.red)('\u2717'),
279
+ warn: (0, ui_1.yellow)('\u26A0'),
280
+ skip: (0, ui_1.dim)('-'),
281
+ };
282
+ let failCount = 0;
283
+ let warnCount = 0;
284
+ for (const check of checks) {
285
+ const icon = icons[check.status];
286
+ console.log(` ${icon} ${(0, ui_1.bold)(check.name)}: ${check.message}`);
287
+ if (check.fix) {
288
+ console.log(` ${(0, ui_1.dim)('\u2192')} ${(0, ui_1.dim)(check.fix)}`);
289
+ }
290
+ if (check.status === 'fail')
291
+ failCount++;
292
+ if (check.status === 'warn')
293
+ warnCount++;
294
+ }
295
+ console.log('');
296
+ if (failCount === 0 && warnCount === 0) {
297
+ (0, ui_1.success)('All checks passed! Your setup looks good.');
298
+ }
299
+ else if (failCount === 0) {
300
+ (0, ui_1.info)(`${warnCount} warning(s), but no critical issues.`);
301
+ }
302
+ else {
303
+ (0, ui_1.error)(`${failCount} issue(s) found. Fix them to get analytics working.`);
304
+ }
305
+ console.log('');
306
+ }
307
+ function findSetupFiles(projectDir) {
308
+ const candidates = [
309
+ 'src/lib/gurulu.tsx',
310
+ 'src/lib/gurulu.ts',
311
+ 'src/gurulu.ts',
312
+ 'src/plugins/gurulu.ts',
313
+ 'plugins/gurulu.client.ts',
314
+ 'src/components/Gurulu.astro',
315
+ 'src/gurulu.middleware.ts',
316
+ ];
317
+ return candidates.filter(f => fs_1.default.existsSync(path_1.default.join(projectDir, f)));
318
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Phase 19.5 W2 B1 — `gurulu events list` / `gurulu events tail`.
3
+ *
4
+ * Replaces the legacy Phase 10 `events` command (which talked to a
5
+ * now-defunct credential store) with CLI-bearer-auth subcommands that hit
6
+ * `/api/cli/events` (list) and `/api/cli/events/tail` (SSE stream).
7
+ *
8
+ * All subcommands support `--json` for scripting + agent parsing.
9
+ */
10
+ import { loadActiveProfile } from '../config';
11
+ export interface EventsArgs {
12
+ action?: string;
13
+ site?: string;
14
+ eventName?: string;
15
+ since?: string;
16
+ filter?: string;
17
+ limit?: number;
18
+ json?: boolean;
19
+ profile?: string;
20
+ }
21
+ export declare function eventsCommand(args: EventsArgs): Promise<void>;
22
+ export declare function legacyEventsCommand(args: {
23
+ site?: string;
24
+ json?: boolean;
25
+ profile?: string;
26
+ }): Promise<void>;
27
+ export declare const _loadActiveProfile: typeof loadActiveProfile;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 19.5 W2 B1 — `gurulu events list` / `gurulu events tail`.
4
+ *
5
+ * Replaces the legacy Phase 10 `events` command (which talked to a
6
+ * now-defunct credential store) with CLI-bearer-auth subcommands that hit
7
+ * `/api/cli/events` (list) and `/api/cli/events/tail` (SSE stream).
8
+ *
9
+ * All subcommands support `--json` for scripting + agent parsing.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports._loadActiveProfile = void 0;
13
+ exports.eventsCommand = eventsCommand;
14
+ exports.legacyEventsCommand = legacyEventsCommand;
15
+ const api_client_1 = require("../api-client");
16
+ const config_1 = require("../config");
17
+ const ui_1 = require("../utils/ui");
18
+ async function eventsCommand(args) {
19
+ const action = args.action || 'list';
20
+ switch (action) {
21
+ case 'list':
22
+ return listCmd(args);
23
+ case 'tail':
24
+ return tailCmd(args);
25
+ default:
26
+ (0, ui_1.error)(`Unknown events action: ${action}`);
27
+ (0, ui_1.info)('Usage: gurulu events [list|tail]');
28
+ process.exit(1);
29
+ }
30
+ }
31
+ function parseFilterFlag(raw) {
32
+ if (!raw)
33
+ return {};
34
+ // Support `event_name:foo` or `name=foo`
35
+ const m = raw.match(/^(?:event_name|name)\s*[:=]\s*(.+)$/);
36
+ if (m)
37
+ return { eventName: m[1] };
38
+ return { eventName: raw };
39
+ }
40
+ async function listCmd(args) {
41
+ const qs = new URLSearchParams();
42
+ if (args.site)
43
+ qs.set('site', args.site);
44
+ const filter = parseFilterFlag(args.filter);
45
+ const eventName = args.eventName || filter.eventName;
46
+ if (eventName)
47
+ qs.set('event_name', eventName);
48
+ if (args.since)
49
+ qs.set('since', args.since);
50
+ if (args.limit)
51
+ qs.set('limit', String(args.limit));
52
+ const path = `/api/cli/events${qs.toString() ? `?${qs.toString()}` : ''}`;
53
+ const body = await (0, api_client_1.cliApiJson)(path, {
54
+ profile: args.profile,
55
+ });
56
+ if (args.json) {
57
+ process.stdout.write(JSON.stringify(body, null, 2) + '\n');
58
+ return;
59
+ }
60
+ if (body.warning) {
61
+ process.stderr.write((0, ui_1.dim)(`(warning: ${body.warning})\n`));
62
+ }
63
+ const events = body.events || [];
64
+ if (events.length === 0) {
65
+ (0, ui_1.info)('No events found in the selected window.');
66
+ return;
67
+ }
68
+ process.stdout.write(['TS', 'EVENT', 'SITE', 'URL'].join('\t') + '\n');
69
+ for (const e of events) {
70
+ process.stdout.write([
71
+ String(e.event_ts || '-'),
72
+ String(e.event_name || '-'),
73
+ String(e.site_id || '-'),
74
+ String(e.url || '-').slice(0, 60),
75
+ ].join('\t') + '\n');
76
+ }
77
+ }
78
+ async function tailCmd(args) {
79
+ const qs = new URLSearchParams();
80
+ if (args.site)
81
+ qs.set('site', args.site);
82
+ if (args.eventName)
83
+ qs.set('event_name', args.eventName);
84
+ const path = `/api/cli/events/tail${qs.toString() ? `?${qs.toString()}` : ''}`;
85
+ // cliApi handles auth + base url; we then stream the body ourselves.
86
+ const res = await (0, api_client_1.cliApi)(path, {
87
+ profile: args.profile,
88
+ headers: { accept: 'text/event-stream' },
89
+ });
90
+ if (!res.body) {
91
+ (0, ui_1.error)('Tail stream returned no body.');
92
+ process.exit(1);
93
+ }
94
+ (0, ui_1.info)('Streaming events — press Ctrl+C to stop');
95
+ const reader = res.body.getReader?.();
96
+ if (!reader) {
97
+ // Node streams fallback
98
+ const chunks = [];
99
+ for await (const chunk of res.body) {
100
+ process.stdout.write(formatFrame(Buffer.from(chunk).toString('utf8'), !!args.json));
101
+ }
102
+ void chunks;
103
+ return;
104
+ }
105
+ const decoder = new TextDecoder();
106
+ // eslint-disable-next-line no-constant-condition
107
+ while (true) {
108
+ const { done, value } = await reader.read();
109
+ if (done)
110
+ return;
111
+ const text = decoder.decode(value, { stream: true });
112
+ process.stdout.write(formatFrame(text, !!args.json));
113
+ }
114
+ }
115
+ function formatFrame(raw, json) {
116
+ if (json)
117
+ return raw;
118
+ const out = [];
119
+ for (const block of raw.split('\n\n')) {
120
+ const lines = block.split('\n');
121
+ let ev = 'message';
122
+ let data = '';
123
+ for (const line of lines) {
124
+ if (line.startsWith('event: '))
125
+ ev = line.slice(7).trim();
126
+ else if (line.startsWith('data: '))
127
+ data += line.slice(6);
128
+ }
129
+ if (data && ev === 'realtime_event') {
130
+ try {
131
+ const parsed = JSON.parse(data);
132
+ out.push(`[${parsed.event_ts || '-'}] ${parsed.event_name || '-'} site=${parsed.site_id || '-'}\n`);
133
+ }
134
+ catch {
135
+ out.push(`[event] ${data}\n`);
136
+ }
137
+ }
138
+ }
139
+ return out.join('');
140
+ }
141
+ // Keep the legacy call-site happy: the old signature used `site/json` only.
142
+ async function legacyEventsCommand(args) {
143
+ return eventsCommand({ ...args, action: 'list' });
144
+ }
145
+ // Ensure profile helper is linked so bundlers don't tree-shake away the
146
+ // config module in edge builds.
147
+ exports._loadActiveProfile = config_1.loadActiveProfile;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Phase 19.5 W2 B7 — `gurulu experiments list|show|results`.
3
+ * Phase 20 W2 B4 — `create|update|delete|start|stop`.
4
+ */
5
+ export interface ExperimentsArgs {
6
+ action?: string;
7
+ target?: string;
8
+ conversionEvent?: string;
9
+ json?: boolean;
10
+ profile?: string;
11
+ fromFile?: string;
12
+ key?: string;
13
+ name?: string;
14
+ description?: string;
15
+ yes?: boolean;
16
+ dryRun?: boolean;
17
+ }
18
+ export declare function experimentsCommand(args: ExperimentsArgs): Promise<void>;