omakit 0.6.5 → 0.6.7
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
CHANGED
package/tools/blocks/commit.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"recordedAt": "2026-09-
|
|
2
|
+
"commit": "5dd9db1969775b87b10f21445570557cf82e8073",
|
|
3
|
+
"recordedAt": "2026-09-21T20:24:57.368Z",
|
|
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
|
}
|
|
@@ -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. |
|
|
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 `b7b29654`, 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. |
|
|
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
|
|
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 one text `doctor` compares and drops (the policy module at HEAD, `HEAD_TEXT_PATHS`), both at a 40-character commit. `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
|
|
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, and the two policy constants by the module's text at HEAD when its blob moved; three tree reads, a fourth for `scripts/` only when its id moved, a fifth GET for the policy text only when that blob moved. Graded: `ok` when nothing in the read set moved (the data files are read live; `scripts/` moving elsewhere is said as "in none of the 16 files omakit reads"), `info` when a read file moved and both constants read the same (the files named, verdicts unchanged, nothing to do), `advice` when a constant differs, 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): 5e401552, which changed only `repository-identity.mjs`, graded `advice` by tree id and grades `ok` here. 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. |
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
// contract is read from, and the procedure in docs/UPSTREAM_CONTRACT.md requires
|
|
14
14
|
// re-proving transport parity and committing the evidence afterwards. An
|
|
15
15
|
// `upgrade` that quietly advanced the pin would break the one guarantee this
|
|
16
|
-
// tool sells. So doctor reports
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// pin
|
|
16
|
+
// tool sells. So doctor reports what moved, graded, and that is all it does:
|
|
17
|
+
// the procedure is the maintainer's, it lives in that document and in this
|
|
18
|
+
// comment, and it is never printed, because the person running doctor is a
|
|
19
|
+
// user of a package that does not even ship docs/. What a user can do is run
|
|
20
|
+
// `omakit upgrade` when a newer omakit is published, since it may carry the
|
|
21
|
+
// new pin; otherwise the weekly pin-freshness workflow in this repository
|
|
22
|
+
// has already told the maintainer, and there is nothing to open.
|
|
22
23
|
//
|
|
23
24
|
// That the pin goes stale unnoticed is, of course, exactly the defect class
|
|
24
25
|
// `omakit watch` exists to report. It would be poor form not to apply it here.
|
|
@@ -35,13 +36,31 @@
|
|
|
35
36
|
// only registry.json and site/catalog.json moved, doctor said `note` and
|
|
36
37
|
// pointed a user at docs/UPSTREAM_CONTRACT.md, though the pin was behind in
|
|
37
38
|
// nothing the tool uses.
|
|
39
|
+
//
|
|
40
|
+
// Nor is "scripts/ moved" the same as "a file omakit reads moved". Measured
|
|
41
|
+
// on 2026-09-21 (M7): of the three marketplace commits that touched
|
|
42
|
+
// scripts/ since the first pin 38060f89, one (5e401552) changed only
|
|
43
|
+
// repository-identity.mjs, a file none of omakit's reads reach, and doctor
|
|
44
|
+
// graded the tree id's change as advice, a user read "run omakit upgrade",
|
|
45
|
+
// and the maintainer moved the pin for a verdict that could not change.
|
|
46
|
+
// So under scripts/ the comparison is by blob, over the 16 files
|
|
47
|
+
// pinnedReadSet() names, and the verdict is graded: `ok` when none of them
|
|
48
|
+
// moved, `info` when the only moved files are ones omakit takes wording or
|
|
49
|
+
// a label out of (WORDING_READS), `advice` when a file omakit executes or
|
|
50
|
+
// reads a rule from moved, a read file is gone at HEAD, or the form
|
|
51
|
+
// directory moved, which is the contract itself. The two policy constants
|
|
52
|
+
// are read at HEAD as text and printed beside the grade, so a person sees
|
|
53
|
+
// whether the baseline itself changed; they never soften the grade, since
|
|
54
|
+
// a rule can change without its version. The other two commits (7dd6e56,
|
|
55
|
+
// 40315f2) touched submission.mjs and the policy module, both executed,
|
|
56
|
+
// and the forms, so they grade advice either way.
|
|
38
57
|
|
|
39
58
|
import { execFileSync } from "node:child_process"
|
|
40
59
|
import { existsSync, readFileSync } from "node:fs"
|
|
41
60
|
import { join } from "node:path"
|
|
42
|
-
import { MARKETPLACE_PIN, PIN_PATHS, marketplacePinDir, pinDiskUsage, pinShape, requirePin } from "./pin.mjs"
|
|
43
|
-
import { LIVE_PATHS } from "./registry.mjs"
|
|
44
|
-
import { credential, defaultBranchHead, getJson, GitHubError } from "./github.mjs"
|
|
61
|
+
import { MARKETPLACE_PIN, PIN_PATHS, POLICY_MODULE, WORDING_READS, marketplacePinDir, pinDiskUsage, pinShape, pinnedReadSet, policyConstants, requirePin } from "./pin.mjs"
|
|
62
|
+
import { LIVE_PATHS, headTextUrl } from "./registry.mjs"
|
|
63
|
+
import { credential, defaultBranchHead, getJson, getText, GitHubError } from "./github.mjs"
|
|
45
64
|
import { compareVersions, NPM_REGISTRY, registryLatest, upgradeCommand } from "./upgrade.mjs"
|
|
46
65
|
import { sourceCommit } from "../blocks/add.mjs"
|
|
47
66
|
import { recordedCommit } from "../blocks/record-commit.mjs"
|
|
@@ -50,19 +69,12 @@ import { completionStatus } from "./completion-check.mjs"
|
|
|
50
69
|
import { inspectLab } from "../lab/inspect.mjs"
|
|
51
70
|
import { labDoctorChecks } from "../lab/report.mjs"
|
|
52
71
|
|
|
53
|
-
/** "git+https://github.com/owner/name.git" in package.json -> "https://github.com/owner/name", or null. */
|
|
54
|
-
function repositoryPage(repository) {
|
|
55
|
-
const url = typeof repository === "string" ? repository : repository?.url
|
|
56
|
-
const match = String(url || "").match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/)
|
|
57
|
-
return match ? `https://github.com/${match[1]}/${match[2]}` : null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
72
|
export function tool(repoRoot) {
|
|
61
73
|
try {
|
|
62
74
|
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"))
|
|
63
|
-
return { name: pkg.name, version: pkg.version, engines: pkg.engines?.node || null
|
|
75
|
+
return { name: pkg.name, version: pkg.version, engines: pkg.engines?.node || null }
|
|
64
76
|
} catch {
|
|
65
|
-
return { name: "omakit", version: "unknown", engines: null
|
|
77
|
+
return { name: "omakit", version: "unknown", engines: null }
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
|
|
@@ -80,17 +92,36 @@ function pinObjectId(pinDir, commit, path) {
|
|
|
80
92
|
return execFileSync("git", ["-C", pinDir, "rev-parse", `${commit}:${path}`], { timeout: 60_000, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
/** A PIN_PATHS pattern as a path: "/site/catalog.json" -> "site/catalog.json". */
|
|
96
|
+
const asPath = (pattern) => pattern.replace(/^\/|\/$/g, "")
|
|
97
|
+
|
|
98
|
+
/** The form directory: the whole contract, compared by tree id and graded advice when it moved. */
|
|
99
|
+
const FORMS = "/.github/ISSUE_TEMPLATE/"
|
|
100
|
+
|
|
83
101
|
/**
|
|
84
|
-
*
|
|
85
|
-
* read from the local checkout; the HEAD side walks
|
|
86
|
-
* exact commit, one read per tree on the way
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
102
|
+
* The pin against `headCommit`, path by path and, under scripts/, file by
|
|
103
|
+
* file. The pin side is read from the local checkout; the HEAD side walks
|
|
104
|
+
* the git-trees API from the exact commit, one read per tree on the way:
|
|
105
|
+
* three for PIN_PATHS as it stands, a fourth for the scripts/ tree only
|
|
106
|
+
* when its id moved, since an identical tree has identical blobs. Each
|
|
107
|
+
* PIN_PATHS entry compares by its own object id (a directory by tree id, a
|
|
108
|
+
* file by blob id) into `changedPaths`; each file in `reads`
|
|
109
|
+
* (pinnedReadSet by default) compares by blob id into `moved`, or
|
|
110
|
+
* `missing` when HEAD no longer has it. A PIN_PATHS entry missing at HEAD
|
|
111
|
+
* counts as changed.
|
|
90
112
|
*
|
|
91
|
-
*
|
|
113
|
+
* When the policy module is among `moved`, its text at HEAD is read once
|
|
114
|
+
* through `fetchText` (the raw file host at the exact commit, the one GET
|
|
115
|
+
* call site; a fifth request) and its two constants are returned as
|
|
116
|
+
* `policyAtHead`; otherwise `policyAtHead` is null, because an identical
|
|
117
|
+
* blob has identical constants. The text is never imported.
|
|
118
|
+
*
|
|
119
|
+
* `fetchJson` and `fetchText` are injectable for tests.
|
|
120
|
+
*
|
|
121
|
+
* @returns {Promise<{ changedPaths: string[], reads: number, moved: string[], missing: string[],
|
|
122
|
+
* policyAtHead: { baselineVersion: string, enforcementMode: string }|null }>}
|
|
92
123
|
*/
|
|
93
|
-
export async function
|
|
124
|
+
export async function comparePin({ pinDir, headCommit, pinCommit = MARKETPLACE_PIN.commit, fetchJson = getJson, fetchText = getText, reads = pinnedReadSet(pinDir) }) {
|
|
94
125
|
const match = MARKETPLACE_PIN.repository.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
|
|
95
126
|
const trees = new Map()
|
|
96
127
|
const tree = async (sha) => {
|
|
@@ -118,73 +149,140 @@ export async function changedPinPaths({ pinDir, headCommit, pinCommit = MARKETPL
|
|
|
118
149
|
}
|
|
119
150
|
return id
|
|
120
151
|
}
|
|
121
|
-
const
|
|
152
|
+
const changedPaths = []
|
|
122
153
|
for (const pattern of PIN_PATHS) {
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
if (pinObjectId(pinDir, pinCommit, asPath(pattern)) !== await headObjectId(asPath(pattern))) changedPaths.push(pattern)
|
|
155
|
+
}
|
|
156
|
+
const moved = []
|
|
157
|
+
const missing = []
|
|
158
|
+
if (changedPaths.includes("/scripts/")) {
|
|
159
|
+
for (const { path } of reads) {
|
|
160
|
+
const atHead = await headObjectId(path)
|
|
161
|
+
if (atHead === null) missing.push(path)
|
|
162
|
+
else if (atHead !== pinObjectId(pinDir, pinCommit, path)) moved.push(path)
|
|
163
|
+
}
|
|
125
164
|
}
|
|
126
|
-
|
|
165
|
+
const policyAtHead = moved.includes(POLICY_MODULE) ? policyConstants(await fetchText(headTextUrl(headCommit, POLICY_MODULE))) : null
|
|
166
|
+
return { changedPaths, reads: reads.length, moved, missing, policyAtHead }
|
|
127
167
|
}
|
|
128
168
|
|
|
129
|
-
/** A PIN_PATHS pattern as a path: "/site/catalog.json" -> "site/catalog.json". */
|
|
130
|
-
const asPath = (pattern) => pattern.replace(/^\/|\/$/g, "")
|
|
131
|
-
|
|
132
169
|
/** "a", "a and b", "a, b and c". */
|
|
133
170
|
function list(items) {
|
|
134
171
|
return items.length < 3 ? items.join(" and ") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`
|
|
135
172
|
}
|
|
136
173
|
|
|
174
|
+
/** "baseline 3 (selective)". */
|
|
175
|
+
const policyText = (policy) => `baseline ${policy.baselineVersion} (${policy.enforcementMode})`
|
|
176
|
+
|
|
177
|
+
/** The moved files a verdict can depend on: everything omakit executes or reads a rule from, which is every read outside WORDING_READS. */
|
|
178
|
+
const verdictBearing = (paths) => paths.filter((path) => !WORDING_READS.includes(path))
|
|
179
|
+
|
|
137
180
|
/**
|
|
138
|
-
* The pin against the marketplace's HEAD, for a user
|
|
139
|
-
*
|
|
181
|
+
* The pin against the marketplace's HEAD, for a user, graded by what the
|
|
182
|
+
* difference can do to a verdict. `comparison` is the answer from
|
|
183
|
+
* comparePin(): its `changedPaths` split by LIVE_PATHS into `readLive`, the
|
|
140
184
|
* data files a moved HEAD cannot make stale because registry.mjs reads them
|
|
141
|
-
* from HEAD, and `pinned`, everything only a new pin can carry
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
185
|
+
* from HEAD, and `pinned`, everything only a new pin can carry; its `moved`
|
|
186
|
+
* and `missing` name the files omakit reads under scripts/ whose blob
|
|
187
|
+
* differs at HEAD; its `policyAtHead` carries the two policy constants
|
|
188
|
+
* there when the policy module moved.
|
|
189
|
+
*
|
|
190
|
+
* ok nothing omakit reads moved: HEAD moved elsewhere, or only in
|
|
191
|
+
* the live-read data files, or scripts/ moved in none of the
|
|
192
|
+
* files omakit reads.
|
|
193
|
+
* info the only read files that moved are ones omakit takes wording
|
|
194
|
+
* or a label out of (WORDING_READS): what it prints may differ
|
|
195
|
+
* at HEAD, what it passes or refuses cannot. A newer omakit will
|
|
196
|
+
* carry the pin. Nothing for a user to do.
|
|
197
|
+
* advice a file omakit executes or reads a rule from moved (a verdict
|
|
198
|
+
* may differ at HEAD, whether or not the two policy constants
|
|
199
|
+
* still read the same, since a rule can change without its
|
|
200
|
+
* version), a read file is gone at HEAD, or the form directory
|
|
201
|
+
* moved (the contract itself). The action is `upgrade` (the
|
|
202
|
+
* command, when the caller found a newer omakit published), else
|
|
203
|
+
* that the maintainer is notified; never an issue to open, since
|
|
204
|
+
* the weekly pin-freshness workflow opens the one there is. The
|
|
205
|
+
* two constants are printed beside the grade in both cases.
|
|
206
|
+
*
|
|
207
|
+
* Null for `comparison` means the comparison was not made, which stays
|
|
208
|
+
* advice: HEAD moved and nothing here can say the pin is fine. Full commits
|
|
209
|
+
* in the evidence; short ones in the detail.
|
|
147
210
|
*/
|
|
148
|
-
export function pinFreshness(identity, head,
|
|
211
|
+
export function pinFreshness(identity, head, comparison = null, { upgrade = null } = {}) {
|
|
149
212
|
const current = head.commit === identity.commit
|
|
150
213
|
const branch = head.branch || "default"
|
|
151
|
-
const
|
|
214
|
+
const compared = !current && comparison !== null
|
|
215
|
+
const changed = compared ? comparison.changedPaths : []
|
|
152
216
|
const readLive = changed.filter((pattern) => LIVE_PATHS.includes(asPath(pattern)))
|
|
153
217
|
const pinned = changed.filter((pattern) => !LIVE_PATHS.includes(asPath(pattern)))
|
|
218
|
+
const moved = compared ? comparison.moved : []
|
|
219
|
+
const missing = compared ? comparison.missing : []
|
|
220
|
+
const pinPolicy = { baselineVersion: identity.baselineVersion, enforcementMode: identity.enforcementMode }
|
|
221
|
+
const headPolicy = (compared && comparison.policyAtHead) || pinPolicy
|
|
222
|
+
const policyDiffers = headPolicy.baselineVersion !== pinPolicy.baselineVersion || headPolicy.enforcementMode !== pinPolicy.enforcementMode
|
|
223
|
+
const formMoved = pinned.includes(FORMS)
|
|
224
|
+
const verdictMoved = verdictBearing(moved)
|
|
225
|
+
const graded = moved.length > 0 || missing.length > 0 || formMoved
|
|
226
|
+
const state = current ? "ok"
|
|
227
|
+
: comparison === null || policyDiffers || formMoved || missing.length || verdictMoved.length ? "advice"
|
|
228
|
+
: moved.length ? "info"
|
|
229
|
+
: "ok"
|
|
230
|
+
|
|
154
231
|
const where = `pin ${identity.commit.slice(0, 7)}; marketplace ${branch} at ${head.commit.slice(0, 7)}`
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
232
|
+
const clauses = []
|
|
233
|
+
if (comparison === null) clauses.push("the paths omakit reads were not compared")
|
|
234
|
+
else if (!changed.length) clauses.push("nothing omakit reads moved")
|
|
235
|
+
else {
|
|
236
|
+
if (moved.length) clauses.push(`moved since the pin: ${list(moved)}${verdictMoved.length ? "" : " (wording and labels only)"}`)
|
|
237
|
+
if (missing.length) clauses.push(`gone at HEAD: ${list(missing)}`)
|
|
238
|
+
if (formMoved) clauses.push(`the form moved (${asPath(FORMS)}/)`)
|
|
239
|
+
if (pinned.includes("/scripts/") && !moved.length && !missing.length) clauses.push(`scripts/ moved in none of the ${comparison.reads} files omakit reads`)
|
|
240
|
+
if (graded) clauses.push(policyDiffers ? `${policyText(pinPolicy)} at the pin, ${policyText(headPolicy)} at HEAD` : `${policyText(pinPolicy)} at both`)
|
|
241
|
+
}
|
|
242
|
+
const live = readLive.length ? `${list(readLive.map(asPath))} moved${clauses.length ? " too" : ""}, and ${readLive.length === 1 ? "that is" : "those are"} read live` : ""
|
|
243
|
+
const tail = !live ? "" : clauses.length ? ` (${live})` : `only ${live}`
|
|
244
|
+
const detail = current ? `the pin is the marketplace's current ${branch}-branch HEAD` : `${where}; ${clauses.join("; ")}${tail}`
|
|
245
|
+
|
|
246
|
+
const action = state === "info"
|
|
247
|
+
? `${moved.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
|
+
: state === "advice"
|
|
249
|
+
? upgrade
|
|
250
|
+
? `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.`
|
|
251
|
+
: "The maintainer is notified by the weekly pin-freshness run; a newer omakit will carry the pin. Until then every verdict here is the pin's, and the marketplace's own run on your issue is the one that counts."
|
|
252
|
+
: null
|
|
253
|
+
|
|
163
254
|
return {
|
|
164
255
|
id: "pin.freshness",
|
|
165
|
-
state
|
|
166
|
-
detail
|
|
167
|
-
action
|
|
168
|
-
? `A newer omakit may already carry the new pin: run \`omakit upgrade\`. If it does not, open an issue${issues ? ` at ${issues}/issues` : ""} naming the paths above.`
|
|
169
|
-
: null,
|
|
256
|
+
state,
|
|
257
|
+
detail,
|
|
258
|
+
action,
|
|
170
259
|
evidence: {
|
|
171
260
|
pinCommit: identity.commit,
|
|
172
261
|
marketplaceHead: head.commit,
|
|
173
262
|
branch,
|
|
174
|
-
...(
|
|
263
|
+
...(comparison === null ? {} : {
|
|
264
|
+
changedPaths: changed,
|
|
265
|
+
readLive,
|
|
266
|
+
pinned,
|
|
267
|
+
reads: comparison.reads,
|
|
268
|
+
moved,
|
|
269
|
+
verdictMoved,
|
|
270
|
+
missing,
|
|
271
|
+
policy: { pin: pinPolicy, head: headPolicy },
|
|
272
|
+
}),
|
|
175
273
|
},
|
|
176
274
|
}
|
|
177
275
|
}
|
|
178
276
|
|
|
179
277
|
/**
|
|
180
278
|
* @param {{ repoRoot: string, offline?: boolean, env?: object, npmPrefix?: () => string|null,
|
|
181
|
-
* resolveHead?: typeof defaultBranchHead, latest?: typeof registryLatest }} options
|
|
279
|
+
* resolveHead?: typeof defaultBranchHead, latest?: typeof registryLatest, compare?: typeof comparePin }} options
|
|
182
280
|
* `env` and `npmPrefix` are injectable for tests of the PATH check;
|
|
183
|
-
* `resolveHead` and `
|
|
184
|
-
* network, whose defaults are the tool's one HEAD resolver
|
|
185
|
-
* registry read.
|
|
281
|
+
* `resolveHead`, `latest` and `compare` for tests of the two checks that
|
|
282
|
+
* read the network, whose defaults are the tool's one HEAD resolver, its
|
|
283
|
+
* one registry read and the pin comparison above.
|
|
186
284
|
*/
|
|
187
|
-
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = registryLatest }) {
|
|
285
|
+
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = registryLatest, compare = comparePin }) {
|
|
188
286
|
// Optional: told what is being read while the network answers. Never
|
|
189
287
|
// affects the result.
|
|
190
288
|
const phase = onPhase || (() => {})
|
|
@@ -201,7 +299,9 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
201
299
|
// What is installed and what is published, one check: the two facts are
|
|
202
300
|
// one question, "is this the current omakit", and were two lines before.
|
|
203
301
|
// Offline, the first fact alone, as information. The evidence carries both
|
|
204
|
-
// and where the second came from.
|
|
302
|
+
// and where the second came from. `upgrade` is the command when a newer
|
|
303
|
+
// omakit is published, for pin.freshness to name below.
|
|
304
|
+
let upgrade = null
|
|
205
305
|
const versionCheck = (state, detail, action = null, latest = null, source = null) =>
|
|
206
306
|
add("omakit.version", state, detail, action, { installed: self.version, latest, source })
|
|
207
307
|
if (offline) {
|
|
@@ -214,11 +314,12 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
214
314
|
if (comparison === null) {
|
|
215
315
|
versionCheck("unknown", `${self.version}; the installed or published version is invalid`)
|
|
216
316
|
} else {
|
|
317
|
+
if (comparison > 0) upgrade = upgradeCommand(repoRoot, self.name)
|
|
217
318
|
versionCheck(comparison > 0 ? "advice" : "ok",
|
|
218
319
|
comparison === 0 ? `${self.version}, the newest published version`
|
|
219
320
|
: comparison < 0 ? `${self.version}; ahead of the newest published version ${published.version}`
|
|
220
321
|
: `${self.version}; ${published.version} is published`,
|
|
221
|
-
comparison > 0 ? `run \`${
|
|
322
|
+
comparison > 0 ? `run \`${upgrade}\`` : null,
|
|
222
323
|
published.version, NPM_REGISTRY)
|
|
223
324
|
}
|
|
224
325
|
} else {
|
|
@@ -293,12 +394,12 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
293
394
|
phase("reading the marketplace's current default-branch HEAD")
|
|
294
395
|
try {
|
|
295
396
|
const head = await resolveHead(MARKETPLACE_PIN.repository)
|
|
296
|
-
let
|
|
397
|
+
let comparison = { changedPaths: [], reads: pinnedReadSet(dir).length, moved: [], missing: [], policyAtHead: null }
|
|
297
398
|
if (head.commit !== identity.commit) {
|
|
298
|
-
phase("comparing each
|
|
299
|
-
|
|
399
|
+
phase("comparing each file omakit reads between the pin and HEAD")
|
|
400
|
+
comparison = await compare({ pinDir: dir, headCommit: head.commit, pinCommit: identity.commit })
|
|
300
401
|
}
|
|
301
|
-
checks.push(pinFreshness(identity, head,
|
|
402
|
+
checks.push(pinFreshness(identity, head, comparison, { upgrade }))
|
|
302
403
|
} catch (error) {
|
|
303
404
|
add("pin.freshness", "unknown", `could not read the marketplace's HEAD (${error.code || "error"})`,
|
|
304
405
|
error.code === "network-unavailable" ? "Connect to the network, or pass --offline to skip the two checks that need it." : null,
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
//
|
|
5
5
|
// It fetches only what omakit reads. The marketplace at this commit is 325 MB,
|
|
6
6
|
// of which 168 MB is preview imagery and 151 MB is history, and omakit reads
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// twenty files out of it, sixteen of them under scripts/ (PIN_READS). A
|
|
8
|
+
// blob-filtered, sparsely checked out fetch of just those paths is 15 MB and
|
|
9
|
+
// takes 2 seconds instead of 17. PIN_PATHS below is the whole list, and
|
|
10
|
+
// tests/unit/pin.test.mjs fails if any module starts reading a path outside
|
|
11
|
+
// it, because on a partial clone such a read would quietly reach for the
|
|
12
|
+
// network instead of failing.
|
|
12
13
|
import { execFileSync, spawnSync } from "node:child_process"
|
|
13
14
|
import { existsSync, readdirSync, readFileSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
14
|
-
import { dirname, join, resolve } from "node:path"
|
|
15
|
+
import { dirname, join, posix, resolve } from "node:path"
|
|
15
16
|
import { omakitCacheDir } from "./paths.mjs"
|
|
16
17
|
|
|
17
18
|
/** Raised for every way the pin can be missing or wrong; the code is what the CLI keys its remedy on. */
|
|
@@ -26,8 +27,8 @@ export class PinError extends Error {
|
|
|
26
27
|
|
|
27
28
|
export const MARKETPLACE_PIN = Object.freeze({
|
|
28
29
|
repository: "https://github.com/omacom/omarchy-plugin-marketplace",
|
|
29
|
-
commit: "
|
|
30
|
-
commitSubject: "Add
|
|
30
|
+
commit: "b7b2965431c52fc6311fdb389bd9f0d6275235c4",
|
|
31
|
+
commitSubject: "Add OmaStudio plugin (#6843)",
|
|
31
32
|
baselineVersion: "3",
|
|
32
33
|
enforcementMode: "selective",
|
|
33
34
|
})
|
|
@@ -52,6 +53,147 @@ export const PIN_PATHS = Object.freeze([
|
|
|
52
53
|
"/.github/ISSUE_TEMPLATE/",
|
|
53
54
|
])
|
|
54
55
|
|
|
56
|
+
/** The policy module, read at the pin for its two constants (readPinIdentity) and at HEAD as text by doctor for the same two. */
|
|
57
|
+
export const POLICY_MODULE = "scripts/security-baseline-policy.mjs"
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The files omakit opens under /scripts/, and how. PIN_PATHS fetches the
|
|
61
|
+
* directory whole because these import each other; this is what is opened
|
|
62
|
+
* out of it, so `omakit doctor` can tell a change to one of the 34 files
|
|
63
|
+
* there at the pin that omakit reads from a change to one it does not.
|
|
64
|
+
* Measured on 2026-09-21 (docs/MEASUREMENTS.md M7): of the three commits
|
|
65
|
+
* that touched scripts/ since the first pin 38060f89, one touched only
|
|
66
|
+
* `repository-identity.mjs`, a file omakit neither opens nor imports
|
|
67
|
+
* through anything it opens, and doctor called the pin behind for it.
|
|
68
|
+
*
|
|
69
|
+
* An `imported` file is executed, and what it imports is read with it:
|
|
70
|
+
* pinnedReadSet() follows the static imports. A file read as text is read
|
|
71
|
+
* alone, since a constant taken out of its text does not change when its
|
|
72
|
+
* imports do.
|
|
73
|
+
*
|
|
74
|
+
* submission.mjs form.mjs, watch.mjs
|
|
75
|
+
* plugin-verification-request.mjs form.mjs, watch.mjs
|
|
76
|
+
* security-baseline-scanner.mjs run-baseline.mjs
|
|
77
|
+
* security-baseline-policy.mjs preflight.mjs, watch.mjs, review-cost.mjs;
|
|
78
|
+
* as text here, for readPinIdentity
|
|
79
|
+
* security-baseline-report.mjs preflight.mjs
|
|
80
|
+
* security-baseline-record.mjs watch.mjs
|
|
81
|
+
* submission-feedback.mjs watch.mjs, and as text for its codes
|
|
82
|
+
* build-catalog.mjs registry.mjs, text: it imports sharp
|
|
83
|
+
* approve-submission.mjs watch.mjs, text: one label
|
|
84
|
+
* approve-plugin-update.mjs review-cost.mjs, text: one label
|
|
85
|
+
*
|
|
86
|
+
* 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.
|
|
88
|
+
*/
|
|
89
|
+
export const PIN_READS = Object.freeze([
|
|
90
|
+
Object.freeze({ path: "scripts/submission.mjs", imported: true }),
|
|
91
|
+
Object.freeze({ path: "scripts/plugin-verification-request.mjs", imported: true }),
|
|
92
|
+
Object.freeze({ path: "scripts/security-baseline-scanner.mjs", imported: true }),
|
|
93
|
+
Object.freeze({ path: POLICY_MODULE, imported: true }),
|
|
94
|
+
Object.freeze({ path: "scripts/security-baseline-report.mjs", imported: true }),
|
|
95
|
+
Object.freeze({ path: "scripts/security-baseline-record.mjs", imported: true }),
|
|
96
|
+
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 }),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The read files omakit takes only wording or a label out of, and decides
|
|
104
|
+
* nothing by: the baseline's rendered details, the failure table a refusal's
|
|
105
|
+
* reason is mapped through for display, and one label name each from the
|
|
106
|
+
* two approval scripts. A change to one of these changes what omakit
|
|
107
|
+
* prints, never whether it passes or refuses. Every other file in
|
|
108
|
+
* pinnedReadSet() is executed or read for a rule, a limit, a constant or a
|
|
109
|
+
* parser, so a change there can change a verdict, and `omakit doctor`
|
|
110
|
+
* grades it advice. The list is a whitelist on purpose: a file that is not
|
|
111
|
+
* here is treated as verdict-bearing until someone proves otherwise here.
|
|
112
|
+
*/
|
|
113
|
+
export const WORDING_READS = Object.freeze([
|
|
114
|
+
"scripts/security-baseline-report.mjs",
|
|
115
|
+
"scripts/submission-feedback.mjs",
|
|
116
|
+
"scripts/approve-submission.mjs",
|
|
117
|
+
"scripts/approve-plugin-update.mjs",
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
/** A static import or re-export of a relative module: `import x from "./y.mjs"`, `export * from "./y.mjs"`, across lines. */
|
|
121
|
+
export const RELATIVE_IMPORT = /\bfrom\s+["'](\.\.?\/[^"']+)["']/g
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The import forms RELATIVE_IMPORT does not follow. (A relative `from`
|
|
125
|
+
* specifier of any kind is followed, a JSON import included, so that needs
|
|
126
|
+
* no guard; Node's ESM loader requires an extension.) If one of these ever
|
|
127
|
+
* appears in an executed file of the pinned read set, pinnedReadSet() could
|
|
128
|
+
* miss a file, and tests/unit/pin.test.mjs fails on the file and line
|
|
129
|
+
* instead of doctor staying quiet. Measured at pin b7b29654: none of
|
|
130
|
+
* the 13 executed files uses any of them (the three read as text are not
|
|
131
|
+
* scanned, since nothing they import is loaded).
|
|
132
|
+
*/
|
|
133
|
+
export const UNFOLLOWED_IMPORTS = Object.freeze([
|
|
134
|
+
Object.freeze({ form: "dynamic import()", pattern: /\bimport\s*\(/ }),
|
|
135
|
+
Object.freeze({ form: "side-effect import", pattern: /^\s*import\s+["']/ }),
|
|
136
|
+
Object.freeze({ form: "require()", pattern: /\brequire\s*\(/ }),
|
|
137
|
+
Object.freeze({ form: "import.meta.resolve()", pattern: /\bimport\.meta\.resolve\s*\(/ }),
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every line in the read set's executed files that carries an import form
|
|
142
|
+
* the closure does not follow: `{ path, line, form, text }`, empty when the
|
|
143
|
+
* closure is complete. Files read as text are not scanned: nothing they
|
|
144
|
+
* import is loaded. Read as text, like the closure itself.
|
|
145
|
+
*/
|
|
146
|
+
export function unfollowedImports(pinDir, reads = pinnedReadSet(pinDir)) {
|
|
147
|
+
const found = []
|
|
148
|
+
for (const { path } of reads.filter((read) => read.imported)) {
|
|
149
|
+
const lines = readFileSync(join(pinDir, path), "utf8").split("\n")
|
|
150
|
+
lines.forEach((text, index) => {
|
|
151
|
+
for (const { form, pattern } of UNFOLLOWED_IMPORTS) {
|
|
152
|
+
if (pattern.test(text)) found.push({ path, line: index + 1, form, text: text.trim() })
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
return found
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Every file omakit reads under /scripts/ at the checkout in `pinDir`:
|
|
161
|
+
* PIN_READS, plus, for each imported one, what it imports in turn, found
|
|
162
|
+
* by regex over `from "./x.mjs"` in the file's text and never by loading
|
|
163
|
+
* it. Paths relative to the checkout, sorted, each once, with `via` naming
|
|
164
|
+
* the file that imports it or null for one omakit opens itself. A
|
|
165
|
+
* specifier that leaves scripts/ (the marketplace's site/ assets) or names
|
|
166
|
+
* a package (`sharp`) is not followed: PIN_PATHS does not fetch it, so
|
|
167
|
+
* omakit could not read it. Measured at pin b7b29654: 16 of the 34 files
|
|
168
|
+
* under scripts/, 10 opened by omakit and 6 imported by those.
|
|
169
|
+
*
|
|
170
|
+
* @returns {{ path: string, imported: boolean, via: string|null }[]}
|
|
171
|
+
*/
|
|
172
|
+
export function pinnedReadSet(pinDir, reads = PIN_READS) {
|
|
173
|
+
const set = new Map()
|
|
174
|
+
const queue = []
|
|
175
|
+
for (const read of reads) {
|
|
176
|
+
set.set(read.path, { path: read.path, imported: read.imported, via: null })
|
|
177
|
+
if (read.imported) queue.push(read.path)
|
|
178
|
+
}
|
|
179
|
+
while (queue.length) {
|
|
180
|
+
const from = queue.shift()
|
|
181
|
+
let text
|
|
182
|
+
try {
|
|
183
|
+
text = readFileSync(join(pinDir, from), "utf8")
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw new PinError(`cannot read ${from} from the pinned checkout at ${pinDir}: ${error.message}`)
|
|
186
|
+
}
|
|
187
|
+
for (const match of text.matchAll(RELATIVE_IMPORT)) {
|
|
188
|
+
const path = posix.normalize(posix.join(posix.dirname(from), match[1]))
|
|
189
|
+
if (!path.startsWith("scripts/") || set.has(path)) continue
|
|
190
|
+
set.set(path, { path, imported: true, via: from })
|
|
191
|
+
queue.push(path)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [...set.values()].sort((a, b) => a.path.localeCompare(b.path))
|
|
195
|
+
}
|
|
196
|
+
|
|
55
197
|
/**
|
|
56
198
|
* The user-writable pin location: `$XDG_CACHE_HOME/omakit/marketplace`, or
|
|
57
199
|
* `~/.cache/omakit/marketplace`. omakit reads no variable of its own; a test or
|
|
@@ -86,14 +228,27 @@ function git(dir, args, options = {}) {
|
|
|
86
228
|
return execFileSync("git", ["-C", dir, ...args], { timeout: 300_000, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...options })
|
|
87
229
|
}
|
|
88
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The two policy constants out of the policy module's text, the way the
|
|
233
|
+
* pin's identity has always read them: `securityBaselineVersion` and
|
|
234
|
+
* `securityBaselineEnforcementMode`, or "unknown" where the text has no
|
|
235
|
+
* such line. Text in, two strings out; the module is not loaded, so the
|
|
236
|
+
* same read serves doctor for the module at HEAD.
|
|
237
|
+
*/
|
|
238
|
+
export function policyConstants(text) {
|
|
239
|
+
const source = String(text || "")
|
|
240
|
+
return {
|
|
241
|
+
baselineVersion: source.match(/securityBaselineVersion\s*=\s*"?([^";\s]+)"?/)?.[1] || "unknown",
|
|
242
|
+
enforcementMode: source.match(/securityBaselineEnforcementMode\s*=\s*"([^"]+)"/)?.[1] || "unknown",
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
89
246
|
/** Identity of the checkout at `dir`: commit plus the policy constants read from the pinned source. */
|
|
90
247
|
function readPinIdentity(dir) {
|
|
91
248
|
const commit = git(dir, ["rev-parse", "HEAD"]).trim()
|
|
92
|
-
const
|
|
93
|
-
const version = policy.match(/securityBaselineVersion\s*=\s*"?([^";\s]+)"?/)?.[1] || "unknown"
|
|
94
|
-
const mode = policy.match(/securityBaselineEnforcementMode\s*=\s*"([^"]+)"/)?.[1] || "unknown"
|
|
249
|
+
const { baselineVersion, enforcementMode } = policyConstants(git(dir, ["show", `${commit}:${POLICY_MODULE}`]))
|
|
95
250
|
const dirty = git(dir, ["status", "--porcelain"]).trim().length > 0
|
|
96
|
-
return { commit, baselineVersion
|
|
251
|
+
return { commit, baselineVersion, enforcementMode, dirty }
|
|
97
252
|
}
|
|
98
253
|
|
|
99
254
|
/**
|
|
@@ -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
|
-
// 3,
|
|
7
|
-
// `passed`, 1,
|
|
6
|
+
// 3,619 listed sources with a recorded baseline at the pin, it produced 2,031
|
|
7
|
+
// `passed`, 1,562 `review-required` and 26 `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, requirePin } from "./pin.mjs"
|
|
29
|
+
import { MARKETPLACE_PIN, POLICY_MODULE, requirePin } from "./pin.mjs"
|
|
30
30
|
import { defaultBranchHead, getJson } from "./github.mjs"
|
|
31
31
|
import { omakitCacheDir } from "./paths.mjs"
|
|
32
32
|
|
|
@@ -34,9 +34,17 @@ export const CATALOG_PATH = "site/catalog.json"
|
|
|
34
34
|
export const REGISTRY_PATH = "registry.json"
|
|
35
35
|
export const CATALOG_BUILDER_PATH = "scripts/build-catalog.mjs"
|
|
36
36
|
|
|
37
|
-
/** The only two marketplace files
|
|
37
|
+
/** The only two marketplace files whose content omakit uses from HEAD. Everything else comes from the pin. */
|
|
38
38
|
export const LIVE_PATHS = Object.freeze([REGISTRY_PATH, CATALOG_PATH])
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The one file read from HEAD as text and used for nothing: `omakit doctor`
|
|
42
|
+
* reads the policy module at HEAD to compare its two constants with the
|
|
43
|
+
* pin's (pin.mjs policyConstants) and drops the text. It is never imported,
|
|
44
|
+
* never cached and never a source of a rule; the rules stay the pin's.
|
|
45
|
+
*/
|
|
46
|
+
export const HEAD_TEXT_PATHS = Object.freeze([POLICY_MODULE])
|
|
47
|
+
|
|
40
48
|
export class RegistryError extends Error {
|
|
41
49
|
constructor(code, message) {
|
|
42
50
|
super(message)
|
|
@@ -140,7 +148,9 @@ export function sameRepository(a, b) {
|
|
|
140
148
|
* one explicit 40-character commit on the marketplace's raw file host. Never
|
|
141
149
|
* a branch name, so the two files always come from the same commit and the
|
|
142
150
|
* commit named in the output is the one they came from; never a path outside
|
|
143
|
-
* LIVE_PATHS, so nothing executable can arrive this way.
|
|
151
|
+
* LIVE_PATHS, so nothing executable can arrive this way. (headTextUrl below
|
|
152
|
+
* reaches the same host for one module's text, which is compared and never
|
|
153
|
+
* run.)
|
|
144
154
|
*/
|
|
145
155
|
export function liveFileUrl(commit, path) {
|
|
146
156
|
if (!/^[0-9a-f]{40}$/.test(String(commit))) {
|
|
@@ -149,6 +159,27 @@ export function liveFileUrl(commit, path) {
|
|
|
149
159
|
if (!LIVE_PATHS.includes(path)) {
|
|
150
160
|
throw new RegistryError("usage", `${path} is never read from HEAD; only ${LIVE_PATHS.join(" and ")} are`)
|
|
151
161
|
}
|
|
162
|
+
return rawFileUrl(commit, path)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The same URL shape for the one file doctor reads from HEAD as text:
|
|
167
|
+
* HEAD_TEXT_PATHS, at a 40-character commit, through the same host and the
|
|
168
|
+
* same GET call site. Its text is compared and dropped; `liveFileUrl` still
|
|
169
|
+
* refuses it, so nothing reads it as data.
|
|
170
|
+
*/
|
|
171
|
+
export function headTextUrl(commit, path) {
|
|
172
|
+
if (!/^[0-9a-f]{40}$/.test(String(commit))) {
|
|
173
|
+
throw new RegistryError("usage", `a marketplace file is read at a 40-character commit, not "${commit}"`)
|
|
174
|
+
}
|
|
175
|
+
if (!HEAD_TEXT_PATHS.includes(path)) {
|
|
176
|
+
throw new RegistryError("usage", `${path} is never read from HEAD as text; only ${HEAD_TEXT_PATHS.join(" and ")} ${HEAD_TEXT_PATHS.length === 1 ? "is" : "are"}`)
|
|
177
|
+
}
|
|
178
|
+
return rawFileUrl(commit, path)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The raw file host at one commit; the two builders above are its only callers, and each holds its own path list. */
|
|
182
|
+
function rawFileUrl(commit, path) {
|
|
152
183
|
const raw = MARKETPLACE_PIN.repository.replace(/^https:\/\/github\.com\//, "https://raw.githubusercontent.com/")
|
|
153
184
|
return `${raw}/${commit}/${path}`
|
|
154
185
|
}
|