@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.
@@ -1,19 +1,59 @@
1
- import { chmod, copyFile, lstat, mkdir, readFile, rename, unlink } from 'node:fs/promises';
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 { registryApiUrl, resolveRegistryUrl } from './config.mjs';
6
- import { downloadBlobToTemp, MAX_ARTIFACT_BYTES, readArtifact, stageCachedArtifact } from './artifacts.mjs';
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 { cachePath, stageLockfileUpdate, validateLockfileUpdate } from './lockfile.mjs';
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 { loadCredentials, promptForToken, removeCredentials, saveCredentials, validateToken } from './credentials.mjs';
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
- export async function runHubSearch(args, { env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}) {
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(args, { env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}) {
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(args, {
50
- env = process.env,
51
- fetchImpl = globalThis.fetch,
52
- core,
53
- cwd = process.cwd(),
54
- homeDir,
55
- print = console.log,
56
- warn = console.error,
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) throw new Error('Could not load @knolo/core for Hub artifact validation.');
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, ['packs', spec.publisher, spec.slug, spec.version]);
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, { expectedName: spec.name, requestedVersion: spec.version });
119
+ manifest = validatePackManifest(body, {
120
+ expectedName: spec.name,
121
+ requestedVersion: spec.version,
122
+ });
68
123
  } catch (error) {
69
- if (error instanceof RegistryError && (error.status === 404 || error.code === 'not_found')) {
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 (error instanceof RegistryError && (error.status === 410 || error.code === 'yanked')) {
73
- manifest = validatePackManifest(error.body, { expectedName: spec.name, requestedVersion: spec.version });
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) throw new Error('version yanked; use --force to download yanked bytes');
82
- if (yanked) warn(`warning: downloading yanked version ${manifest.name}@${manifest.version}`);
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)) throw new Error('manifest sha256 is invalid');
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(`artifact sha256 mismatch: expected ${manifest.sha256}, got ${download.sha256}`);
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 (manifest.format && String(manifest.format).toUpperCase() !== validation.format) {
103
- throw new Error(`artifact format mismatch: manifest=${manifest.format}, bytes=${validation.format}`);
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 (manifest.stateRoot !== undefined && manifest.stateRoot !== validation.stateRoot) {
109
- throw new Error(`artifact stateRoot mismatch: expected ${manifest.stateRoot}, got ${validation.stateRoot || 'none'}`);
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(download.tempPath, finalCachePath);
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(download.tempPath, flags.out, cwd);
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({ cwd, registry, manifest, force: flags.force });
120
- stagedFiles.push({ targetPath: stagedLock.path, tempPath: stagedLock.tempPath });
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(stagedFiles.map(({ tempPath }) => unlinkIfPresent(tempPath)));
230
+ await Promise.all(
231
+ stagedFiles.map(({ tempPath }) => unlinkIfPresent(tempPath))
232
+ );
135
233
  }
136
234
  }
137
235
 
138
- export async function runHubLogin(args, {
139
- env = process.env,
140
- homeDir,
141
- input = process.stdin,
142
- output = process.stderr,
143
- print = console.log,
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(`Create a token at ${registry}/dashboard/tokens (Sign in with GitHub).`);
148
- const token = (flags.token ?? await promptForToken({ input, output, forceStdin: flags.stdin })).trim();
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(args, { env = process.env, homeDir, print = console.log } = {}) {
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(args, { env = process.env, homeDir, print = console.log } = {}) {
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(args, {
173
- env = process.env,
174
- fetchImpl = globalThis.fetch,
175
- cwd = process.cwd(),
176
- homeDir,
177
- print = console.log,
178
- sleep = wait,
179
- pollIntervalMs = 1000,
180
- maxPollAttempts = 120,
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 = resolveRegistryUrl({ value: flags.registry || credentials.registry, env });
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']), { fetchImpl, headers: authHeaders });
307
+ const account = await getJson(registryApiUrl(registry, ['account']), {
308
+ fetchImpl,
309
+ headers: authHeaders,
310
+ });
190
311
  const accountPublisher = extractPublisherHandle(account);
191
- if (flags.publisher && accountPublisher && flags.publisher !== accountPublisher) {
192
- throw new Error(`Token is scoped to publisher ${accountPublisher}, not ${flags.publisher}.`);
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) throw new Error('Artifact exceeds Hub’s 250 MB limit.');
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
- const uploadToken = await postJson(hubApiUrl(registry, ['upload', 'token']), {
202
- sha256,
203
- contentLength: bytes.length,
204
- }, { fetchImpl, headers: authHeaders });
205
- const upload = validateUploadTokenResponse(uploadToken, sha256);
206
- assertPublicBlobUrl(upload.url, 'upload');
207
- const uploaded = await putBytes(upload.url, bytes, {
208
- fetchImpl,
209
- headers: { 'content-length': String(bytes.length) },
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,
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) throw new Error('Hub verify response did not include a publish job id.');
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 ? { intendedUse: flags.intendedUse } : {}),
398
+ ...(flags.intendedUse !== undefined
399
+ ? { intendedUse: flags.intendedUse }
400
+ : {}),
245
401
  };
246
- const draftResponse = await postJson(registryApiUrl(registry, ['publish', 'drafts']), draftPayload, {
247
- fetchImpl,
248
- headers: authHeaders,
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) throw new Error('Hub draft response did not include a draft id.');
252
-
253
- const releaseResponse = await postJson(registryApiUrl(registry, ['publish', 'drafts', draftId, 'release']), undefined, {
254
- fetchImpl,
255
- headers: authHeaders,
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: upload.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(args, {
285
- env = process.env,
286
- fetchImpl = globalThis.fetch,
287
- homeDir,
288
- print = console.log,
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 = resolveRegistryUrl({ value: flags.registry || credentials.registry, env });
293
- const response = await postJson(registryApiUrl(registry, ['packs', spec.publisher, spec.slug, spec.version, 'yank']), undefined, {
294
- fetchImpl,
295
- headers: authorizationHeaders(credentials.token),
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('--')) throw new Error(`Missing value for ${arg}`);
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('--')) throw new Error(`Unknown flag for publish: ${arg}`);
515
+ if (arg.startsWith('--'))
516
+ throw new Error(`Unknown flag for publish: ${arg}`);
334
517
  positional.push(arg);
335
518
  }
336
519
 
337
- if (positional.length !== 1 || !flags.slug || !flags.version || !flags.license) {
338
- throw new Error('Usage: knolo publish <pack.knolo> --slug <slug> --version <version> --license <SPDX> [--publisher <handle>] [--readme <text>] [--sources <text>] [--intended-use <text>] [--json] [--registry <url>]');
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)) throw new Error('Publish slug must contain lowercase letters, numbers, and hyphens.');
341
- if (!flags.version.trim() || flags.version.includes('@')) throw new Error('Publish version must be a non-empty version without @.');
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)) throw new Error('Publish publisher must contain lowercase letters, numbers, and hyphens.');
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('--')) throw new Error('Missing value for --registry');
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) throw new Error('Usage: knolo yank <publisher>/<slug>@<version> [--json] [--registry <url>]');
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') throw new Error('Yank requires an exact version: <publisher>/<slug>@<version>.');
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.find((value) => typeof value === 'string' && value.trim())?.trim();
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:') throw new Error(`Hub ${label} URL must use HTTPS.`);
441
- if (hostname.includes('.private.') || hostname.startsWith('private.')) {
442
- throw new Error(`Refusing private Blob URL for ${label}; Hub verification requires a public Blob URL.`);
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)) return 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) return 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(registry, jobId, {
463
- fetchImpl,
464
- headers,
465
- sleep: sleepImpl = wait,
466
- pollIntervalMs = 1000,
467
- maxPollAttempts = 120,
468
- } = {}) {
469
- if (!Number.isSafeInteger(maxPollAttempts) || maxPollAttempts < 1) throw new Error('maxPollAttempts must be a positive integer.');
470
- if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 0) throw new Error('pollIntervalMs must be a non-negative number.');
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(registryApiUrl(registry, ['publish', 'jobs', jobId]), { fetchImpl, headers });
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 (['passed', 'pass', 'complete', 'completed', 'succeeded', 'success'].includes(status)) return job;
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(`Hub publish verification failed${detail ? `: ${detail}` : '.'}`);
873
+ throw new Error(
874
+ `Hub publish verification failed${detail ? `: ${detail}` : '.'}`
875
+ );
481
876
  }
482
877
  }
483
878
 
484
- throw new Error(`Timed out waiting for Hub publish verification job ${jobId}.`);
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('--')) throw new Error(`Missing value for ${arg}`);
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) throw new Error('Usage: knolo add <publisher>/<slug>[@<version>] [--out <path>] [--force] [--json] [--registry <url>]');
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('--')) throw new Error(`Missing value for ${arg}`);
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('Usage: knolo login [--token kno_…] [--stdin] [--registry <url>]');
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('--')) throw new Error(`Missing value for ${arg}`);
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('--')) throw new Error(`Unknown flag for search: ${arg}`);
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) throw new Error('Usage: knolo search <query> [--format V4|V5] [--license <id>] [--official] [--agents] [--json] [--registry <url>]');
576
- if (flags.format && !['V4', 'V5'].includes(String(flags.format).toUpperCase())) {
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('--')) throw new Error('Missing value for --registry');
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) throw new Error('Usage: knolo info <publisher>/<slug> [--json] [--registry <url>]');
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 (!body || typeof body !== 'object' || typeof body.name !== 'string' || !body.pack || typeof body.pack !== 'object') {
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(`invalid V5 Knowledge Image: ${error instanceof Error ? error.message : String(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(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
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(`invalid V4 Knowledge Image: ${error instanceof Error ? error.message : String(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)) throw new Error(`Install paths must be unique: ${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()) throw new Error(`Cannot replace directory at ${record.targetPath}.`);
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) await unlinkIfPresent(record.backupPath).catch(() => {});
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
- pack.id || pack.name || `${pack.publisher || ''}/${pack.slug || ''}`,
723
- pack.version ?? '',
724
- pack.format ?? '',
725
- pack.license ?? '',
726
- pack.pulls ?? 0,
727
- pack.description ?? '',
728
- ].map((value) => String(value)));
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 = ['NAME', 'VERSION', 'FORMAT', 'LICENSE', 'PULLS', 'DESCRIPTION'];
731
- const formatRow = (row) => [
732
- pad(truncate(row[0], widths[0]), widths[0]),
733
- pad(truncate(row[1], widths[1]), widths[1]),
734
- pad(truncate(row[2], widths[2]), widths[2]),
735
- pad(truncate(row[3], widths[3]), widths[3]),
736
- pad(truncate(row[4], widths[4]), widths[4]),
737
- truncate(row[5], 72),
738
- ].join(' ').trimEnd();
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
  }