rubrkit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/sdk.js +109 -4
package/package.json
CHANGED
package/src/sdk.js
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { DEFAULT_API_URL, normalizeApiUrl } from './config.js';
|
|
4
5
|
|
|
5
6
|
const TERMINAL_JOB_STATES = new Set(['succeeded', 'failed', 'cancelled', 'paused']);
|
|
6
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
|
+
|
|
7
33
|
/**
|
|
8
34
|
* @typedef {{
|
|
9
35
|
* apiKey?: string | null,
|
|
@@ -112,6 +138,21 @@ export class Rubrkit {
|
|
|
112
138
|
body: omit(params, ['artifactBundleId']),
|
|
113
139
|
});
|
|
114
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
|
+
},
|
|
115
156
|
test: (params = {}) => this.startArtifactTest(params),
|
|
116
157
|
};
|
|
117
158
|
|
|
@@ -222,6 +263,8 @@ export class Rubrkit {
|
|
|
222
263
|
let upload = null;
|
|
223
264
|
|
|
224
265
|
if (files.length > 0) {
|
|
266
|
+
const reuseExistingBundle = Boolean(artifactBundleId);
|
|
267
|
+
|
|
225
268
|
if (!artifactBundleId) {
|
|
226
269
|
const created = await this.artifactBundles.create({
|
|
227
270
|
name: params.name ?? inferArtifactTestBundleName(files),
|
|
@@ -238,10 +281,13 @@ export class Rubrkit {
|
|
|
238
281
|
throw new RubrkitError('Rubrkit API response did not include an artifact bundle ID.', { code: 'missing_artifact_bundle_id' });
|
|
239
282
|
}
|
|
240
283
|
|
|
241
|
-
upload
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 });
|
|
245
291
|
}
|
|
246
292
|
|
|
247
293
|
if (!artifactBundleId) {
|
|
@@ -264,6 +310,65 @@ export class Rubrkit {
|
|
|
264
310
|
};
|
|
265
311
|
}
|
|
266
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
|
+
|
|
267
372
|
/**
|
|
268
373
|
* @param {string} id
|
|
269
374
|
* @param {JobWaitOptions} [options]
|