Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/engines.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,38 @@ export const ENGINES = {
},
};

/**
* An explicit path to a build of one engine, from the environment.
*
* `MOSHCODE_ENGINE_BIN_<KEY>` (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
Expand Down
39 changes: 37 additions & 2 deletions test/engines.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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");
Expand All @@ -77,6 +77,7 @@ function run(args, binDir) {
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH || ""}`,
...extraEnv,
},
});
let failed = false;
Expand Down Expand Up @@ -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]);
});
Loading