@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.
@@ -1,19 +1,60 @@
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 { 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, putBytes } from './http.mjs';
9
- import { cachePath, stageLockfileUpdate, validateLockfileUpdate } from './lockfile.mjs';
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 { loadCredentials, promptForToken, removeCredentials, saveCredentials, validateToken } from './credentials.mjs';
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
- export async function runHubSearch(args, { env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}) {
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(args, { env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}) {
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(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
- } = {}) {
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) throw new Error('Could not load @knolo/core for Hub artifact validation.');
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, ['packs', spec.publisher, spec.slug, spec.version]);
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, { expectedName: spec.name, requestedVersion: spec.version });
120
+ manifest = validatePackManifest(body, {
121
+ expectedName: spec.name,
122
+ requestedVersion: spec.version,
123
+ });
68
124
  } catch (error) {
69
- if (error instanceof RegistryError && (error.status === 404 || error.code === 'not_found')) {
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 (error instanceof RegistryError && (error.status === 410 || error.code === 'yanked')) {
73
- manifest = validatePackManifest(error.body, { expectedName: spec.name, requestedVersion: spec.version });
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) throw new Error('version yanked; use --force to download yanked bytes');
82
- if (yanked) warn(`warning: downloading yanked version ${manifest.name}@${manifest.version}`);
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)) throw new Error('manifest sha256 is invalid');
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(`artifact sha256 mismatch: expected ${manifest.sha256}, got ${download.sha256}`);
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 (manifest.format && String(manifest.format).toUpperCase() !== validation.format) {
103
- throw new Error(`artifact format mismatch: manifest=${manifest.format}, bytes=${validation.format}`);
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 (manifest.stateRoot !== undefined && manifest.stateRoot !== validation.stateRoot) {
109
- throw new Error(`artifact stateRoot mismatch: expected ${manifest.stateRoot}, got ${validation.stateRoot || 'none'}`);
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(download.tempPath, finalCachePath);
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(download.tempPath, flags.out, cwd);
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({ cwd, registry, manifest, force: flags.force });
120
- stagedFiles.push({ targetPath: stagedLock.path, tempPath: stagedLock.tempPath });
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(stagedFiles.map(({ tempPath }) => unlinkIfPresent(tempPath)));
231
+ await Promise.all(
232
+ stagedFiles.map(({ tempPath }) => unlinkIfPresent(tempPath))
233
+ );
135
234
  }
136
235
  }
137
236
 
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
- } = {}) {
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(`Create a token at ${registry}/dashboard/tokens (Sign in with GitHub).`);
148
- const token = (flags.token ?? await promptForToken({ input, output, forceStdin: flags.stdin })).trim();
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(args, { env = process.env, homeDir, print = console.log } = {}) {
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(args, { env = process.env, homeDir, print = console.log } = {}) {
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(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
- } = {}) {
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 = resolveRegistryUrl({ value: flags.registry || credentials.registry, env });
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']), { fetchImpl, headers: authHeaders });
309
+ const account = await getJson(registryApiUrl(registry, ['account']), {
310
+ fetchImpl,
311
+ headers: authHeaders,
312
+ });
190
313
  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}.`);
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) throw new Error('Artifact exceeds Hub’s 250 MB limit.');
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
- 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,
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) throw new Error('Hub verify response did not include a publish job id.');
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 ? { intendedUse: flags.intendedUse } : {}),
393
+ ...(flags.intendedUse !== undefined
394
+ ? { intendedUse: flags.intendedUse }
395
+ : {}),
245
396
  };
246
- const draftResponse = await postJson(registryApiUrl(registry, ['publish', 'drafts']), draftPayload, {
247
- fetchImpl,
248
- headers: authHeaders,
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) 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
- });
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: upload.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(args, {
285
- env = process.env,
286
- fetchImpl = globalThis.fetch,
287
- homeDir,
288
- print = console.log,
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 = 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
- });
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('--')) throw new Error(`Missing value for ${arg}`);
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('--')) throw new Error(`Unknown flag for publish: ${arg}`);
510
+ if (arg.startsWith('--'))
511
+ throw new Error(`Unknown flag for publish: ${arg}`);
334
512
  positional.push(arg);
335
513
  }
336
514
 
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>]');
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)) 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 @.');
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)) throw new Error('Publish publisher must contain lowercase letters, numbers, and hyphens.');
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('--')) throw new Error('Missing value for --registry');
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) throw new Error('Usage: knolo yank <publisher>/<slug>@<version> [--json] [--registry <url>]');
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') throw new Error('Yank requires an exact version: <publisher>/<slug>@<version>.');
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.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();
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:') 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.`);
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)) return 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) return 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(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.');
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(registryApiUrl(registry, ['publish', 'jobs', jobId]), { fetchImpl, headers });
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 (['passed', 'pass', 'complete', 'completed', 'succeeded', 'success'].includes(status)) return job;
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(`Hub publish verification failed${detail ? `: ${detail}` : '.'}`);
809
+ throw new Error(
810
+ `Hub publish verification failed${detail ? `: ${detail}` : '.'}`
811
+ );
481
812
  }
482
813
  }
483
814
 
484
- throw new Error(`Timed out waiting for Hub publish verification job ${jobId}.`);
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('--')) throw new Error(`Missing value for ${arg}`);
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) throw new Error('Usage: knolo add <publisher>/<slug>[@<version>] [--out <path>] [--force] [--json] [--registry <url>]');
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('--')) throw new Error(`Missing value for ${arg}`);
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('Usage: knolo login [--token kno_…] [--stdin] [--registry <url>]');
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('--')) throw new Error(`Missing value for ${arg}`);
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('--')) throw new Error(`Unknown flag for search: ${arg}`);
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) 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())) {
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('--')) throw new Error('Missing value for --registry');
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) throw new Error('Usage: knolo info <publisher>/<slug> [--json] [--registry <url>]');
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 (!body || typeof body !== 'object' || typeof body.name !== 'string' || !body.pack || typeof body.pack !== 'object') {
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(`invalid V5 Knowledge Image: ${error instanceof Error ? error.message : String(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(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
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(`invalid V4 Knowledge Image: ${error instanceof Error ? error.message : String(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)) throw new Error(`Install paths must be unique: ${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()) throw new Error(`Cannot replace directory at ${record.targetPath}.`);
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) await unlinkIfPresent(record.backupPath).catch(() => {});
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
- 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)));
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 = ['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();
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
  }