@knolo/cli 5.2.1 → 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 +35 -29
- package/bin/registry/commands.mjs +587 -208
- package/bin/registry/http.mjs +65 -47
- package/package.json +3 -2
|
@@ -1,19 +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';
|
|
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
|
-
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';
|
|
7
26
|
import { RegistryError } from './errors.mjs';
|
|
8
|
-
import { getJson, postJson
|
|
9
|
-
import {
|
|
27
|
+
import { getJson, postJson } from './http.mjs';
|
|
28
|
+
import {
|
|
29
|
+
cachePath,
|
|
30
|
+
stageLockfileUpdate,
|
|
31
|
+
validateLockfileUpdate,
|
|
32
|
+
} from './lockfile.mjs';
|
|
10
33
|
import { validatePackManifest } from './manifest.mjs';
|
|
11
34
|
import { parsePackName, parsePackSpec } from './specs.mjs';
|
|
12
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
loadCredentials,
|
|
37
|
+
promptForToken,
|
|
38
|
+
removeCredentials,
|
|
39
|
+
saveCredentials,
|
|
40
|
+
validateToken,
|
|
41
|
+
} from './credentials.mjs';
|
|
13
42
|
|
|
14
43
|
const PACK_SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
]);
|
|
53
|
+
|
|
54
|
+
export async function runHubSearch(
|
|
55
|
+
args,
|
|
56
|
+
{ env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}
|
|
57
|
+
) {
|
|
17
58
|
const { query, flags } = parseSearchArgs(args);
|
|
18
59
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
19
60
|
const url = registryApiUrl(registry, ['packs'], {
|
|
@@ -32,7 +73,10 @@ export async function runHubSearch(args, { env = process.env, fetchImpl = global
|
|
|
32
73
|
return { registry, url: String(url), body };
|
|
33
74
|
}
|
|
34
75
|
|
|
35
|
-
export async function runHubInfo(
|
|
76
|
+
export async function runHubInfo(
|
|
77
|
+
args,
|
|
78
|
+
{ env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}
|
|
79
|
+
) {
|
|
36
80
|
const { positional, flags } = parseInfoArgs(args);
|
|
37
81
|
const pack = parsePackName(positional[0]);
|
|
38
82
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
@@ -46,31 +90,52 @@ export async function runHubInfo(args, { env = process.env, fetchImpl = globalTh
|
|
|
46
90
|
return { registry, url: String(url), body };
|
|
47
91
|
}
|
|
48
92
|
|
|
49
|
-
export async function runHubAdd(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
) {
|
|
58
105
|
const { spec, flags } = parseAddArgs(args);
|
|
59
|
-
if (!core)
|
|
106
|
+
if (!core)
|
|
107
|
+
throw new Error('Could not load @knolo/core for Hub artifact validation.');
|
|
60
108
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
61
|
-
const manifestUrl = registryApiUrl(registry, [
|
|
109
|
+
const manifestUrl = registryApiUrl(registry, [
|
|
110
|
+
'packs',
|
|
111
|
+
spec.publisher,
|
|
112
|
+
spec.slug,
|
|
113
|
+
spec.version,
|
|
114
|
+
]);
|
|
62
115
|
|
|
63
116
|
let manifest;
|
|
64
117
|
let yanked = false;
|
|
65
118
|
try {
|
|
66
119
|
const body = await getJson(manifestUrl, { fetchImpl });
|
|
67
|
-
manifest = validatePackManifest(body, {
|
|
120
|
+
manifest = validatePackManifest(body, {
|
|
121
|
+
expectedName: spec.name,
|
|
122
|
+
requestedVersion: spec.version,
|
|
123
|
+
});
|
|
68
124
|
} catch (error) {
|
|
69
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
error instanceof RegistryError &&
|
|
127
|
+
(error.status === 404 || error.code === 'not_found')
|
|
128
|
+
) {
|
|
70
129
|
throw new Error('pack version not found');
|
|
71
130
|
}
|
|
72
|
-
if (
|
|
73
|
-
|
|
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
|
+
});
|
|
74
139
|
yanked = true;
|
|
75
140
|
} else {
|
|
76
141
|
throw error;
|
|
@@ -78,10 +143,15 @@ export async function runHubAdd(args, {
|
|
|
78
143
|
}
|
|
79
144
|
|
|
80
145
|
if (manifest.yanked) yanked = true;
|
|
81
|
-
if (yanked && !flags.force)
|
|
82
|
-
|
|
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
|
+
);
|
|
83
152
|
if (!manifest.url) throw new Error('artifact bytes are not stored yet');
|
|
84
|
-
if (!PACK_SHA256_PATTERN.test(manifest.sha256))
|
|
153
|
+
if (!PACK_SHA256_PATTERN.test(manifest.sha256))
|
|
154
|
+
throw new Error('manifest sha256 is invalid');
|
|
85
155
|
|
|
86
156
|
const artifactHome = homeDir || homedir();
|
|
87
157
|
await validateLockfileUpdate({ cwd, registry, manifest, force: flags.force });
|
|
@@ -95,29 +165,56 @@ export async function runHubAdd(args, {
|
|
|
95
165
|
const stagedFiles = [];
|
|
96
166
|
try {
|
|
97
167
|
if (download.sha256 !== manifest.sha256) {
|
|
98
|
-
throw new Error(
|
|
168
|
+
throw new Error(
|
|
169
|
+
`artifact sha256 mismatch: expected ${manifest.sha256}, got ${download.sha256}`
|
|
170
|
+
);
|
|
99
171
|
}
|
|
100
172
|
const bytes = await readArtifact(download.tempPath);
|
|
101
173
|
const validation = await validateKnowledgeImage(core, bytes);
|
|
102
|
-
if (
|
|
103
|
-
|
|
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
|
+
);
|
|
104
181
|
}
|
|
105
182
|
if (validation.format === 'V5' && !manifest.stateRoot) {
|
|
106
183
|
throw new Error('V5 artifact requires manifest stateRoot.');
|
|
107
184
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
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
|
+
);
|
|
110
192
|
}
|
|
111
|
-
const stagedCachePath = await stageCachedArtifact(
|
|
193
|
+
const stagedCachePath = await stageCachedArtifact(
|
|
194
|
+
download.tempPath,
|
|
195
|
+
finalCachePath
|
|
196
|
+
);
|
|
112
197
|
stagedFiles.push({ targetPath: finalCachePath, tempPath: stagedCachePath });
|
|
113
198
|
let outputPath = finalCachePath;
|
|
114
199
|
if (flags.out) {
|
|
115
|
-
const stagedOutput = await stageArtifactCopy(
|
|
200
|
+
const stagedOutput = await stageArtifactCopy(
|
|
201
|
+
download.tempPath,
|
|
202
|
+
flags.out,
|
|
203
|
+
cwd
|
|
204
|
+
);
|
|
116
205
|
stagedFiles.push(stagedOutput);
|
|
117
206
|
outputPath = stagedOutput.targetPath;
|
|
118
207
|
}
|
|
119
|
-
const stagedLock = await stageLockfileUpdate({
|
|
120
|
-
|
|
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
|
+
});
|
|
121
218
|
await commitStagedFiles(stagedFiles);
|
|
122
219
|
const lock = { path: stagedLock.path, entry: stagedLock.entry };
|
|
123
220
|
const result = { ...manifest, path: outputPath, lockfile: lock.path };
|
|
@@ -131,21 +228,31 @@ export async function runHubAdd(args, {
|
|
|
131
228
|
return result;
|
|
132
229
|
} finally {
|
|
133
230
|
await unlinkIfPresent(download.tempPath);
|
|
134
|
-
await Promise.all(
|
|
231
|
+
await Promise.all(
|
|
232
|
+
stagedFiles.map(({ tempPath }) => unlinkIfPresent(tempPath))
|
|
233
|
+
);
|
|
135
234
|
}
|
|
136
235
|
}
|
|
137
236
|
|
|
138
|
-
export async function runHubLogin(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
) {
|
|
145
247
|
const { flags } = parseLoginArgs(args);
|
|
146
248
|
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
147
|
-
print(
|
|
148
|
-
|
|
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();
|
|
149
256
|
validateToken(token);
|
|
150
257
|
const saved = await saveCredentials({ registry, token, env, homeDir });
|
|
151
258
|
print(`logged in to ${saved.registry}`);
|
|
@@ -154,7 +261,10 @@ export async function runHubLogin(args, {
|
|
|
154
261
|
return saved;
|
|
155
262
|
}
|
|
156
263
|
|
|
157
|
-
export async function runHubWhoami(
|
|
264
|
+
export async function runHubWhoami(
|
|
265
|
+
args,
|
|
266
|
+
{ env = process.env, homeDir, print = console.log } = {}
|
|
267
|
+
) {
|
|
158
268
|
if (args.length) throw new Error('Usage: knolo whoami');
|
|
159
269
|
const credentials = await loadCredentials({ env, homeDir });
|
|
160
270
|
print(`registry ${credentials.registry}`);
|
|
@@ -162,69 +272,108 @@ export async function runHubWhoami(args, { env = process.env, homeDir, print = c
|
|
|
162
272
|
return { registry: credentials.registry, prefix: credentials.prefix };
|
|
163
273
|
}
|
|
164
274
|
|
|
165
|
-
export async function runHubLogout(
|
|
275
|
+
export async function runHubLogout(
|
|
276
|
+
args,
|
|
277
|
+
{ env = process.env, homeDir, print = console.log } = {}
|
|
278
|
+
) {
|
|
166
279
|
if (args.length) throw new Error('Usage: knolo logout');
|
|
167
280
|
const result = await removeCredentials({ env, homeDir });
|
|
168
281
|
print(result.removed ? 'logged out' : 'not logged in');
|
|
169
282
|
return result;
|
|
170
283
|
}
|
|
171
284
|
|
|
172
|
-
export async function runHubPublish(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
) {
|
|
182
299
|
const { packPath, flags } = parsePublishArgs(args);
|
|
183
300
|
const credentials = await loadCredentials({ env, homeDir });
|
|
184
|
-
const registry =
|
|
301
|
+
const registry = resolveCredentialRegistry(
|
|
302
|
+
flags.registry,
|
|
303
|
+
credentials.registry
|
|
304
|
+
);
|
|
185
305
|
const authHeaders = authorizationHeaders(credentials.token);
|
|
186
306
|
|
|
187
307
|
// This request is deliberately first: it proves the stored dashboard token
|
|
188
308
|
// is a Hub Bearer credential before any upload or publish mutation starts.
|
|
189
|
-
const account = await getJson(registryApiUrl(registry, ['account']), {
|
|
309
|
+
const account = await getJson(registryApiUrl(registry, ['account']), {
|
|
310
|
+
fetchImpl,
|
|
311
|
+
headers: authHeaders,
|
|
312
|
+
});
|
|
190
313
|
const accountPublisher = extractPublisherHandle(account);
|
|
191
|
-
if (
|
|
192
|
-
|
|
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
|
+
);
|
|
193
322
|
}
|
|
194
323
|
const publisher = flags.publisher || accountPublisher;
|
|
195
324
|
|
|
196
325
|
const packFile = path.resolve(cwd, packPath);
|
|
197
326
|
const bytes = await readFile(packFile);
|
|
198
|
-
if (bytes.length > MAX_ARTIFACT_BYTES)
|
|
327
|
+
if (bytes.length > MAX_ARTIFACT_BYTES)
|
|
328
|
+
throw new Error('Artifact exceeds Hub’s 250 MB limit.');
|
|
199
329
|
const sha256 = createSha256(bytes);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
assertPublicBlobUrl(uploaded.url, 'uploaded artifact');
|
|
212
|
-
|
|
213
|
-
const completed = await postJson(hubApiUrl(registry, ['upload', 'complete']), {
|
|
214
|
-
sha256,
|
|
215
|
-
url: upload.url,
|
|
216
|
-
sizeBytes: bytes.length,
|
|
217
|
-
pathname: upload.pathname,
|
|
218
|
-
}, { fetchImpl, headers: authHeaders });
|
|
219
|
-
const blobUrl = extractBlobUrl(completed) || upload.url;
|
|
220
|
-
assertPublicBlobUrl(blobUrl, 'completed upload');
|
|
221
|
-
|
|
222
|
-
const verifyResponse = await postJson(registryApiUrl(registry, ['publish', 'verify']), { sha256 }, {
|
|
223
|
-
fetchImpl,
|
|
224
|
-
headers: authHeaders,
|
|
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),
|
|
225
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
|
+
);
|
|
226
374
|
const jobId = extractId(verifyResponse, ['jobId', 'id', 'job']);
|
|
227
|
-
if (!jobId)
|
|
375
|
+
if (!jobId)
|
|
376
|
+
throw new Error('Hub verify response did not include a publish job id.');
|
|
228
377
|
const job = await waitForPublishJob(registry, jobId, {
|
|
229
378
|
fetchImpl,
|
|
230
379
|
headers: authHeaders,
|
|
@@ -241,19 +390,30 @@ export async function runHubPublish(args, {
|
|
|
241
390
|
attested: true,
|
|
242
391
|
...(flags.readme !== undefined ? { readme: flags.readme } : {}),
|
|
243
392
|
...(flags.sources !== undefined ? { sources: flags.sources } : {}),
|
|
244
|
-
...(flags.intendedUse !== undefined
|
|
393
|
+
...(flags.intendedUse !== undefined
|
|
394
|
+
? { intendedUse: flags.intendedUse }
|
|
395
|
+
: {}),
|
|
245
396
|
};
|
|
246
|
-
const draftResponse = await postJson(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
397
|
+
const draftResponse = await postJson(
|
|
398
|
+
registryApiUrl(registry, ['publish', 'drafts']),
|
|
399
|
+
draftPayload,
|
|
400
|
+
{
|
|
401
|
+
fetchImpl,
|
|
402
|
+
headers: authHeaders,
|
|
403
|
+
}
|
|
404
|
+
);
|
|
250
405
|
const draftId = extractId(draftResponse, ['draftId', 'id', 'draft']);
|
|
251
|
-
if (!draftId)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
);
|
|
257
417
|
const name = `${publisher ? `${publisher}/` : ''}${flags.slug}`;
|
|
258
418
|
const result = {
|
|
259
419
|
registry,
|
|
@@ -263,7 +423,7 @@ export async function runHubPublish(args, {
|
|
|
263
423
|
version: flags.version,
|
|
264
424
|
sha256,
|
|
265
425
|
sizeBytes: bytes.length,
|
|
266
|
-
pathname
|
|
426
|
+
pathname,
|
|
267
427
|
url: blobUrl,
|
|
268
428
|
jobId: String(jobId),
|
|
269
429
|
draftId: String(draftId),
|
|
@@ -281,19 +441,35 @@ export async function runHubPublish(args, {
|
|
|
281
441
|
return result;
|
|
282
442
|
}
|
|
283
443
|
|
|
284
|
-
export async function runHubYank(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
444
|
+
export async function runHubYank(
|
|
445
|
+
args,
|
|
446
|
+
{
|
|
447
|
+
env = process.env,
|
|
448
|
+
fetchImpl = globalThis.fetch,
|
|
449
|
+
homeDir,
|
|
450
|
+
print = console.log,
|
|
451
|
+
} = {}
|
|
452
|
+
) {
|
|
290
453
|
const { spec, flags } = parseYankArgs(args);
|
|
291
454
|
const credentials = await loadCredentials({ env, homeDir });
|
|
292
|
-
const registry =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
);
|
|
297
473
|
const result = {
|
|
298
474
|
registry,
|
|
299
475
|
name: spec.name,
|
|
@@ -326,21 +502,37 @@ export function parsePublishArgs(args) {
|
|
|
326
502
|
}
|
|
327
503
|
if (valueFlags.has(arg)) {
|
|
328
504
|
const value = args[++i];
|
|
329
|
-
if (value === undefined || value.startsWith('--'))
|
|
505
|
+
if (value === undefined || value.startsWith('--'))
|
|
506
|
+
throw new Error(`Missing value for ${arg}`);
|
|
330
507
|
flags[valueFlags.get(arg)] = value;
|
|
331
508
|
continue;
|
|
332
509
|
}
|
|
333
|
-
if (arg.startsWith('--'))
|
|
510
|
+
if (arg.startsWith('--'))
|
|
511
|
+
throw new Error(`Unknown flag for publish: ${arg}`);
|
|
334
512
|
positional.push(arg);
|
|
335
513
|
}
|
|
336
514
|
|
|
337
|
-
if (
|
|
338
|
-
|
|
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
|
+
);
|
|
339
524
|
}
|
|
340
|
-
if (!/^[a-z0-9-]+$/.test(flags.slug))
|
|
341
|
-
|
|
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 @.');
|
|
342
531
|
if (!flags.license.trim()) throw new Error('Publish license is required.');
|
|
343
|
-
if (flags.publisher && !/^[a-z0-9-]+$/.test(flags.publisher))
|
|
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
|
+
);
|
|
344
536
|
return { packPath: positional[0], flags };
|
|
345
537
|
}
|
|
346
538
|
|
|
@@ -355,16 +547,23 @@ export function parseYankArgs(args) {
|
|
|
355
547
|
}
|
|
356
548
|
if (arg === '--registry') {
|
|
357
549
|
const value = args[++i];
|
|
358
|
-
if (value === undefined || value.startsWith('--'))
|
|
550
|
+
if (value === undefined || value.startsWith('--'))
|
|
551
|
+
throw new Error('Missing value for --registry');
|
|
359
552
|
flags.registry = value;
|
|
360
553
|
continue;
|
|
361
554
|
}
|
|
362
555
|
if (arg.startsWith('--')) throw new Error(`Unknown flag for yank: ${arg}`);
|
|
363
556
|
positional.push(arg);
|
|
364
557
|
}
|
|
365
|
-
if (positional.length !== 1)
|
|
558
|
+
if (positional.length !== 1)
|
|
559
|
+
throw new Error(
|
|
560
|
+
'Usage: knolo yank <publisher>/<slug>@<version> [--json] [--registry <url>]'
|
|
561
|
+
);
|
|
366
562
|
const spec = parsePackSpec(positional[0]);
|
|
367
|
-
if (spec.version === 'latest')
|
|
563
|
+
if (spec.version === 'latest')
|
|
564
|
+
throw new Error(
|
|
565
|
+
'Yank requires an exact version: <publisher>/<slug>@<version>.'
|
|
566
|
+
);
|
|
368
567
|
return { spec, flags };
|
|
369
568
|
}
|
|
370
569
|
|
|
@@ -372,6 +571,18 @@ function authorizationHeaders(token) {
|
|
|
372
571
|
return { Authorization: `Bearer ${token}` };
|
|
373
572
|
}
|
|
374
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
|
+
|
|
375
586
|
function hubApiUrl(registry, segments) {
|
|
376
587
|
const url = new URL(registry);
|
|
377
588
|
url.pathname = `/api/${segments.map((segment) => encodeURIComponent(String(segment))).join('/')}`;
|
|
@@ -382,6 +593,18 @@ function createSha256(bytes) {
|
|
|
382
593
|
return createHash('sha256').update(bytes).digest('hex');
|
|
383
594
|
}
|
|
384
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
|
+
|
|
385
608
|
function extractPublisherHandle(account) {
|
|
386
609
|
const candidates = [
|
|
387
610
|
account?.publisherHandle,
|
|
@@ -395,41 +618,17 @@ function extractPublisherHandle(account) {
|
|
|
395
618
|
account?.publishers?.[0]?.slug,
|
|
396
619
|
typeof account?.publisher === 'string' ? account.publisher : undefined,
|
|
397
620
|
];
|
|
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();
|
|
621
|
+
return candidates
|
|
622
|
+
.find((value) => typeof value === 'string' && value.trim())
|
|
623
|
+
?.trim();
|
|
430
624
|
}
|
|
431
625
|
|
|
432
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
|
+
}
|
|
433
632
|
let url;
|
|
434
633
|
try {
|
|
435
634
|
url = new URL(value);
|
|
@@ -437,13 +636,122 @@ function assertPublicBlobUrl(value, label) {
|
|
|
437
636
|
throw new Error(`Hub ${label} URL is invalid.`);
|
|
438
637
|
}
|
|
439
638
|
const hostname = url.hostname.toLowerCase();
|
|
440
|
-
if (url.protocol !== 'https:')
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
);
|
|
443
654
|
}
|
|
444
655
|
return url;
|
|
445
656
|
}
|
|
446
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
|
+
|
|
447
755
|
function extractId(body, keys) {
|
|
448
756
|
for (const key of keys) {
|
|
449
757
|
const value = body?.[key];
|
|
@@ -451,37 +759,62 @@ function extractId(body, keys) {
|
|
|
451
759
|
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
|
|
452
760
|
if (value && typeof value === 'object') {
|
|
453
761
|
if (typeof value.id === 'string' && value.id) return value.id;
|
|
454
|
-
if (typeof value.id === 'number' && Number.isSafeInteger(value.id))
|
|
762
|
+
if (typeof value.id === 'number' && Number.isSafeInteger(value.id))
|
|
763
|
+
return value.id;
|
|
455
764
|
if (typeof value.jobId === 'string' && value.jobId) return value.jobId;
|
|
456
|
-
if (typeof value.draftId === 'string' && value.draftId)
|
|
765
|
+
if (typeof value.draftId === 'string' && value.draftId)
|
|
766
|
+
return value.draftId;
|
|
457
767
|
}
|
|
458
768
|
}
|
|
459
769
|
return undefined;
|
|
460
770
|
}
|
|
461
771
|
|
|
462
|
-
async function waitForPublishJob(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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.');
|
|
471
787
|
|
|
472
788
|
for (let attempt = 0; attempt < maxPollAttempts; attempt++) {
|
|
473
789
|
if (attempt > 0 && pollIntervalMs > 0) await sleepImpl(pollIntervalMs);
|
|
474
|
-
const body = await getJson(
|
|
790
|
+
const body = await getJson(
|
|
791
|
+
registryApiUrl(registry, ['publish', 'jobs', jobId]),
|
|
792
|
+
{ fetchImpl, headers }
|
|
793
|
+
);
|
|
475
794
|
const job = body?.job && typeof body.job === 'object' ? body.job : body;
|
|
476
795
|
const status = String(job?.status || job?.state || '').toLowerCase();
|
|
477
|
-
if (
|
|
796
|
+
if (
|
|
797
|
+
[
|
|
798
|
+
'passed',
|
|
799
|
+
'pass',
|
|
800
|
+
'complete',
|
|
801
|
+
'completed',
|
|
802
|
+
'succeeded',
|
|
803
|
+
'success',
|
|
804
|
+
].includes(status)
|
|
805
|
+
)
|
|
806
|
+
return job;
|
|
478
807
|
if (['failed', 'failure', 'error', 'rejected'].includes(status)) {
|
|
479
808
|
const detail = job?.error || job?.message || body?.error;
|
|
480
|
-
throw new Error(
|
|
809
|
+
throw new Error(
|
|
810
|
+
`Hub publish verification failed${detail ? `: ${detail}` : '.'}`
|
|
811
|
+
);
|
|
481
812
|
}
|
|
482
813
|
}
|
|
483
814
|
|
|
484
|
-
throw new Error(
|
|
815
|
+
throw new Error(
|
|
816
|
+
`Timed out waiting for Hub publish verification job ${jobId}.`
|
|
817
|
+
);
|
|
485
818
|
}
|
|
486
819
|
|
|
487
820
|
function wait(milliseconds) {
|
|
@@ -499,14 +832,18 @@ export function parseAddArgs(args) {
|
|
|
499
832
|
}
|
|
500
833
|
if (['--out', '--registry'].includes(arg)) {
|
|
501
834
|
const value = args[++i];
|
|
502
|
-
if (value === undefined || value.startsWith('--'))
|
|
835
|
+
if (value === undefined || value.startsWith('--'))
|
|
836
|
+
throw new Error(`Missing value for ${arg}`);
|
|
503
837
|
flags[arg.slice(2)] = value;
|
|
504
838
|
continue;
|
|
505
839
|
}
|
|
506
840
|
if (arg.startsWith('--')) throw new Error(`Unknown flag for add: ${arg}`);
|
|
507
841
|
positional.push(arg);
|
|
508
842
|
}
|
|
509
|
-
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
|
+
);
|
|
510
847
|
return { spec: parsePackSpec(positional[0]), flags };
|
|
511
848
|
}
|
|
512
849
|
|
|
@@ -520,12 +857,15 @@ export function parseLoginArgs(args) {
|
|
|
520
857
|
}
|
|
521
858
|
if (arg === '--token' || arg === '--registry') {
|
|
522
859
|
const value = args[++i];
|
|
523
|
-
if (value === undefined || value.startsWith('--'))
|
|
860
|
+
if (value === undefined || value.startsWith('--'))
|
|
861
|
+
throw new Error(`Missing value for ${arg}`);
|
|
524
862
|
flags[arg.slice(2)] = value;
|
|
525
863
|
continue;
|
|
526
864
|
}
|
|
527
865
|
if (arg.startsWith('--')) throw new Error(`Unknown flag for login: ${arg}`);
|
|
528
|
-
throw new Error(
|
|
866
|
+
throw new Error(
|
|
867
|
+
'Usage: knolo login [--token kno_…] [--stdin] [--registry <url>]'
|
|
868
|
+
);
|
|
529
869
|
}
|
|
530
870
|
return { flags };
|
|
531
871
|
}
|
|
@@ -563,17 +903,25 @@ export function parseSearchArgs(args) {
|
|
|
563
903
|
}
|
|
564
904
|
if (['--format', '--license', '--registry'].includes(arg)) {
|
|
565
905
|
const value = args[++i];
|
|
566
|
-
if (value === undefined || value.startsWith('--'))
|
|
906
|
+
if (value === undefined || value.startsWith('--'))
|
|
907
|
+
throw new Error(`Missing value for ${arg}`);
|
|
567
908
|
flags[arg.slice(2)] = value;
|
|
568
909
|
continue;
|
|
569
910
|
}
|
|
570
|
-
if (arg.startsWith('--'))
|
|
911
|
+
if (arg.startsWith('--'))
|
|
912
|
+
throw new Error(`Unknown flag for search: ${arg}`);
|
|
571
913
|
positional.push(arg);
|
|
572
914
|
}
|
|
573
915
|
|
|
574
916
|
const query = positional.join(' ').trim();
|
|
575
|
-
if (!query)
|
|
576
|
-
|
|
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
|
+
) {
|
|
577
925
|
throw new Error('--format must be V4 or V5.');
|
|
578
926
|
}
|
|
579
927
|
if (flags.format) flags.format = String(flags.format).toUpperCase();
|
|
@@ -591,14 +939,18 @@ export function parseInfoArgs(args) {
|
|
|
591
939
|
}
|
|
592
940
|
if (arg === '--registry') {
|
|
593
941
|
const value = args[++i];
|
|
594
|
-
if (value === undefined || value.startsWith('--'))
|
|
942
|
+
if (value === undefined || value.startsWith('--'))
|
|
943
|
+
throw new Error('Missing value for --registry');
|
|
595
944
|
flags.registry = value;
|
|
596
945
|
continue;
|
|
597
946
|
}
|
|
598
947
|
if (arg.startsWith('--')) throw new Error(`Unknown flag for info: ${arg}`);
|
|
599
948
|
positional.push(arg);
|
|
600
949
|
}
|
|
601
|
-
if (positional.length !== 1)
|
|
950
|
+
if (positional.length !== 1)
|
|
951
|
+
throw new Error(
|
|
952
|
+
'Usage: knolo info <publisher>/<slug> [--json] [--registry <url>]'
|
|
953
|
+
);
|
|
602
954
|
return { positional, flags };
|
|
603
955
|
}
|
|
604
956
|
|
|
@@ -609,7 +961,13 @@ function validatePacksResponse(body) {
|
|
|
609
961
|
}
|
|
610
962
|
|
|
611
963
|
function validatePackInfoResponse(body) {
|
|
612
|
-
if (
|
|
964
|
+
if (
|
|
965
|
+
!body ||
|
|
966
|
+
typeof body !== 'object' ||
|
|
967
|
+
typeof body.name !== 'string' ||
|
|
968
|
+
!body.pack ||
|
|
969
|
+
typeof body.pack !== 'object'
|
|
970
|
+
) {
|
|
613
971
|
throw new Error('Registry returned an invalid pack response.');
|
|
614
972
|
}
|
|
615
973
|
}
|
|
@@ -620,17 +978,24 @@ async function validateKnowledgeImage(core, bytes) {
|
|
|
620
978
|
const verification = core.verifyKnowledgeImageV5(bytes);
|
|
621
979
|
return { format: 'V5', stateRoot: verification.stateRoot };
|
|
622
980
|
} catch (error) {
|
|
623
|
-
throw new Error(
|
|
981
|
+
throw new Error(
|
|
982
|
+
`invalid V5 Knowledge Image: ${error instanceof Error ? error.message : String(error)}`
|
|
983
|
+
);
|
|
624
984
|
}
|
|
625
985
|
}
|
|
626
986
|
|
|
627
|
-
const buffer = bytes.buffer.slice(
|
|
987
|
+
const buffer = bytes.buffer.slice(
|
|
988
|
+
bytes.byteOffset,
|
|
989
|
+
bytes.byteOffset + bytes.byteLength
|
|
990
|
+
);
|
|
628
991
|
if (core.isPackV4?.(buffer)) {
|
|
629
992
|
try {
|
|
630
993
|
await core.mountPack({ src: bytes });
|
|
631
994
|
return { format: 'V4' };
|
|
632
995
|
} catch (error) {
|
|
633
|
-
throw new Error(
|
|
996
|
+
throw new Error(
|
|
997
|
+
`invalid V4 Knowledge Image: ${error instanceof Error ? error.message : String(error)}`
|
|
998
|
+
);
|
|
634
999
|
}
|
|
635
1000
|
}
|
|
636
1001
|
throw new Error('not a Knowledge Image');
|
|
@@ -653,7 +1018,8 @@ async function stageArtifactCopy(sourcePath, outputPath, cwd) {
|
|
|
653
1018
|
async function commitStagedFiles(entries) {
|
|
654
1019
|
const targets = new Set();
|
|
655
1020
|
const records = entries.map((entry) => {
|
|
656
|
-
if (targets.has(entry.targetPath))
|
|
1021
|
+
if (targets.has(entry.targetPath))
|
|
1022
|
+
throw new Error(`Install paths must be unique: ${entry.targetPath}`);
|
|
657
1023
|
targets.add(entry.targetPath);
|
|
658
1024
|
return {
|
|
659
1025
|
...entry,
|
|
@@ -666,7 +1032,8 @@ async function commitStagedFiles(entries) {
|
|
|
666
1032
|
try {
|
|
667
1033
|
for (const record of records) {
|
|
668
1034
|
const existing = await statIfPresent(record.targetPath);
|
|
669
|
-
if (existing?.isDirectory())
|
|
1035
|
+
if (existing?.isDirectory())
|
|
1036
|
+
throw new Error(`Cannot replace directory at ${record.targetPath}.`);
|
|
670
1037
|
}
|
|
671
1038
|
|
|
672
1039
|
for (const record of records) {
|
|
@@ -677,11 +1044,10 @@ async function commitStagedFiles(entries) {
|
|
|
677
1044
|
await rename(record.tempPath, record.targetPath);
|
|
678
1045
|
record.committed = true;
|
|
679
1046
|
}
|
|
680
|
-
|
|
681
1047
|
} catch (error) {
|
|
682
1048
|
for (const record of [...records].reverse()) {
|
|
683
1049
|
if (record.committed) await unlinkIfPresent(record.targetPath);
|
|
684
|
-
if (record.hadOriginal && await pathExists(record.backupPath)) {
|
|
1050
|
+
if (record.hadOriginal && (await pathExists(record.backupPath))) {
|
|
685
1051
|
await unlinkIfPresent(record.targetPath);
|
|
686
1052
|
await rename(record.backupPath, record.targetPath);
|
|
687
1053
|
}
|
|
@@ -690,7 +1056,8 @@ async function commitStagedFiles(entries) {
|
|
|
690
1056
|
}
|
|
691
1057
|
|
|
692
1058
|
for (const record of records) {
|
|
693
|
-
if (record.hadOriginal)
|
|
1059
|
+
if (record.hadOriginal)
|
|
1060
|
+
await unlinkIfPresent(record.backupPath).catch(() => {});
|
|
694
1061
|
}
|
|
695
1062
|
}
|
|
696
1063
|
|
|
@@ -718,24 +1085,36 @@ async function unlinkIfPresent(filePath) {
|
|
|
718
1085
|
export function renderSearchTable(packs) {
|
|
719
1086
|
if (!packs.length) return 'No packs found.';
|
|
720
1087
|
|
|
721
|
-
const rows = packs.map((pack) =>
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
+
);
|
|
729
1098
|
const widths = [24, 9, 6, 11, 8];
|
|
730
|
-
const header = [
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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();
|
|
739
1118
|
|
|
740
1119
|
return [formatRow(header), ...rows.map(formatRow)].join('\n');
|
|
741
1120
|
}
|