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