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.
Files changed (32) hide show
  1. package/README.md +44 -33
  2. package/content/{.opencode → .agents}/agents/.bootstrap/AGENTS.template.md +7 -7
  3. package/content/{.opencode → .agents}/agents/back-engineer.md +18 -17
  4. package/content/{.opencode → .agents}/agents/devops-manager.md +22 -29
  5. package/content/{.opencode → .agents}/agents/front-engineer.md +18 -18
  6. package/content/{.opencode → .agents}/agents/infra-engineer.md +19 -18
  7. package/content/{.opencode → .agents}/agents/quality-engineer.md +17 -18
  8. package/content/{.opencode → .agents}/agents/security-auditor.md +19 -20
  9. package/content/.opencode/package-lock.json +3 -3
  10. package/content/AGENTS.md +1 -1
  11. package/package.json +1 -1
  12. package/src/index.js +105 -67
  13. package/src/steps/__tests__/clean-ai-files.test.js +44 -30
  14. package/src/steps/check-platform.js +2 -2
  15. package/src/steps/check-rtk.js +1 -1
  16. package/src/steps/choose-models.js +141 -0
  17. package/src/steps/choose-skills-provider.js +51 -32
  18. package/src/steps/clean-ai-files.js +9 -9
  19. package/src/steps/copy-content.js +1 -1
  20. package/src/steps/install-browser.js +19 -27
  21. package/src/utils/__tests__/copy.test.js +0 -22
  22. package/src/utils/__tests__/exec.test.js +6 -4
  23. package/src/utils/copy.js +1 -1
  24. package/src/utils/exec.js +161 -84
  25. package/src/utils/models-cache.js +101 -0
  26. package/content/.opencode/agents/.bootstrap/CUSTOM-AGENT.template.md +0 -24
  27. package/content/.opencode/commands/.gitkeep +0 -0
  28. package/src/presets/skills-providers.json +0 -14
  29. package/src/steps/__tests__/choose-team.test.js +0 -105
  30. /package/content/{.opencode → .agents}/skills/browser-automation/SKILL.md +0 -0
  31. /package/content/{.opencode → .agents}/skills/ob-userstory-az/SKILL.md +0 -0
  32. /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 { 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
- }
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
- beforeEach(() => {
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
- findAiFiles.mockResolvedValue([])
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('deletes files when user confirms', async () => {
43
- findAiFiles.mockResolvedValue(['/proj/AGENTS.md', '/proj/CLAUDE.md'])
44
- confirm.mockResolvedValue(true)
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.remove).toHaveBeenCalledWith('/proj/AGENTS.md')
49
- expect(fse.remove).toHaveBeenCalledWith('/proj/CLAUDE.md')
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('skips deletion when user declines', async () => {
54
- findAiFiles.mockResolvedValue(['/proj/AGENTS.md'])
55
- confirm.mockResolvedValue(false)
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.remove).not.toHaveBeenCalled()
60
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('Skipped'))
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 9, Checking Azure DevOps CLI')
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 9, Checking GitHub CLI')
54
+ header('Step 4, Checking GitHub CLI')
55
55
 
56
56
  const hasGh = await commandExists('gh')
57
57
 
@@ -1,7 +1,7 @@
1
1
  import { code, commandExists, header, info, success, warn } from '../utils/exec.js'
2
2
 
3
3
  export async function checkRtk() {
4
- header('Step 8, Checking rtk')
4
+ header('Step 9, Checking rtk')
5
5
 
6
6
  const available = await commandExists('rtk')
7
7
 
@@ -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 SKILLS_PROVIDERS_PATH = path.resolve(__dirname, '../presets/skills-providers.json')
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
- const providers = await fse.readJson(SKILLS_PROVIDERS_PATH)
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 5, Choose your skills provider')
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: 'Install skills from:',
22
- choices: providers.map(p => ({
23
- name: `${p.label}${p.description ? `\n ${p.description}` : ''}`,
24
- value: p.value,
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
- warn('No skills installed. Add skills to .opencode/skills/ manually.')
30
- return null
59
+ return
31
60
  }
32
61
 
33
- if (selected === 'ob-skills') {
34
- const destSkillsDir = path.join(process.cwd(), '.opencode', 'skills')
35
- await fse.ensureDir(destSkillsDir)
36
-
37
- const skills = await fse.readdir(CONTENT_SKILLS_DIR)
38
- for (const skill of skills) {
39
- const src = path.join(CONTENT_SKILLS_DIR, skill)
40
- const dest = path.join(destSkillsDir, skill)
41
- const stat = await fse.stat(src)
42
- if (!stat.isDirectory()) continue
43
- if (await fse.pathExists(dest)) {
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 .opencode contents except skills/ (preserve user skills)
13
- const opencodeDir = path.join(cwd, '.opencode')
14
- const opencodeEntries = []
15
- if (await fse.pathExists(opencodeDir)) {
16
- const entries = await fse.readdir(opencodeDir)
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
- opencodeEntries.push(path.join(opencodeDir, entry))
19
+ agentsEntries.push(path.join(agentsDir, entry))
20
20
  }
21
21
  }
22
22
  }
23
23
 
24
- const allFiles = [...found, ...opencodeEntries]
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
- info('Press Enter to remove them all (your .opencode/skills/ will be kept)')
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 4, Copying opencode-onboard files')
10
+ header('Step 5, Copying opencode-onboard files')
11
11
 
12
12
  const dest = process.cwd()
13
13