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.
- package/README.md +38 -45
- package/blocks/history.json +68 -0
- package/blocks/run/NOTICE +12 -0
- package/blocks/run/Run.qml +242 -0
- package/blocks/run/run-supervisor.py +522 -0
- package/blocks/store/NOTICE +12 -0
- package/blocks/store/Store.qml +157 -0
- package/blocks/store/store-helper.py +431 -0
- package/package.json +12 -5
- package/skills/omarchy-plugin-audit/SKILL.md +11 -5
- package/skills/omarchy-plugin-build/SKILL.md +164 -0
- package/skills/omarchy-plugin-check/SKILL.md +6 -3
- package/skills/omarchy-plugin-submit/SKILL.md +4 -1
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -2
- package/skills/omarchy-plugin-weigh/SKILL.md +13 -2
- package/tests/fixtures/weigh/clean/Widget.qml +19 -0
- package/tests/fixtures/weigh/clean/manifest.json +9 -0
- package/tests/fixtures/weigh/clean/tests/harness.qml +7 -0
- package/tests/fixtures/weigh/idle-panel/Panel.qml +65 -0
- package/tests/fixtures/weigh/idle-panel/manifest.json +9 -0
- package/tests/fixtures/weigh/poller/Service.qml +50 -0
- package/tests/fixtures/weigh/poller/manifest.json +9 -0
- package/tests/fixtures/weigh/timer-180ms/Widget.qml +25 -0
- package/tests/fixtures/weigh/timer-180ms/manifest.json +9 -0
- package/tests/lab/run/harness/scenarios/controls.sh +6 -0
- package/tests/lab/run/harness/scenarios/envprobe.sh +10 -0
- package/tests/lab/run/harness/scenarios/forge.sh +11 -0
- package/tests/lab/run/harness/scenarios/holder.sh +5 -0
- package/tests/lab/run/harness/scenarios/orphan.sh +6 -0
- package/tests/lab/run/harness/scenarios/stall.sh +5 -0
- package/tests/lab/run/harness/scenarios/stubborn.sh +5 -0
- package/tests/lab/run/harness/scenarios/tree.sh +7 -0
- package/tests/lab/run/harness/shell.qml +84 -0
- package/tests/lab/run/report.py +217 -0
- package/tests/lab/run/suite.sh +106 -0
- package/tests/lab/store/harness/shell.qml +73 -0
- package/tests/lab/store/report.py +133 -0
- package/tests/lab/store/suite.sh +109 -0
- package/tests/parity/corpus.mjs +8 -3
- package/tests/parity/run.mjs +4 -4
- package/tools/audit/audit.mjs +17 -6
- package/tools/audit/git.mjs +3 -3
- package/tools/audit/report.mjs +31 -5
- package/tools/blocks/add.mjs +138 -0
- package/tools/blocks/commit.json +5 -0
- package/tools/blocks/record-commit.mjs +77 -0
- package/tools/blocks/registry.mjs +191 -0
- package/tools/blocks/stamp.mjs +61 -0
- package/tools/inspect/contract.mjs +36 -5
- package/tools/inspect/helpers.mjs +217 -0
- package/tools/inspect/inspect.mjs +68 -3
- package/tools/inspect/patterns.mjs +18 -3
- package/tools/inspect/processes.mjs +38 -5
- package/tools/inspect/report.mjs +18 -2
- package/tools/inspect/writes.mjs +22 -4
- package/tools/lab/guest.mjs +155 -0
- package/tools/lab/harness.sh +119 -0
- package/tools/lab/host.mjs +177 -0
- package/tools/lab/inspect.mjs +240 -0
- package/tools/lab/omarchy.gpg +13 -0
- package/tools/lab/patches/omarchy-iso-test.patch +351 -0
- package/tools/lab/paths.mjs +173 -0
- package/tools/lab/pin.json +42 -0
- package/tools/lab/pin.mjs +64 -0
- package/tools/lab/prune.mjs +68 -0
- package/tools/lab/qemu.mjs +153 -0
- package/tools/lab/qmp-cli.mjs +21 -0
- package/tools/lab/report.mjs +183 -0
- package/tools/lab/run.mjs +344 -0
- package/tools/lab/setup.mjs +430 -0
- package/tools/lab/suites/run.sh +35 -0
- package/tools/lab/suites/store.sh +41 -0
- package/tools/lab/suites/weigh.sh +196 -0
- package/tools/lab/suites.mjs +142 -0
- package/tools/lab/verify.mjs +134 -0
- package/tools/marketplace/README.md +38 -1
- package/tools/marketplace/banner.mjs +23 -2
- package/tools/marketplace/cli.mjs +449 -146
- package/tools/marketplace/completion-check.mjs +27 -1
- package/tools/marketplace/completion.mjs +32 -4
- package/tools/marketplace/doctor.mjs +47 -9
- package/tools/marketplace/github.mjs +52 -6
- package/tools/marketplace/local-transport.mjs +1 -1
- package/tools/marketplace/options.mjs +16 -5
- package/tools/marketplace/outcome.mjs +244 -0
- package/tools/marketplace/pin.mjs +178 -33
- package/tools/marketplace/setup.mjs +16 -15
- package/tools/marketplace/tree.mjs +1 -1
- package/tools/marketplace/upgrade.mjs +5 -5
- package/tools/marketplace/usage.mjs +116 -72
- package/tools/subject/resolve.mjs +19 -6
- package/tools/weigh/audit.mjs +47 -16
- package/tools/weigh/config.mjs +105 -24
- 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
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* never a claim about the security of a plugin or a submission; the
|
|
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
|
|
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
|
|
29
|
+
signature: "omakit add <block> [<plugin-dir>] [--update] [--json]",
|
|
26
30
|
lines: [
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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:
|
|
43
|
+
signature: [
|
|
44
|
+
"omakit inspect <target> [--full] [--json] [--out <file>] [--offline]",
|
|
45
|
+
" [--allow-dirty]",
|
|
46
|
+
],
|
|
36
47
|
lines: [
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
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
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
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:
|
|
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
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
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
|
|
97
|
-
"
|
|
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
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
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:
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
"
|
|
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
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
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
|
-
"
|
|
156
|
-
"
|
|
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
|
])
|