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.
Files changed (37) hide show
  1. package/README.md +44 -32
  2. package/content/{.opencode → .agents}/agents/.bootstrap/AGENTS.template.md +7 -7
  3. package/content/{.opencode → .agents}/agents/back-engineer.md +27 -26
  4. package/content/.agents/agents/devops-manager.md +108 -0
  5. package/content/.agents/agents/front-engineer.md +73 -0
  6. package/content/.agents/agents/infra-engineer.md +74 -0
  7. package/content/.agents/agents/quality-engineer.md +74 -0
  8. package/content/.agents/agents/security-auditor.md +84 -0
  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 +49 -19
  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 +52 -33
  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 +86 -9
  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/agents/devops-manager.md +0 -115
  28. package/content/.opencode/agents/front-engineer.md +0 -73
  29. package/content/.opencode/agents/infra-engineer.md +0 -73
  30. package/content/.opencode/agents/quality-engineer.md +0 -75
  31. package/content/.opencode/agents/security-auditor.md +0 -85
  32. package/content/.opencode/commands/.gitkeep +0 -0
  33. package/src/presets/skills-providers.json +0 -14
  34. package/src/steps/__tests__/choose-team.test.js +0 -105
  35. /package/content/{.opencode → .agents}/skills/browser-automation/SKILL.md +0 -0
  36. /package/content/{.opencode → .agents}/skills/ob-userstory-az/SKILL.md +0 -0
  37. /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.0",
327
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
328
- "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
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 `.opencode/agents/.bootstrap/AGENTS.template.md`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
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 { cleanAiFiles } from './steps/clean-ai-files.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'
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 + npm/pnpm
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. Copy content filtered by platform
31
- await copyContentStep(platform)
57
+ // 4. Check platform CLI (az or gh)
58
+ await checkPlatform(platform)
32
59
 
33
- // 5. Choose skills provider
34
- await chooseSkillsProvider()
60
+ // 5. Copy content
61
+ await copyContentStep(platform)
35
62
 
36
63
  // 6. Init OpenSpec
37
64
  await initOpenspec()
38
65
 
39
- // 7. Install opencode-browser
40
- await installBrowser()
66
+ // 7. Install skills
67
+ await chooseSkillsProvider()
68
+
69
+ // 8. Choose models
70
+ await chooseModels()
41
71
 
42
- // 8. Check rtk
72
+ // 9. Check RTK
43
73
  await checkRtk()
44
74
 
45
- // 9. Check platform CLI (az or gh)
46
- await checkPlatform(platform)
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.cyan(' Open OpenCode in this project and type: ') + chalk.bold('"init"'))
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
- 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
+ }
@@ -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 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
- info('Agents detect and load skills automatically, you never need to specify them.')
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
 
@@ -1,9 +1,18 @@
1
1
  import { execa } from 'execa'
2
- import { error, header, success, warn } from '../utils/exec.js'
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 7, Installing opencode-browser')
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 AUTO_ANSWERS = [
16
- { trigger: 'Choose config location', response: '2' }, // global config
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
- if (showOutput) {
30
- process.stdout.write(chunk)
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
- // Step 3 — let user press Enter, then suppress remaining output
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) {