opencode-onboard 0.4.2 → 0.4.4
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/LICENSE +21 -0
- package/README.md +304 -301
- package/content/.agents/agents/basic-engineer.md +4 -2
- package/content/.agents/agents/devops-manager.md +123 -123
- package/content/.agents/skills/ob-default/SKILL.md +25 -21
- package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
- package/content/.agents/skills/ob-global/SKILL.md +92 -49
- package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
- package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
- package/content/.opencode/commands/create-engineer.md +109 -0
- package/content/.opencode/commands/init.md +1 -1
- package/content/.opencode/commands/main.md +1 -1
- package/content/.opencode/commands/opsx-apply.md +131 -70
- package/content/.opencode/commands/plan.md +1 -1
- package/content/.opencode/plugins/session-log.js +523 -519
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +86 -64
- package/content/AGENTS.md +67 -39
- package/package.json +1 -1
- package/src/commands/join.js +3 -3
- package/src/commands/single.js +2 -0
- package/src/commands/wizard.js +124 -99
- package/src/presets/browser.json +22 -18
- package/src/presets/optimization.json +27 -22
- package/src/presets/source.json +7 -1
- package/src/steps/browser/browser.test.js +115 -81
- package/src/steps/browser/index.js +62 -54
- package/src/steps/clean/index.js +108 -107
- package/src/steps/copy/agents.js +28 -0
- package/src/steps/copy/copy.test.js +1 -0
- package/src/steps/copy/index.js +2 -1
- package/src/steps/metadata/index.js +63 -61
- package/src/steps/models/format.js +61 -60
- package/src/steps/models/write.test.js +117 -117
- package/src/steps/openspec/ensemble.js +30 -7
- package/src/steps/openspec/ensemble.test.js +79 -79
- package/src/steps/openspec/index.js +121 -32
- package/src/steps/openspec/index.test.js +63 -0
- package/src/steps/optimization/caveman.js +34 -29
- package/src/steps/optimization/codegraph.js +52 -0
- package/src/steps/optimization/global.js +88 -64
- package/src/steps/optimization/global.test.js +99 -0
- package/src/steps/optimization/index.js +109 -101
- package/src/steps/optimization/optimization.test.js +101 -93
- package/src/steps/optimization/quota.js +84 -84
- package/src/steps/source/index.js +48 -0
- package/src/steps/source/source.test.js +124 -91
- package/src/utils/__tests__/copy.test.js +117 -117
- package/src/utils/exec-spinner.js +47 -47
- package/src/utils/exec.js +134 -131
- package/src/utils/terminal.js +6 -0
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import { patchApplyFile, APPLY_TARGETS } from './ensemble.js'
|
|
6
|
-
|
|
7
|
-
describe('patchApplyFile()', () => {
|
|
8
|
-
let tmpDir
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-test-'))
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('returns ok:false when file does not exist', async () => {
|
|
19
|
-
const result = await patchApplyFile(path.join(tmpDir, 'missing.md'))
|
|
20
|
-
|
|
21
|
-
expect(result).toEqual({ ok: false, reason: 'missing-file' })
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('returns ok:false when Step 6 is not found', async () => {
|
|
25
|
-
const filePath = path.join(tmpDir, 'opsx-apply.md')
|
|
26
|
-
fs.writeFileSync(filePath, 'Some other content without Step 6', 'utf-8')
|
|
27
|
-
|
|
28
|
-
const result = await patchApplyFile(filePath)
|
|
29
|
-
|
|
30
|
-
expect(result).toEqual({ ok: false, reason: 'missing-step-6' })
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('patches file with ENSEMBLE_SECTION when Step 6 found', async () => {
|
|
34
|
-
const filePath = path.join(tmpDir, 'opsx-apply.md')
|
|
35
|
-
const original = `Some header
|
|
36
|
-
|
|
37
|
-
6. **Implement**
|
|
38
|
-
Old implementation here.
|
|
39
|
-
|
|
40
|
-
**Fluid Workflow Integration**
|
|
41
|
-
Fluid section content.
|
|
42
|
-
`
|
|
43
|
-
fs.writeFileSync(filePath, original, 'utf-8')
|
|
44
|
-
|
|
45
|
-
const result = await patchApplyFile(filePath)
|
|
46
|
-
|
|
47
|
-
expect(result.ok).toBe(true)
|
|
48
|
-
const patched = fs.readFileSync(filePath, 'utf-8')
|
|
49
|
-
expect(patched).toContain('**Implement via ensemble team**')
|
|
50
|
-
expect(patched).toContain('6. **Implement via ensemble team**')
|
|
51
|
-
expect(patched).toContain('**Fluid Workflow Integration**')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('removes original Step 6 content before patching', async () => {
|
|
55
|
-
const filePath = path.join(tmpDir, 'SKILL.md')
|
|
56
|
-
const original = `Steps:
|
|
57
|
-
|
|
58
|
-
6. **Implement**
|
|
59
|
-
Do things directly.
|
|
60
|
-
|
|
61
|
-
7. **Quality check**
|
|
62
|
-
`
|
|
63
|
-
fs.writeFileSync(filePath, original, 'utf-8')
|
|
64
|
-
|
|
65
|
-
await patchApplyFile(filePath)
|
|
66
|
-
|
|
67
|
-
const patched = fs.readFileSync(filePath, 'utf-8')
|
|
68
|
-
expect(patched).not.toContain('Do things directly.')
|
|
69
|
-
expect(patched).toContain('NEVER implement tasks directly')
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('APPLY_TARGETS', () => {
|
|
74
|
-
it('contains expected OpenSpec apply file paths', () => {
|
|
75
|
-
expect(APPLY_TARGETS).toHaveLength(2)
|
|
76
|
-
expect(APPLY_TARGETS).toContain(path.join('.opencode', 'commands', 'opsx-apply.md'))
|
|
77
|
-
expect(APPLY_TARGETS).toContain(path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'))
|
|
78
|
-
})
|
|
79
|
-
})
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import { patchApplyFile, APPLY_TARGETS } from './ensemble.js'
|
|
6
|
+
|
|
7
|
+
describe('patchApplyFile()', () => {
|
|
8
|
+
let tmpDir
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openspec-test-'))
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns ok:false when file does not exist', async () => {
|
|
19
|
+
const result = await patchApplyFile(path.join(tmpDir, 'missing.md'))
|
|
20
|
+
|
|
21
|
+
expect(result).toEqual({ ok: false, reason: 'missing-file' })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns ok:false when Step 6 is not found', async () => {
|
|
25
|
+
const filePath = path.join(tmpDir, 'opsx-apply.md')
|
|
26
|
+
fs.writeFileSync(filePath, 'Some other content without Step 6', 'utf-8')
|
|
27
|
+
|
|
28
|
+
const result = await patchApplyFile(filePath)
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ ok: false, reason: 'missing-step-6' })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('patches file with ENSEMBLE_SECTION when Step 6 found', async () => {
|
|
34
|
+
const filePath = path.join(tmpDir, 'opsx-apply.md')
|
|
35
|
+
const original = `Some header
|
|
36
|
+
|
|
37
|
+
6. **Implement**
|
|
38
|
+
Old implementation here.
|
|
39
|
+
|
|
40
|
+
**Fluid Workflow Integration**
|
|
41
|
+
Fluid section content.
|
|
42
|
+
`
|
|
43
|
+
fs.writeFileSync(filePath, original, 'utf-8')
|
|
44
|
+
|
|
45
|
+
const result = await patchApplyFile(filePath)
|
|
46
|
+
|
|
47
|
+
expect(result.ok).toBe(true)
|
|
48
|
+
const patched = fs.readFileSync(filePath, 'utf-8')
|
|
49
|
+
expect(patched).toContain('**Implement via ensemble team**')
|
|
50
|
+
expect(patched).toContain('6. **Implement via ensemble team**')
|
|
51
|
+
expect(patched).toContain('**Fluid Workflow Integration**')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('removes original Step 6 content before patching', async () => {
|
|
55
|
+
const filePath = path.join(tmpDir, 'SKILL.md')
|
|
56
|
+
const original = `Steps:
|
|
57
|
+
|
|
58
|
+
6. **Implement**
|
|
59
|
+
Do things directly.
|
|
60
|
+
|
|
61
|
+
7. **Quality check**
|
|
62
|
+
`
|
|
63
|
+
fs.writeFileSync(filePath, original, 'utf-8')
|
|
64
|
+
|
|
65
|
+
await patchApplyFile(filePath)
|
|
66
|
+
|
|
67
|
+
const patched = fs.readFileSync(filePath, 'utf-8')
|
|
68
|
+
expect(patched).not.toContain('Do things directly.')
|
|
69
|
+
expect(patched).toContain('NEVER implement tasks directly')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('APPLY_TARGETS', () => {
|
|
74
|
+
it('contains expected OpenSpec apply file paths', () => {
|
|
75
|
+
expect(APPLY_TARGETS).toHaveLength(2)
|
|
76
|
+
expect(APPLY_TARGETS).toContain(path.join('.opencode', 'commands', 'opsx-apply.md'))
|
|
77
|
+
expect(APPLY_TARGETS).toContain(path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'))
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -1,32 +1,121 @@
|
|
|
1
|
-
import { execa } from
|
|
2
|
-
import path from
|
|
3
|
-
import { error, header, success, warn
|
|
4
|
-
import { APPLY_TARGETS, patchApplyFile } from
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
1
|
+
import { execa } from "execa"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { commandExists, error, header, info, loading, success, warn} from "../../utils/exec.js"
|
|
4
|
+
import { APPLY_TARGETS, patchApplyFile } from "./ensemble.js"
|
|
5
|
+
|
|
6
|
+
export const openspecSteps = {
|
|
7
|
+
check,
|
|
8
|
+
install,
|
|
9
|
+
init,
|
|
10
|
+
patch,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function initOpenspec() {
|
|
14
|
+
header("Step 6, Initializing OpenSpec")
|
|
15
|
+
|
|
16
|
+
let openSpec = await openspecSteps.check()
|
|
17
|
+
if (!openSpec.available) {
|
|
18
|
+
await openspecSteps.install()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
openSpec = await openspecSteps.check()
|
|
22
|
+
if (!openSpec.available) {
|
|
23
|
+
warn("OpenSpec has not been installed. It is required to create changes.")
|
|
24
|
+
return { ...openSpec, initialized: false, patched: false }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initResult = await openspecSteps.init()
|
|
28
|
+
if (!initResult.initialized) {
|
|
29
|
+
warn(
|
|
30
|
+
"OpenSpec has not been initialized. It is required to create changes.",
|
|
31
|
+
)
|
|
32
|
+
return { ...openSpec, initialized: false, patched: false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const patchesResult = await openspecSteps.patch()
|
|
36
|
+
return {
|
|
37
|
+
...openSpec,
|
|
38
|
+
initialized: true,
|
|
39
|
+
patched: patchesResult.success,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function install() {
|
|
44
|
+
info("Installing OpenSpec...")
|
|
45
|
+
try {
|
|
46
|
+
const result = await execa(
|
|
47
|
+
"npm",
|
|
48
|
+
["install", "@fission-ai/openspec", "--global"],
|
|
49
|
+
{
|
|
50
|
+
cwd: process.cwd(),
|
|
51
|
+
stdio: "pipe",
|
|
52
|
+
reject: false,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
if (result.exitCode !== 0) {
|
|
56
|
+
warn("OpenSpec install failed, check output above")
|
|
57
|
+
}
|
|
58
|
+
success("OpenSpec installed")
|
|
59
|
+
} catch (err) {
|
|
60
|
+
error(`Failed to run openspec install: ${err.message}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function check() {
|
|
65
|
+
loading("Checking OpenSpec...")
|
|
66
|
+
|
|
67
|
+
const available = await commandExists("openspec")
|
|
68
|
+
|
|
69
|
+
if (available) success("OpenSpec is available")
|
|
70
|
+
else warn("OpenSpec not found on PATH.")
|
|
71
|
+
|
|
72
|
+
return { available }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function init() {
|
|
76
|
+
loading("Initializing OpenSpec...")
|
|
77
|
+
try {
|
|
78
|
+
const result = await execa(
|
|
79
|
+
"npx",
|
|
80
|
+
["@fission-ai/openspec", "init", "--tools", "opencode", "--force"],
|
|
81
|
+
{
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
stdio: "pipe",
|
|
84
|
+
reject: false,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`init failed with exit code ${result.exitCode}, check output above`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
error(`Failed to run openspec init: ${err.message}`)
|
|
95
|
+
return { initialized: false }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
success("OpenSpec initialized")
|
|
99
|
+
return { initialized: true }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function patch() {
|
|
103
|
+
loading("Patching OpenSpec ensemble implementation...")
|
|
104
|
+
|
|
105
|
+
const patched = []
|
|
106
|
+
|
|
107
|
+
for (const rel of APPLY_TARGETS) {
|
|
108
|
+
const abs = path.join(process.cwd(), rel)
|
|
109
|
+
try {
|
|
110
|
+
const res = await patchApplyFile(abs)
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
patched.push(rel)
|
|
113
|
+
success(`Patched ensemble implementation section in ${rel}`)
|
|
114
|
+
} else warn(`Could not patch ${rel} (${res.reason})`)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
warn(`Could not patch ${rel}: ${err.message}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { success: patched.length === APPLY_TARGETS.length, patched }
|
|
121
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
vi.mock("execa")
|
|
4
|
+
vi.mock("../../utils/exec.js")
|
|
5
|
+
vi.mock("./ensemble.js")
|
|
6
|
+
|
|
7
|
+
import { execa } from "execa"
|
|
8
|
+
import { commandExists } from "../../utils/exec.js"
|
|
9
|
+
import { patchApplyFile } from "./ensemble.js"
|
|
10
|
+
import { initOpenspec } from "./index.js"
|
|
11
|
+
|
|
12
|
+
const patchSuccess = () => patchApplyFile.mockResolvedValue({ ok: true })
|
|
13
|
+
const installFails = () => execa.mockResolvedValueOnce({ exitCode: 1 })
|
|
14
|
+
const installSuccess = () => execa.mockResolvedValueOnce({ exitCode: 0 })
|
|
15
|
+
const initSuccess = () => execa.mockResolvedValueOnce({ exitCode: 0 })
|
|
16
|
+
const initFails = () => execa.mockResolvedValueOnce({ exitCode: 1 })
|
|
17
|
+
const checkFails = () => commandExists.mockResolvedValueOnce(false)
|
|
18
|
+
const checkSuccess = () => commandExists.mockResolvedValueOnce(true)
|
|
19
|
+
|
|
20
|
+
describe("initOpenspec()", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("installs, initializes, and patches when openspec is initially unavailable", async () => {
|
|
26
|
+
checkFails()
|
|
27
|
+
checkSuccess()
|
|
28
|
+
installSuccess()
|
|
29
|
+
initSuccess()
|
|
30
|
+
patchSuccess()
|
|
31
|
+
|
|
32
|
+
const result = await initOpenspec()
|
|
33
|
+
|
|
34
|
+
expect(result.available).toBe(true)
|
|
35
|
+
expect(result.initialized).toBe(true)
|
|
36
|
+
expect(result.patched).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("fails if openspec remains unavailable after install", async () => {
|
|
40
|
+
checkFails()
|
|
41
|
+
checkFails()
|
|
42
|
+
installFails()
|
|
43
|
+
|
|
44
|
+
const result = await initOpenspec()
|
|
45
|
+
|
|
46
|
+
expect(result.available).toBe(false)
|
|
47
|
+
expect(result.initialized).toBe(false)
|
|
48
|
+
expect(result.patched).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("fails if openspec init command exits non-zero", async () => {
|
|
52
|
+
checkFails()
|
|
53
|
+
checkSuccess()
|
|
54
|
+
installSuccess()
|
|
55
|
+
initFails()
|
|
56
|
+
|
|
57
|
+
const result = await initOpenspec()
|
|
58
|
+
|
|
59
|
+
expect(result.available).toBe(true)
|
|
60
|
+
expect(result.initialized).toBe(false)
|
|
61
|
+
expect(result.patched).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
import { execa } from 'execa'
|
|
2
|
-
import { header, success, warn, error, loading, info } from '../../utils/exec.js'
|
|
3
|
-
|
|
4
|
-
export async function installCaveman(options = {}) {
|
|
5
|
-
if (!options.skipHeader) header('Installing caveman')
|
|
6
|
-
|
|
7
|
-
loading('installing caveman...')
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (result.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { header, success, warn, error, loading, info } from '../../utils/exec.js'
|
|
3
|
+
|
|
4
|
+
export async function installCaveman(options = {}) {
|
|
5
|
+
if (!options.skipHeader) header('Installing caveman')
|
|
6
|
+
|
|
7
|
+
loading('installing caveman...')
|
|
8
|
+
|
|
9
|
+
const isGlobal = options.installScope === 'global'
|
|
10
|
+
const skillsArgs = isGlobal
|
|
11
|
+
? ['skills', 'add', 'JuliusBrussee/caveman/caveman', '-a', 'opencode', '--yes', '-g']
|
|
12
|
+
: ['skills', 'add', 'JuliusBrussee/caveman/caveman', '-a', 'opencode', '--yes']
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
info('Installing caveman via npx skills')
|
|
16
|
+
const result = await execa('npx', skillsArgs, {
|
|
17
|
+
reject: false,
|
|
18
|
+
timeout: 600000,
|
|
19
|
+
stdio: 'pipe',
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
if (result.exitCode === 0) {
|
|
23
|
+
success('caveman installed')
|
|
24
|
+
return { optedIn: true, installed: true }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (result.stderr?.trim()) warn(result.stderr.trim().split('\n').slice(-3).join('\n'))
|
|
28
|
+
warn('caveman install exited with non-zero code')
|
|
29
|
+
return { optedIn: true, installed: false }
|
|
30
|
+
} catch (err) {
|
|
31
|
+
error(`Failed to install caveman: ${err.message}`)
|
|
32
|
+
return { optedIn: true, installed: false }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { header, success, warn, error, loading } from '../../utils/exec.js'
|
|
3
|
+
|
|
4
|
+
export async function installCodegraph(options = {}) {
|
|
5
|
+
if (!options.skipHeader) header('Installing codegraph')
|
|
6
|
+
|
|
7
|
+
const location = options.installScope === 'global' ? 'global' : 'local'
|
|
8
|
+
|
|
9
|
+
loading(`configuring codegraph for opencode (${location})...`)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const installResult = await execa(
|
|
13
|
+
'npx',
|
|
14
|
+
['@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
15
|
+
{
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
reject: false,
|
|
18
|
+
stdio: 'pipe',
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (installResult.exitCode !== 0) {
|
|
23
|
+
warn('codegraph install exited with non-zero code')
|
|
24
|
+
return { optedIn: true, installed: false }
|
|
25
|
+
}
|
|
26
|
+
success(`codegraph configured for opencode (${location})`)
|
|
27
|
+
} catch (err) {
|
|
28
|
+
error(`Failed to install codegraph: ${err.message}`)
|
|
29
|
+
return { optedIn: true, installed: false }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
loading('initializing codegraph project index...')
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const initResult = await execa('codegraph', ['init'], {
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
reject: false,
|
|
38
|
+
stdio: 'pipe',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (initResult.exitCode !== 0) {
|
|
42
|
+
warn('codegraph init exited with non-zero code')
|
|
43
|
+
return { optedIn: true, installed: false }
|
|
44
|
+
}
|
|
45
|
+
success('codegraph project index initialized')
|
|
46
|
+
} catch (err) {
|
|
47
|
+
error(`Failed to initialize codegraph: ${err.message}`)
|
|
48
|
+
return { optedIn: true, installed: false }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { optedIn: true, installed: true }
|
|
52
|
+
}
|
|
@@ -1,64 +1,88 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { info, success, warn } from '../../utils/exec.js'
|
|
4
|
-
|
|
5
|
-
const SOURCE_START = '<!-- OB-SOURCE-ROOTS-START -->'
|
|
6
|
-
const SOURCE_END = '<!-- OB-SOURCE-ROOTS-END -->'
|
|
7
|
-
const RTK_START = '<!-- OB-RTK-START -->'
|
|
8
|
-
const RTK_END = '<!-- OB-RTK-END -->'
|
|
9
|
-
const CAVEMAN_START = '<!-- OB-CAVEMAN-START -->'
|
|
10
|
-
const CAVEMAN_END = '<!-- OB-CAVEMAN-END -->'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { info, success, warn } from '../../utils/exec.js'
|
|
4
|
+
|
|
5
|
+
const SOURCE_START = '<!-- OB-SOURCE-ROOTS-START -->'
|
|
6
|
+
const SOURCE_END = '<!-- OB-SOURCE-ROOTS-END -->'
|
|
7
|
+
const RTK_START = '<!-- OB-RTK-START -->'
|
|
8
|
+
const RTK_END = '<!-- OB-RTK-END -->'
|
|
9
|
+
const CAVEMAN_START = '<!-- OB-CAVEMAN-START -->'
|
|
10
|
+
const CAVEMAN_END = '<!-- OB-CAVEMAN-END -->'
|
|
11
|
+
|
|
12
|
+
const CODEGRAPH_START = '<!-- OB-CODEGRAPH-START -->'
|
|
13
|
+
const CODEGRAPH_END = '<!-- OB-CODEGRAPH-END -->'
|
|
14
|
+
|
|
15
|
+
function relRoot(cwd, abs) {
|
|
16
|
+
const rel = path.relative(cwd, abs).replace(/\\/g, '/')
|
|
17
|
+
return rel || '.'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildSourceRootsSection(sourceRoots, cwd) {
|
|
21
|
+
const roots = Array.isArray(sourceRoots) && sourceRoots.length > 0 ? sourceRoots : [cwd]
|
|
22
|
+
const bullets = roots.map(r => `- \`${relRoot(cwd, r)}\``).join('\n')
|
|
23
|
+
const multiRepo = roots.length > 1
|
|
24
|
+
? '\nEach root is an independent git repository. For branch/commit/push workflows, run git operations per repository. There is no single shared git history across all roots.\n'
|
|
25
|
+
: ''
|
|
26
|
+
|
|
27
|
+
return `Read and analyze code ONLY from these roots:\n\n${bullets}\n${multiRepo}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildRtkSection(rtkEnabled) {
|
|
31
|
+
if (!rtkEnabled) return 'RTK was not selected during onboarding. Do not assume `rtk` exists.'
|
|
32
|
+
return `## RTK, MANDATORY\n\nUse \`rtk\` for ALL CLI commands. Never run commands directly.\n\n- \`rtk git\` NOT \`git\`\n- \`rtk gh\` NOT \`gh\`\n- \`rtk az\` NOT \`az\`\n- \`rtk openspec\` NOT \`openspec\`\n\nIf \`rtk\` is not available, report blocker and stop CLI execution.`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildCavemanSection(cavemanEnabled) {
|
|
36
|
+
if (!cavemanEnabled) return 'Caveman was not selected during onboarding. Use normal concise style.'
|
|
37
|
+
return `## Caveman\n\ncaveman mode. Active now. Every response. No revert unless user asks "stop caveman" or "normal mode".`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildCodegraphSection(codegraphEnabled) {
|
|
41
|
+
if (!codegraphEnabled) return 'Codegraph was not selected during onboarding. Use standard grep/glob/read for code exploration.'
|
|
42
|
+
return `## CodeGraph
|
|
43
|
+
|
|
44
|
+
This project has CodeGraph initialized (\`.codegraph/\` exists). Use it for all code exploration.
|
|
45
|
+
|
|
46
|
+
**NEVER call \`codegraph_explore\` or \`codegraph_context\` directly in the main session** — these return large source payloads that fill context. Instead, ALWAYS spawn an Explore sub-agent for exploration questions ("how does X work?", "where is Y implemented?").
|
|
47
|
+
|
|
48
|
+
When spawning Explore agents, include in the prompt:
|
|
49
|
+
> This project has CodeGraph initialized. Use \`codegraph_explore\` as your PRIMARY tool. Do NOT re-read files that codegraph_explore already returned. Only fall back to grep/glob/read for files listed under "Additional relevant files".
|
|
50
|
+
|
|
51
|
+
**The main session may only use these lightweight tools directly** (targeted lookups before edits):
|
|
52
|
+
- \`codegraph_search\` — find symbols by name
|
|
53
|
+
- \`codegraph_callers\` / \`codegraph_callees\` — trace call flow
|
|
54
|
+
- \`codegraph_impact\` — check what's affected before editing
|
|
55
|
+
- \`codegraph_node\` — get a single symbol's details`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
function replaceBetween(content, start, end, replacement) {
|
|
60
|
+
if (!content.includes(start) || !content.includes(end)) return content
|
|
61
|
+
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
|
|
62
|
+
return content.replace(pattern, `${start}\n${replacement.trim()}\n${end}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function configureObGlobal(ctx = {}, tokenOpt = {}) {
|
|
66
|
+
const cwd = process.cwd()
|
|
67
|
+
const skillPath = path.join(cwd, '.agents', 'skills', 'ob-global', 'SKILL.md')
|
|
68
|
+
|
|
69
|
+
if (!await fse.pathExists(skillPath)) {
|
|
70
|
+
warn('ob-global skill not found, skipping dynamic configuration')
|
|
71
|
+
return { configured: false }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sourceRootsSection = buildSourceRootsSection(ctx.sourceRoots, cwd)
|
|
75
|
+
const rtkSection = buildRtkSection(!!tokenOpt?.rtk?.optedIn)
|
|
76
|
+
const cavemanSection = buildCavemanSection(!!tokenOpt?.caveman?.optedIn)
|
|
77
|
+
const codegraphSection = buildCodegraphSection(!!tokenOpt?.codegraph?.optedIn)
|
|
78
|
+
|
|
79
|
+
let content = await fse.readFile(skillPath, 'utf-8')
|
|
80
|
+
content = replaceBetween(content, SOURCE_START, SOURCE_END, sourceRootsSection)
|
|
81
|
+
content = replaceBetween(content, RTK_START, RTK_END, rtkSection)
|
|
82
|
+
content = replaceBetween(content, CAVEMAN_START, CAVEMAN_END, cavemanSection)
|
|
83
|
+
content = replaceBetween(content, CODEGRAPH_START, CODEGRAPH_END, codegraphSection)
|
|
84
|
+
await fse.writeFile(skillPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
85
|
+
info('Configured ob-global from onboarding selections')
|
|
86
|
+
success('ob-global skill updated')
|
|
87
|
+
return { configured: true, path: skillPath }
|
|
88
|
+
}
|