@rayrun/cli 0.1.0 → 0.4.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/README.md +90 -1
- package/package.json +11 -6
- package/src/execution.js +862 -0
- package/src/main.js +16 -2
- package/src/management.js +552 -0
- package/src/oauth.js +599 -0
- package/src/setup.js +2 -2
package/src/main.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { clientDefinitions, detectClients } from './clients.js';
|
|
2
|
+
import { executionUsage, isExecutionCommand, runExecutionCommand } from './execution.js';
|
|
3
|
+
import { isManagementCommand, managementUsage, runManagementCommand } from './management.js';
|
|
2
4
|
import { applySetup, rollbackSetup } from './setup.js';
|
|
3
5
|
import { createRequire } from 'node:module';
|
|
4
6
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -9,7 +11,11 @@ const usage = `Usage:
|
|
|
9
11
|
rayrun setup <endpoint> [--client <id> ... | --all] [--project] [--yes] [--dry-run] [--login | --no-login]
|
|
10
12
|
rayrun setup rollback <backup-id>
|
|
11
13
|
|
|
12
|
-
Clients: claude-code, codex, cursor, vscode, windsurf, devin, opencode
|
|
14
|
+
Clients: claude-code, codex, cursor, vscode, windsurf, devin, opencode
|
|
15
|
+
|
|
16
|
+
${executionUsage}
|
|
17
|
+
|
|
18
|
+
${managementUsage}`;
|
|
13
19
|
|
|
14
20
|
export const validateEndpoint = (value) => {
|
|
15
21
|
let endpoint;
|
|
@@ -97,7 +103,7 @@ const selectInteractively = async (clients) => {
|
|
|
97
103
|
return clients.filter((_client, index) => indexes.has(index));
|
|
98
104
|
};
|
|
99
105
|
|
|
100
|
-
export const main = async (arguments_) => {
|
|
106
|
+
export const main = async (arguments_, dependencies) => {
|
|
101
107
|
if (arguments_.includes('--help') || arguments_.includes('-h')) {
|
|
102
108
|
process.stdout.write(`${usage}\n`);
|
|
103
109
|
return;
|
|
@@ -106,6 +112,14 @@ export const main = async (arguments_) => {
|
|
|
106
112
|
process.stdout.write(`${String(packageVersion)}\n`);
|
|
107
113
|
return;
|
|
108
114
|
}
|
|
115
|
+
if (isExecutionCommand(arguments_)) {
|
|
116
|
+
await runExecutionCommand(arguments_, { ...dependencies, version: packageVersion });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (isManagementCommand(arguments_[0])) {
|
|
120
|
+
await runManagementCommand(arguments_, dependencies);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
109
123
|
const { options, positional } = parseArguments(arguments_);
|
|
110
124
|
|
|
111
125
|
if (positional[0] !== 'setup') {
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/* eslint-disable node/no-process-env -- management intentionally reads the invoking user's Rayrun environment */
|
|
2
|
+
import { Rayrun, RayrunApiError } from '@rayrun/sdk';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
5
|
+
|
|
6
|
+
export const managementUsage = `Management:
|
|
7
|
+
rayrun connect mcp <url> --name <name> [--transport <streamable-http|sse>] [--json]
|
|
8
|
+
rayrun connect openapi <spec-url> [--name <name>] [--base-url <url>] [--json]
|
|
9
|
+
rayrun connections list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
10
|
+
rayrun clients list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
11
|
+
rayrun tools search <query> [--connection <uid>] [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
12
|
+
rayrun policy inspect <client-uid> [--query <query>] [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
13
|
+
rayrun approvals list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
14
|
+
rayrun activity list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
15
|
+
rayrun hooks pull <connection-uid> <tool-uid> [--output <file>] [--types-output <file>] [--json]
|
|
16
|
+
rayrun hooks test <connection-uid> <tool-uid> --arguments <json> [--file <file>] [--mock-result <json>] [--config <json>] [--json]
|
|
17
|
+
rayrun hooks deploy <connection-uid> <tool-uid> [--file <file>] [--config <json>] [--shadow] [--json]
|
|
18
|
+
rayrun hooks rollback <connection-uid> <tool-uid> <revision-uid> [--shadow] [--json]
|
|
19
|
+
rayrun hooks deactivate <connection-uid> <tool-uid> [--shadow] [--json]
|
|
20
|
+
rayrun hooks logs <connection-uid> <tool-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
21
|
+
|
|
22
|
+
Environment:
|
|
23
|
+
RAYRUN_API_KEY API key created in Dashboard -> Settings -> API keys
|
|
24
|
+
RAYRUN_API_URL Optional API origin (defaults to https://ray.run)`;
|
|
25
|
+
|
|
26
|
+
const managementCommands = new Set([
|
|
27
|
+
'activity',
|
|
28
|
+
'approvals',
|
|
29
|
+
'clients',
|
|
30
|
+
'connect',
|
|
31
|
+
'connections',
|
|
32
|
+
'hooks',
|
|
33
|
+
'policy',
|
|
34
|
+
'tools',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export const isManagementCommand = (command) => managementCommands.has(command);
|
|
38
|
+
|
|
39
|
+
const optionNames = new Map([
|
|
40
|
+
['--base-url', 'baseUrl'],
|
|
41
|
+
['--arguments', 'argumentsJson'],
|
|
42
|
+
['--config', 'configJson'],
|
|
43
|
+
['--connection', 'connectionUid'],
|
|
44
|
+
['--cursor', 'cursor'],
|
|
45
|
+
['--limit', 'limit'],
|
|
46
|
+
['--file', 'filePath'],
|
|
47
|
+
['--mock-result', 'mockResultJson'],
|
|
48
|
+
['--name', 'name'],
|
|
49
|
+
['--query', 'query'],
|
|
50
|
+
['--output', 'outputPath'],
|
|
51
|
+
['--transport', 'transport'],
|
|
52
|
+
['--types-output', 'typesOutputPath'],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const parseCommandArguments = (arguments_, allowedOptions) => {
|
|
56
|
+
const options = { json: false };
|
|
57
|
+
const positional = [];
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
60
|
+
const argument = arguments_[index];
|
|
61
|
+
|
|
62
|
+
if (argument === '--json') {
|
|
63
|
+
if (!allowedOptions.has('json')) throw new Error(`Unknown option: ${argument}`);
|
|
64
|
+
options.json = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (argument === '--shadow') {
|
|
68
|
+
if (!allowedOptions.has('shadow')) throw new Error(`Unknown option: ${argument}`);
|
|
69
|
+
options.shadow = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const optionName = optionNames.get(argument);
|
|
74
|
+
if (optionName) {
|
|
75
|
+
if (!allowedOptions.has(optionName)) throw new Error(`Unknown option: ${argument}`);
|
|
76
|
+
const value = arguments_[index + 1];
|
|
77
|
+
if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value.`);
|
|
78
|
+
if (options[optionName] !== undefined) throw new Error(`${argument} can only be used once.`);
|
|
79
|
+
options[optionName] = value;
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (argument?.startsWith('--')) throw new Error(`Unknown option: ${argument}`);
|
|
85
|
+
if (argument) positional.push(argument);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (options.limit !== undefined) {
|
|
89
|
+
const limit = Number(options.limit);
|
|
90
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
91
|
+
throw new Error('--limit must be an integer from 1 to 100.');
|
|
92
|
+
}
|
|
93
|
+
options.limit = limit;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { options, positional };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const validateApiBaseUrl = (value) => {
|
|
100
|
+
let url;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
url = new URL(value);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error('RAYRUN_API_URL must be an absolute HTTP or HTTPS URL.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
109
|
+
throw new Error('RAYRUN_API_URL must use HTTP or HTTPS.');
|
|
110
|
+
}
|
|
111
|
+
if (url.protocol === 'http:' && !['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)) {
|
|
112
|
+
throw new Error('RAYRUN_API_URL may use HTTP only on localhost.');
|
|
113
|
+
}
|
|
114
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
115
|
+
throw new Error('RAYRUN_API_URL cannot contain credentials, a query string, or a fragment.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return url.href.replace(/\/$/u, '');
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const formatCell = (value) => {
|
|
122
|
+
if (value === null || value === undefined || value === '') return '-';
|
|
123
|
+
if (typeof value === 'boolean') return value ? 'yes' : 'no';
|
|
124
|
+
const sanitized = stripVTControlCharacters(String(value))
|
|
125
|
+
.replaceAll(/\p{Cc}+/gu, ' ')
|
|
126
|
+
.replaceAll(/\s+/gu, ' ')
|
|
127
|
+
.trim();
|
|
128
|
+
|
|
129
|
+
return sanitized || '-';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const renderTable = (items, columns) => {
|
|
133
|
+
if (items.length === 0) return 'No results.\n';
|
|
134
|
+
|
|
135
|
+
const rows = items.map((item) => columns.map(({ value }) => formatCell(value(item))));
|
|
136
|
+
const widths = columns.map(({ label }, index) =>
|
|
137
|
+
Math.max(label.length, ...rows.map((row) => row[index].length)),
|
|
138
|
+
);
|
|
139
|
+
const line = (cells) =>
|
|
140
|
+
cells
|
|
141
|
+
.map((cell, index) => cell.padEnd(widths[index], ' '))
|
|
142
|
+
.join(' ')
|
|
143
|
+
.trimEnd();
|
|
144
|
+
|
|
145
|
+
return `${line(columns.map(({ label }) => label))}\n${line(widths.map((width) => '-'.repeat(width)))}\n${rows.map(line).join('\n')}\n`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const pageQuery = (options, extra = {}) => ({
|
|
149
|
+
...extra,
|
|
150
|
+
cursor: options.cursor,
|
|
151
|
+
limit: options.limit,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const writeValue = (output, value) => {
|
|
155
|
+
output.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const writePage = ({ columns, options, output, page }) => {
|
|
159
|
+
if (options.json) {
|
|
160
|
+
writeValue(output, page);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
output.write(renderTable(page.items, columns));
|
|
165
|
+
if (page.page.nextCursor) output.write(`Next cursor: ${page.page.nextCursor}\n`);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const requireShape = (condition) => {
|
|
169
|
+
if (!condition) throw new Error(managementUsage);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const parseJsonOption = (value, option, fallback) => {
|
|
173
|
+
if (value === undefined) return fallback;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(value);
|
|
177
|
+
} catch {
|
|
178
|
+
throw new Error(`${option} must be valid JSON.`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const hookMode = (options) => (options.shadow ? 'shadow' : 'active');
|
|
183
|
+
|
|
184
|
+
const runCommand = async ({ arguments_, client, output }) => {
|
|
185
|
+
const command = arguments_[0];
|
|
186
|
+
const action = arguments_[1];
|
|
187
|
+
|
|
188
|
+
if (command === 'connect' && action === 'mcp') {
|
|
189
|
+
const { options, positional } = parseCommandArguments(
|
|
190
|
+
arguments_.slice(2),
|
|
191
|
+
new Set(['json', 'name', 'transport']),
|
|
192
|
+
);
|
|
193
|
+
requireShape(positional.length === 1 && typeof options.name === 'string');
|
|
194
|
+
if (options.transport && !['sse', 'streamable-http'].includes(options.transport)) {
|
|
195
|
+
throw new Error('--transport must be streamable-http or sse.');
|
|
196
|
+
}
|
|
197
|
+
const result = await client.connections.create({
|
|
198
|
+
kind: 'mcp',
|
|
199
|
+
name: options.name,
|
|
200
|
+
transport: options.transport,
|
|
201
|
+
url: positional[0],
|
|
202
|
+
});
|
|
203
|
+
if (options.json) writeValue(output, result);
|
|
204
|
+
else output.write(`Connected ${result.connection.uid}.\n`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (command === 'connect' && action === 'openapi') {
|
|
209
|
+
const { options, positional } = parseCommandArguments(
|
|
210
|
+
arguments_.slice(2),
|
|
211
|
+
new Set(['baseUrl', 'json', 'name']),
|
|
212
|
+
);
|
|
213
|
+
requireShape(positional.length === 1);
|
|
214
|
+
const result = await client.connections.create({
|
|
215
|
+
baseUrl: options.baseUrl,
|
|
216
|
+
kind: 'openapi',
|
|
217
|
+
name: options.name,
|
|
218
|
+
specificationUrl: positional[0],
|
|
219
|
+
});
|
|
220
|
+
if (options.json) writeValue(output, result);
|
|
221
|
+
else output.write(`Connected ${result.connection.uid}.\n`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (command === 'connections' && action === 'list') {
|
|
226
|
+
const { options, positional } = parseCommandArguments(
|
|
227
|
+
arguments_.slice(2),
|
|
228
|
+
new Set(['cursor', 'json', 'limit']),
|
|
229
|
+
);
|
|
230
|
+
requireShape(positional.length === 0);
|
|
231
|
+
const page = await client.connections.list(pageQuery(options));
|
|
232
|
+
writePage({
|
|
233
|
+
columns: [
|
|
234
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
235
|
+
{ label: 'NAME', value: (item) => item.displayName },
|
|
236
|
+
{ label: 'KIND', value: (item) => item.kind },
|
|
237
|
+
{ label: 'STATUS', value: (item) => item.status },
|
|
238
|
+
{ label: 'POLICY', value: (item) => item.toolAccessMode },
|
|
239
|
+
],
|
|
240
|
+
options,
|
|
241
|
+
output,
|
|
242
|
+
page,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (command === 'clients' && action === 'list') {
|
|
248
|
+
const { options, positional } = parseCommandArguments(
|
|
249
|
+
arguments_.slice(2),
|
|
250
|
+
new Set(['cursor', 'json', 'limit']),
|
|
251
|
+
);
|
|
252
|
+
requireShape(positional.length === 0);
|
|
253
|
+
const page = await client.clients.list(pageQuery(options));
|
|
254
|
+
writePage({
|
|
255
|
+
columns: [
|
|
256
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
257
|
+
{ label: 'CLIENT', value: (item) => item.clientName },
|
|
258
|
+
{ label: 'USER', value: (item) => item.userName },
|
|
259
|
+
{ label: 'POLICY', value: (item) => item.toolAccessMode },
|
|
260
|
+
{ label: 'PROFILE', value: (item) => item.accessProfile?.name },
|
|
261
|
+
{ label: 'LAST USED', value: (item) => item.lastUsedAt },
|
|
262
|
+
],
|
|
263
|
+
options,
|
|
264
|
+
output,
|
|
265
|
+
page,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (command === 'tools' && action === 'search') {
|
|
271
|
+
const { options, positional } = parseCommandArguments(
|
|
272
|
+
arguments_.slice(2),
|
|
273
|
+
new Set(['connectionUid', 'cursor', 'json', 'limit']),
|
|
274
|
+
);
|
|
275
|
+
const query = positional.join(' ').trim();
|
|
276
|
+
requireShape(query.length > 0);
|
|
277
|
+
const page = await client.tools.list(
|
|
278
|
+
pageQuery(options, { connectionUid: options.connectionUid, query }),
|
|
279
|
+
);
|
|
280
|
+
writePage({
|
|
281
|
+
columns: [
|
|
282
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
283
|
+
{ label: 'NAME', value: (item) => item.name },
|
|
284
|
+
{ label: 'CONNECTION', value: (item) => item.connectionUid },
|
|
285
|
+
{ label: 'RISK', value: (item) => item.risk },
|
|
286
|
+
{ label: 'APPROVED', value: (item) => item.approved },
|
|
287
|
+
{ label: 'RULE', value: (item) => item.rule },
|
|
288
|
+
],
|
|
289
|
+
options,
|
|
290
|
+
output,
|
|
291
|
+
page,
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (command === 'policy' && action === 'inspect') {
|
|
297
|
+
const { options, positional } = parseCommandArguments(
|
|
298
|
+
arguments_.slice(2),
|
|
299
|
+
new Set(['cursor', 'json', 'limit', 'query']),
|
|
300
|
+
);
|
|
301
|
+
requireShape(positional.length === 1);
|
|
302
|
+
const page = await client.clients.listTools(
|
|
303
|
+
positional[0],
|
|
304
|
+
pageQuery(options, { query: options.query }),
|
|
305
|
+
);
|
|
306
|
+
writePage({
|
|
307
|
+
columns: [
|
|
308
|
+
{ label: 'SERVICE', value: (item) => item.serviceName },
|
|
309
|
+
{ label: 'TOOL', value: (item) => item.name },
|
|
310
|
+
{ label: 'EFFECTIVE', value: (item) => item.effectiveDecision },
|
|
311
|
+
{ label: 'REASON', value: (item) => item.effectiveReason },
|
|
312
|
+
{ label: 'CLIENT', value: (item) => item.clientDecision },
|
|
313
|
+
{ label: 'WORKSPACE', value: (item) => item.workspaceDecision },
|
|
314
|
+
],
|
|
315
|
+
options,
|
|
316
|
+
output,
|
|
317
|
+
page,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (command === 'approvals' && action === 'list') {
|
|
323
|
+
const { options, positional } = parseCommandArguments(
|
|
324
|
+
arguments_.slice(2),
|
|
325
|
+
new Set(['cursor', 'json', 'limit']),
|
|
326
|
+
);
|
|
327
|
+
requireShape(positional.length === 0);
|
|
328
|
+
const page = await client.reviews.list(pageQuery(options));
|
|
329
|
+
writePage({
|
|
330
|
+
columns: [
|
|
331
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
332
|
+
{ label: 'SERVICE', value: (item) => item.connectionName },
|
|
333
|
+
{ label: 'TOOL', value: (item) => item.toolName },
|
|
334
|
+
{ label: 'STATUS', value: (item) => item.status },
|
|
335
|
+
{ label: 'CREATED', value: (item) => item.createdAt },
|
|
336
|
+
{ label: 'EXPIRES', value: (item) => item.expiresAt },
|
|
337
|
+
],
|
|
338
|
+
options,
|
|
339
|
+
output,
|
|
340
|
+
page,
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (command === 'activity' && action === 'list') {
|
|
346
|
+
const { options, positional } = parseCommandArguments(
|
|
347
|
+
arguments_.slice(2),
|
|
348
|
+
new Set(['cursor', 'json', 'limit']),
|
|
349
|
+
);
|
|
350
|
+
requireShape(positional.length === 0);
|
|
351
|
+
const page = await client.activity.list(pageQuery(options));
|
|
352
|
+
writePage({
|
|
353
|
+
columns: [
|
|
354
|
+
{ label: 'ID', value: (item) => item.id },
|
|
355
|
+
{ label: 'WHEN', value: (item) => item.calledAt },
|
|
356
|
+
{ label: 'TOOL', value: (item) => item.toolName },
|
|
357
|
+
{ label: 'SERVICE', value: (item) => item.connectionName },
|
|
358
|
+
{ label: 'CLIENT', value: (item) => item.clientName },
|
|
359
|
+
{ label: 'DECISION', value: (item) => item.policyDecision },
|
|
360
|
+
{
|
|
361
|
+
label: 'RESULT',
|
|
362
|
+
value: (item) =>
|
|
363
|
+
item.succeeded === null
|
|
364
|
+
? item.errorCode === 'outcome_unknown'
|
|
365
|
+
? 'outcome unknown'
|
|
366
|
+
: 'pending'
|
|
367
|
+
: item.succeeded
|
|
368
|
+
? 'success'
|
|
369
|
+
: 'failed',
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
options,
|
|
373
|
+
output,
|
|
374
|
+
page,
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (command === 'hooks' && action === 'pull') {
|
|
380
|
+
const { options, positional } = parseCommandArguments(
|
|
381
|
+
arguments_.slice(2),
|
|
382
|
+
new Set(['json', 'outputPath', 'typesOutputPath']),
|
|
383
|
+
);
|
|
384
|
+
requireShape(positional.length === 2);
|
|
385
|
+
const { hook } = await client.hooks.get(positional[0], positional[1]);
|
|
386
|
+
|
|
387
|
+
if (options.outputPath) await writeFile(options.outputPath, hook.draftSource, 'utf8');
|
|
388
|
+
if (options.typesOutputPath) await writeFile(options.typesOutputPath, hook.types, 'utf8');
|
|
389
|
+
if (options.json) writeValue(output, { hook });
|
|
390
|
+
else if (!options.outputPath) output.write(hook.draftSource);
|
|
391
|
+
else output.write(`Wrote hook source to ${options.outputPath}.\n`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (command === 'hooks' && action === 'test') {
|
|
396
|
+
const { options, positional } = parseCommandArguments(
|
|
397
|
+
arguments_.slice(2),
|
|
398
|
+
new Set(['argumentsJson', 'configJson', 'filePath', 'json', 'mockResultJson']),
|
|
399
|
+
);
|
|
400
|
+
requireShape(positional.length === 2 && options.argumentsJson !== undefined);
|
|
401
|
+
const { hook } = await client.hooks.get(positional[0], positional[1]);
|
|
402
|
+
const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
|
|
403
|
+
const body = {
|
|
404
|
+
arguments: parseJsonOption(options.argumentsJson, '--arguments'),
|
|
405
|
+
config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
|
|
406
|
+
source,
|
|
407
|
+
...(options.mockResultJson === undefined
|
|
408
|
+
? {}
|
|
409
|
+
: { mockResult: parseJsonOption(options.mockResultJson, '--mock-result') }),
|
|
410
|
+
};
|
|
411
|
+
const result = await client.hooks.test(positional[0], positional[1], body);
|
|
412
|
+
writeValue(output, result);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (command === 'hooks' && action === 'deploy') {
|
|
417
|
+
const { options, positional } = parseCommandArguments(
|
|
418
|
+
arguments_.slice(2),
|
|
419
|
+
new Set(['configJson', 'filePath', 'json', 'shadow']),
|
|
420
|
+
);
|
|
421
|
+
requireShape(positional.length === 2);
|
|
422
|
+
const connectionUid = positional[0];
|
|
423
|
+
const toolUid = positional[1];
|
|
424
|
+
let { hook } = await client.hooks.get(connectionUid, toolUid);
|
|
425
|
+
|
|
426
|
+
if (hook.hookUid === null || options.filePath || options.configJson !== undefined) {
|
|
427
|
+
const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
|
|
428
|
+
const saved = await client.hooks.saveDraft(connectionUid, toolUid, {
|
|
429
|
+
config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
|
|
430
|
+
expectedVersion: hook.version,
|
|
431
|
+
source,
|
|
432
|
+
});
|
|
433
|
+
hook = saved.hook;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const result = await client.hooks.deploy(connectionUid, toolUid, {
|
|
437
|
+
expectedVersion: hook.version,
|
|
438
|
+
mode: hookMode(options),
|
|
439
|
+
});
|
|
440
|
+
if (options.json) writeValue(output, result);
|
|
441
|
+
else {
|
|
442
|
+
const revisionUid =
|
|
443
|
+
hookMode(options) === 'shadow'
|
|
444
|
+
? result.hook.shadowRevisionUid
|
|
445
|
+
: result.hook.activeRevisionUid;
|
|
446
|
+
output.write(`Deployed ${String(revisionUid)} in ${hookMode(options)} mode.\n`);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (command === 'hooks' && ['deactivate', 'rollback'].includes(action)) {
|
|
452
|
+
const { options, positional } = parseCommandArguments(
|
|
453
|
+
arguments_.slice(2),
|
|
454
|
+
new Set(['json', 'shadow']),
|
|
455
|
+
);
|
|
456
|
+
requireShape(positional.length === (action === 'rollback' ? 3 : 2));
|
|
457
|
+
const { hook } = await client.hooks.get(positional[0], positional[1]);
|
|
458
|
+
const result = await client.hooks.setDeployment(positional[0], positional[1], {
|
|
459
|
+
expectedVersion: hook.version,
|
|
460
|
+
mode: hookMode(options),
|
|
461
|
+
revisionUid: action === 'rollback' ? positional[2] : null,
|
|
462
|
+
});
|
|
463
|
+
if (options.json) writeValue(output, result);
|
|
464
|
+
else {
|
|
465
|
+
output.write(
|
|
466
|
+
action === 'rollback'
|
|
467
|
+
? `Deployed ${positional[2]} in ${hookMode(options)} mode.\n`
|
|
468
|
+
: `Deactivated ${hookMode(options)} mode.\n`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (command === 'hooks' && action === 'logs') {
|
|
475
|
+
const { options, positional } = parseCommandArguments(
|
|
476
|
+
arguments_.slice(2),
|
|
477
|
+
new Set(['cursor', 'json', 'limit']),
|
|
478
|
+
);
|
|
479
|
+
requireShape(positional.length === 2);
|
|
480
|
+
const page = await client.hooks.listRuns(positional[0], positional[1], pageQuery(options));
|
|
481
|
+
writePage({
|
|
482
|
+
columns: [
|
|
483
|
+
{ label: 'WHEN', value: (item) => item.createdAt },
|
|
484
|
+
{ label: 'REQUEST', value: (item) => item.requestId },
|
|
485
|
+
{ label: 'REVISION', value: (item) => item.revisionUid },
|
|
486
|
+
{ label: 'MODE', value: (item) => (item.shadow ? 'shadow' : 'active') },
|
|
487
|
+
{ label: 'STAGE', value: (item) => item.stage },
|
|
488
|
+
{ label: 'OUTCOME', value: (item) => item.outcome },
|
|
489
|
+
{
|
|
490
|
+
label: 'CHANGED',
|
|
491
|
+
value: (item) =>
|
|
492
|
+
item.differsFromActive === null ? undefined : item.differsFromActive ? 'yes' : 'no',
|
|
493
|
+
},
|
|
494
|
+
{ label: 'DURATION', value: (item) => `${String(item.durationMs)}ms` },
|
|
495
|
+
{
|
|
496
|
+
label: 'LOGS',
|
|
497
|
+
value: (item) =>
|
|
498
|
+
item.logs
|
|
499
|
+
?.map(
|
|
500
|
+
(entry) =>
|
|
501
|
+
`[${entry.level}] ${entry.message}${entry.data === undefined ? '' : ` ${JSON.stringify(entry.data)}`}`,
|
|
502
|
+
)
|
|
503
|
+
.join(' | ') ?? 'not captured',
|
|
504
|
+
},
|
|
505
|
+
{ label: 'ERROR', value: (item) => item.errorMessage },
|
|
506
|
+
],
|
|
507
|
+
options,
|
|
508
|
+
output,
|
|
509
|
+
page,
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
throw new Error(managementUsage);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
export const runManagementCommand = async (
|
|
518
|
+
arguments_,
|
|
519
|
+
{
|
|
520
|
+
environment = process.env,
|
|
521
|
+
fetchImplementation = globalThis.fetch,
|
|
522
|
+
output = process.stdout,
|
|
523
|
+
} = {},
|
|
524
|
+
) => {
|
|
525
|
+
const apiKey = environment.RAYRUN_API_KEY;
|
|
526
|
+
if (!apiKey?.trim()) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
'RAYRUN_API_KEY is required. Create a scoped key in Dashboard -> Settings -> API keys.',
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
const baseUrl = environment.RAYRUN_API_URL
|
|
532
|
+
? validateApiBaseUrl(environment.RAYRUN_API_URL)
|
|
533
|
+
: undefined;
|
|
534
|
+
const client = new Rayrun({ apiKey, baseUrl, fetch: fetchImplementation });
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
await runCommand({ arguments_, client, output });
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (!(error instanceof RayrunApiError)) throw error;
|
|
540
|
+
const requestId = error.requestId ? `, request ${error.requestId}` : '';
|
|
541
|
+
const diagnostics = error.diagnostics
|
|
542
|
+
.map(
|
|
543
|
+
(entry) =>
|
|
544
|
+
`hook.ts${entry.line === undefined ? '' : `:${String(entry.line)}${entry.column === undefined ? '' : `:${String(entry.column)}`}`}: ${entry.message}`,
|
|
545
|
+
)
|
|
546
|
+
.join('\n');
|
|
547
|
+
throw new Error(
|
|
548
|
+
`${error.message} (${error.code}${requestId})${diagnostics ? `\n${diagnostics}` : ''}`,
|
|
549
|
+
{ cause: error },
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
};
|