rubrkit 0.1.1 → 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 +417 -404
- 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 +556 -443
- package/src/testingCli.js +504 -431
package/src/sdk.js
CHANGED
|
@@ -1,443 +1,556 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
* }}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export class
|
|
51
|
-
/**
|
|
52
|
-
* @param {string} message
|
|
53
|
-
* @param {{
|
|
54
|
-
*/
|
|
55
|
-
constructor(message, {
|
|
56
|
-
super(message
|
|
57
|
-
this.name = '
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { DEFAULT_API_URL, normalizeApiUrl } from './config.js';
|
|
5
|
+
|
|
6
|
+
const TERMINAL_JOB_STATES = new Set(['succeeded', 'failed', 'cancelled', 'paused']);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Content hash matching the server's `computeContentHash` (sha256 of the UTF-8
|
|
10
|
+
* content bytes, prefixed `sha256:`). Used to detect which files changed.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} content
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function contentHashOf(content) {
|
|
16
|
+
return `sha256:${createHash('sha256').update(Buffer.from(String(content), 'utf8')).digest('hex')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracts the file array from a list-files API response, tolerant of the
|
|
21
|
+
* `{ data: { files } }` / `{ files } / [...]` envelope shapes.
|
|
22
|
+
*
|
|
23
|
+
* @param {unknown} listed
|
|
24
|
+
* @returns {Array<{ id: string, path: string, contentHash?: string }>}
|
|
25
|
+
*/
|
|
26
|
+
function extractBundleFiles(listed) {
|
|
27
|
+
const root = /** @type {any} */ (listed) || {};
|
|
28
|
+
const data = root.data ?? root.result ?? root;
|
|
29
|
+
const files = data?.files ?? data?.artifactFiles ?? (Array.isArray(data) ? data : []);
|
|
30
|
+
return Array.isArray(files) ? files : [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* apiKey?: string | null,
|
|
36
|
+
* apiUrl?: string,
|
|
37
|
+
* fetchImpl?: typeof fetch,
|
|
38
|
+
* userAgent?: string,
|
|
39
|
+
* retryReads?: number
|
|
40
|
+
* }} RubrkitOptions
|
|
41
|
+
*
|
|
42
|
+
* @typedef {{
|
|
43
|
+
* intervalMs?: number,
|
|
44
|
+
* timeoutMs?: number,
|
|
45
|
+
* signal?: AbortSignal,
|
|
46
|
+
* onProgress?: (job: Record<string, any>) => void
|
|
47
|
+
* }} JobWaitOptions
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
export class RubrkitError extends Error {
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} message
|
|
53
|
+
* @param {{ code?: string, details?: unknown }} [options]
|
|
54
|
+
*/
|
|
55
|
+
constructor(message, { code = 'rubrkit_error', details = undefined } = {}) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'RubrkitError';
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.details = details;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class RubrkitApiError extends RubrkitError {
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} message
|
|
66
|
+
* @param {{ code?: string, status?: number, requestId?: string | null, details?: unknown }} [options]
|
|
67
|
+
*/
|
|
68
|
+
constructor(message, { code = 'api_request_failed', status = 0, requestId = null, details = undefined } = {}) {
|
|
69
|
+
super(message, { code, details });
|
|
70
|
+
this.name = 'RubrkitApiError';
|
|
71
|
+
this.status = status;
|
|
72
|
+
this.requestId = requestId;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class RubrkitNetworkError extends RubrkitError {
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} message
|
|
79
|
+
* @param {{ cause?: unknown }} [options]
|
|
80
|
+
*/
|
|
81
|
+
constructor(message, { cause = undefined } = {}) {
|
|
82
|
+
super(message, { code: 'network_error', details: cause });
|
|
83
|
+
this.name = 'RubrkitNetworkError';
|
|
84
|
+
this.cause = cause;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class Rubrkit {
|
|
89
|
+
/**
|
|
90
|
+
* @param {RubrkitOptions} [options]
|
|
91
|
+
*/
|
|
92
|
+
constructor({
|
|
93
|
+
apiKey = process.env.RUBRKIT_API_KEY ?? null,
|
|
94
|
+
apiUrl = process.env.RUBRKIT_API_URL ?? DEFAULT_API_URL,
|
|
95
|
+
fetchImpl = globalThis.fetch,
|
|
96
|
+
userAgent = 'rubrkit-sdk/0.0.0-local',
|
|
97
|
+
retryReads = 0,
|
|
98
|
+
} = {}) {
|
|
99
|
+
this.apiKey = apiKey;
|
|
100
|
+
this.apiUrl = normalizeApiUrl(apiUrl);
|
|
101
|
+
this.fetchImpl = fetchImpl;
|
|
102
|
+
this.userAgent = userAgent;
|
|
103
|
+
this.retryReads = Math.max(0, Number(retryReads) || 0);
|
|
104
|
+
|
|
105
|
+
this.me = {
|
|
106
|
+
get: () => this.request('/me'),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.artifactBundles = {
|
|
110
|
+
list: (params = {}) => this.request('/artifact-bundles', { query: { status: 'active', limit: 100, ...params } }),
|
|
111
|
+
create: (params = {}) => this.request('/artifact-bundles', { method: 'POST', body: params }),
|
|
112
|
+
get: (id) => this.request(`/artifact-bundles/${encodePath(id)}`),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
this.artifacts = {
|
|
116
|
+
list: (params = {}) => {
|
|
117
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
118
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files`, {
|
|
119
|
+
query: omit(params, ['artifactBundleId']),
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
pull: (params = {}) => {
|
|
123
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
124
|
+
const artifactId = params.artifactId ?? params.fileId;
|
|
125
|
+
|
|
126
|
+
if (artifactId) {
|
|
127
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files/${encodePath(artifactId)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files`, {
|
|
131
|
+
query: omit(params, ['artifactBundleId']),
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
upload: (params = {}) => {
|
|
135
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
136
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
body: omit(params, ['artifactBundleId']),
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
update: (params = {}) => {
|
|
142
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
143
|
+
const fileId = requireParam(params, 'fileId');
|
|
144
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files/${encodePath(fileId)}`, {
|
|
145
|
+
method: 'PUT',
|
|
146
|
+
body: omit(params, ['artifactBundleId', 'fileId']),
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
remove: (params = {}) => {
|
|
150
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
151
|
+
const fileId = requireParam(params, 'fileId');
|
|
152
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files/${encodePath(fileId)}`, {
|
|
153
|
+
method: 'DELETE',
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
test: (params = {}) => this.startArtifactTest(params),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
this.audits = {
|
|
160
|
+
run: (params = {}) => {
|
|
161
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
162
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/audits`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: normalizeAuditBody(omit(params, ['artifactBundleId'])),
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
apply: (params = {}) => {
|
|
168
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
169
|
+
const auditRunId = requireParam(params, 'auditRunId');
|
|
170
|
+
return this.request(
|
|
171
|
+
`/artifact-bundles/${encodePath(artifactBundleId)}/audits/${encodePath(auditRunId)}/apply`,
|
|
172
|
+
{ method: 'POST', body: omit(params, ['artifactBundleId', 'auditRunId']) },
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
this.evals = {
|
|
178
|
+
run: (params = {}) => {
|
|
179
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
180
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/evals`, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: omit(params, ['artifactBundleId']),
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
this.rubrFlow = {
|
|
188
|
+
convert: (params = {}) => {
|
|
189
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
190
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/rubr-flow/conversions`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: omit(params, ['artifactBundleId']),
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this.jobs = {
|
|
198
|
+
get: (id) => this.request(`/jobs/${encodePath(id)}`),
|
|
199
|
+
wait: (id, options = {}) => this.waitForJob(id, options),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
this.proofReports = {
|
|
203
|
+
get: (idOrParams, options = {}) => {
|
|
204
|
+
const params = typeof idOrParams === 'object' && idOrParams !== null ? idOrParams : { proofReportId: idOrParams, ...options };
|
|
205
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
206
|
+
const proofReportId = requireParam(params, 'proofReportId');
|
|
207
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/proof-reports/${encodePath(proofReportId)}`);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} path
|
|
214
|
+
* @param {{ method?: string, query?: Record<string, unknown>, body?: unknown, signal?: AbortSignal }} [options]
|
|
215
|
+
*/
|
|
216
|
+
async request(path, { method = 'GET', query = {}, body = undefined, signal = undefined } = {}) {
|
|
217
|
+
const url = buildUrl(this.apiUrl, path, query);
|
|
218
|
+
/** @type {Record<string, string>} */
|
|
219
|
+
const headers = {
|
|
220
|
+
accept: 'application/json',
|
|
221
|
+
'user-agent': this.userAgent,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (this.apiKey) {
|
|
225
|
+
headers.authorization = `Bearer ${this.apiKey}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (body !== undefined) {
|
|
229
|
+
headers['content-type'] = 'application/json';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const attempts = method === 'GET' ? this.retryReads + 1 : 1;
|
|
233
|
+
let lastError;
|
|
234
|
+
|
|
235
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
236
|
+
try {
|
|
237
|
+
const response = await this.fetchImpl(url, {
|
|
238
|
+
method,
|
|
239
|
+
headers,
|
|
240
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
241
|
+
signal,
|
|
242
|
+
});
|
|
243
|
+
return await parseResponse(response);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
lastError = error;
|
|
246
|
+
|
|
247
|
+
if (error instanceof RubrkitApiError || attempt >= attempts) {
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw lastError;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {Record<string, any>} [params]
|
|
258
|
+
*/
|
|
259
|
+
async startArtifactTest(params = {}) {
|
|
260
|
+
const files = Array.isArray(params.files) ? params.files : [];
|
|
261
|
+
let artifactBundleId = typeof params.artifactBundleId === 'string' && params.artifactBundleId.trim() ? params.artifactBundleId.trim() : null;
|
|
262
|
+
let artifactBundle = null;
|
|
263
|
+
let upload = null;
|
|
264
|
+
|
|
265
|
+
if (files.length > 0) {
|
|
266
|
+
const reuseExistingBundle = Boolean(artifactBundleId);
|
|
267
|
+
|
|
268
|
+
if (!artifactBundleId) {
|
|
269
|
+
const created = await this.artifactBundles.create({
|
|
270
|
+
name: params.name ?? inferArtifactTestBundleName(files),
|
|
271
|
+
description: params.description ?? 'Created by a Rubrkit SDK or CLI remote test run.',
|
|
272
|
+
customRubric: params.customRubric,
|
|
273
|
+
settings: params.settings,
|
|
274
|
+
});
|
|
275
|
+
const createdRecord = asRecord(created);
|
|
276
|
+
artifactBundle = createdRecord.artifactBundle ?? createdRecord;
|
|
277
|
+
artifactBundleId = typeof asRecord(artifactBundle).id === 'string' ? asRecord(artifactBundle).id : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!artifactBundleId) {
|
|
281
|
+
throw new RubrkitError('Rubrkit API response did not include an artifact bundle ID.', { code: 'missing_artifact_bundle_id' });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A brand-new bundle is empty, so a plain create-upload is correct. An
|
|
285
|
+
// existing (pinned) bundle is synced: changed files are updated in place,
|
|
286
|
+
// new files added, and files no longer present are removed — so repeated
|
|
287
|
+
// runs don't conflict on already-present paths.
|
|
288
|
+
upload = reuseExistingBundle
|
|
289
|
+
? await this.syncArtifactBundleFiles({ artifactBundleId, files, message: params.message })
|
|
290
|
+
: await this.artifacts.upload({ artifactBundleId, files });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!artifactBundleId) {
|
|
294
|
+
artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const started = await this.audits.run({
|
|
298
|
+
artifactBundleId,
|
|
299
|
+
...pickAuditParams(params),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
...started,
|
|
304
|
+
artifactBundleId,
|
|
305
|
+
artifactBundle,
|
|
306
|
+
upload,
|
|
307
|
+
composedFrom: files.length > 0
|
|
308
|
+
? ['artifact-bundles.create', 'artifact-bundles.files.upload', 'artifact-bundles.audits.create']
|
|
309
|
+
: ['artifact-bundles.audits.create'],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Mirrors a local file set into an existing artifact bundle: updates files
|
|
315
|
+
* whose content changed, creates new ones, and deletes files no longer
|
|
316
|
+
* present. Unchanged files (matching content hash) are left untouched, so
|
|
317
|
+
* re-running on the same content is a no-op instead of a path conflict.
|
|
318
|
+
*
|
|
319
|
+
* @param {{ artifactBundleId: string, files: Array<{ path: string, content: string, artifactType?: string }>, message?: string }} params
|
|
320
|
+
* @returns {Promise<{ created: string[], updated: string[], deleted: string[], unchanged: string[] }>}
|
|
321
|
+
*/
|
|
322
|
+
async syncArtifactBundleFiles({ artifactBundleId, files, message }) {
|
|
323
|
+
// `limit` is capped at 200 by the API; a skills bundle is far smaller.
|
|
324
|
+
const existing = extractBundleFiles(await this.artifacts.list({ artifactBundleId, limit: 200 }));
|
|
325
|
+
const existingByPath = new Map(existing.map(file => [file.path, file]));
|
|
326
|
+
|
|
327
|
+
const created = [];
|
|
328
|
+
const updated = [];
|
|
329
|
+
const unchanged = [];
|
|
330
|
+
const toCreate = [];
|
|
331
|
+
const localPaths = new Set();
|
|
332
|
+
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
localPaths.add(file.path);
|
|
335
|
+
const match = existingByPath.get(file.path);
|
|
336
|
+
|
|
337
|
+
if (!match) {
|
|
338
|
+
toCreate.push(file);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (match.contentHash && match.contentHash === contentHashOf(file.content)) {
|
|
343
|
+
unchanged.push(file.path);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await this.artifacts.update({
|
|
348
|
+
artifactBundleId,
|
|
349
|
+
fileId: match.id,
|
|
350
|
+
content: file.content,
|
|
351
|
+
...(message ? { message } : {}),
|
|
352
|
+
});
|
|
353
|
+
updated.push(file.path);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (toCreate.length > 0) {
|
|
357
|
+
await this.artifacts.upload({ artifactBundleId, files: toCreate });
|
|
358
|
+
created.push(...toCreate.map(file => file.path));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const deleted = [];
|
|
362
|
+
for (const file of existing) {
|
|
363
|
+
if (!localPaths.has(file.path)) {
|
|
364
|
+
await this.artifacts.remove({ artifactBundleId, fileId: file.id });
|
|
365
|
+
deleted.push(file.path);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { created, updated, deleted, unchanged };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @param {string} id
|
|
374
|
+
* @param {JobWaitOptions} [options]
|
|
375
|
+
*/
|
|
376
|
+
async waitForJob(id, { intervalMs = 1000, timeoutMs = 10 * 60 * 1000, signal = undefined, onProgress = undefined } = {}) {
|
|
377
|
+
const startedAt = Date.now();
|
|
378
|
+
let lastPhase = null;
|
|
379
|
+
|
|
380
|
+
while (true) {
|
|
381
|
+
if (signal?.aborted) {
|
|
382
|
+
throw new RubrkitError('Job wait was aborted.', { code: 'aborted' });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const data = await this.jobs.get(id);
|
|
386
|
+
const job = data.job ?? data;
|
|
387
|
+
|
|
388
|
+
if (job.phase !== lastPhase || TERMINAL_JOB_STATES.has(job.state)) {
|
|
389
|
+
onProgress?.(job);
|
|
390
|
+
lastPhase = job.phase;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (TERMINAL_JOB_STATES.has(job.state)) {
|
|
394
|
+
return job;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
398
|
+
throw new RubrkitError(`Timed out waiting for Rubrkit job ${id}.`, { code: 'job_wait_timeout', details: { jobId: id } });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await sleep(intervalMs);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {Response} response
|
|
408
|
+
*/
|
|
409
|
+
async function parseResponse(response) {
|
|
410
|
+
const text = await response.text();
|
|
411
|
+
const body = text ? parseJson(text) : {};
|
|
412
|
+
const requestId = body && typeof body === 'object' && typeof body.requestId === 'string' ? body.requestId : null;
|
|
413
|
+
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
const error = body && typeof body === 'object' && 'error' in body ? body.error : {};
|
|
416
|
+
const message =
|
|
417
|
+
error && typeof error === 'object' && typeof error.message === 'string'
|
|
418
|
+
? error.message
|
|
419
|
+
: `Rubrkit API request failed with HTTP ${response.status}.`;
|
|
420
|
+
const code = error && typeof error === 'object' && typeof error.code === 'string' ? error.code : 'api_request_failed';
|
|
421
|
+
|
|
422
|
+
throw new RubrkitApiError(message, {
|
|
423
|
+
code,
|
|
424
|
+
status: response.status,
|
|
425
|
+
requestId,
|
|
426
|
+
details: error && typeof error === 'object' ? error.details : undefined,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return body && typeof body === 'object' && 'data' in body ? body.data : body;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* @param {string} text
|
|
435
|
+
*/
|
|
436
|
+
function parseJson(text) {
|
|
437
|
+
try {
|
|
438
|
+
return JSON.parse(text);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw new RubrkitApiError('Rubrkit API returned invalid JSON.', {
|
|
441
|
+
code: 'invalid_api_response',
|
|
442
|
+
details: error,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @param {string} apiUrl
|
|
449
|
+
* @param {string} path
|
|
450
|
+
* @param {Record<string, unknown>} query
|
|
451
|
+
*/
|
|
452
|
+
function buildUrl(apiUrl, path, query) {
|
|
453
|
+
const url = new URL(`${apiUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`);
|
|
454
|
+
|
|
455
|
+
for (const [key, value] of Object.entries(query)) {
|
|
456
|
+
if (value === undefined || value === null || value === false) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
url.searchParams.set(key, String(value));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return url.toString();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* @param {unknown} value
|
|
468
|
+
*/
|
|
469
|
+
function encodePath(value) {
|
|
470
|
+
return encodeURIComponent(String(value));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {Record<string, any>} params
|
|
475
|
+
* @param {string} name
|
|
476
|
+
*/
|
|
477
|
+
function requireParam(params, name) {
|
|
478
|
+
const value = params[name];
|
|
479
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
480
|
+
throw new RubrkitError(`Missing required SDK parameter "${name}".`, { code: 'missing_sdk_parameter', details: { name } });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return value.trim();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* @param {unknown} value
|
|
488
|
+
* @returns {Record<string, any>}
|
|
489
|
+
*/
|
|
490
|
+
function asRecord(value) {
|
|
491
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? /** @type {Record<string, any>} */ (value) : {};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @param {Record<string, any>} params
|
|
496
|
+
*/
|
|
497
|
+
function pickAuditParams(params) {
|
|
498
|
+
const picked = {};
|
|
499
|
+
|
|
500
|
+
for (const key of ['artifact', 'artifactType', 'targetFileId', 'targetVersionNumber', 'artifactBundleVersionNumber', 'rubric', 'rubricKey', 'noAi', 'force']) {
|
|
501
|
+
if (params[key] !== undefined && params[key] !== null && params[key] !== '') {
|
|
502
|
+
picked[key] = params[key];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return normalizeAuditBody(picked);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @param {Record<string, any>} body
|
|
511
|
+
*/
|
|
512
|
+
function normalizeAuditBody(body) {
|
|
513
|
+
const normalized = { ...body };
|
|
514
|
+
|
|
515
|
+
if (normalized.rubric !== undefined && normalized.rubricKey === undefined) {
|
|
516
|
+
normalized.rubricKey = normalized.rubric;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
delete normalized.rubric;
|
|
520
|
+
delete normalized.target;
|
|
521
|
+
delete normalized.ci;
|
|
522
|
+
delete normalized.files;
|
|
523
|
+
delete normalized.name;
|
|
524
|
+
delete normalized.description;
|
|
525
|
+
delete normalized.customRubric;
|
|
526
|
+
delete normalized.settings;
|
|
527
|
+
|
|
528
|
+
return normalized;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* @param {Array<Record<string, any>>} files
|
|
533
|
+
*/
|
|
534
|
+
function inferArtifactTestBundleName(files) {
|
|
535
|
+
const firstPath = typeof files[0]?.path === 'string' && files[0].path.trim() ? files[0].path.trim() : 'artifact';
|
|
536
|
+
const suffix = files.length > 1 ? ` and ${files.length - 1} more` : '';
|
|
537
|
+
const name = `CLI test: ${firstPath}${suffix}`;
|
|
538
|
+
|
|
539
|
+
return name.length <= 120 ? name : `${name.slice(0, 117)}...`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* @param {Record<string, any>} params
|
|
544
|
+
* @param {string[]} keys
|
|
545
|
+
*/
|
|
546
|
+
function omit(params, keys) {
|
|
547
|
+
const blocked = new Set(keys);
|
|
548
|
+
return Object.fromEntries(Object.entries(params).filter(([key]) => !blocked.has(key)));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @param {number} ms
|
|
553
|
+
*/
|
|
554
|
+
function sleep(ms) {
|
|
555
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
556
|
+
}
|