@gurulu/cli 0.4.0 → 0.4.2

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 (38) hide show
  1. package/dist/commands/add-server.js +13 -6
  2. package/dist/commands/alerts.d.ts +5 -0
  3. package/dist/commands/alerts.js +43 -15
  4. package/dist/commands/audiences.d.ts +3 -0
  5. package/dist/commands/audiences.js +34 -7
  6. package/dist/commands/events.d.ts +6 -0
  7. package/dist/commands/events.js +182 -1
  8. package/dist/commands/experiments.d.ts +4 -0
  9. package/dist/commands/experiments.js +46 -15
  10. package/dist/commands/funnels.d.ts +17 -0
  11. package/dist/commands/funnels.js +203 -0
  12. package/dist/commands/goals.d.ts +18 -0
  13. package/dist/commands/goals.js +214 -0
  14. package/dist/commands/install.d.ts +8 -0
  15. package/dist/commands/install.js +57 -1
  16. package/dist/commands/sourcemap.d.ts +17 -5
  17. package/dist/commands/sourcemap.js +73 -6
  18. package/dist/commands/watch.d.ts +45 -0
  19. package/dist/commands/watch.js +258 -0
  20. package/dist/frameworks/detect.js +29 -7
  21. package/dist/index.js +158 -13
  22. package/package.json +1 -1
  23. package/scripts/gurulu-agentic-install.mjs +275 -3
  24. package/scripts/gurulu-scan.lib.cjs +539 -19
  25. package/scripts/patches/auto-instrument/ast-helper.cjs +158 -10
  26. package/scripts/patches/auto-instrument/astro.cjs +12 -6
  27. package/scripts/patches/auto-instrument/express.cjs +23 -8
  28. package/scripts/patches/auto-instrument/fastify.cjs +7 -3
  29. package/scripts/patches/auto-instrument/hono.cjs +392 -0
  30. package/scripts/patches/auto-instrument/index.cjs +2 -0
  31. package/scripts/patches/auto-instrument/nestjs.cjs +7 -3
  32. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +40 -13
  33. package/scripts/patches/auto-instrument/nextjs-pages.cjs +23 -10
  34. package/scripts/patches/auto-instrument/remix.cjs +7 -3
  35. package/scripts/patches/auto-instrument/sdk-helper-map.cjs +241 -0
  36. package/scripts/patches/auto-instrument/sveltekit.cjs +7 -3
  37. package/scripts/patches/auto-instrument/vue.cjs +7 -3
  38. package/scripts/patches/index.cjs +6 -0
@@ -46,11 +46,18 @@ async function addServerCommand(args) {
46
46
  let serverApiKey = process.env.GURULU_SERVER_API_KEY;
47
47
  if (!serverApiKey && profile) {
48
48
  try {
49
- const data = await (0, api_client_1.cliApiJson)(`/api/cli/sites/${encodeURIComponent(siteId)}`, { preloadedProfile: profile });
50
- serverApiKey = data.site?.publishableKey || '';
49
+ (0, ui_1.step)('Creating server API key...');
50
+ const data = await (0, api_client_1.cliApiJson)(`/api/cli/sites/${encodeURIComponent(siteId)}/server-keys`, {
51
+ preloadedProfile: profile,
52
+ json: { name: `cli-${new Date().toISOString().slice(0, 10)}` },
53
+ });
54
+ serverApiKey = data.key || '';
55
+ if (serverApiKey) {
56
+ (0, ui_1.success)(`Server API key created (${data.keyId})`);
57
+ }
51
58
  }
52
59
  catch (err) {
53
- (0, ui_1.warn)(`Could not fetch server credentials: ${err.message}`);
60
+ (0, ui_1.warn)(`Could not create server API key: ${err.message}`);
54
61
  }
55
62
  }
56
63
  if (!serverApiKey && !args.noInteractive) {
@@ -89,9 +96,9 @@ async function addServerCommand(args) {
89
96
  }
90
97
  // Step 2: Create server config file
91
98
  const configFilePath = path_1.default.join(projectDir, 'src', 'lib', 'gurulu-server.ts');
92
- const configCode = `import { GuruluNode } from '@gurulu/node';
99
+ const configCode = `import { Gurulu } from '@gurulu/node';
93
100
 
94
- export const gurulu = new GuruluNode({
101
+ export const gurulu = new Gurulu({
95
102
  siteId: process.env.GURULU_SITE_ID || '${siteId}',
96
103
  apiKey: process.env.GURULU_SERVER_API_KEY || '',
97
104
  });
@@ -134,7 +141,7 @@ export const gurulu = new GuruluNode({
134
141
  console.log('');
135
142
  (0, ui_1.step)('Import gurulu from src/lib/gurulu-server.ts in your API routes');
136
143
  (0, ui_1.step)(`Use ${(0, ui_1.cyan)('gurulu.track(event, properties)')} to send server-side events`);
137
- (0, ui_1.step)(`Use ${(0, ui_1.cyan)('gurulu.identify(userId, traits)')} to identify users`);
144
+ (0, ui_1.step)(`Use ${(0, ui_1.cyan)("await gurulu.identify({ userId: '...', anonymousId: '...', traits: {} })")} to identify users`);
138
145
  (0, ui_1.step)(`Run ${(0, ui_1.cyan)('gurulu doctor')} to verify the setup`);
139
146
  console.log('');
140
147
  (0, ui_1.success)('Server SDK setup complete!');
@@ -12,9 +12,14 @@ export interface AlertsArgs {
12
12
  json?: boolean;
13
13
  profile?: string;
14
14
  fromFile?: string;
15
+ site?: string;
16
+ id?: string;
15
17
  name?: string;
16
18
  type?: string;
17
19
  metric?: string;
20
+ thresholdType?: string;
21
+ thresholdValue?: number;
22
+ channel?: string;
18
23
  note?: string;
19
24
  yes?: boolean;
20
25
  dryRun?: boolean;
@@ -33,6 +33,8 @@ async function alertsCommand(args) {
33
33
  }
34
34
  async function listCmd(args) {
35
35
  const qs = new URLSearchParams();
36
+ if (args.site)
37
+ qs.set('siteId', args.site);
36
38
  if (args.severity)
37
39
  qs.set('severity', args.severity);
38
40
  if (args.acknowledged)
@@ -81,12 +83,28 @@ async function createCmd(args) {
81
83
  let payload = {};
82
84
  if (args.fromFile)
83
85
  payload = (0, from_file_1.loadFromFile)(args.fromFile);
86
+ if (args.site)
87
+ payload.siteId = args.site;
88
+ if (args.name)
89
+ payload.name = args.name;
90
+ if (args.type)
91
+ payload.type = args.type;
84
92
  if (args.metric)
85
93
  payload.metric = args.metric;
86
94
  if (args.severity)
87
95
  payload.severity = args.severity;
88
- if (!payload.metric || !payload.severity) {
89
- (0, ui_1.error)('metric and severity are required.');
96
+ if (args.thresholdType)
97
+ payload.thresholdType = args.thresholdType;
98
+ if (args.thresholdValue !== undefined)
99
+ payload.thresholdValue = args.thresholdValue;
100
+ if (args.channel)
101
+ payload.channelId = args.channel;
102
+ if (!payload.name) {
103
+ (0, ui_1.error)('name is required (pass --name or --from-file)');
104
+ process.exit(1);
105
+ }
106
+ if (!payload.siteId) {
107
+ (0, ui_1.error)('site is required (pass --site)');
90
108
  process.exit(1);
91
109
  }
92
110
  if (args.dryRun) {
@@ -98,7 +116,7 @@ async function createCmd(args) {
98
116
  (0, dry_run_1.printDryRun)(body, args.json);
99
117
  return;
100
118
  }
101
- const ok = await (0, confirm_1.promptConfirm)(`Create alert '${payload.metric}'?`, {
119
+ const ok = await (0, confirm_1.promptConfirm)(`Create alert '${payload.name}'?`, {
102
120
  yes: args.yes,
103
121
  defaultYes: true,
104
122
  });
@@ -116,51 +134,61 @@ async function createCmd(args) {
116
134
  (0, ui_1.success)(`Created alert ${body.alert?.id ?? ''}`);
117
135
  }
118
136
  async function updateCmd(args) {
119
- if (!args.target) {
120
- (0, ui_1.error)('Usage: gurulu alerts update <id> [--note ...] [--ack]');
137
+ const resolveTarget = args.id || args.target;
138
+ if (!resolveTarget) {
139
+ (0, ui_1.error)('Usage: gurulu alerts update <id> [--id ...] [--name ...] [--threshold-value ...]');
121
140
  process.exit(1);
122
141
  }
123
142
  const payload = {};
143
+ if (args.name)
144
+ payload.name = args.name;
124
145
  if (args.note !== undefined)
125
146
  payload.note = args.note;
147
+ if (args.thresholdType)
148
+ payload.thresholdType = args.thresholdType;
149
+ if (args.thresholdValue !== undefined)
150
+ payload.thresholdValue = args.thresholdValue;
151
+ if (args.channel)
152
+ payload.channelId = args.channel;
126
153
  if (args.ack === true)
127
154
  payload.acknowledge = true;
128
155
  if (args.dryRun) {
129
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}?dryRun=1`, { profile: args.profile, method: 'PATCH', json: payload });
156
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'PATCH', json: payload });
130
157
  (0, dry_run_1.printDryRun)(body, args.json);
131
158
  return;
132
159
  }
133
- const ok = await (0, confirm_1.promptConfirm)(`Update alert '${args.target}'?`, {
160
+ const ok = await (0, confirm_1.promptConfirm)(`Update alert '${resolveTarget}'?`, {
134
161
  yes: args.yes,
135
162
  defaultYes: true,
136
163
  });
137
164
  if (!ok)
138
165
  return (0, ui_1.info)('Aborted.');
139
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}`, { profile: args.profile, method: 'PATCH', json: payload });
166
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}`, { profile: args.profile, method: 'PATCH', json: payload });
140
167
  if (args.json) {
141
168
  process.stdout.write(JSON.stringify(body, null, 2) + '\n');
142
169
  return;
143
170
  }
144
- (0, ui_1.success)(`Updated alert ${args.target}`);
171
+ (0, ui_1.success)(`Updated alert ${resolveTarget}`);
145
172
  }
146
173
  async function deleteCmd(args) {
147
- if (!args.target) {
148
- (0, ui_1.error)('Usage: gurulu alerts delete <id>');
174
+ const resolveTarget = args.id || args.target;
175
+ if (!resolveTarget) {
176
+ (0, ui_1.error)('Usage: gurulu alerts delete <id> [--id ...]');
149
177
  process.exit(1);
150
178
  }
151
179
  if (args.dryRun) {
152
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}?dryRun=1`, { profile: args.profile, method: 'DELETE' });
180
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'DELETE' });
153
181
  (0, dry_run_1.printDryRun)(body, args.json);
154
182
  return;
155
183
  }
156
- const ok = await (0, confirm_1.promptConfirm)(`About to delete alert '${args.target}'. Continue?`, { yes: args.yes, defaultYes: false });
184
+ const ok = await (0, confirm_1.promptConfirm)(`About to delete alert '${resolveTarget}'. Continue?`, { yes: args.yes, defaultYes: false });
157
185
  if (!ok)
158
186
  return (0, ui_1.info)('Aborted.');
159
- await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(args.target)}`, {
187
+ await (0, api_client_1.cliApiJson)(`/api/cli/alerts/${encodeURIComponent(resolveTarget)}`, {
160
188
  profile: args.profile,
161
189
  method: 'DELETE',
162
190
  });
163
- (0, ui_1.success)(`Deleted alert ${args.target}`);
191
+ (0, ui_1.success)(`Deleted alert ${resolveTarget}`);
164
192
  }
165
193
  // ── channels ──────────────────────────────────────────────────────────────
166
194
  async function channelsCmd(args) {
@@ -8,8 +8,11 @@ export interface AudiencesArgs {
8
8
  json?: boolean;
9
9
  profile?: string;
10
10
  fromFile?: string;
11
+ site?: string;
12
+ id?: string;
11
13
  name?: string;
12
14
  description?: string;
15
+ rules?: string;
13
16
  yes?: boolean;
14
17
  dryRun?: boolean;
15
18
  }
@@ -30,7 +30,8 @@ async function audiencesCommand(args) {
30
30
  }
31
31
  }
32
32
  async function listCmd(args) {
33
- const body = await (0, api_client_1.cliApiJson)('/api/cli/audiences', {
33
+ const qs = args.site ? `?siteId=${encodeURIComponent(args.site)}` : '';
34
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences${qs}`, {
34
35
  profile: args.profile,
35
36
  });
36
37
  const audiences = body.audiences || [];
@@ -88,12 +89,27 @@ async function createCmd(args) {
88
89
  }
89
90
  if (args.name)
90
91
  payload.name = args.name;
92
+ if (args.site)
93
+ payload.siteId = args.site;
91
94
  if (args.description !== undefined)
92
95
  payload.description = args.description;
96
+ if (args.rules) {
97
+ try {
98
+ payload.rules = JSON.parse(args.rules);
99
+ }
100
+ catch {
101
+ (0, ui_1.error)('--rules must be valid JSON');
102
+ process.exit(1);
103
+ }
104
+ }
93
105
  if (!payload.name) {
94
106
  (0, ui_1.error)('name is required (pass --name or --from-file)');
95
107
  process.exit(1);
96
108
  }
109
+ if (!payload.siteId) {
110
+ (0, ui_1.error)('site is required (pass --site)');
111
+ process.exit(1);
112
+ }
97
113
  const qs = args.dryRun ? '?dryRun=1' : '';
98
114
  if (args.dryRun) {
99
115
  const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences${qs}`, {
@@ -121,11 +137,12 @@ async function createCmd(args) {
121
137
  (0, ui_1.success)(`Created audience ${body.audience?.id ?? ''}`);
122
138
  }
123
139
  async function updateCmd(args) {
124
- if (!args.target) {
125
- (0, ui_1.error)('Usage: gurulu audiences update <name-or-id> [--from-file <path>] [--name ...]');
140
+ const resolveTarget = args.id || args.target;
141
+ if (!resolveTarget) {
142
+ (0, ui_1.error)('Usage: gurulu audiences update <name-or-id> [--id ...] [--name ...] [--rules ...]');
126
143
  process.exit(1);
127
144
  }
128
- const id = await resolveId(args.target, args.profile);
145
+ const id = await resolveId(resolveTarget, args.profile);
129
146
  let payload = {};
130
147
  if (args.fromFile) {
131
148
  payload = (0, from_file_1.loadFromFile)(args.fromFile);
@@ -134,6 +151,15 @@ async function updateCmd(args) {
134
151
  payload.name = args.name;
135
152
  if (args.description !== undefined)
136
153
  payload.description = args.description;
154
+ if (args.rules) {
155
+ try {
156
+ payload.rules = JSON.parse(args.rules);
157
+ }
158
+ catch {
159
+ (0, ui_1.error)('--rules must be valid JSON');
160
+ process.exit(1);
161
+ }
162
+ }
137
163
  const qs = args.dryRun ? '?dryRun=1' : '';
138
164
  if (args.dryRun) {
139
165
  const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences/${encodeURIComponent(id)}${qs}`, { profile: args.profile, method: 'PATCH', json: payload });
@@ -156,11 +182,12 @@ async function updateCmd(args) {
156
182
  (0, ui_1.success)(`Updated audience ${id}`);
157
183
  }
158
184
  async function deleteCmd(args) {
159
- if (!args.target) {
160
- (0, ui_1.error)('Usage: gurulu audiences delete <name-or-id>');
185
+ const resolveTarget = args.id || args.target;
186
+ if (!resolveTarget) {
187
+ (0, ui_1.error)('Usage: gurulu audiences delete <name-or-id> [--id ...]');
161
188
  process.exit(1);
162
189
  }
163
- const id = await resolveId(args.target, args.profile);
190
+ const id = await resolveId(resolveTarget, args.profile);
164
191
  const qs = args.dryRun ? '?dryRun=1' : '';
165
192
  if (args.dryRun) {
166
193
  const body = await (0, api_client_1.cliApiJson)(`/api/cli/audiences/${encodeURIComponent(id)}${qs}`, { profile: args.profile, method: 'DELETE' });
@@ -17,6 +17,12 @@ export interface EventsArgs {
17
17
  limit?: number;
18
18
  json?: boolean;
19
19
  profile?: string;
20
+ displayName?: string;
21
+ description?: string;
22
+ category?: string;
23
+ properties?: string;
24
+ event?: string;
25
+ vertical?: string;
20
26
  }
21
27
  export declare function eventsCommand(args: EventsArgs): Promise<void>;
22
28
  export declare function legacyEventsCommand(args: {
@@ -22,9 +22,17 @@ async function eventsCommand(args) {
22
22
  return listCmd(args);
23
23
  case 'tail':
24
24
  return tailCmd(args);
25
+ case 'schema':
26
+ return schemaCmd(args);
27
+ case 'define':
28
+ return defineCmd(args);
29
+ case 'verify':
30
+ return verifyCmd(args);
31
+ case 'templates':
32
+ return templatesCmd(args);
25
33
  default:
26
34
  (0, ui_1.error)(`Unknown events action: ${action}`);
27
- (0, ui_1.info)('Usage: gurulu events [list|tail]');
35
+ (0, ui_1.info)('Usage: gurulu events [list|tail|schema|define|verify|templates]');
28
36
  process.exit(1);
29
37
  }
30
38
  }
@@ -112,6 +120,179 @@ async function tailCmd(args) {
112
120
  process.stdout.write(formatFrame(text, !!args.json));
113
121
  }
114
122
  }
123
+ /* ── schema: show property schema & implementation examples ───────── */
124
+ async function schemaCmd(args) {
125
+ if (!args.eventName) {
126
+ (0, ui_1.error)('Event name is required. Usage: gurulu events schema --event-name <name> --site <id>');
127
+ process.exit(1);
128
+ }
129
+ if (!args.site) {
130
+ (0, ui_1.error)('--site is required for schema lookup.');
131
+ process.exit(1);
132
+ }
133
+ const path = `/api/cli/events/schema/${encodeURIComponent(args.eventName)}?siteId=${args.site}`;
134
+ const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
135
+ if (args.json) {
136
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
137
+ return;
138
+ }
139
+ console.log((0, ui_1.bold)(`\nEvent: ${data.eventName}`));
140
+ if (data.source)
141
+ console.log(`Source: ${data.source}`);
142
+ if (data.description)
143
+ console.log(`Description: ${data.description}`);
144
+ if (data.isRevenue)
145
+ console.log((0, ui_1.yellow)(`Revenue event: ${data.revenueNote || 'yes'}`));
146
+ if (data.propertySchema?.length) {
147
+ console.log((0, ui_1.bold)('\nProperties:'));
148
+ for (const p of data.propertySchema) {
149
+ const req = p.required ? (0, ui_1.red)('REQUIRED') : (0, ui_1.dim)('optional');
150
+ const fmt = p.format ? (0, ui_1.cyan)(` [${p.format}]`) : '';
151
+ const ex = p.example !== undefined ? (0, ui_1.dim)(` e.g. ${JSON.stringify(p.example)}`) : '';
152
+ console.log(` ${p.name}: ${(0, ui_1.cyan)(p.type)} (${req})${fmt} — ${p.description || ''}${ex}`);
153
+ }
154
+ }
155
+ if (data.implementation && Object.keys(data.implementation).length) {
156
+ console.log((0, ui_1.bold)('\nImplementation:'));
157
+ for (const [platform, code] of Object.entries(data.implementation)) {
158
+ console.log(` ${(0, ui_1.cyan)(`[${platform}]`)}: ${code}`);
159
+ }
160
+ }
161
+ console.log('');
162
+ }
163
+ /* ── define: create a new custom event definition ────────────────── */
164
+ async function defineCmd(args) {
165
+ if (!args.site) {
166
+ (0, ui_1.error)('--site is required.');
167
+ process.exit(1);
168
+ }
169
+ if (!args.eventName) {
170
+ (0, ui_1.error)('--event-name is required.');
171
+ process.exit(1);
172
+ }
173
+ if (!args.displayName) {
174
+ (0, ui_1.error)('--display-name is required.');
175
+ process.exit(1);
176
+ }
177
+ let propertySchema = [];
178
+ if (args.properties) {
179
+ try {
180
+ propertySchema = JSON.parse(args.properties);
181
+ }
182
+ catch {
183
+ (0, ui_1.error)('Invalid JSON for --properties');
184
+ process.exit(1);
185
+ }
186
+ }
187
+ const eventName = args.eventName.startsWith('$') ? args.eventName : `$${args.eventName}`;
188
+ const body = {
189
+ siteId: args.site,
190
+ eventName,
191
+ displayName: args.displayName,
192
+ description: args.description || '',
193
+ intentName: args.category || 'engagement',
194
+ fingerprint: { propertySchema },
195
+ };
196
+ const data = await (0, api_client_1.cliApiJson)('/api/cli/events/definitions', { profile: args.profile, method: 'POST', json: body });
197
+ if (args.json) {
198
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
199
+ return;
200
+ }
201
+ (0, ui_1.success)(`Event ${eventName} defined successfully`);
202
+ (0, ui_1.info)(`Run: gurulu events schema --event-name ${eventName} --site ${args.site} to see implementation examples`);
203
+ }
204
+ /* ── verify: check event health / quality ────────────────────────── */
205
+ async function verifyCmd(args) {
206
+ if (!args.site) {
207
+ (0, ui_1.error)('--site is required.');
208
+ process.exit(1);
209
+ }
210
+ const qs = new URLSearchParams();
211
+ qs.set('siteId', args.site);
212
+ if (args.event)
213
+ qs.set('eventName', args.event);
214
+ const path = `/api/cli/events/health?${qs.toString()}`;
215
+ const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
216
+ if (args.json) {
217
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
218
+ return;
219
+ }
220
+ console.log((0, ui_1.bold)('\nEvent Health Report'));
221
+ console.log(`Site: ${args.site}`);
222
+ console.log('');
223
+ if (Array.isArray(data.events)) {
224
+ for (const ev of data.events) {
225
+ const statusIcon = ev.status === 'active'
226
+ ? (0, ui_1.green)('●')
227
+ : ev.status === 'stale'
228
+ ? (0, ui_1.yellow)('●')
229
+ : (0, ui_1.red)('●');
230
+ console.log(`${statusIcon} ${ev.event_name} — ${ev.total_count ?? 0} events (last: ${ev.last_seen ?? 'never'})`);
231
+ if (ev.missing_properties?.length) {
232
+ console.log((0, ui_1.yellow)(` Missing required properties: ${ev.missing_properties.join(', ')}`));
233
+ }
234
+ }
235
+ }
236
+ else if (data.total_events !== undefined) {
237
+ console.log(`Total events (24h): ${data.total_events}`);
238
+ console.log(`Unique event types: ${data.unique_types ?? 'N/A'}`);
239
+ }
240
+ console.log('');
241
+ }
242
+ /* ── templates: list vertical event template catalogs ───────────── */
243
+ async function templatesCmd(args) {
244
+ const qs = new URLSearchParams();
245
+ if (args.vertical)
246
+ qs.set('vertical', args.vertical);
247
+ const path = `/api/events/templates${qs.toString() ? `?${qs.toString()}` : ''}`;
248
+ const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
249
+ if (args.json) {
250
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
251
+ return;
252
+ }
253
+ if (data.error) {
254
+ (0, ui_1.error)(data.error);
255
+ process.exit(1);
256
+ }
257
+ // List mode — no vertical specified
258
+ if (data.verticals) {
259
+ console.log((0, ui_1.bold)('\nAvailable Verticals\n'));
260
+ for (const v of data.verticals) {
261
+ console.log(` ${(0, ui_1.cyan)(v.id.padEnd(14))} ${v.name.padEnd(30)} ${(0, ui_1.dim)(`${v.eventCount} events`)}`);
262
+ }
263
+ console.log('');
264
+ (0, ui_1.info)('Usage: gurulu events templates --vertical <id>');
265
+ return;
266
+ }
267
+ // Detail mode — specific vertical
268
+ console.log((0, ui_1.bold)(`\nVertical: ${data.vertical}`));
269
+ console.log(`Confidence: ${data.confidence}\n`);
270
+ if (data.events?.length) {
271
+ console.log((0, ui_1.bold)('Events:\n'));
272
+ for (const ev of data.events) {
273
+ console.log(` ${(0, ui_1.green)(ev.name)} ${(0, ui_1.dim)(`[${ev.category}]`)}`);
274
+ console.log(` ${(0, ui_1.dim)(ev.reasoning)}`);
275
+ if (ev.propertySchema?.length) {
276
+ for (const p of ev.propertySchema) {
277
+ const req = p.required ? (0, ui_1.red)('REQUIRED') : (0, ui_1.dim)('optional');
278
+ const fmt = p.format ? (0, ui_1.cyan)(` [${p.format}]`) : '';
279
+ const ex = p.example !== undefined ? (0, ui_1.dim)(` e.g. ${JSON.stringify(p.example)}`) : '';
280
+ console.log(` ${p.name}: ${(0, ui_1.cyan)(p.type)} (${req})${fmt}${ex}`);
281
+ }
282
+ }
283
+ console.log('');
284
+ }
285
+ }
286
+ if (data.funnels?.length) {
287
+ console.log((0, ui_1.bold)('Funnels:\n'));
288
+ for (const f of data.funnels) {
289
+ console.log(` ${(0, ui_1.yellow)(f.name)} ${(0, ui_1.dim)(`[${f.category}]`)}`);
290
+ console.log(` Steps: ${f.steps.join(' → ')}`);
291
+ console.log(` ${(0, ui_1.dim)(f.reasoning)}`);
292
+ console.log('');
293
+ }
294
+ }
295
+ }
115
296
  function formatFrame(raw, json) {
116
297
  if (json)
117
298
  return raw;
@@ -9,9 +9,13 @@ export interface ExperimentsArgs {
9
9
  json?: boolean;
10
10
  profile?: string;
11
11
  fromFile?: string;
12
+ site?: string;
13
+ id?: string;
12
14
  key?: string;
13
15
  name?: string;
14
16
  description?: string;
17
+ variants?: string;
18
+ goal?: string;
15
19
  yes?: boolean;
16
20
  dryRun?: boolean;
17
21
  }
@@ -36,7 +36,8 @@ async function experimentsCommand(args) {
36
36
  }
37
37
  }
38
38
  async function listCmd(args) {
39
- const body = await (0, api_client_1.cliApiJson)('/api/cli/experiments', {
39
+ const qs = args.site ? `?siteId=${encodeURIComponent(args.site)}` : '';
40
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments${qs}`, {
40
41
  profile: args.profile,
41
42
  });
42
43
  if (args.json) {
@@ -109,14 +110,31 @@ async function createCmd(args) {
109
110
  let payload = {};
110
111
  if (args.fromFile)
111
112
  payload = (0, from_file_1.loadFromFile)(args.fromFile);
113
+ if (args.site)
114
+ payload.siteId = args.site;
112
115
  if (args.key)
113
116
  payload.key = args.key;
114
117
  if (args.name)
115
118
  payload.name = args.name;
116
119
  if (args.description !== undefined)
117
120
  payload.description = args.description;
118
- if (!payload.key || !payload.name || !Array.isArray(payload.variants)) {
119
- (0, ui_1.error)('key, name, and variants[] are required.');
121
+ if (args.goal)
122
+ payload.goalId = args.goal;
123
+ if (args.variants) {
124
+ try {
125
+ payload.variants = JSON.parse(args.variants);
126
+ }
127
+ catch {
128
+ (0, ui_1.error)('--variants must be valid JSON');
129
+ process.exit(1);
130
+ }
131
+ }
132
+ if (!payload.name || !Array.isArray(payload.variants)) {
133
+ (0, ui_1.error)('name and variants[] are required (pass --name --variants \'[...]\' or --from-file).');
134
+ process.exit(1);
135
+ }
136
+ if (!payload.siteId) {
137
+ (0, ui_1.error)('site is required (pass --site)');
120
138
  process.exit(1);
121
139
  }
122
140
  if (args.dryRun) {
@@ -146,8 +164,9 @@ async function createCmd(args) {
146
164
  (0, ui_1.success)(`Created experiment ${body.experiment?.key ?? ''}`);
147
165
  }
148
166
  async function updateCmd(args) {
149
- if (!args.target) {
150
- (0, ui_1.error)('Usage: gurulu experiments update <key>');
167
+ const resolveTarget = args.id || args.target;
168
+ if (!resolveTarget) {
169
+ (0, ui_1.error)('Usage: gurulu experiments update <key> [--id ...] [--name ...] [--variants ...]');
151
170
  process.exit(1);
152
171
  }
153
172
  let payload = {};
@@ -157,39 +176,51 @@ async function updateCmd(args) {
157
176
  payload.name = args.name;
158
177
  if (args.description !== undefined)
159
178
  payload.description = args.description;
179
+ if (args.goal)
180
+ payload.goalId = args.goal;
181
+ if (args.variants) {
182
+ try {
183
+ payload.variants = JSON.parse(args.variants);
184
+ }
185
+ catch {
186
+ (0, ui_1.error)('--variants must be valid JSON');
187
+ process.exit(1);
188
+ }
189
+ }
160
190
  if (args.dryRun) {
161
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}?dryRun=1`, { profile: args.profile, method: 'PATCH', json: payload });
191
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'PATCH', json: payload });
162
192
  (0, dry_run_1.printDryRun)(body, args.json);
163
193
  return;
164
194
  }
165
- const ok = await (0, confirm_1.promptConfirm)(`Update experiment '${args.target}'?`, {
195
+ const ok = await (0, confirm_1.promptConfirm)(`Update experiment '${resolveTarget}'?`, {
166
196
  yes: args.yes,
167
197
  defaultYes: true,
168
198
  });
169
199
  if (!ok)
170
200
  return (0, ui_1.info)('Aborted.');
171
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}`, { profile: args.profile, method: 'PATCH', json: payload });
201
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(resolveTarget)}`, { profile: args.profile, method: 'PATCH', json: payload });
172
202
  if (args.json) {
173
203
  process.stdout.write(JSON.stringify(body, null, 2) + '\n');
174
204
  return;
175
205
  }
176
- (0, ui_1.success)(`Updated experiment ${args.target}`);
206
+ (0, ui_1.success)(`Updated experiment ${resolveTarget}`);
177
207
  }
178
208
  async function deleteCmd(args) {
179
- if (!args.target) {
180
- (0, ui_1.error)('Usage: gurulu experiments delete <key>');
209
+ const resolveTarget = args.id || args.target;
210
+ if (!resolveTarget) {
211
+ (0, ui_1.error)('Usage: gurulu experiments delete <key> [--id ...]');
181
212
  process.exit(1);
182
213
  }
183
214
  if (args.dryRun) {
184
- const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}?dryRun=1`, { profile: args.profile, method: 'DELETE' });
215
+ const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'DELETE' });
185
216
  (0, dry_run_1.printDryRun)(body, args.json);
186
217
  return;
187
218
  }
188
- const ok = await (0, confirm_1.promptConfirm)(`About to delete experiment '${args.target}'. Continue?`, { yes: args.yes, defaultYes: false });
219
+ const ok = await (0, confirm_1.promptConfirm)(`About to delete experiment '${resolveTarget}'. Continue?`, { yes: args.yes, defaultYes: false });
189
220
  if (!ok)
190
221
  return (0, ui_1.info)('Aborted.');
191
- await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}`, { profile: args.profile, method: 'DELETE' });
192
- (0, ui_1.success)(`Deleted experiment ${args.target}`);
222
+ await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(resolveTarget)}`, { profile: args.profile, method: 'DELETE' });
223
+ (0, ui_1.success)(`Deleted experiment ${resolveTarget}`);
193
224
  }
194
225
  async function startCmd(args) {
195
226
  if (!args.target) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `gurulu funnels list|show|create|update|delete` — manage conversion funnels.
3
+ */
4
+ export interface FunnelsArgs {
5
+ action?: string;
6
+ target?: string;
7
+ json?: boolean;
8
+ profile?: string;
9
+ fromFile?: string;
10
+ site?: string;
11
+ name?: string;
12
+ description?: string;
13
+ steps?: string;
14
+ yes?: boolean;
15
+ dryRun?: boolean;
16
+ }
17
+ export declare function funnelsCommand(args: FunnelsArgs): Promise<void>;