opencode-onboard 0.4.3 → 0.4.5

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 (36) hide show
  1. package/README.md +41 -40
  2. package/content/.agents/agents/devops-manager.md +123 -123
  3. package/content/.agents/skills/ob-default/SKILL.md +25 -21
  4. package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
  5. package/content/.agents/skills/ob-global/SKILL.md +92 -84
  6. package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
  7. package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
  8. package/content/.opencode/commands/create-engineer.md +109 -0
  9. package/content/.opencode/plugins/session-log.js +523 -519
  10. package/content/AGENTS.md +32 -21
  11. package/package.json +1 -1
  12. package/src/commands/wizard.js +124 -113
  13. package/src/presets/browser.json +22 -18
  14. package/src/presets/optimization.json +27 -22
  15. package/src/steps/browser/browser.test.js +115 -81
  16. package/src/steps/browser/index.js +62 -54
  17. package/src/steps/clean/index.js +108 -107
  18. package/src/steps/metadata/index.js +63 -62
  19. package/src/steps/models/format.js +61 -60
  20. package/src/steps/models/write.test.js +117 -117
  21. package/src/steps/openspec/ensemble.test.js +79 -79
  22. package/src/steps/openspec/index.js +121 -32
  23. package/src/steps/openspec/index.test.js +63 -0
  24. package/src/steps/optimization/caveman.js +34 -29
  25. package/src/steps/optimization/codegraph.js +103 -0
  26. package/src/steps/optimization/codegraph.test.js +104 -0
  27. package/src/steps/optimization/global.js +88 -64
  28. package/src/steps/optimization/global.test.js +99 -0
  29. package/src/steps/optimization/index.js +109 -101
  30. package/src/steps/optimization/optimization.test.js +101 -93
  31. package/src/steps/optimization/quota.js +84 -84
  32. package/src/steps/source/source.test.js +124 -124
  33. package/src/utils/__tests__/copy.test.js +117 -117
  34. package/src/utils/exec-spinner.js +47 -47
  35. package/src/utils/exec.js +134 -131
  36. package/src/utils/terminal.js +6 -0
package/content/AGENTS.md CHANGED
@@ -65,9 +65,11 @@ The output must be a real, populated `ARCHITECTURE.md` based on what you found i
65
65
 
66
66
  ### Step 4, Populate OpenSpec config
67
67
 
68
- Read `openspec/config.yaml`. It contains a template with commented-out examples. Fill in the `context:` field with real project information discovered during steps 1-3:
68
+ Write `openspec/config.yaml` with the real project information discovered during steps 1-3. Overwrite whatever is currently in the file. The output must contain `schema: spec-driven` and a populated `context:` block. Do not leave placeholder text.
69
69
 
70
70
  ```yaml
71
+ schema: spec-driven
72
+
71
73
  context: |
72
74
  Tech stack: <languages, frameworks, libraries found in the codebase>
73
75
  Build system: <build tools, package managers>
@@ -76,7 +78,7 @@ context: |
76
78
  Domain: <what this project does, in one line>
77
79
  ```
78
80
 
79
- Keep the `schema: spec-driven` line. Add `rules:` only if the codebase has clear conventions worth enforcing (e.g., max task size, proposal format). Do not invent rules that aren't evidenced by the codebase.
81
+ Replace every `<…>` with real values from the codebase. Add a `rules:` section only if the codebase has clear conventions worth enforcing (e.g., max task size, proposal format). Do not invent rules that aren't evidenced by the codebase.
80
82
 
81
83
  ---
82
84
 
@@ -184,11 +186,11 @@ Core tools used in this workflow:
184
186
  If a teammate stalls due to model quota/rate-limit exhaustion:
185
187
  1. `team_shutdown name:"<stuck-member>" force:true`
186
188
  2. `team_spawn` same member/task with an available model
187
- 3. `team_message` start instruction with the exact next task ID
189
+ 3. `team_message` start instruction with the exact next task ID
188
190
 
189
191
  ---
190
192
 
191
- ## Pipeline
193
+ ## Pipeline
192
194
 
193
195
  ```
194
196
  devops-manager (lead mode)
@@ -206,7 +208,7 @@ devops-manager (ship mode)
206
208
  → verify completion → commit → push → PR → post comment
207
209
  ```
208
210
 
209
- ### Phase 1, Parse & Propose
211
+ ### Phase 1, Parse & Propose
210
212
 
211
213
  ```
212
214
  1. Detect URL type → load matching skill (ob-userstory-gh or ob-userstory-az)
@@ -226,7 +228,7 @@ devops-manager (ship mode)
226
228
  - Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.
227
229
  - Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.
228
230
  2. Verify with tests/build/lint according to task scope.
229
- ```
231
+ ```
230
232
 
231
233
  ### Phase 3, Ship
232
234
 
@@ -280,7 +282,7 @@ Default `basic-engineer` abilities:
280
282
  - Infrastructure: @ob-default
281
283
  ```
282
284
 
283
- ## Skills
285
+ ## Skills
284
286
 
285
287
  Skills provide platform and tech-specific knowledge. Agents detect and load them automatically, **you never tell an agent which skill to use**.
286
288
 
@@ -299,10 +301,10 @@ Skills are located in `.agents/skills/`. Each skill has a `SKILL.md` with a desc
299
301
  | `ob-pullrequest-gh` | Create PR on GitHub |
300
302
  | `openspec-propose` | Propose change artifacts (proposal, specs, tasks) |
301
303
  | `openspec-apply-change` | Implement change with agent team |
302
- | `openspec-archive-change` | Archive completed change |
303
- | `browser-automation` | Browser automation for localhost UI, screenshots, clicks, queries |
304
-
305
- Execution rules live in skills. Keep AGENTS.md focused on orchestration and routing.
304
+ | `openspec-archive-change` | Archive completed change |
305
+ | `browser-automation` | Browser automation for localhost UI, screenshots, clicks, queries |
306
+
307
+ Execution rules live in skills. Keep AGENTS.md focused on orchestration and routing.
306
308
 
307
309
  ---
308
310
 
@@ -343,16 +345,25 @@ When `## Source Roots` lists multiple roots, each root is an independent git rep
343
345
 
344
346
  ---
345
347
 
346
- ## Guardrails
347
-
348
- Guardrails are mandatory via `ob-global` and ability-loaded skills.
349
-
350
- Minimal non-negotiables:
351
- - Never commit or push to `main`.
352
- - Never force push.
353
- - Never expose or commit secrets.
354
- - Use `gh`/`az` CLI for platform operations.
355
- - In multi-repo source scope, run git operations per repository.
348
+ ## Guardrails
349
+
350
+ Guardrails are mandatory via `ob-global` and ability-loaded skills.
351
+
352
+ Minimal non-negotiables:
353
+ - Never commit or push to `main`.
354
+ - Never force push.
355
+ - Never expose or commit secrets.
356
+ - Use `gh`/`az` CLI for platform operations.
357
+ - In multi-repo source scope, run git operations per repository.
358
+
359
+ ### Config file conflict: `opencode.jsonc` vs `.opencode/opencode.json`
360
+
361
+ This project uses `.opencode/opencode.json` as the single OpenCode configuration file. Some tools (e.g., codegraph) may create an `opencode.jsonc` file at the project root. **These two files cannot coexist.**
362
+
363
+ If you detect both `opencode.jsonc` (project root) and `.opencode/opencode.json` exist:
364
+ 1. **Stop immediately** and warn the user: "Conflicting OpenCode config files detected. This project uses `.opencode/opencode.json` only. The root `opencode.jsonc` must be removed or its contents merged into `.opencode/opencode.json`."
365
+ 2. Do NOT proceed with any task until the conflict is resolved.
366
+ 3. If the user asks you to fix it: merge any `mcpServers` or other config from `opencode.jsonc` into `.opencode/opencode.json`, then delete `opencode.jsonc`.
356
367
 
357
368
  ---
358
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -1,113 +1,124 @@
1
- import { select as wizardSelect } from '@inquirer/prompts'
2
- import chalk from 'chalk'
3
- import { chooseSourceScope } from '../steps/source/index.js'
4
- import { cleanAiFiles } from '../steps/clean/index.js'
5
- import { choosePlatform } from '../steps/platform/index.js'
6
- import { copyContentStep } from '../steps/copy/index.js'
7
- import { initOpenspec } from '../steps/openspec/index.js'
8
- import { chooseModels } from '../steps/models/index.js'
9
- import { tokenOptimizationStep } from '../steps/optimization/index.js'
10
- import { installBrowser } from '../steps/browser/index.js'
11
- import { writeOnboardConfig } from '../steps/metadata/index.js'
12
-
13
- export async function runWizard(version) {
14
- const logo = chalk.hex('#fe3d57')
15
- const bannerLines = [
16
- logo(' '),
17
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
18
- logo(' ▒▒▓ ▓▒▓ '),
19
- logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
20
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
21
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
22
- logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
23
- logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
24
- logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
25
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
26
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
27
- logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
28
- '',
29
- chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
30
- chalk.dim(' Prepare your codebase for AI agents'),
31
- ]
32
-
33
- for (const line of bannerLines) console.log(line)
34
- console.log()
35
- console.log(' This tool will set up your project with a team of AI agents,')
36
- console.log(' install skills, select models, and configure OpenCode.')
37
- console.log()
38
-
39
- // Only wait for Enter in a real interactive TTY
40
- if (process.stdin.isTTY) {
41
- console.log(chalk.bold(' Press Enter to begin...'))
42
- console.log()
43
- await new Promise(resolve => {
44
- process.stdin.resume()
45
- process.stdin.once('data', () => {
46
- process.stdin.pause()
47
- resolve()
48
- })
49
- })
50
- }
51
-
52
- const scope = await chooseSourceScope()
53
-
54
- const maxConcurrentAgents = await wizardSelect({
55
- message: 'Max concurrent agents:',
56
- default: 4,
57
- choices: [
58
- { name: '2', value: 2, description: 'Conservative — lower resource usage' },
59
- { name: '3', value: 3, description: 'Moderate parallelism' },
60
- { name: '4 (default)', value: 4, description: 'Recommended for most projects' },
61
- { name: '5', value: 5, description: 'High parallelism — requires more resources' },
62
- { name: '6', value: 6, description: 'Maximum parallelism' },
63
- ],
64
- })
65
-
66
- const preserve = await cleanAiFiles()
67
- const ctx = { ...preserve, ...scope, maxConcurrentAgents }
68
-
69
- const platform = await choosePlatform()
70
-
71
- await copyContentStep(platform, ctx)
72
-
73
- await initOpenspec()
74
-
75
- const selectedModels = await chooseModels()
76
-
77
- const tokenOpt = await tokenOptimizationStep({ ctx })
78
- const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
79
-
80
- await installBrowser()
81
-
82
- await writeOnboardConfig({
83
- ...ctx,
84
- platform,
85
- maxConcurrentAgents,
86
- additionalSkillsProvider: 'npx-skills',
87
- ...selectedModels,
88
- optionalTools: { rtk, quota, caveman },
89
- cavemanGuidance,
90
- })
91
-
92
- const toGenerate = [
93
- !ctx.hasDesign && 'DESIGN.md',
94
- !ctx.hasArchitecture && 'ARCHITECTURE.md',
95
- ].filter(Boolean)
96
-
97
- console.log()
98
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
99
- console.log(chalk.bold.green(' Onboarding complete!'))
100
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
101
- console.log()
102
- console.log(' Open this project in OpenCode and type:')
103
- console.log(chalk.bold(' "init"'))
104
- console.log()
105
- if (toGenerate.length > 0) {
106
- console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
107
- console.log(' from your actual codebase, then activate the agent team.')
108
- } else {
109
- console.log(' OpenCode will activate the agent team.')
110
- }
111
- console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
112
- console.log()
113
- }
1
+ import { select as wizardSelect } from '@inquirer/prompts'
2
+ import chalk from 'chalk'
3
+ import { chooseSourceScope } from '../steps/source/index.js'
4
+ import { cleanAiFiles } from '../steps/clean/index.js'
5
+ import { choosePlatform } from '../steps/platform/index.js'
6
+ import { copyContentStep } from '../steps/copy/index.js'
7
+ import { initOpenspec } from '../steps/openspec/index.js'
8
+ import { chooseModels } from '../steps/models/index.js'
9
+ import { tokenOptimizationStep } from '../steps/optimization/index.js'
10
+ import { installBrowser } from '../steps/browser/index.js'
11
+ import { writeOnboardConfig } from '../steps/metadata/index.js'
12
+
13
+ export async function runWizard(version) {
14
+ const logo = chalk.hex('#fe3d57')
15
+ const bannerLines = [
16
+ logo(' '),
17
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
18
+ logo(' ▒▒▓ ▓▒▓ '),
19
+ logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
20
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
21
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
22
+ logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
23
+ logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
24
+ logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
25
+ logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
26
+ logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
27
+ logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
28
+ '',
29
+ chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
30
+ chalk.dim(' Prepare your codebase for AI agents'),
31
+ ]
32
+
33
+ for (const line of bannerLines) console.log(line)
34
+ console.log()
35
+ console.log(' This tool will set up your project with a team of AI agents,')
36
+ console.log(' install skills, select models, and configure OpenCode.')
37
+ console.log()
38
+
39
+ // Only wait for Enter in a real interactive TTY
40
+ if (process.stdin.isTTY) {
41
+ console.log(chalk.bold(' Press Enter to begin...'))
42
+ console.log()
43
+ await new Promise(resolve => {
44
+ process.stdin.resume()
45
+ process.stdin.once('data', () => {
46
+ process.stdin.pause()
47
+ resolve()
48
+ })
49
+ })
50
+ }
51
+
52
+ const scope = await chooseSourceScope()
53
+
54
+ const maxConcurrentAgents = await wizardSelect({
55
+ message: 'Max concurrent agents:',
56
+ default: 4,
57
+ choices: [
58
+ { name: '2', value: 2, description: 'Conservative — lower resource usage' },
59
+ { name: '3', value: 3, description: 'Moderate parallelism' },
60
+ { name: '4 (default)', value: 4, description: 'Recommended for most projects' },
61
+ { name: '5', value: 5, description: 'High parallelism — requires more resources' },
62
+ { name: '6', value: 6, description: 'Maximum parallelism' },
63
+ ],
64
+ })
65
+
66
+ const installScope = await wizardSelect({
67
+ message: 'Install dependencies:',
68
+ default: 'local',
69
+ choices: [
70
+ { name: 'Locally (default)', value: 'local', description: 'Install tools project-locally where possible' },
71
+ { name: 'Globally', value: 'global', description: 'Install tools globally (affects all projects on this machine)' },
72
+ ],
73
+ })
74
+
75
+ const preserve = await cleanAiFiles()
76
+ const ctx = { ...preserve, ...scope, maxConcurrentAgents, installScope }
77
+
78
+ const platform = await choosePlatform()
79
+
80
+ await copyContentStep(platform, ctx)
81
+
82
+ const openspec = await initOpenspec()
83
+
84
+ const selectedModels = await chooseModels()
85
+
86
+ const tokenOpt = await tokenOptimizationStep({ ctx })
87
+ const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
88
+
89
+ await installBrowser(ctx)
90
+
91
+ await writeOnboardConfig({
92
+ ...ctx,
93
+ platform,
94
+ openspec,
95
+ maxConcurrentAgents,
96
+ installScope,
97
+ additionalSkillsProvider: 'npx-skills',
98
+ ...selectedModels,
99
+ optionalTools: { rtk, quota, caveman },
100
+ cavemanGuidance,
101
+ })
102
+
103
+ const toGenerate = [
104
+ !ctx.hasDesign && 'DESIGN.md',
105
+ !ctx.hasArchitecture && 'ARCHITECTURE.md',
106
+ ].filter(Boolean)
107
+
108
+ console.log()
109
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
110
+ console.log(chalk.bold.green(' Onboarding complete!'))
111
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
112
+ console.log()
113
+ console.log(' Open this project in OpenCode and type:')
114
+ console.log(chalk.bold(' "init"'))
115
+ console.log()
116
+ if (toGenerate.length > 0) {
117
+ console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
118
+ console.log(' from your actual codebase, then activate the agent team.')
119
+ } else {
120
+ console.log(' OpenCode will activate the agent team.')
121
+ }
122
+ console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
123
+ console.log()
124
+ }
@@ -1,18 +1,22 @@
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
- }
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
+ "locationChoices": {
11
+ "local": "2",
12
+ "global": "1"
13
+ },
14
+ "autoAnswers": [
15
+ { "trigger": "Press Enter when", "response": "" },
16
+ { "trigger": "Choose config location", "response": "__LOCATION__" },
17
+ { "trigger": "Add plugin automatically?", "response": "y" },
18
+ { "trigger": "Create one?", "response": "y" },
19
+ { "trigger": "Add browser-automation skill", "response": "n" },
20
+ { "trigger": "Check broker", "response": "n" }
21
+ ]
22
+ }
@@ -1,22 +1,27 @@
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
+ {
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
+ "name": "codegraph semantic index (recommended)",
23
+ "value": "codegraph",
24
+ "checked": true
25
+ }
26
+ ]
27
+ }
@@ -1,81 +1,115 @@
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
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { installBrowser } from './index.js'
3
+
4
+ vi.mock('../../utils/exec.js', () => ({
5
+ header: vi.fn(),
6
+ info: vi.fn(),
7
+ success: vi.fn(),
8
+ warn: vi.fn(),
9
+ error: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('fs-extra', () => ({
13
+ default: {
14
+ readJson: vi.fn().mockResolvedValue({
15
+ installer: { command: 'npx', args: ['@different-ai/opencode-browser', 'install'] },
16
+ output: { showAfter: '===', hideAfter: '===' },
17
+ locationChoices: { local: '2', global: '1' },
18
+ autoAnswers: [
19
+ { trigger: 'Install', response: 'y' },
20
+ { trigger: 'Choose config location', response: '__LOCATION__' },
21
+ ],
22
+ }),
23
+ },
24
+ }))
25
+
26
+ vi.mock('execa', () => ({
27
+ execa: vi.fn(),
28
+ }))
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
+
82
+ it('resolves __LOCATION__ to local answer by default', async () => {
83
+ const { execa } = await import('execa')
84
+ let capturedTriggers = null
85
+ const mockChild = {
86
+ stdout: { on: vi.fn((_, cb) => { capturedTriggers = cb }) },
87
+ stderr: { on: vi.fn() },
88
+ stdin: { write: vi.fn() },
89
+ then: (cb) => cb({ exitCode: 0 }),
90
+ }
91
+ execa.mockReturnValue(mockChild)
92
+
93
+ await installBrowser()
94
+
95
+ if (capturedTriggers) capturedTriggers(Buffer.from('Choose config location'))
96
+ expect(mockChild.stdin.write).toHaveBeenCalledWith('2\n')
97
+ })
98
+
99
+ it('resolves __LOCATION__ to global answer when installScope is global', async () => {
100
+ const { execa } = await import('execa')
101
+ let capturedTriggers = null
102
+ const mockChild = {
103
+ stdout: { on: vi.fn((_, cb) => { capturedTriggers = cb }) },
104
+ stderr: { on: vi.fn() },
105
+ stdin: { write: vi.fn() },
106
+ then: (cb) => cb({ exitCode: 0 }),
107
+ }
108
+ execa.mockReturnValue(mockChild)
109
+
110
+ await installBrowser({ installScope: 'global' })
111
+
112
+ if (capturedTriggers) capturedTriggers(Buffer.from('Choose config location'))
113
+ expect(mockChild.stdin.write).toHaveBeenCalledWith('1\n')
114
+ })
115
+ })