omakit 0.5.0 → 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 +39 -32
- 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 +7 -4
- 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 +37 -6
- package/tools/inspect/functions.mjs +236 -12
- package/tools/inspect/helpers.mjs +217 -0
- package/tools/inspect/inspect.mjs +71 -5
- package/tools/inspect/measure-functions.mjs +12 -3
- package/tools/inspect/patterns.mjs +35 -18
- package/tools/inspect/processes.mjs +38 -5
- package/tools/inspect/report.mjs +33 -6
- 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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/bash
|
|
2
|
+
# `omakit weigh` against a real shell, in the guest, never on a desktop:
|
|
3
|
+
# the command restarts a shell, and the lab is the only shell that is
|
|
4
|
+
# anyone's to restart unasked.
|
|
5
|
+
#
|
|
6
|
+
# omakit_lab_suite <mode> <runs> <plugins-dir> [listed-id...]
|
|
7
|
+
#
|
|
8
|
+
# `smoke` proves what no unit test can, in about a minute after the guest
|
|
9
|
+
# is up: a real shell restarts on a written configuration, the real /proc
|
|
10
|
+
# is read (a Pss, ticks that move), the document follows the contract,
|
|
11
|
+
# shell.json comes back byte for byte, and an interrupt mid-measurement
|
|
12
|
+
# restores it and exits 130. Two fixtures at one run, a 2 s settle and a
|
|
13
|
+
# 3 s window, three restarts, then a second measurement interrupted.
|
|
14
|
+
# `evidence` is the five-run gate over the four fixtures and the three
|
|
15
|
+
# listed plugins, forty restarts at the default 30 s settle and 15 s
|
|
16
|
+
# window, about forty minutes, and the only mode whose document belongs
|
|
17
|
+
# under docs/evidence/weigh/. The listed plugins come staged from the
|
|
18
|
+
# lab's plugin cache, fetched once by `omakit lab setup --plugins` at the
|
|
19
|
+
# exact commit the pinned catalog records as validated; this suite fetches
|
|
20
|
+
# nothing.
|
|
21
|
+
#
|
|
22
|
+
# Runs through tools/lab/harness.sh, which defines the helpers used here.
|
|
23
|
+
|
|
24
|
+
omakit_lab_suite() {
|
|
25
|
+
local mode="$1" runs="$2" plugins_dir="$3"
|
|
26
|
+
shift 3
|
|
27
|
+
local -a reals=("$@") fixtures timing ids
|
|
28
|
+
local fixtures_dir="$OMAKIT_DIR/tests/fixtures/weigh"
|
|
29
|
+
local name real real_dir md5_before md5_after count restarts status ids_json
|
|
30
|
+
# Node in the guest comes through mise, the way a stock Omarchy has it.
|
|
31
|
+
local guest_path='PATH=$HOME/.local/share/mise/shims:$PATH'
|
|
32
|
+
|
|
33
|
+
if [[ $mode == evidence ]]; then
|
|
34
|
+
fixtures=(clean timer-180ms poller idle-panel)
|
|
35
|
+
timing=()
|
|
36
|
+
else
|
|
37
|
+
runs=1
|
|
38
|
+
reals=()
|
|
39
|
+
fixtures=(clean timer-180ms)
|
|
40
|
+
timing=(--settle 2 --window 3)
|
|
41
|
+
fi
|
|
42
|
+
ids=("${fixtures[@]/#/fixture.}" "${reals[@]}")
|
|
43
|
+
count=${#ids[@]}
|
|
44
|
+
restarts=$(( (1 + count) * runs ))
|
|
45
|
+
ids_json=$(jq -c -n '$ARGS.positional' --args "${ids[@]}")
|
|
46
|
+
|
|
47
|
+
# One detached measurement in the guest: the shell it restarts is the
|
|
48
|
+
# session's, so it must not hang off the ssh call.
|
|
49
|
+
guest_weigh() {
|
|
50
|
+
local tag="$1" limit="$2"; shift 2
|
|
51
|
+
guest_job "weigh-$tag" "$limit" "$guest_path node /tmp/omakit/bin/omakit weigh $* --yes --out /tmp/omakit-weigh-$tag.json" || return 1
|
|
52
|
+
guest_file "/tmp/omakit-weigh-$tag.json" "omakit-weigh-$tag.json" || true
|
|
53
|
+
}
|
|
54
|
+
# shell.json is byte for byte what it was, no backup is left, the shell answers.
|
|
55
|
+
assert_restored() {
|
|
56
|
+
md5_after=$(ssh_session "md5sum < \"\$HOME/.config/omarchy/shell.json\"")
|
|
57
|
+
log "shell.json after $1: $md5_after"
|
|
58
|
+
[[ $md5_after == "$md5_before" ]] || { echo "shell.json changed ($1)" >&2; return 1; }
|
|
59
|
+
ssh_session "test -z \"\$(ls \"\$HOME/.config/omarchy/\" | grep omakit-backup)\"" || { echo "a backup was left behind ($1)" >&2; return 1; }
|
|
60
|
+
ssh_session "omarchy-shell shell ping | grep -qx ok" || { echo "the shell does not answer ($1)" >&2; return 1; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log "Checking that the guest has node"
|
|
64
|
+
ssh_session "$guest_path node --version" || { echo "no node in the guest (mise shims)" >&2; return 1; }
|
|
65
|
+
|
|
66
|
+
log "Staging omakit, the ${#fixtures[@]} fixtures and ${#reals[@]} listed plugins in the guest"
|
|
67
|
+
ssh_guest "rm -rf /tmp/omakit" || return 1
|
|
68
|
+
stage_paths "$OMAKIT_DIR" /tmp/omakit bin tools package.json || return 1
|
|
69
|
+
for name in "${fixtures[@]}"; do stage_plugin "$fixtures_dir/$name" "/tmp/omakit-fixtures/$name" || return 1; done
|
|
70
|
+
for real in "${reals[@]}"; do
|
|
71
|
+
real_dir="$plugins_dir/$real"
|
|
72
|
+
log "$real at $(git -C "$real_dir" rev-parse HEAD), the commit the pinned catalog lists as validated"
|
|
73
|
+
stage_plugin "$real_dir" "/tmp/omakit-real/$real" || return 1
|
|
74
|
+
done
|
|
75
|
+
for name in "${fixtures[@]}"; do ssh_session "omarchy-plugin-add /tmp/omakit-fixtures/$name --enable --yes" || return 1; done
|
|
76
|
+
for real in "${reals[@]}"; do ssh_session "omarchy-plugin-add /tmp/omakit-real/$real --enable --yes" || return 1; done
|
|
77
|
+
wait_for_guest_state "the $count plugins are installed and enabled" 60 ssh_session \
|
|
78
|
+
"omarchy-plugin-list --json | jq -e --argjson want '$ids_json' '
|
|
79
|
+
. as \$list | all(\$want[]; . as \$id | any(\$list[]; .id == \$id and .enabled == true))'" || return 1
|
|
80
|
+
capture_console "success-weigh-01-installed"
|
|
81
|
+
|
|
82
|
+
md5_before=$(ssh_session "md5sum < \"\$HOME/.config/omarchy/shell.json\"")
|
|
83
|
+
[[ -n $md5_before ]] || return 1
|
|
84
|
+
log "shell.json before: $md5_before"
|
|
85
|
+
|
|
86
|
+
# The plan, refused: without --yes in a pipe the command prints the count
|
|
87
|
+
# and touches nothing. Exit 2 is the expected outcome.
|
|
88
|
+
ssh_session "$guest_path node /tmp/omakit/bin/omakit weigh --all --runs $runs ${timing[*]} </dev/null; test \$? -eq 2" || { echo "the unconfirmed run did not refuse with exit 2" >&2; return 1; }
|
|
89
|
+
[[ $(ssh_session "md5sum < \"\$HOME/.config/omarchy/shell.json\"") == "$md5_before" ]] || { echo "the refused run touched shell.json" >&2; return 1; }
|
|
90
|
+
|
|
91
|
+
if [[ $mode == evidence ]]; then
|
|
92
|
+
log "Running omakit weigh --all --runs $runs in the guest ($restarts restarts, the default 30 s settle and 15 s window)"
|
|
93
|
+
guest_weigh evidence 3600 --all --runs "$runs" || return 1
|
|
94
|
+
status=$(guest_job_status weigh-evidence)
|
|
95
|
+
# The shell's own journal over the run, for C2: a rescan or a bar rebuild between two levels.
|
|
96
|
+
ssh_session "journalctl --user -t omarchy-shell --no-pager -o short-iso --since '-2 hours'" > "$RUN_DIR/omarchy-shell.journal" 2>/dev/null || true
|
|
97
|
+
ssh_session "ls \$XDG_RUNTIME_DIR/omarchy/plugin-runtime/" > "$RUN_DIR/plugin-runtime-generations.txt" 2>/dev/null || true
|
|
98
|
+
cat "$RUN_DIR/weigh-evidence.log" > "$RUN_DIR/omakit-weigh.log" && cat "$RUN_DIR/omakit-weigh-evidence.json" > "$RUN_DIR/omakit-weigh.json" || return 1
|
|
99
|
+
else
|
|
100
|
+
log "Running omakit weigh --all --runs 1 --settle 2 --window 3 in the guest ($restarts restarts)"
|
|
101
|
+
guest_weigh smoke 300 --all --runs 1 "${timing[@]}" || return 1
|
|
102
|
+
status=$(guest_job_status weigh-smoke)
|
|
103
|
+
cat "$RUN_DIR/weigh-smoke.log" > "$RUN_DIR/omakit-weigh.log" && cat "$RUN_DIR/omakit-weigh-smoke.json" > "$RUN_DIR/omakit-weigh.json" || return 1
|
|
104
|
+
fi
|
|
105
|
+
[[ $status == 0 ]] || { echo "omakit weigh exited $status" >&2; tail -n 40 "$RUN_DIR/omakit-weigh.log" >&2; return 1; }
|
|
106
|
+
assert_restored "the measurement" || return 1
|
|
107
|
+
capture_console "success-weigh-02-restored"
|
|
108
|
+
|
|
109
|
+
# The document follows the contract, on the host, with the tool's own validator.
|
|
110
|
+
node "$OMAKIT_DIR/tools/weigh/contract.mjs" "$RUN_DIR/omakit-weigh.json" || return 1
|
|
111
|
+
|
|
112
|
+
if [[ $mode == smoke ]]; then
|
|
113
|
+
# Every configuration produced a sample from the real /proc: a Pss and a
|
|
114
|
+
# VmRSS in kB, ticks that did not go backwards, and a trace; nothing
|
|
115
|
+
# failed and nothing was read as zero in a gone shell's place.
|
|
116
|
+
jq -e --arg before "${md5_before%% *}" --argjson count "$count" '
|
|
117
|
+
(.failedRuns | length) == 0
|
|
118
|
+
and (.plugins | length) == $count
|
|
119
|
+
and all(.plugins[]; .runsCompleted == 1)
|
|
120
|
+
and ([.baseline.runs[], .plugins[].runs[]] | length) == ($count + 1)
|
|
121
|
+
and all([.baseline.runs[], .plugins[].runs[]][];
|
|
122
|
+
(.shell.pssKb | type) == "number" and .shell.pssKb > 0
|
|
123
|
+
and (.shell.rssKb | type) == "number" and .shell.rssKb > 0
|
|
124
|
+
and .shell.cpuTicksEnd >= .shell.cpuTicksStart
|
|
125
|
+
and (.shell.trace | length) >= 2
|
|
126
|
+
and .readyAfterSeconds >= 0)
|
|
127
|
+
and .config.restored == true
|
|
128
|
+
and .config.md5Before == $before and .config.md5After == $before' "$RUN_DIR/omakit-weigh.json" || {
|
|
129
|
+
jq -c '{failedRuns, config, plugins: [.plugins[] | {id, runsCompleted}]}' "$RUN_DIR/omakit-weigh.json" >&2
|
|
130
|
+
return 1
|
|
131
|
+
}
|
|
132
|
+
grep -q "^▒ WEIGHED" "$RUN_DIR/omakit-weigh.log" || { echo "a one-run measurement does not close with the question mark" >&2; return 1; }
|
|
133
|
+
|
|
134
|
+
# The recovery path, against the real shell: a measurement interrupted
|
|
135
|
+
# inside its settle restores shell.json, restarts the shell once more,
|
|
136
|
+
# removes the backup and exits 130 (docs/WEIGH.md, the shell.json
|
|
137
|
+
# mutation). The signal goes to the recorded pid, the way Ctrl-C would
|
|
138
|
+
# reach the command at a terminal. `set -m` because a background job
|
|
139
|
+
# under a non-interactive bash inherits SIGINT ignored (measured: a
|
|
140
|
+
# `kill -INT` to such a job did nothing).
|
|
141
|
+
log "Interrupting a second measurement inside its settle"
|
|
142
|
+
ssh_session "rm -f /tmp/omakit-weigh-interrupted.done /tmp/omakit-weigh-interrupted.log /tmp/omakit-weigh-interrupted.json /tmp/omakit-weigh-interrupted.pid; \
|
|
143
|
+
setsid bash -c 'set -m; $guest_path node /tmp/omakit/bin/omakit weigh fixture.clean --runs 2 --settle 8 --window 5 --yes --out /tmp/omakit-weigh-interrupted.json \
|
|
144
|
+
> /tmp/omakit-weigh-interrupted.log 2>&1 & echo \$! > /tmp/omakit-weigh-interrupted.pid; wait \$!; echo \$? > /tmp/omakit-weigh-interrupted.done' >/dev/null 2>&1 < /dev/null &" || return 1
|
|
145
|
+
wait_for_guest_state "the interrupted measurement has backed shell.json up" 60 ssh_guest "grep -q 'backed up to' /tmp/omakit-weigh-interrupted.log" || return 1
|
|
146
|
+
# The first restart takes about a second in the guest and the settle is
|
|
147
|
+
# 8 s, so five seconds after the backup line the run is inside its
|
|
148
|
+
# settle, or still waiting for the shell; both paths restore.
|
|
149
|
+
sleep 5
|
|
150
|
+
ssh_guest "kill -INT \$(cat /tmp/omakit-weigh-interrupted.pid)" || { echo "no measurement to interrupt" >&2; return 1; }
|
|
151
|
+
wait_for_guest_state "the interrupted measurement has exited" 120 ssh_guest "test -f /tmp/omakit-weigh-interrupted.done" || {
|
|
152
|
+
ssh_guest "tail -n 40 /tmp/omakit-weigh-interrupted.log" || true
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
guest_file /tmp/omakit-weigh-interrupted.log omakit-weigh-interrupted.log || true
|
|
156
|
+
status=$(ssh_guest "cat /tmp/omakit-weigh-interrupted.done")
|
|
157
|
+
[[ $status == 130 ]] || { echo "the interrupted measurement exited $status, not 130" >&2; tail -n 40 "$RUN_DIR/omakit-weigh-interrupted.log" >&2; return 1; }
|
|
158
|
+
grep -q "interrupted: restoring shell.json before exiting" "$RUN_DIR/omakit-weigh-interrupted.log" || { echo "the interrupt was not announced" >&2; return 1; }
|
|
159
|
+
grep -q "restored and verified" "$RUN_DIR/omakit-weigh-interrupted.log" || { echo "the restore after the interrupt did not verify" >&2; return 1; }
|
|
160
|
+
grep -q "NOT WEIGHED interrupted before the measurement completed" "$RUN_DIR/omakit-weigh-interrupted.log" || { echo "the interrupt did not close as NOT WEIGHED" >&2; return 1; }
|
|
161
|
+
ssh_guest "test ! -f /tmp/omakit-weigh-interrupted.json" || { echo "an interrupted measurement wrote a document" >&2; return 1; }
|
|
162
|
+
assert_restored "the interrupt" || return 1
|
|
163
|
+
capture_console "success-weigh-03-interrupt-restored"
|
|
164
|
+
guest_shell_healthy || return 1
|
|
165
|
+
printf 'ok - omakit weigh smoke: %d measured restarts and a restore weighed two fixtures from the real /proc and put shell.json back byte for byte, the document follows docs/WEIGH.md, and an interrupted measurement restored it and exited 130\n' "$restarts"
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# The acceptance line: the busy fixture is above noise on CPU; the clean
|
|
170
|
+
# fixture is within noise on both and the report says so in words; every
|
|
171
|
+
# plugin completed every run; the md5s in the document match the guest's.
|
|
172
|
+
jq -e --arg before "${md5_before%% *}" --argjson runs "$runs" --argjson count "$count" '
|
|
173
|
+
(.plugins[] | select(.id == "fixture.clean")) as $clean
|
|
174
|
+
| (.plugins[] | select(.id == "fixture.timer-180ms")) as $busy
|
|
175
|
+
| all(.plugins[]; .runsCompleted == $runs)
|
|
176
|
+
and $busy.verdict.cpu == "above-noise"
|
|
177
|
+
and $clean.verdict.cpu == "within-noise"
|
|
178
|
+
and $clean.verdict.memory == "within-noise"
|
|
179
|
+
and $clean.verdict.summary == "no measurable CPU"
|
|
180
|
+
and .config.restored == true
|
|
181
|
+
and .config.md5Before == $before and .config.md5After == $before
|
|
182
|
+
and (.plugins | length) == $count' "$RUN_DIR/omakit-weigh.json" || {
|
|
183
|
+
jq -c '.noiseFloor' "$RUN_DIR/omakit-weigh.json" >&2
|
|
184
|
+
jq -c '.plugins[] | {id, runsCompleted, shellPssMb, shellCpuPercent, verdict}' "$RUN_DIR/omakit-weigh.json" >&2
|
|
185
|
+
return 1
|
|
186
|
+
}
|
|
187
|
+
grep -Eq "^▁ ok +fixture\.clean " "$RUN_DIR/omakit-weigh.log" || { echo "the report does not mark fixture.clean ok" >&2; return 1; }
|
|
188
|
+
grep -A1 -E "ok +fixture\.clean " "$RUN_DIR/omakit-weigh.log" | grep -q "no measurable CPU" || { echo "the report does not say no measurable CPU for fixture.clean" >&2; return 1; }
|
|
189
|
+
grep -q "^memory within the shell's own startup variance" "$RUN_DIR/omakit-weigh.log" || { echo "the header does not label memory as the shell's" >&2; return 1; }
|
|
190
|
+
grep -q "^for the README" "$RUN_DIR/omakit-weigh.log" || { echo "no README sentence" >&2; return 1; }
|
|
191
|
+
|
|
192
|
+
jq -r '.noiseFloor | "noise floor: Pss \(.pssMb) MB and VmRSS \(.rssMb) MB at the end of the window, Pss \(.pssMbSettled) MB at the settle, CPU \(.cpuPercent)%"' "$RUN_DIR/omakit-weigh.json"
|
|
193
|
+
jq -r '.plugins[] | [.id, (.shellPssMb.median * 100 | round / 100), (.shellPssMb.spread * 100 | round / 100), (.shellCpuPercent.median * 100 | round / 100), (.shellCpuPercent.spread * 100 | round / 100), (.childRssMb.median * 100 | round / 100), (.childCpuPercent.median * 100 | round / 100), .childSpawns.median, .verdict.summary] | @tsv' "$RUN_DIR/omakit-weigh.json"
|
|
194
|
+
guest_shell_healthy || return 1
|
|
195
|
+
printf 'ok - omakit weigh measured %d plugins over %d runs each, told the 180 ms timer from the clean fixture, said within noise in words, restored shell.json byte for byte, and the document follows docs/WEIGH.md\n' "$count" "$runs"
|
|
196
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// The suites `omakit lab prove` knows, as data: what each stages, what it
|
|
2
|
+
// runs, which document it writes, and what that document must say for the
|
|
3
|
+
// gate to pass. The host-side body of each is a bash file under
|
|
4
|
+
// tools/lab/suites/, run through tools/lab/harness.sh; the in-guest
|
|
5
|
+
// content (a suite that also runs on the desktop, its reader, its harness
|
|
6
|
+
// QML, the weigh fixtures) lives with the tests, under tests/lab/ and
|
|
7
|
+
// tests/fixtures/weigh/, and ships in the package too (package.json
|
|
8
|
+
// `files`), so an installed omakit proves a suite without a checkout.
|
|
9
|
+
// Measured on 2026-09-19 by a first user: the packaged `lab prove` refused
|
|
10
|
+
// for files that were repository-only, with a remedy that cloned the
|
|
11
|
+
// repository and then ran the global package again (docs/evidence/ux/
|
|
12
|
+
// 2026-09-19-first-user-test.json, finding 5). A suite whose content is
|
|
13
|
+
// missing is still named, not guessed at, and the remedy names the tree's
|
|
14
|
+
// own entry point.
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
17
|
+
import { join } from "node:path"
|
|
18
|
+
import { LAB_DIR } from "./pin.mjs"
|
|
19
|
+
import { marketplacePinDir } from "../marketplace/pin.mjs"
|
|
20
|
+
|
|
21
|
+
/** The listed plugins the weigh evidence gate weighs beside the fixtures, by id; their repositories and commits are the pinned catalog's to say. */
|
|
22
|
+
export const WEIGH_LISTED = Object.freeze(["io.github.calebhat.weather", "omaplug", "io.github.pablo-merino.altswitch"])
|
|
23
|
+
export const WEIGH_FIXTURES = Object.freeze(["clean", "timer-180ms", "poller", "idle-panel"])
|
|
24
|
+
|
|
25
|
+
const BLOCK_PLUMBING = ["tests/lab/run/suite.sh", "tests/lab/run/report.py", "tests/lab/run/harness/shell.qml", "blocks/run/Run.qml", "blocks/run/run-supervisor.py"]
|
|
26
|
+
|
|
27
|
+
export const SUITES = Object.freeze({
|
|
28
|
+
run: Object.freeze({
|
|
29
|
+
name: "run",
|
|
30
|
+
title: "the Run block's lab scenarios on the stock guest",
|
|
31
|
+
host: join(LAB_DIR, "suites/run.sh"),
|
|
32
|
+
needs: Object.freeze(BLOCK_PLUMBING),
|
|
33
|
+
document: "runlab.json",
|
|
34
|
+
evidence: "run-lab-guest",
|
|
35
|
+
timeoutSeconds: 1200,
|
|
36
|
+
args: () => ["1"],
|
|
37
|
+
/** `ok` over 19 summary entries (one per scenario, keyed by name), the count tests/lab/run/suite.sh names in its scenario list. */
|
|
38
|
+
assert: (document) => {
|
|
39
|
+
const summary = document?.summary
|
|
40
|
+
const count = Array.isArray(summary) ? summary.length : summary && typeof summary === "object" ? Object.keys(summary).length : 0
|
|
41
|
+
if (document?.ok !== true || count !== 19) return { ok: false, reason: `the document says ok=${document?.ok} over ${count} scenarios; the gate is ok over 19` }
|
|
42
|
+
return { ok: true, reason: `ok over ${count} scenarios` }
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
store: Object.freeze({
|
|
46
|
+
name: "store",
|
|
47
|
+
title: "the Store block's lab scenarios on the stock guest, the foreign owner simulated",
|
|
48
|
+
host: join(LAB_DIR, "suites/store.sh"),
|
|
49
|
+
needs: Object.freeze([...BLOCK_PLUMBING, "tests/lab/store/suite.sh", "tests/lab/store/report.py", "tests/lab/store/harness/shell.qml", "blocks/store/Store.qml", "blocks/store/store-helper.py"]),
|
|
50
|
+
document: "storelab.json",
|
|
51
|
+
evidence: "store-lab-guest",
|
|
52
|
+
timeoutSeconds: 1200,
|
|
53
|
+
args: () => [],
|
|
54
|
+
/** `ok` over 15 scenarios with `foreign-owner` not skipped: the chown happened. */
|
|
55
|
+
assert: (document) => {
|
|
56
|
+
const scenarios = Array.isArray(document?.scenarios) ? document.scenarios : []
|
|
57
|
+
const foreign = scenarios.find((row) => row?.scenario === "foreign-owner")
|
|
58
|
+
if (document?.ok !== true || scenarios.length !== 15) return { ok: false, reason: `the document says ok=${document?.ok} over ${scenarios.length} scenarios; the gate is ok over 15` }
|
|
59
|
+
if (!foreign || foreign.skipped != null) return { ok: false, reason: "the foreign-owner scenario was skipped; the gate needs it simulated" }
|
|
60
|
+
return { ok: true, reason: `ok over ${scenarios.length} scenarios, the foreign owner simulated` }
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
weigh: Object.freeze({
|
|
64
|
+
name: "weigh",
|
|
65
|
+
title: "omakit weigh against the stock shell: the smoke check, about a minute after the guest is up",
|
|
66
|
+
host: join(LAB_DIR, "suites/weigh.sh"),
|
|
67
|
+
needs: Object.freeze(["bin/omakit", "tools/weigh/contract.mjs", "tests/fixtures/weigh/clean/manifest.json", "tests/fixtures/weigh/timer-180ms/manifest.json"]),
|
|
68
|
+
document: "omakit-weigh.json",
|
|
69
|
+
evidence: null,
|
|
70
|
+
timeoutSeconds: 1500,
|
|
71
|
+
args: (options, layout) => ["smoke", "1", layout.plugins],
|
|
72
|
+
assert: (document) => (document?.config?.restored === true ? { ok: true, reason: "the document says the shell configuration was restored" } : { ok: false, reason: "the document does not say the shell configuration was restored" }),
|
|
73
|
+
}),
|
|
74
|
+
"weigh-evidence": Object.freeze({
|
|
75
|
+
name: "weigh-evidence",
|
|
76
|
+
title: "omakit weigh against the stock shell: five runs over four fixtures and three listed plugins, about forty minutes",
|
|
77
|
+
host: join(LAB_DIR, "suites/weigh.sh"),
|
|
78
|
+
needs: Object.freeze(["bin/omakit", "tools/weigh/contract.mjs", ...WEIGH_FIXTURES.map((name) => `tests/fixtures/weigh/${name}/manifest.json`)]),
|
|
79
|
+
document: "omakit-weigh.json",
|
|
80
|
+
evidence: "weigh-lab",
|
|
81
|
+
timeoutSeconds: 4200,
|
|
82
|
+
plugins: WEIGH_LISTED,
|
|
83
|
+
args: (options, layout) => ["evidence", String(options.runs || 5), layout.plugins, ...WEIGH_LISTED],
|
|
84
|
+
assert: (document) => (document?.config?.restored === true ? { ok: true, reason: "the document says the shell configuration was restored" } : { ok: false, reason: "the document does not say the shell configuration was restored" }),
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
export function suiteNames() {
|
|
89
|
+
return Object.keys(SUITES)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What a suite is missing on this host, before any guest boots: the
|
|
94
|
+
* repository files it stages, and for the evidence gate the listed
|
|
95
|
+
* plugins in the lab's plugin cache at their validated commits.
|
|
96
|
+
*/
|
|
97
|
+
export function suitePreflight(suite, { repoRoot, layout, pinDir = marketplacePinDir(repoRoot) }) {
|
|
98
|
+
const missing = []
|
|
99
|
+
for (const relative of suite.needs) {
|
|
100
|
+
if (!existsSync(join(repoRoot, relative))) missing.push({ what: relative, cost: `a file this omakit ships (${repoRoot}) and does not have; the tree is incomplete`, command: existsSync(join(repoRoot, ".git")) ? `git -C ${repoRoot} checkout -- ${relative} && ${join(repoRoot, "bin/omakit")} lab prove ${suite.name}` : "npm i -g omakit, then run it again" })
|
|
101
|
+
}
|
|
102
|
+
if (suite.plugins) {
|
|
103
|
+
for (const id of suite.plugins) {
|
|
104
|
+
const listing = listedPlugin(pinDir, id)
|
|
105
|
+
if (!listing.ok) {
|
|
106
|
+
missing.push({ what: `${id} in the pinned catalog`, cost: listing.reason, command: "omakit pin" })
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const dir = join(layout.plugins, id)
|
|
110
|
+
const head = existsSync(join(dir, ".git")) ? readHead(dir) : null
|
|
111
|
+
if (head !== listing.commit) missing.push({ what: `${id} at ${listing.commit.slice(0, 12)} in ${dir}`, cost: `one shallow fetch of ${listing.repo} at that commit`, command: "omakit lab setup --plugins" })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return missing
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `<repo> <commit>` for a listed id, from the pinned catalog; never written here. */
|
|
118
|
+
export function listedPlugin(pinDir, id) {
|
|
119
|
+
let catalog
|
|
120
|
+
try {
|
|
121
|
+
catalog = JSON.parse(readFileSync(join(pinDir, "site/catalog.json"), "utf8"))
|
|
122
|
+
} catch {
|
|
123
|
+
return { ok: false, reason: "the pinned catalog is not readable; the marketplace pin is not in place" }
|
|
124
|
+
}
|
|
125
|
+
const plugin = (catalog.plugins || []).find((entry) => entry && entry.id === id)
|
|
126
|
+
const commit = plugin?.listingValidatedCommit
|
|
127
|
+
if (!plugin || !/^[0-9a-f]{40}$/.test(String(commit || "")) || !/^https:\/\/github\.com\/[^/]+\/[^/]+$/.test(String(plugin.repo || ""))) {
|
|
128
|
+
return { ok: false, reason: `${id} is not listed with a validated commit and a github.com repository in the pinned catalog` }
|
|
129
|
+
}
|
|
130
|
+
return { ok: true, repo: plugin.repo, commit }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readHead(dir) {
|
|
134
|
+
try {
|
|
135
|
+
const head = readFileSync(join(dir, ".git/HEAD"), "utf8").trim()
|
|
136
|
+
if (/^[0-9a-f]{40}$/.test(head)) return head
|
|
137
|
+
const ref = head.match(/^ref: (.+)$/)?.[1]
|
|
138
|
+
return ref ? readFileSync(join(dir, ".git", ref), "utf8").trim() : null
|
|
139
|
+
} catch {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The two checks that make a file the pinned release: its SHA-256 against
|
|
2
|
+
// the pin, and its detached signature against the packaged Omarchy key at
|
|
3
|
+
// the pinned fingerprint.
|
|
4
|
+
//
|
|
5
|
+
// The digest is the identity the package reviewed; the signature is the
|
|
6
|
+
// independent Omarchy authenticity check. Both, every time, and a mismatch
|
|
7
|
+
// in either fails closed (packaging/LAB_PLAN.md, the acquisition boundary).
|
|
8
|
+
// The key is imported into a throwaway GNUPGHOME under the lab, never into
|
|
9
|
+
// the user's keyring: a lab that added keys to ~/.gnupg would be changing
|
|
10
|
+
// the host, and the only host change the lab makes is the lab.
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto"
|
|
13
|
+
import { spawnSync } from "node:child_process"
|
|
14
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs"
|
|
15
|
+
import { join } from "node:path"
|
|
16
|
+
import { LAB_DIR, labPin } from "./pin.mjs"
|
|
17
|
+
import { labDir, removeFromLab } from "./paths.mjs"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SHA-256 of a file, streamed in 4 MiB reads. Measured on the 6.26 GB
|
|
21
|
+
* 4.0.3 ISO on the reference host, page-cached: 5.2 s in Node, 5.1 s in
|
|
22
|
+
* `sha256sum` (docs/MEASUREMENTS.md M14), so there is no reason to shell
|
|
23
|
+
* out. `onProgress(read, total)` at most once per read.
|
|
24
|
+
*/
|
|
25
|
+
export function sha256File(file, { onProgress } = {}) {
|
|
26
|
+
const total = statSync(file).size
|
|
27
|
+
const hash = createHash("sha256")
|
|
28
|
+
const buffer = Buffer.alloc(4 * 1024 * 1024)
|
|
29
|
+
const fd = openSync(file, "r")
|
|
30
|
+
let read = 0
|
|
31
|
+
try {
|
|
32
|
+
for (;;) {
|
|
33
|
+
const n = readSync(fd, buffer, 0, buffer.length, null)
|
|
34
|
+
if (n <= 0) break
|
|
35
|
+
hash.update(buffer.subarray(0, n))
|
|
36
|
+
read += n
|
|
37
|
+
if (onProgress) onProgress(read, total)
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
closeSync(fd)
|
|
41
|
+
}
|
|
42
|
+
return { sha256: hash.digest("hex"), bytes: read }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The packaged key, checked against its pinned digest before it is trusted with anything. */
|
|
46
|
+
export function packagedKey(pin = labPin(), dir = LAB_DIR) {
|
|
47
|
+
const file = join(dir, pin.release.signingKey)
|
|
48
|
+
const { sha256 } = sha256File(file)
|
|
49
|
+
if (sha256 !== pin.release.signingKeySha256) {
|
|
50
|
+
throw Object.assign(new Error(`the packaged signing key at ${file} has digest ${sha256}, not the pinned ${pin.release.signingKeySha256}`), { code: "key-mismatch" })
|
|
51
|
+
}
|
|
52
|
+
return file
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Verify a detached signature with gpg in a throwaway keyring under
|
|
57
|
+
* `stagingRoot`. The answer is the fingerprint gpg reports as VALIDSIG, or
|
|
58
|
+
* null; the caller compares it with the pin. `run` is injectable.
|
|
59
|
+
*/
|
|
60
|
+
export function verifySignature({ file, signature, keyFile, stagingRoot, run = spawnSync }) {
|
|
61
|
+
const home = labDir(stagingRoot, `gnupg-${process.pid}`)
|
|
62
|
+
const env = { ...process.env, GNUPGHOME: home }
|
|
63
|
+
try {
|
|
64
|
+
const imported = run("gpg", ["--batch", "--quiet", "--import", keyFile], { env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 60_000 })
|
|
65
|
+
if (imported.error || imported.status !== 0) {
|
|
66
|
+
return { state: "gpg-failed", fingerprint: null, detail: String(imported.stderr || imported.error?.message || "").trim().split("\n")[0] || "gpg could not import the packaged key" }
|
|
67
|
+
}
|
|
68
|
+
const verified = run("gpg", ["--batch", "--status-fd", "1", "--verify", signature, file], { env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 600_000 })
|
|
69
|
+
const status = String(verified.stdout || "")
|
|
70
|
+
const valid = status.match(/^\[GNUPG:\] VALIDSIG ([0-9A-F]{40}) /m)
|
|
71
|
+
if (valid && verified.status === 0) return { state: "valid", fingerprint: valid[1], detail: status.match(/^\[GNUPG:\] GOODSIG \S+ (.+)$/m)?.[1] || null }
|
|
72
|
+
const bad = status.match(/^\[GNUPG:\] (BADSIG|NO_PUBKEY|ERRSIG|NODATA)\b.*$/m)
|
|
73
|
+
return { state: bad ? bad[1].toLowerCase() : "invalid", fingerprint: null, detail: (bad?.[0] || String(verified.stderr || "").trim().split("\n")[0] || "gpg did not report a valid signature").trim() }
|
|
74
|
+
} finally {
|
|
75
|
+
removeFromLab(stagingRoot, `gnupg-${process.pid}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The whole judgement over a file on disk against the pin: byte count,
|
|
81
|
+
* digest, signature. Every field is reported even after the first failure,
|
|
82
|
+
* so a report can say "the size matches, the digest does not" rather than
|
|
83
|
+
* only the first thing wrong. The sidecar's digest is compared too, so a
|
|
84
|
+
* published checksum that disagrees with the pin is named (it would mean
|
|
85
|
+
* the object at the versioned URL was replaced).
|
|
86
|
+
*/
|
|
87
|
+
export function judgeRelease({ file, signature, checksum, pin = labPin(), stagingRoot, keyFile = packagedKey(pin), onProgress, run }) {
|
|
88
|
+
const verdict = { file, bytes: null, bytesMatch: false, sha256: null, sha256Match: false, sidecarSha256: null, sidecarMatch: null, signature: null, ok: false }
|
|
89
|
+
let st
|
|
90
|
+
try {
|
|
91
|
+
st = statSync(file)
|
|
92
|
+
} catch {
|
|
93
|
+
verdict.reason = `${file} is not there`
|
|
94
|
+
return verdict
|
|
95
|
+
}
|
|
96
|
+
verdict.bytes = st.size
|
|
97
|
+
verdict.bytesMatch = st.size === pin.release.bytes
|
|
98
|
+
if (!verdict.bytesMatch) {
|
|
99
|
+
verdict.reason = `${file} is ${st.size.toLocaleString("en-US")} B, the pin says ${pin.release.bytes.toLocaleString("en-US")} B`
|
|
100
|
+
return verdict
|
|
101
|
+
}
|
|
102
|
+
verdict.sha256 = sha256File(file, { onProgress }).sha256
|
|
103
|
+
verdict.sha256Match = verdict.sha256 === pin.release.sha256
|
|
104
|
+
if (checksum) {
|
|
105
|
+
try {
|
|
106
|
+
verdict.sidecarSha256 = readFileSync(checksum, "utf8").trim().split(/\s+/)[0].toLowerCase()
|
|
107
|
+
verdict.sidecarMatch = verdict.sidecarSha256 === pin.release.sha256
|
|
108
|
+
} catch {
|
|
109
|
+
verdict.sidecarSha256 = null
|
|
110
|
+
verdict.sidecarMatch = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!verdict.sha256Match) {
|
|
114
|
+
verdict.reason = `${file} has digest ${verdict.sha256}, the pin says ${pin.release.sha256}`
|
|
115
|
+
return verdict
|
|
116
|
+
}
|
|
117
|
+
if (!signature) {
|
|
118
|
+
verdict.reason = "no detached signature beside the file"
|
|
119
|
+
return verdict
|
|
120
|
+
}
|
|
121
|
+
const signed = verifySignature({ file, signature, keyFile, stagingRoot, run })
|
|
122
|
+
verdict.signature = signed
|
|
123
|
+
if (signed.state !== "valid") {
|
|
124
|
+
verdict.reason = `the signature did not verify: ${signed.detail}`
|
|
125
|
+
return verdict
|
|
126
|
+
}
|
|
127
|
+
if (signed.fingerprint !== pin.release.signingFingerprint) {
|
|
128
|
+
verdict.reason = `the signature is by ${signed.fingerprint}, the pin says ${pin.release.signingFingerprint}`
|
|
129
|
+
return verdict
|
|
130
|
+
}
|
|
131
|
+
verdict.ok = true
|
|
132
|
+
verdict.reason = `${st.size.toLocaleString("en-US")} B, sha256 ${verdict.sha256}, signed by ${signed.fingerprint}`
|
|
133
|
+
return verdict
|
|
134
|
+
}
|
|
@@ -31,7 +31,8 @@ local commit through the transport seam the marketplace tests itself
|
|
|
31
31
|
| `update-check.mjs` | A passive npm-release notice during normal terminal use, throttled to one check per day with a one-second network budget. Stores only installed/latest version metadata and time under XDG_STATE_HOME/omakit. Offline, JSON, pipes, help and CI skip it; DISABLE_UPDATE_NOTIFIER disables it. Version precedence and the registry GET are shared with doctor/upgrade. |
|
|
32
32
|
| `path-hint.mjs` | Is `omakit` reachable as a bare command, and if not, the one line that makes it so for the install that is here: a symlink for a clone, the npm prefix's `bin` on PATH for a package, said for the shell in `$SHELL`. `setup` and `doctor` print it; nothing writes an rc file. |
|
|
33
33
|
| `usage.mjs` | The help text, as data. |
|
|
34
|
-
| `options.mjs` | Every option every command accepts, in one table, and the parser that reads a command line against it before anything runs; `tests/unit/options.test.mjs` holds the help signatures, and through them the completion scripts, to the table. |
|
|
34
|
+
| `options.mjs` | Every option every command accepts, in one table, and the parser that reads a command line against it before anything runs: an option the command does not know, an option without its value or given twice, an empty argument, or one positional too many is refused by name; `tests/unit/options.test.mjs` holds the help signatures, and through them the completion scripts, to the table. |
|
|
35
|
+
| `outcome.mjs` | The one contract every command leaves through: the exit status (0 success, 1 a refusal or failure the tool means, 2 usage, 128 plus the signal for an interrupt), the `--json` envelope `{ command, ok, error, ...document }` with `error.remedy` never null, the text on stdout on exit 0 and on stderr otherwise, and `--out` written on every outcome. `conclude()` is the call; `tests/unit/json-outcomes.test.mjs` holds it per command and per outcome. |
|
|
35
36
|
| `completion.mjs` | A completion script for bash, zsh or fish, derived from the help data and the pin's form: the subcommands and flags are read out of `COMMANDS`, the categories and tags out of the pinned submission form, and the script says which pin it came from. `setup` installs it for the shell in `$SHELL`, the one file this tool writes outside its own checkout. |
|
|
36
37
|
| `completion-check.mjs` | Whether tab completion actually works: a frozen probe per shell run interactively, asking the loader to load `omakit` the way TAB does; the one marked block `setup` may append to an rc file after a yes, looked for by its marker first; the installed script's version and pin read from its first line; `doctor`'s `omakit.completion`; and the once-a-day stale notice. |
|
|
37
38
|
| `banner.mjs` | The wordmark, on a bare `omakit` and in `setup` only. |
|
|
@@ -74,6 +75,29 @@ the tree, no verdict:
|
|
|
74
75
|
| `inspect/contract.mjs` | The JSON contract of docs/INSPECT.md as a validator, run by the unit tests over every fixture document. |
|
|
75
76
|
| `inspect/report.mjs` | The report for a person, drawn with `style.mjs` only: `░ info` for a fact, `▒ ?` for one that could not be read, `▓ note` for a pattern row, `▔ skip` under `--offline`, and the closing word `INSPECTED`. |
|
|
76
77
|
|
|
78
|
+
`tools/lab/` is `omakit lab`, the fourth job (docs/LAB.md): a suite proven
|
|
79
|
+
in a disposable Omarchy guest, never on the desktop, the guest identified
|
|
80
|
+
in the document:
|
|
81
|
+
|
|
82
|
+
| File | Purpose |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `lab/pin.json`, `lab/pin.mjs` | The release pin: Omarchy 4.0.3, its URL, exact bytes, SHA-256, signer fingerprint, expected guest package; the toolchain commit and the digests of its harness before and after the patch; the measured costs. Refuses a URL that resolves latest. `bytesBoth` prints every size in GB and GiB. |
|
|
85
|
+
| `lab/omarchy.gpg` | The Omarchy public signing key, 632 bytes of armoured text, pinned by digest. |
|
|
86
|
+
| `lab/patches/omarchy-iso-test.patch` | The working harness of the reference host against the pinned omarchy-iso commit: the 4.0.3 greeter, no host package install, the host-test extensions. Applied by a person, never by omakit. |
|
|
87
|
+
| `lab/paths.mjs` | The two lab roots under the user cache and state, the layout, and `inLab`, the guard every write goes through; the one rename and the one stream copy in the tree. |
|
|
88
|
+
| `lab/host.mjs` | Read-only probes: KVM, the commands a run, a build and a verification need (`--version`, never `ssh-keygen` bare), the OVMF firmware, free disk, memory, the CPU count. Installs nothing. |
|
|
89
|
+
| `lab/verify.mjs` | SHA-256 streamed in Node, the signature check in a throwaway keyring, and `judgeRelease`: byte count, digest, sidecar, signature, fingerprint, in that order, every field reported. |
|
|
90
|
+
| `lab/inspect.mjs` | The read-only view: the download and its verification record, the base's state from its manifest (`ready`, `mismatch`, `invalid`, `missing`), the toolchain by its harness's hash, staging with QMP liveness, the lock, the totals, what is missing with its cost and command. |
|
|
91
|
+
| `lab/qemu.mjs` | QEMU's argument list, pure; QMP over the Unix socket from Node; the qcode table and `typeText` for the greeter. |
|
|
92
|
+
| `lab/guest.mjs` | SSH to 127.0.0.1 with the base's key and no forwarding; the session preamble; the login loop; the startup-notification dismissal; the guest's identity (`pacman -Q omarchy`, the kernel, whether the session is linked). |
|
|
93
|
+
| `lab/run.mjs` | The lifecycle: the lock, `withGuest` (overlay, the run's firmware copy, QEMU as a child, SSH, login, identity, the body, power-off, the overlay measured and removed, the base checked unchanged), `preflightRun`, `runSuite` with the harness as a child and the document's provenance. |
|
|
94
|
+
| `lab/setup.mjs` | The plan and the disclosure, the resumable literal GET through `github.mjs`'s one call site, the import of a local file, verification and promotion of the ISO, the toolchain record, the build through a copy of the toolchain's harness under staging, the verification boot, the manifest, the promotion; the listed plugins for the evidence suite. |
|
|
95
|
+
| `lab/prune.mjs` | The inventory of what the lab owns with allocated bytes, the refusal while a QEMU answers, the removal of exactly the targets, recovered and remaining bytes. |
|
|
96
|
+
| `lab/suites.mjs` | The four suites as data: host body, files needed, document, assertion, timeout, arguments; the listed plugin ids for `weigh-evidence`, their commits the pinned catalog's. |
|
|
97
|
+
| `lab/harness.sh`, `lab/suites/*.sh` | The one harness every suite runs through, every value an argument; the Run, Store and weigh bodies. |
|
|
98
|
+
| `lab/qmp-cli.mjs` | QMP from a shell: a screendump or a chord, for the harness. |
|
|
99
|
+
| `lab/report.mjs` | The lab's lines for `doctor`, and `inspect`, `setup`, `run` and `prune` for a person, drawn with `style.mjs`. |
|
|
100
|
+
|
|
77
101
|
```text
|
|
78
102
|
omakit pin
|
|
79
103
|
omakit inspect /path/to/plugin-repo # what the tree does, as observations; --json for the document
|
|
@@ -127,3 +151,16 @@ beyond fetching a reviewer-mode subject, and `tests/parity/offline.mjs` proves
|
|
|
127
151
|
it.
|
|
128
152
|
|
|
129
153
|
`watch --list` discovers the signed-in account's open marketplace issues through `github.mjs`; `--user` bypasses the account lookup. `watch --all` runs the existing single-issue watch with four workers and shared repository HEAD promises, preserving each read failure. A bare terminal invocation uses `ask.mjs` to choose one or several issues. `report.mjs` composes the list and batch views; all options remain in `options.mjs` and `usage.mjs`, which also generate completion. The batch JSON retains complete single-issue reports; `current` compares commits and never substitutes for review or publication.
|
|
154
|
+
|
|
155
|
+
## tools/blocks/
|
|
156
|
+
|
|
157
|
+
`tools/blocks/` is the Run block's plumbing on omakit's side: the block
|
|
158
|
+
itself is `blocks/run/`, files a plugin copies (docs/BLOCKS.md).
|
|
159
|
+
|
|
160
|
+
| Module | What it does |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `blocks/registry.mjs` | The shipped blocks read from `blocks/<name>/`: each file's header (block, version, licence, source, body sha256) and body hash; `recogniseBlockFile` for inspect and `add`; `blocks/history.json` for every hash ever shipped; the NOTICE renderer. |
|
|
163
|
+
| `blocks/add.mjs` | `omakit add <block> [plugin-dir]`: the one code path that writes into a plugin tree, held to the registry's names under `omakit/`, checked against the agent-control list first, never over a file without `--update`, never over a modified copy. |
|
|
164
|
+
| `blocks/stamp.mjs` | Maintainer's tool after editing a block: rewrites each header's sha256 to its body's, regenerates NOTICE and appends to history.json, under this checkout's `blocks/` only. |
|
|
165
|
+
| `blocks/record-commit.mjs`, `blocks/commit.json` | The release tool: HEAD written into the record before `npm pack`, so an installed package names the commit its block files come from; null in a checkout, where git is the source. Never a lifecycle hook. |
|
|
166
|
+
|
|
@@ -194,6 +194,25 @@ export function frame(layout, band, revealed = band, { colour = true } = {}) {
|
|
|
194
194
|
})
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* The tagline as the lines drawn under the wordmark: one when it fits the
|
|
199
|
+
* wordmark's width, else the narrowest wrap that takes two lines, and the
|
|
200
|
+
* text on one line when no width under its own length does.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} text
|
|
203
|
+
* @param {number} width the wordmark's width in cells
|
|
204
|
+
* @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
export function taglineLines(text, width) {
|
|
207
|
+
const plain = String(text)
|
|
208
|
+
if ([...plain].length <= width) return [plain]
|
|
209
|
+
for (let columns = width; columns < [...plain].length; columns += 1) {
|
|
210
|
+
const lines = wrap(plain, { width: columns })
|
|
211
|
+
if (lines.length <= 2) return lines
|
|
212
|
+
}
|
|
213
|
+
return [plain]
|
|
214
|
+
}
|
|
215
|
+
|
|
197
216
|
/**
|
|
198
217
|
* @param {{ word?: string, tagline?: string, stream?: NodeJS.WriteStream,
|
|
199
218
|
* enabled?: boolean, animate?: boolean, shines?: number,
|
|
@@ -217,9 +236,11 @@ export async function banner(options = {}) {
|
|
|
217
236
|
// The tagline is centred under the wordmark, not set flush left: the rule
|
|
218
237
|
// is exactly as wide as the letters, so a shorter line starting at column
|
|
219
238
|
// 0 reads as slid to the left. The padding is spaces, no escape, so the
|
|
220
|
-
// line is centred under NO_COLOR and in a pipe alike.
|
|
239
|
+
// line is centred under NO_COLOR and in a pipe alike. A tagline wider
|
|
240
|
+
// than the wordmark is wrapped as narrow as two lines allow, so it sits
|
|
241
|
+
// under the letters instead of running past them.
|
|
221
242
|
const tagline = options.tagline
|
|
222
|
-
? " ".repeat(Math.max(0, Math.floor((width - [...
|
|
243
|
+
? taglineLines(options.tagline, width).map((line) => " ".repeat(Math.max(0, Math.floor((width - [...line].length) / 2))) + c(code("prose"), line)).join("\n")
|
|
223
244
|
: null
|
|
224
245
|
|
|
225
246
|
// Nothing at all when it is not a terminal. There is no plain-text substitute
|