@rockysoft/opencode-hybrid-workflow 0.9.0 → 1.0.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.
package/README.md CHANGED
@@ -125,7 +125,7 @@ Confirm installation of Playwright and Firefox.
125
125
 
126
126
  ![Confirm browser installation](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.3.2-beta.3/docs/images/11-browser-confirm.png)
127
127
 
128
- Each reviewer gets an isolated browser for a captured frontend and disposable API fixtures. Ask the agent to add [repeatable browser checks](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.8.0/docs/browser-evidence.md). Live backend testing stays with your application's integration tests.
128
+ Each reviewer gets an isolated browser for a captured frontend and disposable API fixtures. Ask the agent to add [repeatable browser checks](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@1.0.0/docs/browser-evidence.md). Live backend testing stays with your application's integration tests.
129
129
 
130
130
  ![Browser support installed](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.3.2-beta.3/docs/images/12-browser-ready.png)
131
131
 
@@ -175,10 +175,10 @@ Remove the old plugin entries from `opencode.json` and `cli.json` before adding
175
175
 
176
176
  </details>
177
177
 
178
- [Advanced configuration](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.4.0/docs/configuration.md) · [MIT license](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.4.0/LICENSE) · [Third-party notices](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.4.0/THIRD_PARTY_NOTICES.md)
178
+ [Advanced configuration](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@1.0.0/docs/configuration.md) · [MIT license](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@1.0.0/LICENSE) · [Third-party notices](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@1.0.0/THIRD_PARTY_NOTICES.md)
179
179
 
180
- For deterministic Vitest projects, see [faster verification](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.7.0/docs/verification.md). Full checks remain the default; eligible projects automatically choose full checks when measured optimization overhead is higher.
180
+ For deterministic Vitest projects, see [faster verification](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@1.0.0/docs/verification.md). Full checks remain the default; eligible projects automatically choose full checks when measured optimization overhead is higher.
181
181
 
182
- [Repeatable browser checks](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.8.0/docs/browser-evidence.md) capture changed frontend states once for both reviewers, while keeping their independent inspections and approval decisions.
183
-
184
- [Engineering handoffs](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.9.0/docs/engineering-handoffs.md) give implementation agents concrete responsibility and state ownership. Reviewers check the code against the plan; small changes keep existing patterns, with no file-length limits or extra planning phase.
182
+ [Repeatable browser checks](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@1.0.0/docs/browser-evidence.md) capture changed frontend states once for both reviewers, while keeping their independent inspections and approval decisions.
183
+
184
+ [Engineering handoffs](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@1.0.0/docs/engineering-handoffs.md) give implementation agents concrete responsibility and state ownership. Reviewers check the code against the plan; small changes keep existing patterns, with no file-length limits or extra planning phase.
@@ -2,13 +2,13 @@
2
2
  name: ui-ux-pro-max
3
3
  description: "Search local UI/UX references for palettes, typography, layout patterns, accessibility, and design systems. Use for new visual directions or focused design questions."
4
4
  ---
5
-
5
+
6
6
  ## OpenCode local setup
7
7
 
8
8
  User and repository requirements take precedence over generic style recommendations. Preserve an established design system unless a redesign was requested. Verify external APIs against current official documentation using Context7/webfetch; bundled reference data is not proof of current API behavior.
9
9
 
10
- Use actual screenshots for visual inspection and DOM/accessibility/interaction checks for behavior. Never infer visual approval from captions or hashes.
11
-
10
+ Use actual screenshots for visual inspection and DOM/accessibility/interaction checks for behavior. Never infer visual approval from captions or hashes.
11
+
12
12
 
13
13
  # UI/UX Pro Max - Design Intelligence
14
14
 
@@ -45,11 +45,11 @@ For the full rule list per category (all 119 UX guidelines with rationale), read
45
45
 
46
46
  The search script lives inside this skill's own directory, not the project directory. Always invoke it by its full path — do not assume a particular working directory:
47
47
 
48
- ```powershell
49
- python scripts/search.py "<query>" --domain <domain>
48
+ ```text
49
+ <python3-executable> "<skill-directory>/scripts/search.py" "<query>" --domain <domain>
50
50
  ```
51
51
 
52
- The examples use the existing local Python 3.12 installation. The search tool needs no external Python packages. If this executable is missing, report that rather than silently installing a different runtime.
52
+ The examples below are command templates, not literal shell commands. Replace `<skill-directory>` with the absolute directory containing this SKILL.md. On macOS/Linux, use `python3`; on Windows, use the configured Python 3 executable (usually `python` or `py -3`). If the executable path contains spaces, quote it; PowerShell additionally needs the call operator `&` before a quoted executable path. The plugin's automated research uses its configured `design.pythonExecutable` and resolves the script path itself. Do not assume a particular Python installation or shell. The search tool needs no external Python packages. If Python 3 is unavailable, report the missing prerequisite rather than silently installing another runtime.
53
53
 
54
54
  ## Workflow
55
55
 
@@ -81,23 +81,23 @@ Extract from the user request:
81
81
 
82
82
  Use `--design-system` when the task needs a coherent product-wide visual direction:
83
83
 
84
- ```powershell
85
- python scripts/search.py "<product_type> <industry> <keywords>" --design-system [-p "Project Name"]
84
+ ```text
85
+ <python3-executable> "<skill-directory>/scripts/search.py" "<product_type> <industry> <keywords>" --design-system [-p "Project Name"]
86
86
  ```
87
87
 
88
88
  This aggregates product/style/color/landing/typography matches, applies reasoning rules from `ui-reasoning.csv`, and returns pattern, style, colors, typography, effects, and anti-patterns to avoid.
89
89
 
90
90
  **Example:**
91
- ```powershell
92
- python scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa"
91
+ ```text
92
+ <python3-executable> "<skill-directory>/scripts/search.py" "beauty spa wellness service" --design-system -p "Serenity Spa"
93
93
  ```
94
94
 
95
95
  ### Step 2b: Persist Design System (Master + Overrides Pattern)
96
96
 
97
97
  To save the design system for retrieval across sessions, add `--persist` **and always pass `--output-dir` pointed at the project root** — without it, files are written relative to whatever directory the tool happens to run from:
98
98
 
99
- ```powershell
100
- python scripts/search.py "<query>" --design-system --persist -p "Project Name" --output-dir "<project-root>"
99
+ ```text
100
+ <python3-executable> "<skill-directory>/scripts/search.py" "<query>" --design-system --persist -p "Project Name" --output-dir "<project-root>"
101
101
  ```
102
102
 
103
103
  This creates:
@@ -119,8 +119,8 @@ Read an existing `MASTER.md` before deciding whether `--force` is justified. Nev
119
119
 
120
120
  Three optional 1-10 sliders that tune `--design-system` output without changing your query. Add any combination of them to the same command:
121
121
 
122
- ```powershell
123
- python scripts/search.py "<query>" --design-system --variance <1-10> --motion <1-10> --density <1-10>
122
+ ```text
123
+ <python3-executable> "<skill-directory>/scripts/search.py" "<query>" --design-system --variance <1-10> --motion <1-10> --density <1-10>
124
124
  ```
125
125
 
126
126
  | Dial | Low (1-3) | Mid (4-7) | High (8-10) |
@@ -134,14 +134,14 @@ python scripts/search.py "<query>" --design-system --variance <1-10> --motion <1
134
134
  - Leaving a dial unset keeps that part of the output exactly as it was before (no behavior change).
135
135
 
136
136
  **Example:**
137
- ```powershell
138
- python scripts/search.py "internal analytics dashboard" --design-system --variance 8 --motion 7 --density 8 -p "Ops Console"
137
+ ```text
138
+ <python3-executable> "<skill-directory>/scripts/search.py" "internal analytics dashboard" --design-system --variance 8 --motion 7 --density 8 -p "Ops Console"
139
139
  ```
140
140
 
141
141
  ### Step 3: Supplement with Detailed Searches (as needed)
142
142
 
143
- ```powershell
144
- python scripts/search.py "<keyword>" --domain <domain> [-n <max_results>]
143
+ ```text
144
+ <python3-executable> "<skill-directory>/scripts/search.py" "<keyword>" --domain <domain> [-n <max_results>]
145
145
  ```
146
146
 
147
147
  | Need | Domain | Example |
@@ -163,8 +163,8 @@ Domain is auto-detected from the query if `--domain` is omitted — but auto-det
163
163
 
164
164
  ### Step 4: Stack Guidelines
165
165
 
166
- ```powershell
167
- python scripts/search.py "<keyword>" --stack <stack>
166
+ ```text
167
+ <python3-executable> "<skill-directory>/scripts/search.py" "<keyword>" --stack <stack>
168
168
  ```
169
169
 
170
170
  **Available stacks:** `react`, `nextjs`, `vue`, `svelte`, `astro`, `nuxtjs`, `nuxt-ui`, `angular`, `laravel`, `swiftui`, `react-native`, `flutter`, `jetpack-compose`, `html-tailwind`, `shadcn`, `threejs`, `javafx`, `wpf`, `winui`, `avalonia`, `uno`, `uwp`. Use the stack detected in Step 1.
@@ -182,15 +182,15 @@ Do not fabricate output. Instead:
182
182
 
183
183
  **User request:** "Make an AI search homepage." (stack detected as Next.js from `package.json`)
184
184
 
185
- ```powershell
185
+ ```text
186
186
  # Step 2: design system
187
- python scripts/search.py "AI search tool modern minimal" --design-system -p "AI Search"
187
+ <python3-executable> "<skill-directory>/scripts/search.py" "AI search tool modern minimal" --design-system -p "AI Search"
188
188
 
189
189
  # Step 3: supplement
190
- python scripts/search.py "keyboard focus modal" --domain ux
190
+ <python3-executable> "<skill-directory>/scripts/search.py" "keyboard focus modal" --domain ux
191
191
 
192
192
  # Step 4: stack guidelines
193
- python scripts/search.py "suspense streaming bundle" --stack nextjs
193
+ <python3-executable> "<skill-directory>/scripts/search.py" "suspense streaming bundle" --stack nextjs
194
194
  ```
195
195
 
196
196
  Then synthesize the design system + detailed searches and implement.
@@ -218,4 +218,4 @@ Then synthesize the design system + detailed searches and implement.
218
218
 
219
219
  ## Before Delivering App UI
220
220
 
221
- Read `references/pro-rules.md` and run through its canonical Pre-Delivery Checklist. It covers icon/visual-element discipline, interaction feedback, light/dark contrast, safe-area layout, and accessibility — scoped to native/mobile app UI (iOS/Android/React Native/Flutter).
221
+ Read `references/pro-rules.md` and run through its canonical Pre-Delivery Checklist. It covers icon/visual-element discipline, interaction feedback, light/dark contrast, safe-area layout, and accessibility — scoped to native/mobile app UI (iOS/Android/React Native/Flutter).
package/cli.mjs CHANGED
@@ -1,91 +1,91 @@
1
- #!/usr/bin/env node
2
- import {modelCatalog,selection,selectInteractively} from './lib/select-reviewers.mjs';
3
- import {lanes,reviewer} from './lib/reviewers.mjs';
4
- import {readFileSync,existsSync} from 'node:fs';
5
- import {spawnSync} from 'node:child_process';
6
- import path from 'node:path';
7
- import {readSettings,configDirectory,settingsFile,defaultSettings} from './lib/config.mjs';
8
- import {install,parseJsonc} from './lib/install.mjs';
9
- import {validateBrowser} from './lib/reviewer-browser.mjs';
10
- import {commandSpec,nativeCodex} from './lib/commands.mjs';
11
- import {requireSubscription,workerEnvironment} from './core/workers.mjs';
12
- const args=process.argv.slice(2),command=args.shift()??'help';
13
- function option(name){const i=args.indexOf(name);if(i<0)return undefined;const value=args[i+1];if(!value||value.startsWith('--'))throw Error(`Missing ${name} value.`);return value;}
14
- const root=path.resolve(option('--config-dir')??configDirectory());
15
- // An explicit CLI profile is a human-selected trust root, unlike project plugin options.
16
- process.env.OPENCODE_CONFIG_DIR=root;
17
- const opts={configFile:path.join(root,'hybrid','config.json')};
18
- async function main(){
19
- if(command==='init') {
20
- const patch={};
21
- if(option('--legacy-config')) {
22
- const old=JSON.parse(readFileSync(path.resolve(option('--legacy-config')),'utf8'));
23
- const project=option('--project');if(!project)throw Error('--legacy-config requires --project; history is mapped only for that project.');
24
- const defaults=defaultSettings();
25
- patch.reviewers={astra:{...defaults.reviewers.astra,...old.codex},opus:{...defaults.reviewers.opus,...old.claude}};
26
- patch.legacyReviewerOverrides={...(existsSync(opts.configFile)?JSON.parse(readFileSync(opts.configFile,'utf8')).legacyReviewerOverrides:{}),[path.resolve(project)]:{reviewers:patch.reviewers,designAuthor:'opus',codex:patch.reviewers.astra,claude:patch.reviewers.opus}};
27
- patch.verification={executable:old.pnpmExecutable??'pnpm',timeoutMs:0};
28
- patch.projectRuntimeOverrides={...(existsSync(opts.configFile)?JSON.parse(readFileSync(opts.configFile,'utf8')).projectRuntimeOverrides:{}),[path.resolve(project)]:old.runtimeDirectory};
29
- if(old.design?.pythonExecutable)patch.design={pythonExecutable:old.design.pythonExecutable};
30
- }else if(!existsSync(opts.configFile)) {
31
- patch.setupComplete=false;
32
- try{const defaults=defaultSettings();defaults.reviewers.astra.executable=nativeCodex('codex');patch.reviewers=defaults.reviewers;}catch{/* doctor explains missing commands without preventing installation */}
33
- }
34
- console.log(JSON.stringify(install({root,settingsPatch:patch,replaceLegacyPackage:option('--legacy-config')?path.dirname(path.resolve(option('--legacy-config'))):undefined,apply:!args.includes('--dry-run')}),null,2));
35
- return;
36
- }
37
- if(command==='config'){console.log(settingsFile(opts));return;}
38
- if(command==='browser') {
39
- if(!args.includes('--enable')&&!args.includes('--disable'))throw Error('Use browser --enable or browser --disable.');
40
- if(args.includes('--enable')&&args.includes('--disable'))throw Error('Choose enable or disable, not both.');
41
- let reviewBrowser={enabled:false};
42
- if(args.includes('--enable')) {
43
- // Read only the human-owned GLOBAL config, never project-supplied executables.
44
- const jsonc=path.join(root,'opencode.jsonc'),json=path.join(root,'opencode.json');
45
- const oc=parseJsonc(readFileSync(existsSync(jsonc)?jsonc:json,'utf8'));
46
- const server=oc.mcp?.servers?.playwright;
47
- if(server?.type!=='local'||!Array.isArray(server.command))throw Error('Configure a local Playwright MCP in global OpenCode first, or set reviewBrowser.command in hybrid/config.json.');
48
- // Keep executable + entry script only. Drop shared-profile/CDP/storage flags.
49
- const boundary=server.command.findIndex((v,i)=>i>0&&v.startsWith('--'));
50
- const command=server.command.slice(0,boundary<0?undefined:boundary);
51
- if(command.some(v=>/^(-y|install|add)$/.test(v)))throw Error('Use an installed Playwright MCP command, not an on-demand package download.');
52
- const browserAt=server.command.indexOf('--browser');
53
- reviewBrowser={enabled:true,command,browser:browserAt>=0?server.command[browserAt+1]:'chrome',headless:true};
54
- validateBrowser(reviewBrowser);commandSpec(command[0]);
55
- }
56
- console.log(JSON.stringify(install({root,settingsPatch:{reviewBrowser}}),null,2));
57
- console.log('Reviewer browser setting saved. Restart OpenCode to load it. CLI and OpenCode-provider reviewers each receive their own isolated browser.');return;
58
- }
59
- if(command==='models') {console.log(JSON.stringify(modelCatalog(option('--project')??process.cwd()).map(m=>({provider:m.providerID,model:m.id,name:m.name,variants:(m.variants??[]).map(v=>v.id),images:m.capabilities.input.includes('image')})),null,2));return;}
60
- if(command==='configure') {
61
- const config=readSettings(opts);let patch;
62
- if(option('--reviewer1')) {
63
- const first=option('--reviewer1'),second=option('--reviewer2')??'none';
64
- const catalog=[first,second].some(v=>v.startsWith('opencode:'))?modelCatalog(option('--project')??process.cwd()):[];
65
- const reviewers={astra:selection(first,{catalog,previous:config.reviewers.astra,variant:option('--variant1')}),opus:selection(second,{catalog,previous:config.reviewers.opus??{},variant:option('--variant2')})};
66
- if(!reviewers.astra)throw Error('Reviewer 1 is required.');
67
- const designAuthor=option('--lead')==='1'?'astra':reviewers.opus?'opus':'astra';
68
- patch={reviewers,designAuthor};
69
- }else patch=await selectInteractively(config);
70
- console.log(JSON.stringify(install({root,settingsPatch:{...patch,setupComplete:true}}),null,2));
71
- console.log('Reviewer defaults saved for new tasks. Active tasks retain their original reviewer assignments and findings.');return;
72
- }
73
-
74
- if(command==='doctor') {
75
- const config=readSettings(opts),checks=[];
76
- function check(name,run){try{checks.push({name,ok:true,...run()});}catch(error){checks.push({name,ok:false,error:error.message});}}
77
- const version=executable=>{const spec=commandSpec(executable);const r=spawnSync(spec.executable,[...spec.args,'--version'],{encoding:'utf8',windowsHide:true,env:workerEnvironment(),timeout:15000});if(r.status!==0)throw Error('Version check failed; check executable installation.');return {version:(r.stdout||r.stderr).trim().split('\n')[0]};};
78
- check('OpenCode',()=>{const result=version('opencode');if(!/^(?:opencode\s+v?)?2\./i.test(result.version))throw Error('OpenCode 2 is required.');return result;});
79
- for(const who of lanes(config))check(who,()=>{const lane=reviewer(config,who);if(lane.adapter==='opencode'){const catalog=modelCatalog();selection(`opencode:${lane.providerID}/${lane.model}`,{catalog,variant:lane.variant});return {adapter:lane.adapter,model:lane.model,variant:lane.variant??'default',catalogVerified:true};}const result=version(lane.executable);if(!args.includes('--no-auth'))requireSubscription(who,config,root);return {...result,model:lane.model,effort:lane.effort,authChecked:!args.includes('--no-auth')};});
80
- check('git',()=>version('git'));check('verification',()=>version(config.pnpmExecutable));check('design Python',()=>version(config.design.pythonExecutable));
81
- if(config.reviewBrowser?.enabled)check('reviewer browser',()=>{const b=config.reviewBrowser;validateBrowser(b);const spec=commandSpec(b.command[0]);const r=spawnSync(spec.executable,[...spec.args,...b.command.slice(1),'--version'],{encoding:'utf8',windowsHide:true,env:workerEnvironment(),timeout:15000});if(r.status!==0)throw Error('Playwright MCP version check failed.');return {version:r.stdout.trim(),browser:b.browser??'chrome',scope:'CLI and OpenCode-provider reviewers; separate isolated sessions'};});
82
- for(const skill of ['frontend-design','ui-ux-pro-max','web-design-guidelines'])check(skill,()=>{if(!existsSync(path.join(config.design.skillsDirectory,skill,'SKILL.md')))throw Error('Missing bundled skill');return {};});
83
- console.log(JSON.stringify({configFile:opts.configFile,platform:process.platform,checks,modelCalls:0},null,2));
84
- if(checks.some(c=>!c.ok))process.exitCode=1;
85
- return;
86
- }
87
- if(command!=='help')throw Error(`Unknown command: ${command}`);
88
- console.log('opencode-hybrid browser --enable|--disable [--config-dir PATH] (independent reviewer browsers)');
89
- console.log('opencode-hybrid configure [--config-dir PATH] (interactive reviewer selection)\nopencode-hybrid models [--project PATH]\nopencode-hybrid configure --reviewer1 SOURCE:MODEL --reviewer2 SOURCE:MODEL|none [--variant1 NAME] [--variant2 NAME] [--lead 1|2]\nopencode-hybrid init [--config-dir PATH] [--dry-run]\nopencode-hybrid doctor [--config-dir PATH] [--no-auth]\nopencode-hybrid config [--config-dir PATH]\nMigration: init --legacy-config FILE --project PROJECT\nAfter init, open any project with ordinary opencode. Configuration is user-owned; installation never copies credentials.');
90
- }
91
- main().catch(error=>{console.error(error.message);process.exitCode=1;});
1
+ #!/usr/bin/env node
2
+ import {modelCatalog,selection,selectInteractively} from './lib/select-reviewers.mjs';
3
+ import {lanes,reviewer} from './lib/reviewers.mjs';
4
+ import {readFileSync,existsSync} from 'node:fs';
5
+ import {spawnSync} from 'node:child_process';
6
+ import path from 'node:path';
7
+ import {readSettings,configDirectory,settingsFile,defaultSettings} from './lib/config.mjs';
8
+ import {install,parseJsonc} from './lib/install.mjs';
9
+ import {validateBrowser} from './lib/reviewer-browser.mjs';
10
+ import {commandSpec,nativeCodex} from './lib/commands.mjs';
11
+ import {requireCliAuthentication,workerEnvironment} from './core/workers.mjs';
12
+ const args=process.argv.slice(2),command=args.shift()??'help';
13
+ function option(name){const i=args.indexOf(name);if(i<0)return undefined;const value=args[i+1];if(!value||value.startsWith('--'))throw Error(`Missing ${name} value.`);return value;}
14
+ const root=path.resolve(option('--config-dir')??configDirectory());
15
+ // An explicit CLI profile is a human-selected trust root, unlike project plugin options.
16
+ process.env.OPENCODE_CONFIG_DIR=root;
17
+ const opts={configFile:path.join(root,'hybrid','config.json')};
18
+ async function main(){
19
+ if(command==='init') {
20
+ const patch={};
21
+ if(option('--legacy-config')) {
22
+ const old=JSON.parse(readFileSync(path.resolve(option('--legacy-config')),'utf8'));
23
+ const project=option('--project');if(!project)throw Error('--legacy-config requires --project; history is mapped only for that project.');
24
+ const defaults=defaultSettings();
25
+ patch.reviewers={astra:{...defaults.reviewers.astra,...old.codex},opus:{...defaults.reviewers.opus,...old.claude}};
26
+ patch.legacyReviewerOverrides={...(existsSync(opts.configFile)?JSON.parse(readFileSync(opts.configFile,'utf8')).legacyReviewerOverrides:{}),[path.resolve(project)]:{reviewers:patch.reviewers,designAuthor:'opus',codex:patch.reviewers.astra,claude:patch.reviewers.opus}};
27
+ patch.verification={executable:old.pnpmExecutable??'pnpm',timeoutMs:0};
28
+ patch.projectRuntimeOverrides={...(existsSync(opts.configFile)?JSON.parse(readFileSync(opts.configFile,'utf8')).projectRuntimeOverrides:{}),[path.resolve(project)]:old.runtimeDirectory};
29
+ if(old.design?.pythonExecutable)patch.design={pythonExecutable:old.design.pythonExecutable};
30
+ }else if(!existsSync(opts.configFile)) {
31
+ patch.setupComplete=false;
32
+ try{const defaults=defaultSettings();defaults.reviewers.astra.executable=nativeCodex('codex');patch.reviewers=defaults.reviewers;}catch{/* doctor explains missing commands without preventing installation */}
33
+ }
34
+ console.log(JSON.stringify(install({root,settingsPatch:patch,replaceLegacyPackage:option('--legacy-config')?path.dirname(path.resolve(option('--legacy-config'))):undefined,apply:!args.includes('--dry-run')}),null,2));
35
+ return;
36
+ }
37
+ if(command==='config'){console.log(settingsFile(opts));return;}
38
+ if(command==='browser') {
39
+ if(!args.includes('--enable')&&!args.includes('--disable'))throw Error('Use browser --enable or browser --disable.');
40
+ if(args.includes('--enable')&&args.includes('--disable'))throw Error('Choose enable or disable, not both.');
41
+ let reviewBrowser={enabled:false};
42
+ if(args.includes('--enable')) {
43
+ // Read only the human-owned GLOBAL config, never project-supplied executables.
44
+ const jsonc=path.join(root,'opencode.jsonc'),json=path.join(root,'opencode.json');
45
+ const oc=parseJsonc(readFileSync(existsSync(jsonc)?jsonc:json,'utf8'));
46
+ const server=oc.mcp?.servers?.playwright;
47
+ if(server?.type!=='local'||!Array.isArray(server.command))throw Error('Configure a local Playwright MCP in global OpenCode first, or set reviewBrowser.command in hybrid/config.json.');
48
+ // Keep executable + entry script only. Drop shared-profile/CDP/storage flags.
49
+ const boundary=server.command.findIndex((v,i)=>i>0&&v.startsWith('--'));
50
+ const command=server.command.slice(0,boundary<0?undefined:boundary);
51
+ if(command.some(v=>/^(-y|install|add)$/.test(v)))throw Error('Use an installed Playwright MCP command, not an on-demand package download.');
52
+ const browserAt=server.command.indexOf('--browser');
53
+ reviewBrowser={enabled:true,command,browser:browserAt>=0?server.command[browserAt+1]:'chrome',headless:true};
54
+ validateBrowser(reviewBrowser);commandSpec(command[0]);
55
+ }
56
+ console.log(JSON.stringify(install({root,settingsPatch:{reviewBrowser}}),null,2));
57
+ console.log('Reviewer browser setting saved. Restart OpenCode to load it. CLI and OpenCode-provider reviewers each receive their own isolated browser.');return;
58
+ }
59
+ if(command==='models') {console.log(JSON.stringify(modelCatalog(option('--project')??process.cwd()).map(m=>({provider:m.providerID,model:m.id,name:m.name,variants:(m.variants??[]).map(v=>v.id),images:m.capabilities.input.includes('image')})),null,2));return;}
60
+ if(command==='configure') {
61
+ const config=readSettings(opts);let patch;
62
+ if(option('--reviewer1')) {
63
+ const first=option('--reviewer1'),second=option('--reviewer2')??'none';
64
+ const catalog=[first,second].some(v=>v.startsWith('opencode:'))?modelCatalog(option('--project')??process.cwd()):[];
65
+ const reviewers={astra:selection(first,{catalog,previous:config.reviewers.astra,variant:option('--variant1')}),opus:selection(second,{catalog,previous:config.reviewers.opus??{},variant:option('--variant2')})};
66
+ if(!reviewers.astra)throw Error('Reviewer 1 is required.');
67
+ const designAuthor=option('--lead')==='1'?'astra':reviewers.opus?'opus':'astra';
68
+ patch={reviewers,designAuthor};
69
+ }else patch=await selectInteractively(config);
70
+ console.log(JSON.stringify(install({root,settingsPatch:{...patch,setupComplete:true}}),null,2));
71
+ console.log('Reviewer defaults saved for new tasks. Active tasks retain their original reviewer assignments and findings.');return;
72
+ }
73
+
74
+ if(command==='doctor') {
75
+ const config=readSettings(opts),checks=[];
76
+ function check(name,run){try{checks.push({name,ok:true,...run()});}catch(error){checks.push({name,ok:false,error:error.message});}}
77
+ const version=executable=>{const spec=commandSpec(executable);const r=spawnSync(spec.executable,[...spec.args,'--version'],{encoding:'utf8',windowsHide:true,env:workerEnvironment(),timeout:15000});if(r.status!==0)throw Error('Version check failed; check executable installation.');return {version:(r.stdout||r.stderr).trim().split('\n')[0]};};
78
+ check('OpenCode',()=>{const result=version('opencode');if(!/^(?:opencode\s+v?)?2\./i.test(result.version))throw Error('OpenCode 2 is required.');return result;});
79
+ for(const who of lanes(config))check(who,()=>{const lane=reviewer(config,who);if(lane.adapter==='opencode'){const catalog=modelCatalog();selection(`opencode:${lane.providerID}/${lane.model}`,{catalog,variant:lane.variant});return {adapter:lane.adapter,model:lane.model,variant:lane.variant??'default',catalogVerified:true};}const result=version(lane.executable);if(!args.includes('--no-auth'))requireCliAuthentication(who,config,root);return {...result,model:lane.model,effort:lane.effort,authChecked:!args.includes('--no-auth')};});
80
+ check('git',()=>version('git'));check('verification',()=>version(config.pnpmExecutable));check('design Python',()=>version(config.design.pythonExecutable));
81
+ if(config.reviewBrowser?.enabled)check('reviewer browser',()=>{const b=config.reviewBrowser;validateBrowser(b);const spec=commandSpec(b.command[0]);const r=spawnSync(spec.executable,[...spec.args,...b.command.slice(1),'--version'],{encoding:'utf8',windowsHide:true,env:workerEnvironment(),timeout:15000});if(r.status!==0)throw Error('Playwright MCP version check failed.');return {version:r.stdout.trim(),browser:b.browser??'chrome',scope:'CLI and OpenCode-provider reviewers; separate isolated sessions'};});
82
+ for(const skill of ['frontend-design','ui-ux-pro-max','web-design-guidelines'])check(skill,()=>{if(!existsSync(path.join(config.design.skillsDirectory,skill,'SKILL.md')))throw Error('Missing bundled skill');return {};});
83
+ console.log(JSON.stringify({configFile:opts.configFile,platform:process.platform,checks,modelCalls:0},null,2));
84
+ if(checks.some(c=>!c.ok))process.exitCode=1;
85
+ return;
86
+ }
87
+ if(command!=='help')throw Error(`Unknown command: ${command}`);
88
+ console.log('opencode-hybrid browser --enable|--disable [--config-dir PATH] (independent reviewer browsers)');
89
+ console.log('opencode-hybrid configure [--config-dir PATH] (interactive reviewer selection)\nopencode-hybrid models [--project PATH]\nopencode-hybrid configure --reviewer1 SOURCE:MODEL --reviewer2 SOURCE:MODEL|none [--variant1 NAME] [--variant2 NAME] [--lead 1|2]\nopencode-hybrid init [--config-dir PATH] [--dry-run]\nopencode-hybrid doctor [--config-dir PATH] [--no-auth]\nopencode-hybrid config [--config-dir PATH]\nMigration: init --legacy-config FILE --project PROJECT\nAfter init, open any project with ordinary opencode. Configuration is user-owned; installation never copies credentials.');
90
+ }
91
+ main().catch(error=>{console.error(error.message);process.exitCode=1;});