opencode-onboard 0.0.5 → 0.1.1
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 +44 -33
- package/content/{.opencode → .agents}/agents/.bootstrap/AGENTS.template.md +7 -7
- package/content/{.opencode → .agents}/agents/back-engineer.md +18 -17
- package/content/{.opencode → .agents}/agents/devops-manager.md +22 -29
- package/content/{.opencode → .agents}/agents/front-engineer.md +18 -18
- package/content/{.opencode → .agents}/agents/infra-engineer.md +19 -18
- package/content/{.opencode → .agents}/agents/quality-engineer.md +17 -18
- package/content/{.opencode → .agents}/agents/security-auditor.md +19 -20
- package/content/.opencode/package-lock.json +3 -3
- package/content/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/index.js +105 -67
- package/src/steps/__tests__/clean-ai-files.test.js +44 -30
- package/src/steps/check-platform.js +2 -2
- package/src/steps/check-rtk.js +1 -1
- package/src/steps/choose-models.js +141 -0
- package/src/steps/choose-skills-provider.js +51 -32
- package/src/steps/clean-ai-files.js +9 -9
- package/src/steps/copy-content.js +1 -1
- package/src/steps/install-browser.js +19 -27
- package/src/utils/__tests__/copy.test.js +0 -22
- package/src/utils/__tests__/exec.test.js +6 -4
- package/src/utils/copy.js +1 -1
- package/src/utils/exec.js +161 -84
- package/src/utils/models-cache.js +101 -0
- package/content/.opencode/agents/.bootstrap/CUSTOM-AGENT.template.md +0 -24
- package/content/.opencode/commands/.gitkeep +0 -0
- package/src/presets/skills-providers.json +0 -14
- package/src/steps/__tests__/choose-team.test.js +0 -105
- /package/content/{.opencode → .agents}/skills/browser-automation/SKILL.md +0 -0
- /package/content/{.opencode → .agents}/skills/ob-userstory-az/SKILL.md +0 -0
- /package/content/{.opencode → .agents}/skills/ob-userstory-gh/SKILL.md +0 -0
package/src/index.js
CHANGED
|
@@ -1,68 +1,106 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
import { checkEnv } from './steps/check-env.js'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
console.
|
|
15
|
-
console.log(
|
|
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
|
-
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
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { checkEnv } from './steps/check-env.js'
|
|
4
|
+
import { checkPlatform } from './steps/check-platform.js'
|
|
5
|
+
import { checkRtk } from './steps/check-rtk.js'
|
|
6
|
+
import { chooseModels } from './steps/choose-models.js'
|
|
7
|
+
import { choosePlatform } from './steps/choose-platform.js'
|
|
8
|
+
import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
|
|
9
|
+
import { cleanAiFiles } from './steps/clean-ai-files.js'
|
|
10
|
+
import { copyContentStep } from './steps/copy-content.js'
|
|
11
|
+
import { initOpenspec } from './steps/init-openspec.js'
|
|
12
|
+
import { installBrowser } from './steps/install-browser.js'
|
|
13
|
+
|
|
14
|
+
if (process.stdout.isTTY) console.clear()
|
|
15
|
+
console.log()
|
|
16
|
+
const logo = chalk.hex('#fe3d57')
|
|
17
|
+
const bannerLines = [
|
|
18
|
+
logo(' ▒▒▒▒▒▒▒ '),
|
|
19
|
+
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
|
|
20
|
+
logo(' ▒▒▓ ▓▒▓ '),
|
|
21
|
+
logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
|
|
22
|
+
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
23
|
+
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
|
|
24
|
+
logo(' ▓▒▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒▒░░▒▒▒▒▓▓ '),
|
|
25
|
+
logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
|
|
26
|
+
logo(' ▓▒▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
|
|
27
|
+
logo(' ▓▒▒▒▒▒▒░▓▒▒▓▒▓▒▒▒▒▒▒▒▒▒▓▓ '),
|
|
28
|
+
logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
29
|
+
logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
|
|
30
|
+
'',
|
|
31
|
+
chalk.bold(' 🧰 opencode-onboard'),
|
|
32
|
+
chalk.dim(' Prepare your codebase for AI agents'),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for (const line of bannerLines) console.log(line)
|
|
36
|
+
console.log()
|
|
37
|
+
console.log(' This tool will set up your project with a team of AI agents,')
|
|
38
|
+
console.log(' install skills, select models, and configure OpenCode.')
|
|
39
|
+
console.log()
|
|
40
|
+
|
|
41
|
+
// Only wait for Enter in a real interactive TTY
|
|
42
|
+
if (process.stdin.isTTY) {
|
|
43
|
+
console.log(chalk.bold(' Press Enter to begin...'))
|
|
44
|
+
console.log()
|
|
45
|
+
await new Promise(resolve => {
|
|
46
|
+
process.stdin.resume()
|
|
47
|
+
process.stdin.once('data', () => {
|
|
48
|
+
process.stdin.pause()
|
|
49
|
+
resolve()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
68
52
|
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 1. Check Node + pnpm
|
|
57
|
+
await checkEnv()
|
|
58
|
+
|
|
59
|
+
// 2. Clean existing AI config files
|
|
60
|
+
await cleanAiFiles()
|
|
61
|
+
|
|
62
|
+
// 3. Choose platform
|
|
63
|
+
const platform = await choosePlatform()
|
|
64
|
+
|
|
65
|
+
// 4. Check platform CLI (az or gh)
|
|
66
|
+
await checkPlatform(platform)
|
|
67
|
+
|
|
68
|
+
// 5. Copy content
|
|
69
|
+
await copyContentStep(platform)
|
|
70
|
+
|
|
71
|
+
// 6. Init OpenSpec
|
|
72
|
+
await initOpenspec()
|
|
73
|
+
|
|
74
|
+
// 7. Install skills
|
|
75
|
+
await chooseSkillsProvider()
|
|
76
|
+
|
|
77
|
+
// 8. Choose models
|
|
78
|
+
await chooseModels()
|
|
79
|
+
|
|
80
|
+
// 9. Check RTK
|
|
81
|
+
await checkRtk()
|
|
82
|
+
|
|
83
|
+
// 10. Install opencode-browser
|
|
84
|
+
await installBrowser()
|
|
85
|
+
|
|
86
|
+
// Done
|
|
87
|
+
console.log()
|
|
88
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
89
|
+
console.log(chalk.bold.green(' Onboarding complete!'))
|
|
90
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
91
|
+
console.log()
|
|
92
|
+
console.log(' Next step:')
|
|
93
|
+
console.log(chalk.hex('#fe3d57')(' Open OpenCode in this project and type: ') + chalk.bold('"init"'))
|
|
94
|
+
console.log()
|
|
95
|
+
console.log(' OpenCode will generate ARCHITECTURE.md and DESIGN.md')
|
|
96
|
+
console.log(' from your actual codebase, then activate the agent team.')
|
|
97
|
+
console.log()
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err.name === 'ExitPromptError') {
|
|
100
|
+
console.log()
|
|
101
|
+
console.log(chalk.yellow('Cancelled.'))
|
|
102
|
+
} else {
|
|
103
|
+
console.error(chalk.red('\nUnexpected error:'), err.message)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,62 +1,76 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
2
5
|
|
|
3
6
|
vi.mock('../../utils/exec.js', () => ({
|
|
4
7
|
header: vi.fn(),
|
|
5
8
|
success: vi.fn(),
|
|
6
9
|
warn: vi.fn(),
|
|
7
10
|
info: vi.fn(),
|
|
11
|
+
prompt: vi.fn(),
|
|
8
12
|
}))
|
|
9
13
|
|
|
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
14
|
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
15
|
|
|
28
16
|
describe('cleanAiFiles()', () => {
|
|
29
|
-
|
|
17
|
+
let tmpDir
|
|
18
|
+
let originalCwd
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'ob-clean-test-'))
|
|
22
|
+
originalCwd = process.cwd()
|
|
23
|
+
process.chdir(tmpDir)
|
|
30
24
|
vi.clearAllMocks()
|
|
25
|
+
vi.resetModules()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
process.chdir(originalCwd)
|
|
30
|
+
await fse.remove(tmpDir)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
it('prints success when no AI files are found', async () => {
|
|
34
|
-
|
|
34
|
+
const { cleanAiFiles } = await import('../clean-ai-files.js')
|
|
35
|
+
|
|
36
|
+
// Simulate immediate Enter key
|
|
37
|
+
const stdinPush = () => process.stdin.emit('data', '\n')
|
|
38
|
+
setTimeout(stdinPush, 10)
|
|
35
39
|
|
|
36
40
|
await cleanAiFiles()
|
|
37
41
|
|
|
38
42
|
expect(success).toHaveBeenCalledWith('No existing AI config files found')
|
|
39
|
-
expect(confirm).not.toHaveBeenCalled()
|
|
40
43
|
})
|
|
41
44
|
|
|
42
|
-
it('
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
it('removes found AI files after Enter', async () => {
|
|
46
|
+
await fse.writeFile(path.join(tmpDir, 'AGENTS.md'), '# agents')
|
|
47
|
+
await fse.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# claude')
|
|
48
|
+
|
|
49
|
+
const { cleanAiFiles } = await import('../clean-ai-files.js')
|
|
50
|
+
|
|
51
|
+
setTimeout(() => process.stdin.emit('data', '\n'), 10)
|
|
45
52
|
|
|
46
53
|
await cleanAiFiles()
|
|
47
54
|
|
|
48
|
-
expect(fse.
|
|
49
|
-
expect(fse.
|
|
55
|
+
expect(await fse.pathExists(path.join(tmpDir, 'AGENTS.md'))).toBe(false)
|
|
56
|
+
expect(await fse.pathExists(path.join(tmpDir, 'CLAUDE.md'))).toBe(false)
|
|
50
57
|
expect(success).toHaveBeenCalledWith('Removed existing AI config files')
|
|
51
58
|
})
|
|
52
59
|
|
|
53
|
-
it('
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
it('removes .agents sub-entries but preserves .agents/skills', async () => {
|
|
61
|
+
const agentsDir = path.join(tmpDir, '.agents')
|
|
62
|
+
await fse.ensureDir(path.join(agentsDir, 'agents'))
|
|
63
|
+
await fse.ensureDir(path.join(agentsDir, 'skills', 'my-skill'))
|
|
64
|
+
await fse.writeFile(path.join(agentsDir, 'agents', 'front-engineer.md'), 'agent')
|
|
65
|
+
await fse.writeFile(path.join(agentsDir, 'skills', 'my-skill', 'SKILL.md'), 'skill')
|
|
66
|
+
|
|
67
|
+
const { cleanAiFiles } = await import('../clean-ai-files.js')
|
|
68
|
+
|
|
69
|
+
setTimeout(() => process.stdin.emit('data', '\n'), 10)
|
|
56
70
|
|
|
57
71
|
await cleanAiFiles()
|
|
58
72
|
|
|
59
|
-
expect(fse.
|
|
60
|
-
expect(
|
|
73
|
+
expect(await fse.pathExists(path.join(agentsDir, 'agents'))).toBe(false)
|
|
74
|
+
expect(await fse.pathExists(path.join(agentsDir, 'skills', 'my-skill', 'SKILL.md'))).toBe(true)
|
|
61
75
|
})
|
|
62
76
|
})
|
|
@@ -10,7 +10,7 @@ export async function checkPlatform(platform) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
async function checkAzure() {
|
|
13
|
-
header('Step
|
|
13
|
+
header('Step 4, Checking Azure DevOps CLI')
|
|
14
14
|
|
|
15
15
|
// Check az is installed
|
|
16
16
|
const hasAz = await commandExists('az')
|
|
@@ -51,7 +51,7 @@ async function checkAzure() {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async function checkGithub() {
|
|
54
|
-
header('Step
|
|
54
|
+
header('Step 4, Checking GitHub CLI')
|
|
55
55
|
|
|
56
56
|
const hasGh = await commandExists('gh')
|
|
57
57
|
|
package/src/steps/check-rtk.js
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { search } from '@inquirer/prompts'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { header, info, success, warn } from '../utils/exec.js'
|
|
5
|
+
import { fetchModels } from '../utils/models-cache.js'
|
|
6
|
+
|
|
7
|
+
const COST_TIER = (input) => {
|
|
8
|
+
if (input === undefined || input === null) return ''
|
|
9
|
+
if (input < 1) return ' [$]'
|
|
10
|
+
if (input <= 10) return ' [$$]'
|
|
11
|
+
return ' [$$$]'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Use canonical cost for the tier badge so all providers of the same model
|
|
15
|
+
// show the same tier (e.g. github-copilot $0 subscription shows [$$] not [$])
|
|
16
|
+
const COST_TIER_DISPLAY = (cost, canonicalCost) =>
|
|
17
|
+
COST_TIER(canonicalCost !== undefined ? canonicalCost : cost)
|
|
18
|
+
|
|
19
|
+
function formatPrice(price) {
|
|
20
|
+
if (price === undefined || price === null) return '?'
|
|
21
|
+
if (price === 0) return '$0 (subscription)'
|
|
22
|
+
return `$${price}/M`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildDisplayModels(rawModels) {
|
|
26
|
+
return rawModels.map(m => {
|
|
27
|
+
const priceStr = formatPrice(m.cost)
|
|
28
|
+
const canonicalNote = m.canonicalCost !== undefined
|
|
29
|
+
? ` · official price: ${formatPrice(m.canonicalCost)}/M`
|
|
30
|
+
: ''
|
|
31
|
+
return {
|
|
32
|
+
...m,
|
|
33
|
+
label: `${m.name}${COST_TIER_DISPLAY(m.cost, m.canonicalCost)}, ${m.id}`,
|
|
34
|
+
description: `${priceStr}${canonicalNote} · context: ${m.context ? (m.context / 1000) + 'k' : '?'}`,
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function pickModel(message, models) {
|
|
40
|
+
return await search({
|
|
41
|
+
message,
|
|
42
|
+
source: (input) => {
|
|
43
|
+
const q = (input || '').toLowerCase()
|
|
44
|
+
const filtered = q
|
|
45
|
+
? models.filter(m =>
|
|
46
|
+
m.label.toLowerCase().includes(q) ||
|
|
47
|
+
m.id.toLowerCase().includes(q)
|
|
48
|
+
)
|
|
49
|
+
: models
|
|
50
|
+
return filtered.slice(0, 50).map(m => ({
|
|
51
|
+
name: m.label,
|
|
52
|
+
value: m.id,
|
|
53
|
+
description: m.description,
|
|
54
|
+
}))
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function writeModelToAgent(agentFile, modelId) {
|
|
60
|
+
const content = await fse.readFile(agentFile, 'utf-8')
|
|
61
|
+
const updated = content.replace(
|
|
62
|
+
/^(---\n[\s\S]*?)\n---/m,
|
|
63
|
+
`$1\nmodel: ${modelId}\n---`
|
|
64
|
+
)
|
|
65
|
+
await fse.writeFile(agentFile, updated, 'utf-8')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function chooseModels() {
|
|
69
|
+
header('Step 8, Choose models')
|
|
70
|
+
|
|
71
|
+
info('Fetching available models from models.dev...')
|
|
72
|
+
const { models: rawModels, source } = await fetchModels()
|
|
73
|
+
|
|
74
|
+
if (!rawModels) {
|
|
75
|
+
warn('Could not fetch models (offline and no cache). Skipping model selection.')
|
|
76
|
+
warn('Set models later in .agents/agents/<name>.md and .opencode/opencode.json')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (source === 'stale-cache') {
|
|
81
|
+
warn('Network unavailable, using cached model list (may be outdated).')
|
|
82
|
+
} else if (source === 'cache') {
|
|
83
|
+
info('Using cached model list (refreshes weekly).')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const models = buildDisplayModels(rawModels)
|
|
87
|
+
success(`${models.length} models available`)
|
|
88
|
+
console.log()
|
|
89
|
+
info('Cost indicators: [$] cheap [$$] mid [$$$] expensive')
|
|
90
|
+
info('Type to search. Change selections later in .agents/agents/ and .opencode/opencode.json')
|
|
91
|
+
console.log()
|
|
92
|
+
|
|
93
|
+
// Plan model
|
|
94
|
+
info('PLAN model, used by the main agent for proposals, specs, architecture decisions.')
|
|
95
|
+
info('Pick something capable with strong reasoning.')
|
|
96
|
+
const planModel = await pickModel('Plan model:', models)
|
|
97
|
+
console.log()
|
|
98
|
+
|
|
99
|
+
// Build model
|
|
100
|
+
info('BUILD model, used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.')
|
|
101
|
+
info('Pick something capable for implementation work.')
|
|
102
|
+
const buildModel = await pickModel('Build model:', models)
|
|
103
|
+
console.log()
|
|
104
|
+
|
|
105
|
+
// Fast model
|
|
106
|
+
info('FAST model, used by devops-manager for reading issues, classifying PR comments.')
|
|
107
|
+
info('Pick something fast and cheap, no heavy reasoning needed.')
|
|
108
|
+
const fastModel = await pickModel('Fast model:', models)
|
|
109
|
+
console.log()
|
|
110
|
+
|
|
111
|
+
// Write build model to builder agents
|
|
112
|
+
const buildAgents = ['front-engineer', 'back-engineer', 'infra-engineer', 'quality-engineer', 'security-auditor']
|
|
113
|
+
const agentsDir = path.join(process.cwd(), '.agents', 'agents')
|
|
114
|
+
for (const name of buildAgents) {
|
|
115
|
+
const file = path.join(agentsDir, `${name}.md`)
|
|
116
|
+
if (await fse.pathExists(file)) {
|
|
117
|
+
await writeModelToAgent(file, buildModel)
|
|
118
|
+
success(`${name} → ${buildModel}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Write fast model to devops-manager
|
|
123
|
+
const devopsFile = path.join(agentsDir, 'devops-manager.md')
|
|
124
|
+
if (await fse.pathExists(devopsFile)) {
|
|
125
|
+
await writeModelToAgent(devopsFile, fastModel)
|
|
126
|
+
success(`devops-manager → ${fastModel}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Write plan model to opencode.json
|
|
130
|
+
const opencodeJsonPath = path.join(process.cwd(), '.opencode', 'opencode.json')
|
|
131
|
+
if (await fse.pathExists(opencodeJsonPath)) {
|
|
132
|
+
const config = await fse.readJson(opencodeJsonPath)
|
|
133
|
+
config.model = planModel
|
|
134
|
+
await fse.writeJson(opencodeJsonPath, config, { spaces: 2 })
|
|
135
|
+
success(`plan model → ${planModel} (written to .opencode/opencode.json)`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log()
|
|
139
|
+
warn('Make sure you have API access to the selected models.')
|
|
140
|
+
warn('Change them anytime in .agents/agents/<name>.md and .opencode/opencode.json')
|
|
141
|
+
}
|
|
@@ -1,56 +1,75 @@
|
|
|
1
1
|
import { select } from '@inquirer/prompts'
|
|
2
|
+
import { execa } from 'execa'
|
|
2
3
|
import fse from 'fs-extra'
|
|
3
4
|
import path from 'path'
|
|
4
5
|
import { fileURLToPath } from 'url'
|
|
5
6
|
import { header, info, success, warn } from '../utils/exec.js'
|
|
6
7
|
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const
|
|
9
|
-
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../content/.opencode/skills')
|
|
9
|
+
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../content/.agents/skills')
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
async function installObSkills() {
|
|
12
|
+
const destSkillsDir = path.join(process.cwd(), '.agents', 'skills')
|
|
13
|
+
await fse.ensureDir(destSkillsDir)
|
|
14
|
+
|
|
15
|
+
const skills = await fse.readdir(CONTENT_SKILLS_DIR)
|
|
16
|
+
for (const skill of skills) {
|
|
17
|
+
const src = path.join(CONTENT_SKILLS_DIR, skill)
|
|
18
|
+
const dest = path.join(destSkillsDir, skill)
|
|
19
|
+
const stat = await fse.stat(src)
|
|
20
|
+
if (!stat.isDirectory()) continue
|
|
21
|
+
if (await fse.pathExists(dest)) {
|
|
22
|
+
info(`${skill} already exists, skipping`)
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
await fse.copy(src, dest)
|
|
26
|
+
success(`Installed skill: ${skill}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
12
29
|
|
|
13
30
|
export async function chooseSkillsProvider() {
|
|
14
|
-
header('Step
|
|
31
|
+
header('Step 7, Installing skills')
|
|
32
|
+
|
|
33
|
+
// ob-skills are always installed, mandatory
|
|
34
|
+
info('Installing built-in ob-skills...')
|
|
35
|
+
await installObSkills()
|
|
36
|
+
console.log()
|
|
15
37
|
|
|
16
38
|
info('Skills provide platform and tech-specific knowledge to your agents.')
|
|
17
39
|
info('Agents detect and load skills automatically, you never need to specify them.')
|
|
40
|
+
info('You can add more skills on top of the built-in ones.')
|
|
18
41
|
console.log()
|
|
19
42
|
|
|
20
43
|
const selected = await select({
|
|
21
|
-
message: '
|
|
22
|
-
choices:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
message: 'Add additional skills from:',
|
|
45
|
+
choices: [
|
|
46
|
+
{
|
|
47
|
+
name: 'npx skills (vercel-labs/skills)',
|
|
48
|
+
value: 'npx-skills',
|
|
49
|
+
description: 'Install skills from the vercel-labs community skills registry',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'None, built-in skills are enough',
|
|
53
|
+
value: 'none',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
26
56
|
})
|
|
27
57
|
|
|
28
58
|
if (selected === 'none') {
|
|
29
|
-
|
|
30
|
-
return null
|
|
59
|
+
return
|
|
31
60
|
}
|
|
32
61
|
|
|
33
|
-
if (selected === '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
info(`${skill} already exists, skipping`)
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
await fse.copy(src, dest)
|
|
48
|
-
success(`Installed skill: ${skill}`)
|
|
62
|
+
if (selected === 'npx-skills') {
|
|
63
|
+
info('Running npx skills...')
|
|
64
|
+
console.log()
|
|
65
|
+
try {
|
|
66
|
+
await execa('npx', ['skills'], {
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
reject: false,
|
|
70
|
+
})
|
|
71
|
+
} catch (err) {
|
|
72
|
+
warn(`npx skills failed: ${err.message}`)
|
|
49
73
|
}
|
|
50
|
-
return selected
|
|
51
74
|
}
|
|
52
|
-
|
|
53
|
-
// Custom provider, future: support npx <package> or git URL
|
|
54
|
-
warn(`Custom provider "${selected}" not yet supported. Add skills to .opencode/skills/ manually.`)
|
|
55
|
-
return null
|
|
56
75
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fse from 'fs-extra'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { findAiFiles } from '../utils/copy.js'
|
|
4
|
-
import { header, info, success, warn } from '../utils/exec.js'
|
|
4
|
+
import { header, info, prompt, success, warn } from '../utils/exec.js'
|
|
5
5
|
|
|
6
6
|
export async function cleanAiFiles() {
|
|
7
7
|
header('Step 2, Existing AI config files')
|
|
@@ -9,19 +9,19 @@ export async function cleanAiFiles() {
|
|
|
9
9
|
const cwd = process.cwd()
|
|
10
10
|
const found = await findAiFiles(cwd)
|
|
11
11
|
|
|
12
|
-
// Also find .
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
if (await fse.pathExists(
|
|
16
|
-
const entries = await fse.readdir(
|
|
12
|
+
// Also find .agents contents except skills/ (preserve user skills)
|
|
13
|
+
const agentsDir = path.join(cwd, '.agents')
|
|
14
|
+
const agentsEntries = []
|
|
15
|
+
if (await fse.pathExists(agentsDir)) {
|
|
16
|
+
const entries = await fse.readdir(agentsDir)
|
|
17
17
|
for (const entry of entries) {
|
|
18
18
|
if (entry !== 'skills') {
|
|
19
|
-
|
|
19
|
+
agentsEntries.push(path.join(agentsDir, entry))
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const allFiles = [...found, ...
|
|
24
|
+
const allFiles = [...found, ...agentsEntries]
|
|
25
25
|
|
|
26
26
|
if (allFiles.length === 0) {
|
|
27
27
|
success('No existing AI config files found')
|
|
@@ -33,7 +33,7 @@ export async function cleanAiFiles() {
|
|
|
33
33
|
info(f.replace(cwd, '.'))
|
|
34
34
|
}
|
|
35
35
|
console.log()
|
|
36
|
-
|
|
36
|
+
prompt('Press Enter to remove them all (your .agents/skills/ will be kept)')
|
|
37
37
|
console.log()
|
|
38
38
|
|
|
39
39
|
await new Promise(resolve => {
|
|
@@ -7,7 +7,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
7
7
|
const CONTENT_DIR = path.resolve(__dirname, '../../content')
|
|
8
8
|
|
|
9
9
|
export async function copyContentStep(platform) {
|
|
10
|
-
header('Step
|
|
10
|
+
header('Step 5, Copying opencode-onboard files')
|
|
11
11
|
|
|
12
12
|
const dest = process.cwd()
|
|
13
13
|
|