opencode-onboard 0.0.5 → 0.1.0
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 -32
- package/content/{.opencode → .agents}/agents/.bootstrap/AGENTS.template.md +7 -7
- package/content/{.opencode → .agents}/agents/back-engineer.md +27 -26
- package/content/.agents/agents/devops-manager.md +108 -0
- package/content/.agents/agents/front-engineer.md +73 -0
- package/content/.agents/agents/infra-engineer.md +74 -0
- package/content/.agents/agents/quality-engineer.md +74 -0
- package/content/.agents/agents/security-auditor.md +84 -0
- package/content/.opencode/package-lock.json +3 -3
- package/content/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/index.js +49 -19
- 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 +52 -33
- 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 +86 -9
- package/src/utils/models-cache.js +101 -0
- package/content/.opencode/agents/.bootstrap/CUSTOM-AGENT.template.md +0 -24
- package/content/.opencode/agents/devops-manager.md +0 -115
- package/content/.opencode/agents/front-engineer.md +0 -73
- package/content/.opencode/agents/infra-engineer.md +0 -73
- package/content/.opencode/agents/quality-engineer.md +0 -75
- package/content/.opencode/agents/security-auditor.md +0 -85
- 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
|
@@ -323,9 +323,9 @@
|
|
|
323
323
|
}
|
|
324
324
|
},
|
|
325
325
|
"node_modules/uuid": {
|
|
326
|
-
"version": "13.0.
|
|
327
|
-
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.
|
|
328
|
-
"integrity": "sha512-
|
|
326
|
+
"version": "13.0.1",
|
|
327
|
+
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.1.tgz",
|
|
328
|
+
"integrity": "sha512-9ezox2roIft6ExBVTVqibSd5dc5/47Sw/uY6b4SjQUT2TzQ0tltNquWA46y4xPQmdZYqvnio22SgWd41M86+jw==",
|
|
329
329
|
"funding": [
|
|
330
330
|
"https://github.com/sponsors/broofa",
|
|
331
331
|
"https://github.com/sponsors/ctavan"
|
package/content/AGENTS.md
CHANGED
|
@@ -61,7 +61,7 @@ The output must be a real, populated `ARCHITECTURE.md` covering all sections the
|
|
|
61
61
|
|
|
62
62
|
### Step 4, Rewrite this file
|
|
63
63
|
|
|
64
|
-
Replace the entire contents of `AGENTS.md` with the real agent guidance template located at `.
|
|
64
|
+
Replace the entire contents of `AGENTS.md` with the real agent guidance template located at `.agents/agents/.bootstrap/AGENTS.template.md`.
|
|
65
65
|
|
|
66
66
|
---
|
|
67
67
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,24 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import { checkEnv } from './steps/check-env.js'
|
|
4
|
-
import {
|
|
4
|
+
import { checkPlatform } from './steps/check-platform.js'
|
|
5
|
+
import { checkRtk } from './steps/check-rtk.js'
|
|
6
|
+
import { chooseModels } from './steps/choose-models.js'
|
|
5
7
|
import { choosePlatform } from './steps/choose-platform.js'
|
|
6
|
-
import { copyContentStep } from './steps/copy-content.js'
|
|
7
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'
|
|
8
11
|
import { initOpenspec } from './steps/init-openspec.js'
|
|
9
12
|
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
|
|
|
14
|
+
console.clear()
|
|
15
|
+
console.log()
|
|
16
|
+
const logo = chalk.hex('#fe3d57')
|
|
17
|
+
console.log(logo(' '))
|
|
18
|
+
console.log(logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '))
|
|
19
|
+
console.log(logo(' ▓▒▓ ▓▒▓ '))
|
|
20
|
+
console.log(logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '))
|
|
21
|
+
console.log(logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '))
|
|
22
|
+
console.log(logo(' ▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '))
|
|
23
|
+
console.log(logo(' ▓▓▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '))
|
|
24
|
+
console.log(logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '))
|
|
25
|
+
console.log(logo(' ▓▒▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▒▓ '))
|
|
26
|
+
console.log(logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '))
|
|
27
|
+
console.log(logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '))
|
|
28
|
+
console.log(logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '))
|
|
29
|
+
console.log()
|
|
30
|
+
console.log(chalk.bold(' 🧰 opencode-onboard'))
|
|
31
|
+
console.log(chalk.dim(' Prepare your codebase for AI agents'))
|
|
32
|
+
console.log()
|
|
33
|
+
console.log(' This tool will set up your project with a team of AI agents,')
|
|
34
|
+
console.log(' install skills, select models, and configure OpenCode.')
|
|
13
35
|
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('└─────────────────────────────────────┘'))
|
|
36
|
+
console.log(chalk.bold(' Press Enter to begin...'))
|
|
18
37
|
console.log()
|
|
19
38
|
|
|
39
|
+
await new Promise(resolve => {
|
|
40
|
+
process.stdin.resume()
|
|
41
|
+
process.stdin.once('data', () => {
|
|
42
|
+
process.stdin.pause()
|
|
43
|
+
resolve()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
20
47
|
try {
|
|
21
|
-
// 1. Check Node +
|
|
48
|
+
// 1. Check Node + pnpm
|
|
22
49
|
await checkEnv()
|
|
23
50
|
|
|
24
51
|
// 2. Clean existing AI config files
|
|
@@ -27,23 +54,26 @@ try {
|
|
|
27
54
|
// 3. Choose platform
|
|
28
55
|
const platform = await choosePlatform()
|
|
29
56
|
|
|
30
|
-
// 4.
|
|
31
|
-
await
|
|
57
|
+
// 4. Check platform CLI (az or gh)
|
|
58
|
+
await checkPlatform(platform)
|
|
32
59
|
|
|
33
|
-
// 5.
|
|
34
|
-
await
|
|
60
|
+
// 5. Copy content
|
|
61
|
+
await copyContentStep(platform)
|
|
35
62
|
|
|
36
63
|
// 6. Init OpenSpec
|
|
37
64
|
await initOpenspec()
|
|
38
65
|
|
|
39
|
-
// 7. Install
|
|
40
|
-
await
|
|
66
|
+
// 7. Install skills
|
|
67
|
+
await chooseSkillsProvider()
|
|
68
|
+
|
|
69
|
+
// 8. Choose models
|
|
70
|
+
await chooseModels()
|
|
41
71
|
|
|
42
|
-
//
|
|
72
|
+
// 9. Check RTK
|
|
43
73
|
await checkRtk()
|
|
44
74
|
|
|
45
|
-
//
|
|
46
|
-
await
|
|
75
|
+
// 10. Install opencode-browser
|
|
76
|
+
await installBrowser()
|
|
47
77
|
|
|
48
78
|
// Done
|
|
49
79
|
console.log()
|
|
@@ -52,7 +82,7 @@ try {
|
|
|
52
82
|
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
53
83
|
console.log()
|
|
54
84
|
console.log(' Next step:')
|
|
55
|
-
console.log(chalk.
|
|
85
|
+
console.log(chalk.hex('#fe3d57')(' Open OpenCode in this project and type: ') + chalk.bold('"init"'))
|
|
56
86
|
console.log()
|
|
57
87
|
console.log(' OpenCode will generate ARCHITECTURE.md and DESIGN.md')
|
|
58
88
|
console.log(' from your actual codebase, then activate the agent team.')
|
|
@@ -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
|
+
}
|
|
@@ -2,55 +2,74 @@ import { select } from '@inquirer/prompts'
|
|
|
2
2
|
import fse from 'fs-extra'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
|
+
import { execa } from 'execa'
|
|
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
|
-
info('Agents detect and load skills automatically
|
|
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
|
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
|
-
import {
|
|
2
|
+
import { header, info, success, warn, error } from '../utils/exec.js'
|
|
3
3
|
import os from 'os'
|
|
4
4
|
|
|
5
|
+
const AUTO_ANSWERS = [
|
|
6
|
+
{ trigger: 'Press Enter when', response: '' },
|
|
7
|
+
{ trigger: 'Choose config location', response: '2' },
|
|
8
|
+
{ trigger: 'Add plugin automatically?', response: 'y' },
|
|
9
|
+
{ trigger: 'Create one?', response: 'y' },
|
|
10
|
+
{ trigger: 'Add browser-automation skill', response: 'n' },
|
|
11
|
+
{ trigger: 'Check broker', response: 'n' },
|
|
12
|
+
]
|
|
13
|
+
|
|
5
14
|
export async function installBrowser() {
|
|
6
|
-
header('Step
|
|
15
|
+
header('Step 10, Installing opencode-browser')
|
|
7
16
|
|
|
8
17
|
try {
|
|
9
18
|
const child = execa('npx', ['@different-ai/opencode-browser', 'install'], {
|
|
@@ -12,37 +21,18 @@ export async function installBrowser() {
|
|
|
12
21
|
reject: false,
|
|
13
22
|
})
|
|
14
23
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
{ trigger: 'Create one?', response: 'y' },
|
|
18
|
-
{ trigger: 'Add browser-automation skill', response: 'n' },
|
|
19
|
-
{ trigger: 'Check broker', response: 'n' },
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
let pendingTriggers = [...AUTO_ANSWERS]
|
|
23
|
-
let showOutput = true // show output until after step 3 user interaction
|
|
24
|
-
let waitingForUser = false
|
|
24
|
+
const pendingTriggers = [...AUTO_ANSWERS]
|
|
25
|
+
let show = false
|
|
25
26
|
|
|
26
27
|
child.stdout.on('data', (chunk) => {
|
|
27
28
|
const text = chunk.toString()
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Show only the load/pin instructions, hide everything else
|
|
31
|
+
if (text.includes('To load the extension')) show = true
|
|
32
|
+
if (text.includes('Press Enter when')) show = false
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
if (text.includes('Press Enter when') && !waitingForUser) {
|
|
35
|
-
waitingForUser = true
|
|
36
|
-
process.stdin.resume()
|
|
37
|
-
process.stdin.once('data', () => {
|
|
38
|
-
child.stdin.write('\n')
|
|
39
|
-
process.stdin.pause()
|
|
40
|
-
showOutput = false // suppress steps 4-9 output
|
|
41
|
-
})
|
|
42
|
-
return
|
|
43
|
-
}
|
|
34
|
+
if (show) process.stdout.write(chunk)
|
|
44
35
|
|
|
45
|
-
// Auto-answer remaining prompts
|
|
46
36
|
for (let i = 0; i < pendingTriggers.length; i++) {
|
|
47
37
|
if (text.includes(pendingTriggers[i].trigger)) {
|
|
48
38
|
child.stdin.write(pendingTriggers[i].response + '\n')
|
|
@@ -52,6 +42,8 @@ export async function installBrowser() {
|
|
|
52
42
|
}
|
|
53
43
|
})
|
|
54
44
|
|
|
45
|
+
child.stderr.on('data', (chunk) => process.stderr.write(chunk))
|
|
46
|
+
|
|
55
47
|
const result = await child
|
|
56
48
|
|
|
57
49
|
if (result.exitCode === 0) {
|