From f504e5727539c8a144dc8eb58b7d3141776d43bc Mon Sep 17 00:00:00 2001 From: Tom Hale Date: Wed, 22 Apr 2026 22:49:41 +0700 Subject: [PATCH] fix(setup): remove dead JSON.parse fallback, detect space-indent width, fix JSDoc - Remove the semantically unreachable JSON.parse fallback branch in mergeJsoncFile (jsonc-parser's parseTree is a strict superset of JSON.parse, so the fallback can never fire for content JSON.parse would accept) - Replace binary tab/space detection with detectIndentation() that measures actual indent width from the first indented line - Fix JSDoc: 'valid JSON that is not valid JSONC' is impossible by definition - Add tests for tab indentation and 4-space indentation preservation --- gitnexus/src/cli/setup.ts | 33 +++++++++++--------------- gitnexus/test/unit/setup-jsonc.test.ts | 26 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/gitnexus/src/cli/setup.ts b/gitnexus/src/cli/setup.ts index 2d3b2ad76..e6f20d3f9 100644 --- a/gitnexus/src/cli/setup.ts +++ b/gitnexus/src/cli/setup.ts @@ -128,10 +128,20 @@ async function writeJsonFile(filePath: string, data: any): Promise { await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); } +/** + * Detect indentation style from file content. + * Returns formatting options matching the file's existing style. + */ +function detectIndentation(raw: string): { tabSize: number; insertSpaces: boolean } { + const firstIndented = raw.match(/^( +|\t)/m); + if (!firstIndented) return { tabSize: 2, insertSpaces: true }; + if (firstIndented[1] === '\t') return { tabSize: 1, insertSpaces: false }; + return { tabSize: firstIndented[1].length, insertSpaces: true }; +} + /** * Merge a key/value pair into a JSONC config file, preserving comments and formatting. - * Falls back to JSON.parse + writeJsonFile for valid JSON that is not valid JSONC. - * If the file is genuinely corrupt (not valid JSON or JSONC), leaves it untouched. + * If the file is genuinely corrupt (not valid JSONC), leaves it untouched. */ async function mergeJsoncFile( filePath: string, @@ -164,29 +174,14 @@ async function mergeJsoncFile( const tree = parseTree(raw, parseErrors); if (tree && tree.type === 'object' && parseErrors.length === 0) { - const detectedTabs = /^\t/m.test(raw); - const formattingOptions = detectedTabs - ? { tabSize: 1, insertSpaces: false } - : { tabSize: 2, insertSpaces: true }; + const formattingOptions = detectIndentation(raw); const edits = modify(raw, keyPath, value, { formattingOptions }); const result = applyEdits(raw, edits); await fs.writeFile(filePath, result, 'utf-8'); return true; } - try { - const existing = JSON.parse(raw); - let parent = existing; - for (let i = 0; i < keyPath.length - 1; i++) { - if (!parent[keyPath[i]]) parent[keyPath[i]] = {}; - parent = parent[keyPath[i]]; - } - parent[keyPath[keyPath.length - 1]] = value; - await writeJsonFile(filePath, existing); - return true; - } catch { - return false; - } + return false; } /** diff --git a/gitnexus/test/unit/setup-jsonc.test.ts b/gitnexus/test/unit/setup-jsonc.test.ts index 5097eaba1..187f49d95 100644 --- a/gitnexus/test/unit/setup-jsonc.test.ts +++ b/gitnexus/test/unit/setup-jsonc.test.ts @@ -236,6 +236,32 @@ describe('setupOpenCode — JSONC preservation', () => { }); }); + it('preserves tab indentation in existing file', async () => { + const tabbed = `{\n\t"model": "test"\n}`; + await fs.writeFile(opencodeJsonPath(), tabbed, 'utf-8'); + + const { setupCommand } = await import('../../src/cli/setup.js'); + await setupCommand(); + + const raw = await fs.readFile(opencodeJsonPath(), 'utf-8'); + expect(raw).toContain('\t"model"'); + expect(raw).toContain('\t"gitnexus"'); + }); + + it('preserves 4-space indentation in existing file', async () => { + const fourSpace = `{ + "model": "test" +}`; + await fs.writeFile(opencodeJsonPath(), fourSpace, 'utf-8'); + + const { setupCommand } = await import('../../src/cli/setup.js'); + await setupCommand(); + + const raw = await fs.readFile(opencodeJsonPath(), 'utf-8'); + const mcpLine = raw.split('\n').find((l) => l.includes('"gitnexus"')); + expect(mcpLine).toMatch(/^ /); + }); + it('skips when ~/.config/opencode directory does not exist', async () => { await fs.rm(opencodeDir(), { recursive: true, force: true });