rubrkit 0.1.0 → 0.3.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 +417 -126
- package/package.json +28 -28
- package/src/api.js +108 -101
- package/src/args.js +209 -175
- package/src/cli.js +97 -93
- package/src/config.js +186 -169
- package/src/formats.js +239 -222
- package/src/pull.js +682 -676
- package/src/sdk.js +451 -443
- package/src/testingCli.js +504 -431
package/src/testingCli.js
CHANGED
|
@@ -1,431 +1,504 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
import { authError, exitCodeForApiFailure, EXIT_CODES, RubrkitCliError, usageError } from './errors.js';
|
|
7
|
-
import { scoreFromPayload, writeFormattedResult } from './formats.js';
|
|
8
|
-
import { collectArtifactFilesForUpload, runLocalChecks } from './localChecks.js';
|
|
9
|
-
import { Rubrkit, RubrkitApiError, RubrkitError } from './sdk.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @param {{
|
|
13
|
-
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
14
|
-
* stdout?: NodeJS.WritableStream,
|
|
15
|
-
* stderr?: NodeJS.WritableStream,
|
|
16
|
-
* fetchImpl?: typeof fetch,
|
|
17
|
-
* }} params
|
|
18
|
-
*/
|
|
19
|
-
export async function runTestingCommand({
|
|
20
|
-
config,
|
|
21
|
-
stdout = process.stdout,
|
|
22
|
-
stderr = process.stderr,
|
|
23
|
-
fetchImpl = globalThis.fetch,
|
|
24
|
-
}) {
|
|
25
|
-
if (config.command === 'validate') {
|
|
26
|
-
return runLocalCommand({ config, stdout });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (config.command === 'test' && !config.remote) {
|
|
30
|
-
return runLocalCommand({ config, stdout });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (config.command === 'report') {
|
|
34
|
-
return runReportCommand({ config, stdout, fetchImpl });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (['test', 'audit', 'eval'].includes(config.command)) {
|
|
38
|
-
return runRemoteCommand({ config, stdout, stderr, fetchImpl });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
throw usageError(`Unsupported command "${config.command}".`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {{ config: Awaited<ReturnType<import('./config.js').resolveConfig>>, stdout: NodeJS.WritableStream }} params
|
|
46
|
-
*/
|
|
47
|
-
async function runLocalCommand({ config, stdout }) {
|
|
48
|
-
const payload = await runLocalChecks({
|
|
49
|
-
target: config.selector,
|
|
50
|
-
cwd: config.destination,
|
|
51
|
-
command: config.command,
|
|
52
|
-
failUnder: config.failUnder,
|
|
53
|
-
failOn: config.failOn,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
await writeFormattedResult({
|
|
57
|
-
payload,
|
|
58
|
-
format: config.format,
|
|
59
|
-
output: config.output,
|
|
60
|
-
stdout,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return payload.passed ? 0 : EXIT_CODES.checksFailed;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @param {{
|
|
68
|
-
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
69
|
-
* stdout: NodeJS.WritableStream,
|
|
70
|
-
* stderr: NodeJS.WritableStream,
|
|
71
|
-
* fetchImpl: typeof fetch
|
|
72
|
-
* }} params
|
|
73
|
-
*/
|
|
74
|
-
async function runRemoteCommand({ config, stdout, stderr, fetchImpl }) {
|
|
75
|
-
const dryRun = createRemoteDryRunPayload(config);
|
|
76
|
-
|
|
77
|
-
if (config.dryRun) {
|
|
78
|
-
await writeFormattedResult({
|
|
79
|
-
payload: dryRun,
|
|
80
|
-
format: config.format,
|
|
81
|
-
output: config.output,
|
|
82
|
-
stdout,
|
|
83
|
-
});
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!config.apiKey) {
|
|
88
|
-
throw authError('Missing Rubrkit API key. Set RUBRKIT_API_KEY or pass --api-key for remote checks.');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const client = new Rubrkit({
|
|
92
|
-
apiKey: config.apiKey,
|
|
93
|
-
apiUrl: config.apiUrl,
|
|
94
|
-
fetchImpl,
|
|
95
|
-
userAgent: 'rubrkit-cli/0.0.0-local',
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const started = await startRemoteRun({ client, config });
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* @param {
|
|
395
|
-
*/
|
|
396
|
-
function
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* @param {unknown}
|
|
412
|
-
*/
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { authError, exitCodeForApiFailure, EXIT_CODES, RubrkitCliError, usageError } from './errors.js';
|
|
7
|
+
import { scoreFromPayload, writeFormattedResult } from './formats.js';
|
|
8
|
+
import { collectArtifactFilesForUpload, runLocalChecks } from './localChecks.js';
|
|
9
|
+
import { Rubrkit, RubrkitApiError, RubrkitError } from './sdk.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{
|
|
13
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
14
|
+
* stdout?: NodeJS.WritableStream,
|
|
15
|
+
* stderr?: NodeJS.WritableStream,
|
|
16
|
+
* fetchImpl?: typeof fetch,
|
|
17
|
+
* }} params
|
|
18
|
+
*/
|
|
19
|
+
export async function runTestingCommand({
|
|
20
|
+
config,
|
|
21
|
+
stdout = process.stdout,
|
|
22
|
+
stderr = process.stderr,
|
|
23
|
+
fetchImpl = globalThis.fetch,
|
|
24
|
+
}) {
|
|
25
|
+
if (config.command === 'validate') {
|
|
26
|
+
return runLocalCommand({ config, stdout });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.command === 'test' && !config.remote) {
|
|
30
|
+
return runLocalCommand({ config, stdout });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (config.command === 'report') {
|
|
34
|
+
return runReportCommand({ config, stdout, fetchImpl });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (['test', 'audit', 'eval', 'audit-apply'].includes(config.command)) {
|
|
38
|
+
return runRemoteCommand({ config, stdout, stderr, fetchImpl });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw usageError(`Unsupported command "${config.command}".`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {{ config: Awaited<ReturnType<import('./config.js').resolveConfig>>, stdout: NodeJS.WritableStream }} params
|
|
46
|
+
*/
|
|
47
|
+
async function runLocalCommand({ config, stdout }) {
|
|
48
|
+
const payload = await runLocalChecks({
|
|
49
|
+
target: config.selector,
|
|
50
|
+
cwd: config.destination,
|
|
51
|
+
command: config.command,
|
|
52
|
+
failUnder: config.failUnder,
|
|
53
|
+
failOn: config.failOn,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await writeFormattedResult({
|
|
57
|
+
payload,
|
|
58
|
+
format: config.format,
|
|
59
|
+
output: config.output,
|
|
60
|
+
stdout,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return payload.passed ? 0 : EXIT_CODES.checksFailed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {{
|
|
68
|
+
* config: Awaited<ReturnType<import('./config.js').resolveConfig>>,
|
|
69
|
+
* stdout: NodeJS.WritableStream,
|
|
70
|
+
* stderr: NodeJS.WritableStream,
|
|
71
|
+
* fetchImpl: typeof fetch
|
|
72
|
+
* }} params
|
|
73
|
+
*/
|
|
74
|
+
async function runRemoteCommand({ config, stdout, stderr, fetchImpl }) {
|
|
75
|
+
const dryRun = createRemoteDryRunPayload(config);
|
|
76
|
+
|
|
77
|
+
if (config.dryRun) {
|
|
78
|
+
await writeFormattedResult({
|
|
79
|
+
payload: dryRun,
|
|
80
|
+
format: config.format,
|
|
81
|
+
output: config.output,
|
|
82
|
+
stdout,
|
|
83
|
+
});
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!config.apiKey) {
|
|
88
|
+
throw authError('Missing Rubrkit API key. Set RUBRKIT_API_KEY or pass --api-key for remote checks.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const client = new Rubrkit({
|
|
92
|
+
apiKey: config.apiKey,
|
|
93
|
+
apiUrl: config.apiUrl,
|
|
94
|
+
fetchImpl,
|
|
95
|
+
userAgent: 'rubrkit-cli/0.0.0-local',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const started = await startRemoteRun({ client, config });
|
|
100
|
+
|
|
101
|
+
// audit-apply hits a SYNCHRONOUS endpoint that returns { appliedCount, files }
|
|
102
|
+
// immediately (200, no job). Render the applied summary and exit; never poll.
|
|
103
|
+
if (config.command === 'audit-apply') {
|
|
104
|
+
const apply = started && typeof started === 'object' ? /** @type {Record<string, any>} */ (started) : {};
|
|
105
|
+
const files = Array.isArray(apply.files) ? apply.files : [];
|
|
106
|
+
const appliedCount = typeof apply.appliedCount === 'number' ? apply.appliedCount : files.length;
|
|
107
|
+
const payload = {
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
command: config.command,
|
|
110
|
+
mode: 'remote-apply',
|
|
111
|
+
target: config.selector ?? config.artifactBundle ?? null,
|
|
112
|
+
generatedAt: new Date().toISOString(),
|
|
113
|
+
appliedCount,
|
|
114
|
+
files,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
await writeFormattedResult({
|
|
118
|
+
payload,
|
|
119
|
+
format: config.format,
|
|
120
|
+
output: config.output,
|
|
121
|
+
stdout,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A cache hit returns the stored audit result immediately (200, no job).
|
|
128
|
+
// Short-circuit polling and render the persisted run directly.
|
|
129
|
+
if (started && started.cached) {
|
|
130
|
+
const auditRun = started.auditRun ?? {};
|
|
131
|
+
const job = {
|
|
132
|
+
id: started.jobId ?? auditRun.jobId ?? null,
|
|
133
|
+
state: auditRun.status ?? 'succeeded',
|
|
134
|
+
result: auditRun.result,
|
|
135
|
+
};
|
|
136
|
+
const payload = {
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
command: config.command,
|
|
139
|
+
mode: 'remote',
|
|
140
|
+
cached: true,
|
|
141
|
+
target: config.selector ?? config.artifactBundle ?? null,
|
|
142
|
+
generatedAt: new Date().toISOString(),
|
|
143
|
+
started,
|
|
144
|
+
job,
|
|
145
|
+
jobId: job.id,
|
|
146
|
+
gates: evaluateRemoteGates({ config, started, job }),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
await writeFormattedResult({
|
|
150
|
+
payload,
|
|
151
|
+
format: config.format,
|
|
152
|
+
output: config.output,
|
|
153
|
+
stdout,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return exitCodeForRemotePayload(payload);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const jobId = getJobId(started);
|
|
160
|
+
|
|
161
|
+
if (!jobId) {
|
|
162
|
+
throw new RubrkitCliError('Rubrkit API response did not include a jobId.', {
|
|
163
|
+
code: 'missing_job_id',
|
|
164
|
+
exitCode: EXIT_CODES.network,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const job = await client.jobs.wait(jobId, {
|
|
169
|
+
intervalMs: 1000,
|
|
170
|
+
onProgress: config.format === 'json' || config.ci ? undefined : jobProgress(stderr),
|
|
171
|
+
});
|
|
172
|
+
const payload = {
|
|
173
|
+
schemaVersion: 1,
|
|
174
|
+
command: config.command,
|
|
175
|
+
mode: 'remote',
|
|
176
|
+
cached: false,
|
|
177
|
+
target: config.selector ?? config.artifactBundle ?? null,
|
|
178
|
+
generatedAt: new Date().toISOString(),
|
|
179
|
+
started,
|
|
180
|
+
job,
|
|
181
|
+
jobId,
|
|
182
|
+
gates: evaluateRemoteGates({ config, started, job }),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
await writeFormattedResult({
|
|
186
|
+
payload,
|
|
187
|
+
format: config.format,
|
|
188
|
+
output: config.output,
|
|
189
|
+
stdout,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return exitCodeForRemotePayload(payload);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw mapSdkError(error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {{ config: Awaited<ReturnType<import('./config.js').resolveConfig>>, stdout: NodeJS.WritableStream, fetchImpl: typeof fetch }} params
|
|
200
|
+
*/
|
|
201
|
+
async function runReportCommand({ config, stdout, fetchImpl }) {
|
|
202
|
+
if (!config.selector) {
|
|
203
|
+
throw usageError('The report command needs a job ID or run ID.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!config.apiKey) {
|
|
207
|
+
throw authError('Missing Rubrkit API key. Set RUBRKIT_API_KEY or pass --api-key for reports.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const client = new Rubrkit({
|
|
211
|
+
apiKey: config.apiKey,
|
|
212
|
+
apiUrl: config.apiUrl,
|
|
213
|
+
fetchImpl,
|
|
214
|
+
userAgent: 'rubrkit-cli/0.0.0-local',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const data = await client.jobs.get(config.selector);
|
|
219
|
+
const payload = {
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
command: config.command,
|
|
222
|
+
mode: 'report',
|
|
223
|
+
target: config.selector,
|
|
224
|
+
generatedAt: new Date().toISOString(),
|
|
225
|
+
...data,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await writeFormattedResult({
|
|
229
|
+
payload,
|
|
230
|
+
format: config.format,
|
|
231
|
+
output: config.output,
|
|
232
|
+
stdout,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return 0;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw mapSdkError(error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {{ client: Rubrkit, config: Awaited<ReturnType<import('./config.js').resolveConfig>> }} params
|
|
243
|
+
*/
|
|
244
|
+
async function startRemoteRun({ client, config }) {
|
|
245
|
+
const body = remoteBody(config);
|
|
246
|
+
|
|
247
|
+
if (config.command === 'test') {
|
|
248
|
+
return startRemoteTestRun({ client, config, body });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (config.command === 'audit') {
|
|
252
|
+
return client.audits.run({
|
|
253
|
+
artifactBundleId: getArtifactBundleSelector(config),
|
|
254
|
+
...body,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (config.command === 'eval') {
|
|
259
|
+
return client.evals.run({
|
|
260
|
+
artifactBundleId: getArtifactBundleSelector(config),
|
|
261
|
+
...body,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (config.command === 'audit-apply') {
|
|
266
|
+
if (!config.auditRunId) {
|
|
267
|
+
throw usageError('The audit-apply command needs an audit run ID. Pass --audit-run-id <id>.');
|
|
268
|
+
}
|
|
269
|
+
return client.audits.apply({
|
|
270
|
+
artifactBundleId: getArtifactBundleSelector(config),
|
|
271
|
+
auditRunId: config.auditRunId,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw usageError(`Unsupported remote command "${config.command}".`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {{ client: Rubrkit, config: Awaited<ReturnType<import('./config.js').resolveConfig>>, body: Record<string, unknown> }} params
|
|
280
|
+
*/
|
|
281
|
+
async function startRemoteTestRun({ client, config, body }) {
|
|
282
|
+
if (await shouldUploadLocalTarget(config)) {
|
|
283
|
+
const files = await collectArtifactFilesForUpload({
|
|
284
|
+
target: /** @type {string} */ (config.selector),
|
|
285
|
+
cwd: config.destination,
|
|
286
|
+
message: 'Uploaded by rubrkit test --remote.',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return client.artifacts.test({
|
|
290
|
+
...body,
|
|
291
|
+
artifactBundleId: config.artifactBundle ?? undefined,
|
|
292
|
+
name: remoteArtifactBundleName(config.selector),
|
|
293
|
+
description: 'Created by rubrkit test --remote from local project files.',
|
|
294
|
+
files,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return client.artifacts.test({
|
|
299
|
+
...body,
|
|
300
|
+
artifactBundleId: getArtifactBundleSelector(config),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {Awaited<ReturnType<import('./config.js').resolveConfig>>} config
|
|
306
|
+
*/
|
|
307
|
+
function remoteBody(config) {
|
|
308
|
+
/** @type {Record<string, unknown>} */
|
|
309
|
+
const body = {};
|
|
310
|
+
|
|
311
|
+
if (config.selector) body.target = config.selector;
|
|
312
|
+
if (config.artifactBundle) body.artifactBundleId = config.artifactBundle;
|
|
313
|
+
if (config.artifact) body.artifact = config.artifact;
|
|
314
|
+
if (config.rubric) body.rubric = config.rubric;
|
|
315
|
+
if (config.noAi) body.noAi = true;
|
|
316
|
+
if (config.ci) body.ci = true;
|
|
317
|
+
if (config.noCache) body.force = true;
|
|
318
|
+
|
|
319
|
+
return body;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @param {Awaited<ReturnType<import('./config.js').resolveConfig>>} config
|
|
324
|
+
*/
|
|
325
|
+
function getArtifactBundleSelector(config) {
|
|
326
|
+
const selector = config.artifactBundle ?? config.selector;
|
|
327
|
+
|
|
328
|
+
if (!selector) {
|
|
329
|
+
throw usageError(`The ${config.command} command needs an artifact bundle ID or selector for remote runs.`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return selector;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @param {Awaited<ReturnType<import('./config.js').resolveConfig>>} config
|
|
337
|
+
*/
|
|
338
|
+
function createRemoteDryRunPayload(config) {
|
|
339
|
+
const artifactBundleId = config.command === 'test' ? (config.artifactBundle ?? '{artifactBundleId}') : (config.artifactBundle ?? config.selector ?? '{artifactBundleId}');
|
|
340
|
+
const endpoint =
|
|
341
|
+
config.command === 'test'
|
|
342
|
+
? `/artifact-bundles/${artifactBundleId}/audits`
|
|
343
|
+
: config.command === 'audit'
|
|
344
|
+
? `/artifact-bundles/${artifactBundleId}/audits`
|
|
345
|
+
: config.command === 'eval'
|
|
346
|
+
? `/artifact-bundles/${artifactBundleId}/evals`
|
|
347
|
+
: config.command === 'audit-apply'
|
|
348
|
+
? `/artifact-bundles/${artifactBundleId}/audits/${config.auditRunId ?? '{auditRunId}'}/apply`
|
|
349
|
+
: `/jobs/${config.selector ?? '{jobId}'}`;
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
schemaVersion: 1,
|
|
353
|
+
command: config.command,
|
|
354
|
+
mode: 'remote-dry-run',
|
|
355
|
+
target: config.selector ?? config.artifactBundle ?? null,
|
|
356
|
+
generatedAt: new Date().toISOString(),
|
|
357
|
+
method: config.command === 'report' ? 'GET' : 'POST',
|
|
358
|
+
endpoint,
|
|
359
|
+
composes: config.command === 'test'
|
|
360
|
+
? [
|
|
361
|
+
'POST /api/v1/artifact-bundles when testing local files without --artifact-bundle',
|
|
362
|
+
'POST /api/v1/artifact-bundles/{artifactBundleId}/files when testing local files',
|
|
363
|
+
'POST /api/v1/artifact-bundles/{artifactBundleId}/audits',
|
|
364
|
+
'GET /api/v1/jobs/{jobId}',
|
|
365
|
+
]
|
|
366
|
+
: undefined,
|
|
367
|
+
pollsJob: config.command !== 'report',
|
|
368
|
+
requiresApiKey: true,
|
|
369
|
+
request: redactRequest(remoteBody(config)),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {Awaited<ReturnType<import('./config.js').resolveConfig>>} config
|
|
375
|
+
*/
|
|
376
|
+
async function shouldUploadLocalTarget(config) {
|
|
377
|
+
if (config.command !== 'test' || !config.selector) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (hasGlob(config.selector)) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const stats = await fs.stat(path.resolve(config.destination, config.selector));
|
|
387
|
+
return stats.isFile() || stats.isDirectory();
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {string | null} selector
|
|
395
|
+
*/
|
|
396
|
+
function remoteArtifactBundleName(selector) {
|
|
397
|
+
const basename = selector ? path.basename(selector) || selector : 'artifact';
|
|
398
|
+
const name = `CLI remote test: ${basename}`;
|
|
399
|
+
|
|
400
|
+
return name.length <= 120 ? name : `${name.slice(0, 117)}...`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* @param {string} value
|
|
405
|
+
*/
|
|
406
|
+
function hasGlob(value) {
|
|
407
|
+
return /[*?[\]]/.test(value);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @param {Record<string, unknown>} request
|
|
412
|
+
*/
|
|
413
|
+
function redactRequest(request) {
|
|
414
|
+
const redacted = { ...request };
|
|
415
|
+
delete redacted.apiKey;
|
|
416
|
+
delete redacted['api-key'];
|
|
417
|
+
return redacted;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @param {unknown} started
|
|
422
|
+
*/
|
|
423
|
+
function getJobId(started) {
|
|
424
|
+
if (!started || typeof started !== 'object') {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const record = /** @type {Record<string, any>} */ (started);
|
|
429
|
+
return record.jobId ?? record.job?.id ?? record.data?.jobId ?? record.data?.job?.id ?? null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @param {NodeJS.WritableStream} stderr
|
|
434
|
+
*/
|
|
435
|
+
function jobProgress(stderr) {
|
|
436
|
+
return (job) => {
|
|
437
|
+
const percent = Number.isFinite(Number(job.percent)) ? `${Number(job.percent)}%` : '';
|
|
438
|
+
stderr.write(`rubrkit: ${[job.state, job.phase, percent, job.message].filter(Boolean).join(' - ')}\n`);
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @param {{ config: Awaited<ReturnType<import('./config.js').resolveConfig>>, started: Record<string, any>, job: Record<string, any> }} params
|
|
444
|
+
*/
|
|
445
|
+
function evaluateRemoteGates({ config, started, job }) {
|
|
446
|
+
const payload = { started, job };
|
|
447
|
+
const score = scoreFromPayload(payload);
|
|
448
|
+
const failures = [];
|
|
449
|
+
|
|
450
|
+
if (typeof config.failUnder === 'number' && typeof score === 'number' && score < config.failUnder) {
|
|
451
|
+
failures.push({
|
|
452
|
+
code: 'score_below_threshold',
|
|
453
|
+
message: `Score ${score} is below --fail-under ${config.failUnder}.`,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
passed: failures.length === 0,
|
|
459
|
+
score,
|
|
460
|
+
failUnder: config.failUnder,
|
|
461
|
+
failOn: config.failOn,
|
|
462
|
+
failures,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* @param {Record<string, any>} payload
|
|
468
|
+
*/
|
|
469
|
+
function exitCodeForRemotePayload(payload) {
|
|
470
|
+
const job = payload.job ?? {};
|
|
471
|
+
|
|
472
|
+
if (['failed', 'cancelled', 'paused'].includes(job.state)) {
|
|
473
|
+
return exitCodeForApiFailure(job.error?.code, job.error?.code ? 400 : 500);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (payload.gates && payload.gates.passed === false) {
|
|
477
|
+
return EXIT_CODES.checksFailed;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* @param {unknown} error
|
|
485
|
+
*/
|
|
486
|
+
function mapSdkError(error) {
|
|
487
|
+
if (error instanceof RubrkitApiError) {
|
|
488
|
+
return new RubrkitCliError(error.message, {
|
|
489
|
+
code: error.code,
|
|
490
|
+
exitCode: exitCodeForApiFailure(error.code, error.status),
|
|
491
|
+
details: error.details,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (error instanceof RubrkitError) {
|
|
496
|
+
return new RubrkitCliError(error.message, {
|
|
497
|
+
code: error.code,
|
|
498
|
+
exitCode: error.code === 'missing_sdk_parameter' ? EXIT_CODES.usage : EXIT_CODES.network,
|
|
499
|
+
details: error.details,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return error;
|
|
504
|
+
}
|