@rockysoft/opencode-hybrid-workflow 0.9.0 → 0.9.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.
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;});