fix(installer): opencode .jsonc + AGENTS.md (0.7.8) (#163)
* release: 0.7.7 (multi-agent installer — Cursor, Codex, opencode) * fix(installer): opencode .jsonc + AGENTS.md (0.7.8) v0.7.7 wrote ~/.config/opencode/opencode.json, but opencode reads opencode.jsonc by default — so the codegraph MCP entry never appeared in any opencode session. Also installs AGENTS.md so opencode's model reaches for codegraph_* tools instead of native Grep. - Prefer existing .jsonc, fall back to .json, default new installs to .jsonc. - Surgical edits via jsonc-parser preserve user comments and formatting across install / re-install / uninstall round-trips. - Install AGENTS.md (global ~/.config/opencode/AGENTS.md, local ./AGENTS.md) with the shared INSTRUCTIONS_TEMPLATE — same marker-delimited approach Codex uses. - +9 opencode-specific tests covering filename precedence, comment preservation, AGENTS.md install + sibling-content preservation, uninstall reverses both files. 575/575 tests pass. Hand-verified end-to-end: opencode session calls codegraph_node + codegraph_callers for a structural query, zero Grep calls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: overhaul CLAUDE.md and add scripts/release.sh + Cursor rules file Replaces the old Claude-only CLAUDE.md with a comprehensive guide covering the full project architecture, multi-agent installer, test conventions, NodeKind/EdgeKind reference, and release workflow. Key additions: - Documents the layered pipeline, all module paths, and the multi-target installer (targets/, registry.ts, AgentTarget interface). - Adds the Cursor `--path` quirk and the "update all three surfaces" rule when changing MCP tool guidance. - Documents `npm run eval`, `test:eval`, and the full set of build/test commands including single-file patterns. - `scripts/release.sh` — idempotent bash script that tags the current commit, pushes the tag, and creates a GitHub Release whose notes are extracted from the matching `## [X.Y.Z]` block in CHANGELOG.md. Safe to re-run after partial failure. - `.cursor/rules/codegraph.mdc` — Cursor-specific agent instructions (tool decision table, rules of thumb, index-lag warning) written by the installer and kept in sync with server-instructions.ts and instructions-template.ts. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7d87126ee8
commit
58c1414ce5
@@ -98,7 +98,8 @@ describe('Installer targets — contract', () => {
|
||||
// and any target with no JSON config — they get covered
|
||||
// by their own dedicated tests below.
|
||||
const paths = target.describePaths(location);
|
||||
const jsonPath = paths.find((p) => p.endsWith('.json'));
|
||||
// Match .json or .jsonc — opencode prefers .jsonc.
|
||||
const jsonPath = paths.find((p) => /\.jsonc?$/.test(p));
|
||||
if (!jsonPath) return;
|
||||
|
||||
// Seed pre-existing config.
|
||||
@@ -184,6 +185,152 @@ describe('Installer targets — partial-state idempotency', () => {
|
||||
for (const f of third.files) expect(f.action).toBe('unchanged');
|
||||
});
|
||||
|
||||
it('opencode: prefers .jsonc when both .json and .jsonc exist', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const dir = path.join(tmpHome, '.config', 'opencode');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'opencode.json'), '{\n "$schema": "https://opencode.ai/config.json"\n}\n');
|
||||
fs.writeFileSync(path.join(dir, 'opencode.jsonc'), '{\n "$schema": "https://opencode.ai/config.json"\n}\n');
|
||||
|
||||
const result = opencode.install('global', { autoAllow: true });
|
||||
const written = result.files.find((f) => /\.jsonc$/.test(f.path))!;
|
||||
expect(written).toBeDefined();
|
||||
expect(written.action).not.toBe('not-found');
|
||||
// The .json file is left alone.
|
||||
const jsonText = fs.readFileSync(path.join(dir, 'opencode.json'), 'utf-8');
|
||||
expect(jsonText).not.toContain('codegraph');
|
||||
});
|
||||
|
||||
it('opencode: uses .json when only .json exists (no .jsonc)', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const dir = path.join(tmpHome, '.config', 'opencode');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'opencode.json'), '{\n "$schema": "https://opencode.ai/config.json"\n}\n');
|
||||
|
||||
const result = opencode.install('global', { autoAllow: true });
|
||||
expect(result.files[0].path).toMatch(/opencode\.json$/);
|
||||
expect(fs.existsSync(path.join(dir, 'opencode.jsonc'))).toBe(false);
|
||||
});
|
||||
|
||||
it('opencode: defaults to .jsonc for fresh installs (no existing file)', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const result = opencode.install('global', { autoAllow: true });
|
||||
expect(result.files[0].path).toMatch(/opencode\.jsonc$/);
|
||||
expect(result.files[0].action).toBe('created');
|
||||
});
|
||||
|
||||
it('opencode: preserves line and block comments through install + idempotent re-run', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const dir = path.join(tmpHome, '.config', 'opencode');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, 'opencode.jsonc');
|
||||
const original = [
|
||||
'{',
|
||||
' // top-level note about my opencode setup',
|
||||
' "$schema": "https://opencode.ai/config.json",',
|
||||
' /* multi-line block comment',
|
||||
' describing the providers section */',
|
||||
' "providers": {',
|
||||
' "anthropic": { "model": "claude-opus-4-7" } // pinned',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
fs.writeFileSync(file, original);
|
||||
|
||||
opencode.install('global', { autoAllow: true });
|
||||
const afterInstall = fs.readFileSync(file, 'utf-8');
|
||||
expect(afterInstall).toContain('// top-level note about my opencode setup');
|
||||
expect(afterInstall).toContain('/* multi-line block comment');
|
||||
expect(afterInstall).toContain('// pinned');
|
||||
expect(afterInstall).toContain('"codegraph"');
|
||||
expect(afterInstall).toContain('"providers"');
|
||||
|
||||
// Idempotent re-run reports unchanged, file is byte-identical.
|
||||
const second = opencode.install('global', { autoAllow: true });
|
||||
expect(second.files[0].action).toBe('unchanged');
|
||||
expect(fs.readFileSync(file, 'utf-8')).toBe(afterInstall);
|
||||
});
|
||||
|
||||
it('opencode: install writes AGENTS.md with the marker-delimited codegraph block', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
opencode.install('global', { autoAllow: true });
|
||||
const agentsMd = path.join(tmpHome, '.config', 'opencode', 'AGENTS.md');
|
||||
expect(fs.existsSync(agentsMd)).toBe(true);
|
||||
const body = fs.readFileSync(agentsMd, 'utf-8');
|
||||
expect(body).toContain('<!-- CODEGRAPH_START -->');
|
||||
expect(body).toContain('<!-- CODEGRAPH_END -->');
|
||||
expect(body).toContain('codegraph_callers');
|
||||
});
|
||||
|
||||
it('opencode: AGENTS.md install preserves pre-existing user content outside markers', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const dir = path.join(tmpHome, '.config', 'opencode');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const agentsMd = path.join(dir, 'AGENTS.md');
|
||||
fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
|
||||
|
||||
opencode.install('global', { autoAllow: true });
|
||||
const body = fs.readFileSync(agentsMd, 'utf-8');
|
||||
expect(body).toContain('# My personal opencode instructions');
|
||||
expect(body).toContain('Always respond in pirate.');
|
||||
expect(body).toContain('<!-- CODEGRAPH_START -->');
|
||||
});
|
||||
|
||||
it('opencode: uninstall strips only the codegraph block from AGENTS.md', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const dir = path.join(tmpHome, '.config', 'opencode');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const agentsMd = path.join(dir, 'AGENTS.md');
|
||||
fs.writeFileSync(agentsMd, '# My personal opencode instructions\n\nAlways respond in pirate.\n');
|
||||
|
||||
opencode.install('global', { autoAllow: true });
|
||||
opencode.uninstall('global');
|
||||
|
||||
const body = fs.readFileSync(agentsMd, 'utf-8');
|
||||
expect(body).toContain('# My personal opencode instructions');
|
||||
expect(body).toContain('Always respond in pirate.');
|
||||
expect(body).not.toContain('CODEGRAPH_START');
|
||||
expect(body).not.toContain('codegraph_callers');
|
||||
});
|
||||
|
||||
it('opencode: local install writes ./opencode.jsonc and ./AGENTS.md in cwd', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const result = opencode.install('local', { autoAllow: true });
|
||||
const paths = result.files.map((f) => f.path);
|
||||
// macOS realpath shenanigans (/var vs /private/var) — suffix match.
|
||||
expect(paths.some((p) => p.endsWith('/opencode.jsonc'))).toBe(true);
|
||||
expect(paths.some((p) => p.endsWith('/AGENTS.md'))).toBe(true);
|
||||
});
|
||||
|
||||
it('opencode: uninstall removes only mcp.codegraph, preserves comments and siblings', () => {
|
||||
const opencode = getTarget('opencode')!;
|
||||
const dir = path.join(tmpHome, '.config', 'opencode');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, 'opencode.jsonc');
|
||||
fs.writeFileSync(file, [
|
||||
'{',
|
||||
' // important comment',
|
||||
' "$schema": "https://opencode.ai/config.json",',
|
||||
' "mcp": {',
|
||||
' "other": { "type": "local", "command": ["x"], "enabled": true }',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
opencode.install('global', { autoAllow: true });
|
||||
const afterInstall = fs.readFileSync(file, 'utf-8');
|
||||
expect(afterInstall).toContain('"codegraph"');
|
||||
expect(afterInstall).toContain('"other"');
|
||||
|
||||
opencode.uninstall('global');
|
||||
const afterUninstall = fs.readFileSync(file, 'utf-8');
|
||||
expect(afterUninstall).not.toContain('codegraph');
|
||||
expect(afterUninstall).toContain('// important comment');
|
||||
expect(afterUninstall).toContain('"other"');
|
||||
});
|
||||
|
||||
it('codex: user-added key inside [mcp_servers.codegraph] survives idempotent re-install', () => {
|
||||
const codex = getTarget('codex')!;
|
||||
codex.install('global', { autoAllow: false });
|
||||
|
||||
Reference in New Issue
Block a user