opencode-onboard 0.3.1 → 0.4.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 (79) hide show
  1. package/README.md +266 -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/opsx-apply.md +50 -33
  13. package/content/.opencode/opencode.json +3 -3
  14. package/content/.opencode/plugins/session-log.js +1 -1
  15. package/content/.opencode/skills/openspec-apply-change/SKILL.md +50 -33
  16. package/content/AGENTS.md +95 -141
  17. package/content/skills-lock.json +4 -0
  18. package/package.json +6 -1
  19. package/src/index.js +112 -191
  20. package/src/presets/browser.json +18 -0
  21. package/src/presets/clean.json +21 -0
  22. package/src/presets/models.json +33 -0
  23. package/src/presets/optimization.json +22 -0
  24. package/src/presets/platforms.json +29 -2
  25. package/src/presets/quota.json +14 -0
  26. package/src/presets/source.json +17 -0
  27. package/src/steps/browser/browser.test.js +81 -0
  28. package/src/steps/{install-browser.js → browser/index.js} +12 -15
  29. package/src/steps/{__tests__/clean-ai-files.test.js → clean/clean.test.js} +28 -13
  30. package/src/steps/{clean-ai-files.js → clean/index.js} +32 -30
  31. package/src/steps/{patch-agents-md.js → copy/agents.js} +41 -20
  32. package/src/steps/{__tests__/copy-content.test.js → copy/copy.test.js} +10 -1
  33. package/src/steps/copy/index.js +33 -0
  34. package/src/steps/copy/skills.js +55 -0
  35. package/src/steps/{write-onboard-config.js → metadata/index.js} +3 -3
  36. package/src/steps/metadata/metadata.test.js +96 -0
  37. package/src/steps/models/format.js +60 -0
  38. package/src/steps/models/format.test.js +74 -0
  39. package/src/steps/models/index.js +52 -0
  40. package/src/steps/models/write.js +54 -0
  41. package/src/steps/models/write.test.js +119 -0
  42. package/src/steps/{init-openspec.js → openspec/ensemble.js} +27 -61
  43. package/src/steps/openspec/ensemble.test.js +79 -0
  44. package/src/steps/openspec/index.js +32 -0
  45. package/src/steps/optimization/caveman-guidance.js +11 -0
  46. package/src/steps/{install-caveman.js → optimization/caveman.js} +5 -19
  47. package/src/steps/optimization/global.js +64 -0
  48. package/src/steps/optimization/index.js +101 -0
  49. package/src/steps/{__tests__/token-optimization.test.js → optimization/optimization.test.js} +19 -24
  50. package/src/steps/{install-quota.js → optimization/quota.js} +12 -10
  51. package/src/steps/platform/index.js +81 -0
  52. package/src/steps/platform/platform.test.js +129 -0
  53. package/src/steps/{choose-source-scope.js → source/index.js} +11 -17
  54. package/src/steps/source/source.test.js +89 -0
  55. package/src/utils/__tests__/copy.test.js +12 -5
  56. package/src/utils/copy.js +4 -24
  57. package/src/utils/exec-spinner.js +47 -0
  58. package/src/utils/exec.js +120 -162
  59. package/src/utils/models-cache.js +25 -68
  60. package/src/utils/models-pricing.js +42 -0
  61. package/src/utils/models-pricing.test.js +94 -0
  62. package/content/.agents/agents/back-engineer.md +0 -87
  63. package/content/.agents/agents/front-engineer.md +0 -86
  64. package/content/.agents/agents/infra-engineer.md +0 -85
  65. package/content/.agents/agents/quality-engineer.md +0 -86
  66. package/content/.agents/agents/security-auditor.md +0 -86
  67. package/src/steps/__tests__/check-env.test.js +0 -70
  68. package/src/steps/__tests__/check-platform.test.js +0 -104
  69. package/src/steps/__tests__/check-rtk.test.js +0 -38
  70. package/src/steps/__tests__/choose-platform.test.js +0 -38
  71. package/src/steps/check-env.js +0 -26
  72. package/src/steps/check-platform.js +0 -80
  73. package/src/steps/check-rtk.js +0 -38
  74. package/src/steps/choose-models.js +0 -163
  75. package/src/steps/choose-platform.js +0 -22
  76. package/src/steps/choose-skills-provider.js +0 -79
  77. package/src/steps/copy-content.js +0 -89
  78. package/src/steps/enable-caveman-guidance.js +0 -93
  79. package/src/steps/token-optimization.js +0 -59
package/src/index.js CHANGED
@@ -1,26 +1,17 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import chalk from 'chalk'
3
+ import fse from 'fs-extra'
3
4
  import { createRequire } from 'node:module'
4
5
  import path from 'node:path'
5
- import fse from 'fs-extra'
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 { chooseSourceScope } from './steps/choose-source-scope.js'
12
- import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
13
- import { cleanAiFiles } from './steps/clean-ai-files.js'
14
- import { copyContentStep } from './steps/copy-content.js'
15
- import { initOpenspec } from './steps/init-openspec.js'
16
- import { patchAgentsMd } from './steps/patch-agents-md.js'
17
- import { installQuota } from './steps/install-quota.js'
18
- import { installCaveman } from './steps/install-caveman.js'
19
- import { enableCavemanGuidance } from './steps/enable-caveman-guidance.js'
20
- import { installBrowser } from './steps/install-browser.js'
21
- import { writeOnboardConfig } from './steps/write-onboard-config.js'
22
- import { loading } from './utils/exec.js'
23
- import { tokenOptimizationStep } from './steps/token-optimization.js'
6
+ import { installBrowser } from './steps/browser/index.js'
7
+ import { cleanAiFiles } from './steps/clean/index.js'
8
+ import { copyContentStep } from './steps/copy/index.js'
9
+ import { chooseModels } from './steps/models/index.js'
10
+ import { initOpenspec } from './steps/openspec/index.js'
11
+ import { tokenOptimizationStep } from './steps/optimization/index.js'
12
+ import { choosePlatform } from './steps/platform/index.js'
13
+ import { chooseSourceScope } from './steps/source/index.js'
14
+ import { writeOnboardConfig } from './steps/metadata/index.js'
24
15
 
25
16
  function printHelp(version) {
26
17
  console.log(`opencode-onboard v${version}`)
@@ -34,12 +25,8 @@ function printHelp(version) {
34
25
  console.log(' platform Run platform selection step')
35
26
  console.log(' copy Run content copy step')
36
27
  console.log(' openspec Run OpenSpec initialization step')
37
- console.log(' skills Run skills install step')
38
28
  console.log(' models Run models selection step')
39
29
  console.log(' optimization Run token optimization tools step')
40
- console.log(' quota Run opencode-quota installer step')
41
- console.log(' rtk Run rtk check step')
42
- console.log(' caveman Run caveman install + guidance steps')
43
30
  console.log(' browser Run opencode-browser installer step')
44
31
  console.log(' metadata Write onboarding metadata step')
45
32
  console.log()
@@ -70,78 +57,46 @@ async function runSingleCommand(command) {
70
57
  const platform = savedWizard?.platform
71
58
  const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
72
59
 
73
- if (command === 'clean') {
74
- await cleanAiFiles()
75
- return true
76
- }
77
-
78
- if (command === 'platform') {
79
- await choosePlatform()
80
- return true
81
- }
82
-
83
- if (command === 'copy') {
84
- await copyContentStep(resolvedPlatform, ctx)
85
- await patchAgentsMd(ctx)
86
- return true
87
- }
88
-
89
- if (command === 'openspec') {
90
- await initOpenspec()
91
- return true
92
- }
93
-
94
- if (command === 'skills') {
95
- await chooseSkillsProvider()
96
- return true
97
- }
98
-
99
- if (command === 'models') {
100
- await chooseModels()
101
- return true
102
- }
103
-
104
- if (command === 'optimization') {
105
- await tokenOptimizationStep({ skillsProvider: savedWizard?.additionalSkillsProvider })
106
- return true
107
- }
108
-
109
- if (command === 'quota') {
110
- await installQuota()
111
- return true
112
- }
113
-
114
- if (command === 'rtk') {
115
- await checkRtk()
116
- return true
117
- }
118
-
119
- if (command === 'caveman') {
120
- const caveman = await installCaveman({ skillsProvider: savedWizard?.additionalSkillsProvider })
121
- await enableCavemanGuidance(caveman)
122
- return true
123
- }
124
-
125
- if (command === 'browser') {
126
- await installBrowser()
127
- return true
128
- }
129
-
130
- if (command === 'metadata') {
131
- await writeOnboardConfig({
132
- ...ctx,
133
- platform: resolvedPlatform,
134
- additionalSkillsProvider: savedWizard?.additionalSkillsProvider ?? 'none',
135
- planModel: savedWizard?.models?.plan ?? null,
136
- buildModel: savedWizard?.models?.build ?? null,
137
- fastModel: savedWizard?.models?.fast ?? null,
138
- optionalTools: savedWizard?.optionalTools ?? null,
139
- cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
140
- })
141
- return true
142
- }
143
-
144
- return false
60
+ const handlers = {
61
+ clean: async () => {
62
+ await cleanAiFiles()
63
+ },
64
+ platform: async () => {
65
+ await choosePlatform()
66
+ },
67
+ copy: async () => {
68
+ await copyContentStep(resolvedPlatform, ctx)
69
+ },
70
+ openspec: async () => {
71
+ await initOpenspec()
72
+ },
73
+ models: async () => {
74
+ await chooseModels()
75
+ },
76
+ optimization: async () => {
77
+ await tokenOptimizationStep({ ctx })
78
+ },
79
+ browser: async () => {
80
+ await installBrowser()
81
+ },
82
+ metadata: async () => {
83
+ await writeOnboardConfig({
84
+ ...ctx,
85
+ platform: resolvedPlatform,
86
+ additionalSkillsProvider: 'npx-skills',
87
+ planModel: savedWizard?.models?.plan ?? null,
88
+ buildModel: savedWizard?.models?.build ?? null,
89
+ fastModel: savedWizard?.models?.fast ?? null,
90
+ optionalTools: savedWizard?.optionalTools ?? null,
91
+ cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
92
+ })
93
+ },
94
+ }
95
+
96
+ const handler = handlers[command]
97
+ if (!handler) return false
98
+ await handler()
99
+ return true
145
100
  }
146
101
 
147
102
  if (process.stdout.isTTY) console.clear()
@@ -168,117 +123,83 @@ if (args.length > 0) {
168
123
 
169
124
  const logo = chalk.hex('#fe3d57')
170
125
  const bannerLines = [
171
- logo(' '),
172
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
173
- logo(' ▒▒▓ ▓▒▓ '),
174
- logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
175
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
176
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
177
- logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
178
- logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
179
- logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
180
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
181
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
182
- logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
183
- '',
184
- chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
185
- chalk.dim(' Prepare your codebase for AI agents'),
186
- ]
187
-
188
- for (const line of bannerLines) console.log(line)
189
- console.log()
190
- console.log(' This tool will set up your project with a team of AI agents,')
191
- console.log(' install skills, select models, and configure OpenCode.')
192
- console.log()
193
-
194
- // Only wait for Enter in a real interactive TTY
195
- if (process.stdin.isTTY) {
196
- console.log(chalk.bold(' Press Enter to begin...'))
197
- console.log()
198
- await new Promise(resolve => {
199
- process.stdin.resume()
200
- process.stdin.once('data', () => {
201
- process.stdin.pause()
202
- resolve()
203
- })
204
- })
205
- }
206
-
207
- try {
208
- // 1. Check Node + pnpm
209
- await checkEnv()
210
- loading('preparing next step...')
126
+ logo(' '),
127
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
128
+ logo(' ▒▒▓ ▓▒▓ '),
129
+ logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
130
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
131
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
132
+ logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
133
+ logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
134
+ logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
135
+ logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
136
+ logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
137
+ logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
138
+ '',
139
+ chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
140
+ chalk.dim(' Prepare your codebase for AI agents'),
141
+ ]
142
+
143
+ for (const line of bannerLines) console.log(line)
144
+ console.log()
145
+ console.log(' This tool will set up your project with a team of AI agents,')
146
+ console.log(' install skills, select models, and configure OpenCode.')
147
+ console.log()
148
+
149
+ // Only wait for Enter in a real interactive TTY
150
+ if (process.stdin.isTTY) {
151
+ console.log(chalk.bold(' Press Enter to begin...'))
152
+ console.log()
153
+ await new Promise(resolve => {
154
+ process.stdin.resume()
155
+ process.stdin.once('data', () => {
156
+ process.stdin.pause()
157
+ resolve()
158
+ })
159
+ })
160
+ }
211
161
 
212
- // 2. Choose source code scope for init analysis
162
+ try {
213
163
  const scope = await chooseSourceScope()
214
- loading('preparing next step...')
215
164
 
216
- // 3. Clean existing AI config files, detect preserved state
217
165
  const preserve = await cleanAiFiles()
218
166
  const ctx = { ...preserve, ...scope }
219
- loading('preparing next step...')
220
167
 
221
- // 4. Choose platform
222
168
  const platform = await choosePlatform()
223
- loading('preparing next step...')
224
-
225
- // 5. Check platform CLI (az or gh)
226
- await checkPlatform(platform)
227
- loading('preparing next step...')
228
169
 
229
- // 6. Copy content
230
170
  await copyContentStep(platform, ctx)
231
- loading('preparing next step...')
232
-
233
- // 6b. Patch AGENTS.md to skip steps for already-existing files
234
- await patchAgentsMd(ctx)
235
- loading('preparing next step...')
236
171
 
237
- // 7. Init OpenSpec
238
172
  await initOpenspec()
239
- loading('preparing next step...')
240
173
 
241
- // 8. Install skills
242
- const skillsSelection = await chooseSkillsProvider()
243
- loading('preparing next step...')
244
-
245
- // 9. Choose models
246
174
  const selectedModels = await chooseModels()
247
- loading('preparing next step...')
248
175
 
249
- // 10. Token optimization tools
250
- const tokenOpt = await tokenOptimizationStep({ skillsProvider: skillsSelection.additionalSkillsProvider })
176
+ const tokenOpt = await tokenOptimizationStep({ ctx })
251
177
  const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
252
- loading('preparing next step...')
253
178
 
254
- // 11. Install opencode-browser
255
179
  await installBrowser()
256
- loading('preparing next step...')
257
180
 
258
- // 12. Write onboarding metadata
259
181
  await writeOnboardConfig({
260
182
  ...ctx,
261
183
  platform,
262
- ...skillsSelection,
184
+ additionalSkillsProvider: 'npx-skills',
263
185
  ...selectedModels,
264
186
  optionalTools: { rtk, quota, caveman },
265
187
  cavemanGuidance,
266
188
  })
267
-
268
- // Done
269
- const toGenerate = [
270
- !ctx.hasDesign && 'DESIGN.md',
271
- !ctx.hasArchitecture && 'ARCHITECTURE.md',
272
- ].filter(Boolean)
273
-
274
- console.log()
275
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
276
- console.log(chalk.bold.green(' Onboarding complete!'))
277
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
278
- console.log()
279
- console.log(' Open this project in OpenCode and type:')
280
- console.log(chalk.bold(' "init"'))
281
- console.log()
189
+
190
+ const toGenerate = [
191
+ !ctx.hasDesign && 'DESIGN.md',
192
+ !ctx.hasArchitecture && 'ARCHITECTURE.md',
193
+ ].filter(Boolean)
194
+
195
+ console.log()
196
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
197
+ console.log(chalk.bold.green(' Onboarding complete!'))
198
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
199
+ console.log()
200
+ console.log(' Open this project in OpenCode and type:')
201
+ console.log(chalk.bold(' "init"'))
202
+ console.log()
282
203
  if (toGenerate.length > 0) {
283
204
  console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
284
205
  console.log(' from your actual codebase, then activate the agent team.')
@@ -287,12 +208,12 @@ try {
287
208
  }
288
209
  console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
289
210
  console.log()
290
- } catch (err) {
291
- if (err.name === 'ExitPromptError') {
292
- console.log()
293
- console.log(chalk.yellow('Cancelled.'))
294
- } else {
295
- console.error(chalk.red('\nUnexpected error:'), err.message)
296
- process.exit(1)
297
- }
298
- }
211
+ } catch (err) {
212
+ if (err.name === 'ExitPromptError') {
213
+ console.log()
214
+ console.log(chalk.yellow('Cancelled.'))
215
+ } else {
216
+ console.error(chalk.red('\nUnexpected error:'), err.message)
217
+ process.exit(1)
218
+ }
219
+ }
@@ -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