fix(opencode): guard state parsing, lint the .tsx, correct an overstated doc claim
Three findings from a cross-branch review of the competing opencode
implementations, all of which are fair.
1. `readState` type-guards the parsed JSON now. A bare try/catch does not
cover it: `JSON.parse('null')` succeeds and returns null, so the catch never
fires and the next `state.project` throws a TypeError that takes the plugin
down. Verified the crash before fixing; a test pins the guard in the embedded
copies. Credit to the competing 'opencode-mine' branch, which had this right.
2. eslint now covers `src/opencode-ext/*.tsx`. The glob was `*.ts` only, so the
300-line TUI plugin — the largest file in the addon — was linted by nothing.
It was typechecked, which is why this went unnoticed. Confirmed the rules
actually fire on it rather than the file being silently skipped. The
'abhishek' branch was the only entry that got this right.
3. docs/opencode-extension.md overstated the security argument. "The token would
sit in a 0644 opencode.json" is not a point against a `type: local` stdio
bridge, which needs no token at all because it reads your own credentials.
That reason is a consequence of having picked the HTTP gateway, not a
justification for it. The docs now lead with the real reason — live
re-pointing without a restart — and state the trade honestly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -88,3 +88,16 @@ describe('embedded opencode plugins', () => {
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--skip-marker'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('embedded opencode plugins — state parsing', () => {
|
||||
it('type-guard the parsed state, not just try/catch', () => {
|
||||
// `JSON.parse('null')` succeeds and returns null, so a bare try/catch lets
|
||||
// it through and the next `state.project` throws a TypeError that takes the
|
||||
// plugin down. A hand-edited or truncated state file must degrade to "no
|
||||
// project", never to a broken opencode.
|
||||
for (const src of [OPENCODE_SERVER_PLUGIN_SOURCE, OPENCODE_TUI_PLUGIN_SOURCE]) {
|
||||
expect(src).toContain("typeof parsed === 'object' && parsed !== null");
|
||||
expect(src).not.toMatch(/return JSON\.parse\(await readFile\([^)]*\)\) as OpencodeState;/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,12 @@ function statePath(): string {
|
||||
|
||||
async function readState(): Promise<OpencodeState> {
|
||||
try {
|
||||
return JSON.parse(await readFile(statePath(), 'utf-8')) as OpencodeState;
|
||||
const parsed: unknown = JSON.parse(await readFile(statePath(), 'utf-8'));
|
||||
// Type-guard, not just try/catch: `JSON.parse('null')` succeeds and returns
|
||||
// null, so the catch never fires and the next `state.project` throws a
|
||||
// TypeError that takes the plugin down. A truncated or hand-edited state
|
||||
// file must degrade to "no project", never to a broken opencode.
|
||||
return typeof parsed === 'object' && parsed !== null ? (parsed as OpencodeState) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -40,7 +40,12 @@ function statePath(): string {
|
||||
|
||||
async function readState(): Promise<OpencodeState> {
|
||||
try {
|
||||
return JSON.parse(await readFile(statePath(), 'utf-8')) as OpencodeState;
|
||||
const parsed: unknown = JSON.parse(await readFile(statePath(), 'utf-8'));
|
||||
// Type-guard, not just try/catch: `JSON.parse('null')` succeeds and returns
|
||||
// null, so the catch never fires and the next `state.project` throws a
|
||||
// TypeError that takes the plugin down. A truncated or hand-edited state
|
||||
// file must degrade to "no project", never to a broken opencode.
|
||||
return typeof parsed === 'object' && parsed !== null ? (parsed as OpencodeState) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user