Skip to content

fix: edit an existing opencode.jsonc instead of creating a sibling opencode.json - #59

Merged
boristane merged 2 commits into
mainfrom
claude/opencode-jsonc
Aug 21, 2026
Merged

fix: edit an existing opencode.jsonc instead of creating a sibling opencode.json#59
boristane merged 2 commits into
mainfrom
claude/opencode-jsonc

Conversation

@claude

@claude claude Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Requested by boris · Slack thread

Before: a user who keeps their opencode config in opencode.jsonc ran the installer (or polylane setup) and found a brand-new sibling opencode.json next to it — two config files, and their own one untouched.

After: setup looks for opencode.jsonc / opencode.json and edits the one that exists in place — the polylane MCP entry is inserted with comments, trailing commas, and the formatting of existing members left intact. Only when no opencode config exists at all does it create opencode.json as before. Nothing is ever printed for the user to copy: any valid JSONC is edited directly; only a genuinely malformed file (one opencode itself couldn't parse) is refused, as a clear skipped outcome naming the file.

This makes the opencode MCP-config write variant-aware and JSONC-aware instead of unconditionally creating opencode.json.

How. Verified against opencode's docs and source before encoding anything:

  • opencode parses all of its config as JSONC (comments + trailing commas) and loads both opencode.json and opencode.jsonc when present, merged — global from ~/.config/opencode/ and project files found walking up from the cwd to the git root (docs, config.ts, paths.ts).
  • When opencode edits its own config it targets the first existing of opencode.jsoncopencode.json (globalConfigFile() in config.ts). opencodeConfigFile() mirrors that order at both the user and project level.
  • The edit uses jsonc-parser (modify + applyEdits) — the same zero-dependency library opencode uses to patch its own config — so any valid JSONC edits cleanly and comment-preserving, with the new entry inserted first so existing members keep their exact bytes. The bundle aliases it to its ESM build (the UMD default passes require into its factory, which esbuild can't follow).
  • On the "don't write into my git workspace" preference: the default polylane setup (what the curl installer runs) only writes user-level files under the home directory — the global ~/.config/opencode/ location opencode documents. Repo-local files are written only behind the explicit --project flag, which stays the consent gate; non-interactive runs keep the user-level default and never prompt.

Checks: npm run typecheck, npm run lint, npm run test (318 passing, including heavy-comment + trailing-comma edits, nested mcp blocks, single-quote files treated as malformed like opencode treats them, unchanged/skip paths, and both agent levels preferring an existing opencode.jsonc) and npm run build with an end-to-end setup --agent opencode run against the produced bundle.

Companion installer-side PR: coreplanelabs/polylanedotcom#137.

claude added 2 commits August 20, 2026 19:04
…encode.json

opencode reads and merges both opencode.json and opencode.jsonc (JSONC with
comments and trailing commas, at the global ~/.config/opencode location and
in the project root). setup wrote opencode.json unconditionally, so a user
with an opencode.jsonc ended up with two config files.

setup now targets the existing variant, preferring opencode.jsonc — the same
order opencode itself uses when it edits its config — and edits it JSONC-aware:
the entry is inserted textually so comments and formatting survive, and when
the file can't be edited safely the outcome prints the exact snippet to add
by hand instead of clobbering or duplicating. Nothing changes when neither
variant exists: opencode.json is created as before, and repo-local writes
remain behind the explicit --project flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iuTu1C4JF3WLBodoKktHy
Per review: never print a snippet for the user to copy. The upsert now edits
any valid JSONC in place using jsonc-parser — the library opencode itself
edits its config with — so comments and the formatting of existing members
survive, and the entry is inserted whether or not an mcp block exists. Only
a genuinely malformed file (one opencode couldn't parse either) is refused,
as a clear skipped outcome naming the file. The WriteOutcome.snippet plumbing
is gone. The bundle aliases jsonc-parser to its ESM build; the UMD default
passes require into its factory, which esbuild can't follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iuTu1C4JF3WLBodoKktHy

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM: the opencode setup now edits an existing opencode.jsonc/opencode.json in place via jsonc-parser, never creates a duplicate, and never falls back to a copy-paste snippet — verified against the live code, not just the diff.

Procedure followed (per REVIEW.md): full diff read hunk-by-hunk, callers of every changed export grepped (upsertJsoncEntry, opencodeConfigFile, WriteOutcome.needsManualStep), every PR-body claim checked against code and, where practical, against real execution; npm ci, npm run codegen, npm run typecheck, npm run lint, npm run test run locally on Node 22.x (matches one CI leg; CI already covers 20.x/24.x and all three are green), plus npm run build since the diff touches build.ts/packaging.

Verified claims (ran the built bundle end-to-end against real fixtures, not just unit tests):

  • Existing opencode.jsonc with comments + trailing commas → MCP entry inserted, all comments and trailing commas preserved, new entry written first, sibling opencode.json never created.
  • Only opencode.json (plain JSON) exists → edited in place, no .jsonc created.
  • Neither exists → creates opencode.json as before (no behavior change for the common case).
  • Both exist → .jsonc edited, .json left untouched (matches opencode's own globalConfigFile() precedence per the PR's cited source).
  • Genuinely malformed JSONC ({ "theme": ) → file left byte-for-byte untouched, outcome is skipped with a link to the manual-registration doc — confirmed nothing is ever printed as a copy-paste snippet, per the requirement in the linked Slack thread.
  • jsonc-parser's main field really does point at the UMD build (lib/umd/main.js, which passes require into its factory) while module points at the ESM build (lib/esm/main.js) that the new alias targets — npm run build bundles cleanly and the resulting dist/polylane.mjs runs the scenarios above correctly.

Code review notes:

  • src/agents/registry.ts: upsertJsoncEntry correctly special-cases "file doesn't exist" (create, unchanged from before), "malformed" (skip, needsManualStep: true, no manual-step regression vs. the plain-JSON upsertJsonEntry), "key already present" (unchanged, byte-for-byte, tested), and "needs insertion" (modify+applyEdits, insertion index forced to 0 so existing members keep their exact bytes). The reachedLeaf walk only rejects non-object intermediate nodes and otherwise defers entirely to jsonc-parser's own path-creation — good, that's the right layer to own JSONC structural edits rather than hand-rolling it.
  • opencodeConfigFile mirrors opencode's own jsonc-then-json resolution order exactly (confirmed above).
  • Test coverage is thorough: heavy-comment files, trailing commas in nested blocks, sibling-server preservation with correct ordering, single-quoted-string malformed handling (matches opencode's own JSONC strictness), dry-run no-ops, and both user/project agent-table wiring paths. test/setup.test.ts passes 318/318 locally (0 fail/skipped) — note the PR body says "323 passing"; the discrepancy is almost certainly just other unrelated tests added to main since this branch was cut, not a regression here (confirmed nothing in this diff's own new tests fails).
  • Scope is tight: 7 files, only touches the opencode path; no other agent's behavior changes. Backward compatible — jsonc-parser's JSONC parsing is a strict superset of JSON, so existing plain-opencode.json users get identical behavior to before, just via the new code path.

Nits (non-blocking):

  • isJsonObject/skip('existing file is not a JSON object') guards the root value, but I didn't see a dedicated test for a root that parses to a non-object (e.g. a top-level JSONC array or bare string) — the plain-JSON upsertJsonEntry has the equivalent case tested; wouldn't block on it since the guard clause itself is straightforward and mirrored from the JSON version.
  • The PR body's "323 passing" vs. the 318 I see locally is worth a quick sanity check by the author next time a description is written, just so the number stays trustworthy for future reviewers skimming the description.

CI: all 3 check runs green (Test & typecheck on 20.x/22.x/24.x), no flakes to attribute.


Generated by Claude Code

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: Claude reviewed this PR and posted an LGTM verdict (see its review). A repo admin enabled this via the auto-approve workflow.


Generated by Claude Code

@boristane
boristane merged commit fe7ec1b into main Aug 21, 2026
4 checks passed
@boristane
boristane deleted the claude/opencode-jsonc branch August 21, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants