omakit 0.1.2 → 0.1.4
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 +39 -19
- package/package.json +1 -1
- package/skills/omarchy-plugin-submit/SKILL.md +17 -3
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -0
- package/tools/marketplace/README.md +9 -6
- package/tools/marketplace/cli.mjs +29 -5
- package/tools/marketplace/doctor.mjs +98 -19
- package/tools/marketplace/path-hint.mjs +53 -0
- package/tools/marketplace/registry.mjs +211 -21
- package/tools/marketplace/report.mjs +13 -6
- package/tools/marketplace/setup.mjs +9 -10
- package/tools/marketplace/submit.mjs +103 -14
- package/tools/marketplace/upgrade.mjs +123 -13
- package/tools/marketplace/usage.mjs +7 -6
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
<img src="docs/media/banner.gif" alt="omakit" width="440">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
+
[](https://github.com/tcballard/omarchy-badges)
|
|
6
|
+
|
|
5
7
|
**Everything knowable about an Omarchy Quattro plugin submission, checked
|
|
6
8
|
before you post it:** the tree, the manifest, the form, the commit, and the
|
|
7
9
|
marketplace's own security baseline with its outcome reported as it is. A
|
|
@@ -53,6 +55,13 @@ npm install --global omakit
|
|
|
53
55
|
omakit setup
|
|
54
56
|
```
|
|
55
57
|
|
|
58
|
+
If `omakit` is not found afterwards, npm's global `bin` is not on your PATH
|
|
59
|
+
(measured: a prefix of `~/.local/share/lerd/node-global` whose `bin` no shell
|
|
60
|
+
searched). Run `"$(npm prefix --global)/bin/omakit" setup` once: it prints the
|
|
61
|
+
one line that puts that directory on PATH for the shell in `$SHELL`, and the
|
|
62
|
+
rc file to keep it in; `omakit doctor` reports the same as `omakit.path`.
|
|
63
|
+
Nothing writes to your rc file.
|
|
64
|
+
|
|
56
65
|
Or read what you run:
|
|
57
66
|
|
|
58
67
|
```bash
|
|
@@ -68,9 +77,10 @@ omakit setup
|
|
|
68
77
|
| network, once | `omakit pin`. After that, `submit` and `verify` on a local repository need none at all |
|
|
69
78
|
| 15 MB on disk | the pinned checkout, in `$XDG_CACHE_HOME/omakit/marketplace`, or `~/.cache/omakit/marketplace` |
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
the package,
|
|
73
|
-
|
|
80
|
+
`omakit upgrade` updates either install through the installer that made it:
|
|
81
|
+
`npm` for the package, at the exact version the registry names, and a
|
|
82
|
+
fast-forward for a clone. `omakit doctor` says when a newer version is
|
|
83
|
+
published. Nothing in omakit fetches and runs its own replacement, and
|
|
74
84
|
`omarchy-mise-install npm:omakit` would, so it is not the way in.
|
|
75
85
|
|
|
76
86
|
## Watch
|
|
@@ -101,7 +111,7 @@ omakit verify <plugin-repo> # the official security baseline over the local tra
|
|
|
101
111
|
omakit parity # the baseline over GitHub versus the local transport, on real listings; writes the evidence
|
|
102
112
|
omakit doctor # what is installed, what is pinned, and what has moved
|
|
103
113
|
omakit pin # what setup does for the pin, on its own
|
|
104
|
-
omakit upgrade #
|
|
114
|
+
omakit upgrade # updates omakit through its own installer: npm, or a fast-forward
|
|
105
115
|
omakit help --agent # the operating instructions, for the agent running this
|
|
106
116
|
```
|
|
107
117
|
|
|
@@ -120,11 +130,12 @@ There is nothing to authenticate. If you have `gh auth login` done, omakit
|
|
|
120
130
|
reads that credential for GET requests and stores nothing; a token in
|
|
121
131
|
`GH_TOKEN` or `GITHUB_TOKEN` reaches it the same way, because `gh` honours
|
|
122
132
|
those itself. Without either, `watch` and `parity` share GitHub's
|
|
123
|
-
60-requests-an-hour unauthenticated allowance
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
60-requests-an-hour unauthenticated allowance; `submit` reads two things
|
|
134
|
+
online, the subject's default-branch HEAD and the marketplace's current
|
|
135
|
+
registry, and `--offline` turns both off; `verify` on a local repository does
|
|
136
|
+
not touch the network at all (a `<url>@<sha>` target is fetched once, over
|
|
137
|
+
git, into the cache). omakit reads no environment variable of its own, and
|
|
138
|
+
`omakit doctor` names the credential source it found, or that it found none.
|
|
128
139
|
|
|
129
140
|
Every colour omakit prints is an ANSI palette index, so your Omarchy theme
|
|
130
141
|
decides what it looks like, and nothing is said by colour alone. What the
|
|
@@ -140,7 +151,11 @@ tags and the exact text of the five checklist items are all read from
|
|
|
140
151
|
`.github/ISSUE_TEMPLATE/submit-plugin.yml` in a marketplace checkout pinned to an
|
|
141
152
|
exact commit. The rendered body is then handed to the marketplace's own
|
|
142
153
|
`parseCurrentSubmission` from that same commit. If it accepts the body here, it
|
|
143
|
-
accepts it there.
|
|
154
|
+
accepts it there. The one exception is data, not rules: the registry and the
|
|
155
|
+
catalog that say which ids and repositories are already listed are read from
|
|
156
|
+
the marketplace's current HEAD when the network is there, because the pin's
|
|
157
|
+
copy is stale within hours (4,201 of 4,293 commits in 30 days touched only
|
|
158
|
+
`registry.json`), and from the pin with `--offline`.
|
|
144
159
|
|
|
145
160
|
The security baseline is the marketplace's own code, imported unmodified and run
|
|
146
161
|
over a local snapshot with no network. Omakit adds no rule, renames no outcome,
|
|
@@ -160,17 +175,20 @@ move on its own. That distinction is now enforced rather than argued.
|
|
|
160
175
|
**The tool:**
|
|
161
176
|
|
|
162
177
|
```bash
|
|
163
|
-
omakit upgrade #
|
|
178
|
+
omakit upgrade # the npm package, or a clone: through its own installer
|
|
164
179
|
omakit upgrade --dry-run
|
|
165
180
|
```
|
|
166
181
|
|
|
167
|
-
It
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
and
|
|
182
|
+
It is not a self-updater of the kind this repository warns other people
|
|
183
|
+
about: it never fetches and runs its own replacement. On an npm install it asks
|
|
184
|
+
the registry for the newest version and, if that is newer, runs the `npm` on
|
|
185
|
+
PATH with frozen arguments (`npm install --global --ignore-scripts omakit@<that
|
|
186
|
+
version>`, never `@latest`, never with sudo), and it refuses when the npm on
|
|
187
|
+
PATH is not the one that installed it. On a clone it fast-forwards from the
|
|
188
|
+
remote you cloned it from, and refuses a dirty tree, a detached HEAD, a remote
|
|
189
|
+
that is not this repository, and anything that is not a fast-forward. In every
|
|
190
|
+
refusal it names what to run yourself. `git -C ~/.local/share/omakit pull`
|
|
191
|
+
still works on a clone and does the same thing.
|
|
174
192
|
|
|
175
193
|
**The pin** does not move by itself, ever, and `omakit upgrade` does not move it
|
|
176
194
|
either: a test asserts that its source does not so much as mention the pin or
|
|
@@ -178,7 +196,9 @@ the cache. Bumping it changes where the submission contract and the baseline
|
|
|
178
196
|
policy are read from, and the procedure in
|
|
179
197
|
[docs/UPSTREAM_CONTRACT.md](docs/UPSTREAM_CONTRACT.md) ends in re-proving
|
|
180
198
|
transport parity and committing the evidence. `omakit doctor` tells you when the
|
|
181
|
-
pin is behind the marketplace's current branch
|
|
199
|
+
pin is behind the marketplace's current branch, names which of the paths omakit
|
|
200
|
+
reads actually changed (about 140 commits a day touch only `registry.json`, so
|
|
201
|
+
"behind" alone would be true of every run), and then leaves it alone. That the
|
|
182
202
|
pin can go stale unnoticed is the same defect class `omakit watch` reports, so it
|
|
183
203
|
would be poor form to hide it here.
|
|
184
204
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omakit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -24,16 +24,30 @@ comes from `origin`. You supply the category and the tags, because nobody else
|
|
|
24
24
|
can: they are an editorial choice about where the plugin belongs.
|
|
25
25
|
|
|
26
26
|
If you do not know which category and tags are allowed, run the command without
|
|
27
|
-
them
|
|
27
|
+
them: that is a usage error (exit 2) whose message lists the controlled values,
|
|
28
|
+
read from the pinned form, before any check runs. With `--json` the same lists
|
|
29
|
+
come back under `usage`.
|
|
28
30
|
|
|
29
31
|
Useful flags: `--notes` for the Maintainer notes field, `--suggest-tag` for the
|
|
30
32
|
optional suggestion, `--name` when the manifest has no name, `--json` for a
|
|
31
|
-
machine-readable result, `--offline` to skip the one check that needs the network
|
|
33
|
+
machine-readable result, `--offline` to skip the one check that needs the network
|
|
34
|
+
and to read the listed ids from the pin instead of the marketplace's current HEAD.
|
|
35
|
+
|
|
36
|
+
Never pass `--offline` to get around a listed id. `identity.available` reads the
|
|
37
|
+
registry from HEAD because the pin's copy is stale within hours; a run that
|
|
38
|
+
passes only against the pin describes a marketplace that no longer exists, and
|
|
39
|
+
the marketplace will refuse the id anyway. If an id or repository is listed at
|
|
40
|
+
HEAD, tell the owner and choose another id.
|
|
32
41
|
|
|
33
42
|
## Reading the result
|
|
34
43
|
|
|
35
44
|
Exit code 0 means every blocking check passed and the output contains the issue
|
|
36
|
-
title and body. Exit code 1 means it refused, and no body was produced.
|
|
45
|
+
title and body. Exit code 1 means it refused, and no body was produced. Exit
|
|
46
|
+
code 2 is a usage error: nothing was checked.
|
|
47
|
+
|
|
48
|
+
A check drawn as `▒ ?` did not run because one it depends on failed; its detail
|
|
49
|
+
names that check. It is not a failure of its own, and the closing refusal lists
|
|
50
|
+
root causes only. Fix those.
|
|
37
51
|
|
|
38
52
|
Each check names a source. `[marketplace-pin]` is the marketplace's own rule, read
|
|
39
53
|
from a pinned checkout. `[omakit]` is this tool's own check, derived from public
|
|
@@ -35,6 +35,11 @@ it from the operator's `gh` login (which itself honours `GH_TOKEN` and
|
|
|
35
35
|
`GITHUB_TOKEN`); without one it falls back to the public commit feed. Never ask the operator for a token: run
|
|
36
36
|
`omakit doctor` and read the `github.auth` line, which names the source.
|
|
37
37
|
|
|
38
|
+
If the diagnosis ends in a resubmission under a new id, run `omakit submit`
|
|
39
|
+
online: never pass `--offline` to get around a listed id, because that reads
|
|
40
|
+
the listed ids from the pin, which is stale within hours, and the marketplace
|
|
41
|
+
refuses against its current registry.
|
|
42
|
+
|
|
38
43
|
## Acting on each verdict
|
|
39
44
|
|
|
40
45
|
**`stale`.** The validated commit is behind the repository. Tell the owner plainly: the marketplace has not
|
|
@@ -14,7 +14,7 @@ local commit through the transport seam the marketplace tests itself
|
|
|
14
14
|
| `preflight.mjs` | Translates that result into what it will cause on submission, using the pinned policy, and renders the marketplace's own report text with its attestation marker stripped and asserted absent. |
|
|
15
15
|
| `yaml.mjs` | A deliberately small YAML reader for the pinned issue form. Accepts that subset and throws on anything else. |
|
|
16
16
|
| `form.mjs` | The submission contract, read from the form and cross-checked against the marketplace's own constants. |
|
|
17
|
-
| `registry.mjs` | The plugin-id and repository universe
|
|
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. |
|
|
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. |
|
|
@@ -24,6 +24,7 @@ local commit through the transport seam the marketplace tests itself
|
|
|
24
24
|
| `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. |
|
|
25
25
|
| `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. |
|
|
26
26
|
| `report.mjs` | Text rendering of submit, watch and doctor for the agent that runs this tool, and the person reading over its shoulder. |
|
|
27
|
+
| `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. |
|
|
27
28
|
| `usage.mjs` | The help text, as data. |
|
|
28
29
|
| `completion.mjs` | A completion script for bash, zsh or fish, derived from the help data and the pin's form: the subcommands and flags are read out of `COMMANDS`, the categories and tags out of the pinned submission form, and the script says which pin it came from. `setup` installs it for the shell in `$SHELL`, the one file this tool writes outside its own checkout. |
|
|
29
30
|
| `banner.mjs` | The wordmark, on a bare `omakit` and in `setup` only. |
|
|
@@ -57,8 +58,10 @@ the registry recorded so the corpus always contains repositories that are not
|
|
|
57
58
|
side rather than the findings themselves. The GitHub side uses whatever
|
|
58
59
|
credential `github.mjs` resolves (a `gh` login, and only that), read-only.
|
|
59
60
|
|
|
60
|
-
`parity` and `
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
`parity`, `watch`, `doctor` and `submit` reach the network, with Node's
|
|
62
|
+
built-in `fetch`, which does not read proxy environment variables by default.
|
|
63
|
+
Behind a proxy, run them with `NODE_USE_ENV_PROXY=1`. `submit` reads two things
|
|
64
|
+
online, the subject's default-branch HEAD and the marketplace's current
|
|
65
|
+
registry, and `--offline` turns both off; `verify` needs no network at all
|
|
66
|
+
beyond fetching a reviewer-mode subject, and `tests/parity/offline.mjs` proves
|
|
67
|
+
it.
|
|
@@ -17,7 +17,8 @@ import { fileURLToPath } from "node:url"
|
|
|
17
17
|
import { ensurePin, MARKETPLACE_PIN } from "./pin.mjs"
|
|
18
18
|
import { marketplaceBaselineSection } from "./verify.mjs"
|
|
19
19
|
import { resolveSubject, SubjectError } from "../subject/resolve.mjs"
|
|
20
|
-
import { submitPreflight } from "./submit.mjs"
|
|
20
|
+
import { missingSubmitFlags, submitPreflight } from "./submit.mjs"
|
|
21
|
+
import { submissionContract } from "./form.mjs"
|
|
21
22
|
import { validationWatch } from "./watch.mjs"
|
|
22
23
|
import { renderSubmit, renderWatch, renderDoctor } from "./report.mjs"
|
|
23
24
|
import { doctor } from "./doctor.mjs"
|
|
@@ -26,7 +27,7 @@ import { upgrade } from "./upgrade.mjs"
|
|
|
26
27
|
import { progress } from "./progress.mjs"
|
|
27
28
|
import { banner, bannerEnabled } from "./banner.mjs"
|
|
28
29
|
import { COMMANDS, renderSummary, renderUsage, TAGLINE } from "./usage.mjs"
|
|
29
|
-
import { action, colourEnabled, GUTTER, mark, styler, wrap } from "./style.mjs"
|
|
30
|
+
import { action, colourEnabled, GUTTER, labelled, mark, styler, wrap } from "./style.mjs"
|
|
30
31
|
import { omakitCacheDir } from "./paths.mjs"
|
|
31
32
|
import { parityOutput } from "./parity-output.mjs"
|
|
32
33
|
|
|
@@ -55,11 +56,12 @@ const REMEDY = Object.freeze({
|
|
|
55
56
|
/**
|
|
56
57
|
* Every failure, in one register, on stderr. `usage` errors carry the
|
|
57
58
|
* signature that was expected, so the remedy is the reference and not a
|
|
58
|
-
* restatement of the message.
|
|
59
|
+
* restatement of the message. `body` is extra labelled lines between the
|
|
60
|
+
* message and the arrow, for a usage error that has values to list.
|
|
59
61
|
*/
|
|
60
|
-
function fail(code, message, exit = 1, remedy = REMEDY[code]) {
|
|
62
|
+
function fail(code, message, exit = 1, remedy = REMEDY[code], body = () => []) {
|
|
61
63
|
const c = styler(colourEnabled(process.stderr))
|
|
62
|
-
const lines = [`${mark("fail", c)}${c("name", code)}`, ...wrap(message, { indent: GUTTER }, c)]
|
|
64
|
+
const lines = [`${mark("fail", c)}${c("name", code)}`, ...wrap(message, { indent: GUTTER }, c), ...body(c)]
|
|
63
65
|
if (remedy) lines.push(...action(remedy, c))
|
|
64
66
|
process.stderr.write(`${lines.join("\n")}\n`)
|
|
65
67
|
process.exit(exit)
|
|
@@ -96,6 +98,28 @@ function emit(args, text) {
|
|
|
96
98
|
async function cmdSubmit(args) {
|
|
97
99
|
const target = positionals(args)[0]
|
|
98
100
|
if (!target) fail("usage", "submit needs a target: `omakit submit <target> --category <c> --tags <a,b>`", 2)
|
|
101
|
+
// A missing category or tag list is a usage error, decided before any check
|
|
102
|
+
// runs and before the spinner starts: nothing can be rendered without them,
|
|
103
|
+
// and the values the form accepts are the answer, read from the pin.
|
|
104
|
+
let usage = null
|
|
105
|
+
try {
|
|
106
|
+
usage = missingSubmitFlags(await submissionContract({ repoRoot: ROOT }), { category: option(args, "--category"), tags: option(args, "--tags") })
|
|
107
|
+
} catch (error) {
|
|
108
|
+
failFrom(error)
|
|
109
|
+
}
|
|
110
|
+
if (usage) {
|
|
111
|
+
if (args.includes("--json")) {
|
|
112
|
+
process.stdout.write(`${JSON.stringify({ usage }, null, 2)}\n`)
|
|
113
|
+
process.exit(2)
|
|
114
|
+
}
|
|
115
|
+
const flags = usage.missing.join(" and ")
|
|
116
|
+
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,
|
|
117
|
+
`omakit submit ${target} --category <c> --tags <a,b>`,
|
|
118
|
+
(c) => [
|
|
119
|
+
...labelled("categories", usage.categories.join(", "), c),
|
|
120
|
+
...labelled(`tags, 1 to ${usage.maximumTags}`, usage.tags.join(", "), c),
|
|
121
|
+
])
|
|
122
|
+
}
|
|
99
123
|
const spinner = args.includes("--json") ? { phase: () => {}, done: () => {} } : progress()
|
|
100
124
|
let result
|
|
101
125
|
try {
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// checkout, because the two things a person might want "upgraded" here are not
|
|
5
5
|
// the same thing and only one of them may ever move on its own.
|
|
6
6
|
//
|
|
7
|
-
// The tool version belongs to the installer:
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// The tool version belongs to the installer: `omakit upgrade` hands it to Git
|
|
8
|
+
// for a checkout and to npm for its package, and names pacman for a distro
|
|
9
|
+
// package. A CLI that fetches and executes its own replacement is the
|
|
10
|
+
// supply-chain shape this repository warns about.
|
|
10
11
|
//
|
|
11
12
|
// The pin must not move by itself. Bumping it changes where the submission
|
|
12
13
|
// contract is read from, and the procedure in docs/UPSTREAM_CONTRACT.md requires
|
|
@@ -16,13 +17,22 @@
|
|
|
16
17
|
//
|
|
17
18
|
// That the pin goes stale unnoticed is, of course, exactly the defect class
|
|
18
19
|
// `omakit watch` exists to report. It would be poor form not to apply it here.
|
|
20
|
+
//
|
|
21
|
+
// And "behind" is not the same as "behind in something omakit reads". Measured
|
|
22
|
+
// on 2026-09-13 (docs/MEASUREMENTS.md M7): 4,201 of the marketplace's 4,293
|
|
23
|
+
// commits in 30 days touched only registry.json, which omakit now reads live,
|
|
24
|
+
// while nothing under scripts/ or .github/ISSUE_TEMPLATE/ changed since the
|
|
25
|
+
// pin. A doctor that only said "behind" would say it about every run. So it
|
|
26
|
+
// compares each path in PIN_PATHS between the pin and HEAD, by tree or blob
|
|
27
|
+
// id, and names the ones that moved.
|
|
19
28
|
|
|
20
29
|
import { execFileSync } from "node:child_process"
|
|
21
30
|
import { readFileSync } from "node:fs"
|
|
22
31
|
import { join } from "node:path"
|
|
23
|
-
import { MARKETPLACE_PIN, marketplacePinDir, pinDiskUsage, pinIsSparse, requirePin } from "./pin.mjs"
|
|
32
|
+
import { MARKETPLACE_PIN, PIN_PATHS, marketplacePinDir, pinDiskUsage, pinIsSparse, requirePin } from "./pin.mjs"
|
|
24
33
|
import { credential, defaultBranchHead, getJson, UNAUTHENTICATED_LIMIT, GitHubError } from "./github.mjs"
|
|
25
|
-
import { upgradeCommand } from "./upgrade.mjs"
|
|
34
|
+
import { latestOnRegistry, upgradeCommand } from "./upgrade.mjs"
|
|
35
|
+
import { pathHint } from "./path-hint.mjs"
|
|
26
36
|
|
|
27
37
|
function tool(repoRoot) {
|
|
28
38
|
try {
|
|
@@ -41,37 +51,91 @@ function version(command, args = ["--version"]) {
|
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
/** The object id a path has at a commit in the pinned checkout: a tree id for a directory, a blob id for a file. Local; a blob-filtered clone still has every tree. */
|
|
55
|
+
function pinObjectId(pinDir, commit, path) {
|
|
56
|
+
return execFileSync("git", ["-C", pinDir, "rev-parse", `${commit}:${path}`], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Which of PIN_PATHS differ between the pin and `headCommit`. The pin side is
|
|
61
|
+
* read from the local checkout; the HEAD side walks the git-trees API from the
|
|
62
|
+
* exact commit, one read per tree on the way (three for PIN_PATHS as it
|
|
63
|
+
* stands), so a directory compares by its tree id and a file by its blob id,
|
|
64
|
+
* which is what "the blob at HEAD versus the blob at the pin" means for a
|
|
65
|
+
* directory that omakit reads whole. A path missing at HEAD counts as changed.
|
|
66
|
+
*
|
|
67
|
+
* `fetchJson` is injectable for tests; the default is the one GET call site.
|
|
68
|
+
*/
|
|
69
|
+
export async function changedPinPaths({ pinDir, headCommit, pinCommit = MARKETPLACE_PIN.commit, fetchJson = getJson }) {
|
|
70
|
+
const match = MARKETPLACE_PIN.repository.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
|
|
71
|
+
const trees = new Map()
|
|
72
|
+
const tree = async (sha) => {
|
|
73
|
+
if (!trees.has(sha)) {
|
|
74
|
+
const read = await fetchJson(`https://api.github.com/repos/${match[1]}/${match[2]}/git/trees/${sha}`)
|
|
75
|
+
if (!Array.isArray(read?.tree)) throw new GitHubError("head-unreadable", `the tree ${sha} at the marketplace's HEAD did not read as a tree`)
|
|
76
|
+
trees.set(sha, read.tree)
|
|
77
|
+
}
|
|
78
|
+
return trees.get(sha)
|
|
79
|
+
}
|
|
80
|
+
const headObjectId = async (path) => {
|
|
81
|
+
const segments = path.split("/")
|
|
82
|
+
let entries = await tree(headCommit)
|
|
83
|
+
let id = null
|
|
84
|
+
for (const [index, segment] of segments.entries()) {
|
|
85
|
+
const entry = entries.find((candidate) => candidate.path === segment)
|
|
86
|
+
if (!entry) return null
|
|
87
|
+
id = entry.sha
|
|
88
|
+
// Descend only on the way to a deeper segment: the path's own tree id
|
|
89
|
+
// is the comparison, and its contents need no read.
|
|
90
|
+
if (index < segments.length - 1) {
|
|
91
|
+
if (entry.type !== "tree") return null
|
|
92
|
+
entries = await tree(entry.sha)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return id
|
|
96
|
+
}
|
|
97
|
+
const changed = []
|
|
98
|
+
for (const pattern of PIN_PATHS) {
|
|
99
|
+
const path = pattern.replace(/^\/|\/$/g, "")
|
|
100
|
+
if (pinObjectId(pinDir, pinCommit, path) !== await headObjectId(path)) changed.push(pattern)
|
|
50
101
|
}
|
|
102
|
+
return changed
|
|
51
103
|
}
|
|
52
104
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Full pin evidence for machines; human output deliberately keeps short
|
|
107
|
+
* hashes. `changedPaths` is the answer from changedPinPaths(); null when the
|
|
108
|
+
* comparison was not made, and then the detail says only that HEAD moved.
|
|
109
|
+
*/
|
|
110
|
+
export function pinFreshness(identity, head, changedPaths = null) {
|
|
55
111
|
const current = head.commit === identity.commit
|
|
112
|
+
const branch = head.branch || "default"
|
|
113
|
+
const moved = changedPaths === null
|
|
114
|
+
? ""
|
|
115
|
+
: changedPaths.length
|
|
116
|
+
? `; changed since the pin: ${changedPaths.join(", ")}`
|
|
117
|
+
: "; every path omakit reads is unchanged since the pin"
|
|
56
118
|
return {
|
|
57
119
|
id: "pin.freshness",
|
|
58
120
|
state: current ? "ok" : "advice",
|
|
59
121
|
detail: current
|
|
60
|
-
? `the pin is the marketplace's current ${
|
|
61
|
-
: `the pin is ${identity.commit.slice(0, 7)}; the marketplace's ${
|
|
122
|
+
? `the pin is the marketplace's current ${branch}-branch HEAD`
|
|
123
|
+
: `the pin is ${identity.commit.slice(0, 7)}; the marketplace's ${branch} branch is now at ${head.commit.slice(0, 7)}${moved}`,
|
|
62
124
|
action: current ? null : "Bumping the pin is a deliberate change: docs/UPSTREAM_CONTRACT.md has the procedure, which ends in re-proving parity and committing its evidence. Nothing here does it for you.",
|
|
63
125
|
evidence: {
|
|
64
126
|
pinCommit: identity.commit,
|
|
65
127
|
marketplaceHead: head.commit,
|
|
66
|
-
branch
|
|
128
|
+
branch,
|
|
129
|
+
...(changedPaths === null ? {} : { changedPaths: current ? [] : changedPaths }),
|
|
67
130
|
},
|
|
68
131
|
}
|
|
69
132
|
}
|
|
70
133
|
|
|
71
134
|
/**
|
|
72
|
-
* @param {{ repoRoot: string, offline?: boolean }} options
|
|
135
|
+
* @param {{ repoRoot: string, offline?: boolean, env?: object, npmPrefix?: () => string|null }} options
|
|
136
|
+
* `env` and `npmPrefix` are injectable for tests of the PATH check.
|
|
73
137
|
*/
|
|
74
|
-
export async function doctor({ repoRoot, offline = false, onPhase }) {
|
|
138
|
+
export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix }) {
|
|
75
139
|
// Optional: told what is being read while the network answers. Never
|
|
76
140
|
// affects the result.
|
|
77
141
|
const phase = onPhase || (() => {})
|
|
@@ -87,6 +151,16 @@ export async function doctor({ repoRoot, offline = false, onPhase }) {
|
|
|
87
151
|
const self = tool(repoRoot)
|
|
88
152
|
add("omakit.version", "info", `${self.name} ${self.version}`)
|
|
89
153
|
|
|
154
|
+
// Reachable as a bare command, or the one line that makes it so for this
|
|
155
|
+
// install (path-hint.mjs). Measured: an npm prefix whose bin is not on PATH
|
|
156
|
+
// installs a command nobody can run, and nothing said so.
|
|
157
|
+
const reach = pathHint({ repoRoot, entryPoint: join(repoRoot, "bin/omakit"), env, ...(npmPrefix ? { npmPrefix } : {}) })
|
|
158
|
+
add("omakit.path", reach.reachable ? "ok" : "advice",
|
|
159
|
+
reach.reachable
|
|
160
|
+
? `\`omakit\` is reachable as a command from PATH (${reach.kind} install)`
|
|
161
|
+
: `${reach.reason}${reach.where ? ` Keep the line below in ${reach.where}.` : ""}`,
|
|
162
|
+
reach.reachable ? null : reach.line)
|
|
163
|
+
|
|
90
164
|
const node = process.versions.node
|
|
91
165
|
const major = Number(node.split(".")[0])
|
|
92
166
|
add("node", major >= 22 ? "ok" : "problem", `node ${node}${self.engines ? ` (needs ${self.engines})` : ""}`,
|
|
@@ -112,7 +186,12 @@ export async function doctor({ repoRoot, offline = false, onPhase }) {
|
|
|
112
186
|
phase("reading the marketplace's current default-branch HEAD")
|
|
113
187
|
try {
|
|
114
188
|
const head = await defaultBranchHead(MARKETPLACE_PIN.repository)
|
|
115
|
-
|
|
189
|
+
let changedPaths = []
|
|
190
|
+
if (head.commit !== identity.commit) {
|
|
191
|
+
phase("comparing each path omakit reads between the pin and HEAD")
|
|
192
|
+
changedPaths = await changedPinPaths({ pinDir: dir, headCommit: head.commit, pinCommit: identity.commit })
|
|
193
|
+
}
|
|
194
|
+
checks.push(pinFreshness(identity, head, changedPaths))
|
|
116
195
|
} catch (error) {
|
|
117
196
|
add("pin.freshness", "unknown", `could not read the marketplace's HEAD (${error.code || "error"})`,
|
|
118
197
|
error.code === "network-unavailable" ? "Connect to the network, or pass --offline to skip the two checks that need it." : null,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Is `omakit` reachable as a bare command, and if not, the one line that makes
|
|
2
|
+
// it so, for the install that is actually here.
|
|
3
|
+
//
|
|
4
|
+
// Measured: after `npm install --global omakit` on a machine whose npm prefix
|
|
5
|
+
// was ~/.local/share/lerd/node-global, the command was missing, because that
|
|
6
|
+
// prefix's `bin` was not on PATH. `omakit setup` then printed the `ln -s` hint
|
|
7
|
+
// written for a clone, which points at a bin/omakit that npm did not lay out
|
|
8
|
+
// where the hint assumes. An npm install needs the npm prefix's `bin` on PATH,
|
|
9
|
+
// and the way to say that depends on the shell in $SHELL. Nothing here writes
|
|
10
|
+
// to an rc file: the line is printed, and adding it is the user's.
|
|
11
|
+
|
|
12
|
+
import { existsSync } from "node:fs"
|
|
13
|
+
import { basename, delimiter, join } from "node:path"
|
|
14
|
+
import { installKind, npmGlobalPrefix } from "./upgrade.mjs"
|
|
15
|
+
|
|
16
|
+
/** Is `name` reachable as a bare command, without asking a shell? */
|
|
17
|
+
export function onPath(name = "omakit", env = process.env) {
|
|
18
|
+
return (env.PATH || "").split(delimiter).some((dir) => dir && existsSync(join(dir, name)))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The rc file each supported shell reads at start, for the person to add the line to. */
|
|
22
|
+
const RC = Object.freeze({ bash: "~/.bashrc", zsh: "~/.zshrc" })
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ repoRoot: string, entryPoint: string, env?: object, npmPrefix?: () => string|null }} options
|
|
26
|
+
* `npmPrefix` is injectable for tests; the default asks the npm on PATH.
|
|
27
|
+
* @returns {{ kind: "git"|"npm"|"distro", reachable: boolean, reason: string|null,
|
|
28
|
+
* line: string|null, where: string|null }}
|
|
29
|
+
* `line` is the command that puts `omakit` on PATH; `where` is the rc file
|
|
30
|
+
* to keep it in, or null when the shell keeps it itself (fish) or is not
|
|
31
|
+
* one of the three with a script.
|
|
32
|
+
*/
|
|
33
|
+
export function pathHint({ repoRoot, entryPoint, env = process.env, npmPrefix = npmGlobalPrefix }) {
|
|
34
|
+
const kind = installKind(repoRoot)
|
|
35
|
+
if (onPath("omakit", env)) return { kind, reachable: true, reason: null, line: null, where: null }
|
|
36
|
+
if (kind !== "npm") {
|
|
37
|
+
return { kind, reachable: false, reason: "`omakit` is not on your PATH yet.", line: `ln -s ${entryPoint} ~/.local/bin/omakit`, where: null }
|
|
38
|
+
}
|
|
39
|
+
const prefix = npmPrefix()
|
|
40
|
+
if (!prefix) {
|
|
41
|
+
return { kind, reachable: false, reason: "`omakit` is not on your PATH yet, and no `npm` is on PATH to ask where it was installed.", line: null, where: null }
|
|
42
|
+
}
|
|
43
|
+
const bin = join(prefix, "bin")
|
|
44
|
+
const shell = basename(env.SHELL || "")
|
|
45
|
+
const line = shell === "fish" ? `fish_add_path ${bin}` : `export PATH="${bin}:$PATH"`
|
|
46
|
+
return {
|
|
47
|
+
kind,
|
|
48
|
+
reachable: false,
|
|
49
|
+
reason: `\`omakit\` is not on your PATH yet: npm installed it under ${bin}, which your shell does not search.`,
|
|
50
|
+
line,
|
|
51
|
+
where: RC[shell] || null,
|
|
52
|
+
}
|
|
53
|
+
}
|