omakit 0.4.0 → 0.4.2
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 +29 -91
- package/package.json +1 -1
- package/skills/omarchy-plugin-check/SKILL.md +2 -2
- package/skills/omarchy-plugin-submit/SKILL.md +2 -2
- package/skills/omarchy-plugin-validation-watch/SKILL.md +4 -4
- package/tools/marketplace/README.md +7 -2
- package/tools/marketplace/ask.mjs +4 -4
- package/tools/marketplace/banner.mjs +10 -1
- package/tools/marketplace/cli.mjs +60 -26
- package/tools/marketplace/github.mjs +13 -2
- package/tools/marketplace/measure-review-cost.mjs +39 -0
- package/tools/marketplace/measure-staleness.mjs +76 -0
- package/tools/marketplace/report.mjs +15 -4
- package/tools/marketplace/review-cost.mjs +107 -0
- package/tools/marketplace/style.mjs +24 -8
- package/tools/marketplace/submit.mjs +18 -2
- package/tools/marketplace/usage.mjs +56 -26
- package/tools/marketplace/watch.mjs +64 -17
- package/tools/weigh/audit.mjs +21 -3
- package/tools/weigh/confirm.mjs +3 -3
package/README.md
CHANGED
|
@@ -1,122 +1,60 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/media/banner.gif" alt="omakit" width="440">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/banner.gif" alt="omakit" width="440">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The marketplace validates one exact commit of your plugin. Push a fix or comment "fixed", and nothing re-runs ([M6](docs/MEASUREMENTS.md#m6-the-validated-commit-falls-behind-silently-and-that-is-the-centre-of-this-tool)). omakit runs the marketplace's own checks locally, watches your submission and posts nothing.
|
|
6
6
|
|
|
7
7
|
[](https://github.com/tcballard/omarchy-badges) [](https://www.npmjs.com/package/omakit) [](https://github.com/mtolhuys/omakit/actions/workflows/ci.yml) [](https://socket.dev/npm/package/omakit)
|
|
8
8
|
|
|
9
|
-
`omakit` is a zero-dependency Node CLI that checks an Omarchy Quattro plugin submission on your machine.
|
|
10
|
-
It is for a coding agent or a person submitting a plugin.
|
|
11
|
-
It never posts to the marketplace or writes into a plugin tree.
|
|
12
|
-
|
|
13
9
|
## Install
|
|
14
10
|
|
|
15
11
|
```bash
|
|
16
|
-
npm
|
|
17
|
-
omakit
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
See [docs/INSTALL.md](docs/INSTALL.md) for the clone route, PATH, requirements and upgrading.
|
|
21
|
-
|
|
22
|
-
## Commands
|
|
23
|
-
|
|
24
|
-
| Command | What it does |
|
|
25
|
-
| --- | --- |
|
|
26
|
-
| [`omakit setup`](docs/COMMANDS.md) | The environment, the pin, tab completion, and what to try first. |
|
|
27
|
-
| [`omakit submit <plugin-repo>`](docs/SUBMIT.md) | Every check, the issue title and body; asks for a category and tags at a terminal. |
|
|
28
|
-
| [`omakit watch [<issue-url>]`](docs/VALIDATION_WATCH.md) | Pick your marketplace issues, list them, or check all with `--all`. |
|
|
29
|
-
| [`omakit verify <plugin-repo>`](docs/COMMANDS.md) | The official security baseline over the local transport; `--json` for the document. |
|
|
30
|
-
| [`omakit parity`](docs/COMMANDS.md) | The baseline over GitHub versus the local transport, on real listings; writes the evidence. |
|
|
31
|
-
| [`omakit audit [<plugin>]`](docs/AUDIT.md) | Installed third-party commits against the exact commits the marketplace validated. |
|
|
32
|
-
| [`omakit weigh <plugin>`](docs/WEIGH.md) | What a plugin weighs on the shell, measured by restarting it without and with the plugin; asks first. |
|
|
33
|
-
| [`omakit doctor`](docs/COMMANDS.md) | What is installed, what is pinned, and what has moved. |
|
|
34
|
-
| [`omakit pin`](docs/COMMANDS.md) | What setup does for the pin, on its own. |
|
|
35
|
-
| [`omakit upgrade`](docs/COMMANDS.md) | Updates omakit through its own installer: npm, or a fast-forward. |
|
|
36
|
-
| [`omakit help --agent`](docs/COMMANDS.md) | The operating instructions, for the agent running this. |
|
|
37
|
-
|
|
38
|
-
Normal terminal use also checks for a newer npm release at most once daily
|
|
39
|
-
and shows the upgrade command. It installs nothing automatically; scripts
|
|
40
|
-
and JSON stay quiet. `DISABLE_UPDATE_NOTIFIER=1` disables the notice, and
|
|
41
|
-
`omakit doctor` checks explicitly. [Update behaviour](docs/INSTALL.md#updating).
|
|
42
|
-
|
|
43
|
-
### `submit`
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
omakit submit <plugin-repo> --category Widgets --tags bar,quickshell
|
|
12
|
+
npm i -g omakit && omakit setup
|
|
13
|
+
npx skills add mtolhuys/omakit
|
|
47
14
|
```
|
|
48
15
|
|
|
49
|
-
|
|
16
|
+
Requirements: [Node >=22](package.json); Omarchy Quattro.
|
|
50
17
|
|
|
51
|
-
|
|
18
|
+
Licence: [MIT](LICENSE).
|
|
52
19
|
|
|
53
|
-
|
|
20
|
+
## `omakit submit <plugin-repo>`
|
|
54
21
|
|
|
55
|
-
|
|
22
|
+

|
|
56
23
|
|
|
57
|
-
|
|
58
|
-
omakit watch <submission-issue-url>
|
|
59
|
-
omakit watch --all
|
|
60
|
-
omakit watch --list
|
|
61
|
-
omakit watch # choose one or several issues at a terminal
|
|
62
|
-
```
|
|
24
|
+
Checks the exact commit with the marketplace's own baseline and, when ready, prints the exact issue title and body for you to paste. The GIF shows a refusal with three blocking checks and their fixes.
|
|
63
25
|
|
|
64
|
-
|
|
26
|
+
## `omakit watch --all`
|
|
65
27
|
|
|
66
|
-
|
|
28
|
+

|
|
67
29
|
|
|
68
|
-
|
|
30
|
+
Checks your submission commits and names the action that re-runs stale validation: edit the issue body. The GIF shows five CURRENT issues in the counts and the first two issues with a discussion; CURRENT means matching commits, not approval.
|
|
69
31
|
|
|
70
|
-
|
|
32
|
+
## `omakit audit`
|
|
71
33
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
omakit weigh <plugin-id-or-dir>
|
|
76
|
-
```
|
|
34
|
+

|
|
77
35
|
|
|
78
|
-
|
|
36
|
+
Compares your installed plugin commits with the marketplace's validated commits. The GIF shows drift rows first: on the author's desktop, 9 of 18 audited plugins ran commits the marketplace never validated.
|
|
79
37
|
|
|
80
|
-
|
|
81
|
-
Weighs no CPU above the floor (0.13%) and runs 2 child processes using 8.2 MB and 0.1% CPU, on Omarchy 4.0.0.alpha, measured with omakit weigh on 2026-09-14
|
|
82
|
-
```
|
|
38
|
+
## `omakit weigh <plugin>`
|
|
83
39
|
|
|
84
|
-
|
|
40
|
+

|
|
85
41
|
|
|
86
|
-
|
|
42
|
+
Measures the shell with and without your plugin, reading Pss and CPU. The GIF shows three completed runs on the author's desktop, with baseline and plugin samples and a 0.33% CPU floor ([method](docs/WEIGH.md)).
|
|
87
43
|
|
|
88
44
|
## Evidence, not claims
|
|
89
45
|
|
|
90
|
-
|
|
|
46
|
+
| Measurement | Evidence |
|
|
91
47
|
| --- | --- |
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
| The GIFs above are real output | captures and renderer in [docs/media/](docs/media/) |
|
|
99
|
-
|
|
100
|
-
Committed evidence records a digest of each side rather than the results themselves, because findings about a specific third-party plugin are not this project's to publish.
|
|
48
|
+
| Baseline parity | 30/30 identical results, [recorded corpus](docs/evidence/parity/2026-09-12-local-vs-github-2.json), 2026-09-12 |
|
|
49
|
+
| Stale validated commit | 326/519 readable comparisons stale (62.8%); 64/583 unknown, [2026-09-15 data](docs/evidence/staleness/2026-09-15.json) |
|
|
50
|
+
| Registry churn | 4,201/4,293 registry-only commits in 30 days, 2026-09-13, [M7](docs/MEASUREMENTS.md#m7-the-registry-moves-by-the-hour-the-code-and-the-rules-move-by-the-week) |
|
|
51
|
+
| GIFs are recorded output | 5 GIFs with [captures and scenes](docs/media/README.md) |
|
|
52
|
+
| Posts nothing | 0 marketplace writes, [M10](docs/MEASUREMENTS.md#m10-readme-evidence-and-command-captures) |
|
|
53
|
+
| Zero dependencies | 0 runtime and 0 development dependencies, counted in [package.json](package.json) |
|
|
101
54
|
|
|
102
55
|
## Documentation
|
|
103
56
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
| [docs/COMMANDS.md](docs/COMMANDS.md) | command details, authentication and network behaviour |
|
|
109
|
-
| [docs/AUDIT.md](docs/AUDIT.md) | installed plugin drift against marketplace-validated commits, with JSON origins |
|
|
110
|
-
| [docs/SUBMIT.md](docs/SUBMIT.md) | every check and what it decides |
|
|
111
|
-
| [docs/WEIGH.md](docs/WEIGH.md) | what `weigh` measures, the noise floor, the `shell.json` mutation and its restore, and the JSON contract |
|
|
112
|
-
| [docs/VALIDATION_WATCH.md](docs/VALIDATION_WATCH.md) | the validation watch: what the marketplace validated, and what moves it |
|
|
113
|
-
| [docs/MEASUREMENTS.md](docs/MEASUREMENTS.md) | every number, its method and its limits |
|
|
114
|
-
| [docs/UPSTREAM_CONTRACT.md](docs/UPSTREAM_CONTRACT.md) | the seam, the pin, the boundaries |
|
|
115
|
-
| [docs/MARKETPLACE.md](docs/MARKETPLACE.md) | who this actually helps |
|
|
116
|
-
| [docs/PALETTE.md](docs/PALETTE.md) | every installed Omarchy theme measured, and which palette index each role gets |
|
|
117
|
-
| [docs/TUI.md](docs/TUI.md) | what the terminal shows, and why it looks that way |
|
|
118
|
-
| [AGENTS.md](AGENTS.md) | changing this repository |
|
|
119
|
-
|
|
120
|
-
MIT. Derived work built on public data from
|
|
121
|
-
`omacom/omarchy-plugin-marketplace`; not affiliated with or endorsed by that
|
|
122
|
-
project.
|
|
57
|
+
- Using: [install](docs/INSTALL.md), [commands](docs/COMMANDS.md), [audience](docs/MARKETPLACE.md).
|
|
58
|
+
- Checks and measurements: [submit](docs/SUBMIT.md), [watch](docs/VALIDATION_WATCH.md), [audit](docs/AUDIT.md), [evidence](docs/MEASUREMENTS.md).
|
|
59
|
+
- Method docs: [how](docs/HOW.md), [weigh](docs/WEIGH.md), [upstream contract](docs/UPSTREAM_CONTRACT.md), [palette](docs/PALETTE.md), [terminal](docs/TUI.md).
|
|
60
|
+
- Contributing: [repository rules](AGENTS.md), [releasing](docs/RELEASING.md), [media](docs/media/README.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omakit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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 against the marketplace, posts nothing, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Maarten Tolhuijs",
|
|
@@ -92,8 +92,8 @@ own words, and so should you.
|
|
|
92
92
|
|
|
93
93
|
The marketplace validates the pushed default-branch HEAD, not whatever is
|
|
94
94
|
checked out locally. Run the check on the commit that will be pushed, then
|
|
95
|
-
commit and push before the real submission.
|
|
96
|
-
|
|
95
|
+
commit and push before the real submission. On 2026-09-15, 326/519 readable author-fixes
|
|
96
|
+
comparisons were stale, with 64 of 583 issues unknown; that is the
|
|
97
97
|
round this loop is meant to prevent.
|
|
98
98
|
|
|
99
99
|
## When the plugin is ready
|
|
@@ -133,8 +133,8 @@ strip files from their tree on your own.
|
|
|
133
133
|
|
|
134
134
|
**`submission.validation-commit`.** The marketplace validates the commit it
|
|
135
135
|
resolves when the issue is opened or edited, which is the pushed default-branch
|
|
136
|
-
HEAD, not whatever is checked out locally. Push first, then submit.
|
|
137
|
-
|
|
136
|
+
HEAD, not whatever is checked out locally. Push first, then submit. On 2026-09-15, 326/519 readable author-fixes comparisons were stale
|
|
137
|
+
(62.8%), with 64 of 583 issues unknown.
|
|
138
138
|
|
|
139
139
|
## What the baseline result means
|
|
140
140
|
|
|
@@ -18,10 +18,10 @@ the issue body**.
|
|
|
18
18
|
submissions.
|
|
19
19
|
|
|
20
20
|
So pushing a fix does nothing, and commenting "fixed in `abc123`" does nothing.
|
|
21
|
-
Both feel like progress. Neither is.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
Both feel like progress. Neither is. The full author-fixes queue was measured on 2026-09-15: 326/519 readable
|
|
22
|
+
comparisons were stale (62.8%), with 64 of 583 issues unknown. The older
|
|
23
|
+
2026-09-12 sample found 68/93 readable issues stale (73.1%); that rate was
|
|
24
|
+
not a measurement of all 464 issues in the queue.
|
|
25
25
|
|
|
26
26
|
## Check it
|
|
27
27
|
|
|
@@ -18,6 +18,8 @@ local commit through the transport seam the marketplace tests itself
|
|
|
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
|
+
| `review-cost.mjs` | The advisory review-cost verdict, shared account discovery, and path classification between dated validated snapshots. M4 and M9 carry its evidence. |
|
|
22
|
+
| `measure-review-cost.mjs` | Reproduces M9 across the open update population at live marketplace HEAD, with compare sources and explicit skipped reasons in JSON. |
|
|
21
23
|
| `issue.mjs` | Renders the issue the way the form would, then has the marketplace's own parser judge it. |
|
|
22
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)". |
|
|
23
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. |
|
|
@@ -81,7 +83,8 @@ side rather than the findings themselves. The GitHub side uses whatever
|
|
|
81
83
|
credential `github.mjs` resolves (a `gh` login, and only that), read-only.
|
|
82
84
|
|
|
83
85
|
Two rules about the output, stated as rules because each is an exception to
|
|
84
|
-
a wider one.
|
|
86
|
+
a wider one. Pipes use 80 columns; terminals use their available width up to
|
|
87
|
+
120 columns. Text files compose at 80 columns without colour escapes. Text
|
|
85
88
|
that will be posted verbatim, the marketplace's own baseline report and the
|
|
86
89
|
issue body rendered from the pinned form, is never wrapped and may exceed 80,
|
|
87
90
|
because a wrapped body would not be the body. And stdout is the whole result;
|
|
@@ -97,7 +100,9 @@ sequences and nothing else.
|
|
|
97
100
|
built-in `fetch`, which does not read proxy environment variables by default.
|
|
98
101
|
Behind a proxy, run them with `NODE_USE_ENV_PROXY=1`. `submit` reads two things
|
|
99
102
|
online, the subject's default-branch HEAD and the marketplace's current
|
|
100
|
-
registry
|
|
103
|
+
registry. A manual-review baseline also reads the account's open issue
|
|
104
|
+
discovery and issue bodies for batching advice; `--offline` turns these
|
|
105
|
+
reads off. `verify` needs no network at all
|
|
101
106
|
beyond fetching a reviewer-mode subject, and `tests/parity/offline.mjs` proves
|
|
102
107
|
it.
|
|
103
108
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// the report ends with the command line that repeats the run without asking.
|
|
16
16
|
|
|
17
17
|
import { createInterface } from "node:readline"
|
|
18
|
-
import { colourEnabled, STEP, action, styler, wrap } from "./style.mjs"
|
|
18
|
+
import { colourEnabled, outputColumns, STEP, action, styler, wrap } from "./style.mjs"
|
|
19
19
|
import { resolveCategory, resolveTags } from "./form.mjs"
|
|
20
20
|
import { watchIssueTitle } from "./watch.mjs"
|
|
21
21
|
|
|
@@ -52,10 +52,10 @@ function reader(input) {
|
|
|
52
52
|
/** Ask one question until an answer resolves; `parse` returns { ok, value } or { ok: false, reason }. */
|
|
53
53
|
async function question(lines, output, c, { name, heading, options, defaultIndexes, parse, wrapOptions = false, eofRemedy }) {
|
|
54
54
|
const step = " ".repeat(STEP)
|
|
55
|
-
output.write(`${wrap(heading, {}, c).join("\n")}\n`)
|
|
55
|
+
output.write(`${wrap(heading, { width: outputColumns(output) }, c).join("\n")}\n`)
|
|
56
56
|
for (const [index, option] of options.entries()) {
|
|
57
57
|
output.write(wrapOptions
|
|
58
|
-
? `${wrap(`${String(index + 1).padStart(2)} ${option}`, { indent: STEP }, c).join("\n")}\n`
|
|
58
|
+
? `${wrap(`${String(index + 1).padStart(2)} ${option}`, { indent: STEP, width: outputColumns(output) }, c).join("\n")}\n`
|
|
59
59
|
: `${step}${c("typeable", String(index + 1).padStart(2))} ${option}\n`)
|
|
60
60
|
}
|
|
61
61
|
const fallback = defaultIndexes.length ? defaultIndexes.map((index) => index + 1).join(",") : null
|
|
@@ -70,7 +70,7 @@ async function question(lines, output, c, { name, heading, options, defaultIndex
|
|
|
70
70
|
const text = raw.trim() || (fallback ?? "")
|
|
71
71
|
const parsed = parse(text)
|
|
72
72
|
if (parsed.ok) return parsed.value
|
|
73
|
-
output.write(`${wrap(parsed.reason, {}, c).join("\n")}\n`)
|
|
73
|
+
output.write(`${wrap(parsed.reason, { width: outputColumns(output) }, c).join("\n")}\n`)
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
// letters come from the small font below, so renaming the tool is a change to
|
|
50
50
|
// one string and not a redrawing job.
|
|
51
51
|
|
|
52
|
-
import { code, colourEnabled, DENSITY, motionEnabled, MOTION, rule as floorRule } from "./style.mjs"
|
|
52
|
+
import { code, colourEnabled, DENSITY, motionEnabled, MOTION, outputColumns, rule as floorRule, wrap } from "./style.mjs"
|
|
53
53
|
import { effectAvailable, playEffect } from "./effect.mjs"
|
|
54
54
|
|
|
55
55
|
const ESC = "\u001b["
|
|
@@ -227,6 +227,15 @@ export async function banner(options = {}) {
|
|
|
227
227
|
// and a piped run should differ from a watched one only in decoration.
|
|
228
228
|
if (!enabled) return
|
|
229
229
|
|
|
230
|
+
// Never animate artwork across physical terminal rows: cursor-up would
|
|
231
|
+
// redraw the wrong row. A narrow terminal gets a compact wordmark instead.
|
|
232
|
+
if (stream.isTTY && Number.isFinite(stream.columns) && stream.columns > 0 && stream.columns < width) {
|
|
233
|
+
stream.write(`${c(code("name"), word.toUpperCase())}\n`)
|
|
234
|
+
if (options.tagline) stream.write(`${wrap(options.tagline, { width: outputColumns(stream) }, (name, text) => c(code(name), text)).join("\n")}\n`)
|
|
235
|
+
stream.write("\n")
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
230
239
|
// A five-row animation redrawn with cursor-up needs five rows that stay put.
|
|
231
240
|
// In a terminal with no room the screen scrolls under the animation, the
|
|
232
241
|
// cursor-up lands a line off, and a row from an earlier frame is left stranded
|
|
@@ -34,7 +34,7 @@ import { updateCheckEnabled, updateNotice } from "./update-check.mjs"
|
|
|
34
34
|
import { progress } from "./progress.mjs"
|
|
35
35
|
import { banner, bannerEnabled } from "./banner.mjs"
|
|
36
36
|
import { COMMANDS, renderSummary, renderUsage, TAGLINE } from "./usage.mjs"
|
|
37
|
-
import { action, AUDIT_VERDICTS, colourEnabled, GUTTER, labelled, mark, styler, verdict, wrap } from "./style.mjs"
|
|
37
|
+
import { action, AUDIT_VERDICTS, colourEnabled, GUTTER, labelled, mark, outputColumns, styler, verdict, withOutputStream, wrap } from "./style.mjs"
|
|
38
38
|
import { omakitCacheDir, withHomeAbbreviated } from "./paths.mjs"
|
|
39
39
|
import { DEFAULTS as WEIGH_DEFAULTS, measureWeigh, planWeigh } from "../weigh/audit.mjs"
|
|
40
40
|
import { confirmationQuestion, renderList, renderWeigh, renderPlan } from "../weigh/report.mjs"
|
|
@@ -68,6 +68,16 @@ const REMEDY = Object.freeze({
|
|
|
68
68
|
"interrupted": "shell.json was restored; run it again when the desktop is yours to restart.",
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
/*
|
|
72
|
+
* A command that has written its result leaves through `process.exitCode`,
|
|
73
|
+
* never `process.exit()`: stdout is an API, and on a pipe whose reader has
|
|
74
|
+
* not started reading yet the exit cuts the output. Measured on 0.4.1:
|
|
75
|
+
* `omakit submit <listed plugin> --json | (sleep 2; cat)` delivered 8,192 of
|
|
76
|
+
* 14,033 bytes, and a parser downstream saw invalid JSON. The failure
|
|
77
|
+
* states below still exit at once: they write one short block to stderr,
|
|
78
|
+
* and their callers use them the way a throw is used.
|
|
79
|
+
*/
|
|
80
|
+
|
|
71
81
|
/**
|
|
72
82
|
* Every failure, in one register, on stderr. `usage` errors carry the
|
|
73
83
|
* signature that was expected, so the remedy is the reference and not a
|
|
@@ -76,8 +86,10 @@ const REMEDY = Object.freeze({
|
|
|
76
86
|
*/
|
|
77
87
|
function fail(code, message, exit = 1, remedy = REMEDY[code], body = () => []) {
|
|
78
88
|
const c = styler(colourEnabled(process.stderr))
|
|
79
|
-
const lines =
|
|
80
|
-
|
|
89
|
+
const lines = withOutputStream(process.stderr, () => [
|
|
90
|
+
`${mark("fail", c)}${c("name", code)}`, ...wrap(message, { indent: GUTTER }, c), ...body(c),
|
|
91
|
+
...(remedy ? action(remedy, c) : []),
|
|
92
|
+
])
|
|
81
93
|
process.stderr.write(`${lines.join("\n")}\n`)
|
|
82
94
|
process.exit(exit)
|
|
83
95
|
}
|
|
@@ -88,9 +100,21 @@ function failFrom(error) {
|
|
|
88
100
|
throw error
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The value of a valued option, written either way the table accepts,
|
|
105
|
+
* `--name value` or `--name=value`, the last occurrence winning as it does
|
|
106
|
+
* in options.mjs. Measured on 0.4.1: the table accepted `--out=FILE` and the
|
|
107
|
+
* value was looked up as the token after `--out`, so `doctor --out=x` wrote
|
|
108
|
+
* nothing and exited 0, and `submit --category=Widgets` said the flag was
|
|
109
|
+
* missing.
|
|
110
|
+
*/
|
|
91
111
|
function option(args, name) {
|
|
92
|
-
|
|
93
|
-
|
|
112
|
+
let value
|
|
113
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
114
|
+
if (args[index] === name) value = args[index + 1]
|
|
115
|
+
else if (args[index].startsWith(`${name}=`)) value = args[index].slice(name.length + 1)
|
|
116
|
+
}
|
|
117
|
+
return value
|
|
94
118
|
}
|
|
95
119
|
|
|
96
120
|
/** The bare arguments, with every valued option's value (options.mjs, one table) left out. */
|
|
@@ -123,6 +147,12 @@ function emit(args, text) {
|
|
|
123
147
|
}
|
|
124
148
|
}
|
|
125
149
|
|
|
150
|
+
function reportText(args, render, result, options = {}) {
|
|
151
|
+
const out = option(args, "--out")
|
|
152
|
+
return withOutputStream(out ? { isTTY: false } : process.stdout,
|
|
153
|
+
() => render(result, { ...options, ...(out ? { colour: false } : {}) }))
|
|
154
|
+
}
|
|
155
|
+
|
|
126
156
|
async function cmdSubmit(args) {
|
|
127
157
|
const target = positionals(args)[0]
|
|
128
158
|
if (!target) fail("usage", "submit needs a target: `omakit submit <target> --category <c> --tags <a,b>`", 2)
|
|
@@ -161,7 +191,8 @@ async function cmdSubmit(args) {
|
|
|
161
191
|
const usage = error.usage
|
|
162
192
|
if (json) {
|
|
163
193
|
process.stdout.write(`${JSON.stringify({ usage }, null, 2)}\n`)
|
|
164
|
-
process.
|
|
194
|
+
process.exitCode = 2
|
|
195
|
+
return
|
|
165
196
|
}
|
|
166
197
|
const flags = usage.missing.join(" and ")
|
|
167
198
|
fail("usage", `submit needs ${flags}: ${usage.missing.length === 1 ? "it is" : "they are"} an editorial choice nobody else can make, from the pinned form's own lists.`, 2,
|
|
@@ -174,10 +205,10 @@ async function cmdSubmit(args) {
|
|
|
174
205
|
failFrom(error)
|
|
175
206
|
}
|
|
176
207
|
spinner.done()
|
|
177
|
-
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderSubmit
|
|
208
|
+
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : reportText(args, renderSubmit, result))
|
|
178
209
|
// Three outcomes, two exit codes: `ready` and `listed` are both healthy
|
|
179
210
|
// states, and only a refusal is a 1.
|
|
180
|
-
process.
|
|
211
|
+
process.exitCode = result.outcome === "refused" ? 1 : 0
|
|
181
212
|
}
|
|
182
213
|
|
|
183
214
|
async function cmdWatch(args) {
|
|
@@ -215,8 +246,8 @@ async function cmdWatch(args) {
|
|
|
215
246
|
}
|
|
216
247
|
spinner.done()
|
|
217
248
|
const render = result.mode === "list" ? renderWatchList : result.mode === "all" ? renderWatchAll : renderWatch
|
|
218
|
-
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : render
|
|
219
|
-
process.
|
|
249
|
+
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : reportText(args, render, result))
|
|
250
|
+
process.exitCode = result.verdict?.state === "unknown" || result.summary?.unknown > 0 ? 2 : 0
|
|
220
251
|
}
|
|
221
252
|
|
|
222
253
|
async function cmdFrontDoor() {
|
|
@@ -235,23 +266,24 @@ async function cmdSetup(args) {
|
|
|
235
266
|
if (args.includes("--completion")) {
|
|
236
267
|
const identity = requirePin(ROOT).identity
|
|
237
268
|
const result = await completionStep({ repoRoot: ROOT, pin: identity.commit, version: VERSION, askRc: false })
|
|
238
|
-
process.
|
|
269
|
+
process.exitCode = result.state === "ok" ? 0 : 1
|
|
270
|
+
return
|
|
239
271
|
}
|
|
240
272
|
const result = await setup({ repoRoot: ROOT, entryPoint: resolve(ROOT, "bin/omakit"), yes: args.includes("--yes") })
|
|
241
|
-
process.
|
|
273
|
+
process.exitCode = result.ok ? 0 : 1
|
|
242
274
|
}
|
|
243
275
|
|
|
244
276
|
async function cmdUpgrade(args) {
|
|
245
277
|
const result = await upgrade({ repoRoot: ROOT, dryRun: args.includes("--dry-run") })
|
|
246
|
-
process.
|
|
278
|
+
process.exitCode = result.ok ? 0 : 1
|
|
247
279
|
}
|
|
248
280
|
|
|
249
281
|
async function cmdDoctor(args) {
|
|
250
282
|
const spinner = spinnerFor(args)
|
|
251
283
|
const result = await doctor({ repoRoot: ROOT, offline: args.includes("--offline"), onPhase: spinner.phase })
|
|
252
284
|
spinner.done()
|
|
253
|
-
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderDoctor
|
|
254
|
-
process.
|
|
285
|
+
emit(args, args.includes("--json") ? `${JSON.stringify(result, null, 2)}\n` : reportText(args, renderDoctor, result))
|
|
286
|
+
process.exitCode = result.problems ? 1 : 0
|
|
255
287
|
}
|
|
256
288
|
|
|
257
289
|
async function cmdVerify(args) {
|
|
@@ -293,7 +325,7 @@ async function cmdVerify(args) {
|
|
|
293
325
|
const blockingRules = section.invoked && section.official && !section.official.error
|
|
294
326
|
? (await consequence(requirePin(ROOT).dir, section.official)).selectivelyBlockingRules
|
|
295
327
|
: []
|
|
296
|
-
emit(args, renderVerify
|
|
328
|
+
emit(args, reportText(args, renderVerify, document, { blockingRules }))
|
|
297
329
|
}
|
|
298
330
|
|
|
299
331
|
async function cmdParity(args) {
|
|
@@ -317,7 +349,7 @@ async function cmdParity(args) {
|
|
|
317
349
|
} catch (error) {
|
|
318
350
|
failFrom(error)
|
|
319
351
|
}
|
|
320
|
-
process.
|
|
352
|
+
process.exitCode = ok ? 0 : 1
|
|
321
353
|
}
|
|
322
354
|
|
|
323
355
|
function notAudited(message, remedy = null, exit = 1) {
|
|
@@ -351,7 +383,7 @@ async function cmdAudit(args) {
|
|
|
351
383
|
}
|
|
352
384
|
if (parsed.options.has("--json")) process.stdout.write(json)
|
|
353
385
|
else process.stdout.write(`${renderAudit(document)}\n`)
|
|
354
|
-
process.
|
|
386
|
+
process.exitCode = document.ok ? 0 : 1
|
|
355
387
|
}
|
|
356
388
|
|
|
357
389
|
/**
|
|
@@ -391,7 +423,7 @@ async function cmdWeigh(args) {
|
|
|
391
423
|
throw error
|
|
392
424
|
}
|
|
393
425
|
process.stdout.write(json ? `${JSON.stringify(list.rows, null, 2)}\n` : `${renderList(list)}\n`)
|
|
394
|
-
|
|
426
|
+
return
|
|
395
427
|
}
|
|
396
428
|
if (!target && !all) notWeighed("usage", "weigh needs a plugin: `omakit weigh <plugin-id-or-dir>`, or `omakit weigh --all` for every enabled third-party plugin.", "omakit weigh <plugin-id-or-dir>", 2)
|
|
397
429
|
if (target && all) notWeighed("usage", `--all weighs every enabled third-party plugin, so ${JSON.stringify(target)} is one argument more than it takes.`, "omakit weigh --all, or omakit weigh <plugin-id-or-dir>", 2)
|
|
@@ -420,7 +452,7 @@ async function cmdWeigh(args) {
|
|
|
420
452
|
// The confirmation. The plan is printed either way, so the record says
|
|
421
453
|
// what was agreed to; the question is asked only at a terminal on both
|
|
422
454
|
// ends, and --yes is the only other way past it.
|
|
423
|
-
narrate.write(`${renderPlan(plan, { colour: colourEnabled(narrate) }).join("\n")}\n`)
|
|
455
|
+
narrate.write(`${withOutputStream(narrate, () => renderPlan(plan, { colour: colourEnabled(narrate) })).join("\n")}\n`)
|
|
424
456
|
if (!parsed.options.has("--yes")) {
|
|
425
457
|
const interactive = !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY)
|
|
426
458
|
if (!interactive) notWeighed("not-confirmed", `this restarts the shell ${plan.restarts} times and edits shell.json for the duration; a pipe, an agent or --json cannot answer for the person whose shell it is.`, REMEDY["not-confirmed"], 2)
|
|
@@ -458,7 +490,6 @@ async function cmdWeigh(args) {
|
|
|
458
490
|
} else {
|
|
459
491
|
process.stdout.write(`\n${renderWeigh(document)}\n`)
|
|
460
492
|
}
|
|
461
|
-
process.exit(0)
|
|
462
493
|
}
|
|
463
494
|
|
|
464
495
|
const VERSION = JSON.parse(readFileSync(resolve(ROOT, "package.json"), "utf8")).version
|
|
@@ -487,12 +518,15 @@ const [command, ...rest] = process.argv.slice(2)
|
|
|
487
518
|
// stays empty on success, and never for setup, which is the fix.
|
|
488
519
|
if (updateCheckEnabled({ command, args: rest, stdinTTY: process.stdin.isTTY, stdoutTTY: process.stdout.isTTY, stderrTTY: process.stderr.isTTY })) {
|
|
489
520
|
const notice = await updateNotice({ repoRoot: ROOT, version: VERSION })
|
|
490
|
-
if (notice) process.stderr.write(`${wrap(notice, {}, styler(colourEnabled(process.stderr))).join("\n")}\n`)
|
|
521
|
+
if (notice) process.stderr.write(`${wrap(notice, { width: outputColumns(process.stderr) }, styler(colourEnabled(process.stderr))).join("\n")}\n`)
|
|
491
522
|
}
|
|
492
523
|
|
|
493
524
|
if (command !== "setup" && process.stderr.isTTY) {
|
|
494
525
|
const notice = staleCompletionNotice({ version: VERSION })
|
|
495
|
-
if (notice)
|
|
526
|
+
if (notice) {
|
|
527
|
+
const c = styler(colourEnabled(process.stderr))
|
|
528
|
+
process.stderr.write(`${wrap(notice, { width: outputColumns(process.stderr) }).map((line) => c("label", line)).join("\n")}\n`)
|
|
529
|
+
}
|
|
496
530
|
}
|
|
497
531
|
|
|
498
532
|
if (command === "setup") {
|
|
@@ -556,12 +590,12 @@ if (command === "setup") {
|
|
|
556
590
|
// The short list on a typo, not 53 lines of reference, in the same register
|
|
557
591
|
// as every other failure: what happened, what it means, what to run.
|
|
558
592
|
const c = styler(colourEnabled(process.stderr))
|
|
559
|
-
process.stderr.write([
|
|
593
|
+
process.stderr.write(withOutputStream(process.stderr, () => [
|
|
560
594
|
`${mark("fail", c)}${c("name", "unknown command")}`,
|
|
561
595
|
...wrap(`\`${command}\` is not something omakit does. The commands it has are listed below.`, { indent: GUTTER }, c),
|
|
562
596
|
...action("omakit help", c),
|
|
563
597
|
"",
|
|
564
|
-
renderSummary({ colour: colourEnabled(process.stderr), heading: false }),
|
|
565
|
-
].join("\n"))
|
|
598
|
+
renderSummary({ stream: process.stderr, colour: colourEnabled(process.stderr), heading: false }),
|
|
599
|
+
].join("\n")))
|
|
566
600
|
process.exit(2)
|
|
567
601
|
}
|
|
@@ -180,10 +180,10 @@ export async function authenticatedUser() {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/** Repository issues by their creator. PRs are excluded; pagination never silently truncates. */
|
|
183
|
-
export async function repositoryIssues(owner, repository, creator, { readJson = getJson, maxPages = 100 } = {}) {
|
|
183
|
+
export async function repositoryIssues(owner, repository, creator, { readJson = getJson, maxPages = 100, labels } = {}) {
|
|
184
184
|
const all = []
|
|
185
185
|
for (let page = 1; page <= maxPages; page += 1) {
|
|
186
|
-
const query = new URLSearchParams({ creator, state: "open", sort: "updated", direction: "desc", per_page: "100", page: String(page) })
|
|
186
|
+
const query = new URLSearchParams({ ...(creator ? { creator } : {}), ...(labels ? { labels } : {}), state: "open", sort: "updated", direction: "desc", per_page: "100", page: String(page) })
|
|
187
187
|
const batch = await readJson(`https://api.github.com/repos/${owner}/${repository}/issues?${query}`)
|
|
188
188
|
if (!Array.isArray(batch)) throw new GitHubError("github-unavailable", "GitHub did not return an issue list")
|
|
189
189
|
all.push(...batch.filter((item) => !item.pull_request))
|
|
@@ -205,6 +205,17 @@ export async function issueComments(owner, repository, number, maxPages = 10, re
|
|
|
205
205
|
throw new GitHubError("comments-incomplete", `Issue #${number} exceeded ${maxPages} comment pages; its latest baseline cannot be determined`)
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/** Compare exact validated snapshots. The API caps its file list at 300: never classify a truncated diff. */
|
|
209
|
+
export async function compareCommits(repositoryUrl, previous, validated, { readJson = getJson } = {}) {
|
|
210
|
+
const match = String(repositoryUrl).match(/^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/)
|
|
211
|
+
if (!match || ![previous, validated].every((commit) => /^[a-f0-9]{40}$/i.test(commit))) throw new GitHubError("usage", "compare needs a repository and two full commit identifiers")
|
|
212
|
+
const url = `https://api.github.com/repos/${match[1]}/${match[2]}/compare/${previous}...${validated}?per_page=1`
|
|
213
|
+
const result = await readJson(url)
|
|
214
|
+
if (!["ahead", "identical"].includes(result?.status)) throw new GitHubError("compare-not-forward", "validated snapshots are not a forward comparison")
|
|
215
|
+
if (!Array.isArray(result.files) || result.files.length >= 300) throw new GitHubError("compare-incomplete", "compare file list unavailable or at the 300-file API limit")
|
|
216
|
+
return { url, files: result.files }
|
|
217
|
+
}
|
|
218
|
+
|
|
208
219
|
/**
|
|
209
220
|
* The current default-branch HEAD of a repository.
|
|
210
221
|
*
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Reproduce M9 with GET-only issue discovery and compare reads. No sampling.
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import { MARKETPLACE_PIN } from "./pin.mjs"
|
|
5
|
+
import { repositoryIssues } from "./github.mjs"
|
|
6
|
+
import { liveRegistry } from "./registry.mjs"
|
|
7
|
+
import { reviewPolicy } from "./review-cost.mjs"
|
|
8
|
+
import { validationWatchAll } from "./watch.mjs"
|
|
9
|
+
|
|
10
|
+
export async function measureReviewCost(repoRoot) {
|
|
11
|
+
const policy = await reviewPolicy(repoRoot)
|
|
12
|
+
const registry = await liveRegistry({ repoRoot })
|
|
13
|
+
if (registry.source !== "head") throw new Error(`cannot measure at HEAD: ${registry.reason}`)
|
|
14
|
+
const [owner, repository] = new URL(MARKETPLACE_PIN.repository).pathname.slice(1).split("/")
|
|
15
|
+
const openedAt = new Date().toISOString()
|
|
16
|
+
const subjects = await repositoryIssues(owner, repository, undefined, { labels: policy.updateLabel })
|
|
17
|
+
const issues = subjects.filter((subject) => subject.state === "open" && !subject.pull_request).map((subject) => ({
|
|
18
|
+
number: subject.number, url: `${MARKETPLACE_PIN.repository}/issues/${subject.number}`, title: subject.title,
|
|
19
|
+
state: subject.state, labels: subject.labels.map((label) => typeof label === "string" ? label : label.name),
|
|
20
|
+
}))
|
|
21
|
+
// Only the manual-review subset needs comment reads and comparisons. No plugin HEAD reads.
|
|
22
|
+
const manual = issues.filter((subject) => subject.labels.includes(policy.reviewLabel))
|
|
23
|
+
const batch = await validationWatchAll({ repoRoot, discovery: { account: null, marketplace: MARKETPLACE_PIN.repository, issues: manual },
|
|
24
|
+
readRegistry: async () => registry, github: { defaultBranchHead: async () => null } })
|
|
25
|
+
const counts = batch.reviewCostSummary
|
|
26
|
+
return {
|
|
27
|
+
measurement: "M9", openedAt, completedAt: new Date().toISOString(), marketplace: MARKETPLACE_PIN.repository,
|
|
28
|
+
marketplaceHead: registry.commit, command: "node tools/marketplace/measure-review-cost.mjs", sample: false,
|
|
29
|
+
pluginUpdates: issues.length, manualQueue: manual.length, manualQueueShare: issues.length ? manual.length / issues.length : null,
|
|
30
|
+
docsOnly: counts.docsOnly, compared: counts.compared, skipped: counts.skipped.length,
|
|
31
|
+
docsOnlyShareOfCompared: counts.compared ? counts.docsOnly / counts.compared : null,
|
|
32
|
+
docsOnlyShareOfManualQueue: counts.skipped.length || !manual.length ? null : counts.docsOnly / manual.length,
|
|
33
|
+
rows: batch.issues.map((row) => ({ issue: row.issue.number, ...row.documentationDiff })),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
38
|
+
process.stdout.write(`${JSON.stringify(await measureReviewCost(resolve(process.cwd())), null, 2)}\n`)
|
|
39
|
+
}
|