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/cli.mjs
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `js/cli-table.json` — the CLI as data, and the one place either implementation describes it.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS. `rituals/harness.py`'s `build_argparser()` was 25 subparsers (26
|
|
5
|
+
* invocable names — `update` aliases `upgrade`) and 46 `add_argument` calls. Node cannot
|
|
6
|
+
* introspect argparse, and P6d deferred the console's `cli` docs page for exactly that
|
|
7
|
+
* reason: a Node twin could only be a hand-written copy of the whole CLI surface, describing
|
|
8
|
+
* verbs `bin/geneseed-cli.mjs` cannot even run. This port's standing rule is that a copy of a
|
|
9
|
+
* value under test stops being the value under test, and that would have been the largest
|
|
10
|
+
* copy in it.
|
|
11
|
+
*
|
|
12
|
+
* P10c's answer was to make the metadata DATA — generated into `cli.json` at the repo root by
|
|
13
|
+
* `tests/gen_cli_reference.py` and read by both sides. P2 finishes the thought: the table is
|
|
14
|
+
* no longer GENERATED from anything, it is the OWNED document, and it lives here on a product
|
|
15
|
+
* path because that is where a document npm ships belongs. `package.json`'s `files[]` already
|
|
16
|
+
* carries `js/`, so it needs no row of its own, and nothing has to import out of `tests/`.
|
|
17
|
+
*
|
|
18
|
+
* ITS READERS, and each one takes a different view of the same rows:
|
|
19
|
+
*
|
|
20
|
+
* * `cliReference()` — `js/web/docs.mjs`'s `cli` kind, the page P6d deferred. STRIPS the
|
|
21
|
+
* hidden arguments and the two fields the page never carried;
|
|
22
|
+
* * `cliSpec()` — `bin/geneseed-cli.mjs`'s argument parser. That table used to carry its
|
|
23
|
+
* OWN hand-written transcription of the same `add_argument` calls, gated against the
|
|
24
|
+
* subparsers by name only, so `VERBS` is now `{ fn }` per row and everything
|
|
25
|
+
* argparse-shaped is derived here. KEEPS the hidden arguments, `--port`'s int type and
|
|
26
|
+
* `theme`'s mutex group, none of which the page may show;
|
|
27
|
+
* * `cliCommand()`/`printHelp()` — `--help` on BOTH entry points, so the table owes a row to
|
|
28
|
+
* every verb `bin/geneseed-hook.mjs` dispatches as well, not only to the CLI entry's.
|
|
29
|
+
*
|
|
30
|
+
* THE STORED SHAPE IS THE UNION AND MUST STAY ONE. Neither reader's shape can be the file's:
|
|
31
|
+
* a file filtered for the page breaks the CLI, and a file in `cliSpec`'s shape cannot render
|
|
32
|
+
* the docs. It stays exactly what the argparse walk produced.
|
|
33
|
+
*
|
|
34
|
+
* WHAT USED TO GUARD IT, AND WHAT DOES NOW. While the parser existed, `cliReferenceProblems()`
|
|
35
|
+
* hashed `rituals/harness.py` and doctor reported drift on both binaries. That digest was over
|
|
36
|
+
* a file this migration deletes, so it is gone; `tests/test_cli_reference.py` holds the
|
|
37
|
+
* argparse-vs-table comparison, and it can only ever cover the rows argparse HAS: `catalog`,
|
|
38
|
+
* `mcp` and `memory` never had a subparser and are outside it by construction. The successor
|
|
39
|
+
* that checks the WHOLE table, and outlives the parser, is `tests/unit/cli_table.test.mjs`;
|
|
40
|
+
* after that the table is simply the source of truth and is edited directly.
|
|
41
|
+
*
|
|
42
|
+
* THE COST, NAMED: this entry cannot parse anything without this file. `js/checkout.mjs`
|
|
43
|
+
* already reads `harness.config.json` at import for every render, so a tracked data document
|
|
44
|
+
* the code needs is not a new shape here — and this is not the hook entry, whose latency
|
|
45
|
+
* budget is `bin/geneseed-hook.mjs`'s and is untouched.
|
|
46
|
+
*/
|
|
47
|
+
import { readFileSync } from 'node:fs';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
|
|
50
|
+
import { ROOT } from './checkout.mjs';
|
|
51
|
+
import { pyInt, pyStripSpace } from './lib/fs.mjs';
|
|
52
|
+
|
|
53
|
+
const CLI_JSON = path.join(ROOT, 'js', 'cli-table.json');
|
|
54
|
+
|
|
55
|
+
/** The keys the docs page has always carried, per argument — `type` and `hidden` are ours. */
|
|
56
|
+
const PAGE_ARG_KEYS = ['names', 'dest', 'metavar', 'help', 'choices', 'default',
|
|
57
|
+
'required', 'nargs', 'is_flag'];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `js/cli-table.json`, parsed. An unreadable file is the empty reference; the entry points
|
|
61
|
+
* turn that into a refusal that names itself rather than a parse with no rules.
|
|
62
|
+
*
|
|
63
|
+
* NOT MEMOISED, and the first draft was. `harness.load_cli_reference` re-reads the file on
|
|
64
|
+
* every call, so a cache here would make an edited table visible to the reference's running
|
|
65
|
+
* daemon and invisible to this one until it restarted — a divergence no cell can reach and no
|
|
66
|
+
* reviewer would look for. One re-read of a file this size is not a cost worth buying that with.
|
|
67
|
+
*/
|
|
68
|
+
function load() {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(CLI_JSON, 'utf-8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return { prog: 'harness', commands: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pageArgs = (args) => (args ?? [])
|
|
77
|
+
.filter((a) => !a.hidden)
|
|
78
|
+
.map((a) => Object.fromEntries(PAGE_ARG_KEYS.map((k) => [k, a[k] ?? null])));
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `harness.load_cli_reference` — the file as the docs PAGE consumes it.
|
|
82
|
+
*
|
|
83
|
+
* The page is a VIEW of the file, not the other way round: `cliSpec` below needs the
|
|
84
|
+
* arguments argparse hides and the page must not show them, so the file carries everything
|
|
85
|
+
* and each reader filters. `?? null` reproduces `dict.get(k)` for a key a future generator
|
|
86
|
+
* stops writing — `undefined` would vanish from `JSON.stringify` and change the wire shape.
|
|
87
|
+
*/
|
|
88
|
+
export function cliReference() {
|
|
89
|
+
const data = load();
|
|
90
|
+
return {
|
|
91
|
+
prog: data.prog ?? 'harness',
|
|
92
|
+
commands: (data.commands ?? []).map((c) => ({
|
|
93
|
+
name: c.name ?? '',
|
|
94
|
+
help: c.help ?? '',
|
|
95
|
+
description: c.description ?? '',
|
|
96
|
+
positionals: pageArgs(c.positionals),
|
|
97
|
+
options: pageArgs(c.options),
|
|
98
|
+
})),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `-h/--help`, which the table does NOT carry.
|
|
104
|
+
*
|
|
105
|
+
* argparse adds it at parser construction, before any `add_argument` the walk can see, so it
|
|
106
|
+
* is absent from every command in the file and present in every help text the reference
|
|
107
|
+
* prints. Reproduced here as a row rather than special-cased in the renderer, because it is
|
|
108
|
+
* first in the options section and first in the usage line for every verb the table carries —
|
|
109
|
+
* a position it only gets by being an action like the others.
|
|
110
|
+
*/
|
|
111
|
+
const HELP_ACTION = {
|
|
112
|
+
names: ['-h', '--help'],
|
|
113
|
+
dest: 'help',
|
|
114
|
+
metavar: null,
|
|
115
|
+
help: 'show this help message and exit',
|
|
116
|
+
choices: null,
|
|
117
|
+
required: false,
|
|
118
|
+
nargs: '0',
|
|
119
|
+
is_flag: true,
|
|
120
|
+
hidden: false,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** argparse's `_metavar_formatter`: an explicit metavar, else the choices, else the default. */
|
|
124
|
+
function metavarFor(a, positional) {
|
|
125
|
+
if (a.metavar !== null && a.metavar !== undefined) return a.metavar;
|
|
126
|
+
if (a.choices) return `{${a.choices.join(',')}}`;
|
|
127
|
+
return positional ? a.dest : a.dest.toUpperCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** argparse's `_format_args`, for the three `nargs` spellings this parser uses. */
|
|
131
|
+
function formatArgs(a, positional) {
|
|
132
|
+
const mv = metavarFor(a, positional);
|
|
133
|
+
if (a.nargs === '0') return '';
|
|
134
|
+
if (a.nargs === '?') return `[${mv}]`;
|
|
135
|
+
if (a.nargs === '*') return `[${mv} ...]`;
|
|
136
|
+
return mv;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The usage-line spelling of one action, WITHOUT the `[…]` an optional gets around it. */
|
|
140
|
+
function usagePart(a, positional) {
|
|
141
|
+
if (positional) return formatArgs(a, true);
|
|
142
|
+
const args = formatArgs(a, false);
|
|
143
|
+
return args ? `${a.names[0]} ${args}` : a.names[0];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** argparse's `_format_action_invocation` — the left column of the two-column body. */
|
|
147
|
+
function actionInvocation(a, positional) {
|
|
148
|
+
if (positional) return metavarFor(a, true);
|
|
149
|
+
const args = formatArgs(a, false);
|
|
150
|
+
return args ? `${a.names.join(', ')} ${args}` : a.names.join(', ');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `textwrap`'s `wordsep_re`, which is what makes `pyTextWrap` below a port rather than a guess.
|
|
154
|
+
//
|
|
155
|
+
// A GREEDY SPACE-ONLY WRAP WAS THE FIRST DRAFT AND THE GATE REFUSED IT. `textwrap` splits a
|
|
156
|
+
// hyphenated word into two chunks, so it can break a line inside `back-compat` where a
|
|
157
|
+
// space-only wrap cannot. Measured before deciding: every help string then in the table disagreed
|
|
158
|
+
// with a greedy wrap at some width, the widest at 175 — so "the branch is dead" was simply
|
|
159
|
+
// false, and `test_the_ports_line_breaker_is_textwrap_at_every_width` said so on the run that
|
|
160
|
+
// first asserted it. The rules are transcribed instead.
|
|
161
|
+
//
|
|
162
|
+
// Python's classes, spelled for a `u`-flagged JavaScript regex: `\w` is letters, numbers and
|
|
163
|
+
// underscore; `[^\d\W]` is that minus the DECIMAL digits, which leaves the letter-ish half
|
|
164
|
+
// (`\p{Nl}`/`\p{No}` are `\w` and not `\d` in Python, so they stay). `\s` is the one class left
|
|
165
|
+
// spelled as itself, and the two languages differ on U+FEFF — Python excludes it, JavaScript
|
|
166
|
+
// does not. The gate sweeps the real help strings at every width rather than assuming that
|
|
167
|
+
// stays unreachable.
|
|
168
|
+
const WD = '[\\p{L}\\p{N}_]';
|
|
169
|
+
const LT = '[\\p{L}\\p{Nl}\\p{No}_]';
|
|
170
|
+
const WP = '[\\p{L}\\p{N}_!"\'&.,?]';
|
|
171
|
+
const WORDSEP = new RegExp(
|
|
172
|
+
'(\\s+' // any whitespace
|
|
173
|
+
+ `|(?<=${WP})-{2,}(?=${WD})` // em-dash between words
|
|
174
|
+
+ '|\\S+?(?:' // word, possibly hyphenated
|
|
175
|
+
+ `-(?:(?<=${LT}{2}-)|(?<=${LT}-${LT}-))(?=${LT}-?${LT})`
|
|
176
|
+
+ '|(?=\\s|$)'
|
|
177
|
+
+ `|(?<=${WP})(?=-{2,}${WD})))`, 'u');
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* `textwrap.wrap(text, width)` for the settings argparse's `_split_lines` uses: no indent,
|
|
181
|
+
* `drop_whitespace`, `break_long_words` and `break_on_hyphens` all on, and the text already
|
|
182
|
+
* whitespace-collapsed and stripped by the caller.
|
|
183
|
+
*
|
|
184
|
+
* ponytail: lives here rather than in `js/lib/` because `formatHelp` is its only consumer.
|
|
185
|
+
* `rituals/_harness_tui*.py` calls `textwrap.wrap` in seven places and is declared unported;
|
|
186
|
+
* move this when that changes, not before.
|
|
187
|
+
*/
|
|
188
|
+
export function pyTextWrap(text, width) {
|
|
189
|
+
const chunks = text.split(WORDSEP).filter(Boolean).reverse();
|
|
190
|
+
const blank = (s) => s.trim() === '';
|
|
191
|
+
const lines = [];
|
|
192
|
+
while (chunks.length) {
|
|
193
|
+
const line = [];
|
|
194
|
+
let len = 0;
|
|
195
|
+
if (blank(chunks[chunks.length - 1]) && lines.length) chunks.pop();
|
|
196
|
+
while (chunks.length && len + chunks[chunks.length - 1].length <= width) {
|
|
197
|
+
len += chunks[chunks.length - 1].length;
|
|
198
|
+
line.push(chunks.pop());
|
|
199
|
+
}
|
|
200
|
+
// `_handle_long_word`: a chunk too wide for any line is cut to what is left of this one —
|
|
201
|
+
// after the last hyphen inside that room, if there is one with something before it.
|
|
202
|
+
//
|
|
203
|
+
// `room > 0` IS CPython's gh-139065 fix — `if self.break_long_words and space_left > 0:`,
|
|
204
|
+
// landed on main as 1c598e0 and cherry-picked to 3.13 as b1bc743, first shipped in 3.13.14.
|
|
205
|
+
// Without it a line already exactly `width` long gets `chunk.slice(0, 0)` — an EMPTY string
|
|
206
|
+
// — appended, and that empty chunk then absorbs the single trailing-whitespace `pop` below,
|
|
207
|
+
// so the real whitespace chunk survives and the line keeps a trailing space. This machine's
|
|
208
|
+
// 3.13.5 predates the fix, so the sweep gate agreed with the unguarded port here and
|
|
209
|
+
// disagreed on CI's 3.13.14. `room <= 0` needs no `elif not cur_line` arm: an empty line has
|
|
210
|
+
// `len === 0`, so `room` is `width` (or 1 when `width < 1`) and is never <= 0 there.
|
|
211
|
+
const room = width < 1 ? 1 : width - len;
|
|
212
|
+
if (room > 0 && chunks.length && chunks[chunks.length - 1].length > width) {
|
|
213
|
+
const chunk = chunks[chunks.length - 1];
|
|
214
|
+
let end = room;
|
|
215
|
+
if (chunk.length > room) {
|
|
216
|
+
const hyphen = chunk.lastIndexOf('-', room - 1);
|
|
217
|
+
if (hyphen > 0 && [...chunk.slice(0, hyphen)].some((c) => c !== '-')) end = hyphen + 1;
|
|
218
|
+
}
|
|
219
|
+
line.push(chunk.slice(0, end));
|
|
220
|
+
chunks[chunks.length - 1] = chunk.slice(end);
|
|
221
|
+
len = line.reduce((n, s) => n + s.length, 0);
|
|
222
|
+
}
|
|
223
|
+
if (line.length && blank(line[line.length - 1])) line.pop();
|
|
224
|
+
if (line.length) lines.push(line.join(''));
|
|
225
|
+
}
|
|
226
|
+
return lines.length ? lines : [''];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* argparse's `_format_usage`, including the re-flow it does when the line does not fit.
|
|
231
|
+
*
|
|
232
|
+
* `prog` is the WHOLE program word — `harness diff`, `geneseed diff` — because that is what
|
|
233
|
+
* the indent of a wrapped continuation line is computed from, which is why the divergence
|
|
234
|
+
* cannot be patched into a recorded text after the fact.
|
|
235
|
+
*/
|
|
236
|
+
function formatUsage(prog, optParts, posParts, width) {
|
|
237
|
+
const prefix = 'usage: ';
|
|
238
|
+
const flat = [prog, [...optParts, ...posParts].join(' ')].filter(Boolean).join(' ');
|
|
239
|
+
if (prefix.length + flat.length <= width) return prefix + flat;
|
|
240
|
+
|
|
241
|
+
const getLines = (parts, indent, pre) => {
|
|
242
|
+
const out = [];
|
|
243
|
+
let line = [];
|
|
244
|
+
let lineLen = (pre === undefined ? indent.length : pre.length) - 1;
|
|
245
|
+
for (const part of parts) {
|
|
246
|
+
if (lineLen + 1 + part.length > width && line.length) {
|
|
247
|
+
out.push(indent + line.join(' '));
|
|
248
|
+
line = [];
|
|
249
|
+
lineLen = indent.length - 1;
|
|
250
|
+
}
|
|
251
|
+
line.push(part);
|
|
252
|
+
lineLen += 1 + part.length;
|
|
253
|
+
}
|
|
254
|
+
if (line.length) out.push(indent + line.join(' '));
|
|
255
|
+
if (pre !== undefined && out.length) out[0] = out[0].slice(indent.length);
|
|
256
|
+
return out;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
let lines;
|
|
260
|
+
if (prefix.length + prog.length <= 0.75 * width) {
|
|
261
|
+
const indent = ' '.repeat(prefix.length + prog.length + 1);
|
|
262
|
+
if (optParts.length) {
|
|
263
|
+
lines = getLines([prog, ...optParts], indent, prefix);
|
|
264
|
+
lines.push(...getLines(posParts, indent));
|
|
265
|
+
} else if (posParts.length) {
|
|
266
|
+
lines = getLines([prog, ...posParts], indent, prefix);
|
|
267
|
+
} else {
|
|
268
|
+
lines = [prog];
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
const indent = ' '.repeat(prefix.length);
|
|
272
|
+
lines = getLines([...optParts, ...posParts], indent);
|
|
273
|
+
if (lines.length > 1) {
|
|
274
|
+
lines = [...getLines(optParts, indent), ...getLines(posParts, indent)];
|
|
275
|
+
}
|
|
276
|
+
lines = [prog, ...lines];
|
|
277
|
+
}
|
|
278
|
+
return prefix + lines.join('\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** argparse's `_format_action` — the two-column row, and the wrapped help beside it. */
|
|
282
|
+
function formatAction(a, positional, helpPosition, width) {
|
|
283
|
+
const actionWidth = helpPosition - 2 - 2;
|
|
284
|
+
const header = actionInvocation(a, positional);
|
|
285
|
+
const help = a.help ?? '';
|
|
286
|
+
const parts = [];
|
|
287
|
+
let indentFirst = 0;
|
|
288
|
+
if (!help) {
|
|
289
|
+
parts.push(` ${header}\n`);
|
|
290
|
+
} else if (header.length <= actionWidth) {
|
|
291
|
+
parts.push(` ${header.padEnd(actionWidth)} `);
|
|
292
|
+
} else {
|
|
293
|
+
parts.push(` ${header}\n`);
|
|
294
|
+
indentFirst = helpPosition;
|
|
295
|
+
}
|
|
296
|
+
if (help && help.trim()) {
|
|
297
|
+
// `_whitespace_matcher` is `re.compile(r'\s+', re.ASCII)` — the ASCII class, so a help
|
|
298
|
+
// string carrying U+00A0 keeps it rather than having it collapsed to a break opportunity.
|
|
299
|
+
const collapsed = help.replace(/[ \t\n\v\f\r]+/g, ' ').trim();
|
|
300
|
+
const lines = pyTextWrap(collapsed, Math.max(width - helpPosition, 11));
|
|
301
|
+
parts.push(`${' '.repeat(indentFirst)}${lines[0]}\n`);
|
|
302
|
+
for (const line of lines.slice(1)) parts.push(`${' '.repeat(helpPosition)}${line}\n`);
|
|
303
|
+
} else if (!parts[0].endsWith('\n')) {
|
|
304
|
+
parts.push('\n');
|
|
305
|
+
}
|
|
306
|
+
return parts.join('');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* `parser.format_help()` for one subcommand, byte for byte with argparse.
|
|
311
|
+
*
|
|
312
|
+
* WHY THE PORT RENDERS THIS AT ALL. Task 10 compared the two implementations' `--help` for the
|
|
313
|
+
* first time — `tests/harness_golden.py` has zero `--help` cells — and found the port answering
|
|
314
|
+
* `geneseed: error: unrecognized arguments: --help`, exit 2, on all 26 verbs. That is not the
|
|
315
|
+
* declared wording divergence `parse`'s docblock reserves for argparse's ERROR text: it is the
|
|
316
|
+
* absence of the surface, on the binary that survives the reference's deletion.
|
|
317
|
+
*
|
|
318
|
+
* `prog` IS AN ARGUMENT AND THAT IS THE ONE DELIBERATE DIVERGENCE. The reference names itself
|
|
319
|
+
* `harness`, after the Python file this migration deletes; a user who typed `geneseed status
|
|
320
|
+
* --help` is shown `usage: geneseed status`, which is also how every error this entry prints
|
|
321
|
+
* already spells itself (`die`). `tests/__snapshots__/help/` freezes the reference's answers
|
|
322
|
+
* and `tests/snapshot/cli_help.test.mjs` replays them through here with `prog` set to the reference's
|
|
323
|
+
* own — so the layout is gated byte for byte and the rename is the only thing that moves.
|
|
324
|
+
*
|
|
325
|
+
* NOT EVERY VERB HAS A RECORDED ANSWER, AND THREE NEVER CAN. `catalog`, `mcp` and `memory` had
|
|
326
|
+
* no subparser for the recorder to render from, so the corpus covers the verbs argparse
|
|
327
|
+
* answered to and those three are held ABSOLUTELY instead — `tests/snapshot/cli_help.test.mjs` names
|
|
328
|
+
* both populations (`RECORDED`, `NATIVE`) and fails a verb that falls into neither, which is
|
|
329
|
+
* what keeps "no recorded text" from meaning "no gate".
|
|
330
|
+
*
|
|
331
|
+
* The alias rides on the same argument: argparse gives `update` the parser object it built for
|
|
332
|
+
* `upgrade`, so `harness.py update --help` prints `usage: harness upgrade`. The recorded prog
|
|
333
|
+
* map reproduces that; the shipped call passes `geneseed update`, which is what the user typed.
|
|
334
|
+
*
|
|
335
|
+
* `width` is argparse's `shutil.get_terminal_size().columns - 2`, passed in rather than read,
|
|
336
|
+
* because a recorded help text whose wrap column came from the recorder's terminal is a corpus
|
|
337
|
+
* that reddens on the next machine — the rot vector this branch has now found nine ways.
|
|
338
|
+
*/
|
|
339
|
+
export function formatHelp(cmd, prog, width) {
|
|
340
|
+
const positionals = (cmd.positionals ?? []).filter((a) => !a.hidden);
|
|
341
|
+
const options = [HELP_ACTION, ...(cmd.options ?? []).filter((a) => !a.hidden)];
|
|
342
|
+
|
|
343
|
+
const optParts = [];
|
|
344
|
+
const done = new Set();
|
|
345
|
+
for (const a of options) {
|
|
346
|
+
const group = (cmd.mutex ?? []).find((g) => g.includes(a.names[0]));
|
|
347
|
+
if (group === undefined) {
|
|
348
|
+
optParts.push(a.required ? usagePart(a, false) : `[${usagePart(a, false)}]`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// `add_mutually_exclusive_group()` — never required in this parser, so `[a | b]`, and the
|
|
352
|
+
// members lose their own brackets to it. Emitted once, at the first member's position.
|
|
353
|
+
//
|
|
354
|
+
// ONE PART PER MEMBER, NOT ONE PER GROUP, and that distinction is the whole of a real
|
|
355
|
+
// divergence this port shipped. `_get_actions_usage_parts` decorates the members IN PLACE:
|
|
356
|
+
// the first gets the opening bracket, every part but the last gains a trailing ` |`, the
|
|
357
|
+
// last gains the closing bracket — and it returns them as SEPARATE parts. `_format_usage`
|
|
358
|
+
// then wraps on the part boundaries, so argparse can break the line INSIDE the group and
|
|
359
|
+
// end a line with `[--solid-only |`. A single atomic `[a | b]` part cannot be broken at
|
|
360
|
+
// all, so the two layouts diverge at every width where the group straddles the wrap
|
|
361
|
+
// column: measured on `theme`, 73 of the 181 widths in COLUMNS 20..200, and 64 of those in
|
|
362
|
+
// the arm the fixtures exercise. COLUMNS=80 — the one width the 26 recorded fixtures are
|
|
363
|
+
// rendered at — is not one of them, which is why nothing saw it.
|
|
364
|
+
if (done.has(group)) continue;
|
|
365
|
+
done.add(group);
|
|
366
|
+
const members = group.map((f) => usagePart(options.find((o) => o.names.includes(f)), false));
|
|
367
|
+
members[0] = `[${members[0]}`;
|
|
368
|
+
members[members.length - 1] += ']';
|
|
369
|
+
for (let i = 0; i < members.length - 1; i += 1) members[i] += ' |';
|
|
370
|
+
optParts.push(...members);
|
|
371
|
+
}
|
|
372
|
+
const posParts = positionals.map((a) => usagePart(a, true));
|
|
373
|
+
|
|
374
|
+
// `_action_max_length` is the widest invocation in the WHOLE parser plus the section indent,
|
|
375
|
+
// and the hidden actions contribute nothing to it — `add_argument` drops a SUPPRESSed one
|
|
376
|
+
// before it is measured, which is why `web`'s column is set by `{start,stop,restart,status}`
|
|
377
|
+
// and not by `--daemon-internal`.
|
|
378
|
+
const invocations = [...positionals.map((a) => actionInvocation(a, true)),
|
|
379
|
+
...options.map((a) => actionInvocation(a, false))];
|
|
380
|
+
// `_max_help_position` IS ITSELF A FUNCTION OF THE WIDTH, which the port had baked as the
|
|
381
|
+
// constant 24: `HelpFormatter.__init__` is
|
|
382
|
+
// `min(max_help_position, max(width - 20, indent_increment * 2))`, so the two-column layout
|
|
383
|
+
// collapses to a 4-column indent on a narrow terminal and every action's help moves to its
|
|
384
|
+
// own line. Equal to 24 for every width from 44 up, which is why the fixtures at width 78
|
|
385
|
+
// could not see it and why only a width SWEEP could.
|
|
386
|
+
const maxHelpPosition = Math.min(24, Math.max(width - 20, 2 * 2));
|
|
387
|
+
const helpPosition = Math.min(Math.max(...invocations.map((s) => s.length)) + 2 + 2,
|
|
388
|
+
maxHelpPosition);
|
|
389
|
+
|
|
390
|
+
const blocks = [`${formatUsage(prog, optParts, posParts, width)}\n`];
|
|
391
|
+
if (positionals.length) {
|
|
392
|
+
blocks.push(`positional arguments:\n${positionals
|
|
393
|
+
.map((a) => formatAction(a, true, helpPosition, width)).join('')}`);
|
|
394
|
+
}
|
|
395
|
+
blocks.push(`options:\n${options
|
|
396
|
+
.map((a) => formatAction(a, false, helpPosition, width)).join('')}`);
|
|
397
|
+
// `HelpFormatter.format_help`'s last two steps: collapse a run of blank lines, then trim to
|
|
398
|
+
// exactly one trailing newline.
|
|
399
|
+
return `${blocks.join('\n').replace(/\n\n\n+/g, '\n\n').replace(/^\n+|\n+$/g, '')}\n`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The width argparse would use, from the same inputs: `COLUMNS` first, then the terminal, then
|
|
404
|
+
* 80 — minus the two columns `HelpFormatter.__init__` subtracts. (`shutil.get_terminal_size`
|
|
405
|
+
* reads `COLUMNS` before it asks the tty, and falls back to 80 when neither answers, which is
|
|
406
|
+
* every pipe both implementations write help into.)
|
|
407
|
+
*
|
|
408
|
+
* `pyInt`, NOT `Number.parseInt`, and the tree already owned it. `get_terminal_size` spells
|
|
409
|
+
* this `int(os.environ['COLUMNS'])` inside `except (KeyError, ValueError)`, and `int` is not
|
|
410
|
+
* `parseInt`. Measured against the reference over a corpus of `$COLUMNS` values
|
|
411
|
+
* (`test_the_ports_help_width_is_shutil_get_terminal_size`): `COLUMNS=1_0` is 10 to Python and
|
|
412
|
+
* 1 to `parseInt`, `COLUMNS=٣` is 3 and NaN, `COLUMNS=' 80 '` is 80 and NaN. `COLUMNS=80abc`
|
|
413
|
+
* is a ValueError to Python and 80 to `parseInt` — the same answer down a PIPE, where the
|
|
414
|
+
* fallback is 80 either way, and a different one on any terminal that is not 80 wide, which is
|
|
415
|
+
* why the corpus is the gate rather than a cell. A non-positive value falls through on both
|
|
416
|
+
* sides, which is Python's `if columns <= 0` and was already right here.
|
|
417
|
+
*
|
|
418
|
+
* `pyStripSpace` because THIS is the caller `pyInt`'s docblock did not have: its other two
|
|
419
|
+
* strip before calling, and `$COLUMNS` arrives however the shell set it — `int(' 80 ')` is 80.
|
|
420
|
+
*/
|
|
421
|
+
export function helpWidth() {
|
|
422
|
+
const env = pyInt(pyStripSpace(process.env.COLUMNS ?? ''));
|
|
423
|
+
if (env !== null && env > 0) return env - 2;
|
|
424
|
+
return (process.stdout.columns > 0 ? process.stdout.columns : 80) - 2;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** One command's raw row — everything the table knows, unlike the page's filtered view. */
|
|
428
|
+
export function cliCommand(verb) {
|
|
429
|
+
return (load().commands ?? []).find((c) => c.name === verb) ?? null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* `<verb> --help` for whichever binary asked, or `null` for a verb the table does not
|
|
434
|
+
* describe — the caller already owns that refusal and its message.
|
|
435
|
+
*
|
|
436
|
+
* ONE OWNER FOR TWO ENTRIES. `bin/geneseed-hook.mjs` carries the four hook verbs and
|
|
437
|
+
* `bin/geneseed-cli.mjs` the rest of the table, and
|
|
438
|
+
* `test_the_two_entry_points_carry_disjoint_verb_sets` keeps them from learning each other's
|
|
439
|
+
* tables — so the help each prints has to come from here rather than from a copy in each.
|
|
440
|
+
*
|
|
441
|
+
* CRLF ON WINDOWS for `die`'s reason: argparse writes through a Python text stream, which
|
|
442
|
+
* translates, and this text lands in the same terminals and the same pipes as that one.
|
|
443
|
+
*/
|
|
444
|
+
export function printHelp(prog, verb) {
|
|
445
|
+
const cmd = cliCommand(verb);
|
|
446
|
+
if (cmd === null) return null;
|
|
447
|
+
const text = formatHelp(cmd, `${prog} ${verb}`, helpWidth());
|
|
448
|
+
process.stdout.write(process.platform === 'win32' ? text.replaceAll('\n', '\r\n') : text);
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** argparse's `dest` is snake_case; every JS name in this port is camelCase. */
|
|
453
|
+
const camel = (dest) => dest.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* One subcommand's argparse surface, in the shape `bin/geneseed-cli.mjs`'s `parse` consumes.
|
|
457
|
+
*
|
|
458
|
+
* EVERY ROW OF THIS MAPPING WAS A HAND-WRITTEN COMMENT IN THAT FILE'S `VERBS` TABLE, and each
|
|
459
|
+
* one names a failure it prevents:
|
|
460
|
+
*
|
|
461
|
+
* * a positional with `nargs="?"` is OPTIONAL — `link [dir]`, `upgrade [ref] [theme]`;
|
|
462
|
+
* * a positional with `nargs="*"` is the trailing SINK (`bootstrap extra`), and `parse`
|
|
463
|
+
* keeps it from absorbing options, which is what stops `bootstrap --no-setpu` from
|
|
464
|
+
* running a full update-and-wizard while the user believes they asked for neither;
|
|
465
|
+
* * `store_true` is `nargs === '0'` and takes no value, so `--global=x` is an error;
|
|
466
|
+
* * an OPTION with `nargs="?"` is argparse's `const=True` — bare `diff --out` means "the
|
|
467
|
+
* default timestamped path" and `--out FILE` means the string. A plain option would
|
|
468
|
+
* refuse the bare form; a flag would drop the filename;
|
|
469
|
+
* * `type=int` refuses `--port abc` instead of silently binding 4747;
|
|
470
|
+
* * a mutually exclusive group refuses `--solid-only --transparent-only`, in the order the
|
|
471
|
+
* group declares them, which is the order the message names them in.
|
|
472
|
+
*
|
|
473
|
+
* `null` for a verb the file does not describe — the caller turns that into a refusal rather
|
|
474
|
+
* than a parse with no rules, because a spec of `{}` would accept anything and bind nothing.
|
|
475
|
+
*/
|
|
476
|
+
export function cliSpec(verb) {
|
|
477
|
+
const cmd = (load().commands ?? []).find((c) => c.name === verb);
|
|
478
|
+
if (cmd === undefined) return null;
|
|
479
|
+
const spec = { positionals: [] };
|
|
480
|
+
for (const a of cmd.positionals ?? []) {
|
|
481
|
+
if (a.nargs === '*') { spec.variadic = a.dest; continue; }
|
|
482
|
+
const p = { name: a.dest };
|
|
483
|
+
if (a.nargs === '?') p.optional = true;
|
|
484
|
+
if (a.choices) p.choices = a.choices;
|
|
485
|
+
spec.positionals.push(p);
|
|
486
|
+
}
|
|
487
|
+
for (const a of cmd.options ?? []) {
|
|
488
|
+
const dest = camel(a.dest);
|
|
489
|
+
// eslint-disable-next-line no-nested-ternary
|
|
490
|
+
const table = a.is_flag ? 'flags' : (a.nargs === '?' ? 'optValue' : 'options');
|
|
491
|
+
for (const name of a.names ?? []) {
|
|
492
|
+
(spec[table] ??= {})[name] = dest;
|
|
493
|
+
if (a.type === 'int') (spec.ints ??= []).push(name);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if ((cmd.mutex ?? []).length) spec.mutex = cmd.mutex;
|
|
497
|
+
return spec;
|
|
498
|
+
}
|