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 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. A plugin that is
138
- already listed is refused there and asked for nothing (measured on 0.1.5: it
139
- exited 2 asking for `--category` and `--tags`, then would have said there was
140
- nothing to submit). An unlisted plugin needs both, and they are an editorial
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. Either way the report ends
145
- with the command line that repeats the run without asking, and `--json`
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 the marketplace's current branch, names which of the paths omakit
219
- reads actually changed (about 140 commits a day touch only `registry.json`, so
220
- "behind" alone would be true of every run), and then leaves it alone. That the
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 # 147 tests, node --test, no dependencies
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.6",
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 refused at `identity.available` and asked for nothing. Every report
50
- ends with the command line that repeats the run without asking, and `--json`
51
- carries it as `reproduce`.
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
- Exit code 0 means every blocking check passed and the output contains the issue
76
- title and body. Exit code 1 means it refused, and no body was produced. Exit
77
- code 2 is a usage error: nothing was checked.
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
 
@@ -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
- const repoRoot = resolve(process.env.OMAKIT_ROOT || process.cwd())
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
- const corpus = parityCorpus(pinDir, count, offset)
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
- const dir = shallowClone(target.repo, target.commit)
91
- const local = await runBaseline({
92
- repoRoot,
93
- repoUrl: target.repo,
94
- commitSha: target.commit,
95
- transport: "local",
96
- repoDir: dir,
97
- })
98
- row.localOutcome = local.result.outcome
99
- row.requests = local.adapter.requests
100
- row.localDigest = digest(local.result)
101
- // Rule and capability ids are counted in the summary, unattributed; they are
102
- // deliberately not recorded against this repository.
103
- for (const finding of local.result.findings || []) {
104
- const id = finding.ruleId || finding.id
105
- if (id) ruleTotals[id] = (ruleTotals[id] || 0) + 1
106
- }
107
- for (const capability of local.result.capabilities || []) {
108
- if (capability.id) capabilityTotals[capability.id] = (capabilityTotals[capability.id] || 0) + 1
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 github = await runBaseline({
112
- repoRoot,
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: "github",
116
- token: token() ?? undefined,
117
+ transport: "local",
118
+ repoDir: dir,
117
119
  })
118
- row.githubOutcome = github.result.outcome
119
- row.githubDigest = digest(github.result)
120
- row.identical = row.localDigest === row.githubDigest
121
- if (!row.identical) {
122
- mismatches += 1
123
- // A mismatch is the one case worth describing, and it is described as a
124
- // difference between two transports of the same code, not as a finding
125
- // about the plugin: which keys differ, never their contents.
126
- const localPayload = JSON.parse(comparable(local.result))
127
- const githubPayload = JSON.parse(comparable(github.result))
128
- row.differingKeys = Object.keys(localPayload).filter(
129
- (key) => JSON.stringify(localPayload[key]) !== JSON.stringify(githubPayload[key]),
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.githubError = (error.code || "error") + ": " + error.message
159
+ row.localError = (error.code || "error") + ": " + error.message
134
160
  failures += 1
135
161
  }
136
- } catch (error) {
137
- row.localError = (error.code || "error") + ": " + error.message
138
- failures += 1
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
- function generatorIdentity() {
153
- try {
154
- return { omakitCommit: execFileSync("git", ["-C", repoRoot, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() }
155
- } catch {
156
- const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"))
157
- return { omakitPackage: `${pkg.name}@${pkg.version}` }
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
- const summary = {
162
- generator: generatorIdentity(),
163
- pinnedMarketplaceCommit: pinCommit(pinDir),
164
- corpusSize: corpus.length,
165
- requestedCount: count,
166
- offset,
167
- strata: strataSizes(count),
168
- outcomes: rows.reduce((acc, row) => {
169
- const key = row.localOutcome || "error"
170
- acc[key] = (acc[key] || 0) + 1
171
- return acc
172
- }, {}),
173
- identical: rows.filter((row) => row.identical === true).length,
174
- mismatches,
175
- failures,
176
- // Unattributed aggregates. No repository name is attached to any rule.
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 needs 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. |
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 = json ? { phase: () => {}, done: () => {} } : progress()
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
- process.exit(result.ready ? 0 : 1)
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.includes("--json") ? { phase: () => {}, done: () => {} } : progress()
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.includes("--json") ? { phase: () => {}, done: () => {} } : progress()
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 = progress()
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
- const count = option(args, "--count")
241
- const offset = option(args, "--offset")
242
- if (count) process.env.PARITY_COUNT = count
243
- if (offset) process.env.PARITY_OFFSET = offset
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
- const out = option(args, "--out")
246
- process.env.PARITY_OUT = parityOutput({ repoRoot: ROOT, out })
247
- if (out) process.env.PARITY_OUT_EXPLICIT = "1"
248
- } catch (error) {
249
- failFrom(error)
250
- }
251
- process.env.OMAKIT_ROOT = ROOT
252
- try {
253
- await import("../../tests/parity/run.mjs")
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)