From 19ddb1c6c5a11d2f28a4f5d01b67a2e44071414f Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Fri, 16 Oct 2026 19:50:50 +0200 Subject: [PATCH] fix: refuse root dotted Codex init entries --- docs/client-setup.md | 4 +-- src/cli-init.ts | 2 +- tests/cli-init-saved-file.test.ts | 59 +++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/cli-init-saved-file.test.ts diff --git a/docs/client-setup.md b/docs/client-setup.md index c052e65..851e62f 100644 --- a/docs/client-setup.md +++ b/docs/client-setup.md @@ -117,7 +117,7 @@ Claude Code writes `.mcp.json`; Codex writes `.codex/config.toml`. Both configs `--yes` writes the config without prompts. Run `init` without flags to choose a client interactively and optionally add project instructions. Cursor and OpenCode still use HTTP and need a separately running server. -Existing settings are kept. If a different Codebase Context entry is already present, setup leaves the file unchanged and reports the conflict. Restart the client after setup and approve the server if asked. The first use builds the code index. Rerun setup if the Codebase Context installation moves, because the config uses its installed location. +Existing settings are kept. If a different Codebase Context entry is already present, setup leaves the file unchanged and reports the conflict. If the Codebase Context installation moves, remove only its existing entry from the client config. Keep the other settings, then run setup again. Restart the client after setup and approve the server if asked. The first use builds the code index. ## Optional fixed-folder setup and CLI preparation @@ -129,7 +129,7 @@ To prepare the index separately, run from the project folder: npx -y codebase-context@2.4.0 reindex ``` -The interactive wizard limitations described above are still present in the 2.4.0 source. A successful wizard exit alone does not establish working client registration. +A successful setup run writes the config but does not confirm that the client connected. ## Gemini CLI diff --git a/src/cli-init.ts b/src/cli-init.ts index bf126cc..caafad7 100644 --- a/src/cli-init.ts +++ b/src/cli-init.ts @@ -339,7 +339,7 @@ export async function _buildMergedCodexContent( } const dottedEntry = new RegExp( - `^\\s*${mcpServersKey}\\s*\\.\\s*${codebaseContextKey}\\s*\\.`, + `^\\s*${mcpServersKey}\\s*\\.\\s*${codebaseContextKey}\\s*(?:\\.|=)`, 'm' ); if (dottedEntry.test(normalizedExisting)) { diff --git a/tests/cli-init-saved-file.test.ts b/tests/cli-init-saved-file.test.ts new file mode 100644 index 0000000..a90beaf --- /dev/null +++ b/tests/cli-init-saved-file.test.ts @@ -0,0 +1,59 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { describe, expect, it, vi } from 'vitest'; +import { handleInitCli } from '../src/cli-init.js'; + +describe('Codex init with an existing saved config', () => { + it.each([ + { + name: 'bare dotted keys', + toml: 'mcp_servers.codebase-context = { command = "old", args = [] }\n' + }, + { + name: 'a double-quoted root key', + toml: '"mcp_servers".codebase-context = { command = "old", args = [] }\n' + }, + { + name: 'a double-quoted server key', + toml: 'mcp_servers."codebase-context" = { command = "old", args = [] }\n' + }, + { + name: 'double-quoted root and server keys', + toml: '"mcp_servers"."codebase-context" = { command = "old", args = [] }\n' + }, + { + name: 'single-quoted root and server keys', + toml: "'mcp_servers'.'codebase-context' = { command = \"old\", args = [] }\n" + } + ])('refuses $name without changing the saved file', async ({ toml }) => { + const projectRoot = await mkdtemp(path.join(os.tmpdir(), 'cbc-codex-init-')); + const configPath = path.join(projectRoot, '.codex', 'config.toml'); + const initialBytes = Buffer.from(toml, 'utf8'); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const previousExitCode = process.exitCode; + + try { + await mkdir(path.dirname(configPath), { recursive: true }); + await writeFile(configPath, initialBytes); + process.exitCode = undefined; + + await handleInitCli(['--client', 'codex', '--yes', '--root', projectRoot]); + + const persistedBytes = await readFile(configPath); + expect({ exitCode: process.exitCode, persistedBytes }).toEqual({ + exitCode: 1, + persistedBytes: initialBytes + }); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('already contains a different codebase-context configuration') + ); + } finally { + process.exitCode = previousExitCode; + errorSpy.mockRestore(); + logSpy.mockRestore(); + await rm(projectRoot, { recursive: true, force: true }); + } + }); +});