@knolo/cli 5.2.0 → 5.2.2
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 +75 -29
- package/bin/knolo.mjs +9 -6
- package/bin/registry/artifacts.mjs +21 -5
- package/bin/registry/commands.mjs +850 -87
- package/bin/registry/errors.mjs +3 -1
- package/bin/registry/http.mjs +72 -18
- package/bin/registry/lockfile.mjs +16 -5
- package/bin/registry/manifest.mjs +6 -0
- package/package.json +3 -2
|
@@ -1,18 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmod,
|
|
3
|
+
copyFile,
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
readFile,
|
|
7
|
+
rename,
|
|
8
|
+
unlink,
|
|
9
|
+
} from 'node:fs/promises';
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
11
|
+
import { isIP } from 'node:net';
|
|
2
12
|
import { homedir } from 'node:os';
|
|
3
13
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
14
|
+
import { put as putBlob } from '@vercel/blob';
|
|
15
|
+
import {
|
|
16
|
+
normalizeRegistryUrl,
|
|
17
|
+
registryApiUrl,
|
|
18
|
+
resolveRegistryUrl,
|
|
19
|
+
} from './config.mjs';
|
|
20
|
+
import {
|
|
21
|
+
downloadBlobToTemp,
|
|
22
|
+
MAX_ARTIFACT_BYTES,
|
|
23
|
+
readArtifact,
|
|
24
|
+
stageCachedArtifact,
|
|
25
|
+
} from './artifacts.mjs';
|
|
6
26
|
import { RegistryError } from './errors.mjs';
|
|
7
|
-
import { getJson } from './http.mjs';
|
|
8
|
-
import {
|
|
27
|
+
import { getJson, postJson } from './http.mjs';
|
|
28
|
+
import {
|
|
29
|
+
cachePath,
|
|
30
|
+
stageLockfileUpdate,
|
|
31
|
+
validateLockfileUpdate,
|
|
32
|
+
} from './lockfile.mjs';
|
|
9
33
|
import { validatePackManifest } from './manifest.mjs';
|
|
10
34
|
import { parsePackName, parsePackSpec } from './specs.mjs';
|
|
11
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
loadCredentials,
|
|
37
|
+
promptForToken,
|
|
38
|
+
removeCredentials,
|
|
39
|
+
saveCredentials,
|
|
40
|
+
validateToken,
|
|
41
|
+
} from './credentials.mjs';
|
|
12
42
|
|
|
13
43
|
const PACK_SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
44
|
+
const LOCAL_ONLY_HOSTNAMES = new Set([
|
|
45
|
+
'localhost',
|
|
46
|
+
'localhost.localdomain',
|
|
47
|
+
'ip6-localhost',
|
|
48
|
+
'ip6-loopback',
|
|
49
|
+
'broadcasthost',
|
|
50
|
+
'local',
|
|
51
|
+
'internal',
|
|
52
|
+
]);
|
|
14
53
|
|
|
15
|
-
export async function runHubSearch(
|
|
54
|
+
export async function runHubSearch(
|
|
55
|
+
args,
|
|
56
|
+
{ env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}
|
|
57
|
+
) {
|
|
16
58
|
const { query, flags } = parseSearchArgs(args);
|
|
17
59
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
18
60
|
const url = registryApiUrl(registry, ['packs'], {
|
|
@@ -31,7 +73,10 @@ export async function runHubSearch(args, { env = process.env, fetchImpl = global
|
|
|
31
73
|
return { registry, url: String(url), body };
|
|
32
74
|
}
|
|
33
75
|
|
|
34
|
-
export async function runHubInfo(
|
|
76
|
+
export async function runHubInfo(
|
|
77
|
+
args,
|
|
78
|
+
{ env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}
|
|
79
|
+
) {
|
|
35
80
|
const { positional, flags } = parseInfoArgs(args);
|
|
36
81
|
const pack = parsePackName(positional[0]);
|
|
37
82
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
@@ -45,31 +90,52 @@ export async function runHubInfo(args, { env = process.env, fetchImpl = globalTh
|
|
|
45
90
|
return { registry, url: String(url), body };
|
|
46
91
|
}
|
|
47
92
|
|
|
48
|
-
export async function runHubAdd(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
93
|
+
export async function runHubAdd(
|
|
94
|
+
args,
|
|
95
|
+
{
|
|
96
|
+
env = process.env,
|
|
97
|
+
fetchImpl = globalThis.fetch,
|
|
98
|
+
core,
|
|
99
|
+
cwd = process.cwd(),
|
|
100
|
+
homeDir,
|
|
101
|
+
print = console.log,
|
|
102
|
+
warn = console.error,
|
|
103
|
+
} = {}
|
|
104
|
+
) {
|
|
57
105
|
const { spec, flags } = parseAddArgs(args);
|
|
58
|
-
if (!core)
|
|
106
|
+
if (!core)
|
|
107
|
+
throw new Error('Could not load @knolo/core for Hub artifact validation.');
|
|
59
108
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
60
|
-
const manifestUrl = registryApiUrl(registry, [
|
|
109
|
+
const manifestUrl = registryApiUrl(registry, [
|
|
110
|
+
'packs',
|
|
111
|
+
spec.publisher,
|
|
112
|
+
spec.slug,
|
|
113
|
+
spec.version,
|
|
114
|
+
]);
|
|
61
115
|
|
|
62
116
|
let manifest;
|
|
63
117
|
let yanked = false;
|
|
64
118
|
try {
|
|
65
119
|
const body = await getJson(manifestUrl, { fetchImpl });
|
|
66
|
-
manifest = validatePackManifest(body, {
|
|
120
|
+
manifest = validatePackManifest(body, {
|
|
121
|
+
expectedName: spec.name,
|
|
122
|
+
requestedVersion: spec.version,
|
|
123
|
+
});
|
|
67
124
|
} catch (error) {
|
|
68
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
error instanceof RegistryError &&
|
|
127
|
+
(error.status === 404 || error.code === 'not_found')
|
|
128
|
+
) {
|
|
69
129
|
throw new Error('pack version not found');
|
|
70
130
|
}
|
|
71
|
-
if (
|
|
72
|
-
|
|
131
|
+
if (
|
|
132
|
+
error instanceof RegistryError &&
|
|
133
|
+
(error.status === 410 || error.code === 'yanked')
|
|
134
|
+
) {
|
|
135
|
+
manifest = validatePackManifest(error.body, {
|
|
136
|
+
expectedName: spec.name,
|
|
137
|
+
requestedVersion: spec.version,
|
|
138
|
+
});
|
|
73
139
|
yanked = true;
|
|
74
140
|
} else {
|
|
75
141
|
throw error;
|
|
@@ -77,10 +143,15 @@ export async function runHubAdd(args, {
|
|
|
77
143
|
}
|
|
78
144
|
|
|
79
145
|
if (manifest.yanked) yanked = true;
|
|
80
|
-
if (yanked && !flags.force)
|
|
81
|
-
|
|
146
|
+
if (yanked && !flags.force)
|
|
147
|
+
throw new Error('version yanked; use --force to download yanked bytes');
|
|
148
|
+
if (yanked)
|
|
149
|
+
warn(
|
|
150
|
+
`warning: downloading yanked version ${manifest.name}@${manifest.version}`
|
|
151
|
+
);
|
|
82
152
|
if (!manifest.url) throw new Error('artifact bytes are not stored yet');
|
|
83
|
-
if (!PACK_SHA256_PATTERN.test(manifest.sha256))
|
|
153
|
+
if (!PACK_SHA256_PATTERN.test(manifest.sha256))
|
|
154
|
+
throw new Error('manifest sha256 is invalid');
|
|
84
155
|
|
|
85
156
|
const artifactHome = homeDir || homedir();
|
|
86
157
|
await validateLockfileUpdate({ cwd, registry, manifest, force: flags.force });
|
|
@@ -91,26 +162,61 @@ export async function runHubAdd(args, {
|
|
|
91
162
|
expectedSize: manifest.sizeBytes,
|
|
92
163
|
fetchImpl,
|
|
93
164
|
});
|
|
94
|
-
|
|
165
|
+
const stagedFiles = [];
|
|
95
166
|
try {
|
|
96
167
|
if (download.sha256 !== manifest.sha256) {
|
|
97
|
-
throw new Error(
|
|
168
|
+
throw new Error(
|
|
169
|
+
`artifact sha256 mismatch: expected ${manifest.sha256}, got ${download.sha256}`
|
|
170
|
+
);
|
|
98
171
|
}
|
|
99
172
|
const bytes = await readArtifact(download.tempPath);
|
|
100
173
|
const validation = await validateKnowledgeImage(core, bytes);
|
|
101
|
-
if (
|
|
102
|
-
|
|
174
|
+
if (
|
|
175
|
+
manifest.format &&
|
|
176
|
+
String(manifest.format).toUpperCase() !== validation.format
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`artifact format mismatch: manifest=${manifest.format}, bytes=${validation.format}`
|
|
180
|
+
);
|
|
103
181
|
}
|
|
104
|
-
if (
|
|
105
|
-
throw new Error(
|
|
182
|
+
if (validation.format === 'V5' && !manifest.stateRoot) {
|
|
183
|
+
throw new Error('V5 artifact requires manifest stateRoot.');
|
|
106
184
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
185
|
+
if (
|
|
186
|
+
manifest.stateRoot !== undefined &&
|
|
187
|
+
manifest.stateRoot !== validation.stateRoot
|
|
188
|
+
) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`artifact stateRoot mismatch: expected ${manifest.stateRoot}, got ${validation.stateRoot || 'none'}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const stagedCachePath = await stageCachedArtifact(
|
|
194
|
+
download.tempPath,
|
|
195
|
+
finalCachePath
|
|
196
|
+
);
|
|
197
|
+
stagedFiles.push({ targetPath: finalCachePath, tempPath: stagedCachePath });
|
|
198
|
+
let outputPath = finalCachePath;
|
|
111
199
|
if (flags.out) {
|
|
112
|
-
|
|
200
|
+
const stagedOutput = await stageArtifactCopy(
|
|
201
|
+
download.tempPath,
|
|
202
|
+
flags.out,
|
|
203
|
+
cwd
|
|
204
|
+
);
|
|
205
|
+
stagedFiles.push(stagedOutput);
|
|
206
|
+
outputPath = stagedOutput.targetPath;
|
|
113
207
|
}
|
|
208
|
+
const stagedLock = await stageLockfileUpdate({
|
|
209
|
+
cwd,
|
|
210
|
+
registry,
|
|
211
|
+
manifest,
|
|
212
|
+
force: flags.force,
|
|
213
|
+
});
|
|
214
|
+
stagedFiles.push({
|
|
215
|
+
targetPath: stagedLock.path,
|
|
216
|
+
tempPath: stagedLock.tempPath,
|
|
217
|
+
});
|
|
218
|
+
await commitStagedFiles(stagedFiles);
|
|
219
|
+
const lock = { path: stagedLock.path, entry: stagedLock.entry };
|
|
114
220
|
const result = { ...manifest, path: outputPath, lockfile: lock.path };
|
|
115
221
|
if (flags.json) print(JSON.stringify(result, null, 2));
|
|
116
222
|
else {
|
|
@@ -121,29 +227,44 @@ export async function runHubAdd(args, {
|
|
|
121
227
|
}
|
|
122
228
|
return result;
|
|
123
229
|
} finally {
|
|
124
|
-
|
|
230
|
+
await unlinkIfPresent(download.tempPath);
|
|
231
|
+
await Promise.all(
|
|
232
|
+
stagedFiles.map(({ tempPath }) => unlinkIfPresent(tempPath))
|
|
233
|
+
);
|
|
125
234
|
}
|
|
126
235
|
}
|
|
127
236
|
|
|
128
|
-
export async function runHubLogin(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
237
|
+
export async function runHubLogin(
|
|
238
|
+
args,
|
|
239
|
+
{
|
|
240
|
+
env = process.env,
|
|
241
|
+
homeDir,
|
|
242
|
+
input = process.stdin,
|
|
243
|
+
output = process.stderr,
|
|
244
|
+
print = console.log,
|
|
245
|
+
} = {}
|
|
246
|
+
) {
|
|
135
247
|
const { flags } = parseLoginArgs(args);
|
|
136
248
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
137
|
-
print(
|
|
138
|
-
|
|
249
|
+
print(
|
|
250
|
+
`Create a token at ${registry}/dashboard/tokens (Sign in with GitHub).`
|
|
251
|
+
);
|
|
252
|
+
const token = (
|
|
253
|
+
flags.token ??
|
|
254
|
+
(await promptForToken({ input, output, forceStdin: flags.stdin }))
|
|
255
|
+
).trim();
|
|
139
256
|
validateToken(token);
|
|
140
257
|
const saved = await saveCredentials({ registry, token, env, homeDir });
|
|
141
258
|
print(`logged in to ${saved.registry}`);
|
|
142
259
|
print(`token ${saved.prefix}`);
|
|
260
|
+
print('Authorization: Bearer kno_…');
|
|
143
261
|
return saved;
|
|
144
262
|
}
|
|
145
263
|
|
|
146
|
-
export async function runHubWhoami(
|
|
264
|
+
export async function runHubWhoami(
|
|
265
|
+
args,
|
|
266
|
+
{ env = process.env, homeDir, print = console.log } = {}
|
|
267
|
+
) {
|
|
147
268
|
if (args.length) throw new Error('Usage: knolo whoami');
|
|
148
269
|
const credentials = await loadCredentials({ env, homeDir });
|
|
149
270
|
print(`registry ${credentials.registry}`);
|
|
@@ -151,15 +272,553 @@ export async function runHubWhoami(args, { env = process.env, homeDir, print = c
|
|
|
151
272
|
return { registry: credentials.registry, prefix: credentials.prefix };
|
|
152
273
|
}
|
|
153
274
|
|
|
154
|
-
export async function runHubLogout(
|
|
275
|
+
export async function runHubLogout(
|
|
276
|
+
args,
|
|
277
|
+
{ env = process.env, homeDir, print = console.log } = {}
|
|
278
|
+
) {
|
|
155
279
|
if (args.length) throw new Error('Usage: knolo logout');
|
|
156
280
|
const result = await removeCredentials({ env, homeDir });
|
|
157
281
|
print(result.removed ? 'logged out' : 'not logged in');
|
|
158
282
|
return result;
|
|
159
283
|
}
|
|
160
284
|
|
|
161
|
-
export function
|
|
162
|
-
|
|
285
|
+
export async function runHubPublish(
|
|
286
|
+
args,
|
|
287
|
+
{
|
|
288
|
+
env = process.env,
|
|
289
|
+
fetchImpl = globalThis.fetch,
|
|
290
|
+
cwd = process.cwd(),
|
|
291
|
+
homeDir,
|
|
292
|
+
print = console.log,
|
|
293
|
+
sleep = wait,
|
|
294
|
+
putImpl = putBlob,
|
|
295
|
+
pollIntervalMs = 1000,
|
|
296
|
+
maxPollAttempts = 120,
|
|
297
|
+
} = {}
|
|
298
|
+
) {
|
|
299
|
+
const { packPath, flags } = parsePublishArgs(args);
|
|
300
|
+
const credentials = await loadCredentials({ env, homeDir });
|
|
301
|
+
const registry = resolveCredentialRegistry(
|
|
302
|
+
flags.registry,
|
|
303
|
+
credentials.registry
|
|
304
|
+
);
|
|
305
|
+
const authHeaders = authorizationHeaders(credentials.token);
|
|
306
|
+
|
|
307
|
+
// This request is deliberately first: it proves the stored dashboard token
|
|
308
|
+
// is a Hub Bearer credential before any upload or publish mutation starts.
|
|
309
|
+
const account = await getJson(registryApiUrl(registry, ['account']), {
|
|
310
|
+
fetchImpl,
|
|
311
|
+
headers: authHeaders,
|
|
312
|
+
});
|
|
313
|
+
const accountPublisher = extractPublisherHandle(account);
|
|
314
|
+
if (
|
|
315
|
+
flags.publisher &&
|
|
316
|
+
accountPublisher &&
|
|
317
|
+
flags.publisher !== accountPublisher
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Token is scoped to publisher ${accountPublisher}, not ${flags.publisher}.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const publisher = flags.publisher || accountPublisher;
|
|
324
|
+
|
|
325
|
+
const packFile = path.resolve(cwd, packPath);
|
|
326
|
+
const bytes = await readFile(packFile);
|
|
327
|
+
if (bytes.length > MAX_ARTIFACT_BYTES)
|
|
328
|
+
throw new Error('Artifact exceeds Hub’s 250 MB limit.');
|
|
329
|
+
const sha256 = createSha256(bytes);
|
|
330
|
+
const pathname = `sha256/${sha256}.knolo`;
|
|
331
|
+
|
|
332
|
+
const uploaded = await putImpl(pathname, bytes, {
|
|
333
|
+
access: 'public',
|
|
334
|
+
addRandomSuffix: false,
|
|
335
|
+
// The pathname is content-addressed by the already-computed SHA-256.
|
|
336
|
+
// Allowing overwrite makes a retry safe after a partial Hub publish.
|
|
337
|
+
allowOverwrite: true,
|
|
338
|
+
contentType: 'application/octet-stream',
|
|
339
|
+
cacheControlMaxAge: 31536000,
|
|
340
|
+
token: packsReadWriteToken(env),
|
|
341
|
+
});
|
|
342
|
+
if (!uploaded || typeof uploaded.url !== 'string' || !uploaded.url) {
|
|
343
|
+
throw new Error('Public Blob upload did not return a URL.');
|
|
344
|
+
}
|
|
345
|
+
if (uploaded.pathname !== undefined && uploaded.pathname !== pathname) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Public Blob upload pathname is not locked to ${pathname}.`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const blobUrl = assertPublicBlobUrl(
|
|
351
|
+
uploaded.url,
|
|
352
|
+
'uploaded artifact'
|
|
353
|
+
).toString();
|
|
354
|
+
|
|
355
|
+
await postJson(
|
|
356
|
+
hubApiUrl(registry, ['upload', 'complete']),
|
|
357
|
+
{
|
|
358
|
+
sha256,
|
|
359
|
+
url: blobUrl,
|
|
360
|
+
sizeBytes: bytes.length,
|
|
361
|
+
pathname,
|
|
362
|
+
},
|
|
363
|
+
{ fetchImpl, headers: authHeaders }
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const verifyResponse = await postJson(
|
|
367
|
+
registryApiUrl(registry, ['publish', 'verify']),
|
|
368
|
+
{ sha256 },
|
|
369
|
+
{
|
|
370
|
+
fetchImpl,
|
|
371
|
+
headers: authHeaders,
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
const jobId = extractId(verifyResponse, ['jobId', 'id', 'job']);
|
|
375
|
+
if (!jobId)
|
|
376
|
+
throw new Error('Hub verify response did not include a publish job id.');
|
|
377
|
+
const job = await waitForPublishJob(registry, jobId, {
|
|
378
|
+
fetchImpl,
|
|
379
|
+
headers: authHeaders,
|
|
380
|
+
sleep,
|
|
381
|
+
pollIntervalMs,
|
|
382
|
+
maxPollAttempts,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const draftPayload = {
|
|
386
|
+
sha256,
|
|
387
|
+
slug: flags.slug,
|
|
388
|
+
version: flags.version,
|
|
389
|
+
license: flags.license,
|
|
390
|
+
attested: true,
|
|
391
|
+
...(flags.readme !== undefined ? { readme: flags.readme } : {}),
|
|
392
|
+
...(flags.sources !== undefined ? { sources: flags.sources } : {}),
|
|
393
|
+
...(flags.intendedUse !== undefined
|
|
394
|
+
? { intendedUse: flags.intendedUse }
|
|
395
|
+
: {}),
|
|
396
|
+
};
|
|
397
|
+
const draftResponse = await postJson(
|
|
398
|
+
registryApiUrl(registry, ['publish', 'drafts']),
|
|
399
|
+
draftPayload,
|
|
400
|
+
{
|
|
401
|
+
fetchImpl,
|
|
402
|
+
headers: authHeaders,
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
const draftId = extractId(draftResponse, ['draftId', 'id', 'draft']);
|
|
406
|
+
if (!draftId)
|
|
407
|
+
throw new Error('Hub draft response did not include a draft id.');
|
|
408
|
+
|
|
409
|
+
const releaseResponse = await postJson(
|
|
410
|
+
registryApiUrl(registry, ['publish', 'drafts', draftId, 'release']),
|
|
411
|
+
undefined,
|
|
412
|
+
{
|
|
413
|
+
fetchImpl,
|
|
414
|
+
headers: authHeaders,
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
const name = `${publisher ? `${publisher}/` : ''}${flags.slug}`;
|
|
418
|
+
const result = {
|
|
419
|
+
registry,
|
|
420
|
+
...(publisher ? { publisher } : {}),
|
|
421
|
+
name,
|
|
422
|
+
slug: flags.slug,
|
|
423
|
+
version: flags.version,
|
|
424
|
+
sha256,
|
|
425
|
+
sizeBytes: bytes.length,
|
|
426
|
+
pathname,
|
|
427
|
+
url: blobUrl,
|
|
428
|
+
jobId: String(jobId),
|
|
429
|
+
draftId: String(draftId),
|
|
430
|
+
verification: job,
|
|
431
|
+
release: releaseResponse,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (flags.json) print(JSON.stringify(result, null, 2));
|
|
435
|
+
else {
|
|
436
|
+
print(`published ${name}@${flags.version}`);
|
|
437
|
+
print(`sha256 ${sha256}`);
|
|
438
|
+
print(`url ${blobUrl}`);
|
|
439
|
+
print(`job ${jobId}`);
|
|
440
|
+
}
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export async function runHubYank(
|
|
445
|
+
args,
|
|
446
|
+
{
|
|
447
|
+
env = process.env,
|
|
448
|
+
fetchImpl = globalThis.fetch,
|
|
449
|
+
homeDir,
|
|
450
|
+
print = console.log,
|
|
451
|
+
} = {}
|
|
452
|
+
) {
|
|
453
|
+
const { spec, flags } = parseYankArgs(args);
|
|
454
|
+
const credentials = await loadCredentials({ env, homeDir });
|
|
455
|
+
const registry = resolveCredentialRegistry(
|
|
456
|
+
flags.registry,
|
|
457
|
+
credentials.registry
|
|
458
|
+
);
|
|
459
|
+
const response = await postJson(
|
|
460
|
+
registryApiUrl(registry, [
|
|
461
|
+
'packs',
|
|
462
|
+
spec.publisher,
|
|
463
|
+
spec.slug,
|
|
464
|
+
spec.version,
|
|
465
|
+
'yank',
|
|
466
|
+
]),
|
|
467
|
+
undefined,
|
|
468
|
+
{
|
|
469
|
+
fetchImpl,
|
|
470
|
+
headers: authorizationHeaders(credentials.token),
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
const result = {
|
|
474
|
+
registry,
|
|
475
|
+
name: spec.name,
|
|
476
|
+
version: spec.version,
|
|
477
|
+
response,
|
|
478
|
+
};
|
|
479
|
+
if (flags.json) print(JSON.stringify(result, null, 2));
|
|
480
|
+
else print(`yanked ${spec.name}@${spec.version}`);
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function parsePublishArgs(args) {
|
|
485
|
+
const positional = [];
|
|
486
|
+
const flags = {};
|
|
487
|
+
const valueFlags = new Map([
|
|
488
|
+
['--slug', 'slug'],
|
|
489
|
+
['--version', 'version'],
|
|
490
|
+
['--license', 'license'],
|
|
491
|
+
['--publisher', 'publisher'],
|
|
492
|
+
['--readme', 'readme'],
|
|
493
|
+
['--sources', 'sources'],
|
|
494
|
+
['--intended-use', 'intendedUse'],
|
|
495
|
+
['--registry', 'registry'],
|
|
496
|
+
]);
|
|
497
|
+
for (let i = 0; i < args.length; i++) {
|
|
498
|
+
const arg = args[i];
|
|
499
|
+
if (arg === '--json') {
|
|
500
|
+
flags.json = true;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (valueFlags.has(arg)) {
|
|
504
|
+
const value = args[++i];
|
|
505
|
+
if (value === undefined || value.startsWith('--'))
|
|
506
|
+
throw new Error(`Missing value for ${arg}`);
|
|
507
|
+
flags[valueFlags.get(arg)] = value;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (arg.startsWith('--'))
|
|
511
|
+
throw new Error(`Unknown flag for publish: ${arg}`);
|
|
512
|
+
positional.push(arg);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (
|
|
516
|
+
positional.length !== 1 ||
|
|
517
|
+
!flags.slug ||
|
|
518
|
+
!flags.version ||
|
|
519
|
+
!flags.license
|
|
520
|
+
) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
'Usage: knolo publish <pack.knolo> --slug <slug> --version <version> --license <SPDX> [--publisher <handle>] [--readme <text>] [--sources <text>] [--intended-use <text>] [--json] [--registry <url>]'
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
if (!/^[a-z0-9-]+$/.test(flags.slug))
|
|
526
|
+
throw new Error(
|
|
527
|
+
'Publish slug must contain lowercase letters, numbers, and hyphens.'
|
|
528
|
+
);
|
|
529
|
+
if (!flags.version.trim() || flags.version.includes('@'))
|
|
530
|
+
throw new Error('Publish version must be a non-empty version without @.');
|
|
531
|
+
if (!flags.license.trim()) throw new Error('Publish license is required.');
|
|
532
|
+
if (flags.publisher && !/^[a-z0-9-]+$/.test(flags.publisher))
|
|
533
|
+
throw new Error(
|
|
534
|
+
'Publish publisher must contain lowercase letters, numbers, and hyphens.'
|
|
535
|
+
);
|
|
536
|
+
return { packPath: positional[0], flags };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function parseYankArgs(args) {
|
|
540
|
+
const positional = [];
|
|
541
|
+
const flags = {};
|
|
542
|
+
for (let i = 0; i < args.length; i++) {
|
|
543
|
+
const arg = args[i];
|
|
544
|
+
if (arg === '--json') {
|
|
545
|
+
flags.json = true;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (arg === '--registry') {
|
|
549
|
+
const value = args[++i];
|
|
550
|
+
if (value === undefined || value.startsWith('--'))
|
|
551
|
+
throw new Error('Missing value for --registry');
|
|
552
|
+
flags.registry = value;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (arg.startsWith('--')) throw new Error(`Unknown flag for yank: ${arg}`);
|
|
556
|
+
positional.push(arg);
|
|
557
|
+
}
|
|
558
|
+
if (positional.length !== 1)
|
|
559
|
+
throw new Error(
|
|
560
|
+
'Usage: knolo yank <publisher>/<slug>@<version> [--json] [--registry <url>]'
|
|
561
|
+
);
|
|
562
|
+
const spec = parsePackSpec(positional[0]);
|
|
563
|
+
if (spec.version === 'latest')
|
|
564
|
+
throw new Error(
|
|
565
|
+
'Yank requires an exact version: <publisher>/<slug>@<version>.'
|
|
566
|
+
);
|
|
567
|
+
return { spec, flags };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function authorizationHeaders(token) {
|
|
571
|
+
return { Authorization: `Bearer ${token}` };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function resolveCredentialRegistry(requestedRegistry, storedRegistry) {
|
|
575
|
+
const stored = normalizeRegistryUrl(storedRegistry);
|
|
576
|
+
if (!requestedRegistry) return stored;
|
|
577
|
+
const requested = normalizeRegistryUrl(requestedRegistry);
|
|
578
|
+
if (requested !== stored) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Stored credentials belong to ${stored}; refusing to send them to ${requested}. Log in to the requested registry first.`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
return requested;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function hubApiUrl(registry, segments) {
|
|
587
|
+
const url = new URL(registry);
|
|
588
|
+
url.pathname = `/api/${segments.map((segment) => encodeURIComponent(String(segment))).join('/')}`;
|
|
589
|
+
return url;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function createSha256(bytes) {
|
|
593
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function packsReadWriteToken(env) {
|
|
597
|
+
const token =
|
|
598
|
+
typeof env?.PACKS_READ_WRITE_TOKEN === 'string'
|
|
599
|
+
? env.PACKS_READ_WRITE_TOKEN.trim()
|
|
600
|
+
: '';
|
|
601
|
+
if (!token)
|
|
602
|
+
throw new Error(
|
|
603
|
+
'PACKS_READ_WRITE_TOKEN is required for public Blob uploads.'
|
|
604
|
+
);
|
|
605
|
+
return token;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function extractPublisherHandle(account) {
|
|
609
|
+
const candidates = [
|
|
610
|
+
account?.publisherHandle,
|
|
611
|
+
account?.publisher?.handle,
|
|
612
|
+
account?.publisher?.slug,
|
|
613
|
+
account?.handle,
|
|
614
|
+
account?.account?.publisherHandle,
|
|
615
|
+
account?.account?.publisher?.handle,
|
|
616
|
+
account?.user?.publisherHandle,
|
|
617
|
+
account?.publishers?.[0]?.handle,
|
|
618
|
+
account?.publishers?.[0]?.slug,
|
|
619
|
+
typeof account?.publisher === 'string' ? account.publisher : undefined,
|
|
620
|
+
];
|
|
621
|
+
return candidates
|
|
622
|
+
.find((value) => typeof value === 'string' && value.trim())
|
|
623
|
+
?.trim();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function assertPublicBlobUrl(value, label) {
|
|
627
|
+
if (typeof value !== 'string' || value.includes('.private.')) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Refusing private Blob URL for ${label}; Hub verification requires a public Blob URL.`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
let url;
|
|
633
|
+
try {
|
|
634
|
+
url = new URL(value);
|
|
635
|
+
} catch {
|
|
636
|
+
throw new Error(`Hub ${label} URL is invalid.`);
|
|
637
|
+
}
|
|
638
|
+
const hostname = url.hostname.toLowerCase();
|
|
639
|
+
if (url.protocol !== 'https:')
|
|
640
|
+
throw new Error(`Hub ${label} URL must use HTTPS.`);
|
|
641
|
+
if (
|
|
642
|
+
hostname.includes('.private.') ||
|
|
643
|
+
hostname.startsWith('private.') ||
|
|
644
|
+
isPrivateOrLocalIp(hostname)
|
|
645
|
+
) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`Refusing private Blob URL for ${label}; Hub verification requires a public Blob URL.`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (isLocalOnlyHostname(hostname)) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`Refusing local-only Blob URL for ${label}; Hub verification requires a public Blob URL.`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
return url;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function isLocalOnlyHostname(hostname) {
|
|
659
|
+
const normalized = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
660
|
+
return (
|
|
661
|
+
LOCAL_ONLY_HOSTNAMES.has(normalized) ||
|
|
662
|
+
normalized.endsWith('.localhost') ||
|
|
663
|
+
normalized.endsWith('.local') ||
|
|
664
|
+
normalized.endsWith('.internal') ||
|
|
665
|
+
normalized.endsWith('.home.arpa')
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function isPrivateOrLocalIp(hostname) {
|
|
670
|
+
const normalized = hostname.replace(/^\[|\]$/g, '');
|
|
671
|
+
const version = isIP(normalized);
|
|
672
|
+
if (version === 4) return isPrivateIpv4(normalized);
|
|
673
|
+
if (version !== 6) return false;
|
|
674
|
+
|
|
675
|
+
const words = parseIpv6Words(normalized);
|
|
676
|
+
if (!words) return false;
|
|
677
|
+
const first = words[0];
|
|
678
|
+
const allZero = words.every((word) => word === 0);
|
|
679
|
+
const loopback =
|
|
680
|
+
words.slice(0, 7).every((word) => word === 0) && words[7] === 1;
|
|
681
|
+
const uniqueLocal = (first & 0xfe00) === 0xfc00;
|
|
682
|
+
const linkLocal = (first & 0xffc0) === 0xfe80;
|
|
683
|
+
const mappedIpv4 =
|
|
684
|
+
words.slice(0, 5).every((word) => word === 0) && words[5] === 0xffff;
|
|
685
|
+
const mappedAddress = mappedIpv4
|
|
686
|
+
? `${words[6] >> 8}.${words[6] & 0xff}.${words[7] >> 8}.${words[7] & 0xff}`
|
|
687
|
+
: undefined;
|
|
688
|
+
return (
|
|
689
|
+
allZero ||
|
|
690
|
+
loopback ||
|
|
691
|
+
uniqueLocal ||
|
|
692
|
+
linkLocal ||
|
|
693
|
+
(mappedAddress ? isPrivateIpv4(mappedAddress) : false)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function isPrivateIpv4(value) {
|
|
698
|
+
const octets = value.split('.').map(Number);
|
|
699
|
+
if (
|
|
700
|
+
octets.length !== 4 ||
|
|
701
|
+
octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)
|
|
702
|
+
)
|
|
703
|
+
return false;
|
|
704
|
+
const [first, second] = octets;
|
|
705
|
+
return (
|
|
706
|
+
first === 0 ||
|
|
707
|
+
first === 10 ||
|
|
708
|
+
first === 127 ||
|
|
709
|
+
(first === 100 && second >= 64 && second <= 127) ||
|
|
710
|
+
(first === 169 && second === 254) ||
|
|
711
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
712
|
+
(first === 192 && (second === 0 || second === 168)) ||
|
|
713
|
+
(first === 198 && (second === 18 || second === 19)) ||
|
|
714
|
+
first >= 224
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function parseIpv6Words(value) {
|
|
719
|
+
const sections = value.toLowerCase().split('::');
|
|
720
|
+
if (sections.length > 2) return undefined;
|
|
721
|
+
const left = parseIpv6Section(sections[0]);
|
|
722
|
+
const right = sections.length === 2 ? parseIpv6Section(sections[1]) : [];
|
|
723
|
+
if (!left || !right) return undefined;
|
|
724
|
+
const zeroCount = sections.length === 2 ? 8 - left.length - right.length : 0;
|
|
725
|
+
if (
|
|
726
|
+
zeroCount < (sections.length === 2 ? 1 : 0) ||
|
|
727
|
+
left.length + right.length + zeroCount !== 8
|
|
728
|
+
)
|
|
729
|
+
return undefined;
|
|
730
|
+
return [...left, ...new Array(zeroCount).fill(0), ...right];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function parseIpv6Section(section) {
|
|
734
|
+
if (!section) return [];
|
|
735
|
+
const words = [];
|
|
736
|
+
for (const part of section.split(':')) {
|
|
737
|
+
if (part.includes('.')) {
|
|
738
|
+
const octets = part.split('.').map(Number);
|
|
739
|
+
if (
|
|
740
|
+
octets.length !== 4 ||
|
|
741
|
+
octets.some(
|
|
742
|
+
(octet) => !Number.isInteger(octet) || octet < 0 || octet > 255
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
return undefined;
|
|
746
|
+
words.push((octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]);
|
|
747
|
+
} else {
|
|
748
|
+
if (!/^[0-9a-f]{1,4}$/.test(part)) return undefined;
|
|
749
|
+
words.push(Number.parseInt(part, 16));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return words;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function extractId(body, keys) {
|
|
756
|
+
for (const key of keys) {
|
|
757
|
+
const value = body?.[key];
|
|
758
|
+
if (typeof value === 'string' && value) return value;
|
|
759
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
|
|
760
|
+
if (value && typeof value === 'object') {
|
|
761
|
+
if (typeof value.id === 'string' && value.id) return value.id;
|
|
762
|
+
if (typeof value.id === 'number' && Number.isSafeInteger(value.id))
|
|
763
|
+
return value.id;
|
|
764
|
+
if (typeof value.jobId === 'string' && value.jobId) return value.jobId;
|
|
765
|
+
if (typeof value.draftId === 'string' && value.draftId)
|
|
766
|
+
return value.draftId;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function waitForPublishJob(
|
|
773
|
+
registry,
|
|
774
|
+
jobId,
|
|
775
|
+
{
|
|
776
|
+
fetchImpl,
|
|
777
|
+
headers,
|
|
778
|
+
sleep: sleepImpl = wait,
|
|
779
|
+
pollIntervalMs = 1000,
|
|
780
|
+
maxPollAttempts = 120,
|
|
781
|
+
} = {}
|
|
782
|
+
) {
|
|
783
|
+
if (!Number.isSafeInteger(maxPollAttempts) || maxPollAttempts < 1)
|
|
784
|
+
throw new Error('maxPollAttempts must be a positive integer.');
|
|
785
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 0)
|
|
786
|
+
throw new Error('pollIntervalMs must be a non-negative number.');
|
|
787
|
+
|
|
788
|
+
for (let attempt = 0; attempt < maxPollAttempts; attempt++) {
|
|
789
|
+
if (attempt > 0 && pollIntervalMs > 0) await sleepImpl(pollIntervalMs);
|
|
790
|
+
const body = await getJson(
|
|
791
|
+
registryApiUrl(registry, ['publish', 'jobs', jobId]),
|
|
792
|
+
{ fetchImpl, headers }
|
|
793
|
+
);
|
|
794
|
+
const job = body?.job && typeof body.job === 'object' ? body.job : body;
|
|
795
|
+
const status = String(job?.status || job?.state || '').toLowerCase();
|
|
796
|
+
if (
|
|
797
|
+
[
|
|
798
|
+
'passed',
|
|
799
|
+
'pass',
|
|
800
|
+
'complete',
|
|
801
|
+
'completed',
|
|
802
|
+
'succeeded',
|
|
803
|
+
'success',
|
|
804
|
+
].includes(status)
|
|
805
|
+
)
|
|
806
|
+
return job;
|
|
807
|
+
if (['failed', 'failure', 'error', 'rejected'].includes(status)) {
|
|
808
|
+
const detail = job?.error || job?.message || body?.error;
|
|
809
|
+
throw new Error(
|
|
810
|
+
`Hub publish verification failed${detail ? `: ${detail}` : '.'}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
throw new Error(
|
|
816
|
+
`Timed out waiting for Hub publish verification job ${jobId}.`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function wait(milliseconds) {
|
|
821
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
163
822
|
}
|
|
164
823
|
|
|
165
824
|
export function parseAddArgs(args) {
|
|
@@ -173,14 +832,18 @@ export function parseAddArgs(args) {
|
|
|
173
832
|
}
|
|
174
833
|
if (['--out', '--registry'].includes(arg)) {
|
|
175
834
|
const value = args[++i];
|
|
176
|
-
if (value === undefined || value.startsWith('--'))
|
|
835
|
+
if (value === undefined || value.startsWith('--'))
|
|
836
|
+
throw new Error(`Missing value for ${arg}`);
|
|
177
837
|
flags[arg.slice(2)] = value;
|
|
178
838
|
continue;
|
|
179
839
|
}
|
|
180
840
|
if (arg.startsWith('--')) throw new Error(`Unknown flag for add: ${arg}`);
|
|
181
841
|
positional.push(arg);
|
|
182
842
|
}
|
|
183
|
-
if (positional.length !== 1)
|
|
843
|
+
if (positional.length !== 1)
|
|
844
|
+
throw new Error(
|
|
845
|
+
'Usage: knolo add <publisher>/<slug>[@<version>] [--out <path>] [--force] [--json] [--registry <url>]'
|
|
846
|
+
);
|
|
184
847
|
return { spec: parsePackSpec(positional[0]), flags };
|
|
185
848
|
}
|
|
186
849
|
|
|
@@ -194,12 +857,15 @@ export function parseLoginArgs(args) {
|
|
|
194
857
|
}
|
|
195
858
|
if (arg === '--token' || arg === '--registry') {
|
|
196
859
|
const value = args[++i];
|
|
197
|
-
if (value === undefined || value.startsWith('--'))
|
|
860
|
+
if (value === undefined || value.startsWith('--'))
|
|
861
|
+
throw new Error(`Missing value for ${arg}`);
|
|
198
862
|
flags[arg.slice(2)] = value;
|
|
199
863
|
continue;
|
|
200
864
|
}
|
|
201
865
|
if (arg.startsWith('--')) throw new Error(`Unknown flag for login: ${arg}`);
|
|
202
|
-
throw new Error(
|
|
866
|
+
throw new Error(
|
|
867
|
+
'Usage: knolo login [--token kno_…] [--stdin] [--registry <url>]'
|
|
868
|
+
);
|
|
203
869
|
}
|
|
204
870
|
return { flags };
|
|
205
871
|
}
|
|
@@ -237,17 +903,25 @@ export function parseSearchArgs(args) {
|
|
|
237
903
|
}
|
|
238
904
|
if (['--format', '--license', '--registry'].includes(arg)) {
|
|
239
905
|
const value = args[++i];
|
|
240
|
-
if (value === undefined || value.startsWith('--'))
|
|
906
|
+
if (value === undefined || value.startsWith('--'))
|
|
907
|
+
throw new Error(`Missing value for ${arg}`);
|
|
241
908
|
flags[arg.slice(2)] = value;
|
|
242
909
|
continue;
|
|
243
910
|
}
|
|
244
|
-
if (arg.startsWith('--'))
|
|
911
|
+
if (arg.startsWith('--'))
|
|
912
|
+
throw new Error(`Unknown flag for search: ${arg}`);
|
|
245
913
|
positional.push(arg);
|
|
246
914
|
}
|
|
247
915
|
|
|
248
916
|
const query = positional.join(' ').trim();
|
|
249
|
-
if (!query)
|
|
250
|
-
|
|
917
|
+
if (!query)
|
|
918
|
+
throw new Error(
|
|
919
|
+
'Usage: knolo search <query> [--format V4|V5] [--license <id>] [--official] [--agents] [--json] [--registry <url>]'
|
|
920
|
+
);
|
|
921
|
+
if (
|
|
922
|
+
flags.format &&
|
|
923
|
+
!['V4', 'V5'].includes(String(flags.format).toUpperCase())
|
|
924
|
+
) {
|
|
251
925
|
throw new Error('--format must be V4 or V5.');
|
|
252
926
|
}
|
|
253
927
|
if (flags.format) flags.format = String(flags.format).toUpperCase();
|
|
@@ -265,14 +939,18 @@ export function parseInfoArgs(args) {
|
|
|
265
939
|
}
|
|
266
940
|
if (arg === '--registry') {
|
|
267
941
|
const value = args[++i];
|
|
268
|
-
if (value === undefined || value.startsWith('--'))
|
|
942
|
+
if (value === undefined || value.startsWith('--'))
|
|
943
|
+
throw new Error('Missing value for --registry');
|
|
269
944
|
flags.registry = value;
|
|
270
945
|
continue;
|
|
271
946
|
}
|
|
272
947
|
if (arg.startsWith('--')) throw new Error(`Unknown flag for info: ${arg}`);
|
|
273
948
|
positional.push(arg);
|
|
274
949
|
}
|
|
275
|
-
if (positional.length !== 1)
|
|
950
|
+
if (positional.length !== 1)
|
|
951
|
+
throw new Error(
|
|
952
|
+
'Usage: knolo info <publisher>/<slug> [--json] [--registry <url>]'
|
|
953
|
+
);
|
|
276
954
|
return { positional, flags };
|
|
277
955
|
}
|
|
278
956
|
|
|
@@ -283,7 +961,13 @@ function validatePacksResponse(body) {
|
|
|
283
961
|
}
|
|
284
962
|
|
|
285
963
|
function validatePackInfoResponse(body) {
|
|
286
|
-
if (
|
|
964
|
+
if (
|
|
965
|
+
!body ||
|
|
966
|
+
typeof body !== 'object' ||
|
|
967
|
+
typeof body.name !== 'string' ||
|
|
968
|
+
!body.pack ||
|
|
969
|
+
typeof body.pack !== 'object'
|
|
970
|
+
) {
|
|
287
971
|
throw new Error('Registry returned an invalid pack response.');
|
|
288
972
|
}
|
|
289
973
|
}
|
|
@@ -294,33 +978,100 @@ async function validateKnowledgeImage(core, bytes) {
|
|
|
294
978
|
const verification = core.verifyKnowledgeImageV5(bytes);
|
|
295
979
|
return { format: 'V5', stateRoot: verification.stateRoot };
|
|
296
980
|
} catch (error) {
|
|
297
|
-
throw new Error(
|
|
981
|
+
throw new Error(
|
|
982
|
+
`invalid V5 Knowledge Image: ${error instanceof Error ? error.message : String(error)}`
|
|
983
|
+
);
|
|
298
984
|
}
|
|
299
985
|
}
|
|
300
986
|
|
|
301
|
-
const buffer = bytes.buffer.slice(
|
|
987
|
+
const buffer = bytes.buffer.slice(
|
|
988
|
+
bytes.byteOffset,
|
|
989
|
+
bytes.byteOffset + bytes.byteLength
|
|
990
|
+
);
|
|
302
991
|
if (core.isPackV4?.(buffer)) {
|
|
303
992
|
try {
|
|
304
993
|
await core.mountPack({ src: bytes });
|
|
305
994
|
return { format: 'V4' };
|
|
306
995
|
} catch (error) {
|
|
307
|
-
throw new Error(
|
|
996
|
+
throw new Error(
|
|
997
|
+
`invalid V4 Knowledge Image: ${error instanceof Error ? error.message : String(error)}`
|
|
998
|
+
);
|
|
308
999
|
}
|
|
309
1000
|
}
|
|
310
1001
|
throw new Error('not a Knowledge Image');
|
|
311
1002
|
}
|
|
312
1003
|
|
|
313
|
-
async function
|
|
1004
|
+
async function stageArtifactCopy(sourcePath, outputPath, cwd) {
|
|
314
1005
|
const targetPath = path.resolve(cwd, outputPath);
|
|
315
|
-
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
316
1006
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
1007
|
+
const tempPath = `${targetPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
317
1008
|
try {
|
|
318
1009
|
await copyFile(sourcePath, tempPath);
|
|
319
|
-
await
|
|
320
|
-
|
|
1010
|
+
await chmod(tempPath, 0o644);
|
|
1011
|
+
return { targetPath, tempPath };
|
|
1012
|
+
} catch (error) {
|
|
321
1013
|
await unlinkIfPresent(tempPath);
|
|
1014
|
+
throw error;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function commitStagedFiles(entries) {
|
|
1019
|
+
const targets = new Set();
|
|
1020
|
+
const records = entries.map((entry) => {
|
|
1021
|
+
if (targets.has(entry.targetPath))
|
|
1022
|
+
throw new Error(`Install paths must be unique: ${entry.targetPath}`);
|
|
1023
|
+
targets.add(entry.targetPath);
|
|
1024
|
+
return {
|
|
1025
|
+
...entry,
|
|
1026
|
+
backupPath: `${entry.targetPath}.backup-${process.pid}-${randomUUID()}`,
|
|
1027
|
+
hadOriginal: false,
|
|
1028
|
+
committed: false,
|
|
1029
|
+
};
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
try {
|
|
1033
|
+
for (const record of records) {
|
|
1034
|
+
const existing = await statIfPresent(record.targetPath);
|
|
1035
|
+
if (existing?.isDirectory())
|
|
1036
|
+
throw new Error(`Cannot replace directory at ${record.targetPath}.`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
for (const record of records) {
|
|
1040
|
+
if (await pathExists(record.targetPath)) {
|
|
1041
|
+
record.hadOriginal = true;
|
|
1042
|
+
await rename(record.targetPath, record.backupPath);
|
|
1043
|
+
}
|
|
1044
|
+
await rename(record.tempPath, record.targetPath);
|
|
1045
|
+
record.committed = true;
|
|
1046
|
+
}
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
for (const record of [...records].reverse()) {
|
|
1049
|
+
if (record.committed) await unlinkIfPresent(record.targetPath);
|
|
1050
|
+
if (record.hadOriginal && (await pathExists(record.backupPath))) {
|
|
1051
|
+
await unlinkIfPresent(record.targetPath);
|
|
1052
|
+
await rename(record.backupPath, record.targetPath);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
for (const record of records) {
|
|
1059
|
+
if (record.hadOriginal)
|
|
1060
|
+
await unlinkIfPresent(record.backupPath).catch(() => {});
|
|
322
1061
|
}
|
|
323
|
-
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function statIfPresent(filePath) {
|
|
1065
|
+
try {
|
|
1066
|
+
return await lstat(filePath);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error?.code === 'ENOENT') return undefined;
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function pathExists(filePath) {
|
|
1074
|
+
return Boolean(await statIfPresent(filePath));
|
|
324
1075
|
}
|
|
325
1076
|
|
|
326
1077
|
async function unlinkIfPresent(filePath) {
|
|
@@ -334,24 +1085,36 @@ async function unlinkIfPresent(filePath) {
|
|
|
334
1085
|
export function renderSearchTable(packs) {
|
|
335
1086
|
if (!packs.length) return 'No packs found.';
|
|
336
1087
|
|
|
337
|
-
const rows = packs.map((pack) =>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
1088
|
+
const rows = packs.map((pack) =>
|
|
1089
|
+
[
|
|
1090
|
+
pack.id || pack.name || `${pack.publisher || ''}/${pack.slug || ''}`,
|
|
1091
|
+
pack.version ?? '',
|
|
1092
|
+
pack.format ?? '',
|
|
1093
|
+
pack.license ?? '',
|
|
1094
|
+
pack.pulls ?? 0,
|
|
1095
|
+
pack.description ?? '',
|
|
1096
|
+
].map((value) => String(value))
|
|
1097
|
+
);
|
|
345
1098
|
const widths = [24, 9, 6, 11, 8];
|
|
346
|
-
const header = [
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1099
|
+
const header = [
|
|
1100
|
+
'NAME',
|
|
1101
|
+
'VERSION',
|
|
1102
|
+
'FORMAT',
|
|
1103
|
+
'LICENSE',
|
|
1104
|
+
'PULLS',
|
|
1105
|
+
'DESCRIPTION',
|
|
1106
|
+
];
|
|
1107
|
+
const formatRow = (row) =>
|
|
1108
|
+
[
|
|
1109
|
+
pad(truncate(row[0], widths[0]), widths[0]),
|
|
1110
|
+
pad(truncate(row[1], widths[1]), widths[1]),
|
|
1111
|
+
pad(truncate(row[2], widths[2]), widths[2]),
|
|
1112
|
+
pad(truncate(row[3], widths[3]), widths[3]),
|
|
1113
|
+
pad(truncate(row[4], widths[4]), widths[4]),
|
|
1114
|
+
truncate(row[5], 72),
|
|
1115
|
+
]
|
|
1116
|
+
.join(' ')
|
|
1117
|
+
.trimEnd();
|
|
355
1118
|
|
|
356
1119
|
return [formatRow(header), ...rows.map(formatRow)].join('\n');
|
|
357
1120
|
}
|