fix(codex): harden windows light install
This commit is contained in:
@@ -241,3 +241,84 @@ test("#given managed agent role sections #when script installer updates config #
|
||||
assert.match(config, /description = "read-only explorer"/);
|
||||
assert.match(config, /config_file = "\.\/agents\/explorer\.toml"/);
|
||||
});
|
||||
|
||||
test("#given existing trust and lsp blocks #when updating config #then existing blocks are preserved", async () => {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), "omo-codex-config-baseline-"));
|
||||
const configPath = join(root, "config.toml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
[
|
||||
'[plugins."omo@sisyphuslabs"]',
|
||||
"enabled = true",
|
||||
"",
|
||||
'[plugins."omo@sisyphuslabs".mcp_servers.lsp]',
|
||||
"enabled = true",
|
||||
"",
|
||||
'[hooks.state."omo@sisyphuslabs:hooks/hooks.json:post_tool_use:0:0"]',
|
||||
'trusted_hash = "sha256:keep"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "sisyphuslabs",
|
||||
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex/cache/sisyphuslabs" },
|
||||
pluginNames: ["omo"],
|
||||
trustedHookStates: [{ key: "omo@sisyphuslabs:hooks/hooks.json:post_tool_use:0:0", trustedHash: "sha256:keep" }],
|
||||
});
|
||||
|
||||
// then
|
||||
const content = await readFile(configPath, "utf8");
|
||||
assert.match(content, /\[plugins\."omo@sisyphuslabs"\]/);
|
||||
assert.match(content, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.lsp\]/);
|
||||
assert.match(content, /\[hooks\.state\."omo@sisyphuslabs:hooks\/hooks\.json:post_tool_use:0:0"\]/);
|
||||
assert.match(content, /trusted_hash = "sha256:keep"/);
|
||||
});
|
||||
|
||||
test("#given windows platform #when updating config #then enables git_bash plugin mcp policy", async () => {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), "omo-codex-config-git-bash-win32-"));
|
||||
const configPath = join(root, "config.toml");
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "sisyphuslabs",
|
||||
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex/cache/sisyphuslabs" },
|
||||
pluginNames: ["omo"],
|
||||
platform: "win32",
|
||||
});
|
||||
|
||||
// then
|
||||
const content = await readFile(configPath, "utf8");
|
||||
assert.match(content, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]/);
|
||||
assert.match(content, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\][\s\S]*?enabled = true/);
|
||||
});
|
||||
|
||||
test("#given non-windows platforms #when updating config #then disables git_bash plugin mcp policy", async () => {
|
||||
for (const platform of ["linux", "darwin"]) {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), `omo-codex-config-git-bash-${platform}-`));
|
||||
const configPath = join(root, "config.toml");
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "sisyphuslabs",
|
||||
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex/cache/sisyphuslabs" },
|
||||
pluginNames: ["omo"],
|
||||
platform,
|
||||
});
|
||||
|
||||
// then
|
||||
const content = await readFile(configPath, "utf8");
|
||||
assert.match(content, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]/);
|
||||
assert.match(content, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\][\s\S]*?enabled = false/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,6 +133,7 @@ export async function installMarketplaceLocally(options = {}) {
|
||||
marketplaceName: marketplace.name,
|
||||
marketplaceSource: { sourceType: "local", source: marketplaceRoot },
|
||||
pluginNames,
|
||||
platform,
|
||||
trustedHookStates,
|
||||
agentConfigs: [...agentConfigs.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ test("#given external MCP package runtime #when installing cached plugin #then r
|
||||
const codexHome = await makeTempDir();
|
||||
const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
|
||||
const astGrepPackageRoot = join(repoRoot, "packages", "ast-grep-mcp");
|
||||
const gitBashPackageRoot = join(repoRoot, "packages", "git-bash-mcp");
|
||||
const lspPackageRoot = join(repoRoot, "packages", "lsp-tools-mcp");
|
||||
|
||||
await writeJson(join(astGrepPackageRoot, "package.json"), {
|
||||
@@ -26,6 +27,12 @@ test("#given external MCP package runtime #when installing cached plugin #then r
|
||||
type: "module",
|
||||
bin: { "omo-lsp": "./dist/cli.js" },
|
||||
});
|
||||
await writeJson(join(gitBashPackageRoot, "package.json"), {
|
||||
name: "@example/git-bash-mcp",
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
bin: { "omo-git-bash": "./dist/cli.js" },
|
||||
});
|
||||
await writeJson(join(sourceRoot, "package.json"), {
|
||||
name: "@example/omo",
|
||||
version: "0.1.0",
|
||||
@@ -37,6 +44,11 @@ test("#given external MCP package runtime #when installing cached plugin #then r
|
||||
args: ["../../ast-grep-mcp/dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
git_bash: {
|
||||
command: "node",
|
||||
args: ["../../git-bash-mcp/dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
lsp: {
|
||||
command: "node",
|
||||
args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
|
||||
@@ -45,6 +57,7 @@ test("#given external MCP package runtime #when installing cached plugin #then r
|
||||
},
|
||||
});
|
||||
await writeJson(join(astGrepPackageRoot, "dist", "cli.js"), { executable: true });
|
||||
await writeJson(join(gitBashPackageRoot, "dist", "cli.js"), { executable: true });
|
||||
await writeJson(join(lspPackageRoot, "dist", "cli.js"), { executable: true });
|
||||
await writeJson(join(lspPackageRoot, "dist", "lsp", "manager.js"), { copied: true });
|
||||
|
||||
@@ -59,13 +72,17 @@ test("#given external MCP package runtime #when installing cached plugin #then r
|
||||
|
||||
const cachedMcp = JSON.parse(await readFile(join(result.path, ".mcp.json"), "utf8"));
|
||||
const copiedAstGrepCli = join(result.path, "mcp", "ast_grep", "dist", "cli.js");
|
||||
const copiedGitBashCli = join(result.path, "mcp", "git_bash", "dist", "cli.js");
|
||||
const copiedCli = join(result.path, "mcp", "lsp", "dist", "cli.js");
|
||||
|
||||
assert.deepEqual(cachedMcp.mcpServers.ast_grep.args, [copiedAstGrepCli, "mcp"]);
|
||||
assert.deepEqual(cachedMcp.mcpServers.git_bash.args, [copiedGitBashCli, "mcp"]);
|
||||
assert.deepEqual(cachedMcp.mcpServers.lsp.args, [copiedCli, "mcp"]);
|
||||
assert.equal(Object.hasOwn(cachedMcp.mcpServers.ast_grep, "cwd"), false);
|
||||
assert.equal(Object.hasOwn(cachedMcp.mcpServers.git_bash, "cwd"), false);
|
||||
assert.equal(Object.hasOwn(cachedMcp.mcpServers.lsp, "cwd"), false);
|
||||
assert.equal((await stat(copiedAstGrepCli)).isFile(), true);
|
||||
assert.equal((await stat(copiedGitBashCli)).isFile(), true);
|
||||
assert.equal((await stat(copiedCli)).isFile(), true);
|
||||
assert.equal((await stat(join(result.path, "mcp", "lsp", "dist", "lsp", "manager.js"))).isFile(), true);
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function updateCodexConfig({
|
||||
marketplaceName,
|
||||
marketplaceSource = defaultMarketplaceSource(marketplaceName, repoRoot),
|
||||
pluginNames,
|
||||
platform = process.platform,
|
||||
trustedHookStates = [],
|
||||
agentConfigs = [],
|
||||
}) {
|
||||
@@ -53,6 +54,7 @@ export async function updateCodexConfig({
|
||||
for (const pluginName of pluginNames) {
|
||||
config = ensurePluginEnabled(config, `${pluginName}@${marketplaceName}`);
|
||||
}
|
||||
config = ensureOmoGitBashMcpPolicy(config, { marketplaceName, pluginNames, platform });
|
||||
for (const state of trustedHookStates) {
|
||||
config = ensureHookTrusted(config, state.key, state.trustedHash);
|
||||
}
|
||||
@@ -150,6 +152,19 @@ function ensurePluginEnabled(config, pluginKey) {
|
||||
return replaceOrInsertSetting(config, section, "enabled", "true");
|
||||
}
|
||||
|
||||
function ensurePluginMcpEnabled(config, pluginKey, serverName, enabled) {
|
||||
const header = `plugins.${JSON.stringify(pluginKey)}.mcp_servers.${serverName}`;
|
||||
const section = findTomlSection(config, header);
|
||||
const enabledValue = enabled ? "true" : "false";
|
||||
if (!section) return appendBlock(config, `[${header}]\nenabled = ${enabledValue}\n`);
|
||||
return replaceOrInsertSetting(config, section, "enabled", enabledValue);
|
||||
}
|
||||
|
||||
function ensureOmoGitBashMcpPolicy(config, { marketplaceName, pluginNames, platform }) {
|
||||
if (marketplaceName !== "sisyphuslabs" || !pluginNames.includes("omo")) return config;
|
||||
return ensurePluginMcpEnabled(config, "omo@sisyphuslabs", "git_bash", platform === "win32");
|
||||
}
|
||||
|
||||
function ensureHookTrusted(config, key, trustedHash) {
|
||||
const header = `hooks.state.${JSON.stringify(key)}`;
|
||||
const section = findTomlSection(config, header);
|
||||
@@ -237,7 +252,8 @@ function parseJsonString(value) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return typeof parsed === "string" ? parsed : null;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,3 +128,46 @@ test("#given non-Windows platform #when preparing #then winget is never called",
|
||||
assert.deepEqual(runCalls, []);
|
||||
assert.deepEqual(result, { found: true, path: null, source: "not-required" });
|
||||
});
|
||||
|
||||
test("#given Windows without Git Bash and winget fails #when preparing #then original install hint is preserved", async () => {
|
||||
const missingResolution = {
|
||||
found: false,
|
||||
checkedPaths: [programFilesGitBash, programFilesX86GitBash],
|
||||
installHint: "install hint",
|
||||
};
|
||||
|
||||
const result = await prepareGitBashForInstall({
|
||||
platform: "win32",
|
||||
env: {},
|
||||
cwd: "C:\\repo",
|
||||
resolveGitBash: () => missingResolution,
|
||||
runCommand: async () => {
|
||||
throw new Error("winget unavailable");
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, missingResolution);
|
||||
});
|
||||
|
||||
test("#given Windows without Git Bash and winget succeeds but bash is still missing #when preparing #then install hint remains", async () => {
|
||||
const missingResolution = {
|
||||
found: false,
|
||||
checkedPaths: [programFilesGitBash, programFilesX86GitBash],
|
||||
installHint: "install hint",
|
||||
};
|
||||
let resolveCallCount = 0;
|
||||
|
||||
const result = await prepareGitBashForInstall({
|
||||
platform: "win32",
|
||||
env: {},
|
||||
cwd: "C:\\repo",
|
||||
resolveGitBash: () => {
|
||||
resolveCallCount += 1;
|
||||
return missingResolution;
|
||||
},
|
||||
runCommand: async () => {},
|
||||
});
|
||||
|
||||
assert.equal(resolveCallCount, 2);
|
||||
assert.deepEqual(result, missingResolution);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user