opencode-onboard 0.4.3 → 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/README.md +41 -40
- 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 -84
- 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/plugins/session-log.js +523 -519
- package/content/AGENTS.md +32 -21
- package/package.json +1 -1
- package/src/commands/wizard.js +124 -113
- package/src/presets/browser.json +22 -18
- package/src/presets/optimization.json +27 -22
- 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/metadata/index.js +63 -62
- package/src/steps/models/format.js +61 -60
- package/src/steps/models/write.test.js +117 -117
- 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 +103 -0
- package/src/steps/optimization/codegraph.test.js +104 -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/source.test.js +124 -124
- 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,117 +1,117 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
|
|
6
|
-
vi.mock('../../utils/exec.js', () => ({
|
|
7
|
-
success: vi.fn(),
|
|
8
|
-
}))
|
|
9
|
-
|
|
10
|
-
import { success } from '../../utils/exec.js'
|
|
11
|
-
import { writeModelToAgent, writeModelsToConfigs } from './write.js'
|
|
12
|
-
|
|
13
|
-
describe('writeModelToAgent()', () => {
|
|
14
|
-
let tmpDir
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
vi.clearAllMocks()
|
|
18
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'models-write-test-'))
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('adds model field to agent file frontmatter', async () => {
|
|
26
|
-
const filePath = path.join(tmpDir, 'test-agent.md')
|
|
27
|
-
const original = `---
|
|
28
|
-
name: Test Agent
|
|
29
|
-
description: A test agent
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
# Test Agent`
|
|
33
|
-
fs.writeFileSync(filePath, original, 'utf-8')
|
|
34
|
-
|
|
35
|
-
await writeModelToAgent(filePath, 'anthropic/claude-3-sonnet')
|
|
36
|
-
|
|
37
|
-
const updated = fs.readFileSync(filePath, 'utf-8')
|
|
38
|
-
expect(updated).toContain('model: anthropic/claude-3-sonnet')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('preserves existing frontmatter fields', async () => {
|
|
42
|
-
const filePath = path.join(tmpDir, 'test-agent.md')
|
|
43
|
-
const original = `---
|
|
44
|
-
name: Test Agent
|
|
45
|
-
description: A test agent
|
|
46
|
-
custom_field: custom_value
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
# Test Agent`
|
|
50
|
-
fs.writeFileSync(filePath, original, 'utf-8')
|
|
51
|
-
|
|
52
|
-
await writeModelToAgent(filePath, 'test/model')
|
|
53
|
-
|
|
54
|
-
const updated = fs.readFileSync(filePath, 'utf-8')
|
|
55
|
-
expect(updated).toContain('name: Test Agent')
|
|
56
|
-
expect(updated).toContain('description: A test agent')
|
|
57
|
-
expect(updated).toContain('custom_field: custom_value')
|
|
58
|
-
expect(updated).toContain('model: test/model')
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
describe('writeModelsToConfigs()', () => {
|
|
63
|
-
let tmpDir, agentsDir, opencodeJsonPath,
|
|
64
|
-
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
vi.clearAllMocks()
|
|
67
|
-
originalCwd = process.cwd()
|
|
68
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'models-config-test-'))
|
|
69
|
-
agentsDir = path.join(tmpDir, '.agents', 'agents')
|
|
70
|
-
fs.mkdirSync(agentsDir, { recursive: true })
|
|
71
|
-
opencodeJsonPath = path.join(tmpDir, '.opencode', 'opencode.json')
|
|
72
|
-
|
|
73
|
-
fs.mkdirSync(path.dirname(opencodeJsonPath), { recursive: true })
|
|
74
|
-
process.chdir(tmpDir)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
process.chdir(originalCwd)
|
|
79
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('writes build model to agent files', async () => {
|
|
83
|
-
fs.writeFileSync(path.join(agentsDir, 'back-engineer.md'), '---\nname: Back\n---', 'utf-8')
|
|
84
|
-
fs.writeFileSync(path.join(agentsDir, 'front-engineer.md'), '---\nname: Front\n---', 'utf-8')
|
|
85
|
-
|
|
86
|
-
await writeModelsToConfigs({
|
|
87
|
-
planModel: 'plan-model',
|
|
88
|
-
buildModel: 'build-model',
|
|
89
|
-
fastModel: 'fast-model',
|
|
90
|
-
agentsDir,
|
|
91
|
-
preset: {
|
|
92
|
-
roles: {
|
|
93
|
-
build: { agents: ['back-engineer'] },
|
|
94
|
-
fast: { agents: ['front-engineer'] },
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
expect(success).toHaveBeenCalledWith('back-engineer → build-model')
|
|
100
|
-
expect(success).toHaveBeenCalledWith('front-engineer → fast-model')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('reports success when writing configs', async () => {
|
|
104
|
-
const agentFile = path.join(agentsDir, 'back-engineer.md')
|
|
105
|
-
fs.writeFileSync(agentFile, '---\nname: Back\n---', 'utf-8')
|
|
106
|
-
|
|
107
|
-
await writeModelsToConfigs({
|
|
108
|
-
planModel: 'plan-model',
|
|
109
|
-
buildModel: 'build-model',
|
|
110
|
-
fastModel: 'fast-model',
|
|
111
|
-
agentsDir,
|
|
112
|
-
preset: { roles: { build: { agents: ['back-engineer'] }, fast: { agents: [] } } },
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
expect(success).toHaveBeenCalled()
|
|
116
|
-
})
|
|
117
|
-
})
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
|
|
6
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
7
|
+
success: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
import { success } from '../../utils/exec.js'
|
|
11
|
+
import { writeModelToAgent, writeModelsToConfigs } from './write.js'
|
|
12
|
+
|
|
13
|
+
describe('writeModelToAgent()', () => {
|
|
14
|
+
let tmpDir
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks()
|
|
18
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'models-write-test-'))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('adds model field to agent file frontmatter', async () => {
|
|
26
|
+
const filePath = path.join(tmpDir, 'test-agent.md')
|
|
27
|
+
const original = `---
|
|
28
|
+
name: Test Agent
|
|
29
|
+
description: A test agent
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Test Agent`
|
|
33
|
+
fs.writeFileSync(filePath, original, 'utf-8')
|
|
34
|
+
|
|
35
|
+
await writeModelToAgent(filePath, 'anthropic/claude-3-sonnet')
|
|
36
|
+
|
|
37
|
+
const updated = fs.readFileSync(filePath, 'utf-8')
|
|
38
|
+
expect(updated).toContain('model: anthropic/claude-3-sonnet')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('preserves existing frontmatter fields', async () => {
|
|
42
|
+
const filePath = path.join(tmpDir, 'test-agent.md')
|
|
43
|
+
const original = `---
|
|
44
|
+
name: Test Agent
|
|
45
|
+
description: A test agent
|
|
46
|
+
custom_field: custom_value
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# Test Agent`
|
|
50
|
+
fs.writeFileSync(filePath, original, 'utf-8')
|
|
51
|
+
|
|
52
|
+
await writeModelToAgent(filePath, 'test/model')
|
|
53
|
+
|
|
54
|
+
const updated = fs.readFileSync(filePath, 'utf-8')
|
|
55
|
+
expect(updated).toContain('name: Test Agent')
|
|
56
|
+
expect(updated).toContain('description: A test agent')
|
|
57
|
+
expect(updated).toContain('custom_field: custom_value')
|
|
58
|
+
expect(updated).toContain('model: test/model')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('writeModelsToConfigs()', () => {
|
|
63
|
+
let tmpDir, agentsDir, opencodeJsonPath, originalCwd
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.clearAllMocks()
|
|
67
|
+
originalCwd = process.cwd()
|
|
68
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'models-config-test-'))
|
|
69
|
+
agentsDir = path.join(tmpDir, '.agents', 'agents')
|
|
70
|
+
fs.mkdirSync(agentsDir, { recursive: true })
|
|
71
|
+
opencodeJsonPath = path.join(tmpDir, '.opencode', 'opencode.json')
|
|
72
|
+
path.join(tmpDir, '.opencode', 'ensemble.json')
|
|
73
|
+
fs.mkdirSync(path.dirname(opencodeJsonPath), { recursive: true })
|
|
74
|
+
process.chdir(tmpDir)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
process.chdir(originalCwd)
|
|
79
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('writes build model to agent files', async () => {
|
|
83
|
+
fs.writeFileSync(path.join(agentsDir, 'back-engineer.md'), '---\nname: Back\n---', 'utf-8')
|
|
84
|
+
fs.writeFileSync(path.join(agentsDir, 'front-engineer.md'), '---\nname: Front\n---', 'utf-8')
|
|
85
|
+
|
|
86
|
+
await writeModelsToConfigs({
|
|
87
|
+
planModel: 'plan-model',
|
|
88
|
+
buildModel: 'build-model',
|
|
89
|
+
fastModel: 'fast-model',
|
|
90
|
+
agentsDir,
|
|
91
|
+
preset: {
|
|
92
|
+
roles: {
|
|
93
|
+
build: { agents: ['back-engineer'] },
|
|
94
|
+
fast: { agents: ['front-engineer'] },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(success).toHaveBeenCalledWith('back-engineer → build-model')
|
|
100
|
+
expect(success).toHaveBeenCalledWith('front-engineer → fast-model')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('reports success when writing configs', async () => {
|
|
104
|
+
const agentFile = path.join(agentsDir, 'back-engineer.md')
|
|
105
|
+
fs.writeFileSync(agentFile, '---\nname: Back\n---', 'utf-8')
|
|
106
|
+
|
|
107
|
+
await writeModelsToConfigs({
|
|
108
|
+
planModel: 'plan-model',
|
|
109
|
+
buildModel: 'build-model',
|
|
110
|
+
fastModel: 'fast-model',
|
|
111
|
+
agentsDir,
|
|
112
|
+
preset: { roles: { build: { agents: ['back-engineer'] }, fast: { agents: [] } } },
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(success).toHaveBeenCalled()
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -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
|
+
}
|