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/lib/proc.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spawn flag — one rule for every child process this tool starts, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Windows gives every console child of a CONSOLE-LESS parent a brand-new console window.
|
|
5
|
+
* The web daemon is console-less by design (`DETACHED_PROCESS`), so without this flag every
|
|
6
|
+
* short-lived process it shells out to pops a window and closes it — the Doctor page is the
|
|
7
|
+
* worst case, running `node --check` once per plugin, and it is what a user actually sees:
|
|
8
|
+
* seven consoles flashing on a dashboard load.
|
|
9
|
+
*
|
|
10
|
+
* WHY IT LIVES HERE AND NOT IN `js/update.mjs`, WHICH DEFINED IT FIRST. It was private to
|
|
11
|
+
* that module and six of its spawns used it, so the four spawn sites in `js/doctor.mjs`,
|
|
12
|
+
* `js/hooks.mjs`, `js/setup.mjs` and `js/link.mjs` each had to remember the flag on their
|
|
13
|
+
* own. Two of them did not — and the miss is BYTE-INVISIBLE: a console window is not
|
|
14
|
+
* something the CLI prints, so every recorded cell under `tests/__snapshots__/` stays green
|
|
15
|
+
* while the windows pop. The property under test never reaches the comparison, which is why
|
|
16
|
+
* a recorded corpus cannot be the gate here. A shared constant plus
|
|
17
|
+
* `tests/unit/spawn_hygiene.test.mjs` is what makes the next spawn site inherit the answer
|
|
18
|
+
* instead of re-deciding it.
|
|
19
|
+
*
|
|
20
|
+
* CAPTURING SPAWNS ONLY, and the asymmetry is deliberate. The rule was measured on Windows:
|
|
21
|
+
* a process that sets `STARTF_USESTDHANDLES` only when a stream is redirected gives an
|
|
22
|
+
* INHERITING child spawned with `CREATE_NO_WINDOW` a fresh hidden console and discards
|
|
23
|
+
* everything it prints — `geneseed build > log.txt` wrote an empty file. libuv always passes
|
|
24
|
+
* the handles explicitly, so the flag on an inheriting spawn would be harmless here; it stays
|
|
25
|
+
* off anyway, because an inheriting child of a console-owning parent shows no window in the
|
|
26
|
+
* first place, so it would buy nothing and re-open a failure mode already paid for once.
|
|
27
|
+
*/
|
|
28
|
+
export const NO_WINDOW = { windowsHide: process.platform === 'win32' };
|
package/js/lib/udiff.mjs
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified diff — this tool's sequence matcher, and the hunks it produces.
|
|
3
|
+
*
|
|
4
|
+
* WHY A HAND-WRITTEN MATCHER RATHER THAN A LIBRARY. The diff is the ENTIRE user-visible
|
|
5
|
+
* payload of `geneseed diff`: the hunks in `--full` and in the exported improvements file
|
|
6
|
+
* are this file's output and nothing else's. A generic JS diff would produce a CORRECT diff
|
|
7
|
+
* and a DIFFERENT one, and `tests/__snapshots__/primitives/` compares the hunks character
|
|
8
|
+
* for character, so "also a valid diff" fails every cell. The alignment below IS the
|
|
9
|
+
* specification of what this tool prints, and three of its choices are choices no other
|
|
10
|
+
* algorithm makes:
|
|
11
|
+
*
|
|
12
|
+
* * It is NOT Myers. `findLongestMatch` takes the longest common block, recurses left and
|
|
13
|
+
* right, and therefore prefers a different alignment than an edit-script minimiser
|
|
14
|
+
* wherever the two disagree.
|
|
15
|
+
* * Ties resolve to the EARLIEST match, in `a` first and then in `b`, because `bestsize` is
|
|
16
|
+
* only replaced on a strict `>`.
|
|
17
|
+
* * **`autojunk`.** For a `b` of 200 elements or more, any line occurring more than
|
|
18
|
+
* `floor(len(b) / 100) + 1` times is deleted from the index and stops matching entirely.
|
|
19
|
+
* On this tool's own files — hundreds of lines, many of them blank — that fires
|
|
20
|
+
* constantly and changes the hunks. It is off by construction for anything shorter, so a
|
|
21
|
+
* small fixture cannot see it at all; the recorded corpus is what carries inputs long
|
|
22
|
+
* enough to trip it.
|
|
23
|
+
*
|
|
24
|
+
* PROVENANCE, not a contract with a live second party. Every rule above was measured from
|
|
25
|
+
* CPython's `difflib`, which is why the odd ones are odd — `autojunk` in particular is a
|
|
26
|
+
* heuristic no one would invent from scratch. That history explains the shape; it does not
|
|
27
|
+
* govern it any more. The recording does, and the recording cannot be re-blessed.
|
|
28
|
+
*
|
|
29
|
+
* No junk predicate is implemented: every call site is `opcodes(a, b)` with no `isjunk`, so
|
|
30
|
+
* the junk set is empty and its test is constant false. That deletes two of the four
|
|
31
|
+
* extension loops in `findLongestMatch` — noted here rather than left as an unexplained
|
|
32
|
+
* absence, because their omission is a consequence of the call sites and not of the
|
|
33
|
+
* algorithm.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `SequenceMatcher.__chain_b` — b's element index, minus the popular elements.
|
|
38
|
+
*
|
|
39
|
+
* A `Map`, never an object: the elements are LINES, and a file whose first line is `0` would
|
|
40
|
+
* have its index hoisted to the front of an object literal (the P3a hazard). Nothing here
|
|
41
|
+
* iterates `b2j`, so the hoist would be invisible until the day something did.
|
|
42
|
+
*/
|
|
43
|
+
function chainB(b, autojunk) {
|
|
44
|
+
const b2j = new Map();
|
|
45
|
+
for (let i = 0; i < b.length; i += 1) {
|
|
46
|
+
const cur = b2j.get(b[i]);
|
|
47
|
+
if (cur === undefined) b2j.set(b[i], [i]);
|
|
48
|
+
else cur.push(i);
|
|
49
|
+
}
|
|
50
|
+
const n = b.length;
|
|
51
|
+
if (autojunk && n >= 200) {
|
|
52
|
+
const ntest = Math.floor(n / 100) + 1;
|
|
53
|
+
for (const [elt, idxs] of [...b2j]) {
|
|
54
|
+
if (idxs.length > ntest) b2j.delete(elt);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return b2j;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `SequenceMatcher.find_longest_match`, with `isbjunk` constant False.
|
|
62
|
+
*
|
|
63
|
+
* `j2len` is a `Map` keyed by INTEGER j, where the Python uses a dict. Nothing iterates it
|
|
64
|
+
* either, so the key type is only about lookup; a plain object would coerce every key to a
|
|
65
|
+
* string and work, and a Map says what the keys are.
|
|
66
|
+
*/
|
|
67
|
+
function findLongestMatch(a, b, b2j, alo, ahi, blo, bhi) {
|
|
68
|
+
let besti = alo;
|
|
69
|
+
let bestj = blo;
|
|
70
|
+
let bestsize = 0;
|
|
71
|
+
let j2len = new Map();
|
|
72
|
+
for (let i = alo; i < ahi; i += 1) {
|
|
73
|
+
const newj2len = new Map();
|
|
74
|
+
for (const j of b2j.get(a[i]) ?? []) {
|
|
75
|
+
if (j < blo) continue;
|
|
76
|
+
if (j >= bhi) break;
|
|
77
|
+
const k = (j2len.get(j - 1) ?? 0) + 1;
|
|
78
|
+
newj2len.set(j, k);
|
|
79
|
+
// A strict `>`: a later match of equal length never displaces an earlier one, which is
|
|
80
|
+
// what makes the result the FIRST longest block rather than the last.
|
|
81
|
+
if (k > bestsize) { besti = i - k + 1; bestj = j - k + 1; bestsize = k; }
|
|
82
|
+
}
|
|
83
|
+
j2len = newj2len;
|
|
84
|
+
}
|
|
85
|
+
// The two junk-free extension loops. The popular elements `chainB` dropped are NOT junk —
|
|
86
|
+
// `bjunk` stays empty — so a popular line adjacent to a match is still extended over here
|
|
87
|
+
// even though it could never have started one. That asymmetry is the Python's.
|
|
88
|
+
while (besti > alo && bestj > blo && a[besti - 1] === b[bestj - 1]) {
|
|
89
|
+
besti -= 1; bestj -= 1; bestsize += 1;
|
|
90
|
+
}
|
|
91
|
+
while (besti + bestsize < ahi && bestj + bestsize < bhi
|
|
92
|
+
&& a[besti + bestsize] === b[bestj + bestsize]) {
|
|
93
|
+
bestsize += 1;
|
|
94
|
+
}
|
|
95
|
+
return [besti, bestj, bestsize];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `SequenceMatcher.get_matching_blocks` — the recursion, the sort, and the adjacent merge. */
|
|
99
|
+
function matchingBlocks(a, b, b2j) {
|
|
100
|
+
const la = a.length;
|
|
101
|
+
const lb = b.length;
|
|
102
|
+
const queue = [[0, la, 0, lb]];
|
|
103
|
+
const blocks = [];
|
|
104
|
+
while (queue.length) {
|
|
105
|
+
// `queue.pop()` — LIFO, as the Python. The order reaches the output through the sort
|
|
106
|
+
// below only when two blocks tie, which they cannot, so this is faithfulness rather
|
|
107
|
+
// than necessity.
|
|
108
|
+
const [alo, ahi, blo, bhi] = queue.pop();
|
|
109
|
+
const [i, j, k] = findLongestMatch(a, b, b2j, alo, ahi, blo, bhi);
|
|
110
|
+
if (k) {
|
|
111
|
+
blocks.push([i, j, k]);
|
|
112
|
+
if (alo < i && blo < j) queue.push([alo, i, blo, j]);
|
|
113
|
+
if (i + k < ahi && j + k < bhi) queue.push([i + k, ahi, j + k, bhi]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// `list.sort()` on tuples is lexicographic; `Array.prototype.sort` is string-wise by
|
|
117
|
+
// default and would put [10,...] before [9,...].
|
|
118
|
+
blocks.sort((x, y) => x[0] - y[0] || x[1] - y[1] || x[2] - y[2]);
|
|
119
|
+
|
|
120
|
+
let i1 = 0;
|
|
121
|
+
let j1 = 0;
|
|
122
|
+
let k1 = 0;
|
|
123
|
+
const nonAdjacent = [];
|
|
124
|
+
for (const [i2, j2, k2] of blocks) {
|
|
125
|
+
if (i1 + k1 === i2 && j1 + k1 === j2) {
|
|
126
|
+
k1 += k2;
|
|
127
|
+
} else {
|
|
128
|
+
if (k1) nonAdjacent.push([i1, j1, k1]);
|
|
129
|
+
i1 = i2; j1 = j2; k1 = k2;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (k1) nonAdjacent.push([i1, j1, k1]);
|
|
133
|
+
nonAdjacent.push([la, lb, 0]);
|
|
134
|
+
return nonAdjacent;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** `SequenceMatcher.get_opcodes` — [tag, i1, i2, j1, j2]. */
|
|
138
|
+
export function opcodes(a, b, { autojunk = true } = {}) {
|
|
139
|
+
const b2j = chainB(b, autojunk);
|
|
140
|
+
let i = 0;
|
|
141
|
+
let j = 0;
|
|
142
|
+
const answer = [];
|
|
143
|
+
for (const [ai, bj, size] of matchingBlocks(a, b, b2j)) {
|
|
144
|
+
let tag = '';
|
|
145
|
+
if (i < ai && j < bj) tag = 'replace';
|
|
146
|
+
else if (i < ai) tag = 'delete';
|
|
147
|
+
else if (j < bj) tag = 'insert';
|
|
148
|
+
if (tag) answer.push([tag, i, ai, j, bj]);
|
|
149
|
+
i = ai + size;
|
|
150
|
+
j = bj + size;
|
|
151
|
+
if (size) answer.push(['equal', ai, i, bj, j]);
|
|
152
|
+
}
|
|
153
|
+
return answer;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `SequenceMatcher.get_grouped_opcodes` — the hunks, with `n` lines of context.
|
|
158
|
+
*
|
|
159
|
+
* Returns a list rather than a generator: the Python's caller is a `for` loop that consumes
|
|
160
|
+
* it once, and a lazy JS equivalent would buy nothing but a second shape to reason about.
|
|
161
|
+
*/
|
|
162
|
+
export function groupedOpcodes(a, b, n = 3, opts) {
|
|
163
|
+
let codes = opcodes(a, b, opts);
|
|
164
|
+
if (!codes.length) codes = [['equal', 0, 1, 0, 1]];
|
|
165
|
+
if (codes[0][0] === 'equal') {
|
|
166
|
+
const [tag, i1, i2, j1, j2] = codes[0];
|
|
167
|
+
codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2];
|
|
168
|
+
}
|
|
169
|
+
if (codes[codes.length - 1][0] === 'equal') {
|
|
170
|
+
const [tag, i1, i2, j1, j2] = codes[codes.length - 1];
|
|
171
|
+
codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)];
|
|
172
|
+
}
|
|
173
|
+
const nn = n + n;
|
|
174
|
+
const out = [];
|
|
175
|
+
let group = [];
|
|
176
|
+
for (const code of codes) {
|
|
177
|
+
let [, i1, i2, j1, j2] = code;
|
|
178
|
+
const tag = code[0];
|
|
179
|
+
if (tag === 'equal' && i2 - i1 > nn) {
|
|
180
|
+
group.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]);
|
|
181
|
+
out.push(group);
|
|
182
|
+
group = [];
|
|
183
|
+
i1 = Math.max(i1, i2 - n);
|
|
184
|
+
j1 = Math.max(j1, j2 - n);
|
|
185
|
+
}
|
|
186
|
+
group.push([tag, i1, i2, j1, j2]);
|
|
187
|
+
}
|
|
188
|
+
if (group.length && !(group.length === 1 && group[0][0] === 'equal')) out.push(group);
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `difflib._format_range_unified`. */
|
|
193
|
+
function formatRangeUnified(start, stop) {
|
|
194
|
+
let beginning = start + 1;
|
|
195
|
+
const length = stop - start;
|
|
196
|
+
if (length === 1) return `${beginning}`;
|
|
197
|
+
if (!length) beginning -= 1; // an empty range begins at the line just before it
|
|
198
|
+
return `${beginning},${length}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `difflib.unified_diff` — the lines, WITHOUT trailing newlines when `lineterm` is `""`.
|
|
203
|
+
*
|
|
204
|
+
* Every call site in this port passes `lineterm=""` and feeds it `splitlines()` output, which
|
|
205
|
+
* is the form the Python's own caller uses; the parameter is kept so the shape is the
|
|
206
|
+
* stdlib's rather than a specialisation that would have to be widened later.
|
|
207
|
+
*/
|
|
208
|
+
export function unifiedDiff(a, b, {
|
|
209
|
+
fromfile = '', tofile = '', fromfiledate = '', tofiledate = '', n = 3, lineterm = '\n',
|
|
210
|
+
} = {}) {
|
|
211
|
+
const out = [];
|
|
212
|
+
let started = false;
|
|
213
|
+
for (const group of groupedOpcodes(a, b, n)) {
|
|
214
|
+
if (!started) {
|
|
215
|
+
started = true;
|
|
216
|
+
const fromdate = fromfiledate ? `\t${fromfiledate}` : '';
|
|
217
|
+
const todate = tofiledate ? `\t${tofiledate}` : '';
|
|
218
|
+
out.push(`--- ${fromfile}${fromdate}${lineterm}`);
|
|
219
|
+
out.push(`+++ ${tofile}${todate}${lineterm}`);
|
|
220
|
+
}
|
|
221
|
+
const first = group[0];
|
|
222
|
+
const last = group[group.length - 1];
|
|
223
|
+
out.push(`@@ -${formatRangeUnified(first[1], last[2])} `
|
|
224
|
+
+ `+${formatRangeUnified(first[3], last[4])} @@${lineterm}`);
|
|
225
|
+
for (const [tag, i1, i2, j1, j2] of group) {
|
|
226
|
+
if (tag === 'equal') {
|
|
227
|
+
for (const line of a.slice(i1, i2)) out.push(` ${line}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (tag === 'replace' || tag === 'delete') {
|
|
231
|
+
for (const line of a.slice(i1, i2)) out.push(`-${line}`);
|
|
232
|
+
}
|
|
233
|
+
if (tag === 'replace' || tag === 'insert') {
|
|
234
|
+
for (const line of b.slice(j1, j2)) out.push(`+${line}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `str.splitlines()`, which is NOT `String.prototype.split('\n')`.
|
|
243
|
+
*
|
|
244
|
+
* Python breaks on eight boundaries beyond `\n`. Two of them — `\r\n` and a lone `\r` — are
|
|
245
|
+
* already gone by the time this is called, because `readText` decodes in universal-newlines
|
|
246
|
+
* mode exactly as `Path.read_text` does; they are matched anyway so this reproduces
|
|
247
|
+
* `splitlines` rather than "splitlines after readText". Of the six that survive that
|
|
248
|
+
* translation, the two a real file plausibly carries are U+2028 and U+2029: paste a paragraph
|
|
249
|
+
* from a web page into a deployed AGENT.md and the separator comes with it. Python then
|
|
250
|
+
* reports one more line than a `split('\n')` would, and since `diff` feeds these lists
|
|
251
|
+
* straight to `unifiedDiff`, every hunk after the paste shifts by one on one side only — a
|
|
252
|
+
* whole-file disagreement between the two implementations, on a file no fixture can reach,
|
|
253
|
+
* caused by a character neither diff prints visibly.
|
|
254
|
+
*
|
|
255
|
+
* A trailing terminator does NOT produce a final empty element, in either language.
|
|
256
|
+
*
|
|
257
|
+
* The escapes are `\u2028`/`\u2029` and not the characters themselves for a reason this file
|
|
258
|
+
* learned the hard way: a literal U+2028 in a `.mjs` source is a LineTerminator to the JS
|
|
259
|
+
* grammar, so writing one inside a regex literal ends the line and the file no longer parses.
|
|
260
|
+
*/
|
|
261
|
+
const LINE_BOUNDARY = /\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/g;
|
|
262
|
+
|
|
263
|
+
export function pySplitLines(text) {
|
|
264
|
+
const out = [];
|
|
265
|
+
let last = 0;
|
|
266
|
+
LINE_BOUNDARY.lastIndex = 0;
|
|
267
|
+
let m = LINE_BOUNDARY.exec(text);
|
|
268
|
+
while (m !== null) {
|
|
269
|
+
out.push(text.slice(last, m.index));
|
|
270
|
+
last = m.index + m[0].length;
|
|
271
|
+
m = LINE_BOUNDARY.exec(text);
|
|
272
|
+
}
|
|
273
|
+
if (last < text.length) out.push(text.slice(last));
|
|
274
|
+
return out;
|
|
275
|
+
}
|
package/js/link.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness link` / `harness unlink` — run-from-anywhere, and the phase that had to decide
|
|
3
|
+
* whether they should exist at all under npm.
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------------------
|
|
6
|
+
* DO THEY SHIP? YES — AND NOT FOR THE REASON THE HANDOFF PREDICTED.
|
|
7
|
+
*
|
|
8
|
+
* The case against was strong and it was measured, not assumed. `npm i -g geneseed` already
|
|
9
|
+
* puts `geneseed`, `geneseed-hook` and `geneseed-build` on PATH through npm's own bin
|
|
10
|
+
* linking, so `geneseed link` from an npm install writes a SECOND `geneseed.cmd` — into
|
|
11
|
+
* `%LOCALAPPDATA%\Geneseed\bin` rather than `%APPDATA%\npm` — and edits the persistent USER
|
|
12
|
+
* Path to reach it. `npm uninstall -g` removes neither. That is PATH litter with a registry
|
|
13
|
+
* edit behind it, for a job the package manager already did.
|
|
14
|
+
*
|
|
15
|
+
* What decided it against that was the WEB CONSOLE, not the CLI. `web/src/pages/Settings`
|
|
16
|
+
* has "Run from anywhere" and its inverse as two buttons; both POST `/api/actions/link` and
|
|
17
|
+
* `/api/actions/unlink`; and the Node job runner can only answer them by spawning
|
|
18
|
+
* `node bin/geneseed-cli.mjs link`. So the honest options were exactly two:
|
|
19
|
+
*
|
|
20
|
+
* * PORT THE VERBS — the npm package gains a command that duplicates what npm did; or
|
|
21
|
+
* * DELETE THE TWO ACTIONS from `action_commands` in BOTH implementations, delete the two
|
|
22
|
+
* buttons, and rebuild the tracked `web/dist`.
|
|
23
|
+
*
|
|
24
|
+
* The second removes a working feature from every GIT-CHECKOUT user — for whom `link` is
|
|
25
|
+
* the only thing that puts `geneseed` on PATH, because there is no package manager in that
|
|
26
|
+
* story — in order to spare npm users a duplicate. It also spends a `web/dist` rebuild on a
|
|
27
|
+
* deletion. So: they ship, they port, and `NOT_PORTED_ACTIONS` empties.
|
|
28
|
+
*
|
|
29
|
+
* The npm duplication is left as it is rather than papered over with a new "you already have
|
|
30
|
+
* this" refusal: that would be a REDESIGN of the verb, the reference has no such arm, and a
|
|
31
|
+
* divergence invented by the port is worse than one it inherited.
|
|
32
|
+
*
|
|
33
|
+
* ---------------------------------------------------------------------------------------
|
|
34
|
+
* THE ONE DIVERGENCE, AND IT IS P5b's, NOT A NEW ONE.
|
|
35
|
+
*
|
|
36
|
+
* On Windows the verb writes a shim naming an INTERPRETER and an ENTRY. The reference names
|
|
37
|
+
* `sys.executable` and `rituals/harness.py`; a Node process cannot know which Python would
|
|
38
|
+
* have run, and inventing one writes a shim that names an interpreter the user may not have.
|
|
39
|
+
* P5b settled this shape for the hook shim — "Node bakes node" — and P3a settled the gate:
|
|
40
|
+
* the shim is byte-comparable once runner and entry are ARGUMENTS. So this bakes
|
|
41
|
+
* `process.execPath` + `bin/geneseed-cli.mjs`, `tests/harness_golden.py` normalises the
|
|
42
|
+
* shim's argv line the way `js/web/jobs.mjs` normalises the job runner's, and each side's
|
|
43
|
+
* real content is asserted ABSOLUTELY in `tests/test_win_user_path.py`. A shim written by
|
|
44
|
+
* the Node CLI reaches sixteen verbs rather than twenty-four; that is the cost of the port
|
|
45
|
+
* being a port, and it disappears when P7 lands `tui`/`menu`/`home`.
|
|
46
|
+
*
|
|
47
|
+
* THE DOUBLE CARRIAGE RETURN IS NOT A TYPO. The reference builds the shim with explicit
|
|
48
|
+
* `\r\n` and writes it with `Path.write_text`, whose default newline handling then turns
|
|
49
|
+
* each `\n` into `\r\n` again — the file on disk really is `@echo off\r\r\n`. `writeText`
|
|
50
|
+
* carries exactly that rule (it is why the helper exists), so passing the same string
|
|
51
|
+
* through it reproduces the same bytes. cmd.exe does not care; the byte gate does.
|
|
52
|
+
*
|
|
53
|
+
* ---------------------------------------------------------------------------------------
|
|
54
|
+
* NO TEST IN THIS REPO MAY TOUCH THE REAL USER PATH.
|
|
55
|
+
*
|
|
56
|
+
* `winUserPath` writes to the registry through PowerShell. That is not sandboxable and a
|
|
57
|
+
* cell that ran it would edit the developer's machine, so the module is shaped so the gate
|
|
58
|
+
* never needs to:
|
|
59
|
+
*
|
|
60
|
+
* * `winUserPathScript` is PURE — the escaping, the split and the idempotence live there
|
|
61
|
+
* and `tests/test_win_user_path.py` runs it over a corpus (apostrophes, trailing
|
|
62
|
+
* separators, UNC roots) against the Python twin, byte for byte.
|
|
63
|
+
* * `cmdLink` SHORT-CIRCUITS when the bin dir is already on `PATH`, so a cell that seeds
|
|
64
|
+
* `PATH` with its own sandboxed bin dir exercises the whole verb and never spawns.
|
|
65
|
+
* * `cmdUnlink` has no such short-circuit, so its cells seed a `PATH` with no `powershell`
|
|
66
|
+
* on it: the spawn fails to launch, `winUserPath` returns false through the ENOENT arm,
|
|
67
|
+
* and the verb takes its "removed the shim, said nothing about PATH" branch. A real arm,
|
|
68
|
+
* reached honestly, with the registry untouched.
|
|
69
|
+
*
|
|
70
|
+
* THE SUCCESS ARM OF `winUserPath` IS THEREFORE UNGATED, DELIBERATELY, IN BOTH
|
|
71
|
+
* IMPLEMENTATIONS — declared here rather than faked, because the only way to gate it is to
|
|
72
|
+
* edit the machine running the gate.
|
|
73
|
+
*/
|
|
74
|
+
import {
|
|
75
|
+
chmodSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync,
|
|
76
|
+
} from 'node:fs';
|
|
77
|
+
import { spawnSync } from 'node:child_process';
|
|
78
|
+
import path from 'node:path';
|
|
79
|
+
import os from 'node:os';
|
|
80
|
+
|
|
81
|
+
import { ROOT } from './checkout.mjs';
|
|
82
|
+
import { readMaybe } from './installs.mjs';
|
|
83
|
+
import { pyPathStr, pyPrint, pyPrintErr, writeText } from './lib/fs.mjs';
|
|
84
|
+
|
|
85
|
+
const IS_WIN = process.platform === 'win32';
|
|
86
|
+
|
|
87
|
+
/** `_harness_lifecycle._win_bin_dir`. */
|
|
88
|
+
export function winBinDir() {
|
|
89
|
+
// `os.environ.get(...) or str(Path.home())` — an EMPTY LOCALAPPDATA falls back too,
|
|
90
|
+
// which `??` would not do.
|
|
91
|
+
const base = process.env.LOCALAPPDATA || os.homedir();
|
|
92
|
+
return path.join(base, 'Geneseed', 'bin');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `_harness_lifecycle._win_user_path_script` — the PowerShell one-liner, built but not run.
|
|
97
|
+
*
|
|
98
|
+
* Every quoting hazard in this verb is in this string, which is why it is a separate
|
|
99
|
+
* function on both sides: `''` is PowerShell's single-quote escape, the `;` split drops
|
|
100
|
+
* empty segments so a trailing separator cannot append an empty entry, and `-notcontains`
|
|
101
|
+
* is what makes `link` idempotent.
|
|
102
|
+
*/
|
|
103
|
+
export function winUserPathScript(action, directory) {
|
|
104
|
+
const d = directory.replaceAll("'", "''"); // PS single-quote escape (O'Brien)
|
|
105
|
+
if (action === 'add') {
|
|
106
|
+
return `$d='${d}';`
|
|
107
|
+
+ "$p=[Environment]::GetEnvironmentVariable('Path','User');"
|
|
108
|
+
+ "if (-not $p) {$p=''};"
|
|
109
|
+
+ "$parts=$p.Split(';') | Where-Object {$_ -ne ''};"
|
|
110
|
+
+ 'if ($parts -notcontains $d) {'
|
|
111
|
+
+ " $np=(@($parts)+$d) -join ';';"
|
|
112
|
+
+ " [Environment]::SetEnvironmentVariable('Path',$np,'User')}";
|
|
113
|
+
}
|
|
114
|
+
return `$d='${d}';`
|
|
115
|
+
+ "$p=[Environment]::GetEnvironmentVariable('Path','User');"
|
|
116
|
+
+ 'if ($p) {'
|
|
117
|
+
+ " $np=(($p.Split(';') | Where-Object {$_ -ne '' -and $_ -ne $d}) -join ';');"
|
|
118
|
+
+ " [Environment]::SetEnvironmentVariable('Path',$np,'User')}";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `_harness_lifecycle._win_user_path` — the spawn, and the sixth row of `_ALLOWED_SPAWNS`.
|
|
123
|
+
*
|
|
124
|
+
* There is no in-process equivalent: the persistent USER Path lives in the registry
|
|
125
|
+
* (`HKCU\Environment`), not in this process's environment, and Node has no registry API.
|
|
126
|
+
* `[Environment]::SetEnvironmentVariable(...,'User')` also broadcasts `WM_SETTINGCHANGE`,
|
|
127
|
+
* which a hand-rolled `reg add` would not — so even a registry binding would be the wrong
|
|
128
|
+
* primitive.
|
|
129
|
+
*
|
|
130
|
+
* `except OSError -> False`: Python raises when the executable cannot be launched at all.
|
|
131
|
+
* Node reports the same condition in `r.error` instead of throwing, so the two arms are
|
|
132
|
+
* spelled differently and mean the same thing — a `powershell` that is not on PATH returns
|
|
133
|
+
* false rather than crashing the verb.
|
|
134
|
+
*/
|
|
135
|
+
function winUserPath(action, directory) {
|
|
136
|
+
const r = spawnSync('powershell',
|
|
137
|
+
['-NoProfile', '-Command', winUserPathScript(action, directory)],
|
|
138
|
+
{ stdio: 'inherit', windowsHide: true });
|
|
139
|
+
if (r.error) return false;
|
|
140
|
+
return r.status === 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `_harness_lifecycle.cmd_link` — put `geneseed` on PATH so it runs from any directory. */
|
|
144
|
+
export function cmdLink(args) {
|
|
145
|
+
const here = ROOT;
|
|
146
|
+
if (IS_WIN) {
|
|
147
|
+
const bindir = winBinDir();
|
|
148
|
+
const shim = path.join(bindir, 'geneseed.cmd');
|
|
149
|
+
try {
|
|
150
|
+
mkdirSync(bindir, { recursive: true });
|
|
151
|
+
// The running runtime, not a bare name — the reference's reason (a bare `python` may
|
|
152
|
+
// be missing or resolve to the Microsoft Store alias stub) applies to `node` too, and
|
|
153
|
+
// an npm install is exactly where a second Node on PATH is likeliest.
|
|
154
|
+
writeText(shim, '@echo off\r\n'
|
|
155
|
+
+ `"${process.execPath}" "${path.join(here, 'bin', 'geneseed-cli.mjs')}" %*\r\n`);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
pyPrintErr(`geneseed: could not write ${shim} (${pyOsError(e)})\n`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
pyPrint(`geneseed: wrote shim ${shim}\n`);
|
|
161
|
+
const on_path = (process.env.PATH || '').toLowerCase().includes(bindir.toLowerCase());
|
|
162
|
+
if (on_path || winUserPath('add', bindir)) {
|
|
163
|
+
pyPrint(`geneseed: '${bindir}' is on your user PATH — open a NEW terminal, then run \`geneseed\`.\n`);
|
|
164
|
+
} else {
|
|
165
|
+
pyPrint(`geneseed: add '${bindir}' to your PATH manually, then run \`geneseed\` from anywhere.\n`);
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
// Unix: write a shim into a bin dir (default ~/.local/bin, no sudo). See the shim
|
|
170
|
+
// comment below for why this is no longer a symlink to `ROOT/geneseed`.
|
|
171
|
+
// `Path(args.dir)` — and `pyPathStr` is that conversion, not decoration. The reference
|
|
172
|
+
// builds a `Path` here and prints `str(target_dir)` twice (the PATH notice and the `export
|
|
173
|
+
// PATH=` line) as well as comparing it against `PATH.split(os.pathsep)`, so an argument
|
|
174
|
+
// that is not already normalised diverges in three places at once. A trailing slash is the
|
|
175
|
+
// cheapest one: `str(Path('/x/bin/'))` is `/x/bin` and the raw string is not.
|
|
176
|
+
// `harness_golden`'s `link/an-explicit-dir-argument-is-used-instead-of-the-default` passes
|
|
177
|
+
// one, and it is the cell that found this.
|
|
178
|
+
let targetDir = args.dir ? pyPathStr(args.dir) : null;
|
|
179
|
+
if (targetDir === null) {
|
|
180
|
+
const local = path.join(os.homedir(), '.local', 'bin');
|
|
181
|
+
try {
|
|
182
|
+
mkdirSync(local, { recursive: true });
|
|
183
|
+
targetDir = local;
|
|
184
|
+
} catch {
|
|
185
|
+
targetDir = '/usr/local/bin';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const dest = path.join(targetDir, 'geneseed');
|
|
189
|
+
const entry = path.join(here, 'bin', 'geneseed-cli.mjs');
|
|
190
|
+
try {
|
|
191
|
+
mkdirSync(targetDir, { recursive: true });
|
|
192
|
+
// `dest.is_symlink() or dest.exists()` — the first half is what catches a BROKEN
|
|
193
|
+
// symlink, which `existsSync` follows and reports absent.
|
|
194
|
+
if (isSymlink(dest) || existsSync(dest)) rmSync(dest, { force: true });
|
|
195
|
+
// A WRITTEN SHIM, not a symlink to `ROOT/geneseed`. The bash launcher execs a Python
|
|
196
|
+
// interpreter, so symlinking it puts Python on PATH from the Node verb. The Windows
|
|
197
|
+
// arm above has always written a shim; this is the same shape. `GENESEED_LINK_SHIM`
|
|
198
|
+
// is the marker `cmdUnlink` recognises — without it, unlink cannot tell our file from
|
|
199
|
+
// a stranger's and would have to refuse every regular file, which is a no-op.
|
|
200
|
+
writeText(dest, '#!/bin/sh\n'
|
|
201
|
+
+ '# GENESEED_LINK_SHIM\n'
|
|
202
|
+
+ `exec "${process.execPath}" "${entry}" "$@"\n`);
|
|
203
|
+
chmodSync(dest, 0o755);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
pyPrintErr(`geneseed: could not write ${dest} (${pyOsError(e)}) — pick a writable dir: `
|
|
206
|
+
+ 'geneseed link <dir>\n');
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
pyPrint(`geneseed: linked ${dest} -> ${entry}\n`);
|
|
210
|
+
if ((process.env.PATH || '').split(path.delimiter).includes(targetDir)) {
|
|
211
|
+
pyPrint(`geneseed: '${targetDir}' is on PATH — run 'geneseed' from anywhere.\n`);
|
|
212
|
+
} else {
|
|
213
|
+
pyPrint(`geneseed: NOTE '${targetDir}' is not on your PATH. Add it, e.g.:\n`);
|
|
214
|
+
pyPrint(` echo 'export PATH="${targetDir}:$PATH"' >> ~/.zshrc # or ~/.bashrc\n`);
|
|
215
|
+
}
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `_harness_lifecycle.cmd_unlink`. */
|
|
220
|
+
export function cmdUnlink() {
|
|
221
|
+
if (IS_WIN) {
|
|
222
|
+
const bindir = winBinDir();
|
|
223
|
+
const shim = path.join(bindir, 'geneseed.cmd');
|
|
224
|
+
let removed = false;
|
|
225
|
+
if (existsSync(shim)) {
|
|
226
|
+
try {
|
|
227
|
+
rmSync(shim);
|
|
228
|
+
removed = true;
|
|
229
|
+
pyPrint(`geneseed: removed ${shim}\n`);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
pyPrintErr(`geneseed: could not remove ${shim} (${pyOsError(e)})\n`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (winUserPath('remove', bindir)) {
|
|
235
|
+
pyPrint(`geneseed: removed '${bindir}' from your user PATH (open a new terminal).\n`);
|
|
236
|
+
}
|
|
237
|
+
if (!removed) pyPrint('geneseed: no linked launcher found.\n');
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
let removed = false;
|
|
241
|
+
const candidates = [path.join(os.homedir(), '.local', 'bin'), '/usr/local/bin'];
|
|
242
|
+
for (const d of (process.env.PATH || '').split(path.delimiter)) if (d) candidates.push(d);
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
for (const d of candidates) {
|
|
245
|
+
if (seen.has(d)) continue;
|
|
246
|
+
seen.add(d);
|
|
247
|
+
const f = path.join(d, 'geneseed');
|
|
248
|
+
// TWO shapes, because two shapes exist on disk. (a) `os.readlink(f)` raw, then `.name`
|
|
249
|
+
// — NOT a resolved path: the reference asks whether the link's own target BASENAME is
|
|
250
|
+
// `geneseed`, so a symlink pointing at some other program that happens to sit in a
|
|
251
|
+
// `geneseed` directory is left alone. That is every install made before the shim.
|
|
252
|
+
// (b) a regular file we wrote, identified by its marker — never "any regular file
|
|
253
|
+
// called geneseed", which would delete a stranger's program.
|
|
254
|
+
const ours = (isSymlink(f) && path.basename(readlinkSync(f)) === 'geneseed')
|
|
255
|
+
|| (!isSymlink(f) && existsSync(f) && (readMaybe(f) ?? '').includes('GENESEED_LINK_SHIM'));
|
|
256
|
+
if (ours) {
|
|
257
|
+
try {
|
|
258
|
+
rmSync(f);
|
|
259
|
+
pyPrint(`geneseed: removed ${f}\n`);
|
|
260
|
+
removed = true;
|
|
261
|
+
} catch { /* `except OSError: pass` */ }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!removed) pyPrint('geneseed: no linked launcher found on PATH\n');
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** `Path.is_symlink()` — false for a path that does not exist, never a throw. */
|
|
269
|
+
function isSymlink(p) {
|
|
270
|
+
try {
|
|
271
|
+
return lstatSync(p).isSymbolicLink();
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* `str(OSError)` — what Python interpolates into these two messages.
|
|
279
|
+
*
|
|
280
|
+
* `[Errno 13] Permission denied: 'C:\\...'`, and Node's `e.message` is the same three parts
|
|
281
|
+
* in a different order. Not byte-reproducible and not worth pretending otherwise: both cells
|
|
282
|
+
* that can reach it assert the arm, not the errno text.
|
|
283
|
+
*/
|
|
284
|
+
function pyOsError(e) {
|
|
285
|
+
return e && e.message ? e.message : String(e);
|
|
286
|
+
}
|