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
package/README.md CHANGED
@@ -1,24 +1,12 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/banner.gif" alt="omakit" width="440">
2
+ <img src="https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/banner.gif" alt="Animated Omakit wordmark above the line tested plumbing for plugins" width="512">
3
3
  </p>
4
4
 
5
- The marketplace validates one exact commit of your plugin. Push a fix or comment "fixed", and nothing re-runs ([M6](docs/MEASUREMENTS.md#m6-the-validated-commit-falls-behind-silently-and-that-is-the-centre-of-this-tool)). omakit runs the marketplace's own checks locally, watches your submission and posts nothing.
5
+ **Tested plumbing, the marketplace's own checks, and a disposable Omarchy to test in.**
6
6
 
7
- [![Built for Omarchy: App](https://raw.githubusercontent.com/tcballard/omarchy-badges/75975e5b5bf75e7ede3764bcd2950046f7abfe2c/badges/v1/omarchy-app.svg)](https://github.com/tcballard/omarchy-badges) [![npm version](https://img.shields.io/npm/v/omakit)](https://www.npmjs.com/package/omakit) [![CI status](https://img.shields.io/github/actions/workflow/status/mtolhuys/omakit/ci.yml?branch=main)](https://github.com/mtolhuys/omakit/actions/workflows/ci.yml) [![Socket](https://socket.dev/api/badge/npm/package/omakit)](https://socket.dev/npm/package/omakit)
7
+ The three parts are the Run and Store blocks a plugin copies into its own tree, the marketplace's own checks run locally, and repository suites run in a disposable guest. Behind the first are M13's 1,001 security blocker comments from one measured week: 587 raise a Run line, 527 a Store line, and runner plus store handle 777 of 1,001 when read as an upper bound, not as a result for any plugin ([M13](docs/MEASUREMENTS.md#m13-what-the-review-blocks-on-over-one-week-of-comments-and-which-of-it-a-block-can-own)). No reviewer has seen a ported plugin yet.
8
8
 
9
- ## What it delivers
10
-
11
- | Command | What you get | Read more |
12
- | --- | --- | --- |
13
- | `omakit submit <plugin-repo>` | Run the marketplace's own checks before you open the issue, and get the issue text ready to paste. Nothing is posted for you. | [submit](docs/SUBMIT.md) |
14
- | `omakit inspect <plugin-dir>` | See what needs attention in your plugin before a reviewer does: the longest functions, and the things reviewers flag most often, each with the file and line. | [inspect](docs/INSPECT.md) |
15
- | `omakit watch <issue-url>` | Know whether the commit the marketplace checked is still the one you are shipping, and what to do when it is not. | [watch](docs/VALIDATION_WATCH.md) |
16
- | `omakit audit` | Find installed plugins that are running code the marketplace never checked. | [audit](docs/AUDIT.md) |
17
- | `omakit weigh <plugin>` | Find out what a plugin costs the shell in memory and CPU. | [weigh](docs/WEIGH.md) |
18
- | `omakit verify <plugin-repo>` | Get the marketplace's security result for your commit, exactly as it would see it. | [commands](docs/COMMANDS.md) |
19
- | `omakit doctor`, `omakit setup` | Check what is installed and pinned, or set everything up once, with tab completion. | [install](docs/INSTALL.md) |
20
-
21
- Every number a command prints has a measured origin in [MEASUREMENTS.md](docs/MEASUREMENTS.md); nothing is a guess and nothing is a grade.
9
+ [![Built for Omarchy: App](https://raw.githubusercontent.com/tcballard/omarchy-badges/75975e5b5bf75e7ede3764bcd2950046f7abfe2c/badges/v1/omarchy-app.svg)](https://github.com/tcballard/omarchy-badges) [![npm version](https://img.shields.io/npm/v/omakit)](https://www.npmjs.com/package/omakit) [![CI status](https://img.shields.io/github/actions/workflow/status/mtolhuys/omakit/ci.yml?branch=main)](https://github.com/mtolhuys/omakit/actions/workflows/ci.yml) [![Socket package report](https://socket.dev/api/badge/npm/package/omakit)](https://socket.dev/npm/package/omakit)
22
10
 
23
11
  ## Install
24
12
 
@@ -27,54 +15,59 @@ npm i -g omakit && omakit setup
27
15
  npx skills add mtolhuys/omakit
28
16
  ```
29
17
 
30
- Runs where [Node 22+](package.json) and Git run; `weigh` and `audit` need a running Omarchy shell.
18
+ Omakit needs [Node 22+](package.json) and Git. `omakit setup` prepares the pinned marketplace checkout and shell completion. The blocks also need `/usr/bin/python3`. Audit and weigh need a running Omarchy shell. The lab needs KVM, QEMU and OVMF; `omakit doctor` reports what is missing and installs nothing.
31
19
 
32
- Licence: [MIT](LICENSE).
20
+ ## Build
33
21
 
34
- ## `omakit submit <plugin-repo>`
22
+ `omakit add run <plugin-dir>` and `omakit add store <plugin-dir>` copy versioned process and private-state plumbing into the plugin's `omakit/` directory; the plugin owns those files, and an edited copy is never overwritten. Commit the copied files before checking, because check reads committed `HEAD` unless you pass `--allow-dirty`.
35
23
 
36
- ![submit refusing a fixture plugin before any issue is posted](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/submit.gif)
24
+ ![Recorded terminal showing inspect before omakit add run, the copied Run files, and inspect after the fixture is ported](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/add-run.gif)
37
25
 
38
- Checks the exact commit with the marketplace's own baseline and, when ready, prints the exact issue title and body for you to paste. The GIF shows a refusal with three blocking checks and their fixes.
26
+ ## Check
39
27
 
40
- ## `omakit watch --all`
28
+ `omakit inspect <plugin-dir>` shows what the tree does, `omakit verify <plugin-repo>` runs the marketplace's baseline, and `omakit submit <plugin-repo>` runs every submission check and prints a title and body without posting them.
41
29
 
42
- ![watch counts and two current issues, including human discussion](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/watch-all.gif)
30
+ ![Recorded terminal showing inspect ranking two long functions and listing the review class observed in a fixture](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/inspect.gif)
43
31
 
44
- Checks your submission commits and names the action that re-runs stale validation: edit the issue body. The GIF shows five CURRENT issues in the counts and the first two issues with a discussion; CURRENT means matching commits, not approval.
32
+ ![Recorded terminal showing submit refusing a fixture with three blocking checks and printing their remedies](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/submit.gif)
45
33
 
46
- ## `omakit audit`
34
+ ## Track
47
35
 
48
- ![audit keeping drift rows and the DRIFT summary visible together](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/audit.gif)
36
+ `omakit watch --all` compares open submissions with the commits the marketplace validated, while `omakit audit` compares installed third-party plugins with their validated commits; both are read-only.
49
37
 
50
- Compares your installed plugin commits with the marketplace's validated commits. The GIF shows drift rows first: on the author's desktop, 9 of 18 audited plugins ran commits the marketplace never validated.
38
+ ![Recorded terminal showing watch-all count two current submissions and display their validation and discussion records](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/watch-all.gif)
51
39
 
52
- ## `omakit inspect <plugin-dir>`
40
+ ![Recorded terminal showing audit list installed plugin drift and close with the 13-of-19 drift summary](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/audit.gif)
53
41
 
54
- ![inspect showing a fixture's size score, its two long functions with their ranks, and the one review class it shows](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/inspect.gif)
42
+ ## Prove
55
43
 
56
- Reads a plugin's tree and prints what needs attention, biggest first: a size score (the share of its function lines that sit in functions over the measured size, placed among the listed trees' shares, so a tree with no long function scores 10.00, [M12](docs/MEASUREMENTS.md#m12-how-long-a-plugins-functions-are-in-listed-trees)), the functions over what 90 of 100 listed functions stay under, then each review class the tree shows with the class's measured share of review findings ([M11](docs/MEASUREMENTS.md#m11-what-the-human-review-raises-by-class)) and up to five sites. No verdict, nothing run from the tree; `--full` is every site, `--json` the document. Over 18 listed plugins read at their validated commits, the extraction counted 515 process sites (57 QML `Process` blocks, 458 shell lines), 17 hosts, 63 writes and 40 timers, left 15 rows it could not resolve, and printed 73 pattern rows across 17 of the 18 ([record](docs/evidence/inspect/2026-09-15-listed-sample.json)).
44
+ After one `omakit lab setup`, `omakit lab prove <suite>` boots a fresh overlay from the verified base, prints the guest identity, runs Run, Store or weigh in that guest, records the result, removes the overlay and checks the base unchanged.
57
45
 
58
- ## `omakit weigh <plugin>`
46
+ ![Recorded terminal showing omakit lab prove run identify the disposable guest, complete all 19 Run scenarios, remove the overlay, verify the unchanged base and close PROVED](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/lab-prove.gif)
59
47
 
60
- ![completed three-run desktop weighing with baseline samples and the noise floor](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/weigh.gif)
48
+ `omakit weigh <plugin>` is the supporting desktop measurement: after explicit consent it restarts the shell without and with the plugin, reports CPU and child processes against the baseline's own spread, and restores `shell.json`.
61
49
 
62
- Measures the shell with and without your plugin, reading Pss and CPU. The GIF shows three completed runs on the author's desktop, with baseline and plugin samples and a 0.33% CPU floor ([method](docs/WEIGH.md)).
50
+ ![Recorded terminal showing all six Theme Manager weigh samples, shell restoration, the CPU noise floor and the completed report](https://raw.githubusercontent.com/mtolhuys/omakit/main/docs/media/weigh.gif)
63
51
 
64
- ## Evidence, not claims
52
+ ## Evidence
65
53
 
66
- | Measurement | Evidence |
54
+ | What was measured | Record |
67
55
  | --- | --- |
68
- | Baseline parity | 30/30 identical results, [recorded corpus](docs/evidence/parity/2026-09-12-local-vs-github-2.json), 2026-09-12 |
69
- | Stale validated commit | 326/519 readable comparisons stale (62.8%); 64/583 unknown, [2026-09-15 data](docs/evidence/staleness/2026-09-15.json) |
70
- | Registry churn | 4,201/4,293 registry-only commits in 30 days, 2026-09-13, [M7](docs/MEASUREMENTS.md#m7-the-registry-moves-by-the-hour-the-code-and-the-rules-move-by-the-week) |
71
- | GIFs are recorded output | 6 GIFs with [captures and scenes](docs/media/README.md) |
72
- | Posts nothing | 0 marketplace writes, [M10](docs/MEASUREMENTS.md#m10-readme-evidence-and-command-captures) |
73
- | Zero dependencies | 0 runtime and 0 development dependencies, counted in [package.json](package.json) |
56
+ | Review plumbing | 1,001 blocker comments; Run 587, Store 527, either 777, [M13 record](docs/evidence/blocks/2026-09-17-review-blockers.json) |
57
+ | Run | 19 of 19 on the desktop and 19 of 19 on the stock 4.0.3 guest at Run 0.2.1, [desktop](docs/evidence/blocks/2026-09-18-run-lab-desktop.json), [fresh guest](docs/evidence/lab/20260919-143209-run/runlab.json) |
58
+ | Store | 15 of 15 on the desktop and stock guest, including the simulated foreign owner, [desktop](docs/evidence/blocks/2026-09-18-store-lab-desktop.json), [guest](docs/evidence/lab/20260918-161230-store/storelab.json) |
59
+ | The disposable guest | guest `omarchy 4.0.3-1`, skew false, 2m 42.2s, 440,209,408 B overlay removed, base unchanged, [fresh run](docs/evidence/lab/20260919-143209-run/run.json) |
60
+ | The two ports | [Theme Manager through Run](docs/evidence/blocks/2026-09-17-theme-manager-port.json), [Sidecar through Store](docs/evidence/blocks/2026-09-17-sidecar-port.json); neither submitted, no reviewer has seen either |
61
+ | Validation drift | 326 of 519 readable open-submission comparisons stale, 64 of 583 unknown, [dated record](docs/evidence/staleness/2026-09-15.json) |
62
+ | README captures | eight GIFs made from committed captures and scenes, with commands, hashes, dimensions and timings in the [M10 record](docs/evidence/readme/2026-09-19-positioning.json) |
63
+ | First-reader path | packaged install, blocks, checks, tracking and a 19-scenario guest run exercised from `/tmp`, [dated record](docs/evidence/ux/2026-09-19-readme-test.json) |
64
+ | Package boundary | 0 runtime and 0 development dependencies in [package.json](package.json); 0 marketplace writes enforced by the read-only tests |
74
65
 
75
66
  ## Documentation
76
67
 
77
- - Using: [install](docs/INSTALL.md), [commands](docs/COMMANDS.md), [audience](docs/MARKETPLACE.md).
78
- - Checks and measurements: [submit](docs/SUBMIT.md), [inspect](docs/INSPECT.md), [watch](docs/VALIDATION_WATCH.md), [audit](docs/AUDIT.md), [evidence](docs/MEASUREMENTS.md).
79
- - Method docs: [how](docs/HOW.md), [weigh](docs/WEIGH.md), [upstream contract](docs/UPSTREAM_CONTRACT.md), [palette](docs/PALETTE.md), [terminal](docs/TUI.md).
80
- - Contributing: [repository rules](AGENTS.md), [releasing](docs/RELEASING.md), [media](docs/media/README.md).
68
+ - [Blocks](docs/BLOCKS.md) and [their design spike](docs/BLOCKS_SPIKE.md)
69
+ - [Commands](docs/COMMANDS.md), [inspect](docs/INSPECT.md), [submit](docs/SUBMIT.md), [watch](docs/VALIDATION_WATCH.md), [audit](docs/AUDIT.md) and [weigh](docs/WEIGH.md)
70
+ - [The disposable lab](docs/LAB.md) and [every measurement](docs/MEASUREMENTS.md)
71
+ - [Installation](docs/INSTALL.md), [media recipes](docs/media/README.md) and [repository rules](AGENTS.md)
72
+
73
+ Licence: [MIT](LICENSE).
@@ -0,0 +1,68 @@
1
+ [
2
+ {
3
+ "block": "run",
4
+ "version": "0.1.0",
5
+ "file": "Run.qml",
6
+ "sha256": "7055007d082b81f328b8c3e7f41e648ed27c111ef546220277f0fb7f18332e28"
7
+ },
8
+ {
9
+ "block": "run",
10
+ "version": "0.1.0",
11
+ "file": "run-supervisor.py",
12
+ "sha256": "3a1c8e18c56592e046e1ac1dfe35df5745c39cc60405dfb6d408ee4eb287ef5f"
13
+ },
14
+ {
15
+ "block": "store",
16
+ "version": "0.1.0",
17
+ "file": "Store.qml",
18
+ "sha256": "c71b5646e0b6ccc4b45e5c57a8c7ed08a089319f4ee7d3ce74ef5e28d8af3801"
19
+ },
20
+ {
21
+ "block": "store",
22
+ "version": "0.1.0",
23
+ "file": "store-helper.py",
24
+ "sha256": "28576b8189c9d2547c11b273243731a6c954644b7135186e57257addc3c99dd9"
25
+ },
26
+ {
27
+ "block": "store",
28
+ "version": "0.1.0",
29
+ "file": "store-helper.py",
30
+ "sha256": "3ef0c3f5df90ef93d3ff7cdd979159cce590a4a61b3351f42ea5123fa49f0720"
31
+ },
32
+ {
33
+ "block": "store",
34
+ "version": "0.1.0",
35
+ "file": "store-helper.py",
36
+ "sha256": "cec3fcc9573ce7c12f1365e106cc65a936e213553bfe4e3128e8a87eea511dc5"
37
+ },
38
+ {
39
+ "block": "run",
40
+ "version": "0.2.0",
41
+ "file": "Run.qml",
42
+ "sha256": "0e54a5a82aacbdcd2e61163091bbeebb597787abb285df86bf85d7d835451419"
43
+ },
44
+ {
45
+ "block": "run",
46
+ "version": "0.2.0",
47
+ "file": "run-supervisor.py",
48
+ "sha256": "07c3d5fa103fcbfc40eb081206fdd8571c6e3852a188389d79bbac12db00170d"
49
+ },
50
+ {
51
+ "block": "store",
52
+ "version": "0.2.0",
53
+ "file": "Store.qml",
54
+ "sha256": "6fafd2ec98b4d4fe18ead2ae1f1221386e9714eb9a33ef0f1b9cf48520f2c743"
55
+ },
56
+ {
57
+ "block": "store",
58
+ "version": "0.2.0",
59
+ "file": "store-helper.py",
60
+ "sha256": "2dd4d6ad308bbed473936c29844793411d8849415812b96ca5e7a6688097e37c"
61
+ },
62
+ {
63
+ "block": "run",
64
+ "version": "0.2.1",
65
+ "file": "run-supervisor.py",
66
+ "sha256": "d872a5fc91f648ba4f9b9654d5cb19bba17a8afa4abb618502a04f47f66a6be1"
67
+ }
68
+ ]
@@ -0,0 +1,12 @@
1
+ omakit blocks in this directory
2
+
3
+ Each file below was copied from omakit (https://github.com/mtolhuys/omakit),
4
+ MIT licence, Copyright (c) 2026 Maarten Tolhuijs, by `omakit add`. The body
5
+ sha256 is over everything after the file's header line
6
+ `end of omakit block header`; `omakit inspect` reports a file whose body differs as
7
+ modified, and `omakit add --update` refuses to overwrite one. Modifications
8
+ are to be listed here by the plugin's author.
9
+
10
+ block run 0.2.1, from omakit commit unstamped
11
+ Run.qml sha256 0e54a5a82aacbdcd2e61163091bbeebb597787abb285df86bf85d7d835451419
12
+ run-supervisor.py sha256 d872a5fc91f648ba4f9b9654d5cb19bba17a8afa4abb618502a04f47f66a6be1
@@ -0,0 +1,242 @@
1
+ // omakit block: run 0.2.1
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) 2026 Maarten Tolhuijs
4
+ // Source: omakit blocks/run/Run.qml, commit unstamped
5
+ // Body sha256: 0e54a5a82aacbdcd2e61163091bbeebb597787abb285df86bf85d7d835451419
6
+ // end of omakit block header
7
+ //
8
+ // Run: starts one program for a plugin and always ends it. The program is
9
+ // started by run-supervisor.py, next to this file, through
10
+ // /usr/bin/python3 -I -S -B: absolute path, argv only, a closed environment,
11
+ // a hard deadline, byte and line caps while reading, TERM then grace then
12
+ // KILL to the whole process group, the leader reaped last, cancel on
13
+ // destruction and on supersession, one result object. The protocol between
14
+ // this file and the supervisor is a per-run token this file writes to the
15
+ // supervisor's stdin: every line the supervisor reports carries it, and a
16
+ // line without it, which is what the program can write into the same pipe,
17
+ // changes nothing. docs/BLOCKS.md is the contract and says which review
18
+ // comments each line answers.
19
+ //
20
+ // Run {
21
+ // id: catalog
22
+ // command: [Quickshell.shellDir + "/catalog.sh", "--refresh"]
23
+ // deadlineMs: 30000
24
+ // onFinished: result => { if (result.state === "ok") parse(result.stdout) }
25
+ // }
26
+ // catalog.start()
27
+ import QtQuick
28
+ import Quickshell
29
+ import Quickshell.Io
30
+
31
+ QtObject {
32
+ id: run
33
+
34
+ // argv; command[0] an absolute path. A shell string (an interpreter with
35
+ // its string flag, through a wrapper or not) is refused as spawn-failed
36
+ // unless allowShellString; the forms it knows are in docs/BLOCKS.md.
37
+ property list<string> command: []
38
+ property bool allowShellString: false
39
+ // Added to the base environment, never replacing it. The base is
40
+ // PATH=/usr/bin, HOME, LANG=C.UTF-8 and XDG_RUNTIME_DIR; nothing else
41
+ // of the shell's environment reaches the program.
42
+ property var environment: ({})
43
+ property int deadlineMs: 10000
44
+ property int graceMs: 1000
45
+ // Per stream, counted while reading; over either the run ends as overflow.
46
+ property int maxBytes: 1048576
47
+ property int maxLines: 10000
48
+ // Per stream, what the result carries; the rest is counted and dropped.
49
+ property int keepBytes: 65536
50
+
51
+ readonly property bool running: _state === "running"
52
+ // One object: state is one of ok, exit, timeout, overflow, cancelled,
53
+ // spawn-failed, supervisor-lost, python-missing; stdout and stderr are
54
+ // plain text of at most keepBytes each, control characters removed,
55
+ // meant for Text.PlainText; outBytes, outLines, errBytes, errLines as
56
+ // counted; exitCode (null when signalled), termSignal, ms, pgid,
57
+ // survivors, and reason for the three failure states.
58
+ signal finished(var result)
59
+
60
+ readonly property string supervisor: decodeURIComponent(Qt.resolvedUrl("run-supervisor.py").toString().replace(/^file:\/\//, ""))
61
+ readonly property var states: ["ok", "exit", "timeout", "overflow", "cancelled", "spawn-failed", "supervisor-lost", "python-missing"]
62
+ property string _state: "idle"
63
+ property bool _started: false
64
+ property bool _pending: false
65
+ property int _pgid: 0
66
+ property double _t0: 0
67
+ property var _result: null
68
+ property string _supervisorErr: ""
69
+ property string _token: ""
70
+ property string _buffer: ""
71
+
72
+ readonly property var _baseEnvironment: ({
73
+ PATH: "/usr/bin",
74
+ HOME: Quickshell.env("HOME"),
75
+ LANG: "C.UTF-8",
76
+ XDG_RUNTIME_DIR: Quickshell.env("XDG_RUNTIME_DIR")
77
+ })
78
+
79
+ function _now() { return Date.now() - _t0 }
80
+
81
+ /** Start the program. A run already live is cancelled first, reports cancelled, and this start follows it. */
82
+ function start() {
83
+ if (_state === "running") { _pending = true; cancel(); return }
84
+ if (_proc.running) { _pending = true; return } // the last supervisor is still being reaped (a backstop); it follows on runningChanged
85
+ _t0 = Date.now()
86
+ _state = "running"
87
+ _started = false
88
+ _result = null
89
+ _supervisorErr = ""
90
+ _buffer = ""
91
+ _pgid = 0
92
+ _token = _newToken()
93
+ _proc.command = _argv()
94
+ _proc.environment = Object.assign({}, _baseEnvironment, environment)
95
+ _proc.stdinEnabled = true
96
+ _backstop.interval = deadlineMs + graceMs + 3000
97
+ _backstop.start()
98
+ _proc.running = true
99
+ }
100
+
101
+ /** End the run now: TERM to the group, the grace, KILL; the result comes back as cancelled. */
102
+ function cancel() {
103
+ if (_state === "running") _proc.signal(15)
104
+ }
105
+
106
+ // 128 bits from the engine's securely seeded generator; the program never
107
+ // sees a value of it (stdin is consumed by the supervisor before the fork).
108
+ function _newToken() {
109
+ let token = ""
110
+ for (let i = 0; i < 8; i += 1) token += ("000" + Math.floor(Math.random() * 65536).toString(16)).slice(-4)
111
+ return token
112
+ }
113
+
114
+ function _argv() {
115
+ const options = ["--deadline-ms", String(deadlineMs), "--grace-ms", String(graceMs),
116
+ "--max-bytes", String(maxBytes), "--max-lines", String(maxLines), "--keep-bytes", String(keepBytes)]
117
+ if (allowShellString) options.push("--allow-shell-string")
118
+ return ["/usr/bin/python3", "-I", "-S", "-B", supervisor].concat(options, ["--"], command)
119
+ }
120
+
121
+ // The supervisor's stdout, delivered as it arrives: lines are cut here,
122
+ // and the unfinished line is bounded, so a stream without a newline
123
+ // cannot grow inside the shell process.
124
+ function _chunk(text) {
125
+ _buffer += text
126
+ let end = _buffer.indexOf("\n")
127
+ while (end >= 0) {
128
+ _line(_buffer.slice(0, end))
129
+ _buffer = _buffer.slice(end + 1)
130
+ end = _buffer.indexOf("\n")
131
+ }
132
+ const bound = 6 * keepBytes + 65536
133
+ if (_buffer.length > bound) _buffer = _buffer.slice(-bound)
134
+ }
135
+
136
+ // A protocol line carries the token; anything else on the pipe is the
137
+ // program's and is dropped. The first leader and the first result count.
138
+ function _line(line) {
139
+ const at = _token ? line.indexOf(_token + " ") : -1
140
+ if (at < 0) return
141
+ let event
142
+ try { event = JSON.parse(line.slice(at + _token.length + 1)) } catch (error) { return }
143
+ if (event.ev === "leader") _leader(event)
144
+ else if (event.ev === "result" && !_result) _result = _checked(event)
145
+ }
146
+
147
+ // The first leader line names the group; the acknowledgement releases the
148
+ // supervisor's gate, and stdin closes behind it.
149
+ function _leader(event) {
150
+ if (_pgid !== 0 || _int(event.pgid) === 0) return
151
+ _pgid = _int(event.pgid)
152
+ _proc.write("go\n")
153
+ _proc.stdinEnabled = false
154
+ }
155
+
156
+ function _int(value) { return Number.isInteger(value) && value >= 0 ? value : 0 }
157
+ function _intOrNull(value) { return Number.isInteger(value) ? value : null }
158
+ function _text(value, max) { return _plain(String(value == null ? "" : value)).slice(0, max) }
159
+
160
+ /** The result as this file reports it: the closed set of states, the numbers as integers, the text stripped here as well. */
161
+ function _checked(event) {
162
+ if (!states.includes(event.state)) return null
163
+ return {
164
+ state: event.state, exitCode: _intOrNull(event.exitCode), termSignal: _intOrNull(event.termSignal),
165
+ outBytes: _int(event.outBytes), outLines: _int(event.outLines), errBytes: _int(event.errBytes), errLines: _int(event.errLines),
166
+ survivors: _int(event.survivors), stdout: _text(event.stdout, keepBytes), stderr: _text(event.stderr, keepBytes),
167
+ reason: event.reason == null ? null : _text(event.reason, 4096),
168
+ signals: Array.isArray(event.signals) ? event.signals.filter(s => s && typeof s.sig === "string").map(s => ({ sig: s.sig.slice(0, 8), atMs: _int(s.atMs), esrch: s.esrch === true })) : []
169
+ }
170
+ }
171
+
172
+ function _plain(text) {
173
+ return String(text).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "")
174
+ }
175
+
176
+ function _lost(reason) {
177
+ return { state: "supervisor-lost", reason: reason, stderr: _plain(_supervisorErr).slice(0, 4096) }
178
+ }
179
+
180
+ // The supervisor is gone without a valid result: whatever it started is
181
+ // ended by the detached reaper, and the result says so.
182
+ function _reap(reason) {
183
+ if (_pgid !== 0) {
184
+ Quickshell.execDetached(["/usr/bin/python3", "-I", "-S", "-B", supervisor, "--kill-group", String(_pgid), "--grace-ms", String(graceMs)])
185
+ reason += "; the group " + _pgid + " was sent TERM and, after the grace, KILL by a detached reaper"
186
+ }
187
+ _finish(_lost(reason))
188
+ }
189
+
190
+ function _onExited(code, status) {
191
+ if (_state !== "running") return
192
+ _backstop.stop()
193
+ if (_result) { _finish(_result); return }
194
+ if (code === 2 && /can.t open file/.test(_supervisorErr)) { _reap("run-supervisor.py is not readable at " + supervisor); return }
195
+ _reap("the supervisor " + (status === 1 ? "died on signal " : "exited ") + code + " without a result")
196
+ }
197
+
198
+ function _onRunningChanged() {
199
+ // Never started: the interpreter itself could not be run (stock
200
+ // Omarchy has it; docs/BLOCKS.md says why it is checked anyway).
201
+ if (!_proc.running && _state === "running" && !_started) {
202
+ _backstop.stop()
203
+ _finish({ state: "python-missing", reason: "/usr/bin/python3 could not be started" })
204
+ } else if (!_proc.running && _pending) { _pending = false; start() }
205
+ }
206
+
207
+ function _finish(result) {
208
+ _state = "done"
209
+ result.pgid = _pgid
210
+ result.ms = _now()
211
+ finished(result)
212
+ if (_pending && !_proc.running) { _pending = false; start() }
213
+ }
214
+
215
+ property Timer _backstop: Timer {
216
+ onTriggered: {
217
+ if (run._pgid !== 0) Quickshell.execDetached(["/usr/bin/kill", "-s", "KILL", "--", "-" + run._pgid])
218
+ run._proc.signal(9)
219
+ run._finish(run._lost("no result " + run._backstop.interval + " ms after start; the group was sent KILL"))
220
+ }
221
+ }
222
+
223
+ property Process _proc: Process {
224
+ clearEnvironment: true
225
+ stdinEnabled: true
226
+ stdout: SplitParser { splitMarker: ""; onRead: text => run._chunk(text) }
227
+ stderr: SplitParser { splitMarker: ""; onRead: text => { if (run._supervisorErr.length < 4096) run._supervisorErr += String(text).slice(0, 4096) } }
228
+ onStarted: { run._started = true; run._proc.write(run._token + "\n") }
229
+ onExited: (code, status) => run._onExited(code, status)
230
+ onRunningChanged: run._onRunningChanged()
231
+ }
232
+
233
+ Component.onDestruction: {
234
+ // The Process destructor SIGKILLs the supervisor right after this, so
235
+ // the group is ended by a detached reaper: TERM, the grace, KILL. A
236
+ // run whose leader this file never learned is still behind the
237
+ // supervisor's gate and exits unrun when the supervisor dies.
238
+ if (_state === "running" && _pgid !== 0) {
239
+ Quickshell.execDetached(["/usr/bin/python3", "-I", "-S", "-B", supervisor, "--kill-group", String(_pgid), "--grace-ms", String(graceMs)])
240
+ }
241
+ }
242
+ }