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,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -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
+ })