test(omo-codex): batch 84 (10 files)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { lstat, mkdir, readFile, readlink, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { installMarketplaceLocally } from "./install-local.mjs";
|
||||
import { makeTempDir, writeJson, writePluginAt } from "./install-test-fixtures.mjs";
|
||||
|
||||
const legacyCodexPluginMarketplace = ["code", "yeongyu", "codex", "plugins"].join("-");
|
||||
|
||||
test(
|
||||
"#given bundled agent roles and stale legacy links #when installing locally #then relinks Codex agents to marketplace snapshot",
|
||||
{ skip: process.platform === "win32" ? "Windows copies agent files instead of symlinking them" : false },
|
||||
async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
const pluginRoot = join(codexPackageRoot, "plugin");
|
||||
const agentsRoot = join(pluginRoot, "components", "ultrawork", "agents");
|
||||
|
||||
await writeJson(join(codexPackageRoot, "marketplace.json"), {
|
||||
name: "sisyphuslabs",
|
||||
plugins: [{ name: "omo", source: "./plugins/omo" }],
|
||||
});
|
||||
await writePluginAt(pluginRoot, "omo", "0.1.0");
|
||||
await mkdir(agentsRoot, { recursive: true });
|
||||
for (const agentName of ["explorer", "librarian", "plan"]) {
|
||||
await writeFile(join(agentsRoot, `${agentName}.toml`), `name = "${agentName}"\n`);
|
||||
}
|
||||
await mkdir(join(codexHome, "agents"), { recursive: true });
|
||||
await symlink(
|
||||
join(codexHome, "plugins", "cache", legacyCodexPluginMarketplace, "omo", "0.1.0", "components", "ultrawork", "agents", "explorer.toml"),
|
||||
join(codexHome, "agents", "explorer.toml"),
|
||||
);
|
||||
|
||||
const result = await installMarketplaceLocally({
|
||||
repoRoot,
|
||||
codexHome,
|
||||
platform: "linux",
|
||||
runCommand: async () => {},
|
||||
log: () => {},
|
||||
});
|
||||
|
||||
assert.equal(result.installed.length, 1);
|
||||
const snapshotPluginPath = join(codexHome, ".tmp", "marketplaces", "sisyphuslabs", "plugins", "omo");
|
||||
for (const agentName of ["explorer", "librarian", "plan"]) {
|
||||
const agentPath = join(codexHome, "agents", `${agentName}.toml`);
|
||||
assert.equal((await lstat(agentPath)).isSymbolicLink(), true);
|
||||
assert.equal(await readlink(agentPath), join(snapshotPluginPath, "components", "ultrawork", "agents", `${agentName}.toml`));
|
||||
assert.equal(await readFile(agentPath, "utf8"), `name = "${agentName}"\n`);
|
||||
}
|
||||
|
||||
const installedAgents = JSON.parse(await readFile(join(snapshotPluginPath, ".installed-agents.json"), "utf8"));
|
||||
assert.deepEqual(installedAgents.agents.sort(), [
|
||||
join(codexHome, "agents", "explorer.toml"),
|
||||
join(codexHome, "agents", "librarian.toml"),
|
||||
join(codexHome, "agents", "plan.toml"),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"#given local sisyphuslabs install #when plugin cache is pruned #then agent links still resolve through marketplace snapshot",
|
||||
{ skip: process.platform === "win32" ? "Windows copies agent files instead of symlinking them" : false },
|
||||
async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
const pluginRoot = join(codexPackageRoot, "plugin");
|
||||
const agentsRoot = join(pluginRoot, "components", "ultrawork", "agents");
|
||||
|
||||
await writeJson(join(codexPackageRoot, "marketplace.json"), {
|
||||
name: "sisyphuslabs",
|
||||
plugins: [{ name: "omo", source: "./plugins/omo" }],
|
||||
});
|
||||
await writePluginAt(pluginRoot, "omo", "0.1.0");
|
||||
await mkdir(agentsRoot, { recursive: true });
|
||||
await writeFile(join(agentsRoot, "explorer.toml"), 'name = "explorer"\n');
|
||||
const snapshotRoot = join(codexHome, ".tmp", "marketplaces", "sisyphuslabs");
|
||||
await mkdir(join(snapshotRoot, ".git"), { recursive: true });
|
||||
await writeFile(join(snapshotRoot, ".git", "config"), "[remote \"origin\"]\n");
|
||||
await writeFile(join(snapshotRoot, ".codex-marketplace-install.json"), '{"source_type":"git"}\n');
|
||||
|
||||
const result = await installMarketplaceLocally({
|
||||
repoRoot,
|
||||
codexHome,
|
||||
platform: "linux",
|
||||
runCommand: async () => {},
|
||||
log: () => {},
|
||||
});
|
||||
|
||||
const pluginPath = result.installed[0].path;
|
||||
await rm(pluginPath, { recursive: true, force: true });
|
||||
|
||||
const agentPath = join(codexHome, "agents", "explorer.toml");
|
||||
assert.equal(
|
||||
await readlink(agentPath),
|
||||
join(codexHome, ".tmp", "marketplaces", "sisyphuslabs", "plugins", "omo", "components", "ultrawork", "agents", "explorer.toml"),
|
||||
);
|
||||
assert.equal(await readFile(agentPath, "utf8"), 'name = "explorer"\n');
|
||||
assert.equal(await readFile(join(snapshotRoot, ".git", "config"), "utf8"), "[remote \"origin\"]\n");
|
||||
assert.equal(await readFile(join(snapshotRoot, ".codex-marketplace-install.json"), "utf8"), '{"source_type":"git"}\n');
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,123 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, readFile, readlink, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { linkCachedPluginBins } from "./install/cache.mjs";
|
||||
import { makeTempDir, writeJson } from "./install-test-fixtures.mjs";
|
||||
|
||||
test("#given Windows platform #when linking cached plugin bins #then writes command shims", async () => {
|
||||
const root = await makeTempDir();
|
||||
const pluginRoot = join(root, "plugin");
|
||||
const binDir = join(root, "bin");
|
||||
|
||||
await mkdir(pluginRoot, { recursive: true });
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: "@example/alpha",
|
||||
bin: {
|
||||
alpha: "./dist/cli.js",
|
||||
},
|
||||
});
|
||||
await mkdir(join(pluginRoot, "dist"), { recursive: true });
|
||||
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
|
||||
|
||||
const linked = await linkCachedPluginBins({ binDir, pluginRoot, platform: "win32" });
|
||||
|
||||
assert.deepEqual(linked, [{ name: "alpha", path: join(binDir, "alpha.cmd"), target: join(pluginRoot, "dist", "cli.js") }]);
|
||||
const shim = await readFile(join(binDir, "alpha.cmd"), "utf8");
|
||||
assert.match(shim, /@echo off/);
|
||||
assert.match(shim, new RegExp(`node "${join(pluginRoot, "dist", "cli.js").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}" %\\*`));
|
||||
});
|
||||
|
||||
test("#given existing custom Windows command shim #when linking bins #then rejects without overwriting", async () => {
|
||||
const root = await makeTempDir();
|
||||
const pluginRoot = join(root, "plugin");
|
||||
const binDir = join(root, "bin");
|
||||
|
||||
await mkdir(pluginRoot, { recursive: true });
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: "@example/alpha",
|
||||
bin: {
|
||||
alpha: "./dist/cli.js",
|
||||
},
|
||||
});
|
||||
await mkdir(join(pluginRoot, "dist"), { recursive: true });
|
||||
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
|
||||
await writeFile(join(binDir, "alpha.cmd"), "@echo off\r\necho custom\r\n");
|
||||
|
||||
await assert.rejects(
|
||||
linkCachedPluginBins({ binDir, pluginRoot, platform: "win32" }),
|
||||
/already exists and is not a generated command shim/,
|
||||
);
|
||||
assert.match(await readFile(join(binDir, "alpha.cmd"), "utf8"), /echo custom/);
|
||||
});
|
||||
|
||||
test("#given managed legacy Codex component symlink #when linking bins #then removes stale symlink and writes OMO bin", async () => {
|
||||
const root = await makeTempDir();
|
||||
const pluginRoot = join(root, "plugin");
|
||||
const binDir = join(root, "bin");
|
||||
const oldTarget = join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "rules", "dist", "cli.js");
|
||||
|
||||
await mkdir(join(pluginRoot, "dist"), { recursive: true });
|
||||
await mkdir(join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "rules", "dist"), { recursive: true });
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: "@example/omo",
|
||||
bin: { "omo-rules": "./dist/cli.js" },
|
||||
});
|
||||
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
|
||||
await writeFile(oldTarget, "#!/usr/bin/env node\n");
|
||||
await symlink(oldTarget, join(binDir, "codex-rules"));
|
||||
|
||||
await linkCachedPluginBins({ binDir, pluginRoot, platform: "linux" });
|
||||
|
||||
await assert.rejects(readlink(join(binDir, "codex-rules")));
|
||||
assert.equal(await readlink(join(binDir, "omo-rules")), join(pluginRoot, "dist", "cli.js"));
|
||||
});
|
||||
|
||||
test("#given user-owned legacy Codex symlink #when linking bins #then preserves the user symlink", async () => {
|
||||
const root = await makeTempDir();
|
||||
const pluginRoot = join(root, "plugin");
|
||||
const binDir = join(root, "bin");
|
||||
const userTarget = join(root, "user-tools", "codex-rules");
|
||||
|
||||
await mkdir(join(pluginRoot, "dist"), { recursive: true });
|
||||
await mkdir(join(root, "user-tools"), { recursive: true });
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: "@example/omo",
|
||||
bin: { "omo-rules": "./dist/cli.js" },
|
||||
});
|
||||
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
|
||||
await writeFile(userTarget, "#!/usr/bin/env node\n");
|
||||
await symlink(userTarget, join(binDir, "codex-rules"));
|
||||
|
||||
await linkCachedPluginBins({ binDir, pluginRoot, platform: "linux" });
|
||||
|
||||
assert.equal(await readlink(join(binDir, "codex-rules")), userTarget);
|
||||
assert.equal(await readlink(join(binDir, "omo-rules")), join(pluginRoot, "dist", "cli.js"));
|
||||
});
|
||||
|
||||
test("#given user-owned legacy Codex symlink with component-like target #when linking bins #then preserves it", async () => {
|
||||
const root = await makeTempDir();
|
||||
const pluginRoot = join(root, "plugin");
|
||||
const binDir = join(root, "bin");
|
||||
const userTarget = join(root, "workspace", "components", "rules", "dist", "cli.js");
|
||||
|
||||
await mkdir(join(pluginRoot, "dist"), { recursive: true });
|
||||
await mkdir(join(root, "workspace", "components", "rules", "dist"), { recursive: true });
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: "@example/omo",
|
||||
bin: { "omo-rules": "./dist/cli.js" },
|
||||
});
|
||||
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
|
||||
await writeFile(userTarget, "#!/usr/bin/env node\n");
|
||||
await symlink(userTarget, join(binDir, "codex-rules"));
|
||||
|
||||
await linkCachedPluginBins({ binDir, pluginRoot, platform: "linux" });
|
||||
|
||||
assert.equal(await readlink(join(binDir, "codex-rules")), userTarget);
|
||||
assert.equal(await readlink(join(binDir, "omo-rules")), join(pluginRoot, "dist", "cli.js"));
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { installCachedPlugin } from "./install/cache.mjs";
|
||||
import { makeTempDir } from "./install-test-fixtures.mjs";
|
||||
|
||||
test("#given source plugin has a stale npm lockfile #when caching plugin #then lockfile is regenerated rather than copied", async () => {
|
||||
// given
|
||||
const root = await makeTempDir();
|
||||
const codexHome = join(root, "codex-home");
|
||||
const sourceRoot = join(root, "plugin");
|
||||
await mkdir(sourceRoot, { recursive: true });
|
||||
await writeFile(join(sourceRoot, "package.json"), JSON.stringify({ name: "@scope/omo", version: "0.1.0" }));
|
||||
await writeFile(join(sourceRoot, "package-lock.json"), '{"packages":{"components/ulw-loop":{}}}\n');
|
||||
|
||||
// when
|
||||
const installed = await installCachedPlugin({
|
||||
codexHome,
|
||||
marketplaceName: "debug",
|
||||
name: "omo",
|
||||
sourcePath: sourceRoot,
|
||||
version: "0.1.0",
|
||||
runCommand: async () => {},
|
||||
});
|
||||
|
||||
// then
|
||||
await assert.rejects(stat(join(installed.path, "package-lock.json")));
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { updateCodexConfig } from "./install/config.mjs";
|
||||
|
||||
test("#given empty Codex config #when script installer updates config #then enables MultiAgentV2 with ten thousand session threads", async () => {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-multi-agent-"));
|
||||
const configPath = join(root, "config.toml");
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "debug",
|
||||
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
|
||||
pluginNames: ["omo"],
|
||||
});
|
||||
|
||||
// then
|
||||
const config = await readFile(configPath, "utf8");
|
||||
assert.match(config, /\[features\.multi_agent_v2\]/);
|
||||
assert.match(config, /enabled = true/);
|
||||
assert.match(config, /max_concurrent_threads_per_session = 10000/);
|
||||
});
|
||||
|
||||
test("#given sisyphuslabs config without explicit source #when script installer updates config #then uses local marketplace", async () => {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-sisyphuslabs-"));
|
||||
const configPath = join(root, "config.toml");
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "sisyphuslabs",
|
||||
pluginNames: ["omo"],
|
||||
});
|
||||
|
||||
// then
|
||||
const config = await readFile(configPath, "utf8");
|
||||
assert.match(config, /\[marketplaces\.sisyphuslabs\]/);
|
||||
assert.match(config, /source_type = "local"/);
|
||||
assert.match(config, /source = "\/repo\/packages\/omo-codex"/);
|
||||
assert.doesNotMatch(config, /lazycodex\.git/);
|
||||
assert.doesNotMatch(config, /ref = "main"/);
|
||||
});
|
||||
|
||||
test("#given existing MultiAgentV2 table #when script installer updates config #then preserves unrelated tuning while setting ten thousand session threads", async () => {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-multi-agent-existing-"));
|
||||
const configPath = join(root, "config.toml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
[
|
||||
"[features.multi_agent_v2]",
|
||||
"enabled = false",
|
||||
"usage_hint_enabled = false",
|
||||
"max_concurrent_threads_per_session = 4",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "debug",
|
||||
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
|
||||
pluginNames: ["omo"],
|
||||
});
|
||||
|
||||
// then
|
||||
const config = await readFile(configPath, "utf8");
|
||||
assert.match(config, /\[features\.multi_agent_v2\]/);
|
||||
assert.match(config, /enabled = true/);
|
||||
assert.match(config, /usage_hint_enabled = false/);
|
||||
assert.match(config, /max_concurrent_threads_per_session = 10000/);
|
||||
assert.doesNotMatch(config, /max_concurrent_threads_per_session = 4/);
|
||||
});
|
||||
|
||||
test("#given legacy boolean MultiAgentV2 flag and table #when script installer updates config #then normalizes to table config", async () => {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-multi-agent-legacy-"));
|
||||
const configPath = join(root, "config.toml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
[
|
||||
"[features]",
|
||||
"multi_agent_v2 = true",
|
||||
"plugins = false",
|
||||
"",
|
||||
"[features.multi_agent_v2]",
|
||||
"usage_hint_enabled = false",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "debug",
|
||||
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
|
||||
pluginNames: ["omo"],
|
||||
});
|
||||
|
||||
// then
|
||||
const config = await readFile(configPath, "utf8");
|
||||
assert.doesNotMatch(config, /^multi_agent_v2\s*=/m);
|
||||
assert.match(config, /\[features\.multi_agent_v2\]/);
|
||||
assert.match(config, /enabled = true/);
|
||||
assert.match(config, /usage_hint_enabled = false/);
|
||||
assert.match(config, /max_concurrent_threads_per_session = 10000/);
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
installCachedPlugin,
|
||||
linkCachedPluginBins,
|
||||
pruneMarketplaceCache,
|
||||
pruneMarketplacePluginCaches,
|
||||
} from "./install/cache.mjs";
|
||||
import { linkCachedPluginAgents } from "./install/agents.mjs";
|
||||
import { updateCodexConfig } from "./install/config.mjs";
|
||||
import { trustedHookStatesForPlugin } from "./install/hook-trust.mjs";
|
||||
import { defaultRunCommand } from "./install/process.mjs";
|
||||
import { writeInstalledMarketplaceSnapshot } from "./install/snapshot.mjs";
|
||||
import {
|
||||
readMarketplace,
|
||||
readPluginManifest,
|
||||
resolvePluginSource,
|
||||
validatePathSegment,
|
||||
} from "./install/marketplace.mjs";
|
||||
|
||||
const LEGACY_CODEX_PLUGIN_MARKETPLACE = ["code", "yeongyu", "codex", "plugins"].join("-");
|
||||
const SISYPHUS_LEGACY_CACHE_MARKETPLACES = ["lazycodex", LEGACY_CODEX_PLUGIN_MARKETPLACE];
|
||||
|
||||
export function resolveCodexInstallerBinDir(options = {}) {
|
||||
const homeDir = resolve(options.homeDir ?? homedir());
|
||||
const env = options.env ?? process.env;
|
||||
const explicitBinDir = nonEmptyEnvValue(env, "CODEX_LOCAL_BIN_DIR");
|
||||
if (explicitBinDir !== undefined) return explicitBinDir;
|
||||
|
||||
const codexHome = resolve(options.codexHome ?? nonEmptyEnvValue(env, "CODEX_HOME") ?? join(homeDir, ".codex"));
|
||||
const defaultCodexHome = resolve(join(homeDir, ".codex"));
|
||||
return codexHome === defaultCodexHome ? join(homeDir, ".local", "bin") : join(codexHome, "bin");
|
||||
}
|
||||
|
||||
export async function installMarketplaceLocally(options = {}) {
|
||||
const repoRoot = resolve(options.repoRoot ?? process.cwd());
|
||||
const env = options.env ?? process.env;
|
||||
const homeDir = resolve(options.homeDir ?? homedir());
|
||||
const codexHome = resolve(options.codexHome ?? nonEmptyEnvValue(env, "CODEX_HOME") ?? join(homeDir, ".codex"));
|
||||
const binDir = resolve(options.binDir ?? resolveCodexInstallerBinDir({ codexHome, env, homeDir }));
|
||||
const platform = options.platform ?? process.platform;
|
||||
const runCommand = options.runCommand ?? defaultRunCommand;
|
||||
const log = options.log ?? console.log;
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
const marketplace = await readMarketplace(repoRoot, {
|
||||
marketplacePath: join(codexPackageRoot, "marketplace.json"),
|
||||
});
|
||||
const installed = [];
|
||||
const pluginSources = [];
|
||||
const agentConfigs = new Map();
|
||||
|
||||
for (const entry of marketplace.plugins) {
|
||||
const sourcePath = resolvePluginSource(codexPackageRoot, entry, { pathOverride: "./plugin" });
|
||||
const manifest = await readPluginManifest(sourcePath);
|
||||
if (manifest.name !== entry.name) {
|
||||
throw new Error(
|
||||
`plugin manifest name ${JSON.stringify(manifest.name)} does not match marketplace name ${JSON.stringify(entry.name)}`,
|
||||
);
|
||||
}
|
||||
const version = manifest.version ?? "local";
|
||||
validatePathSegment(version, "plugin version");
|
||||
|
||||
log(`Building ${entry.name}@${version}`);
|
||||
const plugin = await installCachedPlugin({
|
||||
codexHome,
|
||||
marketplaceName: marketplace.name,
|
||||
name: entry.name,
|
||||
runCommand,
|
||||
sourcePath,
|
||||
version,
|
||||
});
|
||||
const binLinks = await linkCachedPluginBins({ binDir, pluginRoot: plugin.path, platform });
|
||||
for (const link of binLinks) {
|
||||
log(`Linked ${link.name} -> ${link.target}`);
|
||||
}
|
||||
pluginSources.push({ name: entry.name, sourcePath });
|
||||
installed.push(plugin);
|
||||
}
|
||||
|
||||
const agentSourceRoots = await agentSourceRootsForInstall({ codexHome, marketplace, installed, pluginSources });
|
||||
for (const plugin of installed) {
|
||||
const pluginRoot = agentSourceRoots.get(plugin.name) ?? plugin.path;
|
||||
const agentLinks = await linkCachedPluginAgents({ codexHome, pluginRoot, platform });
|
||||
for (const link of agentLinks) {
|
||||
log(`Linked agent ${link.name} -> ${link.target}`);
|
||||
const agentName = agentNameFromToml(link.name);
|
||||
agentConfigs.set(agentName, { name: agentName, configFile: `./agents/${link.name}` });
|
||||
}
|
||||
}
|
||||
|
||||
const pluginNames = marketplace.plugins.map((plugin) => plugin.name);
|
||||
const trustedHookStates = (
|
||||
await Promise.all(
|
||||
installed.map((plugin) =>
|
||||
trustedHookStatesForPlugin({
|
||||
marketplaceName: marketplace.name,
|
||||
pluginName: plugin.name,
|
||||
pluginRoot: plugin.path,
|
||||
}),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
await pruneMarketplaceCache({ codexHome, marketplaceName: marketplace.name, keepPluginNames: pluginNames });
|
||||
for (const legacyMarketplaceName of legacyCacheMarketplaces(marketplace.name)) {
|
||||
await pruneMarketplacePluginCaches({ codexHome, marketplaceName: legacyMarketplaceName, pluginNames });
|
||||
}
|
||||
const marketplaceRoot = join(codexHome, "plugins", "cache", marketplace.name);
|
||||
await writeCachedMarketplaceManifest({
|
||||
marketplaceName: marketplace.name,
|
||||
marketplaceRoot,
|
||||
plugins: installed,
|
||||
});
|
||||
await updateCodexConfig({
|
||||
configPath: join(codexHome, "config.toml"),
|
||||
repoRoot: codexPackageRoot,
|
||||
marketplaceName: marketplace.name,
|
||||
marketplaceSource: { sourceType: "local", source: marketplaceRoot },
|
||||
pluginNames,
|
||||
trustedHookStates,
|
||||
agentConfigs: [...agentConfigs.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
||||
});
|
||||
|
||||
for (const plugin of installed) {
|
||||
log(`Installed ${plugin.name}@${marketplace.name} -> ${plugin.path}`);
|
||||
}
|
||||
|
||||
return { marketplaceName: marketplace.name, installed };
|
||||
}
|
||||
|
||||
function agentNameFromToml(fileName) {
|
||||
return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName;
|
||||
}
|
||||
|
||||
async function agentSourceRootsForInstall({ codexHome, marketplace, installed, pluginSources }) {
|
||||
if (marketplace.name !== "sisyphuslabs") {
|
||||
return new Map(installed.map((plugin) => [plugin.name, plugin.path]));
|
||||
}
|
||||
const snapshotPlugins = await writeInstalledMarketplaceSnapshot({
|
||||
codexHome,
|
||||
marketplace,
|
||||
plugins: pluginSources,
|
||||
});
|
||||
return new Map(snapshotPlugins.map((plugin) => [plugin.name, plugin.path]));
|
||||
}
|
||||
|
||||
async function writeCachedMarketplaceManifest({ marketplaceName, marketplaceRoot, plugins }) {
|
||||
const marketplaceDir = join(marketplaceRoot, ".agents", "plugins");
|
||||
await mkdir(marketplaceDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(marketplaceDir, "marketplace.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: marketplaceName,
|
||||
plugins: plugins.map((plugin) => ({
|
||||
name: plugin.name,
|
||||
source: { source: "local", path: `./${plugin.name}/${plugin.version}` },
|
||||
})),
|
||||
},
|
||||
null,
|
||||
"\t",
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function nonEmptyEnvValue(env, key) {
|
||||
const value = env[key];
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length === 0 ? undefined : value;
|
||||
}
|
||||
|
||||
function legacyCacheMarketplaces(marketplaceName) {
|
||||
return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_CACHE_MARKETPLACES : [];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const repoRoot = process.argv[2] ? resolve(process.argv[2]) : process.cwd();
|
||||
const result = await installMarketplaceLocally({ repoRoot });
|
||||
console.log(`Installed ${result.installed.length} plugin(s) from ${result.marketplaceName}.`);
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
|
||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, readFile, readlink, stat, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { installMarketplaceLocally, resolveCodexInstallerBinDir } from "./install-local.mjs";
|
||||
import { makeTempDir, writeJson, writePluginAt } from "./install-test-fixtures.mjs";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const legacyCodexPluginMarketplace = ["code", "yeongyu", "codex", "plugins"].join("-");
|
||||
|
||||
test("#given default CODEX_HOME #when resolving local installer bin dir without override #then preserves user local bin precedence", () => {
|
||||
const homeDir = join("/tmp", "omo-codex-home-default");
|
||||
const codexHome = join(homeDir, ".codex");
|
||||
|
||||
assert.equal(resolveCodexInstallerBinDir({ codexHome, env: {}, homeDir }), join(homeDir, ".local", "bin"));
|
||||
});
|
||||
|
||||
test("#given custom CODEX_HOME #when resolving local installer bin dir without override #then keeps generated omo inside that Codex home", () => {
|
||||
const homeDir = join("/tmp", "omo-codex-home-custom");
|
||||
const codexHome = join("/tmp", "omo-codex-install-custom");
|
||||
|
||||
assert.equal(resolveCodexInstallerBinDir({ codexHome, env: {}, homeDir }), join(codexHome, "bin"));
|
||||
});
|
||||
|
||||
test("#given custom CODEX_HOME and PATH without omo #when installing locally without bin override #then bootstraps via local CLI when omo is absent", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const homeDir = await makeTempDir();
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
const pluginRoot = join(codexPackageRoot, "plugin");
|
||||
|
||||
await writeJson(join(codexPackageRoot, "marketplace.json"), {
|
||||
name: "sisyphuslabs",
|
||||
plugins: [{ name: "omo", source: "./plugins/omo" }],
|
||||
});
|
||||
await writePluginAt(pluginRoot, "omo", "0.1.0");
|
||||
|
||||
const result = await installMarketplaceLocally({
|
||||
repoRoot,
|
||||
codexHome,
|
||||
env: { PATH: "/usr/bin:/bin" },
|
||||
homeDir,
|
||||
platform: "linux",
|
||||
runCommand: async () => {},
|
||||
log: () => {},
|
||||
});
|
||||
|
||||
assert.equal(result.installed.length, 1);
|
||||
assert.equal(await readlink(join(codexHome, "bin", "omo")), join(result.installed[0].path, "dist", "cli.js"));
|
||||
});
|
||||
|
||||
test("#given explicit CODEX_LOCAL_BIN_DIR #when resolving local installer bin dir #then preserves installed omo precedence", () => {
|
||||
const homeDir = join("/tmp", "omo-codex-home-explicit");
|
||||
const codexHome = join("/tmp", "omo-codex-install-explicit");
|
||||
const explicitBinDir = join("/tmp", "omo-codex-explicit-bin");
|
||||
|
||||
assert.equal(
|
||||
resolveCodexInstallerBinDir({
|
||||
codexHome,
|
||||
env: { CODEX_LOCAL_BIN_DIR: explicitBinDir },
|
||||
homeDir,
|
||||
}),
|
||||
explicitBinDir,
|
||||
);
|
||||
});
|
||||
|
||||
test("#given omo plugin source #when inspecting identity #then uses sisyphuslabs omo metadata", async () => {
|
||||
const pluginRoot = join(scriptDir, "..", "plugin");
|
||||
|
||||
const manifest = JSON.parse(await readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"));
|
||||
const packageJson = JSON.parse(await readFile(join(pluginRoot, "package.json"), "utf8"));
|
||||
|
||||
assert.equal(packageJson.name, "@sisyphuslabs/omo-codex-plugin");
|
||||
assert.equal(manifest.homepage, "https://github.com/sisyphuslabs/omo");
|
||||
assert.equal(manifest.repository, "https://github.com/sisyphuslabs/omo");
|
||||
assert.equal(manifest.interface.websiteURL, "https://github.com/sisyphuslabs/omo");
|
||||
assert.equal(manifest.interface.privacyPolicyURL, "https://github.com/sisyphuslabs/omo#privacy");
|
||||
assert.equal(manifest.interface.termsOfServiceURL, "https://github.com/sisyphuslabs/omo#license");
|
||||
});
|
||||
|
||||
test("#given local marketplace #when installing #then copies versioned plugins and enables config", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const binDir = await makeTempDir();
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
const pluginRoot = join(codexPackageRoot, "plugin");
|
||||
|
||||
await writeJson(join(codexPackageRoot, "marketplace.json"), {
|
||||
name: "debug-marketplace",
|
||||
plugins: [
|
||||
{
|
||||
name: "alpha",
|
||||
source: "./plugins/alpha",
|
||||
},
|
||||
],
|
||||
});
|
||||
await writePluginAt(pluginRoot, "alpha", "1.2.3");
|
||||
await mkdir(join(codexPackageRoot, "shared-lsp", "dist"), { recursive: true });
|
||||
await writeJson(join(codexPackageRoot, "shared-lsp", "package.json"), {
|
||||
name: "@example/shared-lsp",
|
||||
version: "0.0.0",
|
||||
type: "module",
|
||||
bin: { "shared-lsp": "./dist/cli.js" },
|
||||
});
|
||||
await writeFile(join(codexPackageRoot, "shared-lsp", "dist", "cli.js"), "#!/usr/bin/env node\n");
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: "@example/alpha",
|
||||
version: "1.2.3",
|
||||
bin: {
|
||||
alpha: "./dist/cli.js",
|
||||
},
|
||||
scripts: {
|
||||
build: "node -e \"require('fs').writeFileSync('dist/cli.js', 'console.log(1)')\"",
|
||||
},
|
||||
dependencies: {
|
||||
"@example/shared-lsp": "file:../shared-lsp",
|
||||
},
|
||||
});
|
||||
await writeJson(join(pluginRoot, ".mcp.json"), {
|
||||
mcpServers: {
|
||||
alpha: {
|
||||
command: "node",
|
||||
args: ["./dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
shared: {
|
||||
command: "node",
|
||||
args: ["../shared-lsp/dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
},
|
||||
});
|
||||
await mkdir(join(pluginRoot, "node_modules"), { recursive: true });
|
||||
await writeFile(join(pluginRoot, "node_modules", "skip.txt"), "skip");
|
||||
await mkdir(join(codexHome, "plugins", "cache", "debug-marketplace", "stale", "0.1.0"), { recursive: true });
|
||||
await writeFile(
|
||||
join(codexHome, "config.toml"),
|
||||
[
|
||||
'[plugins."stale@debug-marketplace"]',
|
||||
"enabled = true",
|
||||
"",
|
||||
'[hooks.state."stale@debug-marketplace:hooks/hooks.json:user_prompt_submit:0:0"]',
|
||||
'trusted_hash = "sha256:old"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const commands = [];
|
||||
const result = await installMarketplaceLocally({
|
||||
repoRoot,
|
||||
codexHome,
|
||||
binDir,
|
||||
platform: "linux",
|
||||
runCommand: async (command, args, options) => {
|
||||
commands.push([command, args, options.cwd]);
|
||||
if (command === "npm" && args.join(" ") === "run build") {
|
||||
await mkdir(join(options.cwd, "dist"), { recursive: true });
|
||||
await writeFile(join(options.cwd, "dist", "cli.js"), "#!/usr/bin/env node\nconsole.log(1)\n");
|
||||
}
|
||||
},
|
||||
log: () => {},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
result.installed.map((plugin) => `${plugin.name}@${plugin.version}`),
|
||||
["alpha@1.2.3"],
|
||||
);
|
||||
const alphaCacheRoot = join(codexHome, "plugins", "cache", "debug-marketplace", "alpha", "1.2.3");
|
||||
assert.equal((await stat(join(alphaCacheRoot, "dist", "cli.js"))).isFile(), true);
|
||||
assert.equal((await stat(join(alphaCacheRoot, ".mcp.json"))).isFile(), true);
|
||||
assert.equal(await readlink(join(binDir, "alpha")), join(alphaCacheRoot, "dist", "cli.js"));
|
||||
const alphaMcp = JSON.parse(await readFile(join(alphaCacheRoot, ".mcp.json"), "utf8"));
|
||||
const sharedMcpCli = join(alphaCacheRoot, "mcp", "shared", "dist", "cli.js");
|
||||
assert.deepEqual(alphaMcp.mcpServers.alpha.args, [join(alphaCacheRoot, "dist", "cli.js"), "mcp"]);
|
||||
assert.deepEqual(alphaMcp.mcpServers.shared.args, [sharedMcpCli, "mcp"]);
|
||||
assert.equal((await stat(sharedMcpCli)).isFile(), true);
|
||||
assert.equal(
|
||||
Object.hasOwn(alphaMcp.mcpServers.alpha, "cwd"),
|
||||
false,
|
||||
"`cwd: \".\"` must be stripped so the spawned MCP server inherits the caller's workspace cwd",
|
||||
);
|
||||
assert.equal(Object.hasOwn(alphaMcp.mcpServers.shared, "cwd"), false);
|
||||
assert.equal(alphaMcp.mcpServers.alpha.command, "node");
|
||||
const alphaPackageJson = JSON.parse(await readFile(join(alphaCacheRoot, "package.json"), "utf8"));
|
||||
assert.equal(alphaPackageJson.dependencies["@example/shared-lsp"], `file:${join(codexPackageRoot, "shared-lsp")}`);
|
||||
await assert.rejects(
|
||||
stat(join(codexHome, "plugins", "cache", "debug-marketplace", "alpha", "1.2.3", "node_modules")),
|
||||
/code: 'ENOENT'|ENOENT/,
|
||||
);
|
||||
await assert.rejects(
|
||||
stat(join(codexHome, "plugins", "cache", "debug-marketplace", "stale")),
|
||||
/code: 'ENOENT'|ENOENT/,
|
||||
);
|
||||
assert.deepEqual(
|
||||
commands.map(([command, args, cwd]) => [command, args.join(" "), cwd]),
|
||||
[
|
||||
["npm", "install", pluginRoot],
|
||||
["npm", "run build", pluginRoot],
|
||||
["npm", "install --omit=dev", join(codexHome, "plugins", "cache", "debug-marketplace", "alpha", "1.2.3")],
|
||||
],
|
||||
);
|
||||
|
||||
const config = await readFile(join(codexHome, "config.toml"), "utf8");
|
||||
assert.match(config, /\[features\]\n(?:plugin_hooks = true\n)?plugins = true/);
|
||||
assert.match(config, /\[marketplaces\.debug-marketplace\]/);
|
||||
assert.match(config, /source_type = "local"/);
|
||||
assert.match(config, /\[plugins\."alpha@debug-marketplace"\]\nenabled = true/);
|
||||
assert.match(config, /\[agents\.explorer\]\nconfig_file = "\.\/agents\/explorer\.toml"/);
|
||||
assert.match(config, /\[agents\.librarian\]\nconfig_file = "\.\/agents\/librarian\.toml"/);
|
||||
assert.match(config, /\[agents\.plan\]\nconfig_file = "\.\/agents\/plan\.toml"/);
|
||||
assert.doesNotMatch(config, /stale@debug-marketplace/);
|
||||
});
|
||||
|
||||
test("#given sisyphuslabs marketplace #when installing #then registers the local built marketplace cache", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
|
||||
await writeJson(join(codexPackageRoot, "marketplace.json"), {
|
||||
name: "sisyphuslabs",
|
||||
plugins: [{ name: "omo", source: "./plugins/omo" }],
|
||||
});
|
||||
await writePluginAt(join(codexPackageRoot, "plugin"), "omo", "0.1.0");
|
||||
await mkdir(join(repoRoot, "packages", "lsp-tools-mcp", "dist"), { recursive: true });
|
||||
await writeJson(join(repoRoot, "packages", "lsp-tools-mcp", "package.json"), {
|
||||
name: "@example/lsp-tools-mcp",
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
bin: { "omo-lsp": "./dist/cli.js" },
|
||||
});
|
||||
await writeFile(join(repoRoot, "packages", "lsp-tools-mcp", "dist", "cli.js"), "#!/usr/bin/env node\n");
|
||||
await writeJson(join(codexPackageRoot, "plugin", ".mcp.json"), {
|
||||
mcpServers: {
|
||||
lsp: {
|
||||
command: "node",
|
||||
args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
},
|
||||
});
|
||||
await mkdir(join(codexHome, "plugins", "cache", legacyCodexPluginMarketplace, "omo", "0.1.0"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeJson(join(codexHome, "plugins", "cache", legacyCodexPluginMarketplace, "omo", "0.1.0", ".mcp.json"), {
|
||||
mcpServers: {
|
||||
lsp: {
|
||||
command: "node",
|
||||
args: ["old/components/lsp/packages/lsp-tools-mcp/dist/cli.js", "mcp"],
|
||||
},
|
||||
},
|
||||
});
|
||||
const legacyPluginKey = `omo@${legacyCodexPluginMarketplace}`;
|
||||
await writeFile(
|
||||
join(codexHome, "config.toml"),
|
||||
[
|
||||
`[marketplaces.${legacyCodexPluginMarketplace}]`,
|
||||
'last_updated = "2026-05-01T00:00:00Z"',
|
||||
'source_type = "git"',
|
||||
'source = "https://github.com/code-yeongyu/codex-plugins.git"',
|
||||
"",
|
||||
`[plugins.${JSON.stringify(legacyPluginKey)}]`,
|
||||
"enabled = true",
|
||||
"",
|
||||
`[plugins.${JSON.stringify(legacyPluginKey)}.mcp_servers.lsp]`,
|
||||
'enabled = true',
|
||||
"",
|
||||
`[hooks.state.${JSON.stringify(`${legacyPluginKey}:hooks/hooks.json:post_tool_use:0:0`)}]`,
|
||||
'trusted_hash = "sha256:old"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await installMarketplaceLocally({
|
||||
repoRoot,
|
||||
codexHome,
|
||||
runCommand: async () => {},
|
||||
log: () => {},
|
||||
});
|
||||
|
||||
const config = await readFile(join(codexHome, "config.toml"), "utf8");
|
||||
assert.match(config, /\[marketplaces\.sisyphuslabs\]/);
|
||||
assert.match(config, /source_type = "local"/);
|
||||
assert.match(config, new RegExp(`source = ${JSON.stringify(join(codexHome, "plugins", "cache", "sisyphuslabs")).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
|
||||
assert.doesNotMatch(config, /ref = "main"/);
|
||||
assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/);
|
||||
assert.doesNotMatch(config, /\[marketplaces\.lazycodex\]/);
|
||||
assert.doesNotMatch(config, new RegExp(legacyCodexPluginMarketplace));
|
||||
assert.doesNotMatch(config, /lazycodex\.git/);
|
||||
const marketplace = JSON.parse(
|
||||
await readFile(join(codexHome, "plugins", "cache", "sisyphuslabs", ".agents", "plugins", "marketplace.json"), "utf8"),
|
||||
);
|
||||
assert.deepEqual(marketplace.plugins, [{ name: "omo", source: { source: "local", path: "./omo/0.1.0" } }]);
|
||||
const cachedMcp = JSON.parse(
|
||||
await readFile(join(codexHome, "plugins", "cache", "sisyphuslabs", "omo", "0.1.0", ".mcp.json"), "utf8"),
|
||||
);
|
||||
assert.equal(
|
||||
cachedMcp.mcpServers.lsp.args[0],
|
||||
join(codexHome, "plugins", "cache", "sisyphuslabs", "omo", "0.1.0", "mcp", "lsp", "dist", "cli.js"),
|
||||
);
|
||||
assert.doesNotMatch(cachedMcp.mcpServers.lsp.args[0], /components\/lsp\/packages/);
|
||||
assert.equal((await stat(cachedMcp.mcpServers.lsp.args[0])).isFile(), true);
|
||||
const snapshotPluginRoot = join(codexHome, ".tmp", "marketplaces", "sisyphuslabs", "plugins", "omo");
|
||||
const snapshotMcp = JSON.parse(await readFile(join(snapshotPluginRoot, ".mcp.json"), "utf8"));
|
||||
assert.equal(
|
||||
snapshotMcp.mcpServers.lsp.args[0],
|
||||
join(snapshotPluginRoot, "mcp", "lsp", "dist", "cli.js"),
|
||||
);
|
||||
assert.doesNotMatch(snapshotMcp.mcpServers.lsp.args[0], /\.\.\/\.\.\/lsp-tools-mcp/);
|
||||
assert.doesNotMatch(snapshotMcp.mcpServers.lsp.args[0], /components\/lsp\/packages/);
|
||||
assert.equal((await stat(snapshotMcp.mcpServers.lsp.args[0])).isFile(), true);
|
||||
await assert.rejects(
|
||||
stat(join(codexHome, "plugins", "cache", legacyCodexPluginMarketplace, "omo")),
|
||||
/code: 'ENOENT'|ENOENT/,
|
||||
);
|
||||
});
|
||||
|
||||
test("#given plugin hooks #when installing #then records trusted hook hashes", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
|
||||
await writeJson(join(codexPackageRoot, "marketplace.json"), {
|
||||
name: "debug-marketplace",
|
||||
plugins: [{ name: "alpha", source: "./plugins/alpha" }],
|
||||
});
|
||||
const pluginRoot = join(codexPackageRoot, "plugin");
|
||||
await writePluginAt(pluginRoot, "alpha", "1.2.3");
|
||||
await writeJson(join(pluginRoot, "hooks", "hooks.json"), {
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
hooks: [
|
||||
{
|
||||
type: "command",
|
||||
command: "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit",
|
||||
timeout: 10,
|
||||
statusMessage: "checking alpha",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await installMarketplaceLocally({
|
||||
repoRoot,
|
||||
codexHome,
|
||||
runCommand: async () => {},
|
||||
log: () => {},
|
||||
});
|
||||
|
||||
const config = await readFile(join(codexHome, "config.toml"), "utf8");
|
||||
assert.match(config, /\[hooks\.state\."alpha@debug-marketplace:hooks\/hooks\.json:user_prompt_submit:0:0"\]/);
|
||||
assert.match(config, /trusted_hash = "sha256:[a-f0-9]{64}"/);
|
||||
});
|
||||
|
||||
test("#given bad plugin source path #when installing #then rejects traversal", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
|
||||
|
||||
await writeJson(join(codexPackageRoot, "marketplace.json"), {
|
||||
name: "debug-marketplace",
|
||||
plugins: [
|
||||
{
|
||||
name: "escape",
|
||||
source: "../escape",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
installMarketplaceLocally({ repoRoot, codexHome, log: () => {} }),
|
||||
/local plugin source path must start with \.\//,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { installCachedPlugin } from "./install/cache.mjs";
|
||||
import { createCachedMcpRuntimeArgRewriter } from "./install/mcp-runtime-cache.mjs";
|
||||
import { makeTempDir, writeJson } from "./install-test-fixtures.mjs";
|
||||
|
||||
test("#given external MCP package runtime #when installing cached plugin #then runtime is copied into the plugin cache", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
|
||||
const astGrepPackageRoot = join(repoRoot, "packages", "ast-grep-mcp");
|
||||
const lspPackageRoot = join(repoRoot, "packages", "lsp-tools-mcp");
|
||||
|
||||
await writeJson(join(astGrepPackageRoot, "package.json"), {
|
||||
name: "@example/does-not-matter-either",
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
bin: { "omo-ast-grep": "./dist/cli.js" },
|
||||
});
|
||||
await writeJson(join(lspPackageRoot, "package.json"), {
|
||||
name: "@example/does-not-matter",
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
bin: { "omo-lsp": "./dist/cli.js" },
|
||||
});
|
||||
await writeJson(join(sourceRoot, "package.json"), {
|
||||
name: "@example/omo",
|
||||
version: "0.1.0",
|
||||
});
|
||||
await writeJson(join(sourceRoot, ".mcp.json"), {
|
||||
mcpServers: {
|
||||
ast_grep: {
|
||||
command: "node",
|
||||
args: ["../../ast-grep-mcp/dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
lsp: {
|
||||
command: "node",
|
||||
args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
},
|
||||
});
|
||||
await writeJson(join(astGrepPackageRoot, "dist", "cli.js"), { executable: true });
|
||||
await writeJson(join(lspPackageRoot, "dist", "cli.js"), { executable: true });
|
||||
await writeJson(join(lspPackageRoot, "dist", "lsp", "manager.js"), { copied: true });
|
||||
|
||||
const result = await installCachedPlugin({
|
||||
codexHome,
|
||||
marketplaceName: "sisyphuslabs",
|
||||
name: "omo",
|
||||
runCommand: async () => {},
|
||||
sourcePath: sourceRoot,
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
const cachedMcp = JSON.parse(await readFile(join(result.path, ".mcp.json"), "utf8"));
|
||||
const copiedAstGrepCli = join(result.path, "mcp", "ast_grep", "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.lsp.args, [copiedCli, "mcp"]);
|
||||
assert.equal(Object.hasOwn(cachedMcp.mcpServers.ast_grep, "cwd"), false);
|
||||
assert.equal(Object.hasOwn(cachedMcp.mcpServers.lsp, "cwd"), false);
|
||||
assert.equal((await stat(copiedAstGrepCli)).isFile(), true);
|
||||
assert.equal((await stat(copiedCli)).isFile(), true);
|
||||
assert.equal((await stat(join(result.path, "mcp", "lsp", "dist", "lsp", "manager.js"))).isFile(), true);
|
||||
});
|
||||
|
||||
test("#given multiple args from one external MCP package #when rewriting #then copies the dist tree once and rewrites each runtime arg", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const pluginRoot = join(repoRoot, "packages", "omo-codex", "plugin");
|
||||
const sourceRoot = pluginRoot;
|
||||
const packageRoot = join(repoRoot, "packages", "multi-tool-mcp");
|
||||
const copiedRoots = [];
|
||||
|
||||
await writeJson(join(packageRoot, "package.json"), {
|
||||
name: "@example/multi-tool-mcp",
|
||||
version: "0.1.0",
|
||||
bin: { "multi-tool": "./dist/cli.js" },
|
||||
});
|
||||
await writeJson(join(packageRoot, "dist", "cli.js"), { executable: true });
|
||||
await writeJson(join(packageRoot, "dist", "worker.js"), { worker: true });
|
||||
|
||||
const rewrite = createCachedMcpRuntimeArgRewriter({
|
||||
copyDist: async (_source, target) => {
|
||||
copiedRoots.push(target);
|
||||
},
|
||||
});
|
||||
|
||||
const first = await rewrite({ arg: "../../multi-tool-mcp/dist/cli.js", pluginRoot, serverName: "multi", sourceRoot });
|
||||
const second = await rewrite({ arg: "../../multi-tool-mcp/dist/worker.js", pluginRoot, serverName: "multi", sourceRoot });
|
||||
|
||||
assert.equal(copiedRoots.length, 1);
|
||||
assert.deepEqual([first, second], [
|
||||
join(pluginRoot, "mcp", "multi", "dist", "cli.js"),
|
||||
join(pluginRoot, "mcp", "multi", "dist", "worker.js"),
|
||||
]);
|
||||
});
|
||||
|
||||
test("#given plugin-local MCP runtime #when rewriting cached manifest args #then keeps the cached plugin dist path", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
|
||||
const pluginRoot = join(codexHome, "plugins", "cache", "sisyphuslabs", "omo", "0.1.0");
|
||||
const copiedRoots = [];
|
||||
|
||||
await writeJson(join(sourceRoot, "package.json"), {
|
||||
name: "@example/source-plugin",
|
||||
version: "0.1.0",
|
||||
});
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: "@example/cached-plugin",
|
||||
version: "0.1.0",
|
||||
});
|
||||
await writeJson(join(pluginRoot, "dist", "cli.js"), { executable: true });
|
||||
|
||||
const rewrite = createCachedMcpRuntimeArgRewriter({
|
||||
copyDist: async (_source, target) => {
|
||||
copiedRoots.push(target);
|
||||
},
|
||||
});
|
||||
|
||||
const runtimeArg = await rewrite({ arg: "./dist/cli.js", pluginRoot, serverName: "omo", sourceRoot });
|
||||
|
||||
assert.equal(runtimeArg, join(pluginRoot, "dist", "cli.js"));
|
||||
assert.equal(copiedRoots.length, 0);
|
||||
});
|
||||
|
||||
test("#given structurally valid external MCP package without mcp suffix #when installing cached plugin #then runtime is copied into the plugin cache", async () => {
|
||||
const repoRoot = await makeTempDir();
|
||||
const codexHome = await makeTempDir();
|
||||
const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
|
||||
const runtimePackageRoot = join(repoRoot, "packages", "language-tools");
|
||||
|
||||
await writeJson(join(runtimePackageRoot, "package.json"), {
|
||||
name: "@example/language-tools",
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
bin: { "omo-language-tools": "./dist/cli.js" },
|
||||
});
|
||||
await writeJson(join(runtimePackageRoot, "dist", "cli.js"), { executable: true });
|
||||
await writeJson(join(sourceRoot, "package.json"), {
|
||||
name: "@example/omo",
|
||||
version: "0.1.0",
|
||||
});
|
||||
await writeJson(join(sourceRoot, ".mcp.json"), {
|
||||
mcpServers: {
|
||||
language_tools: {
|
||||
command: "node",
|
||||
args: ["../../language-tools/dist/cli.js", "mcp", "../local-config.json"],
|
||||
cwd: ".",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await installCachedPlugin({
|
||||
codexHome,
|
||||
marketplaceName: "sisyphuslabs",
|
||||
name: "omo",
|
||||
runCommand: async () => {},
|
||||
sourcePath: sourceRoot,
|
||||
version: "0.1.0",
|
||||
});
|
||||
|
||||
const cachedMcp = JSON.parse(await readFile(join(result.path, ".mcp.json"), "utf8"));
|
||||
const copiedCli = join(result.path, "mcp", "language_tools", "dist", "cli.js");
|
||||
assert.deepEqual(cachedMcp.mcpServers.language_tools.args, [copiedCli, "mcp", join(sourceRoot, "..", "local-config.json")]);
|
||||
assert.equal((await stat(copiedCli)).isFile(), true);
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
export async function makeTempDir() {
|
||||
return mkdtemp(join(tmpdir(), "omo-codex-install-"));
|
||||
}
|
||||
|
||||
export async function writeJson(path, value) {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
export async function writePlugin(root, name, version) {
|
||||
const pluginRoot = join(root, "plugins", name);
|
||||
await writePluginAt(pluginRoot, name, version);
|
||||
}
|
||||
|
||||
export async function writePluginAt(pluginRoot, name, version) {
|
||||
await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true });
|
||||
await mkdir(join(pluginRoot, "dist"), { recursive: true });
|
||||
await mkdir(join(pluginRoot, "hooks"), { recursive: true });
|
||||
await mkdir(join(pluginRoot, "skills", name), { recursive: true });
|
||||
await mkdir(join(pluginRoot, "components", "ultrawork", "agents"), { recursive: true });
|
||||
await writeJson(join(pluginRoot, ".codex-plugin", "plugin.json"), {
|
||||
name,
|
||||
version,
|
||||
description: `${name} test plugin`,
|
||||
mcpServers: "./.mcp.json",
|
||||
hooks: "./hooks/hooks.json",
|
||||
skills: "./skills/",
|
||||
});
|
||||
await writeJson(join(pluginRoot, ".mcp.json"), {
|
||||
mcpServers: {
|
||||
[name]: {
|
||||
command: "node",
|
||||
args: ["./dist/cli.js", "mcp"],
|
||||
cwd: ".",
|
||||
},
|
||||
},
|
||||
});
|
||||
await writeJson(join(pluginRoot, "hooks", "hooks.json"), { hooks: {} });
|
||||
await writeFile(join(pluginRoot, "skills", name, "SKILL.md"), "---\nname: test\n---\n");
|
||||
await writeFile(join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"), 'name = "explorer"\n');
|
||||
await writeFile(join(pluginRoot, "components", "ultrawork", "agents", "librarian.toml"), 'name = "librarian"\n');
|
||||
await writeFile(join(pluginRoot, "components", "ultrawork", "agents", "plan.toml"), 'name = "plan"\n');
|
||||
await writeJson(join(pluginRoot, "package.json"), {
|
||||
name: `@example/${name}`,
|
||||
version,
|
||||
bin: {
|
||||
[name]: "./dist/cli.js",
|
||||
},
|
||||
scripts: {
|
||||
build: "node -e \"require('fs').writeFileSync('dist/cli.js', 'console.log(1)')\"",
|
||||
},
|
||||
dependencies: {},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const TELEMETRY_SYNC_FILES = [
|
||||
"atomic-write.ts",
|
||||
"data-path.ts",
|
||||
"env-flags.ts",
|
||||
"posthog-activity-state.ts",
|
||||
];
|
||||
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const PACKAGE_ROOT = dirname(SCRIPT_DIR);
|
||||
const DEFAULT_SOURCE_DIR = join(PACKAGE_ROOT, "src", "telemetry");
|
||||
const DEFAULT_COMPONENT_DIR = join(PACKAGE_ROOT, "plugin", "components", "telemetry", "src");
|
||||
|
||||
export async function syncTelemetryComponent(options = {}) {
|
||||
const sourceDir = resolve(options.sourceDir ?? DEFAULT_SOURCE_DIR);
|
||||
const componentDir = resolve(options.componentDir ?? DEFAULT_COMPONENT_DIR);
|
||||
const files = options.files ?? TELEMETRY_SYNC_FILES;
|
||||
const check = options.check ?? false;
|
||||
const changed = [];
|
||||
|
||||
for (const fileName of files) {
|
||||
const sourcePath = join(sourceDir, fileName);
|
||||
const componentPath = join(componentDir, fileName);
|
||||
const sourceText = await readFile(sourcePath, "utf8");
|
||||
const componentText = await readOptionalText(componentPath);
|
||||
const nextText = toComponentSource(sourceText);
|
||||
if (componentText === nextText) continue;
|
||||
changed.push(fileName);
|
||||
if (!check) {
|
||||
await mkdir(dirname(componentPath), { recursive: true });
|
||||
await writeFile(componentPath, nextText);
|
||||
}
|
||||
}
|
||||
|
||||
if (check && changed.length > 0) {
|
||||
throw new Error(`telemetry component out of sync: ${changed.join(", ")}`);
|
||||
}
|
||||
|
||||
return { checked: check, changed };
|
||||
}
|
||||
|
||||
function toComponentSource(sourceText) {
|
||||
return sourceText
|
||||
.replaceAll(/\bprocess\.env\.([A-Z0-9_]+)/g, 'process.env["$1"]')
|
||||
.replaceAll(/from "(\.\/[^"]+)"/g, 'from "$1.js"');
|
||||
}
|
||||
|
||||
async function readOptionalText(path) {
|
||||
try {
|
||||
return await readFile(path, "utf8");
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === "ENOENT") return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function isNodeError(error) {
|
||||
return error instanceof Error && "code" in error;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const parsed = {
|
||||
check: false,
|
||||
sourceDir: DEFAULT_SOURCE_DIR,
|
||||
componentDir: DEFAULT_COMPONENT_DIR,
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--check") {
|
||||
parsed.check = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--source-dir") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined) throw new Error("--source-dir requires a value");
|
||||
parsed.sourceDir = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--component-dir") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined) throw new Error("--component-dir requires a value");
|
||||
parsed.componentDir = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await syncTelemetryComponent(parseArgs(process.argv.slice(2)));
|
||||
if (result.changed.length === 0) {
|
||||
console.log("telemetry component in sync");
|
||||
return;
|
||||
}
|
||||
console.log(`synced telemetry component: ${result.changed.join(", ")}`);
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
|
||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const SCRIPT_PATH = new URL("./sync-telemetry-component.mjs", import.meta.url);
|
||||
|
||||
async function makeTempDir() {
|
||||
return mkdtemp(join(tmpdir(), "omo-codex-telemetry-sync-"));
|
||||
}
|
||||
|
||||
async function runSync(args) {
|
||||
const { syncTelemetryComponent } = await import(SCRIPT_PATH);
|
||||
return syncTelemetryComponent(args);
|
||||
}
|
||||
|
||||
test("#given stale telemetry component files #when sync runs #then pure package telemetry source rewrites the component copy", async () => {
|
||||
// given
|
||||
const root = await makeTempDir();
|
||||
const sourceDir = join(root, "source");
|
||||
const componentDir = join(root, "component");
|
||||
await mkdir(sourceDir);
|
||||
await mkdir(componentDir);
|
||||
await writeFile(join(sourceDir, "atomic-write.ts"), "export const source = true\n", { flush: true });
|
||||
await writeFile(join(componentDir, "atomic-write.ts"), "export const stale = true\n", { flush: true });
|
||||
|
||||
try {
|
||||
// when
|
||||
const result = await runSync({
|
||||
sourceDir,
|
||||
componentDir,
|
||||
files: ["atomic-write.ts"],
|
||||
check: false,
|
||||
});
|
||||
|
||||
// then
|
||||
assert.deepEqual(result, {
|
||||
checked: false,
|
||||
changed: ["atomic-write.ts"],
|
||||
});
|
||||
assert.equal(await readFile(join(componentDir, "atomic-write.ts"), "utf8"), "export const source = true\n");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("#given a missing pure telemetry source file #when sync runs #then it fails with the missing source path", async () => {
|
||||
// given
|
||||
const root = await makeTempDir();
|
||||
const sourceDir = join(root, "source");
|
||||
const componentDir = join(root, "component");
|
||||
await mkdir(componentDir);
|
||||
await writeFile(join(componentDir, "atomic-write.ts"), "export const stale = true\n", { flush: true });
|
||||
|
||||
try {
|
||||
// when / then
|
||||
await assert.rejects(
|
||||
runSync({
|
||||
sourceDir,
|
||||
componentDir,
|
||||
files: ["atomic-write.ts"],
|
||||
check: false,
|
||||
}),
|
||||
(error) => error instanceof Error && error.message.includes(join(sourceDir, "atomic-write.ts")),
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user