diff --git a/gitnexus/src/cli/editor-targets.ts b/gitnexus/src/cli/editor-targets.ts index 212a4c400..600fde2d9 100644 --- a/gitnexus/src/cli/editor-targets.ts +++ b/gitnexus/src/cli/editor-targets.ts @@ -56,7 +56,7 @@ export interface McpJsoncTarget { export interface CodexMcpTarget { id: 'codex'; label: string; - /** Absolute path to ~/.codex/config.toml. */ + /** Absolute path to CODEX_HOME/config.toml (default ~/.codex/config.toml). */ configFile: string; /** The TOML table header (without brackets) setup writes / uninstall strips. */ tomlSection: string; @@ -100,6 +100,9 @@ export interface EditorTargets { * `process.env.HOME` before invoking sees the right locations. */ export function getEditorTargets(home: string = os.homedir()): EditorTargets { + const codexHome = process.env.CODEX_HOME + ? path.resolve(process.env.CODEX_HOME) + : path.join(home, '.codex'); const mcpJsonc: McpJsoncTarget[] = [ { id: 'cursor', @@ -166,7 +169,7 @@ export function getEditorTargets(home: string = os.homedir()): EditorTargets { const codex: CodexMcpTarget = { id: 'codex', label: 'Codex', - configFile: path.join(home, '.codex', 'config.toml'), + configFile: path.join(codexHome, 'config.toml'), tomlSection: 'mcp_servers.gitnexus', }; diff --git a/gitnexus/src/cli/setup.ts b/gitnexus/src/cli/setup.ts index 88a9eed93..cdb778af5 100644 --- a/gitnexus/src/cli/setup.ts +++ b/gitnexus/src/cli/setup.ts @@ -1091,16 +1091,13 @@ async function codexHasHttpMcpEntry(configPath: string): Promise { } async function setupCodex(result: SetupResult): Promise { - const configuredHome = process.env.CODEX_HOME; - const codexDir = configuredHome - ? path.resolve(configuredHome) - : path.join(os.homedir(), '.codex'); - if (!configuredHome && !(await dirExists(codexDir))) { + const configPath = getEditorTargets().codex.configFile; + const codexDir = path.dirname(configPath); + if (!process.env.CODEX_HOME && !(await dirExists(codexDir))) { result.skipped.push('Codex (not installed)'); return; } - const configPath = path.join(codexDir, 'config.toml'); try { if (await codexHasHttpMcpEntry(configPath)) { result.configured.push('Codex (existing HTTP MCP entry kept)'); diff --git a/gitnexus/test/unit/uninstall.test.ts b/gitnexus/test/unit/uninstall.test.ts index ad20eb5ca..644ca785f 100644 --- a/gitnexus/test/unit/uninstall.test.ts +++ b/gitnexus/test/unit/uninstall.test.ts @@ -23,6 +23,7 @@ describe('uninstallCommand', () => { let tempHome: string; let originalHome: string | undefined; let originalUserProfile: string | undefined; + let originalCodexHome: string | undefined; let originalSkillsRoot: string | undefined; let originalExitCode: typeof process.exitCode; let skillsRoot: string; @@ -35,6 +36,8 @@ describe('uninstallCommand', () => { originalHome = process.env.HOME; originalUserProfile = process.env.USERPROFILE; + originalCodexHome = process.env.CODEX_HOME; + delete process.env.CODEX_HOME; originalSkillsRoot = process.env.GITNEXUS_TEST_SKILLS_ROOT; originalExitCode = process.exitCode; @@ -57,6 +60,8 @@ describe('uninstallCommand', () => { vi.restoreAllMocks(); process.env.HOME = originalHome; process.env.USERPROFILE = originalUserProfile; + if (originalCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = originalCodexHome; if (originalSkillsRoot === undefined) delete process.env.GITNEXUS_TEST_SKILLS_ROOT; else process.env.GITNEXUS_TEST_SKILLS_ROOT = originalSkillsRoot; // The command sets process.exitCode=1 on partial failure; restore it so a @@ -203,6 +208,28 @@ describe('uninstallCommand', () => { expect(result).toContain('command = "other"'); }); + it('dry-runs and removes only the active CODEX_HOME MCP entry', async () => { + const activeHome = path.join(tempHome, 'active-codex'); + const activeConfig = path.join(activeHome, 'config.toml'); + const defaultConfig = path.join(tempHome, '.codex', 'config.toml'); + const activeRaw = '[mcp_servers.gitnexus]\ncommand = "active"\n'; + const defaultRaw = '[mcp_servers.gitnexus]\ncommand = "default"\n'; + await fs.mkdir(activeHome); + await fs.mkdir(path.dirname(defaultConfig)); + await fs.writeFile(activeConfig, activeRaw); + await fs.writeFile(defaultConfig, defaultRaw); + process.env.CODEX_HOME = activeHome; + + const uninstallCommand = await importUninstall(); + await uninstallCommand(); + expect(vi.mocked(console.log).mock.calls.flat().join('\n')).toContain(activeConfig); + expect(await fs.readFile(activeConfig, 'utf-8')).toBe(activeRaw); + + await uninstallCommand({ force: true }); + expect(await fs.readFile(activeConfig, 'utf-8')).not.toContain('[mcp_servers.gitnexus]'); + expect(await fs.readFile(defaultConfig, 'utf-8')).toBe(defaultRaw); + }); + it('leaves a corrupt JSON config untouched', async () => { const claudeJson = path.join(tempHome, '.claude.json'); const corrupt = '{ not valid json !!!';