omakit 0.1.6 → 0.1.8
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/README.md +31 -11
- package/package.json +1 -1
- package/skills/omarchy-plugin-submit/SKILL.md +34 -7
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -1
- package/tests/parity/run.mjs +145 -113
- package/tools/marketplace/README.md +19 -5
- package/tools/marketplace/cli.mjs +36 -19
- package/tools/marketplace/doctor.mjs +68 -25
- package/tools/marketplace/form.mjs +36 -4
- package/tools/marketplace/registry.mjs +76 -7
- package/tools/marketplace/report.mjs +32 -7
- package/tools/marketplace/style.mjs +13 -3
- package/tools/marketplace/submit.mjs +68 -32
- package/tools/marketplace/usage.mjs +12 -8
- package/tools/marketplace/watch.mjs +21 -11
package/README.md
CHANGED
|
@@ -134,16 +134,32 @@ file and line, and the official text verbatim, then the marketplace's own
|
|
|
134
134
|
statement. `--json` prints the document itself, unchanged from earlier
|
|
135
135
|
releases, and `--out <file>` writes it; agents and the skills use those.
|
|
136
136
|
|
|
137
|
-
`omakit submit` reads the marketplace's registry first
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
`omakit submit` reads the marketplace's registry first, and a run has three
|
|
138
|
+
outcomes. `READY`, exit 0: every blocking check passed and the title and body
|
|
139
|
+
follow. `REFUSED`, exit 1: a blocking check failed and no body is produced.
|
|
140
|
+
`LISTED`, exit 0: the plugin is already listed by its own repository (the
|
|
141
|
+
manifest id is in the catalog, and the listing's repository is the subject's
|
|
142
|
+
declared `origin`, compared as owner and name), so the submission form is not
|
|
143
|
+
the route. Nothing is wrong and nothing was refused: `identity.available`
|
|
144
|
+
passes with the listing's record (since when, which commit, verified or not),
|
|
145
|
+
the five checks that exist only for the body are omitted, nothing is asked,
|
|
146
|
+
and the closing block names the commit the marketplace lists, the local
|
|
147
|
+
commit, whether they are the same, and the marketplace's verification form
|
|
148
|
+
with the choice that lists a newer commit, read from the pin's
|
|
149
|
+
`verify-plugin.yml`. Measured on 0.1.6: this state printed `FAIL
|
|
150
|
+
identity.available`, `REFUSED`, and "Fix it, then run submit again" under a
|
|
151
|
+
remedy that said there was nothing to submit. An id taken by another
|
|
152
|
+
repository, a retired id or a reserved one is still refused. In `--json`, the
|
|
153
|
+
outcome is `outcome: "ready" | "refused" | "listed"`, `ready` stays a boolean
|
|
154
|
+
that is true for the first only, and a listed run carries a `listing` object.
|
|
155
|
+
|
|
156
|
+
An unlisted plugin needs a category and tags, and they are an editorial
|
|
141
157
|
choice nobody else can make: at a terminal it asks, once each, with the form's
|
|
142
158
|
own lists numbered and the marketplace's own default for the manifest's kinds
|
|
143
159
|
offered where it is on the list; in a pipe, from an agent, or with `--json` it
|
|
144
|
-
is the usage error with the same lists, exit 2.
|
|
145
|
-
with the command line that repeats
|
|
146
|
-
carries it as `reproduce`.
|
|
160
|
+
is the usage error with the same lists, exit 2. A listed plugin is asked for
|
|
161
|
+
neither. A `READY` or `REFUSED` report ends with the command line that repeats
|
|
162
|
+
the run without asking, and `--json` carries it as `reproduce`.
|
|
147
163
|
|
|
148
164
|
There is nothing to authenticate. If you have `gh auth login` done, omakit
|
|
149
165
|
reads that credential for GET requests and stores nothing; a token in
|
|
@@ -215,16 +231,20 @@ the cache. Bumping it changes where the submission contract and the baseline
|
|
|
215
231
|
policy are read from, and the procedure in
|
|
216
232
|
[docs/UPSTREAM_CONTRACT.md](docs/UPSTREAM_CONTRACT.md) ends in re-proving
|
|
217
233
|
transport parity and committing the evidence. `omakit doctor` tells you when the
|
|
218
|
-
pin is behind
|
|
219
|
-
|
|
220
|
-
|
|
234
|
+
pin is behind in something omakit reads from it, names which paths changed,
|
|
235
|
+
and then leaves it alone: `registry.json` and `site/catalog.json` moving is
|
|
236
|
+
fine, because those are read live from HEAD (about 140 commits a day touch
|
|
237
|
+
only `registry.json`, so "behind" alone would be true of every run); the
|
|
238
|
+
marketplace's code or forms moving is a note, and what you can do about it
|
|
239
|
+
is run `omakit upgrade`, since a newer omakit may already carry the new pin,
|
|
240
|
+
and otherwise open an issue naming the paths. That the
|
|
221
241
|
pin can go stale unnoticed is the same defect class `omakit watch` reports, so it
|
|
222
242
|
would be poor form to hide it here.
|
|
223
243
|
|
|
224
244
|
## Evidence, not claims
|
|
225
245
|
|
|
226
246
|
```bash
|
|
227
|
-
npm test #
|
|
247
|
+
npm test # node --test, no dependencies; green from `git archive` too
|
|
228
248
|
```
|
|
229
249
|
|
|
230
250
|
| Claim | Proof |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omakit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "The safe place to find out: everything knowable about an Omarchy Quattro plugin submission before you post it, on your own machine. Agent-first, read-only, posts nothing, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Maarten Tolhuijs",
|
|
@@ -46,9 +46,9 @@ message lists the controlled values, read from the pinned form; with `--json`
|
|
|
46
46
|
the same lists come back under `usage`. A person at a terminal is asked
|
|
47
47
|
instead, once each, with the marketplace's own default for the manifest's
|
|
48
48
|
kinds. It is decided after the registry is read: a plugin that is already
|
|
49
|
-
listed is
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
listed is asked for nothing. A `READY` or `REFUSED` report ends with the
|
|
50
|
+
command line that repeats the run without asking, and `--json` carries it as
|
|
51
|
+
`reproduce`.
|
|
52
52
|
|
|
53
53
|
To see the official baseline result alone, with no Omakit check around it:
|
|
54
54
|
|
|
@@ -57,7 +57,9 @@ omakit verify <path-to-the-plugin-repo> --json
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
`verify` without `--json` prints a report for a person; `--json` is the
|
|
60
|
-
document, unchanged between releases.
|
|
60
|
+
document, unchanged between releases. It needs no network once the pin
|
|
61
|
+
exists; a reviewer-mode target (`<https url>@<40-char sha>`) is fetched once,
|
|
62
|
+
read-only, into omakit's own cache.
|
|
61
63
|
|
|
62
64
|
Useful flags: `--notes` for the Maintainer notes field, `--suggest-tag` for the
|
|
63
65
|
optional suggestion, `--name` when the manifest has no name, `--json` for a
|
|
@@ -72,9 +74,34 @@ HEAD, tell the owner and choose another id.
|
|
|
72
74
|
|
|
73
75
|
## Reading the result
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
A run ends one of three ways; `--json` carries it as `outcome`.
|
|
78
|
+
|
|
79
|
+
- `READY`, exit 0, `outcome: "ready"`: every blocking check passed and the
|
|
80
|
+
output contains the issue title and body.
|
|
81
|
+
- `REFUSED`, exit 1, `outcome: "refused"`: a blocking check failed and no body
|
|
82
|
+
was produced.
|
|
83
|
+
- `LISTED`, exit 0, `outcome: "listed"`: the plugin is already listed by its
|
|
84
|
+
own repository. Nothing is wrong and nothing was refused, and there is no
|
|
85
|
+
body, because the submission form is not the route. See below.
|
|
86
|
+
|
|
87
|
+
Exit code 2 is a usage error: nothing was checked.
|
|
88
|
+
|
|
89
|
+
**If the outcome is `listed`, stop.** Do not open a submission issue, and
|
|
90
|
+
never change the plugin id to get past it: the id is listed by this very
|
|
91
|
+
repository, and a renamed id would be a second listing of the same plugin.
|
|
92
|
+
Tell the owner the update route, which the output states: the marketplace
|
|
93
|
+
lists `verificationCommit`; the local commit is `localCommit`, and
|
|
94
|
+
`sameCommit` says whether they are the same; to get a newer commit listed,
|
|
95
|
+
open the marketplace's verification form (its name and the choice to pick,
|
|
96
|
+
"Verify and publish a newer upstream commit", are printed from the pin's own
|
|
97
|
+
form, under `listing.updateRoute` in `--json`), and `omakit watch <the
|
|
98
|
+
submission issue>` shows which commit is listed now. A listed plugin is asked
|
|
99
|
+
for no category and no tags, and the five body checks are omitted rather than
|
|
100
|
+
shown as waiting.
|
|
101
|
+
|
|
102
|
+
An id listed by a *different* repository is a refusal, `identity.available`
|
|
103
|
+
names that repository, and the remedy is another id. That is the one case
|
|
104
|
+
where changing the id is the fix.
|
|
78
105
|
|
|
79
106
|
A check drawn as `▒ ?` did not run because one it depends on failed; its detail
|
|
80
107
|
names that check. It is not a failure of its own, and the closing refusal lists
|
|
@@ -44,7 +44,11 @@ it from the operator's `gh` login (which itself honours `GH_TOKEN` and
|
|
|
44
44
|
If the diagnosis ends in a resubmission under a new id, run `omakit submit`
|
|
45
45
|
online: never pass `--offline` to get around a listed id, because that reads
|
|
46
46
|
the listed ids from the pin, which is stale within hours, and the marketplace
|
|
47
|
-
refuses against its current registry.
|
|
47
|
+
refuses against its current registry. If `omakit submit` ends `LISTED`
|
|
48
|
+
(`outcome: "listed"` in `--json`), the plugin is already listed by its own
|
|
49
|
+
repository: stop, do not change the id, and tell the owner the update route
|
|
50
|
+
the output names, the marketplace's verification form with the choice "Verify
|
|
51
|
+
and publish a newer upstream commit", read from the pin.
|
|
48
52
|
|
|
49
53
|
## Acting on each verdict
|
|
50
54
|
|
package/tests/parity/run.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto"
|
|
|
14
14
|
import { execFileSync } from "node:child_process"
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
16
16
|
import { join, resolve } from "node:path"
|
|
17
|
+
import { pathToFileURL } from "node:url"
|
|
17
18
|
import { runBaseline } from "../../tools/marketplace/run-baseline.mjs"
|
|
18
19
|
import { requirePin } from "../../tools/marketplace/pin.mjs"
|
|
19
20
|
import { parityCorpus, strataSizes } from "./corpus.mjs"
|
|
@@ -22,14 +23,7 @@ import { token } from "../../tools/marketplace/github.mjs"
|
|
|
22
23
|
import { omakitCacheDir } from "../../tools/marketplace/paths.mjs"
|
|
23
24
|
import { parityOutput } from "../../tools/marketplace/parity-output.mjs"
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
const pinDir = requirePin(repoRoot).dir
|
|
27
|
-
const cacheDir = omakitCacheDir("parity")
|
|
28
|
-
const output = parityOutput({ repoRoot, out: process.env.PARITY_OUT || null })
|
|
29
|
-
const count = Number(process.env.PARITY_COUNT || 30)
|
|
30
|
-
const offset = Number(process.env.PARITY_OFFSET || 0)
|
|
31
|
-
|
|
32
|
-
function shallowClone(repoUrl, commit) {
|
|
26
|
+
function shallowClone(cacheDir, repoUrl, commit) {
|
|
33
27
|
const dir = join(cacheDir, subjectSlug(repoUrl))
|
|
34
28
|
mkdirSync(dir, { recursive: true })
|
|
35
29
|
const run = (args) => execFileSync("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
@@ -71,124 +65,162 @@ function pinCommit(dir) {
|
|
|
71
65
|
}
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
const rows = []
|
|
76
|
-
const ruleTotals = {}
|
|
77
|
-
const capabilityTotals = {}
|
|
78
|
-
let mismatches = 0
|
|
79
|
-
let failures = 0
|
|
80
|
-
|
|
81
|
-
for (const target of corpus) {
|
|
82
|
-
const row = {
|
|
83
|
-
repo: target.repo,
|
|
84
|
-
commit: target.commit,
|
|
85
|
-
type: target.type,
|
|
86
|
-
registryOutcome: target.registryOutcome,
|
|
87
|
-
registryOutcomeAtSameCommit: target.registryOutcomeCommit === target.commit,
|
|
88
|
-
}
|
|
68
|
+
function generatorIdentity(repoRoot) {
|
|
89
69
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
70
|
+
return { omakitCommit: execFileSync("git", ["-C", repoRoot, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() }
|
|
71
|
+
} catch {
|
|
72
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"))
|
|
73
|
+
return { omakitPackage: `${pkg.name}@${pkg.version}` }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The parity run. `cli.mjs` calls this with its own root and the flags it
|
|
79
|
+
* parsed; the adapter at the bottom of this file calls it from the command
|
|
80
|
+
* line. Everything the run needs arrives as an argument: omakit reads no
|
|
81
|
+
* environment variable of its own, and this handoff used to be four
|
|
82
|
+
* PARITY_* variables and an OMAKIT_ROOT, which was the one OMAKIT_* name in
|
|
83
|
+
* the tree and needed an exemption in the test that forbids them.
|
|
84
|
+
*
|
|
85
|
+
* @param {{ repoRoot: string, count?: number, offset?: number, out?: string|null, log?: (line: string) => void }} options
|
|
86
|
+
* `out` is an explicit evidence file; without it the evidence lands in
|
|
87
|
+
* `docs/evidence/parity/` under a dated name that never overwrites.
|
|
88
|
+
* @returns {Promise<{ summary: object, outFile: string, ok: boolean }>}
|
|
89
|
+
*/
|
|
90
|
+
export async function runParity({ repoRoot, count = 30, offset = 0, out = null, log = console.log }) {
|
|
91
|
+
const root = resolve(repoRoot)
|
|
92
|
+
const pinDir = requirePin(root).dir
|
|
93
|
+
const cacheDir = omakitCacheDir("parity")
|
|
94
|
+
// Resolved before any fetch, so a packaged read-only install refuses here.
|
|
95
|
+
const output = parityOutput({ repoRoot: root, out })
|
|
96
|
+
const corpus = parityCorpus(pinDir, count, offset)
|
|
97
|
+
const rows = []
|
|
98
|
+
const ruleTotals = {}
|
|
99
|
+
const capabilityTotals = {}
|
|
100
|
+
let mismatches = 0
|
|
101
|
+
let failures = 0
|
|
102
|
+
|
|
103
|
+
for (const target of corpus) {
|
|
104
|
+
const row = {
|
|
105
|
+
repo: target.repo,
|
|
106
|
+
commit: target.commit,
|
|
107
|
+
type: target.type,
|
|
108
|
+
registryOutcome: target.registryOutcome,
|
|
109
|
+
registryOutcomeAtSameCommit: target.registryOutcomeCommit === target.commit,
|
|
109
110
|
}
|
|
110
111
|
try {
|
|
111
|
-
const
|
|
112
|
-
|
|
112
|
+
const dir = shallowClone(cacheDir, target.repo, target.commit)
|
|
113
|
+
const local = await runBaseline({
|
|
114
|
+
repoRoot: root,
|
|
113
115
|
repoUrl: target.repo,
|
|
114
116
|
commitSha: target.commit,
|
|
115
|
-
transport: "
|
|
116
|
-
|
|
117
|
+
transport: "local",
|
|
118
|
+
repoDir: dir,
|
|
117
119
|
})
|
|
118
|
-
row.
|
|
119
|
-
row.
|
|
120
|
-
row.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
row.localOutcome = local.result.outcome
|
|
121
|
+
row.requests = local.adapter.requests
|
|
122
|
+
row.localDigest = digest(local.result)
|
|
123
|
+
// Rule and capability ids are counted in the summary, unattributed; they are
|
|
124
|
+
// deliberately not recorded against this repository.
|
|
125
|
+
for (const finding of local.result.findings || []) {
|
|
126
|
+
const id = finding.ruleId || finding.id
|
|
127
|
+
if (id) ruleTotals[id] = (ruleTotals[id] || 0) + 1
|
|
128
|
+
}
|
|
129
|
+
for (const capability of local.result.capabilities || []) {
|
|
130
|
+
if (capability.id) capabilityTotals[capability.id] = (capabilityTotals[capability.id] || 0) + 1
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const github = await runBaseline({
|
|
134
|
+
repoRoot: root,
|
|
135
|
+
repoUrl: target.repo,
|
|
136
|
+
commitSha: target.commit,
|
|
137
|
+
transport: "github",
|
|
138
|
+
token: token() ?? undefined,
|
|
139
|
+
})
|
|
140
|
+
row.githubOutcome = github.result.outcome
|
|
141
|
+
row.githubDigest = digest(github.result)
|
|
142
|
+
row.identical = row.localDigest === row.githubDigest
|
|
143
|
+
if (!row.identical) {
|
|
144
|
+
mismatches += 1
|
|
145
|
+
// A mismatch is the one case worth describing, and it is described as a
|
|
146
|
+
// difference between two transports of the same code, not as a finding
|
|
147
|
+
// about the plugin: which keys differ, never their contents.
|
|
148
|
+
const localPayload = JSON.parse(comparable(local.result))
|
|
149
|
+
const githubPayload = JSON.parse(comparable(github.result))
|
|
150
|
+
row.differingKeys = Object.keys(localPayload).filter(
|
|
151
|
+
(key) => JSON.stringify(localPayload[key]) !== JSON.stringify(githubPayload[key]),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
row.githubError = (error.code || "error") + ": " + error.message
|
|
156
|
+
failures += 1
|
|
131
157
|
}
|
|
132
158
|
} catch (error) {
|
|
133
|
-
row.
|
|
159
|
+
row.localError = (error.code || "error") + ": " + error.message
|
|
134
160
|
failures += 1
|
|
135
161
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
rows.push(row)
|
|
163
|
+
log(
|
|
164
|
+
[
|
|
165
|
+
row.identical === true ? "ok " : row.identical === false ? "DIFF" : "?? ",
|
|
166
|
+
target.repo.replace("https://github.com/", "").padEnd(44),
|
|
167
|
+
"local=" + (row.localOutcome || row.localError || "-"),
|
|
168
|
+
"github=" + (row.githubOutcome || row.githubError || "-"),
|
|
169
|
+
"registry=" + (row.registryOutcome || "-"),
|
|
170
|
+
].join(" "),
|
|
171
|
+
)
|
|
139
172
|
}
|
|
140
|
-
rows.push(row)
|
|
141
|
-
console.log(
|
|
142
|
-
[
|
|
143
|
-
row.identical === true ? "ok " : row.identical === false ? "DIFF" : "?? ",
|
|
144
|
-
target.repo.replace("https://github.com/", "").padEnd(44),
|
|
145
|
-
"local=" + (row.localOutcome || row.localError || "-"),
|
|
146
|
-
"github=" + (row.githubOutcome || row.githubError || "-"),
|
|
147
|
-
"registry=" + (row.registryOutcome || "-"),
|
|
148
|
-
].join(" "),
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
173
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
const summary = {
|
|
175
|
+
generator: generatorIdentity(root),
|
|
176
|
+
pinnedMarketplaceCommit: pinCommit(pinDir),
|
|
177
|
+
corpusSize: corpus.length,
|
|
178
|
+
requestedCount: count,
|
|
179
|
+
offset,
|
|
180
|
+
strata: strataSizes(count),
|
|
181
|
+
outcomes: rows.reduce((acc, row) => {
|
|
182
|
+
const key = row.localOutcome || "error"
|
|
183
|
+
acc[key] = (acc[key] || 0) + 1
|
|
184
|
+
return acc
|
|
185
|
+
}, {}),
|
|
186
|
+
identical: rows.filter((row) => row.identical === true).length,
|
|
187
|
+
mismatches,
|
|
188
|
+
failures,
|
|
189
|
+
// Unattributed aggregates. No repository name is attached to any rule.
|
|
190
|
+
ruleTotals,
|
|
191
|
+
capabilityTotals,
|
|
192
|
+
publication: {
|
|
193
|
+
perRepositoryDetail: "omitted",
|
|
194
|
+
reason: "Findings about a specific third-party plugin are not published. Equality is evidenced by the sha256 digests below, which anyone can recompute from the repository, the commit and the pinned marketplace commit.",
|
|
195
|
+
digest: "sha256 of {outcome, disposition, blocksApproval, findings, capabilities}, first 32 hex characters",
|
|
196
|
+
},
|
|
197
|
+
rows,
|
|
158
198
|
}
|
|
199
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
200
|
+
const outputIsFile = Boolean(out)
|
|
201
|
+
const outputDir = outputIsFile ? resolve(output, "..") : output
|
|
202
|
+
mkdirSync(outputDir, { recursive: true })
|
|
203
|
+
let outFile = outputIsFile ? output : join(outputDir, `${date}-local-vs-github.json`)
|
|
204
|
+
for (let n = 2; !outputIsFile && existsSync(outFile); n += 1) outFile = join(outputDir, `${date}-local-vs-github-${n}.json`)
|
|
205
|
+
writeFileSync(outFile, JSON.stringify(summary, null, 2) + "\n")
|
|
206
|
+
log("\nidentical " + summary.identical + "/" + corpus.length + ", mismatches " + mismatches + ", failures " + failures + " -> " + outFile)
|
|
207
|
+
return { summary, outFile, ok: mismatches === 0 && failures === 0 }
|
|
159
208
|
}
|
|
160
209
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
ruleTotals,
|
|
178
|
-
capabilityTotals,
|
|
179
|
-
publication: {
|
|
180
|
-
perRepositoryDetail: "omitted",
|
|
181
|
-
reason: "Findings about a specific third-party plugin are not published. Equality is evidenced by the sha256 digests below, which anyone can recompute from the repository, the commit and the pinned marketplace commit.",
|
|
182
|
-
digest: "sha256 of {outcome, disposition, blocksApproval, findings, capabilities}, first 32 hex characters",
|
|
183
|
-
},
|
|
184
|
-
rows,
|
|
210
|
+
// The command-line adapter: `node tests/parity/run.mjs [--count n] [--offset n]
|
|
211
|
+
// [--out file]` from the repository root. Thin on purpose; `omakit parity`
|
|
212
|
+
// is the same call with cli.mjs's own root.
|
|
213
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
214
|
+
const args = process.argv.slice(2)
|
|
215
|
+
const flag = (name) => {
|
|
216
|
+
const index = args.indexOf(name)
|
|
217
|
+
return index >= 0 ? args[index + 1] : undefined
|
|
218
|
+
}
|
|
219
|
+
const { ok } = await runParity({
|
|
220
|
+
repoRoot: process.cwd(),
|
|
221
|
+
count: Number(flag("--count") || 30),
|
|
222
|
+
offset: Number(flag("--offset") || 0),
|
|
223
|
+
out: flag("--out") || null,
|
|
224
|
+
})
|
|
225
|
+
process.exit(ok ? 0 : 1)
|
|
185
226
|
}
|
|
186
|
-
const date = new Date().toISOString().slice(0, 10)
|
|
187
|
-
const outputIsFile = process.env.PARITY_OUT_EXPLICIT === "1"
|
|
188
|
-
const outputDir = outputIsFile ? resolve(output, "..") : output
|
|
189
|
-
mkdirSync(outputDir, { recursive: true })
|
|
190
|
-
let outFile = outputIsFile ? output : join(outputDir, `${date}-local-vs-github.json`)
|
|
191
|
-
for (let n = 2; !outputIsFile && existsSync(outFile); n += 1) outFile = join(outputDir, `${date}-local-vs-github-${n}.json`)
|
|
192
|
-
writeFileSync(outFile, JSON.stringify(summary, null, 2) + "\n")
|
|
193
|
-
console.log("\nidentical " + summary.identical + "/" + corpus.length + ", mismatches " + mismatches + ", failures " + failures + " -> " + outFile)
|
|
194
|
-
process.exit(mismatches === 0 && failures === 0 ? 0 : 1)
|
|
@@ -13,17 +13,18 @@ local commit through the transport seam the marketplace tests itself
|
|
|
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
|
-
| `form.mjs` | The submission contract, read from the form and cross-checked against the marketplace's own constants. |
|
|
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 only way to the raw file host, at a 40-character commit, for those two files. |
|
|
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 only way to the raw file host, at a 40-character commit, for those two files. `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. |
|
|
21
21
|
| `issue.mjs` | Renders the issue the way the form would, then has the marketplace's own parser judge it. |
|
|
22
|
-
| `submit.mjs` | Assembles every check with its measured reason, and withholds the body when a blocking check fails. Decides the category and tags after the registry: a listed plugin
|
|
22
|
+
| `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)". |
|
|
23
23
|
| `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. |
|
|
24
|
+
| `doctor.mjs` | What is installed, what is pinned, and what has moved. `pin.freshness` compares each path in `PIN_PATHS` between the pin and the marketplace's HEAD by tree or blob id and splits what moved by the list `registry.mjs` reads live from: `registry.json` and `site/catalog.json` moving is `ok`, because those are read from HEAD anyway; anything else moving (`scripts/`, the two forms) is a `note` naming the paths, with an action a user can take, `omakit upgrade` and then an issue at the repository named in package.json. 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. |
|
|
24
25
|
| `watch.mjs` | The validation watch: validated commit versus current default-branch HEAD, and the one action that refreshes it. |
|
|
25
26
|
| `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. |
|
|
26
|
-
| `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. `docs/TUI.md` explains it. |
|
|
27
|
+
| `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. |
|
|
27
28
|
| `report.mjs` | Text rendering of submit, watch, doctor and verify for the agent that runs this tool, and the person reading over its shoulder. |
|
|
28
29
|
| `path-hint.mjs` | Is `omakit` reachable as a bare command, and if not, the one line that makes it so for the install that is here: a symlink for a clone, the npm prefix's `bin` on PATH for a package, said for the shell in `$SHELL`. `setup` and `doctor` print it; nothing writes an rc file. |
|
|
29
30
|
| `usage.mjs` | The help text, as data. |
|
|
@@ -31,7 +32,7 @@ local commit through the transport seam the marketplace tests itself
|
|
|
31
32
|
| `banner.mjs` | The wordmark, on a bare `omakit` and in `setup` only. |
|
|
32
33
|
| `effect.mjs` | The one text effect: the wordmark through `ttfx` where it is drawn, with frozen arguments, a hard budget, no colour of its own, and nothing at all when `ttfx` is not there. |
|
|
33
34
|
| `progress.mjs` | The progress line, on stderr, only when a person is looking. |
|
|
34
|
-
| `cli.mjs` | The one entry point behind `bin/omakit`, and the one register every failure is reported in. |
|
|
35
|
+
| `cli.mjs` | The one entry point behind `bin/omakit`, and the one register every failure is reported in. `submit` exits on the outcome: 1 for `refused`, 0 for `ready` and `listed`. |
|
|
35
36
|
|
|
36
37
|
```text
|
|
37
38
|
omakit pin
|
|
@@ -60,6 +61,19 @@ the registry recorded so the corpus always contains repositories that are not
|
|
|
60
61
|
side rather than the findings themselves. The GitHub side uses whatever
|
|
61
62
|
credential `github.mjs` resolves (a `gh` login, and only that), read-only.
|
|
62
63
|
|
|
64
|
+
Two rules about the output, stated as rules because each is an exception to
|
|
65
|
+
a wider one. Everything omakit writes itself stays within 80 columns; text
|
|
66
|
+
that will be posted verbatim, the marketplace's own baseline report and the
|
|
67
|
+
issue body rendered from the pinned form, is never wrapped and may exceed 80,
|
|
68
|
+
because a wrapped body would not be the body. And stdout is the whole result;
|
|
69
|
+
stderr carries interactive decoration, the progress line from `progress.mjs`
|
|
70
|
+
and the chooser from `ask.mjs`, only when stderr is a TTY, so a piped stderr
|
|
71
|
+
is empty on success, and failures go to stderr always. `tests/unit/cli.test.mjs`
|
|
72
|
+
holds both: the width test names the two verbatim regions by their heading
|
|
73
|
+
and holds every other line to 80, the three-way test asserts the empty pipe,
|
|
74
|
+
and a pseudo-terminal test asserts that a terminal's stderr gets the progress
|
|
75
|
+
sequences and nothing else.
|
|
76
|
+
|
|
63
77
|
`parity`, `watch`, `doctor` and `submit` reach the network, with Node's
|
|
64
78
|
built-in `fetch`, which does not read proxy environment variables by default.
|
|
65
79
|
Behind a proxy, run them with `NODE_USE_ENV_PROXY=1`. `submit` reads two things
|
|
@@ -30,7 +30,6 @@ import { banner, bannerEnabled } from "./banner.mjs"
|
|
|
30
30
|
import { COMMANDS, renderSummary, renderUsage, TAGLINE } from "./usage.mjs"
|
|
31
31
|
import { action, colourEnabled, GUTTER, labelled, mark, styler, wrap } from "./style.mjs"
|
|
32
32
|
import { omakitCacheDir } from "./paths.mjs"
|
|
33
|
-
import { parityOutput } from "./parity-output.mjs"
|
|
34
33
|
|
|
35
34
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..")
|
|
36
35
|
|
|
@@ -84,6 +83,18 @@ function positionals(args) {
|
|
|
84
83
|
return args.filter((value, index) => !value.startsWith("--") && !valued.has(args[index - 1]))
|
|
85
84
|
}
|
|
86
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The progress line for a command, or nothing under --json: a machine
|
|
88
|
+
* reading the document on stdout gets no decoration on stderr either.
|
|
89
|
+
* Measured on 0.1.6: submit, watch and doctor silenced it and verify did
|
|
90
|
+
* not, so `verify --json` at a terminal drew a progress line the others
|
|
91
|
+
* never drew.
|
|
92
|
+
*/
|
|
93
|
+
const SILENT = Object.freeze({ phase: () => {}, done: () => {} })
|
|
94
|
+
function spinnerFor(args) {
|
|
95
|
+
return args.includes("--json") ? SILENT : progress()
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
function emit(args, text) {
|
|
88
99
|
const out = option(args, "--out")
|
|
89
100
|
if (out) {
|
|
@@ -100,7 +111,7 @@ async function cmdSubmit(args) {
|
|
|
100
111
|
const target = positionals(args)[0]
|
|
101
112
|
if (!target) fail("usage", "submit needs a target: `omakit submit <target> --category <c> --tags <a,b>`", 2)
|
|
102
113
|
const json = args.includes("--json")
|
|
103
|
-
const spinner =
|
|
114
|
+
const spinner = spinnerFor(args)
|
|
104
115
|
// A missing --category or --tags on an unlisted plugin is asked for, once
|
|
105
116
|
// each, when a person is at a terminal on both ends and no machine is
|
|
106
117
|
// reading the result. Anything else, a pipe, an agent, --json, gets the
|
|
@@ -148,13 +159,15 @@ async function cmdSubmit(args) {
|
|
|
148
159
|
}
|
|
149
160
|
spinner.done()
|
|
150
161
|
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderSubmit(result))
|
|
151
|
-
|
|
162
|
+
// Three outcomes, two exit codes: `ready` and `listed` are both healthy
|
|
163
|
+
// states, and only a refusal is a 1.
|
|
164
|
+
process.exit(result.outcome === "refused" ? 1 : 0)
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
async function cmdWatch(args) {
|
|
155
168
|
const issueUrl = positionals(args)[0]
|
|
156
169
|
if (!issueUrl) fail("usage", "watch needs an issue: `omakit watch <issue-url>`", 2)
|
|
157
|
-
const spinner = args
|
|
170
|
+
const spinner = spinnerFor(args)
|
|
158
171
|
let result
|
|
159
172
|
try {
|
|
160
173
|
result = await validationWatch({ repoRoot: ROOT, issueUrl, onPhase: spinner.phase })
|
|
@@ -187,7 +200,7 @@ async function cmdUpgrade(args) {
|
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
async function cmdDoctor(args) {
|
|
190
|
-
const spinner = args
|
|
203
|
+
const spinner = spinnerFor(args)
|
|
191
204
|
const result = await doctor({ repoRoot: ROOT, offline: args.includes("--offline"), onPhase: spinner.phase })
|
|
192
205
|
spinner.done()
|
|
193
206
|
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderDoctor(result))
|
|
@@ -204,7 +217,7 @@ async function cmdVerify(args) {
|
|
|
204
217
|
if (error instanceof SubjectError) fail(error.code, error.message, error.code === "usage" ? 2 : 1)
|
|
205
218
|
throw error
|
|
206
219
|
}
|
|
207
|
-
const spinner =
|
|
220
|
+
const spinner = spinnerFor(args)
|
|
208
221
|
let section
|
|
209
222
|
try {
|
|
210
223
|
spinner.phase("running the official security baseline over a local snapshot")
|
|
@@ -237,23 +250,27 @@ async function cmdVerify(args) {
|
|
|
237
250
|
}
|
|
238
251
|
|
|
239
252
|
async function cmdParity(args) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
253
|
+
// The runner takes everything as arguments. Before 0.1.8 this handed over
|
|
254
|
+
// four PARITY_* variables and an OMAKIT_ROOT through the process
|
|
255
|
+
// environment, and OMAKIT_ROOT was the one OMAKIT_* name in the tree.
|
|
256
|
+
let ok = false
|
|
244
257
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
// Imported here, not at the top: the runner ships with the package, but
|
|
259
|
+
// no other command needs it, and a copy of bin/ and tools/ alone runs
|
|
260
|
+
// everything else.
|
|
261
|
+
const { runParity } = await import("../../tests/parity/run.mjs")
|
|
262
|
+
const count = option(args, "--count")
|
|
263
|
+
const offset = option(args, "--offset")
|
|
264
|
+
;({ ok } = await runParity({
|
|
265
|
+
repoRoot: ROOT,
|
|
266
|
+
count: count ? Number(count) : undefined,
|
|
267
|
+
offset: offset ? Number(offset) : undefined,
|
|
268
|
+
out: option(args, "--out") || null,
|
|
269
|
+
}))
|
|
254
270
|
} catch (error) {
|
|
255
271
|
failFrom(error)
|
|
256
272
|
}
|
|
273
|
+
process.exit(ok ? 0 : 1)
|
|
257
274
|
}
|
|
258
275
|
|
|
259
276
|
const [command, ...rest] = process.argv.slice(2)
|