geneseed 2.0.0
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/CHANGELOG.md +938 -0
- package/DESIGN.md +1423 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +48 -0
- package/README.md +336 -0
- package/SETUP.md +1027 -0
- package/SHIPPED.md +70 -0
- package/adapters/bob/README.md +100 -0
- package/adapters/claude-code/README.md +101 -0
- package/adapters/claude-code/settings.json +47 -0
- package/adapters/claude-code/skills/geneseed/SKILL.md +72 -0
- package/adapters/copilot/README.md +76 -0
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +434 -0
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +158 -0
- package/adapters/opencode/README.md +582 -0
- package/adapters/opencode/agents/orchestrator.md +25 -0
- package/adapters/opencode/opencode.json +44 -0
- package/adapters/opencode/plugins/geneseed-activity.js +430 -0
- package/adapters/opencode/plugins/geneseed-context.js +876 -0
- package/adapters/opencode/plugins/geneseed-guard.js +290 -0
- package/adapters/opencode/plugins/geneseed-learn.js +396 -0
- package/adapters/opencode/plugins/geneseed-notify.js +142 -0
- package/adapters/opencode/plugins/geneseed-ponytail.js +148 -0
- package/adapters/opencode/plugins/geneseed-workflow.js +183 -0
- package/adapters/opencode/workflows/_runtime.js +277 -0
- package/adapters/opencode/workflows/council.js +64 -0
- package/adapters/opencode/workflows/dispatch.js +89 -0
- package/adapters/opencode/workflows/research-plan-implement.js +43 -0
- package/adapters/opencode/workflows/review.js +57 -0
- package/bin/geneseed-cli.mjs +412 -0
- package/bin/geneseed-hook.mjs +129 -0
- package/bin/geneseed.mjs +1086 -0
- package/docs/declined.md +149 -0
- package/docs/limits.md +270 -0
- package/docs/opencode-plugin-setup.md +64 -0
- package/docs/token-footprint.md +68 -0
- package/docs/web/_groups.json +45 -0
- package/docs/web/about.md +6 -0
- package/docs/web/adapters-claude-code.md +8 -0
- package/docs/web/adapters-opencode-loads.md +8 -0
- package/docs/web/adapters-opencode-spec.md +8 -0
- package/docs/web/adapters-opencode.md +8 -0
- package/docs/web/agents.md +8 -0
- package/docs/web/authoring.md +11 -0
- package/docs/web/autostart.md +9 -0
- package/docs/web/claude-hooks.md +36 -0
- package/docs/web/cli.md +6 -0
- package/docs/web/collaboration.md +27 -0
- package/docs/web/configure-wiki.md +9 -0
- package/docs/web/design.md +7 -0
- package/docs/web/env-knobs.md +9 -0
- package/docs/web/exclusions.md +26 -0
- package/docs/web/first-session.md +28 -0
- package/docs/web/footprint.md +31 -0
- package/docs/web/foreman-mode.md +33 -0
- package/docs/web/glossary.md +6 -0
- package/docs/web/harnesses.md +28 -0
- package/docs/web/headless.md +9 -0
- package/docs/web/ingest-docs.md +9 -0
- package/docs/web/install-paths.md +9 -0
- package/docs/web/install-quick.md +35 -0
- package/docs/web/lsp-overview.md +37 -0
- package/docs/web/mcp-claude-code.md +10 -0
- package/docs/web/mcp-filesystem.md +9 -0
- package/docs/web/mcp-gitlab.md +9 -0
- package/docs/web/mcp-markitdown.md +9 -0
- package/docs/web/mcp-overview.md +34 -0
- package/docs/web/mcp-trouble.md +9 -0
- package/docs/web/mcp-verify.md +9 -0
- package/docs/web/memory.md +7 -0
- package/docs/web/migrate.md +39 -0
- package/docs/web/model.md +20 -0
- package/docs/web/notebook.md +7 -0
- package/docs/web/pipelines.md +24 -0
- package/docs/web/plugin-activity.md +23 -0
- package/docs/web/plugin-context.md +26 -0
- package/docs/web/plugin-guard.md +24 -0
- package/docs/web/plugin-learn.md +27 -0
- package/docs/web/plugin-notify.md +24 -0
- package/docs/web/plugin-ponytail.md +23 -0
- package/docs/web/plugin-setup.md +7 -0
- package/docs/web/plugin-workflow.md +24 -0
- package/docs/web/plugins.md +17 -0
- package/docs/web/project-context.md +9 -0
- package/docs/web/rules.md +8 -0
- package/docs/web/run-anywhere.md +9 -0
- package/docs/web/self-improve.md +8 -0
- package/docs/web/setup-choices.md +39 -0
- package/docs/web/skills.md +8 -0
- package/docs/web/themes-author.md +8 -0
- package/docs/web/themes.md +9 -0
- package/docs/web/token-footprint.md +7 -0
- package/docs/web/trouble.md +38 -0
- package/docs/web/upgrade.md +9 -0
- package/docs/web/verify.md +30 -0
- package/docs/web/wiki.md +7 -0
- package/docs/web/worktree.md +10 -0
- package/docs/web-ui.md +144 -0
- package/docs/wiki.md +43 -0
- package/geneseed +39 -0
- package/geneseed.cmd +34 -0
- package/harness.config.json +6 -0
- package/js/anim.mjs +326 -0
- package/js/catalog.mjs +112 -0
- package/js/checkout.mjs +88 -0
- package/js/cli-table.json +1034 -0
- package/js/cli.mjs +498 -0
- package/js/diff.mjs +384 -0
- package/js/doctor.mjs +1364 -0
- package/js/emit.mjs +1373 -0
- package/js/excludes.mjs +271 -0
- package/js/generate.mjs +379 -0
- package/js/hooks.mjs +1026 -0
- package/js/hosts.mjs +253 -0
- package/js/installs.mjs +378 -0
- package/js/inventory.mjs +175 -0
- package/js/lib/fs.mjs +913 -0
- package/js/lib/proc.mjs +28 -0
- package/js/lib/udiff.mjs +275 -0
- package/js/link.mjs +286 -0
- package/js/mcp.mjs +335 -0
- package/js/memory.mjs +108 -0
- package/js/menu.mjs +118 -0
- package/js/migrate.mjs +251 -0
- package/js/native.mjs +426 -0
- package/js/opencode.mjs +325 -0
- package/js/registry.mjs +110 -0
- package/js/render.mjs +282 -0
- package/js/settings.mjs +924 -0
- package/js/setup.mjs +492 -0
- package/js/status.mjs +281 -0
- package/js/themes.mjs +195 -0
- package/js/tui.mjs +444 -0
- package/js/uninstall.mjs +1137 -0
- package/js/update.mjs +910 -0
- package/js/web/actions.mjs +868 -0
- package/js/web/activity.mjs +284 -0
- package/js/web/api.mjs +873 -0
- package/js/web/docs.mjs +531 -0
- package/js/web/graph.mjs +0 -0
- package/js/web/jobs.mjs +480 -0
- package/js/web/server.mjs +1135 -0
- package/package.json +54 -0
- package/registry.json +474 -0
- package/src/AGENT.md.tmpl +390 -0
- package/src/agents/_template.md +76 -0
- package/src/agents/advocate.md +36 -0
- package/src/agents/architect.md +55 -0
- package/src/agents/developer.md +66 -0
- package/src/agents/docs.md +54 -0
- package/src/agents/empiricist.md +39 -0
- package/src/agents/explorer.md +54 -0
- package/src/agents/framer.md +36 -0
- package/src/agents/historian.md +51 -0
- package/src/agents/operator.md +37 -0
- package/src/agents/pragmatist.md +37 -0
- package/src/agents/reviewer.md +52 -0
- package/src/agents/security.md +57 -0
- package/src/agents/skeptic.md +48 -0
- package/src/agents/steward.md +36 -0
- package/src/agents/tester.md +54 -0
- package/src/agents/user-advocate.md +36 -0
- package/src/agents/visionary.md +36 -0
- package/src/laws/universal.md +387 -0
- package/src/memory/README.md +109 -0
- package/src/memory/gitignore +6 -0
- package/src/modes/README.md +26 -0
- package/src/modes/direct.md +2 -0
- package/src/modes/foreman.md +19 -0
- package/src/notebook/README.md +39 -0
- package/src/notebook/gitignore +7 -0
- package/src/postures/README.md +31 -0
- package/src/postures/artisan.md +14 -0
- package/src/postures/assistant.md +12 -0
- package/src/postures/expert.md +11 -0
- package/src/postures/mentor.md +11 -0
- package/src/postures/peer.md +12 -0
- package/src/skills/_template.md +47 -0
- package/src/skills/brainstorm.md +28 -0
- package/src/skills/clarify.md +28 -0
- package/src/skills/codebase-design.md +44 -0
- package/src/skills/commit.md +48 -0
- package/src/skills/council.md +34 -0
- package/src/skills/crash-course.md +28 -0
- package/src/skills/daydream/.claude-plugin/plugin.json +9 -0
- package/src/skills/daydream/SKILL.md +74 -0
- package/src/skills/daydream/VENDOR.md +25 -0
- package/src/skills/daydream/critic-prompt.md +38 -0
- package/src/skills/daydream/instructions.md +422 -0
- package/src/skills/daydream/synthesizer-prompt.md +41 -0
- package/src/skills/debug.md +37 -0
- package/src/skills/decode.md +28 -0
- package/src/skills/develop.md +29 -0
- package/src/skills/document-project.md +79 -0
- package/src/skills/domain-modeling.md +38 -0
- package/src/skills/drill.md +28 -0
- package/src/skills/feynman.md +26 -0
- package/src/skills/forge-mcp.md +48 -0
- package/src/skills/fresh-eyes.md +29 -0
- package/src/skills/frontend-design.md +58 -0
- package/src/skills/gap-detector.md +27 -0
- package/src/skills/geneseed-code-review.md +33 -0
- package/src/skills/geneseed.md +29 -0
- package/src/skills/git-archaeology.md +47 -0
- package/src/skills/git-rescue.md +68 -0
- package/src/skills/handoff.md +29 -0
- package/src/skills/herdr.md +164 -0
- package/src/skills/ingest.md +49 -0
- package/src/skills/learning-path.md +26 -0
- package/src/skills/migrate.md +45 -0
- package/src/skills/opencode-theme.md +52 -0
- package/src/skills/parallel-agents.md +28 -0
- package/src/skills/pipeline.md +52 -0
- package/src/skills/plan.md +45 -0
- package/src/skills/ponytail.md +29 -0
- package/src/skills/profile.md +58 -0
- package/src/skills/prose.md +42 -0
- package/src/skills/prototype.md +44 -0
- package/src/skills/react-view-transitions/AGENTS.md +955 -0
- package/src/skills/react-view-transitions/README.md +42 -0
- package/src/skills/react-view-transitions/SKILL.md +320 -0
- package/src/skills/react-view-transitions/VENDOR.md +16 -0
- package/src/skills/react-view-transitions/metadata.json +12 -0
- package/src/skills/react-view-transitions/references/css-recipes.md +242 -0
- package/src/skills/react-view-transitions/references/implementation.md +182 -0
- package/src/skills/react-view-transitions/references/nextjs.md +176 -0
- package/src/skills/react-view-transitions/references/patterns.md +262 -0
- package/src/skills/refactor.md +32 -0
- package/src/skills/release.md +43 -0
- package/src/skills/repo-map.md +36 -0
- package/src/skills/research.md +42 -0
- package/src/skills/review-response.md +36 -0
- package/src/skills/roast-me.md +28 -0
- package/src/skills/rule.md +100 -0
- package/src/skills/ship.md +49 -0
- package/src/skills/tdd.md +31 -0
- package/src/skills/tickets.md +44 -0
- package/src/skills/token-report/SKILL.md +76 -0
- package/src/skills/token-report/VENDOR.md +23 -0
- package/src/skills/token-report/scripts/token_report.mjs +1301 -0
- package/src/skills/wayfinder.md +49 -0
- package/src/skills/wiki.md +68 -0
- package/src/skills/workflow.md +38 -0
- package/themes/README.md +69 -0
- package/themes/_TEMPLATE.json +147 -0
- package/themes/biker.json +147 -0
- package/themes/commentator.json +147 -0
- package/themes/cyberpunk.json +147 -0
- package/themes/gamer.json +147 -0
- package/themes/imperial.json +147 -0
- package/themes/joker.json +147 -0
- package/themes/marvin.json +147 -0
- package/themes/mean.json +147 -0
- package/themes/military.json +147 -0
- package/themes/neutral.json +147 -0
- package/themes/opencode/README.md +58 -0
- package/themes/opencode/catppuccin.json +27 -0
- package/themes/opencode/dracula.json +27 -0
- package/themes/opencode/everforest.json +27 -0
- package/themes/opencode/gruvbox.json +27 -0
- package/themes/opencode/kanagawa.json +27 -0
- package/themes/opencode/neon.json +27 -0
- package/themes/opencode/nord.json +27 -0
- package/themes/opencode/rosepine.json +27 -0
- package/themes/opencode/tokyonight.json +27 -0
- package/themes/pirate.json +147 -0
- package/themes/sports.json +147 -0
- package/themes/verstappen.json +147 -0
- package/themes/wizard.json +147 -0
- package/web/dist/assets/About-DX1-VTBP.js +1 -0
- package/web/dist/assets/Activity-Dp9NaXOn.js +1 -0
- package/web/dist/assets/ActivityDetail-HpOBtrXS.js +1 -0
- package/web/dist/assets/Diff-Ct4G3Se8.js +5 -0
- package/web/dist/assets/Doctor-DGjkZFuA.js +1 -0
- package/web/dist/assets/Graph-b8o-S6kv.js +1 -0
- package/web/dist/assets/Harnesses-gr3-aHBF.js +1 -0
- package/web/dist/assets/Laws-DTwF5tTC.js +1 -0
- package/web/dist/assets/Library-BEZscyaQ.js +1 -0
- package/web/dist/assets/Markdown-k_LSLBGq.js +1 -0
- package/web/dist/assets/Profile-DydukTxP.js +1 -0
- package/web/dist/assets/Rules-CIRG4Ldt.js +1 -0
- package/web/dist/assets/Skills-Buoi1IBr.js +1 -0
- package/web/dist/assets/StatusBadge-CE_AJttl.js +1 -0
- package/web/dist/assets/Themes-CI4o3gJC.js +2 -0
- package/web/dist/assets/activity-BCW7e6Cy.js +1 -0
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-italic-Bserb8X5.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-normal-BG6hkEXj.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-italic-BGfwwhek.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-normal-DrDcrrxK.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-600-normal-CIXX6EOa.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-700-normal-CeQ8H3UY.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-B1PIx7RM.js +1 -0
- package/web/dist/assets/index-BnTQUSUz.js +12 -0
- package/web/dist/assets/index-Ck2iCdaC.js +2 -0
- package/web/dist/assets/index-DUCsDv7z.css +1 -0
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/web/dist/assets/marked.esm-JKvyS0Pp.js +46 -0
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/react-DOpiTn5v.js +32 -0
- package/web/dist/assets/roman-DkYN0LX9.js +1 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-italic-YylcN9Ay.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-italic-vNvENeTh.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
- package/web/dist/icon-maskable.svg +10 -0
- package/web/dist/icon.svg +8 -0
- package/web/dist/index.html +21 -0
- package/web/dist/manifest.webmanifest +14 -0
- package/web/dist/sw.js +40 -0
- package/web/src/pages/Laws.jsx +230 -0
package/js/web/jobs.mjs
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web console's BACKGROUND JOBS — `rituals/_web_jobs.py`'s `JobManager`, its process-tree
|
|
3
|
+
* kill and the `action_commands` table.
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------------------
|
|
6
|
+
* WHY THIS SPAWNS, WHEN NOTHING ELSE IN THE PORT IS ALLOWED TO.
|
|
7
|
+
*
|
|
8
|
+
* `bin/geneseed-cli.mjs` carries a transitive `child_process` ALLOW-LIST
|
|
9
|
+
* (`tests/test_hook_cli_parity.py::_ALLOWED_SPAWNS`) whose criterion is not "this verb
|
|
10
|
+
* spawns" — `build` spawns on the reference and is refused there — but "there is no
|
|
11
|
+
* in-process equivalent, and the spawned thing is not this program". This module spawns THIS
|
|
12
|
+
* PROGRAM, which reads like the passthrough the whole port exists to remove, so the argument
|
|
13
|
+
* has to be made rather than assumed.
|
|
14
|
+
*
|
|
15
|
+
* ISOLATION IS THE DISCRIMINATOR, and it is a property of the RUNTIME, not of the verbs.
|
|
16
|
+
* The reference's daemon is threaded: `start()` hands the work to a `threading.Thread` and
|
|
17
|
+
* the HTTP server keeps answering from another. Node's server is single-threaded and every
|
|
18
|
+
* ported `cmdX` is synchronous, so calling one in-process would block the event loop for the
|
|
19
|
+
* whole run — and the console's only progress mechanism is `web/src/api/jobs.js` polling
|
|
20
|
+
* `GET /api/jobs/<id>`. An in-process job therefore freezes the poll that exists to watch it,
|
|
21
|
+
* which is worse than not showing progress: the UI cannot tell a running job from a hung
|
|
22
|
+
* daemon. And a `process.exit` anywhere in a callee kills the DAEMON instead of the job —
|
|
23
|
+
* `cmdRebuildAll` already had to grow a throwing `die` for exactly that reason (P5f).
|
|
24
|
+
*
|
|
25
|
+
* `worker_threads` IS THE ALTERNATIVE AND IT IS WORSE. It restores concurrency, but a worker
|
|
26
|
+
* re-imports the world per job (the generator, the renderer, every host module), needs a
|
|
27
|
+
* message protocol for stdout and the exit code that a pipe gives for nothing, and cannot
|
|
28
|
+
* isolate a `process.exit` any better than the main thread can — `process.exit` in a worker
|
|
29
|
+
* takes the worker down mid-write, which is the same truncation with more machinery. Strictly
|
|
30
|
+
* more work for strictly less isolation.
|
|
31
|
+
*
|
|
32
|
+
* THE EXIT CODE IS EASIER OUT OF PROCESS, not harder. `_run` reads `p.returncode` and stops
|
|
33
|
+
* the chain on the first non-zero. In-process that would be a return value threaded through
|
|
34
|
+
* every verb's error paths, several of which currently end at `process.exitCode`.
|
|
35
|
+
*
|
|
36
|
+
* SO THE ARGV IS `node bin/geneseed-cli.mjs <verb>` — or `node bin/geneseed.mjs <build args>`
|
|
37
|
+
* for the three rows that run the GENERATOR, which is `build.py`'s twin and the same program
|
|
38
|
+
* by a different entry. `process.execPath` is the running interpreter by absolute path, so
|
|
39
|
+
* stripping `node` off PATH cannot change what starts. Nothing here can name `python`.
|
|
40
|
+
*
|
|
41
|
+
* THE SECOND SPAWN IS `taskkill`, and it is a different argument. `cancel()` must kill the
|
|
42
|
+
* job's whole process TREE — the reference's comment says why: killing only the direct child
|
|
43
|
+
* leaves ITS children holding the stdout pipe, the read loop stays blocked and the job wedges
|
|
44
|
+
* on `running` forever. POSIX gets that in-process (`detached: true` makes a process group and
|
|
45
|
+
* `process.kill(-pid)` signals it). Windows has no process-group kill in Node's standard
|
|
46
|
+
* library at all, and `taskkill /T` is what the reference reaches for too. It is a machine
|
|
47
|
+
* primitive, not this program.
|
|
48
|
+
*
|
|
49
|
+
* ---------------------------------------------------------------------------------------
|
|
50
|
+
* THE PARTITION — six of the eight action rows cross and two do not.
|
|
51
|
+
*
|
|
52
|
+
* `doctor`, `build`, `build-all`, `export`, `uninstall` and — since P8c — `update` all name a
|
|
53
|
+
* verb that has crossed. `link`/`unlink` run `harness.py link|unlink` (P10b). A Node runner
|
|
54
|
+
* that spawned the Python harness for those would put Python back into a "no Python
|
|
55
|
+
* needed" install, so they are DECLARED in `NOT_PORTED_ACTIONS` and answered 501 by the
|
|
56
|
+
* dispatcher, and P10b ends by deleting both rows. `tests/test_web_jobs.py` cross-checks the
|
|
57
|
+
* two sets against `action_commands`' own keys with `ast`, so a NINTH row added to the
|
|
58
|
+
* reference cannot quietly answer "unknown action".
|
|
59
|
+
*
|
|
60
|
+
* ---------------------------------------------------------------------------------------
|
|
61
|
+
* THREE DETAILS OF `_run`, EACH A COMMENT EXPLAINING A BUG IT ALREADY FIXED.
|
|
62
|
+
*
|
|
63
|
+
* `GENESEED_WEB_JOB=1` tells `upgrade` it is running INSIDE this daemon so it must not bounce
|
|
64
|
+
* the daemon mid-job — which killed the job's own tracking and left the console on `running`
|
|
65
|
+
* forever. It was set here before any ported row read it, because it was P6g's CONTRACT WITH
|
|
66
|
+
* P8, and a contract written after the fact is a contract that was broken once first. P8a is
|
|
67
|
+
* where `js/update.mjs` began honouring it, and P8c is where the `update` row that triggers
|
|
68
|
+
* it crossed — so it is a live pair now rather than a promise.
|
|
69
|
+
*
|
|
70
|
+
* `PYTHONUNBUFFERED=1` reaches the Python child AND its own Python children, or their stdout
|
|
71
|
+
* is block-buffered into the pipe and the console looks stuck. THE EQUIVALENT THOUGHT FOR
|
|
72
|
+
* NODE, which is the port and not the copy: there is no unbuffering flag to set, because
|
|
73
|
+
* Node's `process.stdout` on a pipe is not block-buffered by libc — every `write` is queued on
|
|
74
|
+
* the stream and flushed by the event loop. The analogous hazard is a child calling
|
|
75
|
+
* `process.exit()` with writes still queued, which truncates them; both `bin/` entries set
|
|
76
|
+
* `process.exitCode` and return instead, which is what makes the flag unnecessary rather than
|
|
77
|
+
* merely inapplicable. So the variable is NOT set: it would be cargo, and the honest
|
|
78
|
+
* statement is this paragraph.
|
|
79
|
+
*
|
|
80
|
+
* `MEM_CAP` truncates a runaway job's output FROM THE FRONT, so a job that prints forever
|
|
81
|
+
* cannot eat the daemon's memory. `OUTPUT_CAP` is the smaller cap the history FILE keeps.
|
|
82
|
+
*
|
|
83
|
+
* ---------------------------------------------------------------------------------------
|
|
84
|
+
* TWO DIVERGENCES, DECLARED.
|
|
85
|
+
*
|
|
86
|
+
* 1. THE TWO STREAMS ARE TWO PIPES. `stderr=subprocess.STDOUT` merges them in the OPERATING
|
|
87
|
+
* SYSTEM, so the reference's interleaving is exact. Node's `stdio` cannot name an
|
|
88
|
+
* already-created pipe, so `stdout` and `stderr` are read separately and appended in
|
|
89
|
+
* arrival order — identical for a line-buffered child writing to one stream (which is
|
|
90
|
+
* every ported row: `rebuild-all` writes stdout only, `diff` writes stderr only), and
|
|
91
|
+
* potentially reordered for a child interleaving both within one tick. No cell can see it,
|
|
92
|
+
* and it is named here rather than left to be discovered.
|
|
93
|
+
* 2. `_append`'s cap slices UTF-16 code UNITS where Python slices code POINTS. It differs only
|
|
94
|
+
* past two million characters and only by a lone surrogate at the truncation front. The
|
|
95
|
+
* history file's cap does the exact thing (`Array.from`), because it runs once per job
|
|
96
|
+
* rather than once per line — doing it here would make a runaway job quadratic, which is
|
|
97
|
+
* the failure the cap exists to prevent.
|
|
98
|
+
*
|
|
99
|
+
* NOT PORTED: `JobManager.wait`. It has no caller in `rituals/` — `tests/test_web.py` is its
|
|
100
|
+
* only consumer, twenty times over. Its Node twin is owed the day a Node-side corpus drives
|
|
101
|
+
* the manager directly, which is P9's; until then it would be a function with no caller and no
|
|
102
|
+
* cell, which is the shape this port keeps being bitten by.
|
|
103
|
+
*/
|
|
104
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
105
|
+
import { randomBytes } from 'node:crypto';
|
|
106
|
+
import { statSync } from 'node:fs';
|
|
107
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
108
|
+
import path from 'node:path';
|
|
109
|
+
|
|
110
|
+
import { ROOT } from '../checkout.mjs';
|
|
111
|
+
import { setupBuildArgs } from '../generate.mjs';
|
|
112
|
+
import { isDict } from '../mcp.mjs';
|
|
113
|
+
import { jsonDumpsCompact, parseJson, pyTruthy, readText, writeText } from '../lib/fs.mjs';
|
|
114
|
+
|
|
115
|
+
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `_kill_job_tree` — terminate a job's WHOLE process tree.
|
|
119
|
+
*
|
|
120
|
+
* NEVER THROWS, as the reference's two nested `except`s do not: it runs from `cancel()` and
|
|
121
|
+
* from the crash path in `_run`'s `catch`, and an exception in either would leave the child
|
|
122
|
+
* alive with the manager believing it dead.
|
|
123
|
+
*/
|
|
124
|
+
export function killJobTree(child) {
|
|
125
|
+
try {
|
|
126
|
+
if (process.platform === 'win32') {
|
|
127
|
+
// The one machine primitive with no Node equivalent — see this module's header.
|
|
128
|
+
spawnSync('taskkill', ['/T', '/F', '/PID', String(child.pid)], {
|
|
129
|
+
timeout: 15000, windowsHide: true, stdio: 'ignore',
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
// `os.killpg(p.pid, SIGTERM)`. The group exists because `_run` spawns with
|
|
133
|
+
// `detached: true`, which is `start_new_session=True`.
|
|
134
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
try {
|
|
138
|
+
child.kill();
|
|
139
|
+
} catch { /* `except OSError: pass` */ }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `Popen(..., text=True, encoding="utf-8", errors="replace")` — the decode Python's text mode
|
|
145
|
+
* performs and Node's pipes do not.
|
|
146
|
+
*
|
|
147
|
+
* TWO THINGS, and the second is the one a port drops. `StringDecoder` holds a partial UTF-8
|
|
148
|
+
* sequence across a chunk boundary (a naive `buf.toString()` per chunk turns one multi-byte
|
|
149
|
+
* character split across two reads into two replacement characters — the exact defect P5i
|
|
150
|
+
* found when a helper reached its second owner). And Python decodes in UNIVERSAL NEWLINES
|
|
151
|
+
* mode, so `\r\n` and a lone `\r` are both `\n` before the caller ever sees them — while Node
|
|
152
|
+
* hands back the bytes. The reference's children are Python processes writing CRLF on Windows;
|
|
153
|
+
* the twin's are Node processes doing the same. Without the fold the job output would carry
|
|
154
|
+
* CRLF on one side and LF on the other, which is P5b's eighth coverage hole with the
|
|
155
|
+
* normalising transport removed.
|
|
156
|
+
*
|
|
157
|
+
* The pending `\r` is held rather than translated on the spot: a `\r\n` split across two
|
|
158
|
+
* chunks would otherwise become two newlines.
|
|
159
|
+
*/
|
|
160
|
+
function textDecoder(sink) {
|
|
161
|
+
const dec = new StringDecoder('utf8');
|
|
162
|
+
let pendingCR = false;
|
|
163
|
+
const emit = (s) => {
|
|
164
|
+
if (pendingCR) {
|
|
165
|
+
s = `\r${s}`;
|
|
166
|
+
pendingCR = false;
|
|
167
|
+
}
|
|
168
|
+
if (s.endsWith('\r')) {
|
|
169
|
+
s = s.slice(0, -1);
|
|
170
|
+
pendingCR = true;
|
|
171
|
+
}
|
|
172
|
+
if (s) sink(s.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
push: (buf) => emit(dec.write(buf)),
|
|
176
|
+
end: () => {
|
|
177
|
+
let s = dec.end();
|
|
178
|
+
if (pendingCR) {
|
|
179
|
+
s = `\r${s}`;
|
|
180
|
+
pendingCR = false;
|
|
181
|
+
}
|
|
182
|
+
if (s) sink(s.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** `s[-n:]` by CODE POINT, which is what Python slices. */
|
|
188
|
+
const tailChars = (s, n) => (s.length <= n ? s : Array.from(s).slice(-n).join(''));
|
|
189
|
+
|
|
190
|
+
/** The signals `killJobTree` sends, for the `-SIGNUM` return code Python reports. */
|
|
191
|
+
const SIGNUM = { SIGTERM: 15, SIGKILL: 9, SIGINT: 2, SIGHUP: 1 };
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `_web_jobs.JobManager` — one mutating action at a time, in the background, output captured.
|
|
195
|
+
*
|
|
196
|
+
* A CLASS rather than P6b's factory-of-a-plain-object, because this one has real state and
|
|
197
|
+
* eleven methods over it; `webState` is a factory because its only behaviour is two cached
|
|
198
|
+
* getters.
|
|
199
|
+
*
|
|
200
|
+
* THE LOCK IS GONE AND ITS ABSENCE IS THE PORT. `self._lock` guards `_jobs`, `_busy` and
|
|
201
|
+
* `_procs` against the run thread and the request threads. Node has one thread and no
|
|
202
|
+
* pre-emption inside a synchronous block, so every critical section here is already atomic —
|
|
203
|
+
* a mutex would be a no-op object whose presence claimed a hazard that cannot occur. What the
|
|
204
|
+
* lock's placement DOES carry over is which sequences must not be interrupted, and none of
|
|
205
|
+
* them awaits.
|
|
206
|
+
*/
|
|
207
|
+
export class JobManager {
|
|
208
|
+
static HISTORY_MAX = 20;
|
|
209
|
+
|
|
210
|
+
static OUTPUT_CAP = 20000; // chars of output kept per job in the history file
|
|
211
|
+
|
|
212
|
+
static MEM_CAP = 2000000; // chars kept in memory per job while it runs
|
|
213
|
+
|
|
214
|
+
constructor(historyPath = null) {
|
|
215
|
+
this._jobs = new Map();
|
|
216
|
+
this._busy = false;
|
|
217
|
+
this._procs = new Map();
|
|
218
|
+
this._historyPath = historyPath;
|
|
219
|
+
this._loadHistory();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A `running` entry in the file means the SERVER died mid-run, so it is dropped rather than
|
|
224
|
+
* restored: nothing will ever finish it, and a console showing it would wait forever.
|
|
225
|
+
*/
|
|
226
|
+
_loadHistory() {
|
|
227
|
+
if (!this._historyPath || !isFile(this._historyPath)) return;
|
|
228
|
+
let jobs;
|
|
229
|
+
try {
|
|
230
|
+
jobs = parseJson(readText(this._historyPath));
|
|
231
|
+
} catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const j of Array.isArray(jobs) ? jobs : []) {
|
|
235
|
+
if (isDict(j) && pyTruthy(j.id) && j.status !== 'running') this._jobs.set(String(j.id), j);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_saveHistory() {
|
|
240
|
+
if (!this._historyPath) return;
|
|
241
|
+
const jobs = [...this._jobs.values()].filter((j) => j.status !== 'running')
|
|
242
|
+
.map((j) => ({ ...j }));
|
|
243
|
+
// `sort(key=...)` is STABLE in both languages, which matters: two jobs started inside the
|
|
244
|
+
// same clock tick keep insertion order rather than swapping between the two runtimes.
|
|
245
|
+
jobs.sort((a, b) => (Number(a.started ?? 0) || 0) - (Number(b.started ?? 0) || 0));
|
|
246
|
+
const kept = jobs.slice(-JobManager.HISTORY_MAX)
|
|
247
|
+
.map((j) => ({ ...j, output: tailChars(String(j.output), JobManager.OUTPUT_CAP) }));
|
|
248
|
+
try {
|
|
249
|
+
writeText(this._historyPath, jsonDumpsCompact(kept, { bareInts: true }));
|
|
250
|
+
} catch { /* `except OSError: pass` — a console that cannot persist still runs */ }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Last `n` jobs, oldest first — the order the console appends in. */
|
|
254
|
+
recent(n = JobManager.HISTORY_MAX) {
|
|
255
|
+
const jobs = [...this._jobs.values()]
|
|
256
|
+
.sort((a, b) => (Number(a.started ?? 0) || 0) - (Number(b.started ?? 0) || 0));
|
|
257
|
+
return jobs.slice(-n).map((j) => ({ ...j }));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `start(action, *cmds, on_done=None)` — `cmds` is a LIST OF ARGVS, run in order, stopping
|
|
262
|
+
* at the first failure. `null` when a job is already running, which the HTTP layer maps
|
|
263
|
+
* to 409.
|
|
264
|
+
*/
|
|
265
|
+
start(action, cmds, onDone = null) {
|
|
266
|
+
if (this._busy) return null;
|
|
267
|
+
this._busy = true;
|
|
268
|
+
const jid = randomBytes(8).toString('hex');
|
|
269
|
+
this._jobs.set(jid, {
|
|
270
|
+
id: jid,
|
|
271
|
+
action,
|
|
272
|
+
status: 'running',
|
|
273
|
+
output: '',
|
|
274
|
+
returncode: null,
|
|
275
|
+
started: Date.now() / 1000,
|
|
276
|
+
duration: null,
|
|
277
|
+
});
|
|
278
|
+
// The reference's `threading.Thread(...).start()`: hand the work to the scheduler and
|
|
279
|
+
// return the id NOW, so the 202 goes out before the child has produced a byte.
|
|
280
|
+
void this._run(jid, cmds, onDone);
|
|
281
|
+
return jid;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_append(jid, text) {
|
|
285
|
+
const j = this._jobs.get(jid);
|
|
286
|
+
j.output += text;
|
|
287
|
+
if (j.output.length > JobManager.MEM_CAP) {
|
|
288
|
+
// Code UNITS, not code points — see this module's header for why the exact slice lives
|
|
289
|
+
// in `_saveHistory` and not here.
|
|
290
|
+
j.output = j.output.slice(-JobManager.MEM_CAP);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async _run(jid, cmds, onDone = null) {
|
|
295
|
+
let rc = 0;
|
|
296
|
+
let p = null;
|
|
297
|
+
try {
|
|
298
|
+
for (let i = 0; i < cmds.length; i += 1) {
|
|
299
|
+
const cmd = cmds[i];
|
|
300
|
+
this._append(jid, `$ ${cmd.map((c) => String(c)).join(' ')}\n`);
|
|
301
|
+
p = spawn(cmd[0], cmd.slice(1), {
|
|
302
|
+
cwd: ROOT,
|
|
303
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
304
|
+
// `harness.NO_WINDOW`: no console window for a child of a windowless daemon.
|
|
305
|
+
windowsHide: true,
|
|
306
|
+
// `start_new_session=True` on POSIX — the process GROUP `killJobTree` signals.
|
|
307
|
+
detached: process.platform !== 'win32',
|
|
308
|
+
env: { ...process.env, GENESEED_WEB_JOB: '1' },
|
|
309
|
+
});
|
|
310
|
+
this._procs.set(jid, p); // reachable for cancel()
|
|
311
|
+
rc = await this._pump(jid, p);
|
|
312
|
+
this._procs.delete(jid);
|
|
313
|
+
if (rc !== 0) {
|
|
314
|
+
const left = cmds.length - i - 1;
|
|
315
|
+
this._append(jid, `\n[web] ✗ command exited with code ${rc}`
|
|
316
|
+
+ (left ? ` — skipping the ${left} remaining step(s).\n` : '.\n'));
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch (e) {
|
|
321
|
+
this._append(jid, `\n[web] job crashed: ${e && e.message ? e.message : e}`);
|
|
322
|
+
rc = 1;
|
|
323
|
+
// Don't orphan the child on a crash of OUR side: kill its tree, or it lingers.
|
|
324
|
+
// `p.pid` is undefined when the spawn itself failed, which is the `p is not None`
|
|
325
|
+
// arm the reference cannot reach at all (its `Popen` raises before the assignment).
|
|
326
|
+
if (p !== null && p.pid && p.exitCode === null && p.signalCode === null) killJobTree(p);
|
|
327
|
+
} finally {
|
|
328
|
+
this._procs.delete(jid);
|
|
329
|
+
const j = this._jobs.get(jid);
|
|
330
|
+
j.status = rc === 0 ? 'done' : 'failed';
|
|
331
|
+
j.returncode = rc;
|
|
332
|
+
// `round(x, 1)`. ponytail: JS rounds a .x5 tie away from zero where Python rounds to
|
|
333
|
+
// even — unreachable for a wall-clock delta, and the field is a display value.
|
|
334
|
+
j.duration = Math.round((Date.now() / 1000 - Number(j.started)) * 10) / 10;
|
|
335
|
+
this._busy = false;
|
|
336
|
+
this._saveHistory();
|
|
337
|
+
if (onDone) {
|
|
338
|
+
try {
|
|
339
|
+
onDone(rc); // callbacks take the exit code (0 = every step ok)
|
|
340
|
+
} catch { /* refresh must never kill the job */ }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* `for line in p.stdout: ...` then `p.wait()` — read to EOF, then the exit code.
|
|
347
|
+
*
|
|
348
|
+
* BOTH `close` AND the two stream ENDS are awaited, not just `exit`: `exit` fires when the
|
|
349
|
+
* process dies and `close` when its stdio have also closed, and resolving on the former
|
|
350
|
+
* would let a job report `done` with its last lines still in the pipe. That is the same
|
|
351
|
+
* ordering the reference gets for free by draining `p.stdout` to EOF before `p.wait()`.
|
|
352
|
+
*/
|
|
353
|
+
_pump(jid, p) {
|
|
354
|
+
return new Promise((resolve, reject) => {
|
|
355
|
+
const sink = (s) => this._append(jid, s);
|
|
356
|
+
const out = textDecoder(sink);
|
|
357
|
+
const err = textDecoder(sink);
|
|
358
|
+
p.stdout.on('data', (b) => out.push(b));
|
|
359
|
+
p.stderr.on('data', (b) => err.push(b));
|
|
360
|
+
p.on('error', reject);
|
|
361
|
+
p.on('close', (code, signal) => {
|
|
362
|
+
out.end();
|
|
363
|
+
err.end();
|
|
364
|
+
// `p.returncode` for a signalled child is `-SIGNUM`, and Node splits the exit code
|
|
365
|
+
// and the signal into two arguments with the code `null` when a signal won. Only
|
|
366
|
+
// `cancel()` produces one, and only on POSIX — which no cell can reach, so the map
|
|
367
|
+
// covers what this program SENDS rather than every signal that exists.
|
|
368
|
+
resolve(code === null ? -(SIGNUM[signal] ?? 1) : code);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Terminate the running job's subprocess; the run thread then winds down normally (stdout
|
|
375
|
+
* closes, the exit code is non-zero, the status becomes `failed`).
|
|
376
|
+
*/
|
|
377
|
+
cancel(jid) {
|
|
378
|
+
const p = this._procs.get(jid);
|
|
379
|
+
const j = this._jobs.get(jid);
|
|
380
|
+
if (p === undefined || !j || j.status !== 'running') return false;
|
|
381
|
+
this._append(jid, '\n[web] cancelled by user.\n');
|
|
382
|
+
killJobTree(p);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
get(jid) {
|
|
387
|
+
const j = this._jobs.get(jid);
|
|
388
|
+
return j ? { ...j } : null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* `_web_jobs.action_commands` — action name -> a list of argvs, each a separate step, stopping
|
|
394
|
+
* at the first failure.
|
|
395
|
+
*
|
|
396
|
+
* THE TABLE IS THE DISPATCH, so `PORTED_ACTIONS` is its own keys rather than a list beside it
|
|
397
|
+
* (the rule `POST_ROUTES` established one file over, for the reason M23 gave).
|
|
398
|
+
*
|
|
399
|
+
* `build` renders the DEPLOYED install in its detected theme + emit + footprint + posture +
|
|
400
|
+
* mode, so a rebuild from an imperial, lean, mentor, foreman opencode-global install stays all
|
|
401
|
+
* five. `update` and `export` self-resolve the deployed theme downstream and take no args.
|
|
402
|
+
*
|
|
403
|
+
* THE ARGV HEAD IS THE PORT. The reference names `sys.executable` and `rituals/harness.py` (or
|
|
404
|
+
* `build.py`); this names `process.execPath` and `bin/geneseed-cli.mjs` (or `bin/geneseed.mjs`,
|
|
405
|
+
* which is `build.py`'s twin). The two can never be byte-equal, `tests/web_golden.py`
|
|
406
|
+
* normalises the `$ <argv>` echo line for that reason, and `tests/test_web_jobs.py` asserts
|
|
407
|
+
* each side's head ABSOLUTELY while comparing the tails literally — which is where a real port
|
|
408
|
+
* bug would live.
|
|
409
|
+
*/
|
|
410
|
+
const NODE = () => process.execPath;
|
|
411
|
+
const CLI = () => path.join(ROOT, 'bin', 'geneseed-cli.mjs');
|
|
412
|
+
const GEN = () => path.join(ROOT, 'bin', 'geneseed.mjs');
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The rows whose VERB has not crossed. **EMPTY SINCE P10b, AND STILL DECLARED.**
|
|
416
|
+
*
|
|
417
|
+
* P6g opened it with three rows and wrote the rule that closes it: each removed by the phase
|
|
418
|
+
* that ports its verb. `update` left in P8c; `link` and `unlink` left in P10b, which is the
|
|
419
|
+
* phase that also had to decide whether those two verbs should exist under npm at all
|
|
420
|
+
* (`js/link.mjs`'s docblock carries the argument, and the deciding vote was these two rows —
|
|
421
|
+
* the console's Settings buttons POST here, and only a ported verb can answer them).
|
|
422
|
+
*
|
|
423
|
+
* WHY AN EMPTY SET RATHER THAN A DELETED ONE. The `ast` cross-check in
|
|
424
|
+
* `tests/test_web_jobs.py` reads this set against `action_commands`' own keys, and an empty
|
|
425
|
+
* set is the strongest form that check ever takes: it asserts that EVERY action the reference
|
|
426
|
+
* dispatches on is answered by the Node runner. Deleting the declaration would delete that
|
|
427
|
+
* assertion, and the next unported action would come back as a silent 404 — the thing the
|
|
428
|
+
* 501 exists to prevent.
|
|
429
|
+
*
|
|
430
|
+
* A 501 AND NOT THE 404 THE TABLE WOULD PRODUCE. `action_commands` returns `None` for an
|
|
431
|
+
* unknown action and the reference maps that to `{"error": "unknown action <x>"}` at 404 — so
|
|
432
|
+
* a real-but-unported action falling through would answer the same thing a typo gets. With
|
|
433
|
+
* this set empty the 501 branch is now UNREACHABLE by construction, which is exactly what the
|
|
434
|
+
* dispatcher probe in `tests/test_web_server.py` asserts: every real action answers 202 and
|
|
435
|
+
* only an invented one answers 404.
|
|
436
|
+
*/
|
|
437
|
+
export const NOT_PORTED_ACTIONS = new Set();
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The three actions `_post_routes` answers INLINE, without consulting the table: `restore` is
|
|
441
|
+
* synchronous and returns a result rather than a job id, and `install`/`deploy` resolve their
|
|
442
|
+
* argv from the request body first. Declared so the `ast` cross-check covers every `action ==`
|
|
443
|
+
* the reference dispatches on, not only the table's keys.
|
|
444
|
+
*/
|
|
445
|
+
export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
|
446
|
+
|
|
447
|
+
function actionTable({
|
|
448
|
+
theme = 'neutral', emit = 'opencode-global', footprint = 'full',
|
|
449
|
+
posture = 'peer', mode = 'direct',
|
|
450
|
+
} = {}) {
|
|
451
|
+
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode);
|
|
452
|
+
return {
|
|
453
|
+
doctor: [[NODE(), CLI(), 'doctor']],
|
|
454
|
+
build: [[NODE(), GEN(), ...buildArgv]],
|
|
455
|
+
// Rebuild EVERY active install in place (each in its own theme+emit). The per-install
|
|
456
|
+
// resolution lives in the rebuild-all subcommand, so the web layer threads no theme/emit.
|
|
457
|
+
'build-all': [[NODE(), CLI(), 'rebuild-all']],
|
|
458
|
+
// P8c. The row names `upgrade`, which is the VERB; `update` is the action name here and
|
|
459
|
+
// argparse's alias there, and the reference's own row is `[py, harness.py, "upgrade"]` for
|
|
460
|
+
// the same reason. The daemon bounce this job must not do itself is `GENESEED_WEB_JOB`'s
|
|
461
|
+
// job, set by `_run` above and read by `js/update.mjs` — P6g's contract with P8, honoured.
|
|
462
|
+
update: [[NODE(), CLI(), 'upgrade']],
|
|
463
|
+
export: [[NODE(), CLI(), 'diff', '--out']],
|
|
464
|
+
// Local-machine maintenance, surfaced in the web Settings. uninstall keeps memory (never
|
|
465
|
+
// deleted) and runs non-interactively with --yes.
|
|
466
|
+
link: [[NODE(), CLI(), 'link']],
|
|
467
|
+
unlink: [[NODE(), CLI(), 'unlink']],
|
|
468
|
+
uninstall: [[NODE(), CLI(), 'uninstall', '--yes']],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function actionCommands(action, opts = {}) {
|
|
473
|
+
const table = actionTable(opts);
|
|
474
|
+
// `dict.get(action)` — `None` for anything the table does not name, which the HTTP layer
|
|
475
|
+
// maps to `{"error": "unknown action <x>"}` at 404.
|
|
476
|
+
return Object.hasOwn(table, action) ? table[action] : null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** `action_commands`' own keys — the table IS the declaration, so this cannot drift from it. */
|
|
480
|
+
export const PORTED_ACTIONS = Object.keys(actionTable());
|