opencode-onboard 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/content/AGENTS.md
CHANGED
|
@@ -356,6 +356,15 @@ Minimal non-negotiables:
|
|
|
356
356
|
- Use `gh`/`az` CLI for platform operations.
|
|
357
357
|
- In multi-repo source scope, run git operations per repository.
|
|
358
358
|
|
|
359
|
+
### Config file conflict: `opencode.jsonc` vs `.opencode/opencode.json`
|
|
360
|
+
|
|
361
|
+
This project uses `.opencode/opencode.json` as the single OpenCode configuration file. Some tools (e.g., codegraph) may create an `opencode.jsonc` file at the project root. **These two files cannot coexist.**
|
|
362
|
+
|
|
363
|
+
If you detect both `opencode.jsonc` (project root) and `.opencode/opencode.json` exist:
|
|
364
|
+
1. **Stop immediately** and warn the user: "Conflicting OpenCode config files detected. This project uses `.opencode/opencode.json` only. The root `opencode.jsonc` must be removed or its contents merged into `.opencode/opencode.json`."
|
|
365
|
+
2. Do NOT proceed with any task until the conflict is resolved.
|
|
366
|
+
3. If the user asks you to fix it: merge any `mcpServers` or other config from `opencode.jsonc` into `.opencode/opencode.json`, then delete `opencode.jsonc`.
|
|
367
|
+
|
|
359
368
|
---
|
|
360
369
|
|
|
361
370
|
## Communication Style
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'node:path'
|
|
2
4
|
import { header, success, warn, error, loading } from '../../utils/exec.js'
|
|
3
5
|
|
|
6
|
+
/**
|
|
7
|
+
* After codegraph install, it may create an `opencode.jsonc` at the project root.
|
|
8
|
+
* This project uses `.opencode/opencode.json` instead. Merge any MCP config from
|
|
9
|
+
* the rogue file into the correct location and remove it.
|
|
10
|
+
*/
|
|
11
|
+
export async function fixCodegraphConfig() {
|
|
12
|
+
const cwd = process.cwd()
|
|
13
|
+
const rogueFile = path.join(cwd, 'opencode.jsonc')
|
|
14
|
+
const correctFile = path.join(cwd, '.opencode', 'opencode.json')
|
|
15
|
+
|
|
16
|
+
if (!await fse.pathExists(rogueFile)) return
|
|
17
|
+
|
|
18
|
+
let rogueContent
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fse.readFile(rogueFile, 'utf-8')
|
|
21
|
+
// Strip JSONC comments (single-line // and block /* */) before parsing
|
|
22
|
+
const stripped = raw
|
|
23
|
+
.replace(/\/\/.*$/gm, '')
|
|
24
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
25
|
+
rogueContent = JSON.parse(stripped)
|
|
26
|
+
} catch {
|
|
27
|
+
warn('Could not parse opencode.jsonc, removing it')
|
|
28
|
+
await fse.remove(rogueFile)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let correctContent = {}
|
|
33
|
+
if (await fse.pathExists(correctFile)) {
|
|
34
|
+
try {
|
|
35
|
+
correctContent = await fse.readJson(correctFile)
|
|
36
|
+
} catch {
|
|
37
|
+
correctContent = {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Merge mcpServers from rogue into correct config
|
|
42
|
+
if (rogueContent.mcpServers || rogueContent.mcp) {
|
|
43
|
+
const mcpServers = rogueContent.mcpServers || rogueContent.mcp
|
|
44
|
+
correctContent.mcpServers = { ...(correctContent.mcpServers || {}), ...mcpServers }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await fse.ensureDir(path.dirname(correctFile))
|
|
48
|
+
await fse.writeJson(correctFile, correctContent, { spaces: 2 })
|
|
49
|
+
await fse.remove(rogueFile)
|
|
50
|
+
warn('Migrated codegraph config from opencode.jsonc → .opencode/opencode.json (removed opencode.jsonc)')
|
|
51
|
+
}
|
|
52
|
+
|
|
4
53
|
export async function installCodegraph(options = {}) {
|
|
5
54
|
if (!options.skipHeader) header('Installing codegraph')
|
|
6
55
|
|
|
@@ -23,6 +72,8 @@ export async function installCodegraph(options = {}) {
|
|
|
23
72
|
warn('codegraph install exited with non-zero code')
|
|
24
73
|
return { optedIn: true, installed: false }
|
|
25
74
|
}
|
|
75
|
+
|
|
76
|
+
await fixCodegraphConfig()
|
|
26
77
|
success(`codegraph configured for opencode (${location})`)
|
|
27
78
|
} catch (err) {
|
|
28
79
|
error(`Failed to install codegraph: ${err.message}`)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import fse from 'fs-extra'
|
|
6
|
+
|
|
7
|
+
vi.mock('execa', () => ({ execa: vi.fn() }))
|
|
8
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
9
|
+
header: vi.fn(),
|
|
10
|
+
success: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
loading: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
import { warn } from '../../utils/exec.js'
|
|
17
|
+
import { fixCodegraphConfig } from './codegraph.js'
|
|
18
|
+
|
|
19
|
+
describe('fixCodegraphConfig()', () => {
|
|
20
|
+
let tmpDir
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'))
|
|
24
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
29
|
+
vi.restoreAllMocks()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('does nothing when opencode.jsonc does not exist', async () => {
|
|
33
|
+
await fixCodegraphConfig()
|
|
34
|
+
// No error, no file created
|
|
35
|
+
expect(fs.existsSync(path.join(tmpDir, '.opencode', 'opencode.json'))).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('merges mcpServers from opencode.jsonc into .opencode/opencode.json', async () => {
|
|
39
|
+
const rogueContent = {
|
|
40
|
+
mcpServers: {
|
|
41
|
+
codegraph: { command: 'codegraph', args: ['mcp'] }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), JSON.stringify(rogueContent))
|
|
45
|
+
|
|
46
|
+
const opencodeDir = path.join(tmpDir, '.opencode')
|
|
47
|
+
fs.mkdirSync(opencodeDir, { recursive: true })
|
|
48
|
+
fs.writeFileSync(path.join(opencodeDir, 'opencode.json'), JSON.stringify({
|
|
49
|
+
"$schema": "https://opencode.ai/config.json",
|
|
50
|
+
"plugin": ["opencode-plugin-openspec@latest"]
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
await fixCodegraphConfig()
|
|
54
|
+
|
|
55
|
+
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
56
|
+
const result = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
|
|
57
|
+
expect(result.mcpServers.codegraph).toEqual({ command: 'codegraph', args: ['mcp'] })
|
|
58
|
+
expect(result.plugin).toEqual(["opencode-plugin-openspec@latest"])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('handles JSONC with comments', async () => {
|
|
62
|
+
const rogueRaw = `{
|
|
63
|
+
// This is a comment
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"codegraph": { "command": "codegraph", "args": ["mcp"] }
|
|
66
|
+
}
|
|
67
|
+
}`
|
|
68
|
+
fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), rogueRaw)
|
|
69
|
+
|
|
70
|
+
const opencodeDir = path.join(tmpDir, '.opencode')
|
|
71
|
+
fs.mkdirSync(opencodeDir, { recursive: true })
|
|
72
|
+
fs.writeFileSync(path.join(opencodeDir, 'opencode.json'), '{}')
|
|
73
|
+
|
|
74
|
+
await fixCodegraphConfig()
|
|
75
|
+
|
|
76
|
+
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
77
|
+
const result = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
|
|
78
|
+
expect(result.mcpServers.codegraph.command).toBe('codegraph')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('removes unparseable opencode.jsonc and warns', async () => {
|
|
82
|
+
fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), 'not valid json {{{')
|
|
83
|
+
|
|
84
|
+
await fixCodegraphConfig()
|
|
85
|
+
|
|
86
|
+
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
87
|
+
expect(warn).toHaveBeenCalledWith('Could not parse opencode.jsonc, removing it')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('creates .opencode/opencode.json if it does not exist', async () => {
|
|
91
|
+
const rogueContent = {
|
|
92
|
+
mcpServers: {
|
|
93
|
+
codegraph: { command: 'codegraph', args: ['mcp'] }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), JSON.stringify(rogueContent))
|
|
97
|
+
|
|
98
|
+
await fixCodegraphConfig()
|
|
99
|
+
|
|
100
|
+
const result = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
|
|
101
|
+
expect(result.mcpServers.codegraph.command).toBe('codegraph')
|
|
102
|
+
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
})
|