omakit 0.6.9 → 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/paths.mjs +1 -1
- package/tools/lab/pin.json +1 -1
- package/tools/lab/verify.mjs +2 -2
- package/tools/marketplace/README.md +3 -3
- package/tools/marketplace/cli.mjs +1 -1
- package/tools/marketplace/doctor.mjs +114 -17
- 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/watch.mjs +2 -3
package/package.json
CHANGED
package/tools/blocks/commit.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"recordedAt": "2026-
|
|
2
|
+
"commit": "517b87d52ba42465a12ab5722e979ed6f8d27805",
|
|
3
|
+
"recordedAt": "2026-10-01T02:52:49.108Z",
|
|
4
4
|
"how": "Written by `node tools/blocks/record-commit.mjs` in the release workflow before `npm pack`, so a packaged omakit, which has no Git checkout, still names the commit its block files come from. In a checkout this file is null and `git rev-parse HEAD` is the source; a package with null here was packed without the release step, and `omakit add` refuses to stamp a header it cannot name."
|
|
5
5
|
}
|
package/tools/lab/guest.mjs
CHANGED
|
@@ -134,7 +134,7 @@ export async function clearStartupNotifications(guest, { onPhase = () => {} } =
|
|
|
134
134
|
* the package or from a linked checkout (`OMARCHY_PATH` in
|
|
135
135
|
* /etc/omarchy.conf; a dev-linked guest names `.local/share/omarchy`).
|
|
136
136
|
* This is the line the old gates never wrote (docs/history/2026-09-18-lab-
|
|
137
|
-
* inventory.md P8), and
|
|
137
|
+
* inventory.md P8), and docs/LAB.md's run identity requires.
|
|
138
138
|
*/
|
|
139
139
|
export function guestIdentity(guest) {
|
|
140
140
|
const read = (command) => sshGuest(guest, command).stdout.trim()
|
package/tools/lab/host.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What the host has and what it lacks, each with its measured reason.
|
|
2
2
|
//
|
|
3
3
|
// Read-only: every probe is a stat, a read of /proc, or a `--version`.
|
|
4
|
-
// Nothing here installs anything (
|
|
4
|
+
// Nothing here installs anything (docs/LAB.md: lab-only host
|
|
5
5
|
// capabilities are preflighted and reported, never installed). Measured
|
|
6
6
|
// before this (docs/history/2026-09-18-lab-inventory.md P10, P20): the
|
|
7
7
|
// upstream harness ran `omarchy-pkg-add` for six packages at the top of
|
|
@@ -168,7 +168,7 @@ export function probeRunHost({ pin = labPin(), run, env = process.env } = {}) {
|
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* The vCPU count a run gives the guest: every logical CPU, which is what
|
|
171
|
-
* the toolchain's `-smp $(nproc)` gave the reference build (32,
|
|
171
|
+
* the toolchain's `-smp $(nproc)` gave the reference build (32, M14). A
|
|
172
172
|
* smaller number would be a guess about what a suite needs, and no run
|
|
173
173
|
* has measured one.
|
|
174
174
|
*/
|
package/tools/lab/paths.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export function labStateDir(env = process.env) {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* The layout under the cache root. One base, not one per release
|
|
32
|
-
* (
|
|
32
|
+
* (docs/LAB.md): setup replaces it when a newer release is built.
|
|
33
33
|
*/
|
|
34
34
|
export function labLayout(env = process.env) {
|
|
35
35
|
const cache = labCacheDir(env)
|
package/tools/lab/pin.json
CHANGED
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"preparedLabBytes": 12442931200,
|
|
34
34
|
"overlayAfterRunBytes": 610734080,
|
|
35
35
|
"pluginsBytes": 6836224,
|
|
36
|
-
"source": "docs/MEASUREMENTS.md M14, measured by omakit lab on the reference host with Omarchy 4.0.3
|
|
36
|
+
"source": "docs/MEASUREMENTS.md M14, measured by omakit lab on the reference host with Omarchy 4.0.3, re-measuring the lab plan's observations of 2026-09-10 to 2026-09-13 (M4 to M7, in git history at 9c667f9)"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/tools/lab/verify.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// The signature is the trust: a substituted ISO with a substituted
|
|
6
6
|
// checksum beside it fails here, because only Omarchy's key signs. The
|
|
7
7
|
// digest names the file and catches a download that went wrong. Both,
|
|
8
|
-
// every time, and a mismatch in either fails closed (
|
|
9
|
-
//
|
|
8
|
+
// every time, and a mismatch in either fails closed (docs/LAB.md, the
|
|
9
|
+
// trust anchor).
|
|
10
10
|
// The key is imported into a throwaway GNUPGHOME under the lab, never into
|
|
11
11
|
// the user's keyring: a lab that added keys to ~/.gnupg would be changing
|
|
12
12
|
// the host, and the only host change the lab makes is the lab.
|
|
@@ -7,14 +7,14 @@ local commit through the transport seam the marketplace tests itself
|
|
|
7
7
|
|
|
8
8
|
| File | Purpose |
|
|
9
9
|
| --- | --- |
|
|
10
|
-
| `pin.mjs` | The pin identity (one home) and the reproducible setup: `omakit pin` fetches exactly that commit into `$XDG_CACHE_HOME/omakit/marketplace` and refuses a modified checkout. `PIN_PATHS` is what is fetched; `PIN_READS` is what omakit opens under `scripts/` and how (imported or read as text), and `pinnedReadSet(pinDir)` adds what the imported files import, by regex over the pinned text and never by loading a module: 16 of 34 files at `
|
|
10
|
+
| `pin.mjs` | The pin identity (one home) and the reproducible setup: `omakit pin` fetches exactly that commit into `$XDG_CACHE_HOME/omakit/marketplace` and refuses a modified checkout. `PIN_PATHS` is what is fetched; `PIN_READS` is what omakit opens under `scripts/` and how (imported or read as text), and `pinnedReadSet(pinDir)` adds what the imported files import, by regex over the pinned text and never by loading a module: 16 of 34 files at `b441b4f0`, pinned by `tests/unit/pin.test.mjs`. `policyConstants(text)` reads the two policy constants out of the module's text, for the pin's identity and for doctor's comparison at HEAD; each file read as text carries its `readers` in `PIN_READS`, the values taken out of it as functions from text to value (`reservedNamespace`, `catalogPresentation`, `validatedLabel`, `updateLabel`), which the checks call on the pin's text and doctor on the pin's and HEAD's. |
|
|
11
11
|
| `local-transport.mjs` | Answers the four request shapes the official resolver makes, from a local clone at the exact commit. No network, no credentials, no writes. With `subdir`, serves a directory below the root as the whole tree, which is how `inspect` keeps the baseline to the plugin's own tree. |
|
|
12
12
|
| `run-baseline.mjs` | Runs the pinned official baseline over either transport and reports the pin identity beside the result. |
|
|
13
13
|
| `verify.mjs` | Builds the `marketplaceBaseline` section: pin, transport, assumptions, the official result verbatim, the statement. `omakit verify` renders it for a person (`renderVerify` in `report.mjs`) and prints the document itself behind `--json` and `--out`. |
|
|
14
14
|
| `preflight.mjs` | Translates that result into what it will cause on submission, using the pinned policy, and renders the marketplace's own report text with its attestation marker stripped and asserted absent. |
|
|
15
15
|
| `yaml.mjs` | A deliberately small YAML reader for the pinned issue form. Accepts that subset and throws on anything else. |
|
|
16
16
|
| `form.mjs` | The submission contract, read from the form and cross-checked against the marketplace's own constants. Also the route for a plugin that is already listed: the marketplace's verification form and its "newer commit" choice, read from `verify-plugin.yml` at the pin and cross-checked against `plugin-verification-request.mjs`. |
|
|
17
|
-
| `registry.mjs` | The plugin-id and repository universe: the reserved namespace from the pinned catalog builder, the listed and retired ids and listed repositories from `registry.json` and `site/catalog.json` at the marketplace's current HEAD when the network is there (cached under `$XDG_CACHE_HOME/omakit/registry/<commit>/`, never in the pin) and at the pin with `--offline`; `liveFileUrl()` is the way to the raw file host for those two files, and `headTextUrl()` for the
|
|
17
|
+
| `registry.mjs` | The plugin-id and repository universe: the reserved namespace from the pinned catalog builder, the listed and retired ids and listed repositories from `registry.json` and `site/catalog.json` at the marketplace's current HEAD when the network is there (cached under `$XDG_CACHE_HOME/omakit/registry/<commit>/`, never in the pin) and at the pin with `--offline`; `liveFileUrl()` is the way to the raw file host for those two files, and `headTextUrl()` for the text `doctor` compares and drops (the policy module and the three files read as text at HEAD, `HEAD_TEXT_PATHS`), both at a 40-character commit; `catalogBuilderText()` is the pin's catalog builder, and `reservedNamespace` and `catalogPresentation` are re-exported from `pin.mjs`. `sameRepository()` is the one rule for "the subject's own repository" (owner and name, case-insensitively, a trailing `.git` ignored), and `listingOf()` is what the catalog records about a listing: since when, which commit, verified or not, checked when. |
|
|
18
18
|
| `tree.mjs` | The installable tree of a subject at one exact commit, from the Git object database. |
|
|
19
19
|
| `plugin.mjs` | The root files the submission contract needs, and the declared plugin identity. |
|
|
20
20
|
| `agent-control.mjs` | The recursive agent-control warning, and its remedy. |
|
|
@@ -23,7 +23,7 @@ local commit through the transport seam the marketplace tests itself
|
|
|
23
23
|
| `issue.mjs` | Renders the issue the way the form would, then has the marketplace's own parser judge it. |
|
|
24
24
|
| `submit.mjs` | Assembles every check with its measured reason, and withholds the body when a blocking check fails. Three outcomes: `ready` (the body), `refused` (a blocking check failed) and `listed` (the plugin is already listed by its own repository: `identity.available` passes with the listing's record, the five body checks are omitted rather than drawn as waiting, no body exists on purpose, and `listing` carries the listed commit against the local one and the form to use for a newer commit). Decides the category and tags after the registry: a listed plugin, own or taken, is asked for neither; an unlisted one without them is asked through `ask.mjs` at a terminal, and is a usage error otherwise. Ends with `reproduce`, the command line that repeats the run without asking. Under `--offline` the validation-commit check is `skipped`, not passed: verdict `skipped`, listed under `skipped` and not `unknown`, never blocking, and the READY line says "1 check skipped (--offline)". |
|
|
25
25
|
| `ask.mjs` | The two questions `submit` asks a person at a terminal, and only there: category and tags, numbered from the pinned form, with the marketplace's own presentation for the manifest's kinds (read from the pinned catalog builder) as the default where it is on the list. Prompts on stderr, nothing persisted. |
|
|
26
|
-
| `doctor.mjs` | What is installed, what is pinned, and what has moved. `omakit.version` is one check for one question, is this the current omakit: `ok` with "0.1.8, the newest published version", a `note` with "0.1.8; 0.1.9 is published" and the upgrade command, `unknown` with the registry's failure code, `info` offline; its evidence carries `{ installed, latest, source }`. `pin.freshness` compares what omakit reads between the pin and the marketplace's HEAD (`comparePin`): the 16 files in `pinnedReadSet()` by blob id, the two data files by blob id, the form directory by tree id,
|
|
26
|
+
| `doctor.mjs` | What is installed, what is pinned, and what has moved. `omakit.version` is one check for one question, is this the current omakit: `ok` with "0.1.8, the newest published version", a `note` with "0.1.8; 0.1.9 is published" and the upgrade command, `unknown` with the registry's failure code, `info` offline; its evidence carries `{ installed, latest, source }`. `pin.freshness` compares what omakit reads between the pin and the marketplace's HEAD (`comparePin`): the 16 files in `pinnedReadSet()` by blob id, the two data files by blob id, the form directory by tree id, the two policy constants by the module's text at HEAD when its blob moved, and each file read as text, when its blob moved, by the values its `readers` take out of it at both ends (`textReadsBetween`, `textReads`); three tree reads, a fourth for `scripts/` only when its id moved, one more GET per moved text. Graded on `effective`, the moved files less the text-read ones whose every value is unchanged: `ok` when it is empty (the data files are read live; `scripts/` moving elsewhere is said as "in none of the 16 files omakit reads"; an unchanged text-read file is named with how many values), `info` when it holds only `WORDING_READS`, `advice` when a verdict-bearing file moved, a text-read value differs (both printed), a read file is gone, or the form moved, with `omakit upgrade` as the action when a newer omakit is published and otherwise that the maintainer is notified; it never asks for an issue, the weekly workflow opens the one there is. Measured (M7): of the five commits since `38060f89` that touched the read set, tree and blob comparison grade five advice, this two. The maintainer's pin procedure stays in `docs/UPSTREAM_CONTRACT.md` and is never printed. HEAD unreadable is `unknown`. `--json` carries `changedPaths`, `readLive` and `pinned` under the check's evidence as before, plus `reads`, `moved`, `missing` and `policy.{pin,head}`. |
|
|
27
27
|
| `watch.mjs` | The validation watch: validated commit versus current default-branch HEAD, and the one action that refreshes it. |
|
|
28
28
|
| `github.mjs` | Read-only GitHub access. GET only. The credential is your `gh` login, read through one frozen `gh auth token` call, and is never written anywhere. |
|
|
29
29
|
| `style.mjs` | The visual system, defined once: the palette, the status vocabulary, the block ramp, the columns, the motion budgets, and the composition helpers every command draws with. Six states: `▁ ok`, `█ FAIL`, `▓ note`, `░ info`, `▒ ?` for a check that could not be made, and `▔ skip` for a check a flag said not to make, the floor's ink at the ceiling so it is never read as a pass. `docs/TUI.md` explains it. |
|
|
@@ -747,7 +747,7 @@ async function cmdLab(args) {
|
|
|
747
747
|
onLine: (line) => {
|
|
748
748
|
spinner.done()
|
|
749
749
|
// The identity block, once the guest has been read: the line
|
|
750
|
-
//
|
|
750
|
+
// docs/LAB.md says every run prints before its suite.
|
|
751
751
|
if (line.record) {
|
|
752
752
|
narrate.write(`${withOutputStream(narrate, () => renderRunIdentity(line.record, { colour: colourEnabled(narrate) }))}\n\n`)
|
|
753
753
|
return
|
|
@@ -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"
|
|
@@ -93,6 +108,45 @@ function pinObjectId(pinDir, commit, path) {
|
|
|
93
108
|
return execFileSync("git", ["-C", pinDir, "rev-parse", `${commit}:${path}`], { timeout: 60_000, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
|
94
109
|
}
|
|
95
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
|
+
|
|
96
150
|
/** A PIN_PATHS pattern as a path: "/site/catalog.json" -> "site/catalog.json". */
|
|
97
151
|
const asPath = (pattern) => pattern.replace(/^\/|\/$/g, "")
|
|
98
152
|
|
|
@@ -117,10 +171,20 @@ const FORMS = "/.github/ISSUE_TEMPLATE/"
|
|
|
117
171
|
* `policyAtHead`; otherwise `policyAtHead` is null, because an identical
|
|
118
172
|
* blob has identical constants. The text is never imported.
|
|
119
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
|
+
*
|
|
120
183
|
* `fetchJson` and `fetchText` are injectable for tests.
|
|
121
184
|
*
|
|
122
185
|
* @returns {Promise<{ changedPaths: string[], reads: number, moved: string[], missing: string[],
|
|
123
|
-
* 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 }> }> }>}
|
|
124
188
|
*/
|
|
125
189
|
export async function comparePin({ pinDir, headCommit, pinCommit = MARKETPLACE_PIN.commit, fetchJson = getJson, fetchText = getText, reads = pinnedReadSet(pinDir) }) {
|
|
126
190
|
const match = MARKETPLACE_PIN.repository.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
|
|
@@ -164,7 +228,8 @@ export async function comparePin({ pinDir, headCommit, pinCommit = MARKETPLACE_P
|
|
|
164
228
|
}
|
|
165
229
|
}
|
|
166
230
|
const policyAtHead = moved.includes(POLICY_MODULE) ? policyConstants(await fetchText(headTextUrl(headCommit, POLICY_MODULE))) : null
|
|
167
|
-
|
|
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 }
|
|
168
233
|
}
|
|
169
234
|
|
|
170
235
|
/** "a", "a and b", "a, b and c". */
|
|
@@ -186,24 +251,36 @@ const verdictBearing = (paths) => paths.filter((path) => !WORDING_READS.includes
|
|
|
186
251
|
* from HEAD, and `pinned`, everything only a new pin can carry; its `moved`
|
|
187
252
|
* and `missing` name the files omakit reads under scripts/ whose blob
|
|
188
253
|
* differs at HEAD; its `policyAtHead` carries the two policy constants
|
|
189
|
-
* 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.
|
|
190
265
|
*
|
|
191
266
|
* ok nothing omakit reads moved: HEAD moved elsewhere, or only in
|
|
192
267
|
* the live-read data files, or scripts/ moved in none of the
|
|
193
|
-
* files omakit reads
|
|
194
|
-
*
|
|
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
|
|
195
271
|
* or a label out of (WORDING_READS): what it prints may differ
|
|
196
272
|
* at HEAD, what it passes or refuses cannot. A newer omakit will
|
|
197
273
|
* carry the pin. Nothing for a user to do.
|
|
198
274
|
* advice a file omakit executes or reads a rule from moved (a verdict
|
|
199
275
|
* may differ at HEAD, whether or not the two policy constants
|
|
200
276
|
* still read the same, since a rule can change without its
|
|
201
|
-
* version), a read
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* the
|
|
206
|
-
*
|
|
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.
|
|
207
284
|
*
|
|
208
285
|
* Null for `comparison` means the comparison was not made, which stays
|
|
209
286
|
* advice: HEAD moved and nothing here can say the pin is fine. Full commits
|
|
@@ -218,15 +295,22 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
218
295
|
const pinned = changed.filter((pattern) => !LIVE_PATHS.includes(asPath(pattern)))
|
|
219
296
|
const moved = compared ? comparison.moved : []
|
|
220
297
|
const missing = compared ? comparison.missing : []
|
|
298
|
+
const textReads = comparison?.textReads || {}
|
|
221
299
|
const pinPolicy = { baselineVersion: identity.baselineVersion, enforcementMode: identity.enforcementMode }
|
|
222
300
|
const headPolicy = (compared && comparison.policyAtHead) || pinPolicy
|
|
223
301
|
const policyDiffers = headPolicy.baselineVersion !== pinPolicy.baselineVersion || headPolicy.enforcementMode !== pinPolicy.enforcementMode
|
|
224
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))
|
|
225
308
|
const verdictMoved = verdictBearing(moved)
|
|
309
|
+
const verdictEffective = verdictBearing(effective)
|
|
226
310
|
const graded = moved.length > 0 || missing.length > 0 || formMoved
|
|
227
311
|
const state = current ? "ok"
|
|
228
|
-
: comparison === null || policyDiffers || formMoved || missing.length ||
|
|
229
|
-
:
|
|
312
|
+
: comparison === null || policyDiffers || formMoved || missing.length || differing.length || verdictEffective.length ? "advice"
|
|
313
|
+
: effective.length ? "info"
|
|
230
314
|
: "ok"
|
|
231
315
|
|
|
232
316
|
const where = `pin ${identity.commit.slice(0, 7)}; marketplace ${branch} at ${head.commit.slice(0, 7)}`
|
|
@@ -234,7 +318,14 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
234
318
|
if (comparison === null) clauses.push("the paths omakit reads were not compared")
|
|
235
319
|
else if (!changed.length) clauses.push("nothing omakit reads moved")
|
|
236
320
|
else {
|
|
237
|
-
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
|
+
}
|
|
238
329
|
if (missing.length) clauses.push(`gone at HEAD: ${list(missing)}`)
|
|
239
330
|
if (formMoved) clauses.push(`the form moved (${asPath(FORMS)}/)`)
|
|
240
331
|
if (pinned.includes("/scripts/") && !moved.length && !missing.length) clauses.push(`scripts/ moved in none of the ${comparison.reads} files omakit reads`)
|
|
@@ -245,7 +336,7 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
245
336
|
const detail = current ? `the pin is the marketplace's current ${branch}-branch HEAD` : `${where}; ${clauses.join("; ")}${tail}`
|
|
246
337
|
|
|
247
338
|
const action = state === "info"
|
|
248
|
-
? `${
|
|
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.`
|
|
249
340
|
: state === "advice"
|
|
250
341
|
? upgrade
|
|
251
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.`
|
|
@@ -268,7 +359,9 @@ export function pinFreshness(identity, head, comparison = null, { upgrade = null
|
|
|
268
359
|
reads: comparison.reads,
|
|
269
360
|
moved,
|
|
270
361
|
verdictMoved,
|
|
362
|
+
effective,
|
|
271
363
|
missing,
|
|
364
|
+
textReads,
|
|
272
365
|
policy: { pin: pinPolicy, head: headPolicy },
|
|
273
366
|
}),
|
|
274
367
|
},
|
|
@@ -396,10 +489,14 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
396
489
|
phase("reading the marketplace's current default-branch HEAD")
|
|
397
490
|
try {
|
|
398
491
|
const head = await resolveHead(MARKETPLACE_PIN.repository)
|
|
399
|
-
let comparison
|
|
492
|
+
let comparison
|
|
400
493
|
if (head.commit !== identity.commit) {
|
|
401
494
|
phase("comparing each file omakit reads between the pin and HEAD")
|
|
402
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 }) }
|
|
403
500
|
}
|
|
404
501
|
checks.push(pinFreshness(identity, head, comparison, { upgrade }))
|
|
405
502
|
} catch (error) {
|
|
@@ -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,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { pathToFileURL } from "node:url"
|
|
5
|
-
import { requirePin } from "./pin.mjs"
|
|
5
|
+
import { requirePin, updateLabel as updateLabelOf } from "./pin.mjs"
|
|
6
6
|
import { token, issue, parseIssueUrl, compareCommits } from "./github.mjs"
|
|
7
7
|
import { discoverWatchIssues, repositoryFor } from "./watch.mjs"
|
|
8
8
|
import { repositorySlug } from "./registry.mjs"
|
|
@@ -12,8 +12,7 @@ import { submissionContract } from "./form.mjs"
|
|
|
12
12
|
export async function reviewPolicy(repoRoot) {
|
|
13
13
|
const { dir } = requirePin(repoRoot)
|
|
14
14
|
const policy = await import(pathToFileURL(join(dir, "scripts/security-baseline-policy.mjs")).href)
|
|
15
|
-
const
|
|
16
|
-
const updateLabel = approval.match(/for \(const required of \["([^"]+)"/)?.[1]
|
|
15
|
+
const updateLabel = updateLabelOf(readFileSync(join(dir, "scripts/approve-plugin-update.mjs"), "utf8"))
|
|
17
16
|
if (!updateLabel) throw new Error("cannot read the update issue label from the pin")
|
|
18
17
|
const manual = policy.currentSecurityBaselinePolicy.maintainerVerificationOutcome
|
|
19
18
|
return { automated: policy.securityBaselineOutcome([], []), manual, updateLabel, reviewLabel: `security-${manual}` }
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { resolveSubject, SubjectError } from "../subject/resolve.mjs"
|
|
15
15
|
import { requirePin } from "./pin.mjs"
|
|
16
16
|
import { submissionContract, newerCommitChoice, resolveCategory, resolveTags, tagSlug } from "./form.mjs"
|
|
17
|
-
import { idUniverse, checkIdentity, listingOf, baselineFigures, figure, liveRegistry, registrySourceDetail, catalogPresentation, defaultPresentation } from "./registry.mjs"
|
|
17
|
+
import { idUniverse, checkIdentity, listingOf, baselineFigures, figure, liveRegistry, registrySourceDetail, catalogBuilderText, catalogPresentation, defaultPresentation } from "./registry.mjs"
|
|
18
18
|
import { readTree } from "./tree.mjs"
|
|
19
19
|
import { inspectTree } from "./plugin.mjs"
|
|
20
20
|
import { findAgentControl, REMEDY as AGENT_CONTROL_REMEDY } from "./agent-control.mjs"
|
|
@@ -283,7 +283,7 @@ export async function submitPreflight(options) {
|
|
|
283
283
|
error.usage = missing
|
|
284
284
|
throw error
|
|
285
285
|
}
|
|
286
|
-
const defaults = defaultPresentation(catalogPresentation(pinDir), tree.manifest?.kinds)
|
|
286
|
+
const defaults = defaultPresentation(catalogPresentation(catalogBuilderText(pinDir)), tree.manifest?.kinds)
|
|
287
287
|
const answers = await options.chooser({ contract, defaults, missing: missing.missing })
|
|
288
288
|
if (answers?.category) chosenCategory = answers.category
|
|
289
289
|
if (answers?.tags) chosenTags = answers.tags
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { join } from "node:path"
|
|
22
22
|
import { pathToFileURL } from "node:url"
|
|
23
23
|
import { readFileSync } from "node:fs"
|
|
24
|
-
import { MARKETPLACE_PIN, requirePin } from "./pin.mjs"
|
|
24
|
+
import { MARKETPLACE_PIN, requirePin, validatedLabel } from "./pin.mjs"
|
|
25
25
|
import { authenticatedUser, repositoryIssues, defaultBranchHead, issue, issueComments, parseIssueUrl, token, GitHubError } from "./github.mjs"
|
|
26
26
|
import { liveRegistry } from "./registry.mjs"
|
|
27
27
|
import { reviewPolicy, validatedDocumentationDiff } from "./review-cost.mjs"
|
|
@@ -266,8 +266,7 @@ export function validationComment(comments, feedback = []) {
|
|
|
266
266
|
*/
|
|
267
267
|
export async function marketplaceLabels(pinDir) {
|
|
268
268
|
const policy = await import(pathToFileURL(join(pinDir, "scripts/security-baseline-policy.mjs")).href)
|
|
269
|
-
const
|
|
270
|
-
const validated = approval.match(/for \(const required of \["submission", "([^"]+)"/)?.[1]
|
|
269
|
+
const validated = validatedLabel(readFileSync(join(pinDir, "scripts/approve-submission.mjs"), "utf8"))
|
|
271
270
|
if (!validated) throw new Error("cannot read the validated label from the pin")
|
|
272
271
|
const manual = policy.currentSecurityBaselinePolicy.maintainerVerificationOutcome
|
|
273
272
|
return { blocking: [...policy.securityBaselineBlockingLabels], validated, reviewRequired: `security-${manual}` }
|