opencode-onboard 0.3.3 → 0.4.2

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 (87) hide show
  1. package/README.md +278 -214
  2. package/content/.agents/agents/basic-engineer.md +30 -0
  3. package/content/.agents/agents/devops-manager.md +38 -29
  4. package/content/.agents/session-log.json +41 -0
  5. package/content/.agents/skills/ob-default/SKILL.md +21 -0
  6. package/content/.agents/skills/ob-generic-guardrails/SKILL.md +32 -0
  7. package/content/.agents/skills/ob-global/SKILL.md +49 -0
  8. package/content/.agents/skills/ob-pullrequest-az/SKILL.md +11 -21
  9. package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +14 -24
  10. package/content/.agents/skills/ob-userstory-az/SKILL.md +8 -14
  11. package/content/.agents/skills/ob-userstory-gh/SKILL.md +6 -14
  12. package/content/.opencode/commands/init.md +8 -0
  13. package/content/.opencode/commands/main.md +17 -0
  14. package/content/.opencode/commands/opsx-apply.md +50 -33
  15. package/content/.opencode/commands/plan.md +37 -0
  16. package/content/.opencode/plugins/session-log.js +1 -1
  17. package/content/.opencode/skills/openspec-apply-change/SKILL.md +50 -33
  18. package/content/AGENTS.md +94 -144
  19. package/content/skills-lock.json +4 -0
  20. package/package.json +6 -1
  21. package/src/commands/join.js +43 -0
  22. package/src/commands/shared.js +12 -0
  23. package/src/commands/shared.test.js +56 -0
  24. package/src/commands/single.js +64 -0
  25. package/src/commands/wizard.js +99 -0
  26. package/src/index.js +25 -202
  27. package/src/presets/browser.json +18 -0
  28. package/src/presets/clean.json +21 -0
  29. package/src/presets/models.json +33 -0
  30. package/src/presets/optimization.json +22 -0
  31. package/src/presets/platforms.json +29 -2
  32. package/src/presets/quota.json +14 -0
  33. package/src/presets/source.json +17 -0
  34. package/src/steps/browser/browser.test.js +81 -0
  35. package/src/steps/{install-browser.js → browser/index.js} +12 -15
  36. package/src/steps/{__tests__/clean-ai-files.test.js → clean/clean.test.js} +28 -13
  37. package/src/steps/{clean-ai-files.js → clean/index.js} +32 -30
  38. package/src/steps/copy/agents.js +106 -0
  39. package/src/steps/{__tests__/copy-content.test.js → copy/copy.test.js} +10 -1
  40. package/src/steps/copy/index.js +33 -0
  41. package/src/steps/copy/skills.js +55 -0
  42. package/src/steps/{write-onboard-config.js → metadata/index.js} +3 -3
  43. package/src/steps/metadata/metadata.test.js +99 -0
  44. package/src/steps/models/format.js +60 -0
  45. package/src/steps/models/format.test.js +75 -0
  46. package/src/steps/models/index.js +52 -0
  47. package/src/steps/models/write.js +54 -0
  48. package/src/steps/models/write.test.js +117 -0
  49. package/src/steps/{init-openspec.js → openspec/ensemble.js} +20 -57
  50. package/src/steps/openspec/ensemble.test.js +79 -0
  51. package/src/steps/openspec/index.js +32 -0
  52. package/src/steps/optimization/caveman-guidance.js +11 -0
  53. package/src/steps/{install-caveman.js → optimization/caveman.js} +5 -19
  54. package/src/steps/optimization/global.js +64 -0
  55. package/src/steps/optimization/index.js +101 -0
  56. package/src/steps/{__tests__/token-optimization.test.js → optimization/optimization.test.js} +37 -22
  57. package/src/steps/{install-quota.js → optimization/quota.js} +12 -10
  58. package/src/steps/platform/index.js +81 -0
  59. package/src/steps/platform/platform.test.js +129 -0
  60. package/src/steps/{choose-source-scope.js → source/index.js} +11 -17
  61. package/src/steps/source/source.test.js +91 -0
  62. package/src/utils/__tests__/copy.test.js +12 -5
  63. package/src/utils/copy.js +4 -24
  64. package/src/utils/exec-spinner.js +47 -0
  65. package/src/utils/exec.js +120 -162
  66. package/src/utils/models-cache.js +25 -68
  67. package/src/utils/models-pricing.js +42 -0
  68. package/src/utils/models-pricing.test.js +93 -0
  69. package/content/.agents/agents/back-engineer.md +0 -87
  70. package/content/.agents/agents/front-engineer.md +0 -86
  71. package/content/.agents/agents/infra-engineer.md +0 -85
  72. package/content/.agents/agents/quality-engineer.md +0 -86
  73. package/content/.agents/agents/security-auditor.md +0 -86
  74. package/src/steps/__tests__/check-env.test.js +0 -70
  75. package/src/steps/__tests__/check-platform.test.js +0 -104
  76. package/src/steps/__tests__/check-rtk.test.js +0 -38
  77. package/src/steps/__tests__/choose-platform.test.js +0 -38
  78. package/src/steps/check-env.js +0 -26
  79. package/src/steps/check-platform.js +0 -80
  80. package/src/steps/check-rtk.js +0 -38
  81. package/src/steps/choose-models.js +0 -165
  82. package/src/steps/choose-platform.js +0 -22
  83. package/src/steps/choose-skills-provider.js +0 -79
  84. package/src/steps/copy-content.js +0 -89
  85. package/src/steps/enable-caveman-guidance.js +0 -78
  86. package/src/steps/patch-agents-md.js +0 -153
  87. package/src/steps/token-optimization.js +0 -59
package/src/index.js CHANGED
@@ -1,25 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from 'chalk'
3
- import fse from 'fs-extra'
4
3
  import { createRequire } from 'node:module'
5
- import path from 'node:path'
6
- import { checkEnv } from './steps/check-env.js'
7
- import { checkPlatform } from './steps/check-platform.js'
8
- import { checkRtk } from './steps/check-rtk.js'
9
- import { chooseModels } from './steps/choose-models.js'
10
- import { choosePlatform } from './steps/choose-platform.js'
11
- import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
12
- import { chooseSourceScope } from './steps/choose-source-scope.js'
13
- import { cleanAiFiles } from './steps/clean-ai-files.js'
14
- import { copyContentStep } from './steps/copy-content.js'
15
- import { enableCavemanGuidance } from './steps/enable-caveman-guidance.js'
16
- import { initOpenspec } from './steps/init-openspec.js'
17
- import { installBrowser } from './steps/install-browser.js'
18
- import { installCaveman } from './steps/install-caveman.js'
19
- import { installQuota } from './steps/install-quota.js'
20
- import { patchAgentsMd } from './steps/patch-agents-md.js'
21
- import { tokenOptimizationStep } from './steps/token-optimization.js'
22
- import { writeOnboardConfig } from './steps/write-onboard-config.js'
4
+ import { runJoin } from './commands/join.js'
5
+ import { runSingleCommand } from './commands/single.js'
6
+ import { runWizard } from './commands/wizard.js'
23
7
 
24
8
  function printHelp(version) {
25
9
  console.log(`opencode-onboard v${version}`)
@@ -29,16 +13,13 @@ function printHelp(version) {
29
13
  console.log(' npx opencode-onboard <command> Run a single step command')
30
14
  console.log()
31
15
  console.log('Commands:')
16
+ console.log(' join New team member setup (checks & local installs only)')
32
17
  console.log(' clean Run AI files cleanup step')
33
18
  console.log(' platform Run platform selection step')
34
19
  console.log(' copy Run content copy step')
35
20
  console.log(' openspec Run OpenSpec initialization step')
36
- console.log(' skills Run skills install step')
37
21
  console.log(' models Run models selection step')
38
22
  console.log(' optimization Run token optimization tools step')
39
- console.log(' quota Run opencode-quota installer step')
40
- console.log(' rtk Run rtk check step')
41
- console.log(' caveman Run caveman install + guidance steps')
42
23
  console.log(' browser Run opencode-browser installer step')
43
24
  console.log(' metadata Write onboarding metadata step')
44
25
  console.log()
@@ -46,85 +27,6 @@ function printHelp(version) {
46
27
  console.log(' -h, --help Show this help message')
47
28
  }
48
29
 
49
- async function readOnboardConfig() {
50
- const cfgPath = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
51
- if (!await fse.pathExists(cfgPath)) return null
52
- try {
53
- return await fse.readJson(cfgPath)
54
- } catch {
55
- return null
56
- }
57
- }
58
-
59
- async function runSingleCommand(command) {
60
- const saved = await readOnboardConfig()
61
- const savedWizard = saved?.wizard ?? {}
62
- const ctx = {
63
- hasDesign: !!savedWizard?.preserved?.design,
64
- hasArchitecture: !!savedWizard?.preserved?.architecture,
65
- hasOpenspec: !!savedWizard?.preserved?.openspec,
66
- sourceMode: savedWizard?.sourceMode ?? 'current',
67
- sourceRoots: Array.isArray(savedWizard?.sourceRoots) ? savedWizard.sourceRoots : [],
68
- }
69
- const platform = savedWizard?.platform
70
- const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
71
-
72
- const handlers = {
73
- clean: async () => {
74
- await cleanAiFiles()
75
- },
76
- platform: async () => {
77
- await choosePlatform()
78
- },
79
- copy: async () => {
80
- await copyContentStep(resolvedPlatform, ctx)
81
- await patchAgentsMd(ctx)
82
- },
83
- openspec: async () => {
84
- await initOpenspec()
85
- },
86
- skills: async () => {
87
- await chooseSkillsProvider()
88
- },
89
- models: async () => {
90
- await chooseModels()
91
- },
92
- optimization: async () => {
93
- await tokenOptimizationStep({ skillsProvider: savedWizard?.additionalSkillsProvider })
94
- },
95
- quota: async () => {
96
- await installQuota()
97
- },
98
- rtk: async () => {
99
- await checkRtk()
100
- },
101
- caveman: async () => {
102
- const caveman = await installCaveman({ skillsProvider: savedWizard?.additionalSkillsProvider })
103
- await enableCavemanGuidance(caveman)
104
- },
105
- browser: async () => {
106
- await installBrowser()
107
- },
108
- metadata: async () => {
109
- await writeOnboardConfig({
110
- ...ctx,
111
- platform: resolvedPlatform,
112
- additionalSkillsProvider: savedWizard?.additionalSkillsProvider ?? 'none',
113
- planModel: savedWizard?.models?.plan ?? null,
114
- buildModel: savedWizard?.models?.build ?? null,
115
- fastModel: savedWizard?.models?.fast ?? null,
116
- optionalTools: savedWizard?.optionalTools ?? null,
117
- cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
118
- })
119
- },
120
- }
121
-
122
- const handler = handlers[command]
123
- if (!handler) return false
124
- await handler()
125
- return true
126
- }
127
-
128
30
  if (process.stdout.isTTY) console.clear()
129
31
  console.log()
130
32
  const require = createRequire(import.meta.url)
@@ -137,111 +39,32 @@ if (args.includes('-h') || args.includes('--help')) {
137
39
  }
138
40
 
139
41
  if (args.length > 0) {
140
- const ok = await runSingleCommand(args[0])
141
- if (!ok) {
142
- console.log(chalk.red(`Unknown command: ${args[0]}`))
143
- console.log()
144
- printHelp(version)
145
- process.exit(1)
42
+ try {
43
+ if (args[0] === 'join') {
44
+ await runJoin()
45
+ } else {
46
+ const ok = await runSingleCommand(args[0])
47
+ if (!ok) {
48
+ console.log(chalk.red(`Unknown command: ${args[0]}`))
49
+ console.log()
50
+ printHelp(version)
51
+ process.exit(1)
52
+ }
53
+ }
54
+ } catch (err) {
55
+ if (err.name === 'ExitPromptError') {
56
+ console.log()
57
+ console.log(chalk.yellow('Cancelled.'))
58
+ } else {
59
+ console.error(chalk.red('\nUnexpected error:'), err.message)
60
+ process.exit(1)
61
+ }
146
62
  }
147
63
  process.exit(0)
148
64
  }
149
65
 
150
- const logo = chalk.hex('#fe3d57')
151
- const bannerLines = [
152
- logo(' '),
153
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
154
- logo(' ▒▒▓ ▓▒▓ '),
155
- logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
156
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
157
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
158
- logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
159
- logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
160
- logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
161
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
162
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
163
- logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
164
- '',
165
- chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
166
- chalk.dim(' Prepare your codebase for AI agents'),
167
- ]
168
-
169
- for (const line of bannerLines) console.log(line)
170
- console.log()
171
- console.log(' This tool will set up your project with a team of AI agents,')
172
- console.log(' install skills, select models, and configure OpenCode.')
173
- console.log()
174
-
175
- // Only wait for Enter in a real interactive TTY
176
- if (process.stdin.isTTY) {
177
- console.log(chalk.bold(' Press Enter to begin...'))
178
- console.log()
179
- await new Promise(resolve => {
180
- process.stdin.resume()
181
- process.stdin.once('data', () => {
182
- process.stdin.pause()
183
- resolve()
184
- })
185
- })
186
- }
187
-
188
66
  try {
189
- await checkEnv()
190
-
191
- const scope = await chooseSourceScope()
192
-
193
- const preserve = await cleanAiFiles()
194
- const ctx = { ...preserve, ...scope }
195
-
196
- const platform = await choosePlatform()
197
-
198
- await checkPlatform(platform)
199
-
200
- await copyContentStep(platform, ctx)
201
-
202
- await patchAgentsMd(ctx)
203
-
204
- await initOpenspec()
205
-
206
- const skillsSelection = await chooseSkillsProvider()
207
-
208
- const selectedModels = await chooseModels()
209
-
210
- const tokenOpt = await tokenOptimizationStep({ skillsProvider: skillsSelection.additionalSkillsProvider })
211
- const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
212
-
213
- await installBrowser()
214
-
215
- await writeOnboardConfig({
216
- ...ctx,
217
- platform,
218
- ...skillsSelection,
219
- ...selectedModels,
220
- optionalTools: { rtk, quota, caveman },
221
- cavemanGuidance,
222
- })
223
-
224
- const toGenerate = [
225
- !ctx.hasDesign && 'DESIGN.md',
226
- !ctx.hasArchitecture && 'ARCHITECTURE.md',
227
- ].filter(Boolean)
228
-
229
- console.log()
230
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
231
- console.log(chalk.bold.green(' Onboarding complete!'))
232
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
233
- console.log()
234
- console.log(' Open this project in OpenCode and type:')
235
- console.log(chalk.bold(' "init"'))
236
- console.log()
237
- if (toGenerate.length > 0) {
238
- console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
239
- console.log(' from your actual codebase, then activate the agent team.')
240
- } else {
241
- console.log(' OpenCode will activate the agent team.')
242
- }
243
- console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
244
- console.log()
67
+ await runWizard(version)
245
68
  } catch (err) {
246
69
  if (err.name === 'ExitPromptError') {
247
70
  console.log()
@@ -0,0 +1,18 @@
1
+ {
2
+ "installer": {
3
+ "command": "npx",
4
+ "args": ["@different-ai/opencode-browser", "install"]
5
+ },
6
+ "output": {
7
+ "showAfter": "To load the extension",
8
+ "hideAfter": "Press Enter when"
9
+ },
10
+ "autoAnswers": [
11
+ { "trigger": "Press Enter when", "response": "" },
12
+ { "trigger": "Choose config location", "response": "2" },
13
+ { "trigger": "Add plugin automatically?", "response": "y" },
14
+ { "trigger": "Create one?", "response": "y" },
15
+ { "trigger": "Add browser-automation skill", "response": "n" },
16
+ { "trigger": "Check broker", "response": "n" }
17
+ ]
18
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "preserve": ["DESIGN.md", "ARCHITECTURE.md", "openspec"],
3
+ "detectFiles": [
4
+ "AGENTS.md",
5
+ "CLAUDE.md",
6
+ "ARCHITECTURE.md",
7
+ "DESIGN.md",
8
+ ".cursorrules",
9
+ ".clinerules",
10
+ ".windsurfrules",
11
+ ".github/copilot-instructions.md",
12
+ "copilot-instructions.md",
13
+ ".aider.conf.yml",
14
+ ".aider",
15
+ ".opencode",
16
+ ".agents"
17
+ ],
18
+ "directoryTargets": [".opencode", ".agents"],
19
+ "preserveSubfolders": ["skills"],
20
+ "selectionMessage": "Select AI config files to remove:"
21
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "costTiers": [
3
+ { "max": 1, "label": "[$]" },
4
+ { "max": 10, "label": "[$$]" },
5
+ { "label": "[$$$]" }
6
+ ],
7
+ "roles": {
8
+ "plan": {
9
+ "prompt": "Plan model:",
10
+ "info": [
11
+ "PLAN model: used by the main agent to read issues, write proposals, coordinate the team.",
12
+ "This model needs to be strong. Use Claude Sonnet/Opus, GPT-4o, o3, or equivalent.",
13
+ "A weak model here will silently skip steps and break the workflow."
14
+ ]
15
+ },
16
+ "build": {
17
+ "prompt": "Build model:",
18
+ "info": [
19
+ "BUILD model: used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.",
20
+ "Needs to be capable for implementation work. Claude Sonnet, GPT-4o, or equivalent."
21
+ ],
22
+ "agents": ["front-engineer", "back-engineer", "infra-engineer", "quality-engineer", "security-auditor"]
23
+ },
24
+ "fast": {
25
+ "prompt": "Fast model:",
26
+ "info": [
27
+ "FAST model: used by devops-manager for reading issues and classifying PR comments.",
28
+ "Something fast and cheap is fine here, no heavy reasoning needed."
29
+ ],
30
+ "agents": ["devops-manager"]
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "message": "Enable tools:",
3
+ "info": "Choose which optimization tools to enable (recommended: all).",
4
+ "timeoutMs": 30000,
5
+ "choices": [
6
+ {
7
+ "name": "RTK check (recommended)",
8
+ "value": "rtk",
9
+ "checked": true
10
+ },
11
+ {
12
+ "name": "opencode-quota plugin (recommended)",
13
+ "value": "quota",
14
+ "checked": true
15
+ },
16
+ {
17
+ "name": "caveman concise mode (recommended)",
18
+ "value": "caveman",
19
+ "checked": true
20
+ }
21
+ ]
22
+ }
@@ -1,10 +1,37 @@
1
1
  [
2
2
  {
3
3
  "value": "github",
4
- "name": "GitHub"
4
+ "name": "GitHub",
5
+ "cli": {
6
+ "command": "gh",
7
+ "displayName": "GitHub CLI (gh)",
8
+ "installUrl": "https://cli.github.com",
9
+ "authCheck": {
10
+ "args": ["auth", "status"],
11
+ "notAuthenticatedMessage": "GitHub CLI not authenticated. Run:",
12
+ "commands": ["gh auth login"]
13
+ }
14
+ }
5
15
  },
6
16
  {
7
17
  "value": "azure",
8
- "name": "Azure DevOps"
18
+ "name": "Azure DevOps",
19
+ "cli": {
20
+ "command": "az",
21
+ "displayName": "Azure CLI (az)",
22
+ "installUrl": "https://learn.microsoft.com/en-us/cli/azure/install-azure-cli",
23
+ "extensionCheck": {
24
+ "args": ["extension", "list", "--query", "[?name=='azure-devops']", "-o", "tsv"],
25
+ "expectedOutput": "azure-devops",
26
+ "missingMessage": "azure-devops extension not found. Run:",
27
+ "errorMessage": "Could not check azure-devops extension. Run:",
28
+ "commands": [
29
+ "az extension add --name azure-devops",
30
+ "az config set extension.dynamic_install_allow_preview=true",
31
+ "az login",
32
+ "az devops login --organization https://dev.azure.com/<your-org>"
33
+ ]
34
+ }
35
+ }
9
36
  }
10
37
  ]
@@ -0,0 +1,14 @@
1
+ {
2
+ "plugin": "@slkiser/opencode-quota@latest",
3
+ "prompt": {
4
+ "message": "Install opencode-quota with recommended defaults?",
5
+ "default": true,
6
+ "timeoutMs": 20000
7
+ },
8
+ "defaults": {
9
+ "enabledProviders": "auto",
10
+ "formatStyle": "singleWindow",
11
+ "percentDisplayMode": "used",
12
+ "showSessionTokens": true
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "message": "Source code location:",
3
+ "default": "current",
4
+ "choices": [
5
+ {
6
+ "name": "Current folder (default)",
7
+ "value": "current",
8
+ "description": "Use this repository only"
9
+ },
10
+ {
11
+ "name": "Select folders in parent (../)",
12
+ "value": "parent",
13
+ "description": "Use when this repo only contains agent config"
14
+ }
15
+ ],
16
+ "parentSelectionMessage": "Select source folders from parent directory:"
17
+ }
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ vi.mock('../../utils/exec.js', () => ({
4
+ header: vi.fn(),
5
+ info: vi.fn(),
6
+ success: vi.fn(),
7
+ warn: vi.fn(),
8
+ error: vi.fn(),
9
+ }))
10
+
11
+ vi.mock('fs-extra', () => ({
12
+ default: {
13
+ readJson: vi.fn().mockResolvedValue({
14
+ installer: { command: 'npx', args: ['@different-ai/opencode-browser', 'install'] },
15
+ output: { showAfter: '===', hideAfter: '===' },
16
+ autoAnswers: [
17
+ { trigger: 'Install', response: 'y' },
18
+ ],
19
+ }),
20
+ },
21
+ }))
22
+
23
+ vi.mock('execa', () => ({
24
+ execa: vi.fn(),
25
+ }))
26
+
27
+ import fse from 'fs-extra'
28
+ import { installBrowser } from './index.js'
29
+
30
+ describe('installBrowser()', () => {
31
+ beforeEach(() => {
32
+ vi.clearAllMocks()
33
+ })
34
+
35
+ it('calls installer command from preset', async () => {
36
+ const { execa } = await import('execa')
37
+ const mockChild = {
38
+ stdout: { on: vi.fn() },
39
+ stderr: { on: vi.fn() },
40
+ stdin: { write: vi.fn() },
41
+ then: (cb) => cb({ exitCode: 0 }),
42
+ }
43
+ execa.mockReturnValue(mockChild)
44
+
45
+ await installBrowser()
46
+
47
+ expect(execa).toHaveBeenCalledWith('npx', expect.arrayContaining(['@different-ai/opencode-browser']), expect.any(Object))
48
+ })
49
+
50
+ it('logs success when exit code is 0', async () => {
51
+ const { execa } = await import('execa')
52
+ const mockChild = {
53
+ stdout: { on: vi.fn() },
54
+ stderr: { on: vi.fn() },
55
+ stdin: { write: vi.fn() },
56
+ then: (cb) => cb({ exitCode: 0 }),
57
+ }
58
+ execa.mockReturnValue(mockChild)
59
+ const { success } = await import('../../utils/exec.js')
60
+
61
+ await installBrowser()
62
+
63
+ expect(success).toHaveBeenCalledWith('opencode-browser installed')
64
+ })
65
+
66
+ it('logs warning when exit code is non-zero', async () => {
67
+ const { execa } = await import('execa')
68
+ const mockChild = {
69
+ stdout: { on: vi.fn() },
70
+ stderr: { on: vi.fn() },
71
+ stdin: { write: vi.fn() },
72
+ then: (cb) => cb({ exitCode: 1 }),
73
+ }
74
+ execa.mockReturnValue(mockChild)
75
+ const { warn } = await import('../../utils/exec.js')
76
+
77
+ await installBrowser()
78
+
79
+ expect(warn).toHaveBeenCalledWith('opencode-browser install exited with non-zero code')
80
+ })
81
+ })
@@ -1,35 +1,32 @@
1
1
  import { execa } from 'execa'
2
- import { header, info, success, warn, error } from '../utils/exec.js'
2
+ import fse from 'fs-extra'
3
+ import { header, info, success, warn, error } from '../../utils/exec.js'
3
4
  import os from 'os'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
4
7
 
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
- ]
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const BROWSER_PRESET_PATH = path.resolve(__dirname, '../../presets/browser.json')
10
+ const browserPreset = await fse.readJson(BROWSER_PRESET_PATH)
13
11
 
14
12
  export async function installBrowser() {
15
- header('Step 11, Installing opencode-browser')
13
+ header('Step 9, Installing opencode-browser')
16
14
 
17
15
  try {
18
- const child = execa('npx', ['@different-ai/opencode-browser', 'install'], {
16
+ const child = execa(browserPreset.installer.command, browserPreset.installer.args, {
19
17
  cwd: os.homedir(),
20
18
  stdio: ['pipe', 'pipe', 'pipe'],
21
19
  reject: false,
22
20
  })
23
21
 
24
- const pendingTriggers = [...AUTO_ANSWERS]
22
+ const pendingTriggers = [...browserPreset.autoAnswers]
25
23
  let show = false
26
24
 
27
25
  child.stdout.on('data', (chunk) => {
28
26
  const text = chunk.toString()
29
27
 
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
28
+ if (text.includes(browserPreset.output.showAfter)) show = true
29
+ if (text.includes(browserPreset.output.hideAfter)) show = false
33
30
 
34
31
  if (show) process.stdout.write(chunk)
35
32
 
@@ -3,6 +3,10 @@ import fse from 'fs-extra'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
5
 
6
+ vi.mock('@inquirer/prompts', () => ({
7
+ checkbox: vi.fn(),
8
+ }))
9
+
6
10
  vi.mock('../../utils/exec.js', () => ({
7
11
  header: vi.fn(),
8
12
  success: vi.fn(),
@@ -11,7 +15,8 @@ vi.mock('../../utils/exec.js', () => ({
11
15
  prompt: vi.fn(),
12
16
  }))
13
17
 
14
- import { success, warn } from '../../utils/exec.js'
18
+ import { success } from '../../utils/exec.js'
19
+ import { checkbox } from '@inquirer/prompts'
15
20
 
16
21
  describe('cleanAiFiles()', () => {
17
22
  let tmpDir
@@ -31,24 +36,22 @@ describe('cleanAiFiles()', () => {
31
36
  })
32
37
 
33
38
  it('prints success when no AI files are found', async () => {
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)
39
+ const { cleanAiFiles } = await import('./index.js')
39
40
 
40
41
  await cleanAiFiles()
41
42
 
42
43
  expect(success).toHaveBeenCalledWith('No existing AI config files to remove')
43
44
  })
44
45
 
45
- it('removes found AI files after Enter', async () => {
46
+ it('removes selected AI files', async () => {
46
47
  await fse.writeFile(path.join(tmpDir, 'AGENTS.md'), '# agents')
47
48
  await fse.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# claude')
49
+ checkbox.mockResolvedValue([
50
+ path.join(tmpDir, 'AGENTS.md'),
51
+ path.join(tmpDir, 'CLAUDE.md'),
52
+ ])
48
53
 
49
- const { cleanAiFiles } = await import('../clean-ai-files.js')
50
-
51
- setTimeout(() => process.stdin.emit('data', '\n'), 10)
54
+ const { cleanAiFiles } = await import('./index.js')
52
55
 
53
56
  await cleanAiFiles()
54
57
 
@@ -57,16 +60,28 @@ describe('cleanAiFiles()', () => {
57
60
  expect(success).toHaveBeenCalledWith('Removed existing AI config files')
58
61
  })
59
62
 
63
+ it('keeps unselected AI files', async () => {
64
+ await fse.writeFile(path.join(tmpDir, 'AGENTS.md'), '# agents')
65
+ await fse.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# claude')
66
+ checkbox.mockResolvedValue([path.join(tmpDir, 'AGENTS.md')])
67
+
68
+ const { cleanAiFiles } = await import('./index.js')
69
+
70
+ await cleanAiFiles()
71
+
72
+ expect(await fse.pathExists(path.join(tmpDir, 'AGENTS.md'))).toBe(false)
73
+ expect(await fse.pathExists(path.join(tmpDir, 'CLAUDE.md'))).toBe(true)
74
+ })
75
+
60
76
  it('removes .agents sub-entries but preserves .agents/skills', async () => {
61
77
  const agentsDir = path.join(tmpDir, '.agents')
62
78
  await fse.ensureDir(path.join(agentsDir, 'agents'))
63
79
  await fse.ensureDir(path.join(agentsDir, 'skills', 'my-skill'))
64
80
  await fse.writeFile(path.join(agentsDir, 'agents', 'front-engineer.md'), 'agent')
65
81
  await fse.writeFile(path.join(agentsDir, 'skills', 'my-skill', 'SKILL.md'), 'skill')
82
+ checkbox.mockResolvedValue([path.join(agentsDir, 'agents')])
66
83
 
67
- const { cleanAiFiles } = await import('../clean-ai-files.js')
68
-
69
- setTimeout(() => process.stdin.emit('data', '\n'), 10)
84
+ const { cleanAiFiles } = await import('./index.js')
70
85
 
71
86
  await cleanAiFiles()
72
87