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/status.mjs
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness status` and `harness version` — `rituals/_harness_status.py`, and the parts of
|
|
3
|
+
* four other modules those two reach.
|
|
4
|
+
*
|
|
5
|
+
* WHAT THE CLOSURE ACTUALLY IS, MEASURED. An `ast` walk over `rituals/` puts `status` at 527
|
|
6
|
+
* LOC across 7 modules with 17 `build.*` names, which is what carried it through two phases
|
|
7
|
+
* as "the cheap half at 159 LOC". Both numbers are wrong in opposite directions, and the
|
|
8
|
+
* second one is the interesting one: a closure walk counts what the CALLEE computes, and
|
|
9
|
+
* `_status_data` consumes a fraction of it.
|
|
10
|
+
*
|
|
11
|
+
* * `_tui_inventory` returns every agent and skill with its one-line purpose, full
|
|
12
|
+
* rendered body, source path and lifecycle badge, and every law with its title, body
|
|
13
|
+
* and governance class. `_status_data` reads three `len()`s off it. So `SKILL_CLASS`,
|
|
14
|
+
* `LAW_CLASS`, `ENTITY_STATUSES`, `load_registry`, `entity_status` and
|
|
15
|
+
* `build._first_blockquote` — the taxonomy P7 owns, ~111 LOC — are in the walk and are
|
|
16
|
+
* not in this file. `inventoryCounts` is the counting half and says so.
|
|
17
|
+
* * `_installed_defaults` detects theme, emit, posture, mode and footprint. The panel
|
|
18
|
+
* prints theme and emit. `_posture_of_dir`/`_mode_of_dir` are therefore not part of the
|
|
19
|
+
* value it returns, and `status/posture-and-mode-in-the-carrier-change-nothing` is the
|
|
20
|
+
* cell that says the omission is invisible rather than assuming it. **P5f ported those
|
|
21
|
+
* two detectors and did NOT change this** — `rebuild-all` calls them per install root,
|
|
22
|
+
* not through `_installed_defaults`, so the cell above is still a positive control over
|
|
23
|
+
* an unchanged omission rather than a claim that has quietly gone stale.
|
|
24
|
+
* * `_footprint_of_dir` is reached from here, and it is the exception that names the rule:
|
|
25
|
+
* its return value is not consumed by the panel either, but an unrecognised marker makes
|
|
26
|
+
* it WARN on stderr, and stderr is a compared column. A closure pruned by what the
|
|
27
|
+
* caller consumes drops a function whose whole contribution is a side effect.
|
|
28
|
+
* * `_memory_facts` builds a record per fact through `_frontmatter`; the panel counts
|
|
29
|
+
* them. `memoryFactCount` is the count, and `frontmatter` stays where it is.
|
|
30
|
+
*
|
|
31
|
+
* WHERE THE DETECTORS WENT. `themeOfDir`, `footprintOfDir`, `installedDefaults`,
|
|
32
|
+
* `manifestIsClaude` and the configured defaults were written here in P5d because `status`
|
|
33
|
+
* was their only caller. `js/installs.mjs` owns them since P5f, when `diff` and `rebuild-all`
|
|
34
|
+
* became the second and third. Nothing about them changed in the move — the 166-cell
|
|
35
|
+
* acceptance matrix is what licensed attempting it, `status`'s own eleven cells among them.
|
|
36
|
+
*
|
|
37
|
+
* WHAT NO CELL CAN REACH, and where it is gated instead. `tests/harness_golden.py`'s
|
|
38
|
+
* status/version section carries the long form; the short form is that ROOT is not
|
|
39
|
+
* redirectable across a process boundary, so three things here are unreachable from any
|
|
40
|
+
* cell: `versionVerdict`'s "up to date" (needs a marker holding a fingerprint no cell can
|
|
41
|
+
* know), `accentFor`'s cyan fallback (an unknown theme is refused upstream by
|
|
42
|
+
* `effectiveTheme`), and the whole ANSI half of `statusLines` (`_color_enabled` is
|
|
43
|
+
* `sys.stdout.isatty()`, and every harness captures stdout through a pipe). All three are
|
|
44
|
+
* PURE FUNCTIONS of their arguments, so all three are gated as a corpus in
|
|
45
|
+
* `tests/test_pure_function_parity.py` instead — which is a third answer to the colour
|
|
46
|
+
* question the P5c handoff posed as a choice between shipping it ungated and not shipping
|
|
47
|
+
* it.
|
|
48
|
+
*
|
|
49
|
+
* `manifestIsClaude` was a FOURTH until wave 2 of the P0/P1 review: it is only reached for
|
|
50
|
+
* a candidate with no known host, and `ROOT/"Harness"` was ordered ahead of the sandbox's
|
|
51
|
+
* own. Both ROOT-relative bundle candidates are gone from this walk and from
|
|
52
|
+
* `installedDefaults`, so the only no-known-host candidate left is `cwd/"Harness"`, which
|
|
53
|
+
* is inside the sandbox. It is reachable now; no cell seeds a manifest without an emit
|
|
54
|
+
* marker, so it is still gated only by the corpus, and that is a gap rather than a wall.
|
|
55
|
+
*/
|
|
56
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
57
|
+
import path from 'node:path';
|
|
58
|
+
|
|
59
|
+
import { THEMES, ROOT, makeCfg } from './checkout.mjs';
|
|
60
|
+
import { tuiInventory } from './inventory.mjs';
|
|
61
|
+
import { readVersion, sourceFingerprint } from './emit.mjs';
|
|
62
|
+
import {
|
|
63
|
+
claudeConfigDir, bobConfigDir, copilotConfigDir, opencodeConfigDir, pyResolve,
|
|
64
|
+
resolveMemoryDir,
|
|
65
|
+
} from './hosts.mjs';
|
|
66
|
+
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
67
|
+
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
68
|
+
// three verbs now read them. `defaultTheme`, `manifestIsClaude` and `installedDefaults` are
|
|
69
|
+
// re-exported below because `tests/fixtures/pure_probe.mjs` names this module for two of them
|
|
70
|
+
// and a corpus that followed the code to its new file would stop testing the caller's view.
|
|
71
|
+
import {
|
|
72
|
+
defaultTheme, installedDefaults, manifestIsClaude, readJsonMaybe, readMaybe,
|
|
73
|
+
} from './installs.mjs';
|
|
74
|
+
import { pyLen, pyLjust, pyPrint } from './lib/fs.mjs';
|
|
75
|
+
|
|
76
|
+
export { defaultTheme, manifestIsClaude };
|
|
77
|
+
|
|
78
|
+
// ---- version ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/** `_harness_status._version_verdict`. */
|
|
81
|
+
export function versionVerdict(installed, current) {
|
|
82
|
+
if (installed === null || installed === undefined) {
|
|
83
|
+
return 'no Geneseed install detected to compare';
|
|
84
|
+
}
|
|
85
|
+
if (installed === current) return 'up to date with this source';
|
|
86
|
+
return 'installed build differs from the current source — run '
|
|
87
|
+
+ '`./geneseed update` (or rebuild) to apply it';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `_harness_status.cmd_version`. */
|
|
91
|
+
export function cmdVersion(args) {
|
|
92
|
+
const cfg = makeCfg();
|
|
93
|
+
const current = sourceFingerprint(cfg);
|
|
94
|
+
// `Path(args.target).expanduser().resolve()` — `pyResolve` already expands, so the tilde
|
|
95
|
+
// is handled once rather than twice.
|
|
96
|
+
let target = args.target ? pyResolve(args.target) : opencodeConfigDir();
|
|
97
|
+
let installed = readVersion(target);
|
|
98
|
+
if (installed === null) {
|
|
99
|
+
// A claude/bob/copilot-only machine must not report "no install detected". The bundle
|
|
100
|
+
// candidates are cwd-relative only — see the Python for why `ROOT / "Harness"` left.
|
|
101
|
+
for (const base of [claudeConfigDir(), bobConfigDir(), copilotConfigDir(),
|
|
102
|
+
path.join(process.cwd(), 'Harness'), process.cwd()]) {
|
|
103
|
+
const v = readVersion(base);
|
|
104
|
+
if (v) { installed = v; target = base; break; }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
pyPrint(`[version] source: ${current} (${ROOT})\n`);
|
|
108
|
+
pyPrint(`[version] installed: ${installed || '(none found)'}`
|
|
109
|
+
+ (installed ? ` (${target})` : '') + '\n');
|
|
110
|
+
pyPrint(`[version] ${versionVerdict(installed, current)}\n`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- what the panel counts ---------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `_harness_tui._tui_inventory`, reduced to the three counts `_status_data` reads off it.
|
|
118
|
+
*
|
|
119
|
+
* THREE `length`s OFF THE REAL WALK SINCE P6c, and the change is the point. P5d shipped a
|
|
120
|
+
* counting-only twin — the bodies, purposes, source paths and taxonomy classes were P7's,
|
|
121
|
+
* and porting them to produce three integers would have been absurd — with a docblock
|
|
122
|
+
* warning that the two must not become two classifiers. `api_catalog` consumes exactly the
|
|
123
|
+
* fields the count threw away, so P6c is the phase that would have made them two. It did
|
|
124
|
+
* not: `tuiInventory` in `js/inventory.mjs` is the one walk, this is three `length`s off
|
|
125
|
+
* it, and the eleven `status` cells and the catalog cells now gate the same classifier
|
|
126
|
+
* from opposite ends.
|
|
127
|
+
*/
|
|
128
|
+
export function inventoryCounts(themeName) {
|
|
129
|
+
const inv = tuiInventory(themeName);
|
|
130
|
+
return { agents: inv.agents.length, skills: inv.skills.length, laws: inv.laws.length };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `_harness_tui_views._memory_facts`, reduced to its length.
|
|
135
|
+
*
|
|
136
|
+
* A path `glob("*.md")` matches but `read_text` cannot read — a DIRECTORY called `notes.md`
|
|
137
|
+
* is the reachable case — is skipped, not counted and not fatal.
|
|
138
|
+
*/
|
|
139
|
+
export function memoryFactCount(mdir) {
|
|
140
|
+
let names;
|
|
141
|
+
try { names = readdirSync(mdir); } catch { return 0; }
|
|
142
|
+
let n = 0;
|
|
143
|
+
for (const name of names.filter((x) => x.endsWith('.md')).sort()) {
|
|
144
|
+
const stem = name.slice(0, -3);
|
|
145
|
+
if (['memory', 'readme'].includes(stem.toLowerCase())) continue;
|
|
146
|
+
if (readMaybe(path.join(mdir, name)) === null) continue;
|
|
147
|
+
n += 1;
|
|
148
|
+
}
|
|
149
|
+
return n;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `_harness_tui_draw._accent_for` — the ACCENT a theme declares, default cyan. */
|
|
153
|
+
export function accentFor(theme) {
|
|
154
|
+
const doc = readJsonMaybe(path.join(THEMES, `${theme}.json`));
|
|
155
|
+
if (!doc || typeof doc !== 'object') return 'cyan';
|
|
156
|
+
return doc.ACCENT ?? 'cyan';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- the panel ----------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/** `_harness_status._status_data`. */
|
|
162
|
+
export function statusData() {
|
|
163
|
+
const inst = installedDefaults();
|
|
164
|
+
const theme = inst.theme || defaultTheme();
|
|
165
|
+
const mdir = resolveMemoryDir(null);
|
|
166
|
+
const inv = inventoryCounts(theme);
|
|
167
|
+
let cfgDir = null;
|
|
168
|
+
try { cfgDir = opencodeConfigDir(); } catch { cfgDir = null; }
|
|
169
|
+
const sourceFp = sourceFingerprint(makeCfg());
|
|
170
|
+
let installedFp = null;
|
|
171
|
+
let verTarget = null;
|
|
172
|
+
const otherCfg = [];
|
|
173
|
+
for (const fn of [claudeConfigDir, bobConfigDir, copilotConfigDir]) {
|
|
174
|
+
try { otherCfg.push(fn()); } catch { /* a missing host dir must not sink status */ }
|
|
175
|
+
}
|
|
176
|
+
// Same walk as `cmdVersion`'s fallback, and cwd-relative for the same reason.
|
|
177
|
+
const candidates = [...(cfgDir ? [cfgDir] : []), ...otherCfg,
|
|
178
|
+
path.join(process.cwd(), 'Harness'), process.cwd()];
|
|
179
|
+
for (const base of candidates) {
|
|
180
|
+
const v = readVersion(base);
|
|
181
|
+
if (v) { installedFp = v; verTarget = base; break; }
|
|
182
|
+
}
|
|
183
|
+
const agentMd = (inst.emit === 'opencode-global' && cfgDir)
|
|
184
|
+
? path.join(cfgDir, 'AGENT.md') : null;
|
|
185
|
+
return {
|
|
186
|
+
theme,
|
|
187
|
+
accent: accentFor(theme),
|
|
188
|
+
emit: inst.emit || '—',
|
|
189
|
+
agents: inv.agents,
|
|
190
|
+
skills: inv.skills,
|
|
191
|
+
laws: inv.laws,
|
|
192
|
+
memory_dir: mdir ? String(mdir) : null,
|
|
193
|
+
facts: mdir ? memoryFactCount(mdir) : 0,
|
|
194
|
+
source_fp: sourceFp,
|
|
195
|
+
installed_fp: installedFp,
|
|
196
|
+
version_target: verTarget ? String(verTarget) : null,
|
|
197
|
+
version_verdict: versionVerdict(installedFp, sourceFp),
|
|
198
|
+
agent_md: agentMd ? String(agentMd) : null,
|
|
199
|
+
agent_md_present: Boolean(agentMd && existsSync(agentMd)),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `_harness_status._ANSI_CODES`. */
|
|
204
|
+
const ANSI_CODES = {
|
|
205
|
+
red: '31', green: '32', yellow: '33', blue: '34', magenta: '35', cyan: '36', white: '37',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `_harness_status._color_enabled`.
|
|
210
|
+
*
|
|
211
|
+
* `process.stdout.isTTY` is `undefined` rather than `false` off a terminal, so the Boolean
|
|
212
|
+
* is not optional. The three terms are Python's, in Python's order — and because `and`
|
|
213
|
+
* short-circuits on the first, NO_COLOR and TERM are never read through a pipe, which is
|
|
214
|
+
* why `golden.cell_env` clearing them is insurance rather than a fix.
|
|
215
|
+
*/
|
|
216
|
+
export function colorEnabled() {
|
|
217
|
+
return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined
|
|
218
|
+
&& process.env.TERM !== 'dumb';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* `_harness_status._status_lines` — pure, returns the lines.
|
|
223
|
+
*
|
|
224
|
+
* `_TUI_ASCII` is read from the environment at CALL time here where Python reads it at
|
|
225
|
+
* import time (`_harness_tui_draw:12`). Same value for a process that runs one verb and
|
|
226
|
+
* exits; spelled this way because a module-level constant in a file the corpus gate imports
|
|
227
|
+
* would freeze at whatever the test runner's environment was.
|
|
228
|
+
*/
|
|
229
|
+
export function statusLines(d, color = false) {
|
|
230
|
+
const asc = Boolean(process.env.GENESEED_TUI_ASCII);
|
|
231
|
+
const [H, V] = asc ? ['-', '|'] : ['─', '│'];
|
|
232
|
+
const [TL, TR, BL, BR, LT, RT] = asc
|
|
233
|
+
? ['+', '+', '+', '+', '+', '+'] : ['┌', '┐', '└', '┘', '├', '┤'];
|
|
234
|
+
const DOT = asc ? '-' : '·';
|
|
235
|
+
const badge = asc ? '*' : '◆';
|
|
236
|
+
const emdash = asc ? '-' : '—';
|
|
237
|
+
|
|
238
|
+
const up = d.version_verdict.includes('up to date');
|
|
239
|
+
const noneInst = d.installed_fp === null || d.installed_fp === undefined;
|
|
240
|
+
const mark = up ? (asc ? 'OK' : '✓') : (noneInst ? (asc ? '-' : '·') : '!');
|
|
241
|
+
const vcode = up ? '32' : (noneInst ? '2' : '33');
|
|
242
|
+
|
|
243
|
+
const rows = [
|
|
244
|
+
['theme', `${d.theme} (accent: ${d.accent})`],
|
|
245
|
+
['install', d.emit],
|
|
246
|
+
['components', `${d.agents} agents ${DOT} ${d.skills} skills ${DOT} ${d.laws} laws`],
|
|
247
|
+
['memory', `${d.memory_dir || '(not found)'} `
|
|
248
|
+
+ `(${d.facts} fact${d.facts === 1 ? '' : 's'})`],
|
|
249
|
+
['version', `${d.installed_fp || '(none)'} ${DOT} source ${d.source_fp}`],
|
|
250
|
+
];
|
|
251
|
+
if (d.agent_md) {
|
|
252
|
+
rows.push(['AGENT.md',
|
|
253
|
+
`${d.agent_md} (${d.agent_md_present ? 'present' : 'MISSING'})`]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const labelW = Math.max(...rows.map(([k]) => pyLen(k)));
|
|
257
|
+
const body = rows.map(([k, v]) => ` ${pyLjust(k, labelW)} ${v}`);
|
|
258
|
+
const verdict = ` ${mark} ${d.version_verdict}`;
|
|
259
|
+
const title = ` ${badge} Geneseed ${emdash} status `;
|
|
260
|
+
const width = Math.max(...body.map(pyLen), pyLen(verdict), pyLen(title) + 2);
|
|
261
|
+
|
|
262
|
+
const ac = ANSI_CODES[d.accent] ?? '36';
|
|
263
|
+
const c = (s, code) => (color ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
264
|
+
|
|
265
|
+
const top = color
|
|
266
|
+
? c(TL + H, ac) + c(title, `${ac};1`) + c(H.repeat(width - pyLen(title) - 1) + TR, ac)
|
|
267
|
+
: TL + H + title + H.repeat(width - pyLen(title) - 1) + TR;
|
|
268
|
+
const edge = c(V, ac);
|
|
269
|
+
const lines = [top];
|
|
270
|
+
for (const b of body) lines.push(edge + pyLjust(b, width) + edge);
|
|
271
|
+
lines.push(c(LT + H.repeat(width) + RT, ac));
|
|
272
|
+
lines.push(edge + c(pyLjust(verdict, width), vcode) + edge);
|
|
273
|
+
lines.push(c(BL + H.repeat(width) + BR, ac));
|
|
274
|
+
return lines;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** `_harness_status.cmd_status`. */
|
|
278
|
+
export function cmdStatus() {
|
|
279
|
+
for (const line of statusLines(statusData(), colorEnabled())) pyPrint(`${line}\n`);
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
package/js/themes.mjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `_build_render.sync_themes` / `_insert_theme_keys` — the generator's `--sync-themes`, the
|
|
3
|
+
* maintainer tool that fills every shipped theme with the keys `_TEMPLATE.json` defines.
|
|
4
|
+
*
|
|
5
|
+
* IT IS A BYTE-EXACT TEXTUAL EDIT OF COMMITTED FILES, which is what makes it the most
|
|
6
|
+
* dangerous thing in this phase and why it has its own corpus before it has a port. Every
|
|
7
|
+
* other tool this port has crossed WRITES A TREE it also renders; this one reaches into
|
|
8
|
+
* fourteen files a human wrote, inserts a line into each, and leaves the other ~170 lines
|
|
9
|
+
* untouched. A port that got the insertion right and the newline translation wrong would
|
|
10
|
+
* rewrite every line of every theme on the first maintainer run, and `git diff` — not a
|
|
11
|
+
* gate — is where that would be discovered.
|
|
12
|
+
*
|
|
13
|
+
* NEITHER `--sync-themes` NOR `--validate-only` IS REACHABLE FROM ANY CELL. `golden.py`'s
|
|
14
|
+
* `_argv` emits neither flag (argued in situ at `bin/geneseed.mjs`'s dispatch), so the
|
|
15
|
+
* acceptance matrix that gates all nine emits is structurally blind to both, and the driver
|
|
16
|
+
* refused them rather than crossing for exactly that reason. The gate is
|
|
17
|
+
* `tests/test_maintainer_tools_parity.py`: a corpus of hand-made theme directories run
|
|
18
|
+
* through BOTH implementations, comparing the printed output, the return value, and the
|
|
19
|
+
* resulting FILE BYTES.
|
|
20
|
+
*
|
|
21
|
+
* WHY THIS MODULE IS NOT `js/doctor.mjs`. `--sync-themes` has no doctor dependency, so it
|
|
22
|
+
* stays on the generator driver where its flag lives — and the driver is under a transitive
|
|
23
|
+
* ban on starting processes, which this module's imports respect. Its sibling
|
|
24
|
+
* `--validate-only` needs the doctor and therefore could not stay; see `cmdValidate`.
|
|
25
|
+
*
|
|
26
|
+
* THE PRIMITIVES ARE BORROWED, NOT REWRITTEN. `pySplitLines`, `pyStripSpace`/`pyLStripSpace`/
|
|
27
|
+
* `pyRStripSpace`, `parseJson`, `pyEq`, `jsonDumpsCompact`, `jsonDumpsIndent`, `readText` and
|
|
28
|
+
* `writeText` all already exist and are all already gated. This file's documented failure
|
|
29
|
+
* mode is the same primitive existing twice under two names and the two disagreeing, and
|
|
30
|
+
* `str.strip` and `json.dumps` are precisely the two that would have been re-derived here.
|
|
31
|
+
*/
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
|
|
34
|
+
import { THEMES } from './checkout.mjs';
|
|
35
|
+
import { themeFiles } from './installs.mjs';
|
|
36
|
+
import { pySplitLines } from './lib/udiff.mjs';
|
|
37
|
+
import {
|
|
38
|
+
jsonDumpsCompact, jsonDumpsIndent, parseJson, pyEq, pyLStripSpace, pyRStripSpace,
|
|
39
|
+
pyStripSpace, readText, writeText,
|
|
40
|
+
} from './lib/fs.mjs';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `_build_render._insert_theme_keys` — insert ONLY the missing keys into the theme's existing
|
|
44
|
+
* TEXT, each as one new line at its template-order position.
|
|
45
|
+
*
|
|
46
|
+
* A full `json.dumps` round-trip cannot reproduce the shipped files byte-for-byte (they mix
|
|
47
|
+
* raw Unicode with legacy `\\uXXXX` escapes), so re-dumping made a one-key sync a ~170-line
|
|
48
|
+
* rewrite per theme. Textual insertion keeps the diff to the inserted line plus at most a
|
|
49
|
+
* comma on its predecessor.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` when the file does not follow the shipped convention (`{` alone on line 1,
|
|
52
|
+
* one ` "KEY": value` per line) or when the result fails the reparse safety check — the
|
|
53
|
+
* caller then falls back to a full re-dump.
|
|
54
|
+
*
|
|
55
|
+
* FOUR PRIMITIVES CARRY THE WHOLE THING, and each is a place the two languages differ:
|
|
56
|
+
* - `pySplitLines` is `str.splitlines()`, which breaks on U+2028/U+2029 where `split('\\n')`
|
|
57
|
+
* does not — a theme file that acquired one from a paste would be re-lined by Python and
|
|
58
|
+
* not by a naive port, and the insertion index would then point at a different key;
|
|
59
|
+
* - the three `strip` spellings are Python's whitespace set, not `String.prototype.trim`'s;
|
|
60
|
+
* - `jsonDumpsCompact(v, {ensureAscii: false})` is `json.dumps(v, ensure_ascii=False)`,
|
|
61
|
+
* including the `', '`/`': '` separators — which matter for exactly ONE key,
|
|
62
|
+
* `AGENT_COLORS`, the only non-string among `_TEMPLATE.json`'s 137 values. `ensureAscii`
|
|
63
|
+
* is false because the reference passes `ensure_ascii=False`, and 33 template values
|
|
64
|
+
* carry non-ASCII text that would otherwise be escaped into the committed file;
|
|
65
|
+
* - `pyEq`, because the safety net compares two `json.loads` results and `===` is identity
|
|
66
|
+
* for containers — a `===` here would return `null` every time and silently route every
|
|
67
|
+
* theme through the re-dump fallback, i.e. rewrite all 14 files in full.
|
|
68
|
+
*/
|
|
69
|
+
export function insertThemeKeys(raw, theme, tmpl, tmplKeys, missing) {
|
|
70
|
+
const lines = pySplitLines(raw);
|
|
71
|
+
if (!lines.length || pyStripSpace(lines[0]) !== '{') return null;
|
|
72
|
+
|
|
73
|
+
const lineOf = (key) => {
|
|
74
|
+
// `f' "{key}":'` — raw interpolation, NOT a JSON-escaped key. A theme key carrying a
|
|
75
|
+
// quote would not match on either side, which is the same answer both give.
|
|
76
|
+
const prefix = ` "${key}":`;
|
|
77
|
+
for (let i = 0; i < lines.length; i += 1) if (lines[i].startsWith(prefix)) return i;
|
|
78
|
+
return null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Every existing key must be locatable as its own line, or the format assumption is wrong
|
|
82
|
+
// and inserting would corrupt the file.
|
|
83
|
+
for (const k of Object.keys(theme)) if (lineOf(k) === null) return null;
|
|
84
|
+
|
|
85
|
+
for (const k of missing) { // template order, so an earlier insert can anchor a later one
|
|
86
|
+
const entry = ` "${k}": ${jsonDumpsCompact(tmpl[k], { ensureAscii: false })}`;
|
|
87
|
+
let pred = null;
|
|
88
|
+
const earlier = tmplKeys.slice(0, tmplKeys.indexOf(k));
|
|
89
|
+
for (let i = earlier.length - 1; i >= 0; i -= 1) {
|
|
90
|
+
const li = lineOf(earlier[i]);
|
|
91
|
+
if (li !== null) { pred = li; break; }
|
|
92
|
+
}
|
|
93
|
+
if (pred === null) {
|
|
94
|
+
// No earlier template key exists in this theme: insert right after `{`.
|
|
95
|
+
const follows = lines.slice(1).some((ln) => pyLStripSpace(ln).startsWith('"'));
|
|
96
|
+
lines.splice(1, 0, entry + (follows ? ',' : ''));
|
|
97
|
+
} else if (pyRStripSpace(lines[pred]).endsWith(',')) {
|
|
98
|
+
lines.splice(pred + 1, 0, `${entry},`);
|
|
99
|
+
} else {
|
|
100
|
+
// Predecessor was the last entry: it gains the comma, the new line is last.
|
|
101
|
+
lines[pred] += ',';
|
|
102
|
+
lines.splice(pred + 1, 0, entry);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const newText = `${lines.join('\n')}\n`;
|
|
107
|
+
// Safety net: the surgically-edited text must parse back to exactly the merge we meant.
|
|
108
|
+
let reparsed;
|
|
109
|
+
try { reparsed = parseJson(newText); } catch { return null; }
|
|
110
|
+
const want = { ...theme };
|
|
111
|
+
for (const k of missing) want[k] = tmpl[k];
|
|
112
|
+
return pyEq(reparsed, want) ? newText : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `_build_render.sync_themes` — fill every shipped theme with the template's keys.
|
|
117
|
+
*
|
|
118
|
+
* Returns the number of themes actually modified (0 == already in sync); the driver maps that
|
|
119
|
+
* to the exit code, so CI can run the tool as a drift check.
|
|
120
|
+
*
|
|
121
|
+
* `themesDir` is a parameter for the corpus, whose whole subject is what this writes; the
|
|
122
|
+
* default is the checkout's own `themes/`, which is what the driver passes.
|
|
123
|
+
*
|
|
124
|
+
* PRINTS THROUGH A BARE `process.stdout.write`, like the generator's other ~25 print sites and
|
|
125
|
+
* unlike every CLI verb: `bin/geneseed.mjs`'s `main` wraps the whole run in `withPyNewlines`,
|
|
126
|
+
* so a `pyPrint` here would translate a second time and put `\r\r\n` on every line.
|
|
127
|
+
*
|
|
128
|
+
* ONE DELIBERATE DIVERGENCE FROM THE REFERENCE, changed on BOTH sides in this commit: a
|
|
129
|
+
* missing or unreadable `_TEMPLATE.json` used to print and `return 0`, which the CLI maps to
|
|
130
|
+
* exit 0 — "already in sync". That is a false green in the one job the tool exists for, and
|
|
131
|
+
* the input that produces it is the one that makes the whole run vacuous. It now refuses with
|
|
132
|
+
* exit 2. Dropping the interpolated exception text is the second half of the same change and
|
|
133
|
+
* is what makes the branch gateable at all: `json.JSONDecodeError` and V8's parse error
|
|
134
|
+
* disagree on both wording and offset, so a message carrying `{e}` can never be compared
|
|
135
|
+
* across the two implementations. The PER-THEME unreadable message below keeps its `{e}` and
|
|
136
|
+
* therefore keeps that divergence, matching the convention `js/doctor.mjs` already ships at
|
|
137
|
+
* six sites — a skipped theme is a warning, not the tool's verdict.
|
|
138
|
+
*/
|
|
139
|
+
export function syncThemes(themesDir = THEMES) {
|
|
140
|
+
const tmplPath = path.join(themesDir, '_TEMPLATE.json');
|
|
141
|
+
let tmpl;
|
|
142
|
+
try {
|
|
143
|
+
tmpl = parseJson(readText(tmplPath));
|
|
144
|
+
} catch {
|
|
145
|
+
process.stdout.write(`[sync-themes] ${path.basename(tmplPath)} is missing or unreadable `
|
|
146
|
+
+ '— there is nothing to sync against.\n');
|
|
147
|
+
const e = new Error('template unreadable');
|
|
148
|
+
e.exitCode = 2;
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
const tmplKeys = Object.keys(tmpl);
|
|
152
|
+
let changed = 0;
|
|
153
|
+
for (const p of themeFiles(themesDir)) {
|
|
154
|
+
const name = path.basename(p);
|
|
155
|
+
let raw;
|
|
156
|
+
let theme;
|
|
157
|
+
try {
|
|
158
|
+
raw = readText(p);
|
|
159
|
+
theme = parseJson(raw);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
process.stdout.write(`[sync-themes] ${name}: unreadable (${e.message}) — skipped\n`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// `Object.hasOwn`, never `k in theme`: a theme defining a `constructor` or `toString`
|
|
165
|
+
// voice token would otherwise be reported as already carrying every key it lacks.
|
|
166
|
+
const missing = tmplKeys.filter((k) => !Object.hasOwn(theme, k));
|
|
167
|
+
const extra = Object.keys(theme).filter((k) => !Object.hasOwn(tmpl, k)).sort();
|
|
168
|
+
if (!missing.length) {
|
|
169
|
+
if (extra.length) {
|
|
170
|
+
process.stdout.write(`[sync-themes] ${name}: in sync (extra key(s) not in template, `
|
|
171
|
+
+ `not removed: ${extra.join(', ')})\n`);
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
let newText = insertThemeKeys(raw, theme, tmpl, tmplKeys, missing);
|
|
176
|
+
if (newText === null) {
|
|
177
|
+
// Unconventional formatting: fall back to a full re-dump — template order, theme-only
|
|
178
|
+
// extras preserved at the end, real Unicode kept as-is.
|
|
179
|
+
const merged = {};
|
|
180
|
+
for (const k of tmplKeys) merged[k] = Object.hasOwn(theme, k) ? theme[k] : tmpl[k];
|
|
181
|
+
for (const k of Object.keys(theme)) if (!Object.hasOwn(merged, k)) merged[k] = theme[k];
|
|
182
|
+
newText = `${jsonDumpsIndent(merged, { ensureAscii: false })}\n`;
|
|
183
|
+
}
|
|
184
|
+
writeText(p, newText);
|
|
185
|
+
changed += 1;
|
|
186
|
+
process.stdout.write(`[sync-themes] ${name}: added ${missing.length} key(s) from the `
|
|
187
|
+
+ `template — RESTYLE these in this theme's voice: ${missing.join(', ')}\n`);
|
|
188
|
+
if (extra.length) {
|
|
189
|
+
process.stdout.write(`[sync-themes] ${name}: extra key(s) not in template, not removed: `
|
|
190
|
+
+ `${extra.join(', ')}\n`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!changed) process.stdout.write('[sync-themes] all themes already carry every template key.\n');
|
|
194
|
+
return changed;
|
|
195
|
+
}
|