opencode-onboard 0.0.1 → 0.0.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 +203 -0
- package/content/.opencode/agents/.bootstrap/AGENTS.template.md +130 -126
- package/content/.opencode/agents/back-engineer.md +73 -0
- package/content/.opencode/agents/devops-manager.md +115 -0
- package/content/.opencode/agents/front-engineer.md +73 -0
- package/content/.opencode/agents/infra-engineer.md +73 -0
- package/content/.opencode/agents/quality-engineer.md +75 -0
- package/content/.opencode/agents/security-auditor.md +85 -0
- package/content/.opencode/skills/browser-automation/SKILL.md +63 -0
- package/content/.opencode/skills/ob-userstory-az/SKILL.md +6 -6
- package/content/.opencode/skills/ob-userstory-gh/SKILL.md +3 -3
- package/content/AGENTS.md +12 -12
- package/content/DESIGN.md +1 -1
- package/package.json +18 -1
- package/src/index.js +67 -1
- package/src/presets/platforms.json +10 -0
- package/src/presets/skills-providers.json +14 -0
- package/src/steps/__tests__/check-env.test.js +70 -0
- package/src/steps/__tests__/check-platform.test.js +104 -0
- package/src/steps/__tests__/check-rtk.test.js +37 -0
- package/src/steps/__tests__/choose-platform.test.js +38 -0
- package/src/steps/__tests__/choose-team.test.js +105 -0
- package/src/steps/__tests__/clean-ai-files.test.js +62 -0
- package/src/steps/__tests__/copy-content.test.js +62 -0
- package/src/steps/check-env.js +26 -0
- package/src/steps/check-platform.js +80 -0
- package/src/steps/check-rtk.js +20 -0
- package/src/steps/choose-platform.js +22 -0
- package/src/steps/choose-skills-provider.js +56 -0
- package/src/steps/clean-ai-files.js +51 -0
- package/src/steps/copy-content.js +21 -0
- package/src/steps/init-openspec.js +22 -0
- package/src/steps/install-browser.js +65 -0
- package/src/utils/__tests__/copy.test.js +132 -0
- package/src/utils/__tests__/exec.test.js +106 -0
- package/src/utils/copy.js +54 -0
- package/src/utils/exec.js +84 -0
- package/content/.opencode/agents/ob-pullrequest-creator-az.md +0 -332
- package/content/.opencode/agents/ob-pullrequest-creator-gh.md +0 -177
- package/content/.opencode/agents/ob-pullrequest-observer-az.md +0 -248
- package/content/.opencode/agents/ob-pullrequest-observer-gh.md +0 -197
- package/content/.opencode/agents/qa.md +0 -137
package/src/index.js
CHANGED
|
@@ -1,2 +1,68 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { checkEnv } from './steps/check-env.js'
|
|
4
|
+
import { cleanAiFiles } from './steps/clean-ai-files.js'
|
|
5
|
+
import { choosePlatform } from './steps/choose-platform.js'
|
|
6
|
+
import { copyContentStep } from './steps/copy-content.js'
|
|
7
|
+
import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
|
|
8
|
+
import { initOpenspec } from './steps/init-openspec.js'
|
|
9
|
+
import { installBrowser } from './steps/install-browser.js'
|
|
10
|
+
import { checkRtk } from './steps/check-rtk.js'
|
|
11
|
+
import { checkPlatform } from './steps/check-platform.js'
|
|
12
|
+
|
|
13
|
+
console.log()
|
|
14
|
+
console.log(chalk.bold('┌─────────────────────────────────────┐'))
|
|
15
|
+
console.log(chalk.bold('│ opencode-onboard │'))
|
|
16
|
+
console.log(chalk.bold('│ Prepare your codebase for AI agents│'))
|
|
17
|
+
console.log(chalk.bold('└─────────────────────────────────────┘'))
|
|
18
|
+
console.log()
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// 1. Check Node + npm/pnpm
|
|
22
|
+
await checkEnv()
|
|
23
|
+
|
|
24
|
+
// 2. Clean existing AI config files
|
|
25
|
+
await cleanAiFiles()
|
|
26
|
+
|
|
27
|
+
// 3. Choose platform
|
|
28
|
+
const platform = await choosePlatform()
|
|
29
|
+
|
|
30
|
+
// 4. Copy content filtered by platform
|
|
31
|
+
await copyContentStep(platform)
|
|
32
|
+
|
|
33
|
+
// 5. Choose skills provider
|
|
34
|
+
await chooseSkillsProvider()
|
|
35
|
+
|
|
36
|
+
// 6. Init OpenSpec
|
|
37
|
+
await initOpenspec()
|
|
38
|
+
|
|
39
|
+
// 7. Install opencode-browser
|
|
40
|
+
await installBrowser()
|
|
41
|
+
|
|
42
|
+
// 8. Check rtk
|
|
43
|
+
await checkRtk()
|
|
44
|
+
|
|
45
|
+
// 9. Check platform CLI (az or gh)
|
|
46
|
+
await checkPlatform(platform)
|
|
47
|
+
|
|
48
|
+
// Done
|
|
49
|
+
console.log()
|
|
50
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
51
|
+
console.log(chalk.bold.green(' Onboarding complete!'))
|
|
52
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
53
|
+
console.log()
|
|
54
|
+
console.log(' Next step:')
|
|
55
|
+
console.log(chalk.cyan(' Open OpenCode in this project and type: ') + chalk.bold('"init"'))
|
|
56
|
+
console.log()
|
|
57
|
+
console.log(' OpenCode will generate ARCHITECTURE.md and DESIGN.md')
|
|
58
|
+
console.log(' from your actual codebase, then activate the agent team.')
|
|
59
|
+
console.log()
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.name === 'ExitPromptError') {
|
|
62
|
+
console.log()
|
|
63
|
+
console.log(chalk.yellow('Cancelled.'))
|
|
64
|
+
} else {
|
|
65
|
+
console.error(chalk.red('\nUnexpected error:'), err.message)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"label": "ob-skills (default, ships with opencode-onboard)",
|
|
4
|
+
"value": "ob-skills",
|
|
5
|
+
"package": "opencode-onboard",
|
|
6
|
+
"description": "Default skill pack: GitHub and Azure DevOps user stories, pull requests, OpenSpec workflows."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"label": "None, I will add skills manually",
|
|
10
|
+
"value": "none",
|
|
11
|
+
"package": null,
|
|
12
|
+
"description": "Skip skill installation. Add skills to .opencode/skills/ manually."
|
|
13
|
+
}
|
|
14
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
4
|
+
commandExists: vi.fn(),
|
|
5
|
+
header: vi.fn(),
|
|
6
|
+
success: vi.fn(),
|
|
7
|
+
error: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
// Mock execa used directly in check-env.js
|
|
11
|
+
vi.mock('execa', () => ({ execa: vi.fn() }))
|
|
12
|
+
|
|
13
|
+
import { commandExists, error, success } from '../../utils/exec.js'
|
|
14
|
+
import { checkEnv } from '../check-env.js'
|
|
15
|
+
|
|
16
|
+
describe('checkEnv()', () => {
|
|
17
|
+
const originalVersion = process.version
|
|
18
|
+
const originalExit = process.exit
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
process.exit = vi.fn()
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
Object.defineProperty(process, 'version', { value: originalVersion, writable: true })
|
|
27
|
+
process.exit = originalExit
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('exits with error when Node version is < 18', async () => {
|
|
31
|
+
Object.defineProperty(process, 'version', { value: 'v16.0.0', writable: true })
|
|
32
|
+
commandExists.mockResolvedValue(true)
|
|
33
|
+
|
|
34
|
+
await checkEnv()
|
|
35
|
+
|
|
36
|
+
expect(error).toHaveBeenCalledWith(expect.stringContaining('v16.0.0'))
|
|
37
|
+
expect(process.exit).toHaveBeenCalledWith(1)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('succeeds when Node version is >= 18 and pnpm is available', async () => {
|
|
41
|
+
Object.defineProperty(process, 'version', { value: 'v20.0.0', writable: true })
|
|
42
|
+
commandExists.mockImplementation(async (cmd) => cmd === 'pnpm')
|
|
43
|
+
|
|
44
|
+
await checkEnv()
|
|
45
|
+
|
|
46
|
+
expect(process.exit).not.toHaveBeenCalled()
|
|
47
|
+
expect(success).toHaveBeenCalledWith(expect.stringContaining('v20.0.0'))
|
|
48
|
+
expect(success).toHaveBeenCalledWith('pnpm available')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('succeeds when Node version is >= 18 and only npm is available', async () => {
|
|
52
|
+
Object.defineProperty(process, 'version', { value: 'v18.0.0', writable: true })
|
|
53
|
+
commandExists.mockImplementation(async (cmd) => cmd === 'npm')
|
|
54
|
+
|
|
55
|
+
await checkEnv()
|
|
56
|
+
|
|
57
|
+
expect(process.exit).not.toHaveBeenCalled()
|
|
58
|
+
expect(success).toHaveBeenCalledWith('npm available')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('exits when neither npm nor pnpm available', async () => {
|
|
62
|
+
Object.defineProperty(process, 'version', { value: 'v20.0.0', writable: true })
|
|
63
|
+
commandExists.mockResolvedValue(false)
|
|
64
|
+
|
|
65
|
+
await checkEnv()
|
|
66
|
+
|
|
67
|
+
expect(error).toHaveBeenCalledWith(expect.stringContaining('Neither npm nor pnpm'))
|
|
68
|
+
expect(process.exit).toHaveBeenCalledWith(1)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
4
|
+
commandExists: vi.fn(),
|
|
5
|
+
header: vi.fn(),
|
|
6
|
+
success: vi.fn(),
|
|
7
|
+
warn: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
code: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('execa', () => ({
|
|
13
|
+
execa: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
import { execa } from 'execa'
|
|
17
|
+
import { commandExists, success, warn } from '../../utils/exec.js'
|
|
18
|
+
import { checkPlatform } from '../check-platform.js'
|
|
19
|
+
|
|
20
|
+
describe('checkPlatform()', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('github path', () => {
|
|
26
|
+
it('prints success when gh is installed and authenticated', async () => {
|
|
27
|
+
commandExists.mockResolvedValue(true)
|
|
28
|
+
execa.mockResolvedValue({ exitCode: 0, stdout: '' })
|
|
29
|
+
|
|
30
|
+
await checkPlatform('github')
|
|
31
|
+
|
|
32
|
+
expect(success).toHaveBeenCalledWith('GitHub CLI (gh) available')
|
|
33
|
+
expect(success).toHaveBeenCalledWith('GitHub CLI authenticated')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('warns when gh is installed but not authenticated', async () => {
|
|
37
|
+
commandExists.mockResolvedValue(true)
|
|
38
|
+
execa.mockResolvedValue({ exitCode: 1, stdout: '' })
|
|
39
|
+
|
|
40
|
+
await checkPlatform('github')
|
|
41
|
+
|
|
42
|
+
expect(success).toHaveBeenCalledWith('GitHub CLI (gh) available')
|
|
43
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('not authenticated'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('warns when gh is not installed', async () => {
|
|
47
|
+
commandExists.mockResolvedValue(false)
|
|
48
|
+
|
|
49
|
+
await checkPlatform('github')
|
|
50
|
+
|
|
51
|
+
expect(warn).toHaveBeenCalledWith('GitHub CLI (gh) not found.')
|
|
52
|
+
expect(success).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('warns when gh auth status check throws', async () => {
|
|
56
|
+
commandExists.mockResolvedValue(true)
|
|
57
|
+
execa.mockRejectedValue(new Error('spawn error'))
|
|
58
|
+
|
|
59
|
+
await checkPlatform('github')
|
|
60
|
+
|
|
61
|
+
expect(warn).toHaveBeenCalledWith('Could not check gh auth status.')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('azure path', () => {
|
|
66
|
+
it('prints success when az is installed and azure-devops extension present', async () => {
|
|
67
|
+
commandExists.mockResolvedValue(true)
|
|
68
|
+
execa.mockResolvedValue({ exitCode: 0, stdout: 'azure-devops\tsome info' })
|
|
69
|
+
|
|
70
|
+
await checkPlatform('azure')
|
|
71
|
+
|
|
72
|
+
expect(success).toHaveBeenCalledWith('Azure CLI (az) available')
|
|
73
|
+
expect(success).toHaveBeenCalledWith('azure-devops extension installed')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('warns when az is installed but azure-devops extension is missing', async () => {
|
|
77
|
+
commandExists.mockResolvedValue(true)
|
|
78
|
+
execa.mockResolvedValue({ exitCode: 0, stdout: '' })
|
|
79
|
+
|
|
80
|
+
await checkPlatform('azure')
|
|
81
|
+
|
|
82
|
+
expect(success).toHaveBeenCalledWith('Azure CLI (az) available')
|
|
83
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('azure-devops extension not found'))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('warns when az is not installed', async () => {
|
|
87
|
+
commandExists.mockResolvedValue(false)
|
|
88
|
+
|
|
89
|
+
await checkPlatform('azure')
|
|
90
|
+
|
|
91
|
+
expect(warn).toHaveBeenCalledWith('Azure CLI (az) not found.')
|
|
92
|
+
expect(success).not.toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('warns when extension check throws', async () => {
|
|
96
|
+
commandExists.mockResolvedValue(true)
|
|
97
|
+
execa.mockRejectedValue(new Error('spawn error'))
|
|
98
|
+
|
|
99
|
+
await checkPlatform('azure')
|
|
100
|
+
|
|
101
|
+
expect(warn).toHaveBeenCalledWith('Could not check azure-devops extension. Run:')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
4
|
+
commandExists: vi.fn(),
|
|
5
|
+
header: vi.fn(),
|
|
6
|
+
success: vi.fn(),
|
|
7
|
+
warn: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
code: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
import { commandExists, success, warn } from '../../utils/exec.js'
|
|
13
|
+
import { checkRtk } from '../check-rtk.js'
|
|
14
|
+
|
|
15
|
+
describe('checkRtk()', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('prints success when rtk is available', async () => {
|
|
21
|
+
commandExists.mockResolvedValue(true)
|
|
22
|
+
|
|
23
|
+
await checkRtk()
|
|
24
|
+
|
|
25
|
+
expect(success).toHaveBeenCalledWith('rtk is available')
|
|
26
|
+
expect(warn).not.toHaveBeenCalled()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('prints warning with install instructions when rtk is not found', async () => {
|
|
30
|
+
commandExists.mockResolvedValue(false)
|
|
31
|
+
|
|
32
|
+
await checkRtk()
|
|
33
|
+
|
|
34
|
+
expect(warn).toHaveBeenCalledWith('rtk not found on PATH.')
|
|
35
|
+
expect(success).not.toHaveBeenCalled()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
4
|
+
header: vi.fn(),
|
|
5
|
+
success: vi.fn(),
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
9
|
+
select: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
import { select } from '@inquirer/prompts'
|
|
13
|
+
import { success } from '../../utils/exec.js'
|
|
14
|
+
import { choosePlatform } from '../choose-platform.js'
|
|
15
|
+
|
|
16
|
+
describe('choosePlatform()', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns "github" when user selects GitHub', async () => {
|
|
22
|
+
select.mockResolvedValue('github')
|
|
23
|
+
|
|
24
|
+
const result = await choosePlatform()
|
|
25
|
+
|
|
26
|
+
expect(result).toBe('github')
|
|
27
|
+
expect(success).toHaveBeenCalledWith('Platform: GitHub')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns "azure" when user selects Azure DevOps', async () => {
|
|
31
|
+
select.mockResolvedValue('azure')
|
|
32
|
+
|
|
33
|
+
const result = await choosePlatform()
|
|
34
|
+
|
|
35
|
+
expect(result).toBe('azure')
|
|
36
|
+
expect(success).toHaveBeenCalledWith('Platform: Azure DevOps')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
7
|
+
header: vi.fn(),
|
|
8
|
+
success: vi.fn(),
|
|
9
|
+
info: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
13
|
+
checkbox: vi.fn(),
|
|
14
|
+
input: vi.fn(),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
import { checkbox, input } from '@inquirer/prompts'
|
|
18
|
+
import { info } from '../../utils/exec.js'
|
|
19
|
+
|
|
20
|
+
describe('chooseTeam()', () => {
|
|
21
|
+
let tmpDir
|
|
22
|
+
let originalCwd
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'ob-team-test-'))
|
|
26
|
+
originalCwd = process.cwd()
|
|
27
|
+
process.chdir(tmpDir)
|
|
28
|
+
vi.clearAllMocks()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
process.chdir(originalCwd)
|
|
33
|
+
await fse.remove(tmpDir)
|
|
34
|
+
vi.resetModules()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns empty array and skips when no agents selected', async () => {
|
|
38
|
+
checkbox.mockResolvedValue([])
|
|
39
|
+
// single empty input to exit the custom loop
|
|
40
|
+
input.mockResolvedValue('')
|
|
41
|
+
|
|
42
|
+
// Dynamic import so process.cwd() is captured at call time
|
|
43
|
+
const { chooseTeam } = await import('../choose-team.js')
|
|
44
|
+
const result = await chooseTeam()
|
|
45
|
+
|
|
46
|
+
expect(result).toEqual([])
|
|
47
|
+
expect(info).toHaveBeenCalledWith('No agents selected, skipping team setup.')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('creates agent files for selected preset agents', async () => {
|
|
51
|
+
checkbox.mockResolvedValue(['frontend', 'backend'])
|
|
52
|
+
input.mockResolvedValue('') // no custom agents
|
|
53
|
+
|
|
54
|
+
const { chooseTeam } = await import('../choose-team.js')
|
|
55
|
+
const result = await chooseTeam()
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual(['frontend', 'backend'])
|
|
58
|
+
|
|
59
|
+
const frontendPath = path.join(tmpDir, '.opencode', 'agents', 'frontend.md')
|
|
60
|
+
const backendPath = path.join(tmpDir, '.opencode', 'agents', 'backend.md')
|
|
61
|
+
expect(await fse.pathExists(frontendPath)).toBe(true)
|
|
62
|
+
expect(await fse.pathExists(backendPath)).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('creates agent file for custom agent name', async () => {
|
|
66
|
+
checkbox.mockResolvedValue([])
|
|
67
|
+
input.mockResolvedValueOnce('devops').mockResolvedValueOnce('') // one custom, then stop
|
|
68
|
+
|
|
69
|
+
const { chooseTeam } = await import('../choose-team.js')
|
|
70
|
+
const result = await chooseTeam()
|
|
71
|
+
|
|
72
|
+
expect(result).toContain('devops')
|
|
73
|
+
const devopsPath = path.join(tmpDir, '.opencode', 'agents', 'devops.md')
|
|
74
|
+
expect(await fse.pathExists(devopsPath)).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('normalises custom agent name (lowercase, spaces to dashes)', async () => {
|
|
78
|
+
checkbox.mockResolvedValue([])
|
|
79
|
+
input.mockResolvedValueOnce('My Agent').mockResolvedValueOnce('')
|
|
80
|
+
|
|
81
|
+
const { chooseTeam } = await import('../choose-team.js')
|
|
82
|
+
const result = await chooseTeam()
|
|
83
|
+
|
|
84
|
+
expect(result).toContain('my-agent')
|
|
85
|
+
const agentPath = path.join(tmpDir, '.opencode', 'agents', 'my-agent.md')
|
|
86
|
+
expect(await fse.pathExists(agentPath)).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('skips agent file creation if it already exists', async () => {
|
|
90
|
+
checkbox.mockResolvedValue(['frontend'])
|
|
91
|
+
input.mockResolvedValue('')
|
|
92
|
+
|
|
93
|
+
const agentsDir = path.join(tmpDir, '.opencode', 'agents')
|
|
94
|
+
await fse.ensureDir(agentsDir)
|
|
95
|
+
await fse.writeFile(path.join(agentsDir, 'frontend.md'), 'existing content')
|
|
96
|
+
|
|
97
|
+
const { chooseTeam } = await import('../choose-team.js')
|
|
98
|
+
await chooseTeam()
|
|
99
|
+
|
|
100
|
+
// File should still have original content (not overwritten)
|
|
101
|
+
const content = await fse.readFile(path.join(agentsDir, 'frontend.md'), 'utf-8')
|
|
102
|
+
expect(content).toBe('existing content')
|
|
103
|
+
expect(info).toHaveBeenCalledWith('frontend.md already exists, skipping')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
4
|
+
header: vi.fn(),
|
|
5
|
+
success: vi.fn(),
|
|
6
|
+
warn: vi.fn(),
|
|
7
|
+
info: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
vi.mock('../../utils/copy.js', () => ({
|
|
11
|
+
findAiFiles: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock('fs-extra', () => ({
|
|
15
|
+
default: { remove: vi.fn() },
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
19
|
+
confirm: vi.fn(),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
import { findAiFiles } from '../../utils/copy.js'
|
|
23
|
+
import { success, warn } from '../../utils/exec.js'
|
|
24
|
+
import fse from 'fs-extra'
|
|
25
|
+
import { confirm } from '@inquirer/prompts'
|
|
26
|
+
import { cleanAiFiles } from '../clean-ai-files.js'
|
|
27
|
+
|
|
28
|
+
describe('cleanAiFiles()', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('prints success when no AI files are found', async () => {
|
|
34
|
+
findAiFiles.mockResolvedValue([])
|
|
35
|
+
|
|
36
|
+
await cleanAiFiles()
|
|
37
|
+
|
|
38
|
+
expect(success).toHaveBeenCalledWith('No existing AI config files found')
|
|
39
|
+
expect(confirm).not.toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('deletes files when user confirms', async () => {
|
|
43
|
+
findAiFiles.mockResolvedValue(['/proj/AGENTS.md', '/proj/CLAUDE.md'])
|
|
44
|
+
confirm.mockResolvedValue(true)
|
|
45
|
+
|
|
46
|
+
await cleanAiFiles()
|
|
47
|
+
|
|
48
|
+
expect(fse.remove).toHaveBeenCalledWith('/proj/AGENTS.md')
|
|
49
|
+
expect(fse.remove).toHaveBeenCalledWith('/proj/CLAUDE.md')
|
|
50
|
+
expect(success).toHaveBeenCalledWith('Removed existing AI config files')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('skips deletion when user declines', async () => {
|
|
54
|
+
findAiFiles.mockResolvedValue(['/proj/AGENTS.md'])
|
|
55
|
+
confirm.mockResolvedValue(false)
|
|
56
|
+
|
|
57
|
+
await cleanAiFiles()
|
|
58
|
+
|
|
59
|
+
expect(fse.remove).not.toHaveBeenCalled()
|
|
60
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Skipped'))
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
4
|
+
header: vi.fn(),
|
|
5
|
+
success: vi.fn(),
|
|
6
|
+
error: vi.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
vi.mock('../../utils/copy.js', () => ({
|
|
10
|
+
copyContent: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
import { copyContent } from '../../utils/copy.js'
|
|
14
|
+
import { success, error } from '../../utils/exec.js'
|
|
15
|
+
import { copyContentStep } from '../copy-content.js'
|
|
16
|
+
|
|
17
|
+
describe('copyContentStep()', () => {
|
|
18
|
+
const originalExit = process.exit
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
process.exit = vi.fn()
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.exit = originalExit
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('calls copyContent with the correct platform and prints success', async () => {
|
|
30
|
+
copyContent.mockResolvedValue(undefined)
|
|
31
|
+
|
|
32
|
+
await copyContentStep('github')
|
|
33
|
+
|
|
34
|
+
expect(copyContent).toHaveBeenCalledWith(
|
|
35
|
+
expect.stringContaining('content'),
|
|
36
|
+
process.cwd(),
|
|
37
|
+
'github'
|
|
38
|
+
)
|
|
39
|
+
expect(success).toHaveBeenCalledWith('Files copied to project root')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('calls copyContent with azure platform', async () => {
|
|
43
|
+
copyContent.mockResolvedValue(undefined)
|
|
44
|
+
|
|
45
|
+
await copyContentStep('azure')
|
|
46
|
+
|
|
47
|
+
expect(copyContent).toHaveBeenCalledWith(
|
|
48
|
+
expect.stringContaining('content'),
|
|
49
|
+
process.cwd(),
|
|
50
|
+
'azure'
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('calls process.exit(1) when copyContent throws', async () => {
|
|
55
|
+
copyContent.mockRejectedValue(new Error('disk full'))
|
|
56
|
+
|
|
57
|
+
await copyContentStep('github')
|
|
58
|
+
|
|
59
|
+
expect(error).toHaveBeenCalledWith(expect.stringContaining('disk full'))
|
|
60
|
+
expect(process.exit).toHaveBeenCalledWith(1)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { commandExists, error, header, success } from '../utils/exec.js'
|
|
2
|
+
|
|
3
|
+
export async function checkEnv() {
|
|
4
|
+
header('Step 1, Checking environment')
|
|
5
|
+
|
|
6
|
+
// Node version
|
|
7
|
+
const nodeVersion = process.version
|
|
8
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0], 10)
|
|
9
|
+
if (major < 18) {
|
|
10
|
+
error(`Node.js ${nodeVersion} detected. Version 18+ is required.`)
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
success(`Node.js ${nodeVersion}`)
|
|
14
|
+
|
|
15
|
+
// npm or pnpm
|
|
16
|
+
const hasPnpm = await commandExists('pnpm')
|
|
17
|
+
const hasNpm = await commandExists('npm')
|
|
18
|
+
|
|
19
|
+
if (!hasPnpm && !hasNpm) {
|
|
20
|
+
error('Neither npm nor pnpm found. Please install Node.js from https://nodejs.org')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (hasPnpm) success('pnpm available')
|
|
25
|
+
else success('npm available')
|
|
26
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { code, commandExists, header, info, success, warn } from '../utils/exec.js'
|
|
3
|
+
|
|
4
|
+
export async function checkPlatform(platform) {
|
|
5
|
+
if (platform === 'azure') {
|
|
6
|
+
await checkAzure()
|
|
7
|
+
} else {
|
|
8
|
+
await checkGithub()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function checkAzure() {
|
|
13
|
+
header('Step 9, Checking Azure DevOps CLI')
|
|
14
|
+
|
|
15
|
+
// Check az is installed
|
|
16
|
+
const hasAz = await commandExists('az')
|
|
17
|
+
if (!hasAz) {
|
|
18
|
+
warn('Azure CLI (az) not found.')
|
|
19
|
+
info('Install from: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli')
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
success('Azure CLI (az) available')
|
|
23
|
+
|
|
24
|
+
// Check az devops extension
|
|
25
|
+
try {
|
|
26
|
+
const result = await execa('az', ['extension', 'list', '--query', "[?name=='azure-devops']", '-o', 'tsv'], {
|
|
27
|
+
reject: false,
|
|
28
|
+
})
|
|
29
|
+
const hasExtension = result.stdout && result.stdout.includes('azure-devops')
|
|
30
|
+
|
|
31
|
+
if (hasExtension) {
|
|
32
|
+
success('azure-devops extension installed')
|
|
33
|
+
} else {
|
|
34
|
+
warn('azure-devops extension not found. Run:')
|
|
35
|
+
code([
|
|
36
|
+
'az extension add --name azure-devops',
|
|
37
|
+
'az config set extension.dynamic_install_allow_preview=true',
|
|
38
|
+
'az login',
|
|
39
|
+
'az devops login --organization https://dev.azure.com/<your-org>',
|
|
40
|
+
])
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
warn('Could not check azure-devops extension. Run:')
|
|
44
|
+
code([
|
|
45
|
+
'az extension add --name azure-devops',
|
|
46
|
+
'az config set extension.dynamic_install_allow_preview=true',
|
|
47
|
+
'az login',
|
|
48
|
+
'az devops login --organization https://dev.azure.com/<your-org>',
|
|
49
|
+
])
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function checkGithub() {
|
|
54
|
+
header('Step 9, Checking GitHub CLI')
|
|
55
|
+
|
|
56
|
+
const hasGh = await commandExists('gh')
|
|
57
|
+
|
|
58
|
+
if (hasGh) {
|
|
59
|
+
success('GitHub CLI (gh) available')
|
|
60
|
+
|
|
61
|
+
// Check auth status
|
|
62
|
+
try {
|
|
63
|
+
const result = await execa('gh', ['auth', 'status'], { reject: false })
|
|
64
|
+
if (result.exitCode === 0) {
|
|
65
|
+
success('GitHub CLI authenticated')
|
|
66
|
+
} else {
|
|
67
|
+
warn('GitHub CLI not authenticated. Run:')
|
|
68
|
+
code(['gh auth login'])
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
warn('Could not check gh auth status.')
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
warn('GitHub CLI (gh) not found.')
|
|
75
|
+
info('Install from: https://cli.github.com')
|
|
76
|
+
console.log()
|
|
77
|
+
info('After installing, authenticate with:')
|
|
78
|
+
code(['gh auth login'])
|
|
79
|
+
}
|
|
80
|
+
}
|