@rayrun/cli 0.4.0 → 0.5.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 +179 -23
- package/index.js +4 -1
- package/package.json +9 -4
- package/src/deployments.js +233 -0
- package/src/main.js +26 -0
- package/src/management.js +1020 -6
- package/src/sanitizeTerminalError.js +16 -0
- package/src/skills.js +122 -0
package/src/management.js
CHANGED
|
@@ -1,20 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseSecretBindings,
|
|
3
|
+
readDeploymentDirectory,
|
|
4
|
+
readDeploymentProjectState,
|
|
5
|
+
writeDeploymentProjectState,
|
|
6
|
+
} from './deployments.js';
|
|
7
|
+
import { readSkillDirectory, writeSkillDirectory } from './skills.js';
|
|
1
8
|
/* eslint-disable node/no-process-env -- management intentionally reads the invoking user's Rayrun environment */
|
|
2
9
|
import { Rayrun, RayrunApiError } from '@rayrun/sdk';
|
|
10
|
+
import { diffLines } from 'diff';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
3
12
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
13
|
import { stripVTControlCharacters } from 'node:util';
|
|
5
14
|
|
|
6
15
|
export const managementUsage = `Management:
|
|
16
|
+
rayrun deploy [directory] [--connection <uid>] [--name <name>] [--secret <NAME=secret-uid> ...] [--wait] [--json]
|
|
17
|
+
rayrun deployments list [--connection <uid>] [--limit <1-100>] [--json]
|
|
18
|
+
rayrun deployments get <build-uid> [--json]
|
|
19
|
+
rayrun deployments logs <build-uid> [--json]
|
|
20
|
+
rayrun deployments releases <connection-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
21
|
+
rayrun deployments rollback <connection-uid> <release-uid> [--wait] [--json]
|
|
7
22
|
rayrun connect mcp <url> --name <name> [--transport <streamable-http|sse>] [--json]
|
|
8
23
|
rayrun connect openapi <spec-url> [--name <name>] [--base-url <url>] [--json]
|
|
9
24
|
rayrun connections list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
25
|
+
rayrun connections history <connection-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
26
|
+
rayrun connections diff <connection-uid> <version-uid> [--json]
|
|
27
|
+
rayrun connections restore <connection-uid> <version-uid> [--reason <note>] [--json]
|
|
28
|
+
rayrun access-profiles list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
29
|
+
rayrun access-profiles history <profile-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
30
|
+
rayrun access-profiles diff <profile-uid> <version-uid> [--json]
|
|
31
|
+
rayrun access-profiles restore <profile-uid> <version-uid> [--reason <note>] [--confirm-risk] [--json]
|
|
32
|
+
rayrun webhooks list [--json]
|
|
33
|
+
rayrun webhooks update <webhook-uid> --config <json> [--reason <note>] [--json]
|
|
34
|
+
rayrun webhooks history <webhook-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
35
|
+
rayrun webhooks diff <webhook-uid> <version-uid> [--json]
|
|
36
|
+
rayrun webhooks restore <webhook-uid> <version-uid> [--reason <note>] [--json]
|
|
37
|
+
rayrun skills validate <directory> [--json]
|
|
38
|
+
rayrun skills push <directory> [--publish] [--confirm-risk] [--mode <both|code|direct>] [--profiles <uid,uid>|--all-clients] [--reason <note>] [--json]
|
|
39
|
+
rayrun skills list [--json]
|
|
40
|
+
rayrun skills show <skill-uid> [--json]
|
|
41
|
+
rayrun skills history <skill-uid> [--json]
|
|
42
|
+
rayrun skills diff <skill-uid> <version-uid> [--json]
|
|
43
|
+
rayrun skills pull <skill-uid> --output <new-directory> [--version <version-uid>] [--json]
|
|
44
|
+
rayrun skills publish <skill-uid> [--confirm-risk] [--mode <both|code|direct>] [--profiles <uid,uid>|--all-clients] [--reason <note>] [--json]
|
|
45
|
+
rayrun skills audience <skill-uid> [--mode <both|code|direct>] [--profiles <uid,uid>|--all-clients] [--json]
|
|
46
|
+
rayrun skills enable|disable|archive <skill-uid> [--json]
|
|
47
|
+
rayrun skills restore <skill-uid> <version-uid> [--reason <note>] [--json]
|
|
48
|
+
rayrun skills delete <skill-uid> --confirm-name <name> [--json]
|
|
10
49
|
rayrun clients list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
11
50
|
rayrun tools search <query> [--connection <uid>] [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
12
51
|
rayrun policy inspect <client-uid> [--query <query>] [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
13
52
|
rayrun approvals list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
14
53
|
rayrun activity list [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
15
54
|
rayrun hooks pull <connection-uid> <tool-uid> [--output <file>] [--types-output <file>] [--json]
|
|
55
|
+
rayrun hooks save <connection-uid> <tool-uid> [--file <file>] [--config <json>] [--reason <note>] [--json]
|
|
16
56
|
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]
|
|
57
|
+
rayrun hooks deploy <connection-uid> <tool-uid> [--file <file>] [--config <json>] [--reason <note>] [--shadow] [--json]
|
|
58
|
+
rayrun hooks history <connection-uid> <tool-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
59
|
+
rayrun hooks diff <connection-uid> <tool-uid> <version-uid> [--json]
|
|
60
|
+
rayrun hooks restore <connection-uid> <tool-uid> <version-uid> [--reason <note>] [--json]
|
|
61
|
+
rayrun hooks reset <connection-uid> <tool-uid> --confirm <hook-uid>@<version> --reason <note>
|
|
18
62
|
rayrun hooks rollback <connection-uid> <tool-uid> <revision-uid> [--shadow] [--json]
|
|
19
63
|
rayrun hooks deactivate <connection-uid> <tool-uid> [--shadow] [--json]
|
|
20
64
|
rayrun hooks logs <connection-uid> <tool-uid> [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
@@ -24,14 +68,19 @@ Environment:
|
|
|
24
68
|
RAYRUN_API_URL Optional API origin (defaults to https://ray.run)`;
|
|
25
69
|
|
|
26
70
|
const managementCommands = new Set([
|
|
71
|
+
'access-profiles',
|
|
27
72
|
'activity',
|
|
28
73
|
'approvals',
|
|
29
74
|
'clients',
|
|
30
75
|
'connect',
|
|
31
76
|
'connections',
|
|
77
|
+
'deploy',
|
|
78
|
+
'deployments',
|
|
32
79
|
'hooks',
|
|
33
80
|
'policy',
|
|
81
|
+
'skills',
|
|
34
82
|
'tools',
|
|
83
|
+
'webhooks',
|
|
35
84
|
]);
|
|
36
85
|
|
|
37
86
|
export const isManagementCommand = (command) => managementCommands.has(command);
|
|
@@ -40,20 +89,27 @@ const optionNames = new Map([
|
|
|
40
89
|
['--base-url', 'baseUrl'],
|
|
41
90
|
['--arguments', 'argumentsJson'],
|
|
42
91
|
['--config', 'configJson'],
|
|
92
|
+
['--confirm', 'confirmationHookUid'],
|
|
43
93
|
['--connection', 'connectionUid'],
|
|
94
|
+
['--confirm-name', 'confirmationName'],
|
|
44
95
|
['--cursor', 'cursor'],
|
|
45
96
|
['--limit', 'limit'],
|
|
46
97
|
['--file', 'filePath'],
|
|
47
98
|
['--mock-result', 'mockResultJson'],
|
|
48
99
|
['--name', 'name'],
|
|
100
|
+
['--mode', 'mode'],
|
|
101
|
+
['--profiles', 'profiles'],
|
|
49
102
|
['--query', 'query'],
|
|
103
|
+
['--reason', 'reason'],
|
|
50
104
|
['--output', 'outputPath'],
|
|
51
105
|
['--transport', 'transport'],
|
|
106
|
+
['--secret', 'secret'],
|
|
107
|
+
['--version', 'versionUid'],
|
|
52
108
|
['--types-output', 'typesOutputPath'],
|
|
53
109
|
]);
|
|
54
110
|
|
|
55
111
|
const parseCommandArguments = (arguments_, allowedOptions) => {
|
|
56
|
-
const options = { json: false };
|
|
112
|
+
const options = { json: false, secrets: [] };
|
|
57
113
|
const positional = [];
|
|
58
114
|
|
|
59
115
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -69,14 +125,38 @@ const parseCommandArguments = (arguments_, allowedOptions) => {
|
|
|
69
125
|
options.shadow = true;
|
|
70
126
|
continue;
|
|
71
127
|
}
|
|
128
|
+
if (argument === '--confirm-risk') {
|
|
129
|
+
if (!allowedOptions.has('confirmRisk')) throw new Error(`Unknown option: ${argument}`);
|
|
130
|
+
options.confirmRisk = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (argument === '--publish') {
|
|
134
|
+
if (!allowedOptions.has('publish')) throw new Error(`Unknown option: ${argument}`);
|
|
135
|
+
options.publish = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (argument === '--wait') {
|
|
139
|
+
if (!allowedOptions.has('wait')) throw new Error(`Unknown option: ${argument}`);
|
|
140
|
+
options.wait = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (argument === '--all-clients') {
|
|
144
|
+
if (!allowedOptions.has('allClients')) throw new Error(`Unknown option: ${argument}`);
|
|
145
|
+
options.allClients = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
72
148
|
|
|
73
149
|
const optionName = optionNames.get(argument);
|
|
74
150
|
if (optionName) {
|
|
75
151
|
if (!allowedOptions.has(optionName)) throw new Error(`Unknown option: ${argument}`);
|
|
76
152
|
const value = arguments_[index + 1];
|
|
77
153
|
if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value.`);
|
|
78
|
-
if (
|
|
79
|
-
|
|
154
|
+
if (optionName === 'secret') options.secrets.push(value);
|
|
155
|
+
else {
|
|
156
|
+
if (options[optionName] !== undefined)
|
|
157
|
+
throw new Error(`${argument} can only be used once.`);
|
|
158
|
+
options[optionName] = value;
|
|
159
|
+
}
|
|
80
160
|
index += 1;
|
|
81
161
|
continue;
|
|
82
162
|
}
|
|
@@ -123,12 +203,31 @@ const formatCell = (value) => {
|
|
|
123
203
|
if (typeof value === 'boolean') return value ? 'yes' : 'no';
|
|
124
204
|
const sanitized = stripVTControlCharacters(String(value))
|
|
125
205
|
.replaceAll(/\p{Cc}+/gu, ' ')
|
|
206
|
+
.replaceAll(
|
|
207
|
+
/\p{Bidi_Control}/gu,
|
|
208
|
+
(character) => `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`,
|
|
209
|
+
)
|
|
126
210
|
.replaceAll(/\s+/gu, ' ')
|
|
127
211
|
.trim();
|
|
128
212
|
|
|
129
213
|
return sanitized || '-';
|
|
130
214
|
};
|
|
131
215
|
|
|
216
|
+
const formatTerminalText = (value) => {
|
|
217
|
+
return stripVTControlCharacters(String(value))
|
|
218
|
+
.replaceAll(/\r\n?/gu, '\n')
|
|
219
|
+
.split('\n')
|
|
220
|
+
.map((line) =>
|
|
221
|
+
line
|
|
222
|
+
.replaceAll(/\p{Cc}+/gu, ' ')
|
|
223
|
+
.replaceAll(
|
|
224
|
+
/\p{Bidi_Control}/gu,
|
|
225
|
+
(character) => `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`,
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
.join('\n');
|
|
229
|
+
};
|
|
230
|
+
|
|
132
231
|
export const renderTable = (items, columns) => {
|
|
133
232
|
if (items.length === 0) return 'No results.\n';
|
|
134
233
|
|
|
@@ -181,10 +280,491 @@ const parseJsonOption = (value, option, fallback) => {
|
|
|
181
280
|
|
|
182
281
|
const hookMode = (options) => (options.shadow ? 'shadow' : 'active');
|
|
183
282
|
|
|
283
|
+
const deploymentFinished = (deployment) =>
|
|
284
|
+
['cancelled', 'failed'].includes(deployment.build.status) ||
|
|
285
|
+
(deployment.release &&
|
|
286
|
+
['active', 'cancelled', 'failed', 'superseded'].includes(deployment.release.status));
|
|
287
|
+
|
|
288
|
+
const waitForDeployment = async (client, { buildUid, connectionUid, releaseUid }) => {
|
|
289
|
+
const deadline = Date.now() + 60 * 60 * 1_000;
|
|
290
|
+
while (Date.now() < deadline) {
|
|
291
|
+
const deployment = await client.deployments.get(buildUid);
|
|
292
|
+
if (releaseUid) {
|
|
293
|
+
const exactRelease = await client.deployments.getRelease(connectionUid, releaseUid);
|
|
294
|
+
const exactDeployment = { ...deployment, release: exactRelease };
|
|
295
|
+
if (deploymentFinished(exactDeployment)) return exactDeployment;
|
|
296
|
+
} else if (deploymentFinished(deployment)) {
|
|
297
|
+
return deployment;
|
|
298
|
+
}
|
|
299
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
300
|
+
}
|
|
301
|
+
throw new Error(
|
|
302
|
+
'Deployment is still running after 60 minutes. Inspect it with rayrun deployments get.',
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const writeDeployment = ({ deployment, options, output }) => {
|
|
307
|
+
if (options.json) {
|
|
308
|
+
writeValue(output, deployment);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const status = deployment.release?.status ?? deployment.build.status;
|
|
312
|
+
const toolSummary = deployment.release?.toolNames.map(formatCell).join(', ');
|
|
313
|
+
output.write(
|
|
314
|
+
`${formatCell(deployment.connection.displayName)}: ${formatCell(status)} (build ${formatCell(deployment.build.uid)}, source r${String(deployment.source.revision)})${toolSummary ? ` — ${toolSummary}` : ''}\n`,
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const renderLineDiff = (before, after, beforeLabel, afterLabel) => {
|
|
319
|
+
const parts = diffLines(before, after, { timeout: 75 });
|
|
320
|
+
if (!parts) {
|
|
321
|
+
return `--- ${beforeLabel}\n+++ ${afterLabel}\nDiff exceeded the 75 ms safety limit. Use --json to inspect both versions.\n`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const lines = parts
|
|
325
|
+
.flatMap((part) => {
|
|
326
|
+
const prefix = part.added ? '+' : part.removed ? '-' : ' ';
|
|
327
|
+
const partLines = stripVTControlCharacters(part.value)
|
|
328
|
+
.split('\n')
|
|
329
|
+
.map((line) =>
|
|
330
|
+
line
|
|
331
|
+
.replaceAll(/\p{Cc}+/gu, ' ')
|
|
332
|
+
.replaceAll(
|
|
333
|
+
/\p{Bidi_Control}/gu,
|
|
334
|
+
(character) => `\\u{${character.codePointAt(0).toString(16).toUpperCase()}}`,
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (partLines.at(-1) === '') partLines.pop();
|
|
339
|
+
|
|
340
|
+
return partLines.map((line) => `${prefix}${line}`);
|
|
341
|
+
})
|
|
342
|
+
.join('\n');
|
|
343
|
+
|
|
344
|
+
return `--- ${beforeLabel}\n+++ ${afterLabel}\n${lines}\n`;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const skillFileSummary = (files) => {
|
|
348
|
+
return files.map((file) => {
|
|
349
|
+
const bytes = Buffer.from(file.contentBase64, 'base64');
|
|
350
|
+
return {
|
|
351
|
+
digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}`,
|
|
352
|
+
path: file.path,
|
|
353
|
+
size: bytes.byteLength,
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const skillAudience = (options, fallback) => {
|
|
359
|
+
if (options.allClients && options.profiles !== undefined) {
|
|
360
|
+
throw new Error('Use either --profiles or --all-clients, not both.');
|
|
361
|
+
}
|
|
362
|
+
const deliveryMode = options.mode ?? fallback?.deliveryMode ?? 'both';
|
|
363
|
+
if (!['both', 'code', 'direct'].includes(deliveryMode)) {
|
|
364
|
+
throw new Error('--mode must be both, code, or direct.');
|
|
365
|
+
}
|
|
366
|
+
let profileUids;
|
|
367
|
+
if (options.allClients) {
|
|
368
|
+
profileUids = [];
|
|
369
|
+
} else if (options.profiles !== undefined) {
|
|
370
|
+
profileUids = options.profiles
|
|
371
|
+
.split(',')
|
|
372
|
+
.map((uid) => uid.trim())
|
|
373
|
+
.filter(Boolean);
|
|
374
|
+
if (profileUids.length === 0) {
|
|
375
|
+
throw new Error('--profiles requires at least one profile UID.');
|
|
376
|
+
}
|
|
377
|
+
} else if (fallback) {
|
|
378
|
+
profileUids = fallback.profileUids;
|
|
379
|
+
} else {
|
|
380
|
+
throw new Error('Choose an audience with --profiles <uid,uid> or --all-clients.');
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
deliveryMode,
|
|
384
|
+
profileUids,
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
|
|
184
388
|
const runCommand = async ({ arguments_, client, output }) => {
|
|
185
389
|
const command = arguments_[0];
|
|
186
390
|
const action = arguments_[1];
|
|
187
391
|
|
|
392
|
+
if (command === 'deploy') {
|
|
393
|
+
const { options, positional } = parseCommandArguments(
|
|
394
|
+
arguments_.slice(1),
|
|
395
|
+
new Set(['connectionUid', 'json', 'name', 'secret', 'wait']),
|
|
396
|
+
);
|
|
397
|
+
requireShape(positional.length <= 1);
|
|
398
|
+
const directory = positional[0] ?? '.';
|
|
399
|
+
const state = options.connectionUid ? null : await readDeploymentProjectState(directory);
|
|
400
|
+
const connectionUid = options.connectionUid ?? state?.connectionUid;
|
|
401
|
+
if (connectionUid && options.name) {
|
|
402
|
+
throw new Error('--name is only available when creating a source deployment.');
|
|
403
|
+
}
|
|
404
|
+
const files = await readDeploymentDirectory(directory);
|
|
405
|
+
await client.deployments.validate(files);
|
|
406
|
+
const created = await client.deployments.create({
|
|
407
|
+
connectionUid,
|
|
408
|
+
displayName: options.name,
|
|
409
|
+
files,
|
|
410
|
+
secretBindings: parseSecretBindings(options.secrets),
|
|
411
|
+
});
|
|
412
|
+
await writeDeploymentProjectState(directory, created.connectionUid);
|
|
413
|
+
if (options.wait) {
|
|
414
|
+
const deployment = await waitForDeployment(client, created);
|
|
415
|
+
writeDeployment({ deployment, options, output });
|
|
416
|
+
if (
|
|
417
|
+
['cancelled', 'failed'].includes(deployment.build.status) ||
|
|
418
|
+
(deployment.release &&
|
|
419
|
+
['cancelled', 'failed', 'superseded'].includes(deployment.release.status))
|
|
420
|
+
) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Deployment ${created.buildUid} failed. Run rayrun deployments logs ${created.buildUid}.`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
} else if (options.json) writeValue(output, created);
|
|
426
|
+
else {
|
|
427
|
+
output.write(
|
|
428
|
+
created.unchanged
|
|
429
|
+
? `Already active as build ${created.buildUid}.\n`
|
|
430
|
+
: `Queued build ${created.buildUid}. Wait with: rayrun deployments get ${created.buildUid}\n`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (command === 'deployments' && action === 'list') {
|
|
437
|
+
const { options, positional } = parseCommandArguments(
|
|
438
|
+
arguments_.slice(2),
|
|
439
|
+
new Set(['connectionUid', 'json', 'limit']),
|
|
440
|
+
);
|
|
441
|
+
requireShape(positional.length === 0);
|
|
442
|
+
const response = await client.deployments.list({
|
|
443
|
+
connectionUid: options.connectionUid,
|
|
444
|
+
limit: options.limit,
|
|
445
|
+
});
|
|
446
|
+
if (options.json) writeValue(output, response);
|
|
447
|
+
else {
|
|
448
|
+
output.write(
|
|
449
|
+
renderTable(response.items, [
|
|
450
|
+
{ label: 'BUILD', value: (item) => item.build.uid },
|
|
451
|
+
{ label: 'SERVICE', value: (item) => item.connection.displayName },
|
|
452
|
+
{ label: 'SOURCE', value: (item) => `r${String(item.source.revision)}` },
|
|
453
|
+
{ label: 'STATUS', value: (item) => item.release?.status ?? item.build.status },
|
|
454
|
+
{ label: 'CREATED', value: (item) => item.build.createdAt },
|
|
455
|
+
]),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (command === 'deployments' && action === 'get') {
|
|
462
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
463
|
+
requireShape(positional.length === 1);
|
|
464
|
+
writeDeployment({ deployment: await client.deployments.get(positional[0]), options, output });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (command === 'deployments' && action === 'logs') {
|
|
469
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
470
|
+
requireShape(positional.length === 1);
|
|
471
|
+
const buildLog = await client.deployments.getLog(positional[0]);
|
|
472
|
+
if (options.json) writeValue(output, buildLog);
|
|
473
|
+
else
|
|
474
|
+
output.write(
|
|
475
|
+
`${buildLog.truncated ? '[older output truncated]\n' : ''}${formatTerminalText(buildLog.text)}\n`,
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (command === 'deployments' && action === 'releases') {
|
|
481
|
+
const { options, positional } = parseCommandArguments(
|
|
482
|
+
arguments_.slice(2),
|
|
483
|
+
new Set(['cursor', 'json', 'limit']),
|
|
484
|
+
);
|
|
485
|
+
requireShape(positional.length === 1);
|
|
486
|
+
const response = await client.deployments.listReleases(positional[0], pageQuery(options));
|
|
487
|
+
writePage({
|
|
488
|
+
columns: [
|
|
489
|
+
{ label: 'RELEASE', value: (item) => item.uid },
|
|
490
|
+
{ label: 'NUMBER', value: (item) => item.number },
|
|
491
|
+
{ label: 'SOURCE', value: (item) => `r${String(item.sourceRevision)}` },
|
|
492
|
+
{ label: 'STATUS', value: (item) => item.status },
|
|
493
|
+
{ label: 'ACTIVATED', value: (item) => item.activatedAt },
|
|
494
|
+
],
|
|
495
|
+
options,
|
|
496
|
+
output,
|
|
497
|
+
page: response,
|
|
498
|
+
});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (command === 'deployments' && action === 'rollback') {
|
|
503
|
+
const { options, positional } = parseCommandArguments(
|
|
504
|
+
arguments_.slice(2),
|
|
505
|
+
new Set(['json', 'wait']),
|
|
506
|
+
);
|
|
507
|
+
requireShape(positional.length === 2);
|
|
508
|
+
const created = await client.deployments.rollback(positional[0], positional[1]);
|
|
509
|
+
if (options.wait) {
|
|
510
|
+
const deadline = Date.now() + 20 * 60 * 1_000;
|
|
511
|
+
let release;
|
|
512
|
+
while (Date.now() < deadline) {
|
|
513
|
+
release = await client.deployments.getRelease(positional[0], created.releaseUid);
|
|
514
|
+
if (['active', 'cancelled', 'failed', 'superseded'].includes(release.status)) break;
|
|
515
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
516
|
+
}
|
|
517
|
+
if (!release || !['active', 'cancelled', 'failed', 'superseded'].includes(release.status)) {
|
|
518
|
+
throw new Error('Rollback is still running after 20 minutes.');
|
|
519
|
+
}
|
|
520
|
+
if (options.json) writeValue(output, release);
|
|
521
|
+
else output.write(`Release ${release.uid}: ${release.status}.\n`);
|
|
522
|
+
if (release.status !== 'active') throw new Error(`Rollback ${release.uid} failed.`);
|
|
523
|
+
} else if (options.json) writeValue(output, created);
|
|
524
|
+
else output.write(`Queued rollback release ${created.releaseUid}.\n`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (command === 'skills' && action === 'validate') {
|
|
529
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
530
|
+
requireShape(positional.length === 1);
|
|
531
|
+
const result = await client.skills.validate(await readSkillDirectory(positional[0]));
|
|
532
|
+
if (options.json) writeValue(output, result);
|
|
533
|
+
else {
|
|
534
|
+
output.write(
|
|
535
|
+
`Valid ${result.name}: ${String(result.fileCount)} files, ${String(result.byteCount)} bytes${result.riskFlags.length ? `; review ${result.riskFlags.join(', ')}` : ''}.\n`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (command === 'skills' && action === 'push') {
|
|
542
|
+
const { options, positional } = parseCommandArguments(
|
|
543
|
+
arguments_.slice(2),
|
|
544
|
+
new Set(['allClients', 'confirmRisk', 'json', 'mode', 'profiles', 'publish', 'reason']),
|
|
545
|
+
);
|
|
546
|
+
requireShape(positional.length === 1);
|
|
547
|
+
const files = await readSkillDirectory(positional[0]);
|
|
548
|
+
const validation = await client.skills.validate(files);
|
|
549
|
+
const listed = await client.skills.list();
|
|
550
|
+
const existing = listed.items.find((item) => item.name === validation.name);
|
|
551
|
+
const audience = options.publish
|
|
552
|
+
? skillAudience(
|
|
553
|
+
options,
|
|
554
|
+
existing
|
|
555
|
+
? {
|
|
556
|
+
deliveryMode: existing.deliveryMode,
|
|
557
|
+
profileUids: existing.profiles.map((profile) => profile.uid),
|
|
558
|
+
}
|
|
559
|
+
: undefined,
|
|
560
|
+
)
|
|
561
|
+
: undefined;
|
|
562
|
+
let result = existing
|
|
563
|
+
? await client.skills.saveDraft(existing.uid, {
|
|
564
|
+
changeReason: options.reason,
|
|
565
|
+
expectedVersion: existing.configurationVersion,
|
|
566
|
+
files,
|
|
567
|
+
source: 'cli',
|
|
568
|
+
})
|
|
569
|
+
: await client.skills.create({ changeReason: options.reason, files, source: 'cli' });
|
|
570
|
+
if (options.publish) {
|
|
571
|
+
result = await client.skills.publish(result.skillUid, {
|
|
572
|
+
...audience,
|
|
573
|
+
changeReason: options.reason,
|
|
574
|
+
expectedVersion: result.configurationVersion,
|
|
575
|
+
riskConfirmed: options.confirmRisk,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (options.json) writeValue(output, result);
|
|
579
|
+
else {
|
|
580
|
+
output.write(
|
|
581
|
+
`${options.publish ? 'Published' : result.unchanged ? 'Unchanged' : 'Saved'} ${validation.name} (${result.skillUid}) at configuration v${String(result.configurationVersion)}.\n`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (command === 'skills' && action === 'list') {
|
|
588
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
589
|
+
requireShape(positional.length === 0);
|
|
590
|
+
const result = await client.skills.list();
|
|
591
|
+
if (options.json) writeValue(output, result);
|
|
592
|
+
else {
|
|
593
|
+
output.write(
|
|
594
|
+
renderTable(result.items, [
|
|
595
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
596
|
+
{ label: 'NAME', value: (item) => item.name },
|
|
597
|
+
{ label: 'STATE', value: (item) => item.state },
|
|
598
|
+
{ label: 'REVISION', value: (item) => item.publishedRevisionNumber },
|
|
599
|
+
{ label: 'MODE', value: (item) => item.deliveryMode },
|
|
600
|
+
{ label: 'FETCHES', value: (item) => item.fetchCount },
|
|
601
|
+
]),
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (command === 'skills' && ['history', 'show'].includes(action)) {
|
|
608
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
609
|
+
requireShape(positional.length === 1);
|
|
610
|
+
const detail = await client.skills.get(positional[0]);
|
|
611
|
+
if (options.json || action === 'show') writeValue(output, detail);
|
|
612
|
+
else {
|
|
613
|
+
output.write(
|
|
614
|
+
renderTable(detail.versions, [
|
|
615
|
+
{ label: 'REVISION', value: (item) => item.revisionNumber },
|
|
616
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
617
|
+
{ label: 'WHEN', value: (item) => item.createdAt },
|
|
618
|
+
{ label: 'ACTOR', value: (item) => item.actorName },
|
|
619
|
+
{ label: 'DRAFT', value: (item) => item.isCurrentDraft },
|
|
620
|
+
{ label: 'PUBLISHED', value: (item) => item.isPublished },
|
|
621
|
+
{ label: 'NOTE', value: (item) => item.changeReason },
|
|
622
|
+
]),
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (command === 'skills' && action === 'diff') {
|
|
629
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
630
|
+
requireShape(positional.length === 2);
|
|
631
|
+
const [draft, version] = await Promise.all([
|
|
632
|
+
client.skills.export(positional[0]),
|
|
633
|
+
client.skills.exportVersion(positional[0], positional[1]),
|
|
634
|
+
]);
|
|
635
|
+
const comparison = {
|
|
636
|
+
draft: skillFileSummary(draft.files),
|
|
637
|
+
version: skillFileSummary(version.files),
|
|
638
|
+
};
|
|
639
|
+
if (options.json) writeValue(output, comparison);
|
|
640
|
+
else {
|
|
641
|
+
output.write(
|
|
642
|
+
renderLineDiff(
|
|
643
|
+
`${JSON.stringify(comparison.version, null, 2)}\n`,
|
|
644
|
+
`${JSON.stringify(comparison.draft, null, 2)}\n`,
|
|
645
|
+
positional[1],
|
|
646
|
+
'current draft',
|
|
647
|
+
),
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (command === 'skills' && action === 'pull') {
|
|
654
|
+
const { options, positional } = parseCommandArguments(
|
|
655
|
+
arguments_.slice(2),
|
|
656
|
+
new Set(['json', 'outputPath', 'versionUid']),
|
|
657
|
+
);
|
|
658
|
+
requireShape(positional.length === 1 && typeof options.outputPath === 'string');
|
|
659
|
+
const result = options.versionUid
|
|
660
|
+
? await client.skills.exportVersion(positional[0], options.versionUid)
|
|
661
|
+
: await client.skills.export(positional[0]);
|
|
662
|
+
await writeSkillDirectory(options.outputPath, result.files);
|
|
663
|
+
if (options.json)
|
|
664
|
+
writeValue(output, { fileCount: result.files.length, output: options.outputPath });
|
|
665
|
+
else output.write(`Wrote ${String(result.files.length)} files to ${options.outputPath}.\n`);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (command === 'skills' && action === 'publish') {
|
|
670
|
+
const { options, positional } = parseCommandArguments(
|
|
671
|
+
arguments_.slice(2),
|
|
672
|
+
new Set(['allClients', 'confirmRisk', 'json', 'mode', 'profiles', 'reason']),
|
|
673
|
+
);
|
|
674
|
+
requireShape(positional.length === 1);
|
|
675
|
+
const detail = await client.skills.get(positional[0]);
|
|
676
|
+
const result = await client.skills.publish(positional[0], {
|
|
677
|
+
...skillAudience(options, {
|
|
678
|
+
deliveryMode: detail.skill.deliveryMode,
|
|
679
|
+
profileUids: detail.skill.profiles.map((profile) => profile.uid),
|
|
680
|
+
}),
|
|
681
|
+
changeReason: options.reason,
|
|
682
|
+
expectedVersion: detail.skill.configurationVersion,
|
|
683
|
+
riskConfirmed: options.confirmRisk,
|
|
684
|
+
});
|
|
685
|
+
if (options.json) writeValue(output, result);
|
|
686
|
+
else
|
|
687
|
+
output.write(
|
|
688
|
+
`Published ${detail.skill.name} at configuration v${String(result.configurationVersion)}.\n`,
|
|
689
|
+
);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (command === 'skills' && action === 'audience') {
|
|
694
|
+
const { options, positional } = parseCommandArguments(
|
|
695
|
+
arguments_.slice(2),
|
|
696
|
+
new Set(['allClients', 'json', 'mode', 'profiles']),
|
|
697
|
+
);
|
|
698
|
+
requireShape(positional.length === 1);
|
|
699
|
+
const detail = await client.skills.get(positional[0]);
|
|
700
|
+
const result = await client.skills.updateAudience(positional[0], {
|
|
701
|
+
...skillAudience(options, {
|
|
702
|
+
deliveryMode: detail.skill.deliveryMode,
|
|
703
|
+
profileUids: detail.skill.profiles.map((profile) => profile.uid),
|
|
704
|
+
}),
|
|
705
|
+
expectedVersion: detail.skill.configurationVersion,
|
|
706
|
+
});
|
|
707
|
+
if (options.json) writeValue(output, result);
|
|
708
|
+
else {
|
|
709
|
+
output.write(
|
|
710
|
+
`${result.unchanged ? 'Kept' : 'Updated'} audience for ${detail.skill.name} at configuration v${String(result.configurationVersion)}.\n`,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (command === 'skills' && ['archive', 'disable', 'enable'].includes(action)) {
|
|
717
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
718
|
+
requireShape(positional.length === 1);
|
|
719
|
+
const detail = await client.skills.get(positional[0]);
|
|
720
|
+
const result =
|
|
721
|
+
action === 'archive'
|
|
722
|
+
? await client.skills.archive(positional[0], detail.skill.configurationVersion)
|
|
723
|
+
: await client.skills.setEnabled(
|
|
724
|
+
positional[0],
|
|
725
|
+
action === 'enable',
|
|
726
|
+
detail.skill.configurationVersion,
|
|
727
|
+
);
|
|
728
|
+
if (options.json) writeValue(output, result);
|
|
729
|
+
else output.write(`${action[0].toUpperCase()}${action.slice(1)}d ${detail.skill.name}.\n`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (command === 'skills' && action === 'restore') {
|
|
734
|
+
const { options, positional } = parseCommandArguments(
|
|
735
|
+
arguments_.slice(2),
|
|
736
|
+
new Set(['json', 'reason']),
|
|
737
|
+
);
|
|
738
|
+
requireShape(positional.length === 2);
|
|
739
|
+
const detail = await client.skills.get(positional[0]);
|
|
740
|
+
const result = await client.skills.restoreVersion(positional[0], positional[1], {
|
|
741
|
+
changeReason: options.reason,
|
|
742
|
+
expectedVersion: detail.skill.configurationVersion,
|
|
743
|
+
});
|
|
744
|
+
if (options.json) writeValue(output, result);
|
|
745
|
+
else output.write(`Restored ${positional[1]} as a new draft.\n`);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (command === 'skills' && action === 'delete') {
|
|
750
|
+
const { options, positional } = parseCommandArguments(
|
|
751
|
+
arguments_.slice(2),
|
|
752
|
+
new Set(['confirmationName', 'json']),
|
|
753
|
+
);
|
|
754
|
+
requireShape(positional.length === 1 && typeof options.confirmationName === 'string');
|
|
755
|
+
const detail = await client.skills.get(positional[0]);
|
|
756
|
+
const result = await client.skills.delete(positional[0], {
|
|
757
|
+
confirmationName: options.confirmationName,
|
|
758
|
+
expectedVersion: detail.skill.configurationVersion,
|
|
759
|
+
});
|
|
760
|
+
if (options.json) writeValue(output, result);
|
|
761
|
+
else
|
|
762
|
+
output.write(
|
|
763
|
+
`Permanently deleted content for ${detail.skill.name}; its name remains reserved.\n`,
|
|
764
|
+
);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
188
768
|
if (command === 'connect' && action === 'mcp') {
|
|
189
769
|
const { options, positional } = parseCommandArguments(
|
|
190
770
|
arguments_.slice(2),
|
|
@@ -244,6 +824,306 @@ const runCommand = async ({ arguments_, client, output }) => {
|
|
|
244
824
|
return;
|
|
245
825
|
}
|
|
246
826
|
|
|
827
|
+
if (command === 'connections' && action === 'history') {
|
|
828
|
+
const { options, positional } = parseCommandArguments(
|
|
829
|
+
arguments_.slice(2),
|
|
830
|
+
new Set(['cursor', 'json', 'limit']),
|
|
831
|
+
);
|
|
832
|
+
requireShape(positional.length === 1);
|
|
833
|
+
const page = await client.connections.listConfigurationVersions(
|
|
834
|
+
positional[0],
|
|
835
|
+
pageQuery(options),
|
|
836
|
+
);
|
|
837
|
+
writePage({
|
|
838
|
+
columns: [
|
|
839
|
+
{ label: 'VERSION', value: (item) => item.revisionNumber },
|
|
840
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
841
|
+
{ label: 'WHEN', value: (item) => item.createdAt },
|
|
842
|
+
{ label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
|
|
843
|
+
{ label: 'CHANGE', value: (item) => item.changeType },
|
|
844
|
+
{ label: 'NOTE', value: (item) => item.changeReason },
|
|
845
|
+
{ label: 'CURRENT', value: (item) => item.isCurrent },
|
|
846
|
+
],
|
|
847
|
+
options,
|
|
848
|
+
output,
|
|
849
|
+
page,
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (command === 'connections' && action === 'diff') {
|
|
855
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
856
|
+
requireShape(positional.length === 2);
|
|
857
|
+
const result = await client.connections.getConfigurationVersion(positional[0], positional[1]);
|
|
858
|
+
if (options.json) {
|
|
859
|
+
writeValue(output, result);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const previousLabel = result.version.previous
|
|
863
|
+
? `configuration v${String(result.version.previous.revisionNumber)}`
|
|
864
|
+
: 'empty configuration';
|
|
865
|
+
output.write(
|
|
866
|
+
renderLineDiff(
|
|
867
|
+
`${JSON.stringify(result.version.previous?.snapshot ?? {}, null, 2)}\n`,
|
|
868
|
+
`${JSON.stringify(result.version.snapshot, null, 2)}\n`,
|
|
869
|
+
previousLabel,
|
|
870
|
+
`configuration v${String(result.version.revisionNumber)}`,
|
|
871
|
+
),
|
|
872
|
+
);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (command === 'connections' && action === 'restore') {
|
|
877
|
+
const { options, positional } = parseCommandArguments(
|
|
878
|
+
arguments_.slice(2),
|
|
879
|
+
new Set(['json', 'reason']),
|
|
880
|
+
);
|
|
881
|
+
requireShape(positional.length === 2);
|
|
882
|
+
const history = await client.connections.listConfigurationVersions(positional[0], { limit: 1 });
|
|
883
|
+
const current = history.items[0];
|
|
884
|
+
if (!current?.isCurrent) {
|
|
885
|
+
throw new Error('The current connection configuration could not be read.');
|
|
886
|
+
}
|
|
887
|
+
const result = await client.connections.restoreConfigurationVersion(
|
|
888
|
+
positional[0],
|
|
889
|
+
positional[1],
|
|
890
|
+
{ expectedVersion: current.revisionNumber, reason: options.reason },
|
|
891
|
+
);
|
|
892
|
+
if (options.json) writeValue(output, result);
|
|
893
|
+
else
|
|
894
|
+
output.write(
|
|
895
|
+
`Restored ${positional[1]} as connection configuration v${String(result.configurationVersion)} (${result.versionUid}).\n`,
|
|
896
|
+
);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (command === 'access-profiles' && action === 'list') {
|
|
901
|
+
const { options, positional } = parseCommandArguments(
|
|
902
|
+
arguments_.slice(2),
|
|
903
|
+
new Set(['cursor', 'json', 'limit']),
|
|
904
|
+
);
|
|
905
|
+
requireShape(positional.length === 0);
|
|
906
|
+
const page = await client.accessProfiles.list(pageQuery(options));
|
|
907
|
+
writePage({
|
|
908
|
+
columns: [
|
|
909
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
910
|
+
{ label: 'NAME', value: (item) => item.name },
|
|
911
|
+
{ label: 'POLICY', value: (item) => item.toolAccessMode },
|
|
912
|
+
{ label: 'VERSION', value: (item) => item.toolPolicyVersion },
|
|
913
|
+
{ label: 'CLIENTS', value: (item) => item.assignedClientCount },
|
|
914
|
+
{ label: 'SKILLS', value: (item) => item.assignedSkillCount },
|
|
915
|
+
],
|
|
916
|
+
options,
|
|
917
|
+
output,
|
|
918
|
+
page,
|
|
919
|
+
});
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (command === 'access-profiles' && action === 'history') {
|
|
924
|
+
const { options, positional } = parseCommandArguments(
|
|
925
|
+
arguments_.slice(2),
|
|
926
|
+
new Set(['cursor', 'json', 'limit']),
|
|
927
|
+
);
|
|
928
|
+
requireShape(positional.length === 1);
|
|
929
|
+
const page = await client.accessProfiles.listConfigurationVersions(
|
|
930
|
+
positional[0],
|
|
931
|
+
pageQuery(options),
|
|
932
|
+
);
|
|
933
|
+
writePage({
|
|
934
|
+
columns: [
|
|
935
|
+
{ label: 'VERSION', value: (item) => item.revisionNumber },
|
|
936
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
937
|
+
{ label: 'WHEN', value: (item) => item.createdAt },
|
|
938
|
+
{ label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
|
|
939
|
+
{ label: 'CHANGE', value: (item) => item.changeType },
|
|
940
|
+
{ label: 'NOTE', value: (item) => item.changeReason },
|
|
941
|
+
{ label: 'CURRENT', value: (item) => item.isCurrent },
|
|
942
|
+
],
|
|
943
|
+
options,
|
|
944
|
+
output,
|
|
945
|
+
page,
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (command === 'access-profiles' && action === 'diff') {
|
|
951
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
952
|
+
requireShape(positional.length === 2);
|
|
953
|
+
const result = await client.accessProfiles.getConfigurationVersion(
|
|
954
|
+
positional[0],
|
|
955
|
+
positional[1],
|
|
956
|
+
);
|
|
957
|
+
if (options.json) {
|
|
958
|
+
writeValue(output, result);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const previousLabel = result.version.previous
|
|
962
|
+
? `profile v${String(result.version.previous.revisionNumber)}`
|
|
963
|
+
: 'empty profile';
|
|
964
|
+
output.write(
|
|
965
|
+
renderLineDiff(
|
|
966
|
+
`${JSON.stringify(result.version.previous?.snapshot ?? {}, null, 2)}\n`,
|
|
967
|
+
`${JSON.stringify(result.version.snapshot, null, 2)}\n`,
|
|
968
|
+
previousLabel,
|
|
969
|
+
`profile v${String(result.version.revisionNumber)}`,
|
|
970
|
+
),
|
|
971
|
+
);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (command === 'access-profiles' && action === 'restore') {
|
|
976
|
+
const { options, positional } = parseCommandArguments(
|
|
977
|
+
arguments_.slice(2),
|
|
978
|
+
new Set(['confirmRisk', 'json', 'reason']),
|
|
979
|
+
);
|
|
980
|
+
requireShape(positional.length === 2);
|
|
981
|
+
const history = await client.accessProfiles.listConfigurationVersions(positional[0], {
|
|
982
|
+
limit: 1,
|
|
983
|
+
});
|
|
984
|
+
const current = history.items[0];
|
|
985
|
+
if (!current?.isCurrent) {
|
|
986
|
+
throw new Error('The current access-profile configuration could not be read.');
|
|
987
|
+
}
|
|
988
|
+
const result = await client.accessProfiles.restoreConfigurationVersion(
|
|
989
|
+
positional[0],
|
|
990
|
+
positional[1],
|
|
991
|
+
{
|
|
992
|
+
expectedVersion: current.revisionNumber,
|
|
993
|
+
reason: options.reason,
|
|
994
|
+
riskConfirmed: options.confirmRisk === true,
|
|
995
|
+
},
|
|
996
|
+
);
|
|
997
|
+
if (options.json) writeValue(output, result);
|
|
998
|
+
else
|
|
999
|
+
output.write(
|
|
1000
|
+
`Restored ${positional[1]} as access-profile configuration v${String(result.toolPolicyVersion)} (${result.versionUid}).\n`,
|
|
1001
|
+
);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (command === 'webhooks' && action === 'list') {
|
|
1006
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
1007
|
+
requireShape(positional.length === 0);
|
|
1008
|
+
const result = await client.webhooks.list();
|
|
1009
|
+
if (options.json) {
|
|
1010
|
+
writeValue(output, result);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
output.write(
|
|
1014
|
+
renderTable(result.items, [
|
|
1015
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
1016
|
+
{ label: 'DESCRIPTION', value: (item) => item.description },
|
|
1017
|
+
{ label: 'DESTINATION', value: (item) => item.url },
|
|
1018
|
+
{ label: 'ENABLED', value: (item) => item.enabled },
|
|
1019
|
+
{ label: 'VERSION', value: (item) => item.configurationVersion },
|
|
1020
|
+
{ label: 'HEALTH', value: (item) => item.health },
|
|
1021
|
+
]),
|
|
1022
|
+
);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (command === 'webhooks' && action === 'update') {
|
|
1027
|
+
const { options, positional } = parseCommandArguments(
|
|
1028
|
+
arguments_.slice(2),
|
|
1029
|
+
new Set(['configJson', 'json', 'reason']),
|
|
1030
|
+
);
|
|
1031
|
+
requireShape(positional.length === 1 && options.configJson !== undefined);
|
|
1032
|
+
const configuration = parseJsonOption(options.configJson, '--config');
|
|
1033
|
+
if (!configuration || typeof configuration !== 'object' || Array.isArray(configuration)) {
|
|
1034
|
+
throw new Error('--config must be a JSON object.');
|
|
1035
|
+
}
|
|
1036
|
+
const listed = await client.webhooks.list();
|
|
1037
|
+
const current = listed.items.find((webhook) => webhook.uid === positional[0]);
|
|
1038
|
+
if (!current) {
|
|
1039
|
+
throw new Error(`Webhook ${positional[0]} was not found.`);
|
|
1040
|
+
}
|
|
1041
|
+
const result = await client.webhooks.update(positional[0], {
|
|
1042
|
+
...configuration,
|
|
1043
|
+
expectedVersion: current.configurationVersion,
|
|
1044
|
+
reason: options.reason,
|
|
1045
|
+
});
|
|
1046
|
+
if (options.json) writeValue(output, result);
|
|
1047
|
+
else if (result.unchanged)
|
|
1048
|
+
output.write(
|
|
1049
|
+
`No changes; webhook configuration v${String(result.configurationVersion)} is current.\n`,
|
|
1050
|
+
);
|
|
1051
|
+
else
|
|
1052
|
+
output.write(
|
|
1053
|
+
`Saved webhook configuration v${String(result.configurationVersion)} (${result.versionUid}).\n`,
|
|
1054
|
+
);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (command === 'webhooks' && action === 'history') {
|
|
1059
|
+
const { options, positional } = parseCommandArguments(
|
|
1060
|
+
arguments_.slice(2),
|
|
1061
|
+
new Set(['cursor', 'json', 'limit']),
|
|
1062
|
+
);
|
|
1063
|
+
requireShape(positional.length === 1);
|
|
1064
|
+
const page = await client.webhooks.listConfigurationVersions(positional[0], pageQuery(options));
|
|
1065
|
+
writePage({
|
|
1066
|
+
columns: [
|
|
1067
|
+
{ label: 'VERSION', value: (item) => item.revisionNumber },
|
|
1068
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
1069
|
+
{ label: 'WHEN', value: (item) => item.createdAt },
|
|
1070
|
+
{ label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
|
|
1071
|
+
{ label: 'CHANGE', value: (item) => item.changeType },
|
|
1072
|
+
{ label: 'NOTE', value: (item) => item.changeReason },
|
|
1073
|
+
{ label: 'CURRENT', value: (item) => item.isCurrent },
|
|
1074
|
+
],
|
|
1075
|
+
options,
|
|
1076
|
+
output,
|
|
1077
|
+
page,
|
|
1078
|
+
});
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (command === 'webhooks' && action === 'diff') {
|
|
1083
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
1084
|
+
requireShape(positional.length === 2);
|
|
1085
|
+
const result = await client.webhooks.getConfigurationVersion(positional[0], positional[1]);
|
|
1086
|
+
if (options.json) {
|
|
1087
|
+
writeValue(output, result);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const previousLabel = result.version.previous
|
|
1091
|
+
? `webhook v${String(result.version.previous.revisionNumber)}`
|
|
1092
|
+
: 'empty webhook configuration';
|
|
1093
|
+
output.write(
|
|
1094
|
+
renderLineDiff(
|
|
1095
|
+
`${JSON.stringify(result.version.previous?.snapshot ?? {}, null, 2)}\n`,
|
|
1096
|
+
`${JSON.stringify(result.version.snapshot, null, 2)}\n`,
|
|
1097
|
+
previousLabel,
|
|
1098
|
+
`webhook v${String(result.version.revisionNumber)}`,
|
|
1099
|
+
),
|
|
1100
|
+
);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (command === 'webhooks' && action === 'restore') {
|
|
1105
|
+
const { options, positional } = parseCommandArguments(
|
|
1106
|
+
arguments_.slice(2),
|
|
1107
|
+
new Set(['json', 'reason']),
|
|
1108
|
+
);
|
|
1109
|
+
requireShape(positional.length === 2);
|
|
1110
|
+
const history = await client.webhooks.listConfigurationVersions(positional[0], { limit: 1 });
|
|
1111
|
+
const current = history.items[0];
|
|
1112
|
+
if (!current?.isCurrent) {
|
|
1113
|
+
throw new Error('The current webhook configuration could not be read.');
|
|
1114
|
+
}
|
|
1115
|
+
const result = await client.webhooks.restoreConfigurationVersion(positional[0], positional[1], {
|
|
1116
|
+
expectedVersion: current.revisionNumber,
|
|
1117
|
+
reason: options.reason,
|
|
1118
|
+
});
|
|
1119
|
+
if (options.json) writeValue(output, result);
|
|
1120
|
+
else
|
|
1121
|
+
output.write(
|
|
1122
|
+
`Restored ${positional[1]} as webhook configuration v${String(result.configurationVersion)} (${result.versionUid}).\n`,
|
|
1123
|
+
);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
247
1127
|
if (command === 'clients' && action === 'list') {
|
|
248
1128
|
const { options, positional } = parseCommandArguments(
|
|
249
1129
|
arguments_.slice(2),
|
|
@@ -413,10 +1293,142 @@ const runCommand = async ({ arguments_, client, output }) => {
|
|
|
413
1293
|
return;
|
|
414
1294
|
}
|
|
415
1295
|
|
|
1296
|
+
if (command === 'hooks' && action === 'save') {
|
|
1297
|
+
const { options, positional } = parseCommandArguments(
|
|
1298
|
+
arguments_.slice(2),
|
|
1299
|
+
new Set(['configJson', 'filePath', 'json', 'reason']),
|
|
1300
|
+
);
|
|
1301
|
+
requireShape(positional.length === 2);
|
|
1302
|
+
const { hook } = await client.hooks.get(positional[0], positional[1]);
|
|
1303
|
+
const source = options.filePath ? await readFile(options.filePath, 'utf8') : hook.draftSource;
|
|
1304
|
+
const result = await client.hooks.saveDraft(positional[0], positional[1], {
|
|
1305
|
+
config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
|
|
1306
|
+
expectedVersion: hook.version,
|
|
1307
|
+
reason: options.reason,
|
|
1308
|
+
source,
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
if (options.json) writeValue(output, result);
|
|
1312
|
+
else if (result.draftVersion.unchanged)
|
|
1313
|
+
output.write(
|
|
1314
|
+
`No changes; draft v${String(result.draftVersion.revisionNumber)} is current.\n`,
|
|
1315
|
+
);
|
|
1316
|
+
else
|
|
1317
|
+
output.write(
|
|
1318
|
+
`Saved draft v${String(result.draftVersion.revisionNumber)} (${result.draftVersion.uid}).\n`,
|
|
1319
|
+
);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (command === 'hooks' && action === 'history') {
|
|
1324
|
+
const { options, positional } = parseCommandArguments(
|
|
1325
|
+
arguments_.slice(2),
|
|
1326
|
+
new Set(['cursor', 'json', 'limit']),
|
|
1327
|
+
);
|
|
1328
|
+
requireShape(positional.length === 2);
|
|
1329
|
+
const page = await client.hooks.listDraftVersions(
|
|
1330
|
+
positional[0],
|
|
1331
|
+
positional[1],
|
|
1332
|
+
pageQuery(options),
|
|
1333
|
+
);
|
|
1334
|
+
writePage({
|
|
1335
|
+
columns: [
|
|
1336
|
+
{ label: 'VERSION', value: (item) => item.revisionNumber },
|
|
1337
|
+
{ label: 'UID', value: (item) => item.uid },
|
|
1338
|
+
{ label: 'WHEN', value: (item) => item.createdAt },
|
|
1339
|
+
{ label: 'ACTOR', value: (item) => `${item.actor.name} (${item.actor.type})` },
|
|
1340
|
+
{ label: 'CHANGE', value: (item) => item.changeType },
|
|
1341
|
+
{ label: 'NOTE', value: (item) => item.changeReason },
|
|
1342
|
+
{ label: 'CURRENT', value: (item) => item.isCurrent },
|
|
1343
|
+
],
|
|
1344
|
+
options,
|
|
1345
|
+
output,
|
|
1346
|
+
page,
|
|
1347
|
+
});
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (command === 'hooks' && action === 'diff') {
|
|
1352
|
+
const { options, positional } = parseCommandArguments(arguments_.slice(2), new Set(['json']));
|
|
1353
|
+
requireShape(positional.length === 3);
|
|
1354
|
+
const result = await client.hooks.getDraftVersion(positional[0], positional[1], positional[2]);
|
|
1355
|
+
|
|
1356
|
+
if (options.json) {
|
|
1357
|
+
writeValue(output, result);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
const previousLabel = result.version.previous
|
|
1361
|
+
? `draft v${String(result.version.previous.revisionNumber)}`
|
|
1362
|
+
: 'empty draft';
|
|
1363
|
+
const currentLabel = `draft v${String(result.version.revisionNumber)}`;
|
|
1364
|
+
output.write(
|
|
1365
|
+
renderLineDiff(
|
|
1366
|
+
result.version.previous?.source ?? '',
|
|
1367
|
+
result.version.source,
|
|
1368
|
+
`${previousLabel} source`,
|
|
1369
|
+
`${currentLabel} source`,
|
|
1370
|
+
),
|
|
1371
|
+
);
|
|
1372
|
+
output.write(
|
|
1373
|
+
renderLineDiff(
|
|
1374
|
+
`${JSON.stringify(result.version.previous?.config ?? {}, null, 2)}\n`,
|
|
1375
|
+
`${JSON.stringify(result.version.config, null, 2)}\n`,
|
|
1376
|
+
`${previousLabel} config`,
|
|
1377
|
+
`${currentLabel} config`,
|
|
1378
|
+
),
|
|
1379
|
+
);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (command === 'hooks' && action === 'restore') {
|
|
1384
|
+
const { options, positional } = parseCommandArguments(
|
|
1385
|
+
arguments_.slice(2),
|
|
1386
|
+
new Set(['json', 'reason']),
|
|
1387
|
+
);
|
|
1388
|
+
requireShape(positional.length === 3);
|
|
1389
|
+
const { hook } = await client.hooks.get(positional[0], positional[1]);
|
|
1390
|
+
const result = await client.hooks.restoreDraft(positional[0], positional[1], positional[2], {
|
|
1391
|
+
expectedVersion: hook.version,
|
|
1392
|
+
reason: options.reason,
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
if (options.json) writeValue(output, result);
|
|
1396
|
+
else
|
|
1397
|
+
output.write(
|
|
1398
|
+
`Restored ${positional[2]} as draft v${String(result.draftVersion.revisionNumber)} (${result.draftVersion.uid}).\n`,
|
|
1399
|
+
);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (command === 'hooks' && action === 'reset') {
|
|
1404
|
+
const { options, positional } = parseCommandArguments(
|
|
1405
|
+
arguments_.slice(2),
|
|
1406
|
+
new Set(['confirmationHookUid', 'reason']),
|
|
1407
|
+
);
|
|
1408
|
+
requireShape(
|
|
1409
|
+
positional.length === 2 &&
|
|
1410
|
+
typeof options.confirmationHookUid === 'string' &&
|
|
1411
|
+
typeof options.reason === 'string',
|
|
1412
|
+
);
|
|
1413
|
+
const separatorIndex = options.confirmationHookUid.lastIndexOf('@');
|
|
1414
|
+
const hookUid = options.confirmationHookUid.slice(0, separatorIndex);
|
|
1415
|
+
const expectedVersion = Number(options.confirmationHookUid.slice(separatorIndex + 1));
|
|
1416
|
+
if (separatorIndex <= 0 || !Number.isInteger(expectedVersion) || expectedVersion <= 0) {
|
|
1417
|
+
throw new Error('--confirm must be the current hook UID and version: <hook-uid>@<version>.');
|
|
1418
|
+
}
|
|
1419
|
+
await client.hooks.reset(positional[0], positional[1], {
|
|
1420
|
+
confirmationHookUid: hookUid,
|
|
1421
|
+
expectedVersion,
|
|
1422
|
+
reason: options.reason,
|
|
1423
|
+
});
|
|
1424
|
+
output.write(`Permanently reset hook ${hookUid} at version ${String(expectedVersion)}.\n`);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
416
1428
|
if (command === 'hooks' && action === 'deploy') {
|
|
417
1429
|
const { options, positional } = parseCommandArguments(
|
|
418
1430
|
arguments_.slice(2),
|
|
419
|
-
new Set(['configJson', 'filePath', 'json', 'shadow']),
|
|
1431
|
+
new Set(['configJson', 'filePath', 'json', 'reason', 'shadow']),
|
|
420
1432
|
);
|
|
421
1433
|
requireShape(positional.length === 2);
|
|
422
1434
|
const connectionUid = positional[0];
|
|
@@ -428,6 +1440,7 @@ const runCommand = async ({ arguments_, client, output }) => {
|
|
|
428
1440
|
const saved = await client.hooks.saveDraft(connectionUid, toolUid, {
|
|
429
1441
|
config: parseJsonOption(options.configJson, '--config', hook.draftConfig),
|
|
430
1442
|
expectedVersion: hook.version,
|
|
1443
|
+
reason: options.reason,
|
|
431
1444
|
source,
|
|
432
1445
|
});
|
|
433
1446
|
hook = saved.hook;
|
|
@@ -538,10 +1551,11 @@ export const runManagementCommand = async (
|
|
|
538
1551
|
} catch (error) {
|
|
539
1552
|
if (!(error instanceof RayrunApiError)) throw error;
|
|
540
1553
|
const requestId = error.requestId ? `, request ${error.requestId}` : '';
|
|
1554
|
+
const diagnosticTarget = error.code === 'invalid_source' ? 'source' : 'hook.ts';
|
|
541
1555
|
const diagnostics = error.diagnostics
|
|
542
1556
|
.map(
|
|
543
1557
|
(entry) =>
|
|
544
|
-
|
|
1558
|
+
`${diagnosticTarget}${entry.line === undefined ? '' : `:${String(entry.line)}${entry.column === undefined ? '' : `:${String(entry.column)}`}`}: ${entry.message}`,
|
|
545
1559
|
)
|
|
546
1560
|
.join('\n');
|
|
547
1561
|
throw new Error(
|