opencode-onboard 0.4.7 → 0.4.9

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.7",
3
+ "version": "0.4.9",
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": {
@@ -52,4 +52,20 @@ export async function installSkills() {
52
52
  } catch (err) {
53
53
  warn(`npx skills failed: ${err.message}`)
54
54
  }
55
+
56
+ info('Installing opencode-ensemble skill...')
57
+ try {
58
+ const result = await execa('npx', ['skills@latest', 'add', 'hueyexe/opencode-ensemble', '--skill', 'opencode-ensemble', '-y'], {
59
+ reject: false,
60
+ timeout: 120000,
61
+ stdio: 'inherit',
62
+ })
63
+ if (result.exitCode === 0) {
64
+ success('opencode-ensemble skill installed')
65
+ } else {
66
+ warn('opencode-ensemble install exited with non-zero code')
67
+ }
68
+ } catch (err) {
69
+ warn(`opencode-ensemble install failed: ${err.message}`)
70
+ }
55
71
  }
@@ -8,8 +8,8 @@ export async function installCaveman(options = {}) {
8
8
 
9
9
  const isGlobal = options.installScope === 'global'
10
10
  const skillsArgs = isGlobal
11
- ? ['skills', 'add', 'JuliusBrussee/caveman/caveman', '-a', 'opencode', '--yes', '-g']
12
- : ['skills', 'add', 'JuliusBrussee/caveman/caveman', '-a', 'opencode', '--yes']
11
+ ? ['skills', 'add', 'https://github.com/juliusbrussee/caveman', '--skill', 'caveman', '-a', 'opencode', '--yes', '-g']
12
+ : ['skills', 'add', 'https://github.com/juliusbrussee/caveman', '--skill', 'caveman', '-a', 'opencode', '--yes']
13
13
 
14
14
  try {
15
15
  info('Installing caveman via npx skills')
@@ -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,11 +37,10 @@ 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
44
  if (rogueContent.mcpServers || rogueContent.mcp) {
43
45
  const mcpServers = rogueContent.mcpServers || rogueContent.mcp
44
46
  correctContent.mcpServers = { ...(correctContent.mcpServers || {}), ...mcpServers }
@@ -48,6 +50,8 @@ export async function fixCodegraphConfig() {
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}`)
@@ -29,9 +29,9 @@ 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
 
@@ -50,12 +50,13 @@ 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.mcpServers.codegraph).toEqual({ command: 'codegraph', args: ['mcp'] })
59
+ expect(readResult.plugin).toEqual(["opencode-plugin-openspec@latest"])
59
60
  })
60
61
 
61
62
  it('handles JSONC with comments', async () => {
@@ -71,18 +72,43 @@ describe('fixCodegraphConfig()', () => {
71
72
  fs.mkdirSync(opencodeDir, { recursive: true })
72
73
  fs.writeFileSync(path.join(opencodeDir, 'opencode.json'), '{}')
73
74
 
74
- await fixCodegraphConfig()
75
+ const result = await fixCodegraphConfig()
75
76
 
77
+ expect(result).toBe(true)
76
78
  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
+ const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
80
+ expect(readResult.mcpServers.codegraph.command).toBe('codegraph')
79
81
  })
80
82
 
81
- it('removes unparseable opencode.jsonc and warns', async () => {
83
+ it('handles JSONC with URLs containing //', async () => {
84
+ const rogueRaw = `{
85
+ "url": "https://example.com/path",
86
+ "mcpServers": {
87
+ "codegraph": { "command": "codegraph", "args": ["mcp"] }
88
+ }
89
+ }`
90
+ fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), rogueRaw)
91
+
92
+ const opencodeDir = path.join(tmpDir, '.opencode')
93
+ fs.mkdirSync(opencodeDir, { recursive: true })
94
+ fs.writeFileSync(path.join(opencodeDir, 'opencode.json'), '{}')
95
+
96
+ const result = await fixCodegraphConfig()
97
+
98
+ // The old regex-based parser would have mangled the URL and failed.
99
+ // jsonc-parser handles this correctly.
100
+ expect(result).toBe(true)
101
+ expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
102
+ const readResult = await fse.readJson(path.join(opencodeDir, 'opencode.json'))
103
+ expect(readResult.mcpServers.codegraph.command).toBe('codegraph')
104
+ })
105
+
106
+ it('removes unparseable opencode.jsonc, warns, and returns false', async () => {
82
107
  fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), 'not valid json {{{')
83
108
 
84
- await fixCodegraphConfig()
109
+ const result = await fixCodegraphConfig()
85
110
 
111
+ expect(result).toBe(false)
86
112
  expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
87
113
  expect(warn).toHaveBeenCalledWith('Could not parse opencode.jsonc, removing it')
88
114
  })
@@ -95,10 +121,11 @@ describe('fixCodegraphConfig()', () => {
95
121
  }
96
122
  fs.writeFileSync(path.join(tmpDir, 'opencode.jsonc'), JSON.stringify(rogueContent))
97
123
 
98
- await fixCodegraphConfig()
124
+ const result = await fixCodegraphConfig()
99
125
 
100
- const result = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
101
- expect(result.mcpServers.codegraph.command).toBe('codegraph')
126
+ expect(result).toBe(true)
127
+ const readResult = await fse.readJson(path.join(tmpDir, '.opencode', 'opencode.json'))
128
+ expect(readResult.mcpServers.codegraph.command).toBe('codegraph')
102
129
  expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false)
103
130
  })
104
131
  })