opencode-onboard 0.4.8 → 0.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -30,6 +30,7 @@
30
30
  "chalk": "^5.0.0",
31
31
  "execa": "^9.6.1",
32
32
  "fs-extra": "^11.0.0",
33
+ "jsonc-parser": "^3.3.1",
33
34
  "ora": "^8.0.0"
34
35
  },
35
36
  "devDependencies": {
@@ -1,32 +1,35 @@
1
1
  import { execa } from 'execa'
2
2
  import fse from 'fs-extra'
3
3
  import path from 'node:path'
4
+ import { parse as parseJsonc } from 'jsonc-parser'
4
5
  import { header, success, warn, error, loading } from '../../utils/exec.js'
5
6
 
6
7
  /**
7
8
  * After codegraph install, it may create an `opencode.jsonc` at the project root.
8
9
  * This project uses `.opencode/opencode.json` instead. Merge any MCP config from
9
10
  * the rogue file into the correct location and remove it.
11
+ * Returns true if config was successfully merged (or no rogue file existed), false on parse failure.
10
12
  */
11
13
  export async function fixCodegraphConfig() {
12
14
  const cwd = process.cwd()
13
15
  const rogueFile = path.join(cwd, 'opencode.jsonc')
14
16
  const correctFile = path.join(cwd, '.opencode', 'opencode.json')
15
17
 
16
- if (!await fse.pathExists(rogueFile)) return
18
+ if (!await fse.pathExists(rogueFile)) return true
17
19
 
18
20
  let rogueContent
19
21
  try {
20
22
  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)
23
+ const errors = []
24
+ rogueContent = parseJsonc(raw, errors)
25
+ if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
26
+ if (!rogueContent || typeof rogueContent !== 'object' || Array.isArray(rogueContent)) {
27
+ throw new Error('unexpected structure')
28
+ }
26
29
  } catch {
27
30
  warn('Could not parse opencode.jsonc, removing it')
28
31
  await fse.remove(rogueFile)
29
- return
32
+ return false
30
33
  }
31
34
 
32
35
  let correctContent = {}
@@ -34,20 +37,21 @@ export async function fixCodegraphConfig() {
34
37
  try {
35
38
  correctContent = await fse.readJson(correctFile)
36
39
  } catch {
37
- correctContent = {}
40
+ // ignore invalid existing config
38
41
  }
39
42
  }
40
43
 
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 }
44
+ const rogueMcp = rogueContent.mcpServers || rogueContent.mcp
45
+ if (rogueMcp) {
46
+ correctContent.mcp = { ...(correctContent.mcp || {}), ...rogueMcp }
45
47
  }
46
48
 
47
49
  await fse.ensureDir(path.dirname(correctFile))
48
50
  await fse.writeJson(correctFile, correctContent, { spaces: 2 })
49
51
  await fse.remove(rogueFile)
50
52
  warn('Migrated codegraph config from opencode.jsonc → .opencode/opencode.json (removed opencode.jsonc)')
53
+
54
+ return true
51
55
  }
52
56
 
53
57
  export async function installCodegraph(options = {}) {
@@ -73,7 +77,13 @@ export async function installCodegraph(options = {}) {
73
77
  return { optedIn: true, installed: false }
74
78
  }
75
79
 
76
- await fixCodegraphConfig()
80
+ const configFixed = await fixCodegraphConfig()
81
+
82
+ if (!configFixed) {
83
+ warn('codegraph config could not be merged — skipping init')
84
+ return { optedIn: true, installed: false }
85
+ }
86
+
77
87
  success(`codegraph configured for opencode (${location})`)
78
88
  } catch (err) {
79
89
  error(`Failed to install codegraph: ${err.message}`)
@@ -83,7 +93,7 @@ export async function installCodegraph(options = {}) {
83
93
  loading('initializing codegraph project index...')
84
94
 
85
95
  try {
86
- const initResult = await execa('codegraph', ['init'], {
96
+ const initResult = await execa('npx', ['codegraph', 'init'], {
87
97
  cwd: process.cwd(),
88
98
  reject: false,
89
99
  stdio: 'pipe',
@@ -29,13 +29,13 @@ describe('fixCodegraphConfig()', () => {
29
29
  vi.restoreAllMocks()
30
30
  })
31
31
 
32
- it('does nothing when opencode.jsonc does not exist', async () => {
33
- await fixCodegraphConfig()
34
- // No error, no file created
32
+ it('returns true when opencode.jsonc does not exist', async () => {
33
+ const result = await fixCodegraphConfig()
34
+ expect(result).toBe(true)
35
35
  expect(fs.existsSync(path.join(tmpDir, '.opencode', 'opencode.json'))).toBe(false)
36
36
  })
37
37
 
38
- it('merges mcpServers from opencode.jsonc into .opencode/opencode.json', async () => {
38
+ it('merges mcpServers from opencode.jsonc into .opencode/opencode.json as mcp', async () => {
39
39
  const rogueContent = {
40
40
  mcpServers: {
41
41
  codegraph: { command: 'codegraph', args: ['mcp'] }
@@ -50,12 +50,31 @@ describe('fixCodegraphConfig()', () => {
50
50
  "plugin": ["opencode-plugin-openspec@latest"]
51
51
  }))
52
52
 
53
- await fixCodegraphConfig()
53
+ const result = await fixCodegraphConfig()
54
54
 
55
+ expect(result).toBe(true)
55
56
  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"])
57
+ const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
58
+ expect(readResult.mcp.codegraph).toEqual({ command: 'codegraph', args: ['mcp'] })
59
+ expect(readResult.plugin).toEqual(["opencode-plugin-openspec@latest"])
60
+ })
61
+
62
+ it('handles rogue file with mcp key directly', async () => {
63
+ const rogueContent = {
64
+ mcp: {
65
+ codegraph: { command: 'codegraph', args: ['mcp'] }
66
+ }
67
+ }
68
+ fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), JSON.stringify(rogueContent))
69
+ fs.mkdirSync(path.join(tmpDir, '.opencode'), { recursive: true })
70
+ fs.writeFileSync(path.join(tmpDir, '.opencode', 'opencode.json'), '{}')
71
+
72
+ const result = await fixCodegraphConfig()
73
+
74
+ expect(result).toBe(true)
75
+ expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
76
+ const readResult = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
77
+ expect(readResult.mcp.codegraph.command).toBe('codegraph')
59
78
  })
60
79
 
61
80
  it('handles JSONC with comments', async () => {
@@ -71,18 +90,43 @@ describe('fixCodegraphConfig()', () => {
71
90
  fs.mkdirSync(opencodeDir, { recursive: true })
72
91
  fs.writeFileSync(path.join(opencodeDir, 'opencode.json'), '{}')
73
92
 
74
- await fixCodegraphConfig()
93
+ const result = await fixCodegraphConfig()
94
+
95
+ expect(result).toBe(true)
96
+ expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
97
+ const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
98
+ expect(readResult.mcp.codegraph.command).toBe('codegraph')
99
+ })
100
+
101
+ it('handles JSONC with URLs containing //', async () => {
102
+ const rogueRaw = `{
103
+ "url": "https://example.com/path",
104
+ "mcpServers": {
105
+ "codegraph": { "command": "codegraph", "args": ["mcp"] }
106
+ }
107
+ }`
108
+ fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), rogueRaw)
109
+
110
+ const opencodeDir = path.join(tmpDir, '.opencode')
111
+ fs.mkdirSync(opencodeDir, { recursive: true })
112
+ fs.writeFileSync(path.join(opencodeDir, 'opencode.json'), '{}')
113
+
114
+ const result = await fixCodegraphConfig()
75
115
 
116
+ // The old regex-based parser would have mangled the URL and failed.
117
+ // jsonc-parser handles this correctly.
118
+ expect(result).toBe(true)
76
119
  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')
120
+ const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
121
+ expect(readResult.mcp.codegraph.command).toBe('codegraph')
79
122
  })
80
123
 
81
- it('removes unparseable opencode.jsonc and warns', async () => {
124
+ it('removes unparseable opencode.jsonc, warns, and returns false', async () => {
82
125
  fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), 'not valid json {{{')
83
126
 
84
- await fixCodegraphConfig()
127
+ const result = await fixCodegraphConfig()
85
128
 
129
+ expect(result).toBe(false)
86
130
  expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
87
131
  expect(warn).toHaveBeenCalledWith('Could not parse opencode.jsonc, removing it')
88
132
  })
@@ -95,10 +139,11 @@ describe('fixCodegraphConfig()', () => {
95
139
  }
96
140
  fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), JSON.stringify(rogueContent))
97
141
 
98
- await fixCodegraphConfig()
142
+ const result = await fixCodegraphConfig()
99
143
 
100
- const result = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
101
- expect(result.mcpServers.codegraph.command).toBe('codegraph')
144
+ expect(result).toBe(true)
145
+ const readResult = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
146
+ expect(readResult.mcp.codegraph.command).toBe('codegraph')
102
147
  expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
103
148
  })
104
149
  })