From f99d1f489a972621f264c4713c3a99e92d08a4b1 Mon Sep 17 00:00:00 2001 From: x Date: Tue, 22 Sep 2026 16:29:35 +0000 Subject: [PATCH] feat(engines): launch your own build of an engine with MOSHCODE_ENGINE_BIN_ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MOSHCODE_ENGINE_BIN_CODEX=/path/to/codex` makes moshcode open that executable for the codex engine instead of searching for `codex` on PATH. The same shape works for every engine in the table. It exists for the case where you are running your own build of an engine — a fork carrying a patch upstream has not taken, or a debug build you are bisecting — and the copy PATH finds first is not the one you mean. openai/codex, for instance, accepts no external pull requests at all, so a fix you need has nowhere to land but a fork you build yourself. Unset by default, and deliberately so: with no override the engine's declared name is resolved against PATH exactly as before, which is the behaviour every engine documents and every test relies on. An override is a claim to know better than PATH for one engine, so it is only ever made out loud. Applied once where the table is built rather than at each launch site, because `bin` is read directly by the CLI, the TUI's error messages, the swarm and the MCP bridge, and an override only some of them honoured would be worse than none. Co-Authored-By: Claude Opus 5 (1M context) --- src/engines.mjs | 32 ++++++++++++++++++++++++++++++++ test/engines.test.mjs | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/engines.mjs b/src/engines.mjs index 9c0d3d4c..9a483c4f 100644 --- a/src/engines.mjs +++ b/src/engines.mjs @@ -360,6 +360,38 @@ export const ENGINES = { }, }; +/** + * An explicit path to a build of one engine, from the environment. + * + * `MOSHCODE_ENGINE_BIN_` (for example `MOSHCODE_ENGINE_BIN_CODEX`) names + * the executable to launch for that engine instead of searching for its usual + * name. It exists for the case where you are running your own build of an + * engine — a fork carrying a patch its upstream has not taken, a debug build + * you are bisecting — and want moshcode to open *that* rather than whichever + * copy happens to come first on PATH. + * + * It is unset by default, and deliberately so: with no override the name below + * is resolved against PATH exactly as before, which is the behaviour every + * engine here documents and every test relies on. An override is a statement + * that you know better than PATH for this one engine, so it is only ever made + * out loud. + * + * The value is used as-is, including `~`-free absolute paths; an empty or + * whitespace-only value is treated as unset rather than as an empty command. + */ +export function engineBinOverride(key, env = process.env) { + const value = env[`MOSHCODE_ENGINE_BIN_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`]; + return typeof value === "string" && value.trim() ? value.trim() : null; +} + +// Applied once, here, rather than at each launch site: `bin` is read directly +// by the CLI, the TUI's error messages, the swarm and the MCP bridge, and an +// override that only some of them honoured would be worse than none. +for (const [key, engine] of Object.entries(ENGINES)) { + const override = engineBinOverride(key); + if (override) engine.bin = override; +} + /** * The command that upgrades an already-installed engine in place: its native * updater if it has one, else re-run the installer (they're idempotent and diff --git a/test/engines.test.mjs b/test/engines.test.mjs index daf32a67..e6cd7406 100644 --- a/test/engines.test.mjs +++ b/test/engines.test.mjs @@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import test from "node:test"; -import { ENGINES, agentLaunchArgs, aiExecArgs, exitReason, isInstalled, openPassthrough, pickAiEngine, ranOk, resolveEngine, runCmd } from "../src/engines.mjs"; +import { ENGINES, agentLaunchArgs, aiExecArgs, engineBinOverride, exitReason, isInstalled, openPassthrough, pickAiEngine, ranOk, resolveEngine, runCmd } from "../src/engines.mjs"; const BIN = fileURLToPath(new URL("../bin/moshcode.mjs", import.meta.url)); // The autonomous-session bypass flags each engine declares (engine.agentArgs). @@ -62,7 +62,7 @@ process.stdout.write(JSON.stringify(process.argv.slice(2))); chmodSync(file, 0o755); } -function run(args, binDir) { +function run(args, binDir, extraEnv = {}) { return new Promise((resolve, reject) => { const ioDir = tempDir("moshcode-engine-stdio-"); const stdinFile = path.join(ioDir, "stdin"); @@ -77,6 +77,7 @@ function run(args, binDir) { env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH || ""}`, + ...extraEnv, }, }); let failed = false; @@ -274,3 +275,37 @@ test("PATH still wins over a tool's install dir", async () => { process.env.PATH = previous; } }); + +test("an engine bin override is read from the environment, and only when it says something", () => { + assert.deepEqual( + [ + engineBinOverride("codex", {}), + engineBinOverride("codex", { MOSHCODE_ENGINE_BIN_CODEX: "" }), + engineBinOverride("codex", { MOSHCODE_ENGINE_BIN_CODEX: " " }), + engineBinOverride("codex", { MOSHCODE_ENGINE_BIN_CODEX: " /opt/codex " }), + engineBinOverride("privacycode", { MOSHCODE_ENGINE_BIN_PRIVACYCODE: "/opt/pc" }), + ], + [null, null, null, "/opt/codex", "/opt/pc"], + ); +}); + +test("an engine bin override launches that build instead of the copy on PATH", async () => { + // The name every other test would get: first on PATH, and not what we asked for. + const pathDir = tempDir(); + mkdirSync(pathDir, { recursive: true }); + writeEngine(pathDir, "codex"); + + // Our own build, deliberately somewhere PATH would never find it. + const buildDir = tempDir("moshcode-engine-build-"); + mkdirSync(buildDir, { recursive: true }); + const build = path.join(buildDir, "codex-fork"); + writeFileSync(build, `#!/usr/bin/env node +process.stdout.write(JSON.stringify(["override", ...process.argv.slice(2)])); +`); + chmodSync(build, 0o755); + + const result = await run(["agents", "codex"], pathDir, { MOSHCODE_ENGINE_BIN_CODEX: build }); + + assert.equal(result.status, 0); + assert.deepEqual(JSON.parse(result.stdout), ["override", ...EXPECTED_LAUNCH_ARGS.codex]); +});