omakit 0.1.8 → 0.1.9

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
@@ -2,7 +2,7 @@
2
2
  <img src="docs/media/banner.gif" alt="omakit" width="440">
3
3
  </p>
4
4
 
5
- [![Built for Omarchy](https://raw.githubusercontent.com/tcballard/omarchy-badges/85f859029e236e784e7b05ada6dbe73506d07a91/badges/v1/built-for-omarchy.svg)](https://github.com/tcballard/omarchy-badges)
5
+ [![Built for Omarchy: App](https://raw.githubusercontent.com/tcballard/omarchy-badges/75975e5b5bf75e7ede3764bcd2950046f7abfe2c/badges/v1/omarchy-app.svg)](https://github.com/tcballard/omarchy-badges)
6
6
 
7
7
  **Everything knowable about an Omarchy Quattro plugin submission, checked
8
8
  before you post it:** the tree, the manifest, the form, the commit, and the
@@ -56,7 +56,7 @@ omakit setup
56
56
  ```
57
57
 
58
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
59
+ (measured: an npm global prefix under `~/.local/share` whose `bin` no shell
60
60
  searched). Run `"$(npm prefix --global)/bin/omakit" setup` once: it prints the
61
61
  one line that puts that directory on PATH for the shell in `$SHELL`, and the
62
62
  rc file to keep it in; `omakit doctor` reports the same as `omakit.path`.
@@ -192,6 +192,11 @@ the marketplace's current HEAD when the network is there, because the pin's
192
192
  copy is stale within hours (4,201 of 4,293 commits in 30 days touched only
193
193
  `registry.json`), and from the pin with `--offline`.
194
194
 
195
+ This is why the tool is Node: the marketplace's scanner, form parser and
196
+ catalog builder are Node modules, and omakit runs them verbatim from the
197
+ pinned commit instead of reimplementing their rules, where a different
198
+ language would mean a copy that can drift.
199
+
195
200
  The security baseline is the marketplace's own code, imported unmodified and run
196
201
  over a local snapshot with no network. Omakit adds no rule, renames no outcome,
197
202
  and never restates the result as a safety claim: the baseline does no data-flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omakit",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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",
@@ -21,7 +21,7 @@ local commit through the transport seam the marketplace tests itself
21
21
  | `issue.mjs` | Renders the issue the way the form would, then has the marketplace's own parser judge it. |
22
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
+ | `doctor.mjs` | What is installed, what is pinned, and what has moved. `omakit.version` is one check for one question, is this the current omakit: `ok` with "0.1.8, the newest published version", a `note` with "0.1.8; 0.1.9 is published" and the upgrade command, `unknown` with the registry's failure code, `info` offline; its evidence carries `{ installed, latest, source }`. `pin.freshness` compares 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. |
25
25
  | `watch.mjs` | The validation watch: validated commit versus current default-branch HEAD, and the one action that refreshes it. |
26
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. |
27
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. |
@@ -32,6 +32,7 @@ local commit through the transport seam the marketplace tests itself
32
32
  | `banner.mjs` | The wordmark, on a bare `omakit` and in `setup` only. |
33
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. |
34
34
  | `progress.mjs` | The progress line, on stderr, only when a person is looking. |
35
+ | `paths.mjs` | Omakit's cache directory, following XDG, and `withHomeAbbreviated()`: a path under `$HOME` written as `~/...` for a person, applied where doctor, setup and pin render text and never where a result is built, so `--json` keeps every path absolute. |
35
36
  | `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`. |
36
37
 
37
38
  ```text
@@ -29,7 +29,7 @@ import { progress } from "./progress.mjs"
29
29
  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
- import { omakitCacheDir } from "./paths.mjs"
32
+ import { omakitCacheDir, withHomeAbbreviated } from "./paths.mjs"
33
33
 
34
34
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..")
35
35
 
@@ -284,7 +284,7 @@ if (command === "setup") {
284
284
  // ensurePin narrates: a state line to keep, then a fetch it is about to
285
285
  // start. The fetch is the slow part, so it gets the progress line.
286
286
  if (line.state === "fetching") spinner.phase(line.text)
287
- else process.stdout.write(`${mark(line.state, c)}${wrap(line.text, { indent: GUTTER }, c).join("\n").trimStart()}\n`)
287
+ else process.stdout.write(`${mark(line.state, c)}${wrap(withHomeAbbreviated(line.text), { indent: GUTTER }, c).join("\n").trimStart()}\n`)
288
288
  })
289
289
  } catch (error) {
290
290
  spinner.done()
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
13
13
  import { basename, dirname, join } from "node:path"
14
14
  import { tagSlug } from "./form.mjs"
15
15
  import { COMMANDS, COMPLETION_SHELLS } from "./usage.mjs"
16
+ import { withHomeAbbreviated } from "./paths.mjs"
16
17
 
17
18
  /**
18
19
  * The completion model, read out of the help data. A subcommand is the word
@@ -221,7 +222,7 @@ export function completionInstall(env = process.env) {
221
222
  const shell = basename(env.SHELL || "")
222
223
  const home = env.HOME || ""
223
224
  if (!home || !COMPLETION_SHELLS.includes(shell)) return null
224
- const tilde = (dir) => (dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir)
225
+ const tilde = (dir) => withHomeAbbreviated(dir, env)
225
226
  if (shell === "bash") {
226
227
  const dir = join(env.XDG_DATA_HOME || join(home, ".local/share"), "bash-completion/completions")
227
228
  return { shell, path: join(dir, "omakit"), display: `${tilde(dir)}/omakit`, note: null }
@@ -42,7 +42,7 @@ import { join } from "node:path"
42
42
  import { MARKETPLACE_PIN, PIN_PATHS, marketplacePinDir, pinDiskUsage, pinIsSparse, requirePin } from "./pin.mjs"
43
43
  import { LIVE_PATHS } from "./registry.mjs"
44
44
  import { credential, defaultBranchHead, getJson, UNAUTHENTICATED_LIMIT, GitHubError } from "./github.mjs"
45
- import { latestOnRegistry, upgradeCommand } from "./upgrade.mjs"
45
+ import { NPM_REGISTRY, registryLatest, upgradeCommand } from "./upgrade.mjs"
46
46
  import { pathHint } from "./path-hint.mjs"
47
47
 
48
48
  /** "git+https://github.com/owner/name.git" in package.json -> "https://github.com/owner/name", or null. */
@@ -172,13 +172,13 @@ export function pinFreshness(identity, head, changedPaths = null, { issues = nul
172
172
 
173
173
  /**
174
174
  * @param {{ repoRoot: string, offline?: boolean, env?: object, npmPrefix?: () => string|null,
175
- * resolveHead?: typeof defaultBranchHead, latest?: typeof latestOnRegistry }} options
175
+ * resolveHead?: typeof defaultBranchHead, latest?: typeof registryLatest }} options
176
176
  * `env` and `npmPrefix` are injectable for tests of the PATH check;
177
177
  * `resolveHead` and `latest` for tests of the two checks that read the
178
178
  * network, whose defaults are the tool's one HEAD resolver and its one
179
179
  * registry read.
180
180
  */
181
- export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = latestOnRegistry }) {
181
+ export async function doctor({ repoRoot, offline = false, onPhase, env = process.env, npmPrefix, resolveHead = defaultBranchHead, latest: latestVersion = registryLatest }) {
182
182
  // Optional: told what is being read while the network answers. Never
183
183
  // affects the result.
184
184
  const phase = onPhase || (() => {})
@@ -192,7 +192,27 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
192
192
  })
193
193
 
194
194
  const self = tool(repoRoot)
195
- add("omakit.version", "info", `${self.name} ${self.version}`)
195
+ // What is installed and what is published, one check: the two facts are
196
+ // one question, "is this the current omakit", and were two lines before.
197
+ // Offline, the first fact alone, as information. The evidence carries both
198
+ // and where the second came from.
199
+ const versionCheck = (state, detail, action = null, latest = null, source = null) =>
200
+ add("omakit.version", state, detail, action, { installed: self.version, latest, source })
201
+ if (offline) {
202
+ versionCheck("info", `${self.version}; the newest published version is not checked (--offline)`)
203
+ } else {
204
+ phase("asking the npm registry for the newest published version")
205
+ const published = await latestVersion(self.name)
206
+ if (published.version) {
207
+ const current = published.version === self.version
208
+ versionCheck(current ? "ok" : "advice",
209
+ current ? `${self.version}, the newest published version` : `${self.version}; ${published.version} is published`,
210
+ current ? null : `run \`${upgradeCommand(repoRoot, self.name)}\``,
211
+ published.version, NPM_REGISTRY)
212
+ } else {
213
+ versionCheck("unknown", `${self.version}; could not read the npm registry (${published.error?.code || "error"})`)
214
+ }
215
+ }
196
216
 
197
217
  // Reachable as a bare command, or the one line that makes it so for this
198
218
  // install (path-hint.mjs). Measured: an npm prefix whose bin is not on PATH
@@ -241,16 +261,6 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
241
261
  { pinCommit: identity.commit, marketplaceHead: null, branch: null })
242
262
  }
243
263
 
244
- phase("asking the npm registry for the newest published version")
245
- const latest = await latestVersion(self.name)
246
- if (latest) {
247
- const current = latest === self.version
248
- add("omakit.latest", current ? "ok" : "advice",
249
- current ? `${latest} is the newest published version` : `${latest} is published, this is ${self.version}`,
250
- current ? null : upgradeCommand(repoRoot, self.name))
251
- } else {
252
- add("omakit.latest", "unknown", "the npm registry did not answer, or this version is unpublished")
253
- }
254
264
  }
255
265
 
256
266
  // Where the credential comes from, said out loud. Borrowing someone's `gh`
@@ -2,7 +2,7 @@
2
2
  // it so, for the install that is actually here.
3
3
  //
4
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
5
+ // was a directory under ~/.local/share, the command was missing, because that
6
6
  // prefix's `bin` was not on PATH. `omakit setup` then printed the `ln -s` hint
7
7
  // written for a clone, which points at a bin/omakit that npm did not lay out
8
8
  // where the hint assumes. An npm install needs the npm prefix's `bin` on PATH,
@@ -8,3 +8,21 @@ export function omakitCacheDir(name = "", env = process.env) {
8
8
  : join(env.HOME || homedir(), ".cache")
9
9
  return join(base, "omakit", name)
10
10
  }
11
+
12
+ /**
13
+ * Text for a person, with every path under the home directory written the
14
+ * way a shell would take it: `~/.cache/omakit/marketplace`. Only `$HOME`
15
+ * counts, because `~` is what the shell expands to `$HOME` and nothing else;
16
+ * with it unset, or for a path outside it, the text is returned as it came.
17
+ * Human output only: `--json` keeps every path absolute, so this is applied
18
+ * where text is rendered, never where a result is built.
19
+ */
20
+ export function withHomeAbbreviated(text, env = process.env) {
21
+ const home = String(env.HOME || "").replace(/\/+$/, "")
22
+ if (!home || !home.startsWith("/")) return String(text)
23
+ // The home directory where a path starts: at the start of the text or after
24
+ // the characters a path follows in prose or a command, and followed by a
25
+ // separator or the end, so /tmp/home/me/x and /home/meh/x are left alone.
26
+ const literal = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
27
+ return String(text).replace(new RegExp(`(^|[\\s"'(=:])${literal}(?=/|$|[\\s"'),:])`, "g"), "$1~")
28
+ }
@@ -20,6 +20,7 @@
20
20
  import {
21
21
  action, colourEnabled, COLUMNS, continuation, field, GUTTER, labelled, mark, section, STEP, styler, verdict, width, wrap,
22
22
  } from "./style.mjs"
23
+ import { withHomeAbbreviated } from "./paths.mjs"
23
24
 
24
25
  const body = " ".repeat(GUTTER)
25
26
 
@@ -317,7 +318,13 @@ export function renderVerify(document, { colour = colourEnabled(), blockingRules
317
318
 
318
319
  const DOCTOR_STATE = { ok: "pass", advice: "advisory", problem: "fail", info: "info", unknown: "unknown" }
319
320
 
320
- export function renderDoctor(result, { colour = colourEnabled() } = {}) {
321
+ /**
322
+ * @param {{ colour?: boolean, env?: object }} [options] `env` is where `$HOME`
323
+ * is read from: a path under it is printed as `~/...` for a person, the
324
+ * way a shell takes it, while the result itself, and so `--json`, keeps
325
+ * every path absolute.
326
+ */
327
+ export function renderDoctor(result, { colour = colourEnabled(), env = process.env } = {}) {
321
328
  const c = styler(colour)
322
329
  const out = []
323
330
  let previous = false
@@ -326,8 +333,8 @@ export function renderDoctor(result, { colour = colourEnabled() } = {}) {
326
333
  const loud = state === "fail" || state === "advisory"
327
334
  if (index > 0 && (loud || previous)) out.push("")
328
335
  out.push(`${mark(state, c)}${c("name", check.id)}`)
329
- out.push(...wrap(check.detail, { indent: GUTTER }, c))
330
- if (check.action) out.push(...action(check.action, c))
336
+ out.push(...wrap(withHomeAbbreviated(check.detail, env), { indent: GUTTER }, c))
337
+ if (check.action) out.push(...action(withHomeAbbreviated(check.action, env), c))
331
338
  previous = loud
332
339
  }
333
340
  out.push("")
@@ -20,6 +20,7 @@ import { TAGLINE } from "./usage.mjs"
20
20
  import { installCompletion } from "./completion.mjs"
21
21
  import { submissionContract } from "./form.mjs"
22
22
  import { pathHint } from "./path-hint.mjs"
23
+ import { withHomeAbbreviated } from "./paths.mjs"
23
24
 
24
25
  function version(command) {
25
26
  try {
@@ -32,16 +33,18 @@ function version(command) {
32
33
  }
33
34
 
34
35
  /**
35
- * @param {{ repoRoot: string, entryPoint: string, stream?: NodeJS.WriteStream }} options
36
+ * @param {{ repoRoot: string, entryPoint: string, stream?: NodeJS.WriteStream, env?: object }} options
37
+ * `env` is where `$HOME` is read from: this is output for a person, so a
38
+ * path under it is printed as `~/...`.
36
39
  */
37
- export async function setup({ repoRoot, entryPoint, stream = process.stdout }) {
40
+ export async function setup({ repoRoot, entryPoint, stream = process.stdout, env = process.env }) {
38
41
  const c = styler(colourEnabled(stream))
39
42
  const out = (line = "") => stream.write(`${line}\n`)
40
43
  // A step is a status line: the mark, then the fact, wrapped under itself.
41
- const step = (state, text) => out(`${mark(state, c)}${wrap(text, { indent: GUTTER }, c).join("\n").trimStart()}`)
44
+ const step = (state, text) => out(`${mark(state, c)}${wrap(withHomeAbbreviated(text, env), { indent: GUTTER }, c).join("\n").trimStart()}`)
42
45
  // The one action under a step sits in the step's body; under a sentence it
43
46
  // sits where the sentence does.
44
- const fix = (text, indent = GUTTER) => { for (const line of action(text, c, { indent })) out(line) }
47
+ const fix = (text, indent = GUTTER) => { for (const line of action(withHomeAbbreviated(text, env), c, { indent })) out(line) }
45
48
 
46
49
  // The one place the wordmark runs through `ttfx` (effect.mjs): a first run
47
50
  // already spending seconds fetching the pin.
@@ -59,11 +59,29 @@ export const NPM_UPGRADE_ARGS = Object.freeze(["install", "--global", "--ignore-
59
59
 
60
60
  /** The newest published version, or null when the registry did not answer. */
61
61
  export async function latestOnRegistry(name) {
62
+ return (await registryLatest(name)).version
63
+ }
64
+
65
+ /** The registry the newest version is read from, named in doctor's evidence. */
66
+ export const NPM_REGISTRY = "https://registry.npmjs.org"
67
+
68
+ /**
69
+ * The newest published version, with the reason when there is none: the
70
+ * failure code from the one GET call site (network-unavailable,
71
+ * github-unavailable's npm sibling, not-found for an unpublished name), or
72
+ * `unpublished` when the registry answered without a version. `doctor` prints
73
+ * the code; `upgrade` only needs the version.
74
+ *
75
+ * @returns {Promise<{ version: string|null, error: { code: string, message: string }|null }>}
76
+ */
77
+ export async function registryLatest(name) {
62
78
  try {
63
- const meta = await getJson(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`)
64
- return meta?.version || null
65
- } catch {
66
- return null
79
+ const meta = await getJson(`${NPM_REGISTRY}/${encodeURIComponent(name)}/latest`)
80
+ return meta?.version
81
+ ? { version: meta.version, error: null }
82
+ : { version: null, error: { code: "unpublished", message: "the registry answered without a version" } }
83
+ } catch (error) {
84
+ return { version: null, error: { code: error?.code || "error", message: String(error?.message || error) } }
67
85
  }
68
86
  }
69
87