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.
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
40
|
+
// ignore invalid existing config
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
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('
|
|
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('
|
|
33
|
-
await fixCodegraphConfig()
|
|
34
|
-
|
|
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
|
|
57
|
-
expect(
|
|
58
|
-
expect(
|
|
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
|
|
78
|
-
expect(
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
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
|
})
|