@gurulu/cli 0.4.6 → 1.0.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.
- package/LICENSE +92 -0
- package/README.md +35 -106
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +25410 -0
- package/dist/commands/auth.d.ts +23 -20
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts +20 -6
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/init.d.ts +25 -11
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/pull.d.ts +13 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/push.d.ts +40 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/validate.d.ts +36 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24985 -853
- package/dist/lib/api.d.ts +139 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/codegen.d.ts +4 -0
- package/dist/lib/codegen.d.ts.map +1 -0
- package/dist/lib/config.d.ts +43 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/package.json +40 -20
- package/bin/gurulu.js +0 -2
- package/dist/api-client.d.ts +0 -33
- package/dist/api-client.js +0 -175
- package/dist/commands/add-server.d.ts +0 -9
- package/dist/commands/add-server.js +0 -162
- package/dist/commands/alerts.d.ts +0 -27
- package/dist/commands/alerts.js +0 -309
- package/dist/commands/api-keys.d.ts +0 -20
- package/dist/commands/api-keys.js +0 -130
- package/dist/commands/attribution.d.ts +0 -22
- package/dist/commands/attribution.js +0 -111
- package/dist/commands/audiences.d.ts +0 -23
- package/dist/commands/audiences.js +0 -243
- package/dist/commands/audit.d.ts +0 -20
- package/dist/commands/audit.js +0 -130
- package/dist/commands/auth.js +0 -249
- package/dist/commands/chat.d.ts +0 -18
- package/dist/commands/chat.js +0 -117
- package/dist/commands/config.d.ts +0 -10
- package/dist/commands/config.js +0 -92
- package/dist/commands/consent.d.ts +0 -27
- package/dist/commands/consent.js +0 -233
- package/dist/commands/conversion-paths.d.ts +0 -19
- package/dist/commands/conversion-paths.js +0 -55
- package/dist/commands/db.d.ts +0 -25
- package/dist/commands/db.js +0 -330
- package/dist/commands/destinations.d.ts +0 -20
- package/dist/commands/destinations.js +0 -191
- package/dist/commands/doctor.js +0 -360
- package/dist/commands/errors.d.ts +0 -27
- package/dist/commands/errors.js +0 -121
- package/dist/commands/events.d.ts +0 -33
- package/dist/commands/events.js +0 -349
- package/dist/commands/experiments.d.ts +0 -22
- package/dist/commands/experiments.js +0 -264
- package/dist/commands/funnels.d.ts +0 -17
- package/dist/commands/funnels.js +0 -203
- package/dist/commands/goals.d.ts +0 -18
- package/dist/commands/goals.js +0 -214
- package/dist/commands/heatmap.d.ts +0 -27
- package/dist/commands/heatmap.js +0 -112
- package/dist/commands/identity.d.ts +0 -29
- package/dist/commands/identity.js +0 -328
- package/dist/commands/init.js +0 -215
- package/dist/commands/insights.d.ts +0 -10
- package/dist/commands/insights.js +0 -65
- package/dist/commands/install.d.ts +0 -259
- package/dist/commands/install.js +0 -1590
- package/dist/commands/login.d.ts +0 -20
- package/dist/commands/login.js +0 -170
- package/dist/commands/logout.d.ts +0 -10
- package/dist/commands/logout.js +0 -41
- package/dist/commands/playground.d.ts +0 -11
- package/dist/commands/playground.js +0 -47
- package/dist/commands/releases.d.ts +0 -17
- package/dist/commands/releases.js +0 -54
- package/dist/commands/replay.d.ts +0 -18
- package/dist/commands/replay.js +0 -64
- package/dist/commands/secrets.d.ts +0 -19
- package/dist/commands/secrets.js +0 -145
- package/dist/commands/sites.d.ts +0 -18
- package/dist/commands/sites.js +0 -139
- package/dist/commands/skad.d.ts +0 -18
- package/dist/commands/skad.js +0 -53
- package/dist/commands/sourcemap.d.ts +0 -33
- package/dist/commands/sourcemap.js +0 -204
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -136
- package/dist/commands/upgrade.d.ts +0 -21
- package/dist/commands/upgrade.js +0 -183
- package/dist/commands/warehouse.d.ts +0 -20
- package/dist/commands/warehouse.js +0 -65
- package/dist/commands/warehouses.d.ts +0 -17
- package/dist/commands/warehouses.js +0 -182
- package/dist/commands/watch.d.ts +0 -45
- package/dist/commands/watch.js +0 -258
- package/dist/commands/whoami.d.ts +0 -9
- package/dist/commands/whoami.js +0 -50
- package/dist/config.d.ts +0 -75
- package/dist/config.js +0 -329
- package/dist/frameworks/detect.d.ts +0 -8
- package/dist/frameworks/detect.js +0 -444
- package/dist/install-intent-proposal.d.ts +0 -99
- package/dist/install-intent-proposal.js +0 -202
- package/dist/utils/api.d.ts +0 -20
- package/dist/utils/api.js +0 -47
- package/dist/utils/config.d.ts +0 -13
- package/dist/utils/config.js +0 -30
- package/dist/utils/confirm.d.ts +0 -17
- package/dist/utils/confirm.js +0 -40
- package/dist/utils/dry-run.d.ts +0 -20
- package/dist/utils/dry-run.js +0 -67
- package/dist/utils/from-file.d.ts +0 -9
- package/dist/utils/from-file.js +0 -72
- package/dist/utils/redact.d.ts +0 -14
- package/dist/utils/redact.js +0 -48
- package/dist/utils/ui.d.ts +0 -14
- package/dist/utils/ui.js +0 -59
- package/scripts/.gitkeep +0 -0
- package/scripts/README-gurulu-agentic-install.md +0 -114
- package/scripts/README-gurulu-scan.md +0 -98
- package/scripts/audit-cli-scopes.mjs +0 -204
- package/scripts/backfill-tenant-id.mjs +0 -172
- package/scripts/backfill-tenant-links.ts +0 -252
- package/scripts/backup-clickhouse.sh +0 -27
- package/scripts/backup-postgres.sh +0 -19
- package/scripts/bootstrap-runtime-schema.mjs +0 -87
- package/scripts/bootstrap-stripe.mjs +0 -158
- package/scripts/gurulu-agentic-install.lib.cjs +0 -762
- package/scripts/gurulu-agentic-install.mjs +0 -623
- package/scripts/gurulu-scan.lib.cjs +0 -1509
- package/scripts/gurulu-scan.mjs +0 -91
- package/scripts/gurulu-verify-install.lib.cjs +0 -334
- package/scripts/gurulu-verify-install.mjs +0 -59
- package/scripts/init-ssl.sh +0 -26
- package/scripts/migrate-flow-graph-enums.sh +0 -86
- package/scripts/monitor-disk.sh +0 -24
- package/scripts/patches/astro.patch.cjs +0 -74
- package/scripts/patches/auto-instrument/ast-helper.cjs +0 -480
- package/scripts/patches/auto-instrument/astro.cjs +0 -273
- package/scripts/patches/auto-instrument/express.cjs +0 -383
- package/scripts/patches/auto-instrument/fastify.cjs +0 -262
- package/scripts/patches/auto-instrument/hono.cjs +0 -392
- package/scripts/patches/auto-instrument/index.cjs +0 -80
- package/scripts/patches/auto-instrument/nestjs.cjs +0 -286
- package/scripts/patches/auto-instrument/nextjs-app-router.cjs +0 -345
- package/scripts/patches/auto-instrument/nextjs-pages.cjs +0 -361
- package/scripts/patches/auto-instrument/remix.cjs +0 -168
- package/scripts/patches/auto-instrument/sdk-helper-map.cjs +0 -241
- package/scripts/patches/auto-instrument/singleton-helper.cjs +0 -193
- package/scripts/patches/auto-instrument/sveltekit.cjs +0 -161
- package/scripts/patches/auto-instrument/vite-react.cjs +0 -37
- package/scripts/patches/auto-instrument/vue.cjs +0 -196
- package/scripts/patches/express.patch.cjs +0 -99
- package/scripts/patches/fastify.patch.cjs +0 -108
- package/scripts/patches/index.cjs +0 -300
- package/scripts/patches/nestjs.patch.cjs +0 -112
- package/scripts/patches/nextjs-app-router.patch.cjs +0 -97
- package/scripts/patches/nextjs-pages.patch.cjs +0 -97
- package/scripts/patches/remix.patch.cjs +0 -75
- package/scripts/patches/sveltekit.patch.cjs +0 -72
- package/scripts/patches/vite-react.patch.cjs +0 -73
- package/scripts/patches/vue.patch.cjs +0 -82
- package/scripts/renew-ssl.sh +0 -14
- package/scripts/resolve-migration.sh +0 -23
- package/scripts/seed-cli-dev-keys.mjs +0 -130
- package/scripts/seed-test-data.mjs +0 -391
- package/scripts/spike-browserless.ts +0 -65
- package/scripts/tenant-pivot-consistency-check.mjs +0 -205
- package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +0 -258
- package/scripts/tenant-pivot-phase-3-cleanup.mjs +0 -98
- package/scripts/test-identity-resolution.ts +0 -804
- package/scripts/validate-gurulu-schemas.mjs +0 -79
package/dist/commands/events.js
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
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
|
-
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);
|
|
33
|
-
default:
|
|
34
|
-
(0, ui_1.error)(`Unknown events action: ${action}`);
|
|
35
|
-
(0, ui_1.info)('Usage: gurulu events [list|tail|schema|define|verify|templates]');
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function parseFilterFlag(raw) {
|
|
40
|
-
if (!raw)
|
|
41
|
-
return {};
|
|
42
|
-
// Support `event_name:foo` or `name=foo`
|
|
43
|
-
const m = raw.match(/^(?:event_name|name)\s*[:=]\s*(.+)$/);
|
|
44
|
-
if (m)
|
|
45
|
-
return { eventName: m[1] };
|
|
46
|
-
return { eventName: raw };
|
|
47
|
-
}
|
|
48
|
-
async function listCmd(args) {
|
|
49
|
-
const qs = new URLSearchParams();
|
|
50
|
-
if (args.site)
|
|
51
|
-
qs.set('site', args.site);
|
|
52
|
-
const filter = parseFilterFlag(args.filter);
|
|
53
|
-
const eventName = args.eventName || filter.eventName;
|
|
54
|
-
if (eventName)
|
|
55
|
-
qs.set('event_name', eventName);
|
|
56
|
-
if (args.since)
|
|
57
|
-
qs.set('since', args.since);
|
|
58
|
-
if (args.limit)
|
|
59
|
-
qs.set('limit', String(args.limit));
|
|
60
|
-
const path = `/api/cli/events${qs.toString() ? `?${qs.toString()}` : ''}`;
|
|
61
|
-
const body = await (0, api_client_1.cliApiJson)(path, {
|
|
62
|
-
profile: args.profile,
|
|
63
|
-
});
|
|
64
|
-
if (args.json) {
|
|
65
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (body.warning) {
|
|
69
|
-
process.stderr.write((0, ui_1.dim)(`(warning: ${body.warning})\n`));
|
|
70
|
-
}
|
|
71
|
-
const events = body.events || [];
|
|
72
|
-
if (events.length === 0) {
|
|
73
|
-
(0, ui_1.info)('No events found in the selected window.');
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
process.stdout.write(['TS', 'EVENT', 'SITE', 'URL'].join('\t') + '\n');
|
|
77
|
-
for (const e of events) {
|
|
78
|
-
process.stdout.write([
|
|
79
|
-
String(e.timestamp || e.event_ts || '-'),
|
|
80
|
-
String(e.event_name || '-'),
|
|
81
|
-
String(e.site_id || '-'),
|
|
82
|
-
String(e.page_url || e.url || '-').slice(0, 60),
|
|
83
|
-
].join('\t') + '\n');
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
async function tailCmd(args) {
|
|
87
|
-
const qs = new URLSearchParams();
|
|
88
|
-
if (args.site)
|
|
89
|
-
qs.set('site', args.site);
|
|
90
|
-
if (args.eventName)
|
|
91
|
-
qs.set('event_name', args.eventName);
|
|
92
|
-
const path = `/api/cli/events/tail${qs.toString() ? `?${qs.toString()}` : ''}`;
|
|
93
|
-
// cliApi handles auth + base url; we then stream the body ourselves.
|
|
94
|
-
const res = await (0, api_client_1.cliApi)(path, {
|
|
95
|
-
profile: args.profile,
|
|
96
|
-
headers: { accept: 'text/event-stream' },
|
|
97
|
-
});
|
|
98
|
-
if (!res.body) {
|
|
99
|
-
(0, ui_1.error)('Tail stream returned no body.');
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
(0, ui_1.info)('Streaming events — press Ctrl+C to stop');
|
|
103
|
-
const reader = res.body.getReader?.();
|
|
104
|
-
if (!reader) {
|
|
105
|
-
// Node streams fallback
|
|
106
|
-
const chunks = [];
|
|
107
|
-
for await (const chunk of res.body) {
|
|
108
|
-
process.stdout.write(formatFrame(Buffer.from(chunk).toString('utf8'), !!args.json));
|
|
109
|
-
}
|
|
110
|
-
void chunks;
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const decoder = new TextDecoder();
|
|
114
|
-
// eslint-disable-next-line no-constant-condition
|
|
115
|
-
while (true) {
|
|
116
|
-
const { done, value } = await reader.read();
|
|
117
|
-
if (done)
|
|
118
|
-
return;
|
|
119
|
-
const text = decoder.decode(value, { stream: true });
|
|
120
|
-
process.stdout.write(formatFrame(text, !!args.json));
|
|
121
|
-
}
|
|
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
|
-
// Use the user-provided name verbatim. The CLI used to force a `$` prefix
|
|
188
|
-
// because Gurulu's built-ins use one (`$page_view`, `$session_start`), but
|
|
189
|
-
// built-ins are inconsistent (`click`, `engaged_session` have none) and
|
|
190
|
-
// mutating the customer's chosen name silently broke schema lookups + made
|
|
191
|
-
// the generated SDK snippets reference a name that no longer matched the
|
|
192
|
-
// ingest payload.
|
|
193
|
-
const eventName = args.eventName;
|
|
194
|
-
const body = {
|
|
195
|
-
siteId: args.site,
|
|
196
|
-
eventName,
|
|
197
|
-
displayName: args.displayName,
|
|
198
|
-
description: args.description || '',
|
|
199
|
-
intentName: args.category || 'engagement',
|
|
200
|
-
fingerprint: { propertySchema },
|
|
201
|
-
};
|
|
202
|
-
const data = await (0, api_client_1.cliApiJson)('/api/cli/events/definitions', { profile: args.profile, method: 'POST', json: body });
|
|
203
|
-
if (args.json) {
|
|
204
|
-
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
(0, ui_1.success)(`Event ${eventName} defined successfully`);
|
|
208
|
-
(0, ui_1.info)(`Run: gurulu events schema --event-name ${eventName} --site ${args.site} to see implementation examples`);
|
|
209
|
-
}
|
|
210
|
-
/* ── verify: check event health / quality ────────────────────────── */
|
|
211
|
-
async function verifyCmd(args) {
|
|
212
|
-
if (!args.site) {
|
|
213
|
-
(0, ui_1.error)('--site is required.');
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
// Accept both `--event` (verify-original) and `--event-name` (used by
|
|
217
|
-
// every other events subcommand). Removes the surprising flag-name
|
|
218
|
-
// inconsistency without breaking existing scripts.
|
|
219
|
-
const filterEvent = args.event || args.eventName;
|
|
220
|
-
const qs = new URLSearchParams();
|
|
221
|
-
qs.set('siteId', args.site);
|
|
222
|
-
if (filterEvent)
|
|
223
|
-
qs.set('eventName', filterEvent);
|
|
224
|
-
const path = `/api/cli/events/health?${qs.toString()}`;
|
|
225
|
-
const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
|
|
226
|
-
if (args.json) {
|
|
227
|
-
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
console.log((0, ui_1.bold)('\nEvent Health Report'));
|
|
231
|
-
console.log(`Site: ${args.site}`);
|
|
232
|
-
console.log('');
|
|
233
|
-
if (Array.isArray(data.events) && data.events.length > 0) {
|
|
234
|
-
for (const ev of data.events) {
|
|
235
|
-
const statusIcon = ev.status === 'active'
|
|
236
|
-
? (0, ui_1.green)('●')
|
|
237
|
-
: ev.status === 'stale'
|
|
238
|
-
? (0, ui_1.yellow)('●')
|
|
239
|
-
: (0, ui_1.red)('●');
|
|
240
|
-
console.log(`${statusIcon} ${ev.event_name} — ${ev.total_count ?? 0} events (last: ${ev.last_seen ?? 'never'})`);
|
|
241
|
-
if (ev.missing_properties?.length) {
|
|
242
|
-
console.log((0, ui_1.yellow)(` Missing required properties: ${ev.missing_properties.join(', ')}`));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
else if (data.total_events !== undefined) {
|
|
247
|
-
console.log(`Total events (24h): ${data.total_events}`);
|
|
248
|
-
console.log(`Unique event types: ${data.unique_types ?? 'N/A'}`);
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
// Empty: no telemetry yet for the requested filter / definition. Without
|
|
252
|
-
// this branch the report printed only its header — users couldn't tell
|
|
253
|
-
// success from "no data found".
|
|
254
|
-
if (filterEvent) {
|
|
255
|
-
(0, ui_1.info)(`No telemetry yet for "${filterEvent}". Track it from your app and run verify again.`);
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
(0, ui_1.info)('No telemetry data found for this site in the last 24h.');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
console.log('');
|
|
262
|
-
}
|
|
263
|
-
/* ── templates: list vertical event template catalogs ───────────── */
|
|
264
|
-
async function templatesCmd(args) {
|
|
265
|
-
const qs = new URLSearchParams();
|
|
266
|
-
if (args.vertical)
|
|
267
|
-
qs.set('vertical', args.vertical);
|
|
268
|
-
const path = `/api/events/templates${qs.toString() ? `?${qs.toString()}` : ''}`;
|
|
269
|
-
const data = await (0, api_client_1.cliApiJson)(path, { profile: args.profile });
|
|
270
|
-
if (args.json) {
|
|
271
|
-
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (data.error) {
|
|
275
|
-
(0, ui_1.error)(data.error);
|
|
276
|
-
process.exit(1);
|
|
277
|
-
}
|
|
278
|
-
// List mode — no vertical specified
|
|
279
|
-
if (data.verticals) {
|
|
280
|
-
console.log((0, ui_1.bold)('\nAvailable Verticals\n'));
|
|
281
|
-
for (const v of data.verticals) {
|
|
282
|
-
console.log(` ${(0, ui_1.cyan)(v.id.padEnd(14))} ${v.name.padEnd(30)} ${(0, ui_1.dim)(`${v.eventCount} events`)}`);
|
|
283
|
-
}
|
|
284
|
-
console.log('');
|
|
285
|
-
(0, ui_1.info)('Usage: gurulu events templates --vertical <id>');
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
// Detail mode — specific vertical
|
|
289
|
-
console.log((0, ui_1.bold)(`\nVertical: ${data.vertical}`));
|
|
290
|
-
console.log(`Confidence: ${data.confidence}\n`);
|
|
291
|
-
if (data.events?.length) {
|
|
292
|
-
console.log((0, ui_1.bold)('Events:\n'));
|
|
293
|
-
for (const ev of data.events) {
|
|
294
|
-
console.log(` ${(0, ui_1.green)(ev.name)} ${(0, ui_1.dim)(`[${ev.category}]`)}`);
|
|
295
|
-
console.log(` ${(0, ui_1.dim)(ev.reasoning)}`);
|
|
296
|
-
if (ev.propertySchema?.length) {
|
|
297
|
-
for (const p of ev.propertySchema) {
|
|
298
|
-
const req = p.required ? (0, ui_1.red)('REQUIRED') : (0, ui_1.dim)('optional');
|
|
299
|
-
const fmt = p.format ? (0, ui_1.cyan)(` [${p.format}]`) : '';
|
|
300
|
-
const ex = p.example !== undefined ? (0, ui_1.dim)(` e.g. ${JSON.stringify(p.example)}`) : '';
|
|
301
|
-
console.log(` ${p.name}: ${(0, ui_1.cyan)(p.type)} (${req})${fmt}${ex}`);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
console.log('');
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (data.funnels?.length) {
|
|
308
|
-
console.log((0, ui_1.bold)('Funnels:\n'));
|
|
309
|
-
for (const f of data.funnels) {
|
|
310
|
-
console.log(` ${(0, ui_1.yellow)(f.name)} ${(0, ui_1.dim)(`[${f.category}]`)}`);
|
|
311
|
-
console.log(` Steps: ${f.steps.join(' → ')}`);
|
|
312
|
-
console.log(` ${(0, ui_1.dim)(f.reasoning)}`);
|
|
313
|
-
console.log('');
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
function formatFrame(raw, json) {
|
|
318
|
-
if (json)
|
|
319
|
-
return raw;
|
|
320
|
-
const out = [];
|
|
321
|
-
for (const block of raw.split('\n\n')) {
|
|
322
|
-
const lines = block.split('\n');
|
|
323
|
-
let ev = 'message';
|
|
324
|
-
let data = '';
|
|
325
|
-
for (const line of lines) {
|
|
326
|
-
if (line.startsWith('event: '))
|
|
327
|
-
ev = line.slice(7).trim();
|
|
328
|
-
else if (line.startsWith('data: '))
|
|
329
|
-
data += line.slice(6);
|
|
330
|
-
}
|
|
331
|
-
if (data && ev === 'realtime_event') {
|
|
332
|
-
try {
|
|
333
|
-
const parsed = JSON.parse(data);
|
|
334
|
-
out.push(`[${parsed.event_ts || '-'}] ${parsed.event_name || '-'} site=${parsed.site_id || '-'}\n`);
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
out.push(`[event] ${data}\n`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return out.join('');
|
|
342
|
-
}
|
|
343
|
-
// Keep the legacy call-site happy: the old signature used `site/json` only.
|
|
344
|
-
async function legacyEventsCommand(args) {
|
|
345
|
-
return eventsCommand({ ...args, action: 'list' });
|
|
346
|
-
}
|
|
347
|
-
// Ensure profile helper is linked so bundlers don't tree-shake away the
|
|
348
|
-
// config module in edge builds.
|
|
349
|
-
exports._loadActiveProfile = config_1.loadActiveProfile;
|
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
site?: string;
|
|
13
|
-
id?: string;
|
|
14
|
-
key?: string;
|
|
15
|
-
name?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
variants?: string;
|
|
18
|
-
goal?: string;
|
|
19
|
-
yes?: boolean;
|
|
20
|
-
dryRun?: boolean;
|
|
21
|
-
}
|
|
22
|
-
export declare function experimentsCommand(args: ExperimentsArgs): Promise<void>;
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Phase 19.5 W2 B7 — `gurulu experiments list|show|results`.
|
|
4
|
-
* Phase 20 W2 B4 — `create|update|delete|start|stop`.
|
|
5
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.experimentsCommand = experimentsCommand;
|
|
8
|
-
const api_client_1 = require("../api-client");
|
|
9
|
-
const ui_1 = require("../utils/ui");
|
|
10
|
-
const confirm_1 = require("../utils/confirm");
|
|
11
|
-
const dry_run_1 = require("../utils/dry-run");
|
|
12
|
-
const from_file_1 = require("../utils/from-file");
|
|
13
|
-
async function experimentsCommand(args) {
|
|
14
|
-
const action = args.action || 'list';
|
|
15
|
-
switch (action) {
|
|
16
|
-
case 'list':
|
|
17
|
-
return listCmd(args);
|
|
18
|
-
case 'show':
|
|
19
|
-
return showCmd(args);
|
|
20
|
-
case 'results':
|
|
21
|
-
return resultsCmd(args);
|
|
22
|
-
case 'create':
|
|
23
|
-
return createCmd(args);
|
|
24
|
-
case 'update':
|
|
25
|
-
return updateCmd(args);
|
|
26
|
-
case 'delete':
|
|
27
|
-
return deleteCmd(args);
|
|
28
|
-
case 'start':
|
|
29
|
-
return startCmd(args);
|
|
30
|
-
case 'stop':
|
|
31
|
-
return stopCmd(args);
|
|
32
|
-
default:
|
|
33
|
-
(0, ui_1.error)(`Unknown experiments action: ${action}`);
|
|
34
|
-
(0, ui_1.info)('Usage: gurulu experiments [list|show|results|create|update|delete|start|stop]');
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function listCmd(args) {
|
|
39
|
-
const qs = args.site ? `?siteId=${encodeURIComponent(args.site)}` : '';
|
|
40
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments${qs}`, {
|
|
41
|
-
profile: args.profile,
|
|
42
|
-
});
|
|
43
|
-
if (args.json) {
|
|
44
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const rows = body.experiments || [];
|
|
48
|
-
if (rows.length === 0) {
|
|
49
|
-
(0, ui_1.info)('No experiments yet.');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
process.stdout.write(['KEY', 'NAME', 'ACTIVE', 'ASSIGNMENTS', 'STARTED'].join('\t') + '\n');
|
|
53
|
-
for (const e of rows) {
|
|
54
|
-
process.stdout.write([
|
|
55
|
-
e.key,
|
|
56
|
-
e.name,
|
|
57
|
-
e.isActive ? 'yes' : 'no',
|
|
58
|
-
String(e.assignmentsCount ?? 0),
|
|
59
|
-
String(e.startedAt || '-'),
|
|
60
|
-
].join('\t') + '\n');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function showCmd(args) {
|
|
64
|
-
if (!args.target) {
|
|
65
|
-
(0, ui_1.error)('Usage: gurulu experiments show <key>');
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}`, { profile: args.profile });
|
|
69
|
-
if (args.json) {
|
|
70
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const e = body.experiment;
|
|
74
|
-
process.stdout.write(`Key: ${e.key}\n`);
|
|
75
|
-
process.stdout.write(`Name: ${e.name}\n`);
|
|
76
|
-
process.stdout.write(`Active: ${e.isActive ? 'yes' : 'no'}\n`);
|
|
77
|
-
process.stdout.write(`Assignments: ${e.assignmentsCount}\n`);
|
|
78
|
-
process.stdout.write(`Variants: ${JSON.stringify(e.variants)}\n`);
|
|
79
|
-
process.stdout.write(`Started: ${e.startedAt || '-'}\n`);
|
|
80
|
-
process.stdout.write(`Ended: ${e.endedAt || '-'}\n`);
|
|
81
|
-
}
|
|
82
|
-
async function resultsCmd(args) {
|
|
83
|
-
if (!args.target) {
|
|
84
|
-
(0, ui_1.error)('Usage: gurulu experiments results <key>');
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
const qs = args.conversionEvent
|
|
88
|
-
? `?conversionEvent=${encodeURIComponent(args.conversionEvent)}`
|
|
89
|
-
: '';
|
|
90
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}/results${qs}`, { profile: args.profile });
|
|
91
|
-
if (args.json) {
|
|
92
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
process.stdout.write(`Experiment: ${body.experimentKey || body.experimentId}\n`);
|
|
96
|
-
process.stdout.write(`Conversion: ${body.conversionEvent}\n`);
|
|
97
|
-
process.stdout.write(['VARIANT', 'ASSIGNS', 'CONV', 'RATE', 'CI_LO', 'CI_HI'].join('\t') + '\n');
|
|
98
|
-
for (const r of body.results || []) {
|
|
99
|
-
process.stdout.write([
|
|
100
|
-
r.variantId,
|
|
101
|
-
String(r.assignments),
|
|
102
|
-
String(r.conversions),
|
|
103
|
-
r.conversionRate.toFixed(3),
|
|
104
|
-
r.ciLower.toFixed(3),
|
|
105
|
-
r.ciUpper.toFixed(3),
|
|
106
|
-
].join('\t') + '\n');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
async function createCmd(args) {
|
|
110
|
-
let payload = {};
|
|
111
|
-
if (args.fromFile)
|
|
112
|
-
payload = (0, from_file_1.loadFromFile)(args.fromFile);
|
|
113
|
-
if (args.site)
|
|
114
|
-
payload.siteId = args.site;
|
|
115
|
-
if (args.key)
|
|
116
|
-
payload.key = args.key;
|
|
117
|
-
if (args.name)
|
|
118
|
-
payload.name = args.name;
|
|
119
|
-
if (args.description !== undefined)
|
|
120
|
-
payload.description = args.description;
|
|
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)');
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
if (args.dryRun) {
|
|
141
|
-
const body = await (0, api_client_1.cliApiJson)('/api/cli/experiments?dryRun=1', {
|
|
142
|
-
profile: args.profile,
|
|
143
|
-
method: 'POST',
|
|
144
|
-
json: payload,
|
|
145
|
-
});
|
|
146
|
-
(0, dry_run_1.printDryRun)(body, args.json);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const ok = await (0, confirm_1.promptConfirm)(`Create experiment '${payload.key}'?`, {
|
|
150
|
-
yes: args.yes,
|
|
151
|
-
defaultYes: true,
|
|
152
|
-
});
|
|
153
|
-
if (!ok)
|
|
154
|
-
return (0, ui_1.info)('Aborted.');
|
|
155
|
-
const body = await (0, api_client_1.cliApiJson)('/api/cli/experiments', {
|
|
156
|
-
profile: args.profile,
|
|
157
|
-
method: 'POST',
|
|
158
|
-
json: payload,
|
|
159
|
-
});
|
|
160
|
-
if (args.json) {
|
|
161
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
(0, ui_1.success)(`Created experiment ${body.experiment?.key ?? ''}`);
|
|
165
|
-
}
|
|
166
|
-
async function updateCmd(args) {
|
|
167
|
-
const resolveTarget = args.id || args.target;
|
|
168
|
-
if (!resolveTarget) {
|
|
169
|
-
(0, ui_1.error)('Usage: gurulu experiments update <key> [--id ...] [--name ...] [--variants ...]');
|
|
170
|
-
process.exit(1);
|
|
171
|
-
}
|
|
172
|
-
let payload = {};
|
|
173
|
-
if (args.fromFile)
|
|
174
|
-
payload = (0, from_file_1.loadFromFile)(args.fromFile);
|
|
175
|
-
if (args.name)
|
|
176
|
-
payload.name = args.name;
|
|
177
|
-
if (args.description !== undefined)
|
|
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
|
-
}
|
|
190
|
-
if (args.dryRun) {
|
|
191
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'PATCH', json: payload });
|
|
192
|
-
(0, dry_run_1.printDryRun)(body, args.json);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const ok = await (0, confirm_1.promptConfirm)(`Update experiment '${resolveTarget}'?`, {
|
|
196
|
-
yes: args.yes,
|
|
197
|
-
defaultYes: true,
|
|
198
|
-
});
|
|
199
|
-
if (!ok)
|
|
200
|
-
return (0, ui_1.info)('Aborted.');
|
|
201
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(resolveTarget)}`, { profile: args.profile, method: 'PATCH', json: payload });
|
|
202
|
-
if (args.json) {
|
|
203
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
(0, ui_1.success)(`Updated experiment ${resolveTarget}`);
|
|
207
|
-
}
|
|
208
|
-
async function deleteCmd(args) {
|
|
209
|
-
const resolveTarget = args.id || args.target;
|
|
210
|
-
if (!resolveTarget) {
|
|
211
|
-
(0, ui_1.error)('Usage: gurulu experiments delete <key> [--id ...]');
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
if (args.dryRun) {
|
|
215
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(resolveTarget)}?dryRun=1`, { profile: args.profile, method: 'DELETE' });
|
|
216
|
-
(0, dry_run_1.printDryRun)(body, args.json);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const ok = await (0, confirm_1.promptConfirm)(`About to delete experiment '${resolveTarget}'. Continue?`, { yes: args.yes, defaultYes: false });
|
|
220
|
-
if (!ok)
|
|
221
|
-
return (0, ui_1.info)('Aborted.');
|
|
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}`);
|
|
224
|
-
}
|
|
225
|
-
async function startCmd(args) {
|
|
226
|
-
if (!args.target) {
|
|
227
|
-
(0, ui_1.error)('Usage: gurulu experiments start <key>');
|
|
228
|
-
process.exit(1);
|
|
229
|
-
}
|
|
230
|
-
const ok = await (0, confirm_1.promptConfirm)(`Start experiment '${args.target}'?`, {
|
|
231
|
-
yes: args.yes,
|
|
232
|
-
defaultYes: true,
|
|
233
|
-
});
|
|
234
|
-
if (!ok)
|
|
235
|
-
return (0, ui_1.info)('Aborted.');
|
|
236
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}/start${args.dryRun ? '?dryRun=1' : ''}`, { profile: args.profile, method: 'POST', json: {} });
|
|
237
|
-
if (args.dryRun)
|
|
238
|
-
return (0, dry_run_1.printDryRun)(body, args.json);
|
|
239
|
-
if (args.json) {
|
|
240
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
(0, ui_1.success)(`Started experiment ${args.target}`);
|
|
244
|
-
}
|
|
245
|
-
async function stopCmd(args) {
|
|
246
|
-
if (!args.target) {
|
|
247
|
-
(0, ui_1.error)('Usage: gurulu experiments stop <key>');
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
const ok = await (0, confirm_1.promptConfirm)(`Stop experiment '${args.target}'?`, {
|
|
251
|
-
yes: args.yes,
|
|
252
|
-
defaultYes: true,
|
|
253
|
-
});
|
|
254
|
-
if (!ok)
|
|
255
|
-
return (0, ui_1.info)('Aborted.');
|
|
256
|
-
const body = await (0, api_client_1.cliApiJson)(`/api/cli/experiments/${encodeURIComponent(args.target)}/stop${args.dryRun ? '?dryRun=1' : ''}`, { profile: args.profile, method: 'POST', json: {} });
|
|
257
|
-
if (args.dryRun)
|
|
258
|
-
return (0, dry_run_1.printDryRun)(body, args.json);
|
|
259
|
-
if (args.json) {
|
|
260
|
-
process.stdout.write(JSON.stringify(body, null, 2) + '\n');
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
(0, ui_1.success)(`Stopped experiment ${args.target}`);
|
|
264
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
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>;
|