Skip to content
Open
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
30 changes: 22 additions & 8 deletions scripts/claude-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,15 @@ function buildSetupReport(
// setup
// ---------------------------------------------------------------------------

function isCodexAppServerUnavailable(error) {
const message = error instanceof Error ? error.message : String(error);
return (
/Failed to start [^:]+:/i.test(message) ||
/app-server exited before responding/i.test(message) ||
/app-server timed out waiting/i.test(message)
);
}

async function handleSetup(argv) {
const { options } = parseCommandInput(argv, {
valueOptions: ["cwd"],
Expand Down Expand Up @@ -657,6 +666,7 @@ async function handleSetup(argv) {
let pluginStateDetail = `plugin data roots verified: ${pluginDataRoots.join(", ")}`;
let pluginStateNextStep = null;
let pluginConfig = null;
let pluginRootProbeError = null;
const recordPluginStateFailure = (error) => {
pluginStateReady = false;
pluginStateDetail = `unable to access plugin state: ${
Expand All @@ -668,14 +678,11 @@ async function handleSetup(argv) {
try {
writableRootChanged = await ensureCodexWritableRoots(cwd, pluginDataRoots);
} catch (error) {
pluginStateReady = false;
pluginStateDetail = `unable to configure plugin data roots: ${
error instanceof Error ? error.message : String(error)
}`;
pluginStateNextStep =
`Allow ${pluginDataRoots.join(", ")} under ` +
"`sandbox_workspace_write.writable_roots` in `~/.codex/config.toml`, " +
"restart Codex, then rerun `$cc:setup`.";
if (isCodexAppServerUnavailable(error)) {
pluginRootProbeError = error;
} else {
throw error;
}
}
if (writableRootChanged) {
pluginStateReady = false;
Expand All @@ -693,6 +700,13 @@ async function handleSetup(argv) {
try {
ensureStateDir(workspaceRoot);
pluginConfig = getConfig(workspaceRoot);
if (pluginRootProbeError) {
pluginStateDetail =
"plugin data roots app-server check unavailable; direct plugin state access verified: " +
(pluginRootProbeError instanceof Error
? pluginRootProbeError.message
: String(pluginRootProbeError));
}
} catch (error) {
recordPluginStateFailure(error);
}
Expand Down
35 changes: 33 additions & 2 deletions tests/integration/claude-companion.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,9 @@ describe("claude-companion integration", () => {

it("setup reports plugin-state repair guidance when the app-server is unavailable", () => {
const testEnv = createTestEnvironment();
const codexHome = path.join(testEnv.homeDir, ".codex");
fs.mkdirSync(codexHome, { recursive: true });
fs.writeFileSync(path.join(codexHome, "plugins"), "", "utf8");

try {
const report = runCompanionJson(
Expand All @@ -739,16 +742,44 @@ describe("claude-companion integration", () => {
assert.equal(report.ready, false);
assert.equal(report.reviewGateEnabled, null);
assert.equal(report.pluginState.ready, false);
assert.match(report.pluginState.detail, /unable to configure/i);
assert.match(report.pluginState.detail, /unable to access/i);
assert.match(
report.nextSteps.join("\n"),
/sandbox_workspace_write\.writable_roots/
/Restart Codex.*rerun `\$cc:setup`/
);
} finally {
cleanupTestEnvironment(testEnv);
}
});

it("setup remains ready when the app-server reports spawn EPERM but plugin state is writable", () => {
const testEnv = createTestEnvironment();
const failingServer = path.join(testEnv.rootDir, "spawn-eperm-app-server.mjs");
fs.writeFileSync(
failingServer,
'process.stderr.write("spawn EPERM\\n"); process.exitCode = 1;\\n',
"utf8"
);

try {
const report = runCompanionJson(
["setup", "--cwd", testEnv.workspaceDir, "--json"],
{
env: {
...testEnv.env,
CC_PLUGIN_CODEX_EXECUTABLE: process.execPath,
CC_PLUGIN_CODEX_APP_SERVER_ARGS_JSON: JSON.stringify([failingServer]),
},
}
);

assert.equal(report.pluginState.ready, true);
assert.match(report.pluginState.detail, /direct plugin state access verified/i);
} finally {
cleanupTestEnvironment(testEnv);
}
});

it("setup trusts current native plugin hooks through Codex hooks/list", () => {
const testEnv = createTestEnvironment();
const sourcePath = path.join(PROJECT_ROOT, "hooks", "hooks.json");
Expand Down