omakit 0.6.8 → 0.6.10
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/package.json +1 -1
- package/tools/blocks/commit.json +2 -2
- package/tools/lab/guest.mjs +1 -1
- package/tools/lab/host.mjs +2 -2
- package/tools/lab/inspect.mjs +170 -31
- package/tools/lab/paths.mjs +1 -1
- package/tools/lab/pin.json +10 -14
- package/tools/lab/pin.mjs +78 -13
- package/tools/lab/prune.mjs +10 -7
- package/tools/lab/release.mjs +178 -0
- package/tools/lab/report.mjs +78 -18
- package/tools/lab/run.mjs +27 -9
- package/tools/lab/setup.mjs +301 -68
- package/tools/lab/verify.mjs +149 -20
- package/tools/marketplace/README.md +11 -10
- package/tools/marketplace/cli.mjs +36 -8
- package/tools/marketplace/doctor.mjs +128 -23
- package/tools/marketplace/github.mjs +11 -5
- package/tools/marketplace/options.mjs +1 -1
- package/tools/marketplace/pin.mjs +79 -14
- package/tools/marketplace/preflight.mjs +2 -2
- package/tools/marketplace/registry.mjs +31 -49
- package/tools/marketplace/review-cost.mjs +2 -3
- package/tools/marketplace/submit.mjs +2 -2
- package/tools/marketplace/usage.mjs +19 -16
- package/tools/marketplace/watch.mjs +2 -3
|
@@ -54,11 +54,26 @@
|
|
|
54
54
|
// a rule can change without its version. The other two commits (7dd6e56,
|
|
55
55
|
// 40315f2) touched submission.mjs and the policy module, both executed,
|
|
56
56
|
// and the forms, so they grade advice either way.
|
|
57
|
+
//
|
|
58
|
+
// Nor is "a file omakit reads moved" the same as "what omakit reads out of
|
|
59
|
+
// it moved", for the three files it reads as text and never runs. Measured
|
|
60
|
+
// on 2026-10-01 (M7, docs/evidence/pin-freshness/2026-10-01-grades.json):
|
|
61
|
+
// build-catalog.mjs, the most edited file under scripts/, moved three times
|
|
62
|
+
// after b7b29654 (a166e3ac, b612419c, cb7947ec), the reserved namespace and
|
|
63
|
+
// the category mapping read out of it the same each time, and the blob
|
|
64
|
+
// comparison graded each advice, a release for nothing. So a text-read file
|
|
65
|
+
// whose blob moved is read once at HEAD and each value its `readers`
|
|
66
|
+
// (PIN_READS) take out of it is compared with the pin's, by the same
|
|
67
|
+
// function; one whose every value is unchanged does not count as moved for
|
|
68
|
+
// the grade (`effective`), and one whose value differs is advice with both
|
|
69
|
+
// values printed. Of the five commits since 38060f89 that touched a file in
|
|
70
|
+
// the read set, the tree comparison and the blob comparison grade five
|
|
71
|
+
// advice and this grades two: 7dd6e56 and 40315f2, as above.
|
|
57
72
|
|
|
58
73
|
import { execFileSync } from "node:child_process"
|
|
59
74
|
import { existsSync, readFileSync } from "node:fs"
|
|
60
75
|
import { join } from "node:path"
|
|
61
|
-
import { MARKETPLACE_PIN, PIN_PATHS, POLICY_MODULE, WORDING_READS, marketplacePinDir, pinDiskUsage, pinShape, pinnedReadSet, policyConstants, requirePin } from "./pin.mjs"
|
|
76
|
+
import { MARKETPLACE_PIN, PIN_PATHS, PIN_READS, POLICY_MODULE, WORDING_READS, marketplacePinDir, pinDiskUsage, pinShape, pinnedReadSet, policyConstants, requirePin } from "./pin.mjs"
|
|
62
77
|
import { LIVE_PATHS, headTextUrl } from "./registry.mjs"
|
|
63
78
|
import { credential, defaultBranchHead, getJson, getText, GitHubError } from "./github.mjs"
|
|
64
79
|
import { compareVersions, NPM_REGISTRY, registryLatest, upgradeCommand } from "./upgrade.mjs"
|
|
@@ -68,6 +83,7 @@ import { pathHint } from "./path-hint.mjs"
|
|
|
68
83
|
import { completionStatus } from "./completion-check.mjs"
|
|
69
84
|
import { inspectLab } from "../lab/inspect.mjs"
|
|
70
85
|
import { labDoctorChecks } from "../lab/report.mjs"
|
|
86
|
+
import { checkNewestRelease } from "../lab/release.mjs"
|
|
71
87
|
|
|
72
88
|
export function tool(repoRoot) {
|
|
73
89
|
try {
|
|
@@ -92,6 +108,45 @@ function pinObjectId(pinDir, commit, path) {
|
|
|
92
108
|
return execFileSync("git", ["-C", pinDir, "rev-parse", `${commit}:${path}`], { timeout: 60_000, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
|
93
109
|
}
|
|
94
110
|
|
|
111
|
+
/** A file's text at a commit in the pinned checkout. Local: the read set's blobs are in the checkout. */
|
|
112
|
+
function pinFileText(pinDir, commit, path) {
|
|
113
|
+
return execFileSync("git", ["-C", pinDir, "show", `${commit}:${path}`], { timeout: 60_000, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Each value `readers` takes out of a file, read out of the pin's text and
|
|
118
|
+
* HEAD's by the same function, and whether the two are the same: equal
|
|
119
|
+
* when their JSON is, since every reader returns a string, null or plain
|
|
120
|
+
* data.
|
|
121
|
+
*
|
|
122
|
+
* @returns {Record<string, { pin: unknown, head: unknown, same: boolean }>}
|
|
123
|
+
*/
|
|
124
|
+
export function textReadValues(readers, pinText, headText) {
|
|
125
|
+
return Object.fromEntries(Object.entries(readers).map(([name, read]) => {
|
|
126
|
+
const pin = read(pinText)
|
|
127
|
+
const head = read(headText)
|
|
128
|
+
return [name, { pin, head, same: JSON.stringify(pin) === JSON.stringify(head) }]
|
|
129
|
+
}))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The text-read files of `reads` (those PIN_READS gives `readers`), each
|
|
134
|
+
* with whether its blob moved and the values read out of it at both ends.
|
|
135
|
+
* The pin's text comes from the checkout; HEAD's is fetched once, and only
|
|
136
|
+
* for a file in `moved`: an identical blob has identical values, so an
|
|
137
|
+
* unmoved file's values are the pin's at both ends and cost no request.
|
|
138
|
+
* With nothing moved (a current pin) it reads locally only.
|
|
139
|
+
*/
|
|
140
|
+
export async function textReadsBetween({ pinDir, pinCommit = MARKETPLACE_PIN.commit, headCommit = pinCommit, reads = pinnedReadSet(pinDir), moved = [], fetchText = getText }) {
|
|
141
|
+
const textReads = {}
|
|
142
|
+
for (const { path, readers } of PIN_READS.filter((read) => read.readers && reads.some((entry) => entry.path === read.path))) {
|
|
143
|
+
const atPin = pinFileText(pinDir, pinCommit, path)
|
|
144
|
+
const blobMoved = moved.includes(path)
|
|
145
|
+
textReads[path] = { moved: blobMoved, values: textReadValues(readers, atPin, blobMoved ? await fetchText(headTextUrl(headCommit, path)) : atPin) }
|
|
146
|
+
}
|
|
147
|
+
return textReads
|
|
148
|
+
}
|
|
149
|
+
|
|
95
150
|
/** A PIN_PATHS pattern as a path: "/site/catalog.json" -> "site/catalog.json". */
|
|
96
151
|
const asPath = (pattern) => pattern.replace(/^\/|\/$/g, "")
|
|
97
152
|
|
|
@@ -116,10 +171,20 @@ const FORMS = "/.github/ISSUE_TEMPLATE/"
|
|
|
116
171
|
* `policyAtHead`; otherwise `policyAtHead` is null, because an identical
|
|
117
172
|
* blob has identical constants. The text is never imported.
|
|
118
173
|
*
|
|
174
|
+
* Each file PIN_READS reads as text is compared by what omakit reads out
|
|
175
|
+
* of it, under `textReads`: its `readers` run on the pin's text (from the
|
|
176
|
+
* checkout) and on HEAD's, and each value is returned with both sides and
|
|
177
|
+
* whether they are the same. HEAD's text is read once through `fetchText`
|
|
178
|
+
* only when the file's blob moved, one more GET per such file; an
|
|
179
|
+
* unmoved file's values are the pin's at both ends, and a file gone at
|
|
180
|
+
* HEAD is in `missing` and not here. The text is compared and dropped,
|
|
181
|
+
* never imported.
|
|
182
|
+
*
|
|
119
183
|
* `fetchJson` and `fetchText` are injectable for tests.
|
|
120
184
|
*
|
|
121
185
|
* @returns {Promise<{ changedPaths: string[], reads: number, moved: string[], missing: string[],
|
|
122
|
-
* policyAtHead: { baselineVersion: string, enforcementMode: string }|null
|
|
186
|
+
* policyAtHead: { baselineVersion: string, enforcementMode: string }|null,
|
|
187
|
+
* textReads: Record<string, { moved: boolean, values: Record<string, { pin: unknown, head: unknown, same: boolean }> }> }>}
|
|
123
188
|
*/
|
|
124
189
|
export async function comparePin({ pinDir, headCommit, pinCommit = MARKETPLACE_PIN.commit, fetchJson = getJson, fetchText = getText, reads = pinnedReadSet(pinDir) }) {
|
|
125
190
|
const match = MARKETPLACE_PIN.repository.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
|
|
@@ -163,7 +228,8 @@ export async function comparePin({ pinDir, headCommit, pinCommit = MARKETPLACE_P
|
|
|
163
228
|
}
|
|
164
229
|
}
|
|
165
230
|
const policyAtHead = moved.includes(POLICY_MODULE) ? policyConstants(await fetchText(headTextUrl(headCommit, POLICY_MODULE))) : null
|
|
166
|
-
|
|
231
|
+
const textReads = await textReadsBetween({ pinDir, pinCommit, headCommit, reads: reads.filter((read) => !missing.includes(read.path)), moved, fetchText })
|
|
232
|
+
return { changedPaths, reads: reads.length, moved, missing, policyAtHead, textReads }
|
|
167
233
|
}
|
|
168
234
|
|
|
169
235
|
/** "a", "a and b", "a, b and c". */
|
|
@@ -185,24 +251,36 @@ const verdictBearing = (paths) => paths.filter((path) => !WORDING_READS.includes
|
|
|
185
251
|
* from HEAD, and `pinned`, everything only a new pin can carry; its `moved`
|
|
186
252
|
* and `missing` name the files omakit reads under scripts/ whose blob
|
|
187
253
|
* differs at HEAD; its `policyAtHead` carries the two policy constants
|
|
188
|
-
* there when the policy module moved
|
|
254
|
+
* there when the policy module moved; its `textReads` carries, for each
|
|
255
|
+
* file read as text, the values omakit reads out of it at both ends.
|
|
256
|
+
*
|
|
257
|
+
* A file read as text whose blob moved and whose every value reads the
|
|
258
|
+
* same at HEAD does not count as moved for the grade: omakit takes those
|
|
259
|
+
* values out of it and nothing else, so the move cannot reach a verdict or
|
|
260
|
+
* a line. `effective` is `moved` without such files, and the grade is
|
|
261
|
+
* taken from it; `moved` keeps meaning the blob. Measured on 2026-10-01
|
|
262
|
+
* (M7): build-catalog.mjs moved three times after b7b29654, the reserved
|
|
263
|
+
* namespace and the category mapping read the same each time, and the
|
|
264
|
+
* blob comparison graded the pin advice for it.
|
|
189
265
|
*
|
|
190
266
|
* ok nothing omakit reads moved: HEAD moved elsewhere, or only in
|
|
191
267
|
* the live-read data files, or scripts/ moved in none of the
|
|
192
|
-
* files omakit reads
|
|
193
|
-
*
|
|
268
|
+
* files omakit reads, or only in files read as text whose values
|
|
269
|
+
* are unchanged (named, with how many values).
|
|
270
|
+
* info the only read files that count are ones omakit takes wording
|
|
194
271
|
* or a label out of (WORDING_READS): what it prints may differ
|
|
195
272
|
* at HEAD, what it passes or refuses cannot. A newer omakit will
|
|
196
273
|
* carry the pin. Nothing for a user to do.
|
|
197
274
|
* advice a file omakit executes or reads a rule from moved (a verdict
|
|
198
275
|
* may differ at HEAD, whether or not the two policy constants
|
|
199
276
|
* still read the same, since a rule can change without its
|
|
200
|
-
* version), a read
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* the
|
|
205
|
-
*
|
|
277
|
+
* version), a value read out of a text-read file differs (both
|
|
278
|
+
* values printed), a read file is gone at HEAD, or the form
|
|
279
|
+
* directory moved (the contract itself). The action is `upgrade`
|
|
280
|
+
* (the command, when the caller found a newer omakit published),
|
|
281
|
+
* else that the maintainer is notified; never an issue to open,
|
|
282
|
+
* since the weekly pin-freshness workflow opens the one there is.
|
|
283
|
+
* The two constants are printed beside the grade in both cases.
|
|
206
284
|
*
|
|
207
285
|
* Null for `comparison` means the comparison was not made, which stays
|
|
208
286
|
* advice: HEAD moved and nothing here can say the pin is fine. Full commits
|
|
@@ -217,15 +295,22 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
217
295
|
const pinned = changed.filter((pattern) => !LIVE_PATHS.includes(asPath(pattern)))
|
|
218
296
|
const moved = compared ? comparison.moved : []
|
|
219
297
|
const missing = compared ? comparison.missing : []
|
|
298
|
+
const textReads = comparison?.textReads || {}
|
|
220
299
|
const pinPolicy = { baselineVersion: identity.baselineVersion, enforcementMode: identity.enforcementMode }
|
|
221
300
|
const headPolicy = (compared && comparison.policyAtHead) || pinPolicy
|
|
222
301
|
const policyDiffers = headPolicy.baselineVersion !== pinPolicy.baselineVersion || headPolicy.enforcementMode !== pinPolicy.enforcementMode
|
|
223
302
|
const formMoved = pinned.includes(FORMS)
|
|
303
|
+
// The text-read files that moved, by whether every value read out of them is the same at HEAD.
|
|
304
|
+
const valuesOf = (path) => Object.entries(textReads[path]?.moved ? textReads[path].values : {})
|
|
305
|
+
const unchanged = moved.filter((path) => valuesOf(path).length && valuesOf(path).every(([, value]) => value.same))
|
|
306
|
+
const differing = moved.filter((path) => valuesOf(path).some(([, value]) => !value.same))
|
|
307
|
+
const effective = moved.filter((path) => !unchanged.includes(path))
|
|
224
308
|
const verdictMoved = verdictBearing(moved)
|
|
309
|
+
const verdictEffective = verdictBearing(effective)
|
|
225
310
|
const graded = moved.length > 0 || missing.length > 0 || formMoved
|
|
226
311
|
const state = current ? "ok"
|
|
227
|
-
: comparison === null || policyDiffers || formMoved || missing.length ||
|
|
228
|
-
:
|
|
312
|
+
: comparison === null || policyDiffers || formMoved || missing.length || differing.length || verdictEffective.length ? "advice"
|
|
313
|
+
: effective.length ? "info"
|
|
229
314
|
: "ok"
|
|
230
315
|
|
|
231
316
|
const where = `pin ${identity.commit.slice(0, 7)}; marketplace ${branch} at ${head.commit.slice(0, 7)}`
|
|
@@ -233,7 +318,14 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
233
318
|
if (comparison === null) clauses.push("the paths omakit reads were not compared")
|
|
234
319
|
else if (!changed.length) clauses.push("nothing omakit reads moved")
|
|
235
320
|
else {
|
|
236
|
-
if (
|
|
321
|
+
if (effective.length) clauses.push(`moved since the pin: ${list(effective)}${verdictEffective.length || differing.length ? "" : " (wording and labels only)"}`)
|
|
322
|
+
for (const path of differing) {
|
|
323
|
+
for (const [name, value] of valuesOf(path).filter(([, entry]) => !entry.same)) clauses.push(`${name} ${JSON.stringify(value.pin)} at the pin, ${JSON.stringify(value.head)} at HEAD`)
|
|
324
|
+
}
|
|
325
|
+
if (unchanged.length) {
|
|
326
|
+
const count = unchanged.reduce((sum, path) => sum + valuesOf(path).length, 0)
|
|
327
|
+
clauses.push(`${list(unchanged)} moved, and ${count === 1 ? "the one value" : `the ${count} values`} omakit reads from ${unchanged.length === 1 ? "it" : "them"} ${count === 1 ? "is" : "are"} unchanged`)
|
|
328
|
+
}
|
|
237
329
|
if (missing.length) clauses.push(`gone at HEAD: ${list(missing)}`)
|
|
238
330
|
if (formMoved) clauses.push(`the form moved (${asPath(FORMS)}/)`)
|
|
239
331
|
if (pinned.includes("/scripts/") && !moved.length && !missing.length) clauses.push(`scripts/ moved in none of the ${comparison.reads} files omakit reads`)
|
|
@@ -244,7 +336,7 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
244
336
|
const detail = current ? `the pin is the marketplace's current ${branch}-branch HEAD` : `${where}; ${clauses.join("; ")}${tail}`
|
|
245
337
|
|
|
246
338
|
const action = state === "info"
|
|
247
|
-
? `${
|
|
339
|
+
? `${effective.length === 1 ? "That file is" : "Those files are"} read for wording and labels, not for a pass or a refusal; what omakit prints may differ at HEAD, what it decides cannot. A newer omakit will carry the pin; nothing to do.`
|
|
248
340
|
: state === "advice"
|
|
249
341
|
? upgrade
|
|
250
342
|
? `A newer omakit is published and may carry the pin: run \`${upgrade}\`. Until then every verdict here is the pin's, and the marketplace's own run on your issue is the one that counts.`
|
|
@@ -267,7 +359,9 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
267
359
|
reads: comparison.reads,
|
|
268
360
|
moved,
|
|
269
361
|
verdictMoved,
|
|
362
|
+
effective,
|
|
270
363
|
missing,
|
|
364
|
+
textReads,
|
|
271
365
|
policy: { pin: pinPolicy, head: headPolicy },
|
|
272
366
|
}),
|
|
273
367
|
},
|
|
@@ -278,11 +372,12 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
278
372
|
* @param {{ repoRoot: string, offline?: boolean, env?: object, npmPrefix?: () => string|null,
|
|
279
373
|
* resolveHead?: typeof defaultBranchHead, latest?: typeof registryLatest, compare?: typeof comparePin }} options
|
|
280
374
|
* `env` and `npmPrefix` are injectable for tests of the PATH check;
|
|
281
|
-
* `resolveHead`, `latest` and `
|
|
282
|
-
* read the network, whose defaults are the tool's one HEAD
|
|
283
|
-
* one registry read
|
|
375
|
+
* `resolveHead`, `latest`, `compare` and `newestRelease` for tests of the
|
|
376
|
+
* checks that read the network, whose defaults are the tool's one HEAD
|
|
377
|
+
* resolver, its one registry read, the pin comparison above and the
|
|
378
|
+
* lab's read of Omarchy's release list (tools/lab/release.mjs).
|
|
284
379
|
*/
|
|
285
|
-
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = registryLatest, compare = comparePin }) {
|
|
380
|
+
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = registryLatest, compare = comparePin, newestRelease = checkNewestRelease }) {
|
|
286
381
|
// Optional: told what is being read while the network answers. Never
|
|
287
382
|
// affects the result.
|
|
288
383
|
const phase = onPhase || (() => {})
|
|
@@ -394,10 +489,14 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
394
489
|
phase("reading the marketplace's current default-branch HEAD")
|
|
395
490
|
try {
|
|
396
491
|
const head = await resolveHead(MARKETPLACE_PIN.repository)
|
|
397
|
-
let comparison
|
|
492
|
+
let comparison
|
|
398
493
|
if (head.commit !== identity.commit) {
|
|
399
494
|
phase("comparing each file omakit reads between the pin and HEAD")
|
|
400
495
|
comparison = await compare({ pinDir: dir, headCommit: head.commit, pinCommit: identity.commit })
|
|
496
|
+
} else {
|
|
497
|
+
// The pin is HEAD: nothing moved, and the values read as text are the pin's, read locally.
|
|
498
|
+
const reads = pinnedReadSet(dir)
|
|
499
|
+
comparison = { changedPaths: [], reads: reads.length, moved: [], missing: [], policyAtHead: null, textReads: await textReadsBetween({ pinDir: dir, pinCommit: identity.commit, reads }) }
|
|
401
500
|
}
|
|
402
501
|
checks.push(pinFreshness(identity, head, comparison, { upgrade }))
|
|
403
502
|
} catch (error) {
|
|
@@ -431,9 +530,15 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
431
530
|
// the base, each with its measured reason, every one advice and never a
|
|
432
531
|
// problem, because the lab is optional and doctor installs nothing.
|
|
433
532
|
// Read-only: inspectLab creates no directory, verifies nothing online
|
|
434
|
-
// and starts nothing.
|
|
533
|
+
// and starts nothing. The one read is Omarchy's release list, so the
|
|
534
|
+
// lab says when a newer release is out; --offline skips it.
|
|
535
|
+
let newest = { checked: false, code: "offline", reason: "--offline" }
|
|
536
|
+
if (!offline) {
|
|
537
|
+
phase("looking for the newest Omarchy release")
|
|
538
|
+
newest = await newestRelease({ signal: AbortSignal.timeout(45_000) })
|
|
539
|
+
}
|
|
435
540
|
phase("reading the lab")
|
|
436
|
-
for (const check of labDoctorChecks(await inspectLab({ env }))) checks.push(check)
|
|
541
|
+
for (const check of labDoctorChecks(await inspectLab({ env, newest }))) checks.push(check)
|
|
437
542
|
|
|
438
543
|
return { checks, problems: checks.filter((check) => check.state === "problem").length }
|
|
439
544
|
}
|
|
@@ -119,7 +119,7 @@ export const CREDENTIAL_HOST = "api.github.com"
|
|
|
119
119
|
*/
|
|
120
120
|
export const GET_DEADLINE_MS = 20_000
|
|
121
121
|
|
|
122
|
-
async function get(url, { accept, signal, rangeFrom = 0 } = {}) {
|
|
122
|
+
async function get(url, { accept, signal, rangeFrom = 0, rangeEnd = null } = {}) {
|
|
123
123
|
const { host } = new URL(url)
|
|
124
124
|
// The credential is GitHub's and goes to GitHub's API and nowhere else.
|
|
125
125
|
// Measured before this held: `omakit upgrade` and `doctor` sent the gh
|
|
@@ -136,7 +136,9 @@ async function get(url, { accept, signal, rangeFrom = 0 } = {}) {
|
|
|
136
136
|
// A resumed download asks for the rest of the object; the lab's ISO
|
|
137
137
|
// fetch is the one caller, and a server that ignores the range answers
|
|
138
138
|
// 200 from the start, which the caller handles by starting over.
|
|
139
|
-
|
|
139
|
+
// The lab's release search asks for the first byte alone (`rangeEnd` 0)
|
|
140
|
+
// to read the ISO's size from Content-Range without fetching the image.
|
|
141
|
+
if (rangeFrom > 0 || rangeEnd !== null) headers.range = `bytes=${rangeFrom}-${rangeEnd ?? ""}`
|
|
140
142
|
let response
|
|
141
143
|
try {
|
|
142
144
|
response = await fetch(url, { method: "GET", headers, redirect: "follow", signal: signal || AbortSignal.timeout(GET_DEADLINE_MS) })
|
|
@@ -144,8 +146,12 @@ async function get(url, { accept, signal, rangeFrom = 0 } = {}) {
|
|
|
144
146
|
// Node reports every transport failure as "fetch failed" with the real
|
|
145
147
|
// reason in `cause`. A person needs the reason, and the CLI keys its
|
|
146
148
|
// remedy on the code, so both are carried out of here.
|
|
147
|
-
const cause = error?.name === "TimeoutError" ? `no answer within ${GET_DEADLINE_MS / 1000} s` : error?.cause?.code || error?.cause?.message || error?.message || "fetch failed"
|
|
148
149
|
const { host, pathname } = new URL(url)
|
|
150
|
+
// The caller stopped it (SIGINT, SIGTERM): that is an interrupt, not a
|
|
151
|
+
// network that did not answer, and the caller has to be able to tell.
|
|
152
|
+
if (signal?.aborted && signal.reason?.name !== "TimeoutError") throw new GitHubError("interrupted", `interrupted while reading ${pathname} from ${host}`)
|
|
153
|
+
// A caller's own deadline is the caller's figure; only the default is 20 s.
|
|
154
|
+
const cause = error?.name === "TimeoutError" ? (signal ? "no answer before the caller's deadline" : `no answer within ${GET_DEADLINE_MS / 1000} s`) : error?.cause?.code || error?.cause?.message || error?.message || "fetch failed"
|
|
149
155
|
throw new GitHubError("network-unavailable", `${host} did not answer (${cause}) while reading ${pathname}`)
|
|
150
156
|
}
|
|
151
157
|
if (!response.ok) {
|
|
@@ -178,8 +184,8 @@ export async function getText(url, accept) {
|
|
|
178
184
|
* same literal method, and the credential stays with api.github.com; the
|
|
179
185
|
* ISO origin (iso.omarchy.org) never sees it.
|
|
180
186
|
*/
|
|
181
|
-
export async function getStream(url, { rangeFrom = 0, signal } = {}) {
|
|
182
|
-
return get(url, { accept: "application/octet-stream", signal, rangeFrom })
|
|
187
|
+
export async function getStream(url, { rangeFrom = 0, rangeEnd = null, signal } = {}) {
|
|
188
|
+
return get(url, { accept: "application/octet-stream", signal, rangeFrom, rangeEnd })
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
/** Parse a marketplace issue URL into its parts. */
|
|
@@ -31,7 +31,7 @@ export const ACCEPTED = Object.freeze({
|
|
|
31
31
|
inspect: Object.freeze({ valued: ["--out"], flags: ["--full", "--json", "--offline", "--allow-dirty"], positionals: 1 }),
|
|
32
32
|
add: Object.freeze({ valued: [], flags: ["--update", "--json"], positionals: 2 }),
|
|
33
33
|
weigh: Object.freeze({ valued: ["--runs", "--window", "--settle", "--out"], flags: ["--all", "--list", "--json", "--yes"], positionals: 1 }),
|
|
34
|
-
lab: Object.freeze({ valued: ["--runs", "--from", "--toolchain", "--out"], flags: ["--json", "--yes", "--verify", "--plugins", "--keep-iso", "--records"], positionals: 2 }),
|
|
34
|
+
lab: Object.freeze({ valued: ["--runs", "--from", "--toolchain", "--out"], flags: ["--json", "--yes", "--verify", "--plugins", "--keep-iso", "--records", "--offline"], positionals: 2 }),
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
/** The accepted options of a command in the words a refusal prints: `--runs N, --all, ...`. */
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// of which 168 MB is preview imagery and 151 MB is history, and omakit reads
|
|
7
7
|
// twenty files out of it, sixteen of them under scripts/ (PIN_READS). A
|
|
8
8
|
// blob-filtered, sparsely checked out fetch of just those paths is 15 MB and
|
|
9
|
-
// takes 2 seconds instead of 17
|
|
9
|
+
// takes 2 seconds instead of 17; at pin b441b4f0 (2026-10-01) the same fetch
|
|
10
|
+
// was 23 MB on disk in 3.1 seconds. PIN_PATHS below is the whole list, and
|
|
10
11
|
// tests/unit/pin.test.mjs fails if any module starts reading a path outside
|
|
11
12
|
// it, because on a partial clone such a read would quietly reach for the
|
|
12
13
|
// network instead of failing.
|
|
@@ -27,8 +28,8 @@ export class PinError extends Error {
|
|
|
27
28
|
|
|
28
29
|
export const MARKETPLACE_PIN = Object.freeze({
|
|
29
30
|
repository: "https://github.com/omacom/omarchy-plugin-marketplace",
|
|
30
|
-
commit: "
|
|
31
|
-
commitSubject: "Add
|
|
31
|
+
commit: "b441b4f059132b5b7203a7563027f2b3a9adaa1d",
|
|
32
|
+
commitSubject: "Add LINE for Omarchy plugin (#9470)",
|
|
32
33
|
baselineVersion: "3",
|
|
33
34
|
enforcementMode: "selective",
|
|
34
35
|
})
|
|
@@ -40,8 +41,9 @@ export const MARKETPLACE_PIN = Object.freeze({
|
|
|
40
41
|
* /scripts/ the official submission parser, baseline
|
|
41
42
|
* scanner, policy, report and record modules,
|
|
42
43
|
* and build-catalog.mjs read as text for the
|
|
43
|
-
* reserved plugin-id namespace
|
|
44
|
-
*
|
|
44
|
+
* reserved plugin-id namespace and the
|
|
45
|
+
* category mapping. Taken whole because
|
|
46
|
+
* those modules import each other.
|
|
45
47
|
* /registry.json retired plugin ids, listed repositories
|
|
46
48
|
* /site/catalog.json listed plugin ids
|
|
47
49
|
* /.github/ISSUE_TEMPLATE/ the submission form: the whole contract
|
|
@@ -83,8 +85,19 @@ export const POLICY_MODULE = "scripts/security-baseline-policy.mjs"
|
|
|
83
85
|
* approve-submission.mjs watch.mjs, text: one label
|
|
84
86
|
* approve-plugin-update.mjs review-cost.mjs, text: one label
|
|
85
87
|
*
|
|
88
|
+
* A file read as text carries `readers`: each value omakit takes out of
|
|
89
|
+
* it, by name, as a function from the file's text to the value. This is
|
|
90
|
+
* the one list of what is read out of such a file, and the functions are
|
|
91
|
+
* the ones the modules above call on the pin's text and the ones `omakit
|
|
92
|
+
* doctor` calls on the pin's text and on HEAD's, so the two sides of the
|
|
93
|
+
* comparison cannot read differently. Measured on 2026-10-01 (M7): the
|
|
94
|
+
* three commits that moved build-catalog.mjs since b7b29654 changed
|
|
95
|
+
* neither of the two values read out of it, and the blob comparison
|
|
96
|
+
* graded each of them advice.
|
|
97
|
+
*
|
|
86
98
|
* tests/unit/pin.test.mjs derives this list from the sources and fails on
|
|
87
|
-
* a read that is not here, or one listed the wrong way round
|
|
99
|
+
* a read that is not here, or one listed the wrong way round, and holds
|
|
100
|
+
* `readers` to exactly the files read as text.
|
|
88
101
|
*/
|
|
89
102
|
export const PIN_READS = Object.freeze([
|
|
90
103
|
Object.freeze({ path: "scripts/submission.mjs", imported: true }),
|
|
@@ -94,9 +107,9 @@ export const PIN_READS = Object.freeze([
|
|
|
94
107
|
Object.freeze({ path: "scripts/security-baseline-report.mjs", imported: true }),
|
|
95
108
|
Object.freeze({ path: "scripts/security-baseline-record.mjs", imported: true }),
|
|
96
109
|
Object.freeze({ path: "scripts/submission-feedback.mjs", imported: true }),
|
|
97
|
-
Object.freeze({ path: "scripts/build-catalog.mjs", imported: false }),
|
|
98
|
-
Object.freeze({ path: "scripts/approve-submission.mjs", imported: false }),
|
|
99
|
-
Object.freeze({ path: "scripts/approve-plugin-update.mjs", imported: false }),
|
|
110
|
+
Object.freeze({ path: "scripts/build-catalog.mjs", imported: false, readers: Object.freeze({ reservedNamespace, catalogPresentation }) }),
|
|
111
|
+
Object.freeze({ path: "scripts/approve-submission.mjs", imported: false, readers: Object.freeze({ validatedLabel }) }),
|
|
112
|
+
Object.freeze({ path: "scripts/approve-plugin-update.mjs", imported: false, readers: Object.freeze({ updateLabel }) }),
|
|
100
113
|
])
|
|
101
114
|
|
|
102
115
|
/**
|
|
@@ -126,9 +139,9 @@ export const RELATIVE_IMPORT = /\bfrom\s+["'](\.\.?\/[^"']+)["']/g
|
|
|
126
139
|
* no guard; Node's ESM loader requires an extension.) If one of these ever
|
|
127
140
|
* appears in an executed file of the pinned read set, pinnedReadSet() could
|
|
128
141
|
* miss a file, and tests/unit/pin.test.mjs fails on the file and line
|
|
129
|
-
* instead of doctor staying quiet. Measured at
|
|
130
|
-
* the 13 executed files uses any of them (the three read
|
|
131
|
-
* scanned, since nothing they import is loaded).
|
|
142
|
+
* instead of doctor staying quiet. Measured at pins b7b29654 and
|
|
143
|
+
* b441b4f0: none of the 13 executed files uses any of them (the three read
|
|
144
|
+
* as text are not scanned, since nothing they import is loaded).
|
|
132
145
|
*/
|
|
133
146
|
export const UNFOLLOWED_IMPORTS = Object.freeze([
|
|
134
147
|
Object.freeze({ form: "dynamic import()", pattern: /\bimport\s*\(/ }),
|
|
@@ -164,8 +177,8 @@ export function unfollowedImports(pinDir, reads = pinnedReadSet(pinDir)) {
|
|
|
164
177
|
* the file that imports it or null for one omakit opens itself. A
|
|
165
178
|
* specifier that leaves scripts/ (the marketplace's site/ assets) or names
|
|
166
179
|
* a package (`sharp`) is not followed: PIN_PATHS does not fetch it, so
|
|
167
|
-
* omakit could not read it. Measured at
|
|
168
|
-
* under scripts/, 10 opened by omakit and 6 imported by those.
|
|
180
|
+
* omakit could not read it. Measured at pins b7b29654 and b441b4f0: 16 of
|
|
181
|
+
* the 34 files under scripts/, 10 opened by omakit and 6 imported by those.
|
|
169
182
|
*
|
|
170
183
|
* @returns {{ path: string, imported: boolean, via: string|null }[]}
|
|
171
184
|
*/
|
|
@@ -243,6 +256,58 @@ export function policyConstants(text) {
|
|
|
243
256
|
}
|
|
244
257
|
}
|
|
245
258
|
|
|
259
|
+
// The readers PIN_READS registers for the files read as text. Each takes
|
|
260
|
+
// the file's text and returns the value, or null where the text does not
|
|
261
|
+
// carry it, the way policyConstants() does for the policy module: no file
|
|
262
|
+
// is opened here and no module is loaded, so the same function reads the
|
|
263
|
+
// pin's copy for a check and HEAD's copy for doctor's comparison.
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The reserved plugin-id namespace as the marketplace states it in
|
|
267
|
+
* `build-catalog.mjs`, next to its own `reserved-plugin-id` check, with
|
|
268
|
+
* its trailing dot. Read, not copied, so a pin update moves it.
|
|
269
|
+
*/
|
|
270
|
+
export function reservedNamespace(text) {
|
|
271
|
+
const source = String(text || "")
|
|
272
|
+
const match = source.match(/startsWith\("([A-Za-z0-9.\-_]+\.)"\)\s*\)\s*\{\s*\n\s*checkError\("reserved-plugin-id"/)
|
|
273
|
+
|| source.match(/checkError\("reserved-plugin-id",\s*`[^`]*\$\{[^}]*\}:\s*the ([A-Za-z0-9.\-_]+)\.\*\s+namespace is reserved`/)
|
|
274
|
+
if (!match) return null
|
|
275
|
+
return match[1].endsWith(".") ? match[1] : `${match[1]}.`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* How the marketplace itself presents a plugin it lists, read out of
|
|
280
|
+
* `build-catalog.mjs` rather than copied: `categoryFor(kinds)` maps the
|
|
281
|
+
* manifest's kinds to a category in order of its `if` lines, with a
|
|
282
|
+
* fallback, and the tags are the first three kinds lowercased. `omakit
|
|
283
|
+
* submit` offers these as the default answer when it has to ask for a
|
|
284
|
+
* category and tags, so the default is the marketplace's own choice; a
|
|
285
|
+
* mapping that cannot be read offers no default. tests/unit/registry-
|
|
286
|
+
* figures.test.mjs pins what the mapping is at this commit, so a
|
|
287
|
+
* marketplace that changes it fails the suite until the docs follow.
|
|
288
|
+
*
|
|
289
|
+
* @returns {{ rules: Array<{ kinds: string[], category: string }>, fallback: string|null, tagsFromKinds: boolean }}
|
|
290
|
+
*/
|
|
291
|
+
export function catalogPresentation(text) {
|
|
292
|
+
const source = String(text || "")
|
|
293
|
+
const body = source.match(/function categoryFor\([^)]*\)\s*\{([\s\S]*?)\n\}/)?.[1] || ""
|
|
294
|
+
const rules = [...body.matchAll(/if \(((?:kinds\.includes\("[^"]+"\)(?:\s*\|\|\s*)?)+)\) return "([^"]+)";/g)]
|
|
295
|
+
.map((match) => ({ kinds: [...match[1].matchAll(/"([^"]+)"/g)].map((kind) => kind[1]), category: match[2] }))
|
|
296
|
+
const fallback = body.match(/\n\s*return "([^"]+)";\s*$/)?.[1] || null
|
|
297
|
+
const tagsFromKinds = /tags:\s*kinds\.slice\(0,\s*3\)\.map\(\(kind\) => kind\.toLowerCase\(\)\)/.test(source)
|
|
298
|
+
return { rules, fallback, tagsFromKinds }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The label `approve-submission.mjs` requires beside `submission` before it approves: the one `omakit watch` reads as validated. */
|
|
302
|
+
export function validatedLabel(text) {
|
|
303
|
+
return String(text || "").match(/for \(const required of \["submission", "([^"]+)"/)?.[1] || null
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The first label `approve-plugin-update.mjs` requires: the one an update issue carries, for review cost. */
|
|
307
|
+
export function updateLabel(text) {
|
|
308
|
+
return String(text || "").match(/for \(const required of \["([^"]+)"/)?.[1] || null
|
|
309
|
+
}
|
|
310
|
+
|
|
246
311
|
/** Identity of the checkout at `dir`: commit plus the policy constants read from the pinned source. */
|
|
247
312
|
function readPinIdentity(dir) {
|
|
248
313
|
const commit = git(dir, ["rev-parse", "HEAD"]).trim()
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// its outcome will cause on submission.
|
|
4
4
|
//
|
|
5
5
|
// Measured reason this runs before submitting (docs/MEASUREMENTS.md M4): of the
|
|
6
|
-
//
|
|
7
|
-
// `passed`, 1,
|
|
6
|
+
// 4,449 listed sources with a recorded baseline at the pin, it produced 2,451
|
|
7
|
+
// `passed`, 1,962 `review-required` and 36 `needs-fixes` (counted by
|
|
8
8
|
// registry.mjs baselineFigures, pinned by tests/unit/registry-figures.test.mjs). `review-required` is not a defect and
|
|
9
9
|
// needs no source change, but it does mean a human must look, and it is the
|
|
10
10
|
// single largest determinant of whether a submission waits on a person. An
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
28
28
|
import { dirname, join } from "node:path"
|
|
29
|
-
import { MARKETPLACE_PIN, POLICY_MODULE, requirePin } from "./pin.mjs"
|
|
29
|
+
import { MARKETPLACE_PIN, PIN_READS, POLICY_MODULE, catalogPresentation, requirePin, reservedNamespace } from "./pin.mjs"
|
|
30
30
|
import { defaultBranchHead, getJson } from "./github.mjs"
|
|
31
31
|
import { omakitCacheDir } from "./paths.mjs"
|
|
32
32
|
|
|
@@ -38,12 +38,14 @@ export const CATALOG_BUILDER_PATH = "scripts/build-catalog.mjs"
|
|
|
38
38
|
export const LIVE_PATHS = Object.freeze([REGISTRY_PATH, CATALOG_PATH])
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* The
|
|
42
|
-
* reads the policy module at HEAD to compare its two
|
|
43
|
-
* pin's (pin.mjs policyConstants) and
|
|
44
|
-
*
|
|
41
|
+
* The files read from HEAD as text and used for nothing but a comparison:
|
|
42
|
+
* `omakit doctor` reads the policy module at HEAD to compare its two
|
|
43
|
+
* constants with the pin's (pin.mjs policyConstants), and each file
|
|
44
|
+
* PIN_READS reads as text to compare the values its `readers` take out of
|
|
45
|
+
* it, and drops the text. None is ever imported, cached or a source of a
|
|
46
|
+
* rule; the rules stay the pin's.
|
|
45
47
|
*/
|
|
46
|
-
export const HEAD_TEXT_PATHS = Object.freeze([POLICY_MODULE])
|
|
48
|
+
export const HEAD_TEXT_PATHS = Object.freeze([POLICY_MODULE, ...PIN_READS.filter((read) => read.readers).map((read) => read.path)])
|
|
47
49
|
|
|
48
50
|
export class RegistryError extends Error {
|
|
49
51
|
constructor(code, message) {
|
|
@@ -62,45 +64,17 @@ function readJson(pinDir, relative) {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
|
-
* The
|
|
66
|
-
*
|
|
67
|
-
*
|
|
67
|
+
* The text of the pinned catalog builder, the one file under scripts/ this
|
|
68
|
+
* module reads: never imported (it imports `sharp`), read for the two
|
|
69
|
+
* values PIN_READS registers for it, `reservedNamespace` and
|
|
70
|
+
* `catalogPresentation` (pin.mjs). Those two are re-exported here, where
|
|
71
|
+
* the checks that use them have always found them.
|
|
68
72
|
*/
|
|
69
|
-
export function
|
|
70
|
-
|
|
71
|
-
const match = source.match(/startsWith\("([A-Za-z0-9.\-_]+\.)"\)\s*\)\s*\{\s*\n\s*checkError\("reserved-plugin-id"/)
|
|
72
|
-
|| source.match(/checkError\("reserved-plugin-id",\s*`[^`]*\$\{[^}]*\}:\s*the ([A-Za-z0-9.\-_]+)\.\*\s+namespace is reserved`/)
|
|
73
|
-
if (!match) {
|
|
74
|
-
throw new RegistryError(
|
|
75
|
-
"pin-unreadable",
|
|
76
|
-
`cannot read the reserved plugin-id namespace from ${CATALOG_BUILDER_PATH} at the pin; refusing to guess it`,
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
return match[1].endsWith(".") ? match[1] : `${match[1]}.`
|
|
73
|
+
export function catalogBuilderText(pinDir) {
|
|
74
|
+
return readFileSync(join(pinDir, CATALOG_BUILDER_PATH), "utf8")
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
* How the marketplace itself presents a plugin it lists, read out of the
|
|
84
|
-
* pinned `build-catalog.mjs` rather than copied: `categoryFor(kinds)` maps the
|
|
85
|
-
* manifest's kinds to a category in order of its `if` lines, with a fallback,
|
|
86
|
-
* and the tags are the first three kinds lowercased. `omakit submit` offers
|
|
87
|
-
* these as the default answer when it has to ask for a category and tags, so
|
|
88
|
-
* the default is the marketplace's own choice; a mapping that cannot be read
|
|
89
|
-
* offers no default. tests/unit/registry-figures.test.mjs pins what the
|
|
90
|
-
* mapping is at this commit, so a marketplace that changes it fails the suite
|
|
91
|
-
* until the docs follow.
|
|
92
|
-
*
|
|
93
|
-
* @returns {{ rules: Array<{ kinds: string[], category: string }>, fallback: string|null, tagsFromKinds: boolean }}
|
|
94
|
-
*/
|
|
95
|
-
export function catalogPresentation(pinDir) {
|
|
96
|
-
const source = readFileSync(join(pinDir, CATALOG_BUILDER_PATH), "utf8")
|
|
97
|
-
const body = source.match(/function categoryFor\([^)]*\)\s*\{([\s\S]*?)\n\}/)?.[1] || ""
|
|
98
|
-
const rules = [...body.matchAll(/if \(((?:kinds\.includes\("[^"]+"\)(?:\s*\|\|\s*)?)+)\) return "([^"]+)";/g)]
|
|
99
|
-
.map((match) => ({ kinds: [...match[1].matchAll(/"([^"]+)"/g)].map((kind) => kind[1]), category: match[2] }))
|
|
100
|
-
const fallback = body.match(/\n\s*return "([^"]+)";\s*$/)?.[1] || null
|
|
101
|
-
const tagsFromKinds = /tags:\s*kinds\.slice\(0,\s*3\)\.map\(\(kind\) => kind\.toLowerCase\(\)\)/.test(source)
|
|
102
|
-
return { rules, fallback, tagsFromKinds }
|
|
103
|
-
}
|
|
77
|
+
export { catalogPresentation, reservedNamespace }
|
|
104
78
|
|
|
105
79
|
/**
|
|
106
80
|
* The marketplace's own presentation for a manifest's kinds: the category its
|
|
@@ -149,8 +123,8 @@ export function sameRepository(a, b) {
|
|
|
149
123
|
* a branch name, so the two files always come from the same commit and the
|
|
150
124
|
* commit named in the output is the one they came from; never a path outside
|
|
151
125
|
* LIVE_PATHS, so nothing executable can arrive this way. (headTextUrl below
|
|
152
|
-
* reaches the same host for
|
|
153
|
-
* run.)
|
|
126
|
+
* reaches the same host for the text of the files doctor compares, which is
|
|
127
|
+
* compared and never run.)
|
|
154
128
|
*/
|
|
155
129
|
export function liveFileUrl(commit, path) {
|
|
156
130
|
if (!/^[0-9a-f]{40}$/.test(String(commit))) {
|
|
@@ -163,17 +137,17 @@ export function liveFileUrl(commit, path) {
|
|
|
163
137
|
}
|
|
164
138
|
|
|
165
139
|
/**
|
|
166
|
-
* The same URL shape for the
|
|
140
|
+
* The same URL shape for the files doctor reads from HEAD as text:
|
|
167
141
|
* HEAD_TEXT_PATHS, at a 40-character commit, through the same host and the
|
|
168
|
-
* same GET call site.
|
|
169
|
-
* refuses
|
|
142
|
+
* same GET call site. Their text is compared and dropped; `liveFileUrl`
|
|
143
|
+
* still refuses them, so nothing reads them as data.
|
|
170
144
|
*/
|
|
171
145
|
export function headTextUrl(commit, path) {
|
|
172
146
|
if (!/^[0-9a-f]{40}$/.test(String(commit))) {
|
|
173
147
|
throw new RegistryError("usage", `a marketplace file is read at a 40-character commit, not "${commit}"`)
|
|
174
148
|
}
|
|
175
149
|
if (!HEAD_TEXT_PATHS.includes(path)) {
|
|
176
|
-
throw new RegistryError("usage", `${path} is never read from HEAD as text; only ${HEAD_TEXT_PATHS.join("
|
|
150
|
+
throw new RegistryError("usage", `${path} is never read from HEAD as text; only ${HEAD_TEXT_PATHS.join(", ")} are`)
|
|
177
151
|
}
|
|
178
152
|
return rawFileUrl(commit, path)
|
|
179
153
|
}
|
|
@@ -371,9 +345,17 @@ export function idUniverse(options = {}) {
|
|
|
371
345
|
throw new RegistryError("pin-unreadable", "the pinned catalog and registry produced no listed ids or repositories")
|
|
372
346
|
}
|
|
373
347
|
|
|
348
|
+
const reservedPrefix = reservedNamespace(catalogBuilderText(pinDir))
|
|
349
|
+
if (!reservedPrefix) {
|
|
350
|
+
throw new RegistryError(
|
|
351
|
+
"pin-unreadable",
|
|
352
|
+
`cannot read the reserved plugin-id namespace from ${CATALOG_BUILDER_PATH} at the pin; refusing to guess it`,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
374
356
|
return {
|
|
375
357
|
pinDir,
|
|
376
|
-
reservedPrefix
|
|
358
|
+
reservedPrefix,
|
|
377
359
|
listedIds,
|
|
378
360
|
retiredIds,
|
|
379
361
|
listedRepositories,
|