opencode-onboard 0.4.3 → 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.
Files changed (35) hide show
  1. package/README.md +41 -40
  2. package/content/.agents/agents/devops-manager.md +123 -123
  3. package/content/.agents/skills/ob-default/SKILL.md +25 -21
  4. package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
  5. package/content/.agents/skills/ob-global/SKILL.md +92 -84
  6. package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
  7. package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
  8. package/content/.opencode/commands/create-engineer.md +109 -0
  9. package/content/.opencode/plugins/session-log.js +523 -519
  10. package/content/AGENTS.md +23 -21
  11. package/package.json +1 -1
  12. package/src/commands/wizard.js +124 -113
  13. package/src/presets/browser.json +22 -18
  14. package/src/presets/optimization.json +27 -22
  15. package/src/steps/browser/browser.test.js +115 -81
  16. package/src/steps/browser/index.js +62 -54
  17. package/src/steps/clean/index.js +108 -107
  18. package/src/steps/metadata/index.js +63 -62
  19. package/src/steps/models/format.js +61 -60
  20. package/src/steps/models/write.test.js +117 -117
  21. package/src/steps/openspec/ensemble.test.js +79 -79
  22. package/src/steps/openspec/index.js +121 -32
  23. package/src/steps/openspec/index.test.js +63 -0
  24. package/src/steps/optimization/caveman.js +34 -29
  25. package/src/steps/optimization/codegraph.js +52 -0
  26. package/src/steps/optimization/global.js +88 -64
  27. package/src/steps/optimization/global.test.js +99 -0
  28. package/src/steps/optimization/index.js +109 -101
  29. package/src/steps/optimization/optimization.test.js +101 -93
  30. package/src/steps/optimization/quota.js +84 -84
  31. package/src/steps/source/source.test.js +124 -124
  32. package/src/utils/__tests__/copy.test.js +117 -117
  33. package/src/utils/exec-spinner.js +47 -47
  34. package/src/utils/exec.js +134 -131
  35. 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, ensembleJsonPath, 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
- ensembleJsonPath = 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
+ 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, vi, 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
+ 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 'execa'
2
- import path from 'node:path'
3
- import { error, header, success, warn } from '../../utils/exec.js'
4
- import { APPLY_TARGETS, patchApplyFile } from './ensemble.js'
5
-
6
- export async function initOpenspec() {
7
- header('Step 6, Initializing OpenSpec');
8
-
9
- try {
10
- const result = await execa('npx', ['@fission-ai/openspec', 'init', '--tools', 'opencode', '--force'], {
11
- cwd: process.cwd(),
12
- stdio: 'pipe',
13
- reject: false,
14
- });
15
-
16
- if (result.exitCode === 0) success('OpenSpec initialized');
17
- else warn('OpenSpec init exited with non-zero code, check output above');
18
- } catch (err) {
19
- error(`Failed to run openspec init: ${err.message}`);
20
- }
21
-
22
- for (const rel of APPLY_TARGETS) {
23
- const abs = path.join(process.cwd(), rel);
24
- try {
25
- const res = await patchApplyFile(abs);
26
- if (res.ok) success(`Patched ensemble implementation section in ${rel}`);
27
- else warn(`Could not patch ${rel} (${res.reason})`);
28
- } catch (err) {
29
- warn(`Could not patch ${rel}: ${err.message}`);
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
- try {
10
- info('Installing caveman via npx skills')
11
- const result = await execa('npx', ['skills', 'add', 'JuliusBrussee/caveman/caveman', '-a', 'opencode', '--yes'], {
12
- reject: false,
13
- timeout: 600000,
14
- stdio: 'pipe',
15
- })
16
-
17
- if (result.exitCode === 0) {
18
- success('caveman installed')
19
- return { optedIn: true, installed: true }
20
- }
21
-
22
- if (result.stderr?.trim()) warn(result.stderr.trim().split('\n').slice(-3).join('\n'))
23
- warn('caveman install exited with non-zero code')
24
- return { optedIn: true, installed: false }
25
- } catch (err) {
26
- error(`Failed to install caveman: ${err.message}`)
27
- return { optedIn: true, installed: false }
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
+ }