Skip to content

Commit b253db4

Browse files
ralyodioxxxxxxxxxxxxxclaude
authored
feat(engines): launch your own build of an engine with MOSHCODE_ENGINE_BIN_<KEY> (#530)
`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: x <x@x> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8c7f422 commit b253db4

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

‎src/engines.mjs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,38 @@ export const ENGINES = {
360360
},
361361
};
362362

363+
/**
364+
* An explicit path to a build of one engine, from the environment.
365+
*
366+
* `MOSHCODE_ENGINE_BIN_<KEY>` (for example `MOSHCODE_ENGINE_BIN_CODEX`) names
367+
* the executable to launch for that engine instead of searching for its usual
368+
* name. It exists for the case where you are running your own build of an
369+
* engine — a fork carrying a patch its upstream has not taken, a debug build
370+
* you are bisecting — and want moshcode to open *that* rather than whichever
371+
* copy happens to come first on PATH.
372+
*
373+
* It is unset by default, and deliberately so: with no override the name below
374+
* is resolved against PATH exactly as before, which is the behaviour every
375+
* engine here documents and every test relies on. An override is a statement
376+
* that you know better than PATH for this one engine, so it is only ever made
377+
* out loud.
378+
*
379+
* The value is used as-is, including `~`-free absolute paths; an empty or
380+
* whitespace-only value is treated as unset rather than as an empty command.
381+
*/
382+
export function engineBinOverride(key, env = process.env) {
383+
const value = env[`MOSHCODE_ENGINE_BIN_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`];
384+
return typeof value === "string" && value.trim() ? value.trim() : null;
385+
}
386+
387+
// Applied once, here, rather than at each launch site: `bin` is read directly
388+
// by the CLI, the TUI's error messages, the swarm and the MCP bridge, and an
389+
// override that only some of them honoured would be worse than none.
390+
for (const [key, engine] of Object.entries(ENGINES)) {
391+
const override = engineBinOverride(key);
392+
if (override) engine.bin = override;
393+
}
394+
363395
/**
364396
* The command that upgrades an already-installed engine in place: its native
365397
* updater if it has one, else re-run the installer (they're idempotent and

‎test/engines.test.mjs‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
1414
import { spawn } from "node:child_process";
1515
import test from "node:test";
1616

17-
import { ENGINES, agentLaunchArgs, aiExecArgs, exitReason, isInstalled, openPassthrough, pickAiEngine, ranOk, resolveEngine, runCmd } from "../src/engines.mjs";
17+
import { ENGINES, agentLaunchArgs, aiExecArgs, engineBinOverride, exitReason, isInstalled, openPassthrough, pickAiEngine, ranOk, resolveEngine, runCmd } from "../src/engines.mjs";
1818

1919
const BIN = fileURLToPath(new URL("../bin/moshcode.mjs", import.meta.url));
2020
// The autonomous-session bypass flags each engine declares (engine.agentArgs).
@@ -62,7 +62,7 @@ process.stdout.write(JSON.stringify(process.argv.slice(2)));
6262
chmodSync(file, 0o755);
6363
}
6464

65-
function run(args, binDir) {
65+
function run(args, binDir, extraEnv = {}) {
6666
return new Promise((resolve, reject) => {
6767
const ioDir = tempDir("moshcode-engine-stdio-");
6868
const stdinFile = path.join(ioDir, "stdin");
@@ -77,6 +77,7 @@ function run(args, binDir) {
7777
env: {
7878
...process.env,
7979
PATH: `${binDir}${path.delimiter}${process.env.PATH || ""}`,
80+
...extraEnv,
8081
},
8182
});
8283
let failed = false;
@@ -274,3 +275,37 @@ test("PATH still wins over a tool's install dir", async () => {
274275
process.env.PATH = previous;
275276
}
276277
});
278+
279+
test("an engine bin override is read from the environment, and only when it says something", () => {
280+
assert.deepEqual(
281+
[
282+
engineBinOverride("codex", {}),
283+
engineBinOverride("codex", { MOSHCODE_ENGINE_BIN_CODEX: "" }),
284+
engineBinOverride("codex", { MOSHCODE_ENGINE_BIN_CODEX: " " }),
285+
engineBinOverride("codex", { MOSHCODE_ENGINE_BIN_CODEX: " /opt/codex " }),
286+
engineBinOverride("privacycode", { MOSHCODE_ENGINE_BIN_PRIVACYCODE: "/opt/pc" }),
287+
],
288+
[null, null, null, "/opt/codex", "/opt/pc"],
289+
);
290+
});
291+
292+
test("an engine bin override launches that build instead of the copy on PATH", async () => {
293+
// The name every other test would get: first on PATH, and not what we asked for.
294+
const pathDir = tempDir();
295+
mkdirSync(pathDir, { recursive: true });
296+
writeEngine(pathDir, "codex");
297+
298+
// Our own build, deliberately somewhere PATH would never find it.
299+
const buildDir = tempDir("moshcode-engine-build-");
300+
mkdirSync(buildDir, { recursive: true });
301+
const build = path.join(buildDir, "codex-fork");
302+
writeFileSync(build, `#!/usr/bin/env node
303+
process.stdout.write(JSON.stringify(["override", ...process.argv.slice(2)]));
304+
`);
305+
chmodSync(build, 0o755);
306+
307+
const result = await run(["agents", "codex"], pathDir, { MOSHCODE_ENGINE_BIN_CODEX: build });
308+
309+
assert.equal(result.status, 0);
310+
assert.deepEqual(JSON.parse(result.stdout), ["override", ...EXPECTED_LAUNCH_ARGS.codex]);
311+
});

0 commit comments

Comments
 (0)