fix(config): isolate plugin list reads from a broken JSONC layer

Plugin listing still called readConfigFile per layer, so one unparseable
project file made GET /api/config/plugins and VS Code listPluginEntries
fail. Reuse readConfigLayer isolation and pin comment-only empty parse
in the VS Code suite.

Co-authored-by: serkraser <serkraser@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-15 06:30:36 +00:00
co-authored by serkraser
parent 8b086343fd
commit 6751c7dc7a
7 changed files with 60 additions and 8 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ The webview CSP permits `blob:` only for `worker-src` so shared UI parsers can r
- `bridge-config-runtime.ts`
- Config and skills message handlers (`api:config/*`).
- Includes OpenCode resolution diagnostics parity handler used by shared UI (`/api/config/opencode-resolution`).
- OpenCode JSONC reads in `opencodeConfig.ts` fail closed on a partial or non-object `jsonc-parser` tree (`INVALID_JSONC`) so mutations cannot rewrite a `$schema`-only stub over an existing config. Comment-only files read as empty. A broken layer is omitted from the merge and recorded on `layerErrors`; valid sibling layers still load, and writes still refuse to overwrite the broken file.
- OpenCode JSONC reads in `opencodeConfig.ts` fail closed on a partial or non-object `jsonc-parser` tree (`INVALID_JSONC`) so mutations cannot rewrite a `$schema`-only stub over an existing config. Comment-only files read as empty. A broken layer is omitted from the merge and recorded on `layerErrors`; valid sibling layers still load, including plugin list/read via `getPluginConfigSources`. Writes still refuse to overwrite the broken file.
- `bridge-settings-runtime.ts`
- Settings read/write and OpenCode skills discovery via API for bridge consumers.
@@ -4,7 +4,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { updateMcpConfig } from './opencodeConfig';
import { listPluginEntries, updateMcpConfig } from './opencodeConfig';
const PARTIAL_PARSE_CONFIG = [
'{',
@@ -94,6 +94,29 @@ describe('opencodeConfig JSONC parse safety (issue #2923)', () => {
assert.equal(fs.readFileSync(`${configPath}.openchamber.backup`, 'utf8'), VALID_CONFIG);
});
test('returns an empty object for a comment-only config file', () => {
const configPath = path.join(tempDir, 'comments.jsonc');
fs.writeFileSync(configPath, '// placeholder\n/* still empty */\n', 'utf8');
process.env.OPENCODE_CONFIG = configPath;
assert.deepEqual(listPluginEntries(), []);
});
test('lists custom-layer plugins when a project layer is unparseable', () => {
const customPath = path.join(tempDir, 'custom.jsonc');
const projectDir = path.join(tempDir, 'project');
const projectFile = path.join(projectDir, '.opencode', 'opencode.jsonc');
fs.writeFileSync(customPath, VALID_CONFIG, 'utf8');
fs.mkdirSync(path.dirname(projectFile), { recursive: true });
fs.writeFileSync(projectFile, PARTIAL_PARSE_CONFIG, 'utf8');
process.env.OPENCODE_CONFIG = customPath;
const specs = listPluginEntries(projectDir).map((entry) => entry.spec);
assert.deepEqual(specs, ['opencode-see-image']);
assert.equal(fs.readFileSync(projectFile, 'utf8'), PARTIAL_PARSE_CONFIG);
assert.equal(fs.existsSync(`${projectFile}.openchamber.backup`), false);
});
test('keeps a valid custom layer writable when a project layer is unparseable', () => {
const customPath = path.join(tempDir, 'custom.jsonc');
const projectDir = path.join(tempDir, 'project');
+3 -3
View File
@@ -926,10 +926,10 @@ const getPluginConfigSources = (workingDirectory?: string | null): Array<{ scope
const projectPath = getProjectConfigPath(workingDirectory || undefined);
return [
customPath
? { scope: 'user', path: customPath, config: readConfigFile(customPath) }
: { scope: 'user', path: userPath, config: readConfigFile(userPath) },
? { scope: 'user', path: customPath, config: readConfigLayer(customPath).config }
: { scope: 'user', path: userPath, config: readConfigLayer(userPath).config },
...(projectPath
? [{ scope: 'project' as const, path: projectPath, config: readConfigFile(projectPath) }]
? [{ scope: 'project' as const, path: projectPath, config: readConfigLayer(projectPath).config }]
: []),
];
};