@knolo/cli 5.2.0 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,8 +18,10 @@ export class RegistryNetworkError extends Error {
18
18
  }
19
19
 
20
20
  export function registryErrorMessage({ status, code, body, url }) {
21
+ if (status === 401 || code === 'unauthenticated') {
22
+ return 'Hub authentication failed. Use Authorization: Bearer kno_…; a revoked token can also return 401. GitHub sign-in is only required to mint tokens.';
23
+ }
21
24
  if (typeof body?.error === 'string' && body.error.trim()) return body.error.trim();
22
- if (status === 401 || code === 'unauthenticated') return 'Registry authentication is required.';
23
25
  if (status === 404 || code === 'not_found') return 'Registry resource not found.';
24
26
  if (status === 410 || code === 'yanked') return 'Registry version is yanked.';
25
27
  if (status === 413 || code === 'too_large') return 'Registry artifact is too large.';
@@ -1,17 +1,57 @@
1
- import { RegistryError, RegistryNetworkError, registryErrorMessage } from './errors.mjs';
1
+ import {
2
+ RegistryError,
3
+ RegistryNetworkError,
4
+ registryErrorMessage,
5
+ } from './errors.mjs';
2
6
 
3
- export async function getJson(url, { fetchImpl = globalThis.fetch, headers = {} } = {}) {
4
- if (typeof fetchImpl !== 'function') throw new Error('This Node runtime does not provide fetch.');
7
+ export async function getJson(
8
+ url,
9
+ { fetchImpl = globalThis.fetch, headers = {} } = {}
10
+ ) {
11
+ return requestJson(url, { fetchImpl, headers, method: 'GET' });
12
+ }
13
+
14
+ export async function postJson(
15
+ url,
16
+ body,
17
+ { fetchImpl = globalThis.fetch, headers = {} } = {}
18
+ ) {
19
+ return requestJson(url, { fetchImpl, headers, method: 'POST', body });
20
+ }
21
+
22
+ async function requestJson(
23
+ url,
24
+ {
25
+ fetchImpl = globalThis.fetch,
26
+ headers = {},
27
+ method = 'GET',
28
+ body: requestBody,
29
+ } = {}
30
+ ) {
31
+ if (typeof fetchImpl !== 'function')
32
+ throw new Error('This Node runtime does not provide fetch.');
5
33
 
6
34
  let response;
7
35
  try {
8
36
  response = await fetchImpl(url, {
9
- method: 'GET',
37
+ method,
10
38
  redirect: 'follow',
11
- headers: { accept: 'application/json', ...headers },
39
+ headers: {
40
+ accept: 'application/json',
41
+ ...(requestBody !== undefined
42
+ ? { 'content-type': 'application/json' }
43
+ : {}),
44
+ ...headers,
45
+ },
46
+ ...(requestBody !== undefined
47
+ ? { body: JSON.stringify(requestBody) }
48
+ : {}),
12
49
  });
13
50
  } catch (error) {
14
- throw new RegistryNetworkError(`Could not reach registry at ${new URL(url).origin}.`, { url: String(url), cause: error });
51
+ throw new RegistryNetworkError(
52
+ `Could not reach registry at ${new URL(url).origin}.`,
53
+ { url: String(url), cause: error }
54
+ );
15
55
  }
16
56
 
17
57
  const text = await readResponseText(response, url);
@@ -21,11 +61,14 @@ export async function getJson(url, { fetchImpl = globalThis.fetch, headers = {}
21
61
  body = JSON.parse(text);
22
62
  } catch {
23
63
  if (!response.ok) {
24
- throw new RegistryError(`Registry returned HTTP ${response.status} with a non-JSON response.`, {
25
- status: response.status,
26
- url: String(url),
27
- body: text.slice(0, 240),
28
- });
64
+ throw new RegistryError(
65
+ `Registry returned HTTP ${response.status} with a non-JSON response.`,
66
+ {
67
+ status: response.status,
68
+ url: String(url),
69
+ body: text.slice(0, 240),
70
+ }
71
+ );
29
72
  }
30
73
  throw new RegistryError('Registry returned invalid JSON.', {
31
74
  status: response.status,
@@ -38,12 +81,20 @@ export async function getJson(url, { fetchImpl = globalThis.fetch, headers = {}
38
81
 
39
82
  if (!response.ok) {
40
83
  const code = typeof body?.code === 'string' ? body.code : undefined;
41
- throw new RegistryError(registryErrorMessage({ status: response.status, code, body, url: String(url) }), {
42
- status: response.status,
43
- code,
44
- url: String(url),
45
- body,
46
- });
84
+ throw new RegistryError(
85
+ registryErrorMessage({
86
+ status: response.status,
87
+ code,
88
+ body,
89
+ url: String(url),
90
+ }),
91
+ {
92
+ status: response.status,
93
+ code,
94
+ url: String(url),
95
+ body,
96
+ }
97
+ );
47
98
  }
48
99
 
49
100
  return body;
@@ -53,6 +104,9 @@ async function readResponseText(response, url) {
53
104
  try {
54
105
  return await response.text();
55
106
  } catch (error) {
56
- throw new RegistryNetworkError(`Could not read registry response from ${new URL(url).origin}.`, { url: String(url), cause: error });
107
+ throw new RegistryNetworkError(
108
+ `Could not read registry response from ${new URL(url).origin}.`,
109
+ { url: String(url), cause: error }
110
+ );
57
111
  }
58
112
  }
@@ -37,13 +37,23 @@ export async function validateLockfileUpdate({ cwd = process.cwd(), registry, ma
37
37
  }
38
38
 
39
39
  export async function upsertLockfile({ cwd = process.cwd(), registry, manifest, force = false } = {}) {
40
+ const staged = await stageLockfileUpdate({ cwd, registry, manifest, force });
41
+ try {
42
+ await rename(staged.tempPath, staged.path);
43
+ } finally {
44
+ await unlink(staged.tempPath).catch(() => {});
45
+ }
46
+ return { path: staged.path, entry: staged.entry };
47
+ }
48
+
49
+ export async function stageLockfileUpdate({ cwd = process.cwd(), registry, manifest, force = false } = {}) {
40
50
  const { targetPath, entry, packs } = await validateLockfileUpdate({ cwd, registry, manifest, force });
41
51
  const next = {
42
52
  registry,
43
53
  packs: { ...packs, [manifest.name]: entry },
44
54
  };
45
- await atomicWriteJson(targetPath, next);
46
- return { path: targetPath, entry };
55
+ const tempPath = await writeJsonTemp(targetPath, next);
56
+ return { path: targetPath, tempPath, entry };
47
57
  }
48
58
 
49
59
  async function readLockfile(filePath) {
@@ -66,13 +76,14 @@ async function readLockfile(filePath) {
66
76
  return value;
67
77
  }
68
78
 
69
- async function atomicWriteJson(filePath, value) {
79
+ async function writeJsonTemp(filePath, value) {
70
80
  await mkdir(path.dirname(filePath), { recursive: true });
71
81
  const tempPath = `${filePath}.tmp-${process.pid}-${randomUUID()}`;
72
82
  try {
73
83
  await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o644 });
74
- await rename(tempPath, filePath);
75
- } finally {
84
+ return tempPath;
85
+ } catch (error) {
76
86
  await unlink(tempPath).catch(() => {});
87
+ throw error;
77
88
  }
78
89
  }
@@ -20,6 +20,12 @@ export function validatePackManifest(value, { expectedName, requestedVersion } =
20
20
  if (value.stateRoot !== undefined && typeof value.stateRoot !== 'string') {
21
21
  throw new Error('Registry manifest stateRoot must be a string when present.');
22
22
  }
23
+ if (value.format !== undefined && !['V4', 'V5'].includes(value.format)) {
24
+ throw new Error('Registry manifest format must be V4 or V5 when present.');
25
+ }
26
+ if (value.format === 'V5' && (!value.stateRoot || typeof value.stateRoot !== 'string')) {
27
+ throw new Error('Registry manifest stateRoot is required for V5 manifests.');
28
+ }
23
29
  if (value.license !== undefined && typeof value.license !== 'string') {
24
30
  throw new Error('Registry manifest license must be a string when present.');
25
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knolo/cli",
3
- "version": "5.2.0",
3
+ "version": "5.2.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "knolo": "bin/knolo.mjs"
@@ -32,7 +32,8 @@
32
32
  "test:hub": "node --test test/hub.integration.test.mjs"
33
33
  },
34
34
  "dependencies": {
35
- "@knolo/core": "^5.1.0"
35
+ "@knolo/core": "^5.1.0",
36
+ "@vercel/blob": "2.8.0"
36
37
  },
37
38
  "private": false
38
39
  }