rubrkit 0.1.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 +126 -0
- package/bin/rubrkit.js +16 -0
- package/package.json +28 -0
- package/src/adapters.js +118 -0
- package/src/api.js +101 -0
- package/src/args.js +175 -0
- package/src/cli.js +93 -0
- package/src/config.js +169 -0
- package/src/errors.js +55 -0
- package/src/formats.js +222 -0
- package/src/index.d.ts +76 -0
- package/src/localChecks.js +680 -0
- package/src/manifest.js +118 -0
- package/src/pathSafety.js +62 -0
- package/src/prompts.js +149 -0
- package/src/pull.js +676 -0
- package/src/sdk.js +443 -0
- package/src/testingCli.js +431 -0
package/src/sdk.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_API_URL, normalizeApiUrl } from './config.js';
|
|
4
|
+
|
|
5
|
+
const TERMINAL_JOB_STATES = new Set(['succeeded', 'failed', 'cancelled', 'paused']);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* apiKey?: string | null,
|
|
10
|
+
* apiUrl?: string,
|
|
11
|
+
* fetchImpl?: typeof fetch,
|
|
12
|
+
* userAgent?: string,
|
|
13
|
+
* retryReads?: number
|
|
14
|
+
* }} RubrkitOptions
|
|
15
|
+
*
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* intervalMs?: number,
|
|
18
|
+
* timeoutMs?: number,
|
|
19
|
+
* signal?: AbortSignal,
|
|
20
|
+
* onProgress?: (job: Record<string, any>) => void
|
|
21
|
+
* }} JobWaitOptions
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export class RubrkitError extends Error {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @param {{ code?: string, details?: unknown }} [options]
|
|
28
|
+
*/
|
|
29
|
+
constructor(message, { code = 'rubrkit_error', details = undefined } = {}) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'RubrkitError';
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.details = details;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class RubrkitApiError extends RubrkitError {
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} message
|
|
40
|
+
* @param {{ code?: string, status?: number, requestId?: string | null, details?: unknown }} [options]
|
|
41
|
+
*/
|
|
42
|
+
constructor(message, { code = 'api_request_failed', status = 0, requestId = null, details = undefined } = {}) {
|
|
43
|
+
super(message, { code, details });
|
|
44
|
+
this.name = 'RubrkitApiError';
|
|
45
|
+
this.status = status;
|
|
46
|
+
this.requestId = requestId;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class RubrkitNetworkError extends RubrkitError {
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} message
|
|
53
|
+
* @param {{ cause?: unknown }} [options]
|
|
54
|
+
*/
|
|
55
|
+
constructor(message, { cause = undefined } = {}) {
|
|
56
|
+
super(message, { code: 'network_error', details: cause });
|
|
57
|
+
this.name = 'RubrkitNetworkError';
|
|
58
|
+
this.cause = cause;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class Rubrkit {
|
|
63
|
+
/**
|
|
64
|
+
* @param {RubrkitOptions} [options]
|
|
65
|
+
*/
|
|
66
|
+
constructor({
|
|
67
|
+
apiKey = process.env.RUBRKIT_API_KEY ?? null,
|
|
68
|
+
apiUrl = process.env.RUBRKIT_API_URL ?? DEFAULT_API_URL,
|
|
69
|
+
fetchImpl = globalThis.fetch,
|
|
70
|
+
userAgent = 'rubrkit-sdk/0.0.0-local',
|
|
71
|
+
retryReads = 0,
|
|
72
|
+
} = {}) {
|
|
73
|
+
this.apiKey = apiKey;
|
|
74
|
+
this.apiUrl = normalizeApiUrl(apiUrl);
|
|
75
|
+
this.fetchImpl = fetchImpl;
|
|
76
|
+
this.userAgent = userAgent;
|
|
77
|
+
this.retryReads = Math.max(0, Number(retryReads) || 0);
|
|
78
|
+
|
|
79
|
+
this.me = {
|
|
80
|
+
get: () => this.request('/me'),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.artifactBundles = {
|
|
84
|
+
list: (params = {}) => this.request('/artifact-bundles', { query: { status: 'active', limit: 100, ...params } }),
|
|
85
|
+
create: (params = {}) => this.request('/artifact-bundles', { method: 'POST', body: params }),
|
|
86
|
+
get: (id) => this.request(`/artifact-bundles/${encodePath(id)}`),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.artifacts = {
|
|
90
|
+
list: (params = {}) => {
|
|
91
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
92
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files`, {
|
|
93
|
+
query: omit(params, ['artifactBundleId']),
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
pull: (params = {}) => {
|
|
97
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
98
|
+
const artifactId = params.artifactId ?? params.fileId;
|
|
99
|
+
|
|
100
|
+
if (artifactId) {
|
|
101
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files/${encodePath(artifactId)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files`, {
|
|
105
|
+
query: omit(params, ['artifactBundleId']),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
upload: (params = {}) => {
|
|
109
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
110
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/files`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
body: omit(params, ['artifactBundleId']),
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
test: (params = {}) => this.startArtifactTest(params),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.audits = {
|
|
119
|
+
run: (params = {}) => {
|
|
120
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
121
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/audits`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: normalizeAuditBody(omit(params, ['artifactBundleId'])),
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
this.evals = {
|
|
129
|
+
run: (params = {}) => {
|
|
130
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
131
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/evals`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
body: omit(params, ['artifactBundleId']),
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
this.rubrFlow = {
|
|
139
|
+
convert: (params = {}) => {
|
|
140
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
141
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/rubr-flow/conversions`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
body: omit(params, ['artifactBundleId']),
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.jobs = {
|
|
149
|
+
get: (id) => this.request(`/jobs/${encodePath(id)}`),
|
|
150
|
+
wait: (id, options = {}) => this.waitForJob(id, options),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
this.proofReports = {
|
|
154
|
+
get: (idOrParams, options = {}) => {
|
|
155
|
+
const params = typeof idOrParams === 'object' && idOrParams !== null ? idOrParams : { proofReportId: idOrParams, ...options };
|
|
156
|
+
const artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
157
|
+
const proofReportId = requireParam(params, 'proofReportId');
|
|
158
|
+
return this.request(`/artifact-bundles/${encodePath(artifactBundleId)}/proof-reports/${encodePath(proofReportId)}`);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} path
|
|
165
|
+
* @param {{ method?: string, query?: Record<string, unknown>, body?: unknown, signal?: AbortSignal }} [options]
|
|
166
|
+
*/
|
|
167
|
+
async request(path, { method = 'GET', query = {}, body = undefined, signal = undefined } = {}) {
|
|
168
|
+
const url = buildUrl(this.apiUrl, path, query);
|
|
169
|
+
/** @type {Record<string, string>} */
|
|
170
|
+
const headers = {
|
|
171
|
+
accept: 'application/json',
|
|
172
|
+
'user-agent': this.userAgent,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (this.apiKey) {
|
|
176
|
+
headers.authorization = `Bearer ${this.apiKey}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (body !== undefined) {
|
|
180
|
+
headers['content-type'] = 'application/json';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const attempts = method === 'GET' ? this.retryReads + 1 : 1;
|
|
184
|
+
let lastError;
|
|
185
|
+
|
|
186
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
187
|
+
try {
|
|
188
|
+
const response = await this.fetchImpl(url, {
|
|
189
|
+
method,
|
|
190
|
+
headers,
|
|
191
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
192
|
+
signal,
|
|
193
|
+
});
|
|
194
|
+
return await parseResponse(response);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
lastError = error;
|
|
197
|
+
|
|
198
|
+
if (error instanceof RubrkitApiError || attempt >= attempts) {
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw lastError;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {Record<string, any>} [params]
|
|
209
|
+
*/
|
|
210
|
+
async startArtifactTest(params = {}) {
|
|
211
|
+
const files = Array.isArray(params.files) ? params.files : [];
|
|
212
|
+
let artifactBundleId = typeof params.artifactBundleId === 'string' && params.artifactBundleId.trim() ? params.artifactBundleId.trim() : null;
|
|
213
|
+
let artifactBundle = null;
|
|
214
|
+
let upload = null;
|
|
215
|
+
|
|
216
|
+
if (files.length > 0) {
|
|
217
|
+
if (!artifactBundleId) {
|
|
218
|
+
const created = await this.artifactBundles.create({
|
|
219
|
+
name: params.name ?? inferArtifactTestBundleName(files),
|
|
220
|
+
description: params.description ?? 'Created by a Rubrkit SDK or CLI remote test run.',
|
|
221
|
+
customRubric: params.customRubric,
|
|
222
|
+
settings: params.settings,
|
|
223
|
+
});
|
|
224
|
+
const createdRecord = asRecord(created);
|
|
225
|
+
artifactBundle = createdRecord.artifactBundle ?? createdRecord;
|
|
226
|
+
artifactBundleId = typeof asRecord(artifactBundle).id === 'string' ? asRecord(artifactBundle).id : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!artifactBundleId) {
|
|
230
|
+
throw new RubrkitError('Rubrkit API response did not include an artifact bundle ID.', { code: 'missing_artifact_bundle_id' });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
upload = await this.artifacts.upload({
|
|
234
|
+
artifactBundleId,
|
|
235
|
+
files,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!artifactBundleId) {
|
|
240
|
+
artifactBundleId = requireParam(params, 'artifactBundleId');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const started = await this.audits.run({
|
|
244
|
+
artifactBundleId,
|
|
245
|
+
...pickAuditParams(params),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...started,
|
|
250
|
+
artifactBundleId,
|
|
251
|
+
artifactBundle,
|
|
252
|
+
upload,
|
|
253
|
+
composedFrom: files.length > 0
|
|
254
|
+
? ['artifact-bundles.create', 'artifact-bundles.files.upload', 'artifact-bundles.audits.create']
|
|
255
|
+
: ['artifact-bundles.audits.create'],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {string} id
|
|
261
|
+
* @param {JobWaitOptions} [options]
|
|
262
|
+
*/
|
|
263
|
+
async waitForJob(id, { intervalMs = 1000, timeoutMs = 10 * 60 * 1000, signal = undefined, onProgress = undefined } = {}) {
|
|
264
|
+
const startedAt = Date.now();
|
|
265
|
+
let lastPhase = null;
|
|
266
|
+
|
|
267
|
+
while (true) {
|
|
268
|
+
if (signal?.aborted) {
|
|
269
|
+
throw new RubrkitError('Job wait was aborted.', { code: 'aborted' });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const data = await this.jobs.get(id);
|
|
273
|
+
const job = data.job ?? data;
|
|
274
|
+
|
|
275
|
+
if (job.phase !== lastPhase || TERMINAL_JOB_STATES.has(job.state)) {
|
|
276
|
+
onProgress?.(job);
|
|
277
|
+
lastPhase = job.phase;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (TERMINAL_JOB_STATES.has(job.state)) {
|
|
281
|
+
return job;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
285
|
+
throw new RubrkitError(`Timed out waiting for Rubrkit job ${id}.`, { code: 'job_wait_timeout', details: { jobId: id } });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await sleep(intervalMs);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {Response} response
|
|
295
|
+
*/
|
|
296
|
+
async function parseResponse(response) {
|
|
297
|
+
const text = await response.text();
|
|
298
|
+
const body = text ? parseJson(text) : {};
|
|
299
|
+
const requestId = body && typeof body === 'object' && typeof body.requestId === 'string' ? body.requestId : null;
|
|
300
|
+
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const error = body && typeof body === 'object' && 'error' in body ? body.error : {};
|
|
303
|
+
const message =
|
|
304
|
+
error && typeof error === 'object' && typeof error.message === 'string'
|
|
305
|
+
? error.message
|
|
306
|
+
: `Rubrkit API request failed with HTTP ${response.status}.`;
|
|
307
|
+
const code = error && typeof error === 'object' && typeof error.code === 'string' ? error.code : 'api_request_failed';
|
|
308
|
+
|
|
309
|
+
throw new RubrkitApiError(message, {
|
|
310
|
+
code,
|
|
311
|
+
status: response.status,
|
|
312
|
+
requestId,
|
|
313
|
+
details: error && typeof error === 'object' ? error.details : undefined,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return body && typeof body === 'object' && 'data' in body ? body.data : body;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @param {string} text
|
|
322
|
+
*/
|
|
323
|
+
function parseJson(text) {
|
|
324
|
+
try {
|
|
325
|
+
return JSON.parse(text);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new RubrkitApiError('Rubrkit API returned invalid JSON.', {
|
|
328
|
+
code: 'invalid_api_response',
|
|
329
|
+
details: error,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @param {string} apiUrl
|
|
336
|
+
* @param {string} path
|
|
337
|
+
* @param {Record<string, unknown>} query
|
|
338
|
+
*/
|
|
339
|
+
function buildUrl(apiUrl, path, query) {
|
|
340
|
+
const url = new URL(`${apiUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`);
|
|
341
|
+
|
|
342
|
+
for (const [key, value] of Object.entries(query)) {
|
|
343
|
+
if (value === undefined || value === null || value === false) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
url.searchParams.set(key, String(value));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return url.toString();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @param {unknown} value
|
|
355
|
+
*/
|
|
356
|
+
function encodePath(value) {
|
|
357
|
+
return encodeURIComponent(String(value));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @param {Record<string, any>} params
|
|
362
|
+
* @param {string} name
|
|
363
|
+
*/
|
|
364
|
+
function requireParam(params, name) {
|
|
365
|
+
const value = params[name];
|
|
366
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
367
|
+
throw new RubrkitError(`Missing required SDK parameter "${name}".`, { code: 'missing_sdk_parameter', details: { name } });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return value.trim();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {unknown} value
|
|
375
|
+
* @returns {Record<string, any>}
|
|
376
|
+
*/
|
|
377
|
+
function asRecord(value) {
|
|
378
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? /** @type {Record<string, any>} */ (value) : {};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {Record<string, any>} params
|
|
383
|
+
*/
|
|
384
|
+
function pickAuditParams(params) {
|
|
385
|
+
const picked = {};
|
|
386
|
+
|
|
387
|
+
for (const key of ['artifact', 'artifactType', 'targetFileId', 'targetVersionNumber', 'artifactBundleVersionNumber', 'rubric', 'rubricKey', 'noAi']) {
|
|
388
|
+
if (params[key] !== undefined && params[key] !== null && params[key] !== '') {
|
|
389
|
+
picked[key] = params[key];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return normalizeAuditBody(picked);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @param {Record<string, any>} body
|
|
398
|
+
*/
|
|
399
|
+
function normalizeAuditBody(body) {
|
|
400
|
+
const normalized = { ...body };
|
|
401
|
+
|
|
402
|
+
if (normalized.rubric !== undefined && normalized.rubricKey === undefined) {
|
|
403
|
+
normalized.rubricKey = normalized.rubric;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
delete normalized.rubric;
|
|
407
|
+
delete normalized.target;
|
|
408
|
+
delete normalized.ci;
|
|
409
|
+
delete normalized.files;
|
|
410
|
+
delete normalized.name;
|
|
411
|
+
delete normalized.description;
|
|
412
|
+
delete normalized.customRubric;
|
|
413
|
+
delete normalized.settings;
|
|
414
|
+
|
|
415
|
+
return normalized;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* @param {Array<Record<string, any>>} files
|
|
420
|
+
*/
|
|
421
|
+
function inferArtifactTestBundleName(files) {
|
|
422
|
+
const firstPath = typeof files[0]?.path === 'string' && files[0].path.trim() ? files[0].path.trim() : 'artifact';
|
|
423
|
+
const suffix = files.length > 1 ? ` and ${files.length - 1} more` : '';
|
|
424
|
+
const name = `CLI test: ${firstPath}${suffix}`;
|
|
425
|
+
|
|
426
|
+
return name.length <= 120 ? name : `${name.slice(0, 117)}...`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @param {Record<string, any>} params
|
|
431
|
+
* @param {string[]} keys
|
|
432
|
+
*/
|
|
433
|
+
function omit(params, keys) {
|
|
434
|
+
const blocked = new Set(keys);
|
|
435
|
+
return Object.fromEntries(Object.entries(params).filter(([key]) => !blocked.has(key)));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* @param {number} ms
|
|
440
|
+
*/
|
|
441
|
+
function sleep(ms) {
|
|
442
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
443
|
+
}
|