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
This commit is contained in:
Tom Hale 2026-04-22 22:49:41 +07:00
parent 3c9d24ee86
commit f504e57275
2 changed files with 40 additions and 19 deletions

View file

@ -128,10 +128,20 @@ async function writeJsonFile(filePath: string, data: any): Promise<void> {
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;
}
/**

View file

@ -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 });