@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 +6 -6
- package/assets/skills/ui-ux-pro-max/SKILL.md +25 -25
- package/cli.mjs +91 -91
- package/core/controller.mjs +492 -471
- package/core/design.mjs +1 -0
- package/core/plugin.mjs +93 -93
- package/core/telemetry.mjs +111 -111
- package/core/workers.mjs +202 -202
- package/docs/CHANGELOG.md +18 -0
- package/docs/configuration.md +12 -0
- package/lib/context-layout.mjs +20 -0
- package/lib/install.mjs +112 -111
- package/lib/project-knowledge.mjs +68 -0
- package/lib/settings-service.mjs +137 -137
- package/package.json +76 -76
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ Confirm installation of Playwright and Firefox.
|
|
|
125
125
|
|
|
126
126
|

|
|
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.
|
|
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
|

|
|
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.
|
|
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.
|
|
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.
|
|
183
|
-
|
|
184
|
-
[Engineering handoffs](https://cdn.jsdelivr.net/npm/@rockysoft/opencode-hybrid-workflow@0.
|
|
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
|
-
```
|
|
49
|
-
|
|
48
|
+
```text
|
|
49
|
+
<python3-executable> "<skill-directory>/scripts/search.py" "<query>" --domain <domain>
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
The examples use the
|
|
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
|
-
```
|
|
85
|
-
|
|
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
|
-
```
|
|
92
|
-
|
|
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
|
-
```
|
|
100
|
-
|
|
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
|
-
```
|
|
123
|
-
|
|
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
|
-
```
|
|
138
|
-
|
|
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
|
-
```
|
|
144
|
-
|
|
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
|
-
```
|
|
167
|
-
|
|
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
|
-
```
|
|
185
|
+
```text
|
|
186
186
|
# Step 2: design system
|
|
187
|
-
|
|
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
|
-
|
|
190
|
+
<python3-executable> "<skill-directory>/scripts/search.py" "keyboard focus modal" --domain ux
|
|
191
191
|
|
|
192
192
|
# Step 4: stack guidelines
|
|
193
|
-
|
|
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 {
|
|
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'))
|
|
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;});
|