@astrale-os/cli 1.0.0-beta.24 → 1.0.0-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/astrale.js +55 -12
- package/package.json +1 -1
- package/src/ui/__tests__/ui.test.ts +91 -0
- package/src/ui/release.ts +70 -15
package/dist/astrale.js
CHANGED
|
@@ -2578,7 +2578,7 @@ var package_default;
|
|
|
2578
2578
|
var init_package = __esm(() => {
|
|
2579
2579
|
package_default = {
|
|
2580
2580
|
name: "@astrale-os/cli",
|
|
2581
|
-
version: "1.0.0-beta.
|
|
2581
|
+
version: "1.0.0-beta.26",
|
|
2582
2582
|
description: "Astrale CLI — connect to existing Astrale kernels",
|
|
2583
2583
|
keywords: [
|
|
2584
2584
|
"astrale",
|
|
@@ -86741,8 +86741,18 @@ async function json4(fetcher, url3, label) {
|
|
|
86741
86741
|
cause
|
|
86742
86742
|
});
|
|
86743
86743
|
}
|
|
86744
|
+
const body = await responseText(response2, label);
|
|
86745
|
+
try {
|
|
86746
|
+
return JSON.parse(body);
|
|
86747
|
+
} catch (cause) {
|
|
86748
|
+
throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned malformed JSON.", undefined, {
|
|
86749
|
+
cause
|
|
86750
|
+
});
|
|
86751
|
+
}
|
|
86752
|
+
}
|
|
86753
|
+
async function responseText(response2, label) {
|
|
86744
86754
|
if (!response2.ok) {
|
|
86745
|
-
throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned HTTP " + response2.status + ".");
|
|
86755
|
+
throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned HTTP " + response2.status + ".", undefined, { cause: new HttpStatusError(response2.status) });
|
|
86746
86756
|
}
|
|
86747
86757
|
const declaredLength = Number(response2.headers.get("content-length"));
|
|
86748
86758
|
if (Number.isFinite(declaredLength) && declaredLength > MAX_DOCUMENT_BYTES) {
|
|
@@ -86771,13 +86781,7 @@ async function json4(fetcher, url3, label) {
|
|
|
86771
86781
|
body.set(chunk, offset);
|
|
86772
86782
|
offset += chunk.byteLength;
|
|
86773
86783
|
}
|
|
86774
|
-
|
|
86775
|
-
return JSON.parse(new TextDecoder().decode(body));
|
|
86776
|
-
} catch (cause) {
|
|
86777
|
-
throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned malformed JSON.", undefined, {
|
|
86778
|
-
cause
|
|
86779
|
-
});
|
|
86780
|
-
}
|
|
86784
|
+
return new TextDecoder().decode(body);
|
|
86781
86785
|
}
|
|
86782
86786
|
async function resolveUiRelease(requested, fetcher = fetch) {
|
|
86783
86787
|
const versionDocument = requested ? { version: requested.replace(/^v/u, "") } : await json4(fetcher, NPM_PACKAGE + "/beta", "npm UI release");
|
|
@@ -86789,13 +86793,45 @@ async function resolveUiRelease(requested, fetcher = fetch) {
|
|
|
86789
86793
|
throw new UiError("UI_REGISTRY_UNAVAILABLE", "Invalid UI beta release version: " + version2);
|
|
86790
86794
|
}
|
|
86791
86795
|
const ref = "v" + version2;
|
|
86792
|
-
const
|
|
86793
|
-
const commit = reference4.object.type === "commit" ? reference4.object.sha : (await json4(fetcher, reference4.object.url, "annotated UI tag " + ref)).object.sha;
|
|
86796
|
+
const commit = await resolveReleaseCommit(ref, fetcher);
|
|
86794
86797
|
if (!/^[0-9a-f]{40}$/u.test(commit)) {
|
|
86795
86798
|
throw new UiError("UI_REGISTRY_UNAVAILABLE", "UI ref " + ref + " did not resolve to a commit.");
|
|
86796
86799
|
}
|
|
86797
86800
|
return readUiReleaseSnapshot({ version: version2, ref, commit }, fetcher);
|
|
86798
86801
|
}
|
|
86802
|
+
async function resolveReleaseCommit(ref, fetcher) {
|
|
86803
|
+
try {
|
|
86804
|
+
const reference4 = await json4(fetcher, GITHUB_API + "/git/ref/tags/" + encodeURIComponent(ref), "UI ref " + ref);
|
|
86805
|
+
return reference4.object.type === "commit" ? reference4.object.sha : (await json4(fetcher, reference4.object.url, "annotated UI tag " + ref)).object.sha;
|
|
86806
|
+
} catch (cause) {
|
|
86807
|
+
const status = cause instanceof UiError && cause.cause instanceof HttpStatusError ? cause.cause.status : undefined;
|
|
86808
|
+
if (status !== 403 && status !== 429 && !(status !== undefined && status >= 500))
|
|
86809
|
+
throw cause;
|
|
86810
|
+
}
|
|
86811
|
+
const label = "UI release " + ref;
|
|
86812
|
+
let response2;
|
|
86813
|
+
try {
|
|
86814
|
+
response2 = await fetcher(GITHUB_WEB + "/releases/tag/" + encodeURIComponent(ref), {
|
|
86815
|
+
headers: { accept: "text/html" }
|
|
86816
|
+
});
|
|
86817
|
+
} catch (cause) {
|
|
86818
|
+
throw new UiError("UI_REGISTRY_UNAVAILABLE", "Unable to reach " + label + ".", undefined, {
|
|
86819
|
+
cause
|
|
86820
|
+
});
|
|
86821
|
+
}
|
|
86822
|
+
const html = await responseText(response2, label);
|
|
86823
|
+
const marker = 'href="/astrale-os/ui/tree/' + ref + '"';
|
|
86824
|
+
const markerOffset = html.lastIndexOf(marker);
|
|
86825
|
+
const releaseHeader = markerOffset < 0 ? "" : html.slice(markerOffset, markerOffset + 8192);
|
|
86826
|
+
const commits = new Set([
|
|
86827
|
+
...releaseHeader.matchAll(/data-hovercard-type=["']commit["'][^>]+href=["']\/astrale-os\/ui\/commit\/([0-9a-f]{40})["']/gu)
|
|
86828
|
+
].map((match) => match[1]));
|
|
86829
|
+
const commit = commits.size === 1 ? commits.values().next().value : undefined;
|
|
86830
|
+
if (!commit) {
|
|
86831
|
+
throw new UiError("UI_REGISTRY_UNAVAILABLE", "UI ref " + ref + " did not resolve to a commit.");
|
|
86832
|
+
}
|
|
86833
|
+
return commit;
|
|
86834
|
+
}
|
|
86799
86835
|
async function readUiReleaseSnapshot(identity3, fetcher = fetch) {
|
|
86800
86836
|
const [compatibility, registry2] = await Promise.all([
|
|
86801
86837
|
json4(fetcher, RAW + "/" + identity3.commit + "/tooling/compatibility.json", "UI compatibility metadata"),
|
|
@@ -86907,9 +86943,16 @@ function isSafeRelative2(value3) {
|
|
|
86907
86943
|
function registryItemUrl(release, itemName) {
|
|
86908
86944
|
return RAW + "/" + release.commit + "/registry/public/r/" + encodeURIComponent(itemName) + ".json";
|
|
86909
86945
|
}
|
|
86910
|
-
var NPM_PACKAGE = "https://registry.npmjs.org/@astrale-os/ui", GITHUB_API = "https://api.github.com/repos/astrale-os/ui", RAW = "https://raw.githubusercontent.com/astrale-os/ui", MAX_DOCUMENT_BYTES = 1048576, MAX_REGISTRY_DOCUMENTS = 100;
|
|
86946
|
+
var NPM_PACKAGE = "https://registry.npmjs.org/@astrale-os/ui", GITHUB_API = "https://api.github.com/repos/astrale-os/ui", GITHUB_WEB = "https://github.com/astrale-os/ui", RAW = "https://raw.githubusercontent.com/astrale-os/ui", MAX_DOCUMENT_BYTES = 1048576, MAX_REGISTRY_DOCUMENTS = 100, HttpStatusError;
|
|
86911
86947
|
var init_release = __esm(() => {
|
|
86912
86948
|
init_model7();
|
|
86949
|
+
HttpStatusError = class HttpStatusError extends Error {
|
|
86950
|
+
status;
|
|
86951
|
+
constructor(status) {
|
|
86952
|
+
super("HTTP " + status);
|
|
86953
|
+
this.status = status;
|
|
86954
|
+
}
|
|
86955
|
+
};
|
|
86913
86956
|
});
|
|
86914
86957
|
|
|
86915
86958
|
// src/ui/runner.ts
|
package/package.json
CHANGED
|
@@ -330,6 +330,97 @@ describe('UI release and runner contracts', () => {
|
|
|
330
330
|
)
|
|
331
331
|
})
|
|
332
332
|
|
|
333
|
+
test('resolves the immutable release commit when the anonymous GitHub API is rate limited', async () => {
|
|
334
|
+
const seen: string[] = []
|
|
335
|
+
const fallback = mockFetch(seen)
|
|
336
|
+
const fetcher = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
337
|
+
const url = String(input)
|
|
338
|
+
if (url.includes('/git/ref/tags/')) {
|
|
339
|
+
seen.push(url)
|
|
340
|
+
return Response.json({ message: 'API rate limit exceeded' }, { status: 403 })
|
|
341
|
+
}
|
|
342
|
+
if (url.includes('/releases/tag/')) {
|
|
343
|
+
seen.push(url)
|
|
344
|
+
return new Response(
|
|
345
|
+
`<a data-hovercard-type="commit" href="/astrale-os/ui/commit/${'b'.repeat(40)}">decoy</a>
|
|
346
|
+
<a href="/astrale-os/ui/tree/v0.3.0-beta.0">repository navigation</a>
|
|
347
|
+
${'x'.repeat(35_000)}
|
|
348
|
+
<a href="/astrale-os/ui/tree/v0.3.0-beta.0">tag</a>
|
|
349
|
+
<a data-hovercard-type="commit" href="/astrale-os/ui/commit/${commit}">commit</a>`,
|
|
350
|
+
{ headers: { 'content-type': 'text/html' } },
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
return fallback(input, init)
|
|
354
|
+
}) as typeof fetch
|
|
355
|
+
|
|
356
|
+
const release = await resolveUiRelease('0.3.0-beta.0', fetcher)
|
|
357
|
+
|
|
358
|
+
expect(release.commit).toBe(commit)
|
|
359
|
+
expect(seen).toContain('https://github.com/astrale-os/ui/releases/tag/v0.3.0-beta.0')
|
|
360
|
+
expect(seen).toContain(
|
|
361
|
+
'https://raw.githubusercontent.com/astrale-os/ui/' + commit + '/tooling/compatibility.json',
|
|
362
|
+
)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('rejects a release page without an exact Astrale UI commit target', async () => {
|
|
366
|
+
const fetcher = (async (input: string | URL | Request) => {
|
|
367
|
+
const url = String(input)
|
|
368
|
+
if (url.includes('/git/ref/tags/')) return new Response('limited', { status: 403 })
|
|
369
|
+
if (url.includes('/releases/tag/')) return new Response('<main>release unavailable</main>')
|
|
370
|
+
throw new Error('release snapshot must not be fetched')
|
|
371
|
+
}) as typeof fetch
|
|
372
|
+
|
|
373
|
+
await expect(resolveUiRelease('0.3.0-beta.0', fetcher)).rejects.toMatchObject({
|
|
374
|
+
code: 'UI_REGISTRY_UNAVAILABLE',
|
|
375
|
+
message: 'UI ref v0.3.0-beta.0 did not resolve to a commit.',
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('does not replace an authoritative missing tag with release-page HTML', async () => {
|
|
380
|
+
const seen: string[] = []
|
|
381
|
+
const fetcher = (async (input: string | URL | Request) => {
|
|
382
|
+
const url = String(input)
|
|
383
|
+
seen.push(url)
|
|
384
|
+
if (url.includes('/git/ref/tags/')) return new Response('missing', { status: 404 })
|
|
385
|
+
throw new Error('release page must not be fetched')
|
|
386
|
+
}) as typeof fetch
|
|
387
|
+
|
|
388
|
+
await expect(resolveUiRelease('0.3.0-beta.0', fetcher)).rejects.toMatchObject({
|
|
389
|
+
code: 'UI_REGISTRY_UNAVAILABLE',
|
|
390
|
+
message: 'UI ref v0.3.0-beta.0 returned HTTP 404.',
|
|
391
|
+
})
|
|
392
|
+
expect(seen).toEqual(['https://api.github.com/repos/astrale-os/ui/git/ref/tags/v0.3.0-beta.0'])
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test('bounds declared and streamed release-page HTML before reading a snapshot', async () => {
|
|
396
|
+
for (const oversized of [
|
|
397
|
+
new Response('large', { headers: { 'content-length': '1048577' } }),
|
|
398
|
+
new Response(
|
|
399
|
+
new ReadableStream({
|
|
400
|
+
start(controller) {
|
|
401
|
+
controller.enqueue(new Uint8Array(1_048_577))
|
|
402
|
+
controller.close()
|
|
403
|
+
},
|
|
404
|
+
}),
|
|
405
|
+
),
|
|
406
|
+
]) {
|
|
407
|
+
const seen: string[] = []
|
|
408
|
+
const fetcher = (async (input: string | URL | Request) => {
|
|
409
|
+
const url = String(input)
|
|
410
|
+
seen.push(url)
|
|
411
|
+
if (url.includes('/git/ref/tags/')) return new Response('limited', { status: 403 })
|
|
412
|
+
if (url.includes('/releases/tag/')) return oversized
|
|
413
|
+
throw new Error('release snapshot must not be fetched')
|
|
414
|
+
}) as typeof fetch
|
|
415
|
+
|
|
416
|
+
await expect(resolveUiRelease('0.3.0-beta.0', fetcher)).rejects.toMatchObject({
|
|
417
|
+
code: 'UI_REGISTRY_UNAVAILABLE',
|
|
418
|
+
message: 'UI release v0.3.0-beta.0 exceeds the supported response size.',
|
|
419
|
+
})
|
|
420
|
+
expect(seen).toHaveLength(2)
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
|
|
333
424
|
test('admits a release theme as one canonical consumer-owned CSS target', async () => {
|
|
334
425
|
const release = await resolveUiRelease('0.3.0-beta.0', themeFetch())
|
|
335
426
|
expect(release.registry.items).toEqual([
|
package/src/ui/release.ts
CHANGED
|
@@ -2,11 +2,17 @@ import { UiError, type UiCompatibility, type UiRegistry, type UiRelease } from '
|
|
|
2
2
|
|
|
3
3
|
const NPM_PACKAGE = 'https://registry.npmjs.org/@astrale-os/ui'
|
|
4
4
|
const GITHUB_API = 'https://api.github.com/repos/astrale-os/ui'
|
|
5
|
+
const GITHUB_WEB = 'https://github.com/astrale-os/ui'
|
|
5
6
|
const RAW = 'https://raw.githubusercontent.com/astrale-os/ui'
|
|
6
7
|
const MAX_DOCUMENT_BYTES = 1_048_576
|
|
7
8
|
const MAX_REGISTRY_DOCUMENTS = 100
|
|
8
9
|
|
|
9
10
|
type Fetch = typeof fetch
|
|
11
|
+
class HttpStatusError extends Error {
|
|
12
|
+
constructor(readonly status: number) {
|
|
13
|
+
super('HTTP ' + status)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
10
16
|
type RegistrySource = {
|
|
11
17
|
name?: string
|
|
12
18
|
homepage?: string
|
|
@@ -23,8 +29,24 @@ async function json<T>(fetcher: Fetch, url: string, label: string): Promise<T> {
|
|
|
23
29
|
cause,
|
|
24
30
|
})
|
|
25
31
|
}
|
|
32
|
+
const body = await responseText(response, label)
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(body) as T
|
|
35
|
+
} catch (cause) {
|
|
36
|
+
throw new UiError('UI_REGISTRY_UNAVAILABLE', label + ' returned malformed JSON.', undefined, {
|
|
37
|
+
cause,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function responseText(response: Response, label: string): Promise<string> {
|
|
26
43
|
if (!response.ok) {
|
|
27
|
-
throw new UiError(
|
|
44
|
+
throw new UiError(
|
|
45
|
+
'UI_REGISTRY_UNAVAILABLE',
|
|
46
|
+
label + ' returned HTTP ' + response.status + '.',
|
|
47
|
+
undefined,
|
|
48
|
+
{ cause: new HttpStatusError(response.status) },
|
|
49
|
+
)
|
|
28
50
|
}
|
|
29
51
|
const declaredLength = Number(response.headers.get('content-length'))
|
|
30
52
|
if (Number.isFinite(declaredLength) && declaredLength > MAX_DOCUMENT_BYTES) {
|
|
@@ -52,13 +74,7 @@ async function json<T>(fetcher: Fetch, url: string, label: string): Promise<T> {
|
|
|
52
74
|
body.set(chunk, offset)
|
|
53
75
|
offset += chunk.byteLength
|
|
54
76
|
}
|
|
55
|
-
|
|
56
|
-
return JSON.parse(new TextDecoder().decode(body)) as T
|
|
57
|
-
} catch (cause) {
|
|
58
|
-
throw new UiError('UI_REGISTRY_UNAVAILABLE', label + ' returned malformed JSON.', undefined, {
|
|
59
|
-
cause,
|
|
60
|
-
})
|
|
61
|
-
}
|
|
77
|
+
return new TextDecoder().decode(body)
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
export async function resolveUiRelease(
|
|
@@ -76,11 +92,19 @@ export async function resolveUiRelease(
|
|
|
76
92
|
throw new UiError('UI_REGISTRY_UNAVAILABLE', 'Invalid UI beta release version: ' + version)
|
|
77
93
|
}
|
|
78
94
|
const ref = 'v' + version
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
const commit = await resolveReleaseCommit(ref, fetcher)
|
|
96
|
+
if (!/^[0-9a-f]{40}$/u.test(commit)) {
|
|
97
|
+
throw new UiError('UI_REGISTRY_UNAVAILABLE', 'UI ref ' + ref + ' did not resolve to a commit.')
|
|
98
|
+
}
|
|
99
|
+
return readUiReleaseSnapshot({ version, ref, commit }, fetcher)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function resolveReleaseCommit(ref: string, fetcher: Fetch): Promise<string> {
|
|
103
|
+
try {
|
|
104
|
+
const reference = await json<{
|
|
105
|
+
object: { type: 'commit' | 'tag'; sha: string; url: string }
|
|
106
|
+
}>(fetcher, GITHUB_API + '/git/ref/tags/' + encodeURIComponent(ref), 'UI ref ' + ref)
|
|
107
|
+
return reference.object.type === 'commit'
|
|
84
108
|
? reference.object.sha
|
|
85
109
|
: (
|
|
86
110
|
await json<{ object: { sha: string } }>(
|
|
@@ -89,10 +113,41 @@ export async function resolveUiRelease(
|
|
|
89
113
|
'annotated UI tag ' + ref,
|
|
90
114
|
)
|
|
91
115
|
).object.sha
|
|
92
|
-
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
const status =
|
|
118
|
+
cause instanceof UiError && cause.cause instanceof HttpStatusError
|
|
119
|
+
? cause.cause.status
|
|
120
|
+
: undefined
|
|
121
|
+
if (status !== 403 && status !== 429 && !(status !== undefined && status >= 500)) throw cause
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const label = 'UI release ' + ref
|
|
125
|
+
let response: Response
|
|
126
|
+
try {
|
|
127
|
+
response = await fetcher(GITHUB_WEB + '/releases/tag/' + encodeURIComponent(ref), {
|
|
128
|
+
headers: { accept: 'text/html' },
|
|
129
|
+
})
|
|
130
|
+
} catch (cause) {
|
|
131
|
+
throw new UiError('UI_REGISTRY_UNAVAILABLE', 'Unable to reach ' + label + '.', undefined, {
|
|
132
|
+
cause,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
const html = await responseText(response, label)
|
|
136
|
+
const marker = 'href="/astrale-os/ui/tree/' + ref + '"'
|
|
137
|
+
const markerOffset = html.lastIndexOf(marker)
|
|
138
|
+
const releaseHeader = markerOffset < 0 ? '' : html.slice(markerOffset, markerOffset + 8_192)
|
|
139
|
+
const commits = new Set(
|
|
140
|
+
[
|
|
141
|
+
...releaseHeader.matchAll(
|
|
142
|
+
/data-hovercard-type=["']commit["'][^>]+href=["']\/astrale-os\/ui\/commit\/([0-9a-f]{40})["']/gu,
|
|
143
|
+
),
|
|
144
|
+
].map((match) => match[1]),
|
|
145
|
+
)
|
|
146
|
+
const commit = commits.size === 1 ? commits.values().next().value : undefined
|
|
147
|
+
if (!commit) {
|
|
93
148
|
throw new UiError('UI_REGISTRY_UNAVAILABLE', 'UI ref ' + ref + ' did not resolve to a commit.')
|
|
94
149
|
}
|
|
95
|
-
return
|
|
150
|
+
return commit
|
|
96
151
|
}
|
|
97
152
|
|
|
98
153
|
export async function readUiReleaseSnapshot(
|