omakit 0.5.1 → 0.6.1

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.
Files changed (94) hide show
  1. package/README.md +38 -45
  2. package/blocks/history.json +68 -0
  3. package/blocks/run/NOTICE +12 -0
  4. package/blocks/run/Run.qml +242 -0
  5. package/blocks/run/run-supervisor.py +522 -0
  6. package/blocks/store/NOTICE +12 -0
  7. package/blocks/store/Store.qml +157 -0
  8. package/blocks/store/store-helper.py +431 -0
  9. package/package.json +12 -5
  10. package/skills/omarchy-plugin-audit/SKILL.md +11 -5
  11. package/skills/omarchy-plugin-build/SKILL.md +164 -0
  12. package/skills/omarchy-plugin-check/SKILL.md +6 -3
  13. package/skills/omarchy-plugin-submit/SKILL.md +4 -1
  14. package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -2
  15. package/skills/omarchy-plugin-weigh/SKILL.md +13 -2
  16. package/tests/fixtures/weigh/clean/Widget.qml +19 -0
  17. package/tests/fixtures/weigh/clean/manifest.json +9 -0
  18. package/tests/fixtures/weigh/clean/tests/harness.qml +7 -0
  19. package/tests/fixtures/weigh/idle-panel/Panel.qml +65 -0
  20. package/tests/fixtures/weigh/idle-panel/manifest.json +9 -0
  21. package/tests/fixtures/weigh/poller/Service.qml +50 -0
  22. package/tests/fixtures/weigh/poller/manifest.json +9 -0
  23. package/tests/fixtures/weigh/timer-180ms/Widget.qml +25 -0
  24. package/tests/fixtures/weigh/timer-180ms/manifest.json +9 -0
  25. package/tests/lab/run/harness/scenarios/controls.sh +6 -0
  26. package/tests/lab/run/harness/scenarios/envprobe.sh +10 -0
  27. package/tests/lab/run/harness/scenarios/forge.sh +11 -0
  28. package/tests/lab/run/harness/scenarios/holder.sh +5 -0
  29. package/tests/lab/run/harness/scenarios/orphan.sh +6 -0
  30. package/tests/lab/run/harness/scenarios/stall.sh +5 -0
  31. package/tests/lab/run/harness/scenarios/stubborn.sh +5 -0
  32. package/tests/lab/run/harness/scenarios/tree.sh +7 -0
  33. package/tests/lab/run/harness/shell.qml +84 -0
  34. package/tests/lab/run/report.py +217 -0
  35. package/tests/lab/run/suite.sh +106 -0
  36. package/tests/lab/store/harness/shell.qml +73 -0
  37. package/tests/lab/store/report.py +133 -0
  38. package/tests/lab/store/suite.sh +109 -0
  39. package/tests/parity/corpus.mjs +8 -3
  40. package/tests/parity/run.mjs +4 -4
  41. package/tools/audit/audit.mjs +17 -6
  42. package/tools/audit/git.mjs +3 -3
  43. package/tools/audit/report.mjs +31 -5
  44. package/tools/blocks/add.mjs +138 -0
  45. package/tools/blocks/commit.json +5 -0
  46. package/tools/blocks/record-commit.mjs +77 -0
  47. package/tools/blocks/registry.mjs +191 -0
  48. package/tools/blocks/stamp.mjs +61 -0
  49. package/tools/inspect/contract.mjs +36 -5
  50. package/tools/inspect/helpers.mjs +217 -0
  51. package/tools/inspect/inspect.mjs +68 -3
  52. package/tools/inspect/patterns.mjs +18 -3
  53. package/tools/inspect/processes.mjs +38 -5
  54. package/tools/inspect/report.mjs +18 -2
  55. package/tools/inspect/writes.mjs +22 -4
  56. package/tools/lab/guest.mjs +155 -0
  57. package/tools/lab/harness.sh +119 -0
  58. package/tools/lab/host.mjs +177 -0
  59. package/tools/lab/inspect.mjs +240 -0
  60. package/tools/lab/omarchy.gpg +13 -0
  61. package/tools/lab/patches/omarchy-iso-test.patch +351 -0
  62. package/tools/lab/paths.mjs +173 -0
  63. package/tools/lab/pin.json +42 -0
  64. package/tools/lab/pin.mjs +64 -0
  65. package/tools/lab/prune.mjs +68 -0
  66. package/tools/lab/qemu.mjs +153 -0
  67. package/tools/lab/qmp-cli.mjs +21 -0
  68. package/tools/lab/report.mjs +183 -0
  69. package/tools/lab/run.mjs +344 -0
  70. package/tools/lab/setup.mjs +430 -0
  71. package/tools/lab/suites/run.sh +35 -0
  72. package/tools/lab/suites/store.sh +41 -0
  73. package/tools/lab/suites/weigh.sh +196 -0
  74. package/tools/lab/suites.mjs +142 -0
  75. package/tools/lab/verify.mjs +134 -0
  76. package/tools/marketplace/README.md +38 -1
  77. package/tools/marketplace/banner.mjs +23 -2
  78. package/tools/marketplace/cli.mjs +449 -146
  79. package/tools/marketplace/completion-check.mjs +27 -1
  80. package/tools/marketplace/completion.mjs +32 -4
  81. package/tools/marketplace/doctor.mjs +47 -9
  82. package/tools/marketplace/github.mjs +52 -6
  83. package/tools/marketplace/local-transport.mjs +1 -1
  84. package/tools/marketplace/options.mjs +16 -5
  85. package/tools/marketplace/outcome.mjs +244 -0
  86. package/tools/marketplace/pin.mjs +178 -33
  87. package/tools/marketplace/setup.mjs +16 -15
  88. package/tools/marketplace/tree.mjs +1 -1
  89. package/tools/marketplace/upgrade.mjs +5 -5
  90. package/tools/marketplace/usage.mjs +116 -72
  91. package/tools/subject/resolve.mjs +19 -6
  92. package/tools/weigh/audit.mjs +47 -16
  93. package/tools/weigh/config.mjs +105 -24
  94. package/tools/weigh/list.mjs +10 -1
@@ -10,7 +10,7 @@
10
10
  // path outside it, because on a partial clone such a read would quietly reach
11
11
  // for the network instead of failing.
12
12
  import { execFileSync, spawnSync } from "node:child_process"
13
- import { existsSync, mkdirSync, writeFileSync } from "node:fs"
13
+ import { existsSync, readdirSync, readFileSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs"
14
14
  import { dirname, join, resolve } from "node:path"
15
15
  import { omakitCacheDir } from "./paths.mjs"
16
16
 
@@ -83,7 +83,7 @@ function pinMigration(repoRoot, env = process.env) {
83
83
  }
84
84
 
85
85
  function git(dir, args, options = {}) {
86
- return execFileSync("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...options })
86
+ return execFileSync("git", ["-C", dir, ...args], { timeout: 300_000, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...options })
87
87
  }
88
88
 
89
89
  /** Identity of the checkout at `dir`: commit plus the policy constants read from the pinned source. */
@@ -127,35 +127,69 @@ function hasCommit(dir) {
127
127
  }
128
128
  }
129
129
 
130
+ /** Is a process alive: signal 0 asks without sending; EPERM means it is there and somebody else's. */
131
+ function alive(pid) {
132
+ if (!Number.isInteger(pid) || pid <= 0) return false
133
+ try {
134
+ process.kill(pid, 0)
135
+ return true
136
+ } catch (error) {
137
+ return error.code === "EPERM"
138
+ }
139
+ }
140
+
141
+ function sleepMs(ms) {
142
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
143
+ }
144
+
145
+ /** How long a second first run waits for the first to finish fetching before it gives up: the fetch is about 2 s on the reference network, so this is generous. */
146
+ export const PIN_WAIT_MS = 300_000
147
+
130
148
  /**
131
- * Reproducible setup: fetch exactly the pinned commit (depth 1) into
132
- * the XDG cache and check it out detached. Idempotent; never rewrites
133
- * an existing checkout that already sits at the pin.
134
- *
135
- * `log` is told what is happening as `{ state, text }`: a `pass` or `info`
136
- * line to keep, or `fetching` for the slow step about to start, which the CLI
137
- * draws as a progress line rather than a line of output.
149
+ * The lock beside the pin, `<dir>.lock/`, made atomically: `mkdir` without
150
+ * `recursive` fails with EEXIST when it is there, so exactly one process
151
+ * holds it. The holder writes its pid into it; a lock whose pid is gone is
152
+ * stale and is taken over. Returns `release()`, or null when another live
153
+ * process holds it.
138
154
  */
139
- export function ensurePin(repoRoot, log = () => {}, env = process.env) {
140
- const migration = pinMigration(repoRoot, env)
141
- if (migration) throw migration
142
- const dir = marketplacePinDir(repoRoot, env)
143
- if (existsSync(join(dir, ".git")) && hasCommit(dir)) {
144
- const identity = readPinIdentity(dir)
145
- if (identity.commit === MARKETPLACE_PIN.commit && !identity.dirty) {
146
- log({ state: "pass", text: `marketplace pin ${identity.commit.slice(0, 7)} present at ${dir}` })
147
- return { dir, identity, fetched: false }
155
+ function claimLock(lockDir) {
156
+ for (let attempt = 0; attempt < 2; attempt += 1) {
157
+ try {
158
+ mkdirSync(lockDir)
159
+ writeFileSync(join(lockDir, "holder.json"), `${JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() })}\n`)
160
+ return () => rmSync(lockDir, { recursive: true, force: true })
161
+ } catch (error) {
162
+ if (error.code !== "EEXIST") throw error
163
+ let holder = null
164
+ try {
165
+ holder = JSON.parse(readFileSync(join(lockDir, "holder.json"), "utf8")).pid
166
+ } catch {
167
+ holder = null
168
+ }
169
+ // A lock without a holder file yet is one being written this instant; a lock whose holder is dead is stale.
170
+ if (holder !== null && !alive(holder)) {
171
+ rmSync(lockDir, { recursive: true, force: true })
172
+ continue
173
+ }
174
+ return null
148
175
  }
149
- if (identity.dirty) throw new PinError(`${dir} has local modifications; remove the directory and run again`)
150
- log({ state: "info", text: `${dir} is at ${identity.commit}, not the pin` })
151
- } else if (!existsSync(join(dir, ".git"))) {
152
- mkdirSync(dir, { recursive: true })
153
- execFileSync("git", ["init", "-q", dir], { encoding: "utf8" })
154
- git(dir, ["remote", "add", "origin", MARKETPLACE_PIN.repository])
155
176
  }
177
+ return null
178
+ }
179
+
180
+ /**
181
+ * Fetch the pinned commit into a staging directory beside the pin, sparse
182
+ * and blob-filtered, and check it out detached. Written as plumbing rather
183
+ * than through `git sparse-checkout`, so the result does not depend on the
184
+ * git version's cone-mode defaults. `dir` here is the staging directory,
185
+ * and the sparse-checkout file is the one write beside the pin.
186
+ */
187
+ function populatePin(dir, log) {
188
+ rmSync(dir, { recursive: true, force: true })
189
+ mkdirSync(dir, { recursive: true })
190
+ execFileSync("git", ["init", "-q", dir], { timeout: 60_000, encoding: "utf8" })
191
+ git(dir, ["remote", "add", "origin", MARKETPLACE_PIN.repository])
156
192
  log({ state: "fetching", text: `fetching the pinned marketplace checkout, ${MARKETPLACE_PIN.commit.slice(0, 7)}, about 15 MB` })
157
- // Written as plumbing rather than through `git sparse-checkout`, so the
158
- // result does not depend on the git version's cone-mode defaults.
159
193
  git(dir, ["config", "core.sparseCheckout", "true"])
160
194
  mkdirSync(join(dir, ".git/info"), { recursive: true })
161
195
  writeFileSync(join(dir, ".git/info/sparse-checkout"), `${PIN_PATHS.join("\n")}\n`)
@@ -174,8 +208,93 @@ export function ensurePin(repoRoot, log = () => {}, env = process.env) {
174
208
  git(dir, ["checkout", "-q", "--detach", MARKETPLACE_PIN.commit])
175
209
  const identity = readPinIdentity(dir)
176
210
  if (identity.commit !== MARKETPLACE_PIN.commit) throw new PinError(`checkout ended at ${identity.commit}`)
177
- log({ state: "pass", text: `marketplace pin ${identity.commit.slice(0, 7)} (baseline ${identity.baselineVersion}, ${identity.enforcementMode}) at ${dir}, ${pinDiskUsage(dir)}` })
178
- return { dir, identity, fetched: true }
211
+ return identity
212
+ }
213
+
214
+ /**
215
+ * Reproducible setup: fetch exactly the pinned commit (depth 1) into
216
+ * the XDG cache and check it out detached. Idempotent; never rewrites
217
+ * an existing checkout that already sits at the pin.
218
+ *
219
+ * Two first runs against one cache are serialised: the fetch goes into a
220
+ * staging directory (`<dir>.staging-<pid>`) under a lock (`<dir>.lock/`,
221
+ * made atomically), and the finished checkout is renamed into place, so
222
+ * the pin is either absent or whole and never a directory two `git init`s
223
+ * are racing in. A process that finds the lock held waits for the holder
224
+ * and then verifies what it left. Measured on 2026-09-19: two `omakit pin`
225
+ * against one empty cache ran `git init` in the same directory, and one
226
+ * died on "cannot copy .git/description: File exists"
227
+ * (docs/evidence/ux/2026-09-19-acceptance.json, finding 6).
228
+ *
229
+ * `log` is told what is happening as `{ state, text }`: a `pass` or `info`
230
+ * line to keep, or `fetching` for the slow step about to start, which the CLI
231
+ * draws as a progress line rather than a line of output. `populate` is the
232
+ * fetch step, injectable for the tests that prove the serialisation without
233
+ * a network.
234
+ */
235
+ export function ensurePin(repoRoot, log = () => {}, env = process.env, { populate = populatePin, waitMs = PIN_WAIT_MS } = {}) {
236
+ const migration = pinMigration(repoRoot, env)
237
+ if (migration) throw migration
238
+ const dir = marketplacePinDir(repoRoot, env)
239
+ const present = () => {
240
+ if (!existsSync(join(dir, ".git")) || !hasCommit(dir)) return null
241
+ const identity = readPinIdentity(dir)
242
+ if (identity.dirty) throw new PinError(`${dir} has local modifications; remove the directory and run again`)
243
+ return identity
244
+ }
245
+ const found = present()
246
+ if (found && found.commit === MARKETPLACE_PIN.commit) {
247
+ log({ state: "pass", text: `marketplace pin ${found.commit.slice(0, 7)} present at ${dir}` })
248
+ return { dir, identity: found, fetched: false }
249
+ }
250
+ if (found) log({ state: "info", text: `${dir} is at ${found.commit}, not the pin` })
251
+ mkdirSync(dirname(dir), { recursive: true })
252
+ const lockDir = `${dir}.lock`
253
+ let release = claimLock(lockDir)
254
+ if (!release) {
255
+ // Another first run holds the lock: wait for it, then read what it left.
256
+ log({ state: "info", text: `another omakit is fetching the pin at ${dir}; waiting for it` })
257
+ const deadline = Date.now() + waitMs
258
+ while (existsSync(lockDir) && Date.now() < deadline) {
259
+ sleepMs(200)
260
+ // A holder that died mid-fetch leaves its lock; take it over.
261
+ release = claimLock(lockDir)
262
+ if (release) break
263
+ }
264
+ if (!release) {
265
+ const after = present()
266
+ if (after && after.commit === MARKETPLACE_PIN.commit) {
267
+ log({ state: "pass", text: `marketplace pin ${after.commit.slice(0, 7)} present at ${dir}, fetched by the other omakit` })
268
+ return { dir, identity: after, fetched: false, waited: true }
269
+ }
270
+ if (existsSync(lockDir)) throw new PinError(`another omakit has held the pin's lock at ${lockDir} for ${Math.round(waitMs / 1000)} s; if it is gone, remove the lock directory and run again`)
271
+ throw new PinError(`the other omakit left no pin at ${dir}; run \`omakit pin\` again`)
272
+ }
273
+ }
274
+ try {
275
+ // The lock is ours; the pin may have appeared while we waited for it.
276
+ const meanwhile = present()
277
+ if (meanwhile && meanwhile.commit === MARKETPLACE_PIN.commit) {
278
+ log({ state: "pass", text: `marketplace pin ${meanwhile.commit.slice(0, 7)} present at ${dir}, fetched by the other omakit` })
279
+ return { dir, identity: meanwhile, fetched: false, waited: true }
280
+ }
281
+ const staging = `${dir}.staging-${process.pid}`
282
+ try {
283
+ const identity = populate(staging, log)
284
+ // Into place in one rename; a checkout at another commit, or one that
285
+ // never got its HEAD, is moved aside first and removed after.
286
+ const aside = `${dir}.replaced-${process.pid}`
287
+ if (existsSync(dir)) renameSync(dir, aside)
288
+ renameSync(staging, dir)
289
+ rmSync(aside, { recursive: true, force: true })
290
+ log({ state: "pass", text: `marketplace pin ${identity.commit.slice(0, 7)} (baseline ${identity.baselineVersion}, ${identity.enforcementMode}) at ${dir}, ${pinDiskUsage(dir)}` })
291
+ return { dir, identity, fetched: true }
292
+ } finally {
293
+ rmSync(staging, { recursive: true, force: true })
294
+ }
295
+ } finally {
296
+ release()
297
+ }
179
298
  }
180
299
 
181
300
  /**
@@ -193,7 +312,7 @@ export function pinDiskUsage(dir, env = process.env) {
193
312
  // warned "cannot access '.git/index.lock'" over its momentary lock file
194
313
  // and exited 1, so doctor said "size unknown" for a checkout it had the
195
314
  // size of. Only a run that printed no total is unknown.
196
- const result = spawnSync("du", ["-skH", dir], { encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"] })
315
+ const result = spawnSync("du", ["-skH", dir], { timeout: 60_000, encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"] })
197
316
  const output = String(result.stdout || "").trim().split(/\s+/)[0]
198
317
  if (result.error || !/^\d+$/.test(output)) return "size unknown"
199
318
  const mib = Number(output) / 1024
@@ -201,11 +320,37 @@ export function pinDiskUsage(dir, env = process.env) {
201
320
  }
202
321
 
203
322
  /** True when the checkout was fetched with only PIN_PATHS, as a fresh one is. */
204
- export function pinIsSparse(dir) {
323
+ /**
324
+ * Whether the checkout is the sparse one `omakit pin` fetches: judged by
325
+ * what is on disk, the top-level entries being the pinned paths and
326
+ * nothing else, not by a git setting. Measured on 2026-09-19 by a first
327
+ * user whose freshly fetched pin was told it "predates the sparse fetch"
328
+ * and should be removed, because the answer came from `git config
329
+ * core.sparseCheckout` and that read failed on their machine while the
330
+ * tree itself was exactly the four pinned paths (docs/evidence/ux/
331
+ * 2026-09-19-first-user-test.json, finding 9). Returns the entries beyond
332
+ * the pin too, so doctor can name what a full checkout carries.
333
+ *
334
+ * @returns {{ sparse: boolean, extra: string[], sparseCheckoutConfig: boolean|null }}
335
+ */
336
+ export function pinShape(dir) {
337
+ const pinned = new Set(PIN_PATHS.map((pattern) => pattern.replace(/^\//, "").split("/")[0]))
338
+ let entries = []
205
339
  try {
206
- const enabled = execFileSync("git", ["-C", dir, "config", "--get", "core.sparseCheckout"], { encoding: "utf8" }).trim()
207
- return enabled === "true"
340
+ entries = readdirSync(dir).filter((name) => name !== ".git")
208
341
  } catch {
209
- return false
342
+ return { sparse: false, extra: [], sparseCheckoutConfig: null }
343
+ }
344
+ const extra = entries.filter((name) => !pinned.has(name)).sort()
345
+ let sparseCheckoutConfig = null
346
+ try {
347
+ sparseCheckoutConfig = execFileSync("git", ["-C", dir, "config", "--get", "core.sparseCheckout"], { timeout: 60_000, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() === "true"
348
+ } catch {
349
+ sparseCheckoutConfig = null
210
350
  }
351
+ return { sparse: entries.length > 0 && extra.length === 0, extra, sparseCheckoutConfig }
352
+ }
353
+
354
+ export function pinIsSparse(dir) {
355
+ return pinShape(dir).sparse
211
356
  }
@@ -12,14 +12,14 @@
12
12
  // makes completions load, when a new shell has no loader. Everywhere else a
13
13
  // step that is the user's to take is printed as the command, and stops.
14
14
 
15
- import { execFileSync } from "node:child_process"
16
15
  import { banner } from "./banner.mjs"
16
+ import { version } from "./doctor.mjs"
17
17
  import { credential, UNAUTHENTICATED_LIMIT } from "./github.mjs"
18
18
  import { ensurePin, marketplacePinDir, pinDiskUsage } from "./pin.mjs"
19
19
  import { progress } from "./progress.mjs"
20
- import { action, colourEnabled, GUTTER, mark, styler, wrap } from "./style.mjs"
20
+ import { action, colourEnabled, GUTTER, mark, styler, verdict, wrap } from "./style.mjs"
21
21
  import { TAGLINE } from "./usage.mjs"
22
- import { installCompletion } from "./completion.mjs"
22
+ import { completionInstall, installCompletion } from "./completion.mjs"
23
23
  import { appendLoaderBlock, completionWorks, loaderBlock, loaderBlockPresent, verifyCompletion } from "./completion-check.mjs"
24
24
  import { submissionContract } from "./form.mjs"
25
25
  import { pathHint } from "./path-hint.mjs"
@@ -51,8 +51,13 @@ export async function completionStep({ repoRoot, pin, version, stream = process.
51
51
  const contract = await submissionContract({ repoRoot })
52
52
  completion = installCompletion({ contract, pin, version, env })
53
53
  } catch (error) {
54
- step("info", `tab completion was not installed: ${error.message}`)
55
- return { state: "error", shell: null, rcAppended: false }
54
+ // A failed install is a failure, in setup's own verdict and in doctor's
55
+ // (measured on 2026-09-19: an EROFS here printed as `info`, setup ended
56
+ // READY, and doctor called the old script healthy; finding 8).
57
+ const target = completionInstall(env)
58
+ step("fail", `tab completion was not installed: ${error.message}${error.code ? ` (${error.code})` : ""}`)
59
+ if (target) fix(`Make ${target.display} writable (or remove the file there), then run \`omakit setup --completion\`; until then \`omakit doctor\` reports the script as stale.`)
60
+ return { state: "error", shell: target?.shell || null, rcAppended: false, error: error.message }
56
61
  }
57
62
  if (completion.state === "unsupported") {
58
63
  step("info", completion.shell
@@ -101,15 +106,6 @@ export async function completionStep({ repoRoot, pin, version, stream = process.
101
106
  return { state: "note", shell: completion.shell, rcAppended }
102
107
  }
103
108
 
104
- function version(command) {
105
- try {
106
- return execFileSync(command, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
107
- .trim()
108
- .split("\n")[0]
109
- } catch {
110
- return null
111
- }
112
- }
113
109
 
114
110
  /**
115
111
  * @param {{ repoRoot: string, entryPoint: string, stream?: NodeJS.WriteStream, env?: object, yes?: boolean,
@@ -192,7 +188,7 @@ export async function setup({ repoRoot, entryPoint, stream = process.stdout, env
192
188
 
193
189
  // Tab completion, installed for the shell in $SHELL where that shell loads
194
190
  // it from, and then proven in a new shell (completionStep).
195
- await completionStep({ repoRoot, pin: identity.commit, version: tool(repoRoot).version, stream, env, yes, askRc: true, input, verify })
191
+ const completion = await completionStep({ repoRoot, pin: identity.commit, version: tool(repoRoot).version, stream, env, yes, askRc: true, input, verify })
196
192
  out()
197
193
 
198
194
  out("Try it on a plugin you have checked out:")
@@ -200,5 +196,10 @@ export async function setup({ repoRoot, entryPoint, stream = process.stdout, env
200
196
  fix("omakit submit <plugin-repo> --category Widgets --tags bar,quickshell", 0)
201
197
  out()
202
198
  for (const line of wrap("It prints the issue title and body. It never posts anything.", {}, c)) out(line)
199
+ if (completion.state === "error") {
200
+ out()
201
+ for (const line of verdict("fail", "NOT READY", `tab completion was not installed (${completion.error}); everything else is in place.`, c)) out(line)
202
+ return { ok: false }
203
+ }
203
204
  return { ok: true }
204
205
  }
@@ -7,7 +7,7 @@
7
7
  import { execFileSync } from "node:child_process"
8
8
 
9
9
  function git(dir, args, encoding = "utf8") {
10
- return execFileSync("git", ["-C", dir, ...args], {
10
+ return execFileSync("git", ["-C", dir, ...args], { timeout: 60_000,
11
11
  encoding,
12
12
  maxBuffer: 256 * 1024 * 1024,
13
13
  stdio: ["ignore", "pipe", "pipe"],
@@ -126,7 +126,7 @@ function installedVersion(repoRoot) {
126
126
  /** Where the `npm` on PATH installs global packages, or null when there is no npm. */
127
127
  function npmGlobalRoot() {
128
128
  try {
129
- return resolve(execFileSync("npm", ["root", "--global"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim())
129
+ return resolve(execFileSync("npm", ["root", "--global"], { timeout: 60_000, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim())
130
130
  } catch {
131
131
  return null
132
132
  }
@@ -143,14 +143,14 @@ export const NPM_PREFIX_ARGS = Object.freeze(["prefix", "--global"])
143
143
  /** The `npm` on PATH's global prefix, or null when there is no npm. */
144
144
  export function npmGlobalPrefix() {
145
145
  try {
146
- return resolve(execFileSync("npm", [...NPM_PREFIX_ARGS], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim())
146
+ return resolve(execFileSync("npm", [...NPM_PREFIX_ARGS], { timeout: 60_000, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim())
147
147
  } catch {
148
148
  return null
149
149
  }
150
150
  }
151
151
 
152
152
  function git(dir, args) {
153
- return execFileSync("git", ["-C", dir, ...args], {
153
+ return execFileSync("git", ["-C", dir, ...args], { timeout: 300_000,
154
154
  encoding: "utf8",
155
155
  stdio: ["ignore", "pipe", "pipe"],
156
156
  }).trim()
@@ -190,7 +190,7 @@ function refreshCompletionWith(root, stream) {
190
190
  // past eighty columns in a sentence (measured in CI: 109).
191
191
  if (!existsSync(entryPoint)) return { ran: false, reason: "this install has no bin/omakit under its root" }
192
192
  try {
193
- const out = execFileSync(process.execPath, [entryPoint, ...COMPLETION_REFRESH_ARGS], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
193
+ const out = execFileSync(process.execPath, [entryPoint, ...COMPLETION_REFRESH_ARGS], { timeout: 60_000, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
194
194
  stream.write(out)
195
195
  return { ran: true, ok: true }
196
196
  } catch (error) {
@@ -348,7 +348,7 @@ async function upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, ref
348
348
  }
349
349
  spinner.phase(`npm install --global ${spec}`)
350
350
  try {
351
- execFileSync("npm", [...NPM_UPGRADE_ARGS, spec], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
351
+ execFileSync("npm", [...NPM_UPGRADE_ARGS, spec], { timeout: 600_000, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
352
352
  } catch (error) {
353
353
  spinner.done()
354
354
  const reason = String(error?.stderr || "").trim().split("\n").filter((line) => /^npm (?:error|ERR!)/.test(line)).pop() || "npm install failed"
@@ -1,4 +1,7 @@
1
- // The help text, as data rather than one painted string.
1
+ // The help text, as data rather than one painted string, in the order the
2
+ // README tells it: build (add), check (inspect, verify, submit), track
3
+ // (watch), prove (lab), then the rest; completion and COMMANDS.md follow
4
+ // the same list.
2
5
  //
3
6
  // Kept as structure so it can be coloured without pattern-matching a paragraph,
4
7
  // and so the same words serve a terminal and a pipe. A signature is coloured by
@@ -10,44 +13,60 @@ import { MARKETPLACE_PIN } from "./pin.mjs"
10
13
  import { colourEnabled, paintProse, STEP, styler, withOutputStream, wrap } from "./style.mjs"
11
14
 
12
15
  /**
13
- * "Safe" means one thing, everywhere it appears: this runs on your own
14
- * machine, posts nothing, opens no issue and spends nobody's attention. It is
15
- * never a claim about the security of a plugin or a submission; the baseline's
16
- * outcome is reported verbatim and is never restated as one.
16
+ * The line under the wordmark says what the tool is for, and only that: the
17
+ * plumbing the review blocks on, shipped as tested files a plugin owns. It is
18
+ * never a claim about the security of a plugin or a submission; the
19
+ * baseline's outcome is reported verbatim and is never restated as one, and
20
+ * a block is described by what it does and what was measured.
17
21
  */
18
- export const TAGLINE = "the safe place to find out"
22
+ export const TAGLINE = "the plumbing plugin reviews block most, built and tested once"
19
23
 
20
24
  /** The shells `omakit setup` installs tab completion for; completion.mjs holds the scripts. */
21
25
  export const COMPLETION_SHELLS = Object.freeze(["bash", "zsh", "fish"])
22
26
 
23
27
  export const COMMANDS = Object.freeze([
24
28
  {
25
- signature: "omakit setup [--yes] [--completion]",
29
+ signature: "omakit add <block> [<plugin-dir>] [--update] [--json]",
26
30
  lines: [
27
- "First run, in one command: check the environment, fetch the pinned",
28
- "marketplace checkout, install tab completion and prove it in a new shell,",
29
- "and say what to try first. Idempotent. When a new shell has no completion",
30
- "loader it asks once before adding one guarded block to the rc file; --yes",
31
- "answers for an agent. --completion is that step alone, never the question.",
31
+ "Copy a block into the plugin's omakit/ directory: `run` (Run.qml and the",
32
+ "supervisor it starts by absolute path) or `store` (Store.qml and its",
33
+ "helper, with run, which it uses), and NOTICE, each file with a header",
34
+ "naming the block, its version, the licence, the omakit commit and the",
35
+ "body's sha256. Writes those files and nothing else, never over a file",
36
+ "that is already there without --update, and never over a copy whose",
37
+ "body is not one omakit shipped: a modified block is the author's, and",
38
+ "the command says so and stops. docs/BLOCKS.md is the contract; each of",
39
+ "its lines cites how many review comments in one week asked for it (M13).",
32
40
  ],
33
41
  },
34
42
  {
35
- signature: "omakit pin",
43
+ signature: [
44
+ "omakit inspect <target> [--full] [--json] [--out <file>] [--offline]",
45
+ " [--allow-dirty]",
46
+ ],
36
47
  lines: [
37
- "Fetch or verify the pinned marketplace checkout in the user cache:",
38
- "$XDG_CACHE_HOME/omakit/marketplace, or ~/.cache/omakit/marketplace.",
39
- `Read-only, exact commit ${MARKETPLACE_PIN.commit}.`,
48
+ "What a plugin tree does, as observations: every process with its argv,",
49
+ "every host with its timeout and size-cap flags, every write with whether",
50
+ "it falls under a directory the plugin controls, every timer with its",
51
+ "interval, and the capabilities the marketplace baseline records. Below",
52
+ "the facts, the review classes the marketplace's human review raised,",
53
+ "each with its measured share, only where the tree shows the class.",
54
+ "Regular expressions over QML and shell, labelled observed; runs nothing",
55
+ "from the tree, decides nothing, exits 0 with a report and 2 when the",
56
+ "target cannot be read. The report opens with a size score, the share",
57
+ "of the tree's function lines in functions over the measured size,",
58
+ "placed among the listed trees' shares, 10.00 with no long function;",
59
+ "then what needs attention: functions over the measured size, longest",
60
+ "first, then the review classes by measured share, five sites each;",
61
+ "--full is every site with every qualifier; --json prints the document.",
40
62
  ],
41
63
  },
42
64
  {
43
- signature: [
44
- "omakit audit <plugin-id-or-dir> [--drift] [--json] [--out <file>] [--offline]",
45
- "omakit audit [--drift] [--json] [--out <file>] [--offline]",
46
- ],
65
+ signature: "omakit verify <target> [--allow-dirty] [--json] [--out <file>]",
47
66
  lines: [
48
- "Compare every installed third-party plugin's running commit with the",
49
- "exact commits the marketplace records as validated. Read-only. --drift",
50
- "shows only rows that are not validated; --offline reads the pin.",
67
+ "The official marketplace security baseline over the local Git transport,",
68
+ "reported verbatim beside the pin identity. A report for a person; --json",
69
+ "prints the document itself, and --out writes it to a file.",
51
70
  ],
52
71
  },
53
72
  {
@@ -84,48 +103,63 @@ export const COMMANDS = Object.freeze([
84
103
  ],
85
104
  },
86
105
  {
87
- signature: "omakit verify <target> [--allow-dirty] [--json] [--out <file>]",
106
+ signature: [
107
+ "omakit lab prove <suite> [--runs <n>] [--json] [--out <file>]",
108
+ "omakit lab inspect [--verify] [--json] [--out <file>]",
109
+ "omakit lab setup [--from <file>] [--toolchain <dir>] [--plugins] [--yes]",
110
+ "omakit lab prune [--keep-iso] [--records] [--yes] [--json]",
111
+ ],
88
112
  lines: [
89
- "The official marketplace security baseline over the local Git transport,",
90
- "reported verbatim beside the pin identity. A report for a person; --json",
91
- "prints the document itself, and --out writes it to a file.",
113
+ "Prove a suite in a disposable Omarchy guest, never on the desktop: the",
114
+ "pinned 4.0.3 release booted from an immutable verified base, a fresh",
115
+ "overlay per run, the guest's installed omarchy package read and printed",
116
+ "before the suite, the document written with that identity. `prove` boots",
117
+ "nothing until the base, the host and the suite's files are there, and",
118
+ "names what is missing, what it takes, and the one command; it fetches",
119
+ "nothing. `inspect` is read-only: the pinned release, its exact size,",
120
+ "digest and signer, what is on disk and verified, what the host lacks.",
121
+ "`setup` is the only path that fetches bytes: one consent naming the",
122
+ "exact size and destination (--yes for an agent), a resumable GET of",
123
+ "the pinned URL or a copy of --from, verified against the pinned SHA-256",
124
+ "and the Omarchy signature before anything boots it, then one base built",
125
+ "by the pinned omarchy-iso toolchain, whose checkout --toolchain records",
126
+ "and which setup never fetches. `prune` frees the lab cache and says how",
127
+ "much. docs/LAB.md is the contract. Suites: run, store, weigh,",
128
+ "weigh-evidence.",
92
129
  ],
93
130
  },
94
131
  {
95
132
  signature: [
96
- "omakit inspect <target> [--full] [--json] [--out <file>] [--offline]",
97
- " [--allow-dirty]",
133
+ "omakit audit <plugin-id-or-dir> [--drift] [--json] [--out <file>] [--offline]",
134
+ "omakit audit [--drift] [--json] [--out <file>] [--offline]",
98
135
  ],
99
136
  lines: [
100
- "What a plugin tree does, as observations: every process with its argv,",
101
- "every host with its timeout and size-cap flags, every write with whether",
102
- "it falls under a directory the plugin controls, every timer with its",
103
- "interval, and the capabilities the marketplace baseline records. Below",
104
- "the facts, the review classes the marketplace's human review raised,",
105
- "each with its measured share, only where the tree shows the class.",
106
- "Regular expressions over QML and shell, labelled observed; runs nothing",
107
- "from the tree, decides nothing, exits 0 with a report and 2 when the",
108
- "target cannot be read. The report opens with a size score, the share",
109
- "of the tree's function lines in functions over the measured size,",
110
- "placed among the listed trees' shares, 10.00 with no long function;",
111
- "then what needs attention: functions over the measured size, longest",
112
- "first, then the review classes by measured share, five sites each;",
113
- "--full is every site with every qualifier; --json prints the document.",
137
+ "Compare every installed third-party plugin's running commit with the",
138
+ "exact commits the marketplace records as validated. Read-only. --drift",
139
+ "shows only rows that are not validated; --offline reads the pin.",
114
140
  ],
115
141
  },
116
142
  {
117
- signature: "omakit help --agent",
118
- lines: [
119
- "The operating instructions for a coding agent, printed from skills/, so an",
120
- "agent can read the contract out of the tool instead of the repository.",
143
+ signature: [
144
+ "omakit weigh <plugin-id-or-dir> [--runs <n>] [--window <s>] [--settle <s>]",
145
+ " [--yes] [--json] [--out <file>]",
146
+ "omakit weigh --all",
147
+ "omakit weigh --list [--json]",
121
148
  ],
122
- },
123
- {
124
- signature: "omakit upgrade [--dry-run]",
125
149
  lines: [
126
- "Update omakit through the installer that made it: npm, at the exact",
127
- "version the registry names, or a fast-forward of a clone. Refuses",
128
- "anything else, and never moves the marketplace pin.",
150
+ "What a plugin weighs on the shell, measured: the shell is restarted",
151
+ "without it and with it, several runs, and the difference is the weight,",
152
+ "with the baseline's own spread as the noise floor; memory is printed as",
153
+ "the shell's own startup variance, CPU and child processes as the weight.",
154
+ "The one command that changes your machine: it edits shell.json for the",
155
+ "duration, backs it up first, restores it on every exit path, and asks",
156
+ "before the first restart (--yes answers for you): about a minute per",
157
+ "restart, six restarts for one plugin at three runs. --all weighs every",
158
+ "enabled third-party plugin and is sized for a lab machine, not a working",
159
+ "desktop. Writes the document to --out, by default",
160
+ "$XDG_STATE_HOME/omakit/weigh/<date>.json, and ends with the sentence",
161
+ "for the plugin's README. --list is read-only: every installed plugin",
162
+ "and when it was last weighed, unweighed enabled plugins first.",
129
163
  ],
130
164
  },
131
165
  {
@@ -137,6 +171,32 @@ export const COMMANDS = Object.freeze([
137
171
  "checks at most once daily; DISABLE_UPDATE_NOTIFIER=1 disables notices.",
138
172
  ],
139
173
  },
174
+ {
175
+ signature: "omakit setup [--yes] [--completion]",
176
+ lines: [
177
+ "First run, in one command: check the environment, fetch the pinned",
178
+ "marketplace checkout, install tab completion and prove it in a new shell,",
179
+ "and say what to try first. Idempotent. When a new shell has no completion",
180
+ "loader it asks once before adding one guarded block to the rc file; --yes",
181
+ "answers for an agent. --completion is that step alone, never the question.",
182
+ ],
183
+ },
184
+ {
185
+ signature: "omakit pin",
186
+ lines: [
187
+ "Fetch or verify the pinned marketplace checkout in the user cache:",
188
+ "$XDG_CACHE_HOME/omakit/marketplace, or ~/.cache/omakit/marketplace.",
189
+ `Read-only, exact commit ${MARKETPLACE_PIN.commit}.`,
190
+ ],
191
+ },
192
+ {
193
+ signature: "omakit upgrade [--dry-run]",
194
+ lines: [
195
+ "Update omakit through the installer that made it: npm, at the exact",
196
+ "version the registry names, or a fast-forward of a clone. Refuses",
197
+ "anything else, and never moves the marketplace pin.",
198
+ ],
199
+ },
140
200
  {
141
201
  signature: "omakit parity [--count <n>] [--offset <n>] [--out <file>]",
142
202
  lines: [
@@ -145,26 +205,10 @@ export const COMMANDS = Object.freeze([
145
205
  ],
146
206
  },
147
207
  {
148
- signature: [
149
- "omakit weigh <plugin-id-or-dir> [--runs <n>] [--window <s>] [--settle <s>]",
150
- " [--yes] [--json] [--out <file>]",
151
- "omakit weigh --all",
152
- "omakit weigh --list [--json]",
153
- ],
208
+ signature: "omakit help --agent",
154
209
  lines: [
155
- "What a plugin weighs on the shell, measured: the shell is restarted",
156
- "without it and with it, several runs, and the difference is the weight,",
157
- "with the baseline's own spread as the noise floor; memory is printed as",
158
- "the shell's own startup variance, CPU and child processes as the weight.",
159
- "The one command that changes your machine: it edits shell.json for the",
160
- "duration, backs it up first, restores it on every exit path, and asks",
161
- "before the first restart (--yes answers for you): about a minute per",
162
- "restart, six restarts for one plugin at three runs. --all weighs every",
163
- "enabled third-party plugin and is sized for a lab machine, not a working",
164
- "desktop. Writes the document to --out, by default",
165
- "$XDG_STATE_HOME/omakit/weigh/<date>.json, and ends with the sentence",
166
- "for the plugin's README. --list is read-only: every installed plugin",
167
- "and when it was last weighed, unweighed enabled plugins first.",
210
+ "The operating instructions for a coding agent, printed from skills/, so an",
211
+ "agent can read the contract out of the tool instead of the repository.",
168
212
  ],
169
213
  },
170
214
  ])