@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 +91 -91
- package/core/controller.mjs +471 -471
- package/core/plugin.mjs +93 -93
- package/core/telemetry.mjs +111 -111
- package/core/workers.mjs +202 -202
- package/docs/configuration.md +6 -0
- package/lib/settings-service.mjs +137 -137
- package/package.json +76 -76
package/lib/settings-service.mjs
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import {existsSync,readFileSync,writeFileSync,mkdirSync,renameSync} from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import {createHash,randomUUID} from 'node:crypto';
|
|
4
|
-
import {configDirectory,readSettings,settingsFile,packageRoot} from './config.mjs';
|
|
5
|
-
import {install,parseJsonc,safeTarget} from './install.mjs';
|
|
6
|
-
import {selection} from './select-reviewers.mjs';
|
|
7
|
-
import {commandSpec} from './commands.mjs';
|
|
8
|
-
import {runProcess,
|
|
9
|
-
import {NativeBrowser} from './native-browser.mjs';
|
|
10
|
-
import {SettingsRPC} from './settings-rpc.mjs';
|
|
11
|
-
import {discoverCLIModels} from './cli-models.mjs';
|
|
12
|
-
import {projectMeasurements,formatMeasurements} from './trace-summary.mjs';
|
|
13
|
-
const writes=new Set();
|
|
14
|
-
const unwrap=x=>x?.data??x;
|
|
15
|
-
export function revision(file){return createHash('sha256').update(existsSync(file)?readFileSync(file):'missing').digest('hex');}
|
|
16
|
-
export function validateChoices(input,catalog,previous){
|
|
17
|
-
if(!input.reviewers||Object.keys(input.reviewers).some(k=>!['astra','opus'].includes(k)))throw Error('Choose one or two reviewer slots.');
|
|
18
|
-
const reviewers={};
|
|
19
|
-
for(const who of ['astra','opus']){
|
|
20
|
-
const item=input.reviewers[who];
|
|
21
|
-
if(item===null&&who==='opus'){reviewers[who]=null;continue;}
|
|
22
|
-
if(!item||typeof item.model!=='string')throw Error('Reviewer model is required.');
|
|
23
|
-
if(item.adapter==='opencode')reviewers[who]=selection(`opencode:${item.providerID}/${item.model}`,{catalog,variant:item.variant});
|
|
24
|
-
else if(['codex-cli','claude-cli'].includes(item.adapter)){
|
|
25
|
-
if(item.effort&&!/^[a-zA-Z0-9_-]+$/.test(item.effort))throw Error('Invalid reasoning effort.');
|
|
26
|
-
reviewers[who]=selection(`${item.adapter}:${item.model}`,{previous:previous.reviewers?.[who]??{},variant:item.effort});
|
|
27
|
-
}else throw Error('Unknown reviewer connection.');
|
|
28
|
-
}
|
|
29
|
-
if(!['astra','opus'].includes(input.designAuthor)||!reviewers[input.designAuthor])throw Error('Choose an enabled planning/design lead.');
|
|
30
|
-
return {reviewers,designAuthor:input.designAuthor};
|
|
31
|
-
}
|
|
32
|
-
export function createSettingsService(ctx,options={}){
|
|
33
|
-
const root=configDirectory(),file=settingsFile(options);
|
|
34
|
-
const readOnly=path.resolve(file)!==path.join(root,'hybrid','config.json');
|
|
35
|
-
const load=()=>readSettings(options);
|
|
36
|
-
const catalog=async()=>{const values=unwrap(await ctx.model.list({location:ctx.location}));return values.filter(m=>m.enabled&&m.capabilities?.tools&&(!m.type||m.type==='chat'));};
|
|
37
|
-
const globalFile=()=>{const json=path.join(root,'opencode.json'),jsonc=path.join(root,'opencode.jsonc');if(existsSync(json)&&existsSync(jsonc))throw Error('Both opencode.json and opencode.jsonc exist; resolve the duplicate configuration.');return existsSync(jsonc)?jsonc:json;};
|
|
38
|
-
const globalRead=()=>existsSync(globalFile())?parseJsonc(readFileSync(safeTarget(root,globalFile()),'utf8')):{};
|
|
39
|
-
const patchGlobal=update=>{
|
|
40
|
-
const target=globalFile(),before=existsSync(target)?readFileSync(safeTarget(root,target),'utf8'):null;
|
|
41
|
-
const value=before?parseJsonc(before):{};update(value);
|
|
42
|
-
const backup=path.join(root,'backups','hybrid-ui-'+randomUUID());mkdirSync(backup,{recursive:true,mode:0o700});
|
|
43
|
-
if(before!==null)writeFileSync(path.join(backup,path.basename(target)),before,{mode:0o600});
|
|
44
|
-
const tmp=target+'.hybrid-'+randomUUID();writeFileSync(tmp,JSON.stringify(value,null,2)+'\n',{mode:0o600});renameSync(tmp,target);
|
|
45
|
-
};
|
|
46
|
-
const persist=patch=>install({root,settingsPatch:{...(!existsSync(file)?{setupComplete:false}:{}),...patch},registerPlugin:false});
|
|
47
|
-
async function exclusive(fn){if(readOnly)throw Error('This installation uses a custom OPENCODE_HYBRID_CONFIG override. It remains active; interactive changes require the standard global configuration location.');if(writes.has(file))throw Error('Another settings change is in progress. Please retry when it finishes.');writes.add(file);try{return await fn();}finally{writes.delete(file);}}
|
|
48
|
-
async function get(){
|
|
49
|
-
const config=load(),models=await catalog();
|
|
50
|
-
return {configured:existsSync(file)&&config.setupComplete!==false,readOnly,revision:revision(file),version:JSON.parse(readFileSync(path.join(packageRoot,'package.json'),'utf8')).version,
|
|
51
|
-
reviewers:config.reviewers,designAuthor:config.designAuthor??(config.reviewers.opus?'opus':'astra'),browserEnabled:!!config.reviewBrowser?.enabled,
|
|
52
|
-
catalog:models.map(m=>({providerID:m.providerID,id:m.id,name:m.name,variants:(m.variants??[]).map(v=>v.id),vision:m.capabilities.input?.includes('image')===true})),
|
|
53
|
-
configFile:file,implementationModel:globalRead().model??'Selected in OpenCode',host:process.platform};
|
|
54
|
-
}
|
|
55
|
-
async function diagnostics(){
|
|
56
|
-
const config=load(),checks=[];
|
|
57
|
-
const check=async(name,fn)=>{try{checks.push({name,ok:true,detail:await fn()});}catch(e){checks.push({name,ok:false,detail:e.message});}};
|
|
58
|
-
for(const [name,exe] of [['Node','node'],['Git','git'],['pnpm',config.pnpmExecutable],['Design research Python',config.design.pythonExecutable]])await check(name,async()=>{const spec=commandSpec(exe);return (await runProcess(spec.executable,[...spec.args,'--version'],root,'',15000)).trim().split('\n')[0];});
|
|
59
|
-
const models=await catalog();
|
|
60
|
-
for(const [who,lane] of Object.entries(config.reviewers))if(lane)await check(who==='astra'?'Reviewer 1':'Reviewer 2',async()=>{
|
|
61
|
-
if(lane.adapter==='opencode'){selection(`opencode:${lane.providerID}/${lane.model}`,{catalog:models,variant:lane.variant});return `${lane.providerID} / ${lane.model} / ${lane.variant??'default'}`;}
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
if(config.reviewBrowser?.enabled)await check('Reviewer browser',async()=>{
|
|
65
|
-
const folder=path.join(root,'hybrid','checks',randomUUID());safeTarget(root,folder);mkdirSync(folder,{recursive:true});
|
|
66
|
-
const browser=new NativeBrowser(config,folder);
|
|
67
|
-
try{await browser.start();const result=await browser.request('tools/call',{name:'browser_snapshot',arguments:{}});if(result.isError)throw Error((result.content??[]).filter(c=>c.type==='text').map(c=>c.text).join('\n')||'Browser check failed');return 'Isolated browser launched and responded';}finally{await browser.close();}
|
|
68
|
-
});
|
|
69
|
-
for(const name of ['frontend-design','ui-ux-pro-max','web-design-guidelines'])await check(name,async()=>{if(!existsSync(path.join(config.design.skillsDirectory,name,'SKILL.md')))throw Error('Bundled skill missing');return 'Available';});
|
|
70
|
-
if(ctx.location?.directory)await check('Workflow measurements',async()=>formatMeasurements(projectMeasurements(ctx.location.directory,config)));
|
|
71
|
-
return {checks,modelCalls:0};
|
|
72
|
-
}
|
|
73
|
-
async function installTool(spec,signal){
|
|
74
|
-
const folder=path.join(root,'hybrid','tools');safeTarget(root,folder);mkdirSync(folder,{recursive:true,mode:0o700});
|
|
75
|
-
await runProcess('npm',['install','--prefix',folder,'--ignore-scripts','--no-audit','--no-fund',spec],folder,'',0,signal);
|
|
76
|
-
return folder;
|
|
77
|
-
}
|
|
78
|
-
return {get,diagnostics,
|
|
79
|
-
cliModels:(input,context={})=>{
|
|
80
|
-
if(!['astra','opus'].includes(input.who)||!['codex-cli','claude-cli'].includes(input.adapter))throw Error('Unknown reviewer connection.');
|
|
81
|
-
const previous=load().reviewers?.[input.who];
|
|
82
|
-
return discoverCLIModels(input.adapter,{executable:previous?.adapter===input.adapter?previous.executable:undefined,signal:context.signal});
|
|
83
|
-
},
|
|
84
|
-
save:input=>exclusive(async()=>{
|
|
85
|
-
if(input.revision!==revision(file))throw Error('Settings changed in another window. Reopen settings and try again.');
|
|
86
|
-
const patch=validateChoices(input,await catalog(),load());
|
|
87
|
-
// Recheck after catalog I/O; never overwrite another UI's saved defaults.
|
|
88
|
-
if(input.revision!==revision(file))throw Error('Settings changed in another window. Reopen settings and try again.');
|
|
89
|
-
const installed=persist({...patch,setupComplete:true});return {saved:true,preservedCustomFiles:installed.preservedCustomFiles,message:'Saved globally. New tasks use these reviewers; active tasks keep their assignments.'};
|
|
90
|
-
}),
|
|
91
|
-
browser:(input,context={})=>exclusive(async()=>{
|
|
92
|
-
if(input.action==='disable'){persist({reviewBrowser:{...load().reviewBrowser,enabled:false}});return {message:'Reviewer browser disabled for future calls.'};}
|
|
93
|
-
if(input.action==='enable'){
|
|
94
|
-
let setting=load().reviewBrowser;
|
|
95
|
-
if(!setting?.command){
|
|
96
|
-
const server=globalRead().mcp?.servers?.playwright;
|
|
97
|
-
if(server?.type!=='local'||!Array.isArray(server.command))throw Error('No installed browser found. Choose Install browser support in this menu.');
|
|
98
|
-
const stop=server.command.findIndex((x,i)=>i>0&&x.startsWith('--')),command=server.command.slice(0,stop<0?undefined:stop);
|
|
99
|
-
if(command.some(x=>/^(-y|install|add)$/.test(x)))throw Error('Browser uses on-demand downloads. Choose Install browser support for a managed installation.');
|
|
100
|
-
const pos=server.command.indexOf('--browser');setting={command,browser:pos>=0?server.command[pos+1]:'chrome',headless:true};
|
|
101
|
-
}
|
|
102
|
-
persist({reviewBrowser:{...setting,enabled:true}});return {message:'Browser support enabled.'};
|
|
103
|
-
}
|
|
104
|
-
if(input.action!=='install')throw Error('Unknown browser action.');
|
|
105
|
-
const folder=await installTool('@playwright/mcp@0.0.83',context.signal),node=commandSpec('node');
|
|
106
|
-
await runProcess(node.executable,[...node.args,path.join(folder,'node_modules','playwright','cli.js'),'install','firefox'],folder,'',0,context.signal);
|
|
107
|
-
const command=[node.executable,...node.args,path.join(folder,'node_modules','@playwright','mcp','cli.js')];
|
|
108
|
-
persist({reviewBrowser:{enabled:true,command,browser:'firefox',headless:true}});
|
|
109
|
-
patchGlobal(value=>{value.mcp??={};value.mcp.servers??={};value.mcp.servers.playwright??={type:'local',command:[...command,'--browser','firefox','--isolated','--headless'],enabled:true};});
|
|
110
|
-
return {message:'Playwright and Firefox installed. Reviewer browser enabled; implementation MCP configured unless one already existed.'};
|
|
111
|
-
}),
|
|
112
|
-
dependencies:(input,context={})=>exclusive(async()=>{
|
|
113
|
-
if(input.action==='pnpm'){
|
|
114
|
-
const folder=await installTool('pnpm@12.14.0',context.signal);
|
|
115
|
-
// pnpm 12 ships a native binary; only run this explicitly selected package's
|
|
116
|
-
// installer, then verify it before saving a working executable.
|
|
117
|
-
const node=commandSpec('node'),packageFolder=path.join(folder,'node_modules','pnpm');
|
|
118
|
-
await runProcess(node.executable,[...node.args,path.join(packageFolder,'install.js')],folder,'',0,context.signal);
|
|
119
|
-
const executable=path.join(packageFolder,process.platform==='win32'?'pnpm.exe':'pnpm');
|
|
120
|
-
await runProcess(executable,['--version'],folder,'',15000,context.signal);
|
|
121
|
-
persist({verification:{...load().verification,executable}});
|
|
122
|
-
return {message:'Managed pnpm installed for hybrid verification. Project-required versions still take precedence.'};
|
|
123
|
-
}
|
|
124
|
-
if(input.action==='context7'){
|
|
125
|
-
patchGlobal(value=>{value.mcp??={};value.mcp.servers??={};value.mcp.servers.context7??={type:'remote',url:'https://mcp.context7.com/mcp',enabled:true};});
|
|
126
|
-
return {message:'Context7 documentation MCP configured. Any existing configuration was preserved.'};
|
|
127
|
-
}
|
|
128
|
-
throw Error('Unknown dependency.');
|
|
129
|
-
}),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
export async function registerSettings(ctx,options){
|
|
133
|
-
const service=createSettingsService(ctx,options);
|
|
134
|
-
// Expected setup errors must reach the dialog, not become opaque HTTP defects.
|
|
135
|
-
const handlers=Object.fromEntries(Object.entries(service).map(([name,fn])=>[name,async(...args)=>{try{return await fn(...args);}catch(error){return {error:{message:error.message}};}}]));
|
|
136
|
-
await ctx.rpc.register(SettingsRPC,handlers);return service;
|
|
137
|
-
}
|
|
1
|
+
import {existsSync,readFileSync,writeFileSync,mkdirSync,renameSync} from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createHash,randomUUID} from 'node:crypto';
|
|
4
|
+
import {configDirectory,readSettings,settingsFile,packageRoot} from './config.mjs';
|
|
5
|
+
import {install,parseJsonc,safeTarget} from './install.mjs';
|
|
6
|
+
import {selection} from './select-reviewers.mjs';
|
|
7
|
+
import {commandSpec} from './commands.mjs';
|
|
8
|
+
import {runProcess,requireCliAuthentication} from '../core/workers.mjs';
|
|
9
|
+
import {NativeBrowser} from './native-browser.mjs';
|
|
10
|
+
import {SettingsRPC} from './settings-rpc.mjs';
|
|
11
|
+
import {discoverCLIModels} from './cli-models.mjs';
|
|
12
|
+
import {projectMeasurements,formatMeasurements} from './trace-summary.mjs';
|
|
13
|
+
const writes=new Set();
|
|
14
|
+
const unwrap=x=>x?.data??x;
|
|
15
|
+
export function revision(file){return createHash('sha256').update(existsSync(file)?readFileSync(file):'missing').digest('hex');}
|
|
16
|
+
export function validateChoices(input,catalog,previous){
|
|
17
|
+
if(!input.reviewers||Object.keys(input.reviewers).some(k=>!['astra','opus'].includes(k)))throw Error('Choose one or two reviewer slots.');
|
|
18
|
+
const reviewers={};
|
|
19
|
+
for(const who of ['astra','opus']){
|
|
20
|
+
const item=input.reviewers[who];
|
|
21
|
+
if(item===null&&who==='opus'){reviewers[who]=null;continue;}
|
|
22
|
+
if(!item||typeof item.model!=='string')throw Error('Reviewer model is required.');
|
|
23
|
+
if(item.adapter==='opencode')reviewers[who]=selection(`opencode:${item.providerID}/${item.model}`,{catalog,variant:item.variant});
|
|
24
|
+
else if(['codex-cli','claude-cli'].includes(item.adapter)){
|
|
25
|
+
if(item.effort&&!/^[a-zA-Z0-9_-]+$/.test(item.effort))throw Error('Invalid reasoning effort.');
|
|
26
|
+
reviewers[who]=selection(`${item.adapter}:${item.model}`,{previous:previous.reviewers?.[who]??{},variant:item.effort});
|
|
27
|
+
}else throw Error('Unknown reviewer connection.');
|
|
28
|
+
}
|
|
29
|
+
if(!['astra','opus'].includes(input.designAuthor)||!reviewers[input.designAuthor])throw Error('Choose an enabled planning/design lead.');
|
|
30
|
+
return {reviewers,designAuthor:input.designAuthor};
|
|
31
|
+
}
|
|
32
|
+
export function createSettingsService(ctx,options={}){
|
|
33
|
+
const root=configDirectory(),file=settingsFile(options);
|
|
34
|
+
const readOnly=path.resolve(file)!==path.join(root,'hybrid','config.json');
|
|
35
|
+
const load=()=>readSettings(options);
|
|
36
|
+
const catalog=async()=>{const values=unwrap(await ctx.model.list({location:ctx.location}));return values.filter(m=>m.enabled&&m.capabilities?.tools&&(!m.type||m.type==='chat'));};
|
|
37
|
+
const globalFile=()=>{const json=path.join(root,'opencode.json'),jsonc=path.join(root,'opencode.jsonc');if(existsSync(json)&&existsSync(jsonc))throw Error('Both opencode.json and opencode.jsonc exist; resolve the duplicate configuration.');return existsSync(jsonc)?jsonc:json;};
|
|
38
|
+
const globalRead=()=>existsSync(globalFile())?parseJsonc(readFileSync(safeTarget(root,globalFile()),'utf8')):{};
|
|
39
|
+
const patchGlobal=update=>{
|
|
40
|
+
const target=globalFile(),before=existsSync(target)?readFileSync(safeTarget(root,target),'utf8'):null;
|
|
41
|
+
const value=before?parseJsonc(before):{};update(value);
|
|
42
|
+
const backup=path.join(root,'backups','hybrid-ui-'+randomUUID());mkdirSync(backup,{recursive:true,mode:0o700});
|
|
43
|
+
if(before!==null)writeFileSync(path.join(backup,path.basename(target)),before,{mode:0o600});
|
|
44
|
+
const tmp=target+'.hybrid-'+randomUUID();writeFileSync(tmp,JSON.stringify(value,null,2)+'\n',{mode:0o600});renameSync(tmp,target);
|
|
45
|
+
};
|
|
46
|
+
const persist=patch=>install({root,settingsPatch:{...(!existsSync(file)?{setupComplete:false}:{}),...patch},registerPlugin:false});
|
|
47
|
+
async function exclusive(fn){if(readOnly)throw Error('This installation uses a custom OPENCODE_HYBRID_CONFIG override. It remains active; interactive changes require the standard global configuration location.');if(writes.has(file))throw Error('Another settings change is in progress. Please retry when it finishes.');writes.add(file);try{return await fn();}finally{writes.delete(file);}}
|
|
48
|
+
async function get(){
|
|
49
|
+
const config=load(),models=await catalog();
|
|
50
|
+
return {configured:existsSync(file)&&config.setupComplete!==false,readOnly,revision:revision(file),version:JSON.parse(readFileSync(path.join(packageRoot,'package.json'),'utf8')).version,
|
|
51
|
+
reviewers:config.reviewers,designAuthor:config.designAuthor??(config.reviewers.opus?'opus':'astra'),browserEnabled:!!config.reviewBrowser?.enabled,
|
|
52
|
+
catalog:models.map(m=>({providerID:m.providerID,id:m.id,name:m.name,variants:(m.variants??[]).map(v=>v.id),vision:m.capabilities.input?.includes('image')===true})),
|
|
53
|
+
configFile:file,implementationModel:globalRead().model??'Selected in OpenCode',host:process.platform};
|
|
54
|
+
}
|
|
55
|
+
async function diagnostics(){
|
|
56
|
+
const config=load(),checks=[];
|
|
57
|
+
const check=async(name,fn)=>{try{checks.push({name,ok:true,detail:await fn()});}catch(e){checks.push({name,ok:false,detail:e.message});}};
|
|
58
|
+
for(const [name,exe] of [['Node','node'],['Git','git'],['pnpm',config.pnpmExecutable],['Design research Python',config.design.pythonExecutable]])await check(name,async()=>{const spec=commandSpec(exe);return (await runProcess(spec.executable,[...spec.args,'--version'],root,'',15000)).trim().split('\n')[0];});
|
|
59
|
+
const models=await catalog();
|
|
60
|
+
for(const [who,lane] of Object.entries(config.reviewers))if(lane)await check(who==='astra'?'Reviewer 1':'Reviewer 2',async()=>{
|
|
61
|
+
if(lane.adapter==='opencode'){selection(`opencode:${lane.providerID}/${lane.model}`,{catalog:models,variant:lane.variant});return `${lane.providerID} / ${lane.model} / ${lane.variant??'default'}`;}
|
|
62
|
+
requireCliAuthentication(who,config,root);return `${lane.adapter} signed in`;
|
|
63
|
+
});
|
|
64
|
+
if(config.reviewBrowser?.enabled)await check('Reviewer browser',async()=>{
|
|
65
|
+
const folder=path.join(root,'hybrid','checks',randomUUID());safeTarget(root,folder);mkdirSync(folder,{recursive:true});
|
|
66
|
+
const browser=new NativeBrowser(config,folder);
|
|
67
|
+
try{await browser.start();const result=await browser.request('tools/call',{name:'browser_snapshot',arguments:{}});if(result.isError)throw Error((result.content??[]).filter(c=>c.type==='text').map(c=>c.text).join('\n')||'Browser check failed');return 'Isolated browser launched and responded';}finally{await browser.close();}
|
|
68
|
+
});
|
|
69
|
+
for(const name of ['frontend-design','ui-ux-pro-max','web-design-guidelines'])await check(name,async()=>{if(!existsSync(path.join(config.design.skillsDirectory,name,'SKILL.md')))throw Error('Bundled skill missing');return 'Available';});
|
|
70
|
+
if(ctx.location?.directory)await check('Workflow measurements',async()=>formatMeasurements(projectMeasurements(ctx.location.directory,config)));
|
|
71
|
+
return {checks,modelCalls:0};
|
|
72
|
+
}
|
|
73
|
+
async function installTool(spec,signal){
|
|
74
|
+
const folder=path.join(root,'hybrid','tools');safeTarget(root,folder);mkdirSync(folder,{recursive:true,mode:0o700});
|
|
75
|
+
await runProcess('npm',['install','--prefix',folder,'--ignore-scripts','--no-audit','--no-fund',spec],folder,'',0,signal);
|
|
76
|
+
return folder;
|
|
77
|
+
}
|
|
78
|
+
return {get,diagnostics,
|
|
79
|
+
cliModels:(input,context={})=>{
|
|
80
|
+
if(!['astra','opus'].includes(input.who)||!['codex-cli','claude-cli'].includes(input.adapter))throw Error('Unknown reviewer connection.');
|
|
81
|
+
const previous=load().reviewers?.[input.who];
|
|
82
|
+
return discoverCLIModels(input.adapter,{executable:previous?.adapter===input.adapter?previous.executable:undefined,signal:context.signal});
|
|
83
|
+
},
|
|
84
|
+
save:input=>exclusive(async()=>{
|
|
85
|
+
if(input.revision!==revision(file))throw Error('Settings changed in another window. Reopen settings and try again.');
|
|
86
|
+
const patch=validateChoices(input,await catalog(),load());
|
|
87
|
+
// Recheck after catalog I/O; never overwrite another UI's saved defaults.
|
|
88
|
+
if(input.revision!==revision(file))throw Error('Settings changed in another window. Reopen settings and try again.');
|
|
89
|
+
const installed=persist({...patch,setupComplete:true});return {saved:true,preservedCustomFiles:installed.preservedCustomFiles,message:'Saved globally. New tasks use these reviewers; active tasks keep their assignments.'};
|
|
90
|
+
}),
|
|
91
|
+
browser:(input,context={})=>exclusive(async()=>{
|
|
92
|
+
if(input.action==='disable'){persist({reviewBrowser:{...load().reviewBrowser,enabled:false}});return {message:'Reviewer browser disabled for future calls.'};}
|
|
93
|
+
if(input.action==='enable'){
|
|
94
|
+
let setting=load().reviewBrowser;
|
|
95
|
+
if(!setting?.command){
|
|
96
|
+
const server=globalRead().mcp?.servers?.playwright;
|
|
97
|
+
if(server?.type!=='local'||!Array.isArray(server.command))throw Error('No installed browser found. Choose Install browser support in this menu.');
|
|
98
|
+
const stop=server.command.findIndex((x,i)=>i>0&&x.startsWith('--')),command=server.command.slice(0,stop<0?undefined:stop);
|
|
99
|
+
if(command.some(x=>/^(-y|install|add)$/.test(x)))throw Error('Browser uses on-demand downloads. Choose Install browser support for a managed installation.');
|
|
100
|
+
const pos=server.command.indexOf('--browser');setting={command,browser:pos>=0?server.command[pos+1]:'chrome',headless:true};
|
|
101
|
+
}
|
|
102
|
+
persist({reviewBrowser:{...setting,enabled:true}});return {message:'Browser support enabled.'};
|
|
103
|
+
}
|
|
104
|
+
if(input.action!=='install')throw Error('Unknown browser action.');
|
|
105
|
+
const folder=await installTool('@playwright/mcp@0.0.83',context.signal),node=commandSpec('node');
|
|
106
|
+
await runProcess(node.executable,[...node.args,path.join(folder,'node_modules','playwright','cli.js'),'install','firefox'],folder,'',0,context.signal);
|
|
107
|
+
const command=[node.executable,...node.args,path.join(folder,'node_modules','@playwright','mcp','cli.js')];
|
|
108
|
+
persist({reviewBrowser:{enabled:true,command,browser:'firefox',headless:true}});
|
|
109
|
+
patchGlobal(value=>{value.mcp??={};value.mcp.servers??={};value.mcp.servers.playwright??={type:'local',command:[...command,'--browser','firefox','--isolated','--headless'],enabled:true};});
|
|
110
|
+
return {message:'Playwright and Firefox installed. Reviewer browser enabled; implementation MCP configured unless one already existed.'};
|
|
111
|
+
}),
|
|
112
|
+
dependencies:(input,context={})=>exclusive(async()=>{
|
|
113
|
+
if(input.action==='pnpm'){
|
|
114
|
+
const folder=await installTool('pnpm@12.14.0',context.signal);
|
|
115
|
+
// pnpm 12 ships a native binary; only run this explicitly selected package's
|
|
116
|
+
// installer, then verify it before saving a working executable.
|
|
117
|
+
const node=commandSpec('node'),packageFolder=path.join(folder,'node_modules','pnpm');
|
|
118
|
+
await runProcess(node.executable,[...node.args,path.join(packageFolder,'install.js')],folder,'',0,context.signal);
|
|
119
|
+
const executable=path.join(packageFolder,process.platform==='win32'?'pnpm.exe':'pnpm');
|
|
120
|
+
await runProcess(executable,['--version'],folder,'',15000,context.signal);
|
|
121
|
+
persist({verification:{...load().verification,executable}});
|
|
122
|
+
return {message:'Managed pnpm installed for hybrid verification. Project-required versions still take precedence.'};
|
|
123
|
+
}
|
|
124
|
+
if(input.action==='context7'){
|
|
125
|
+
patchGlobal(value=>{value.mcp??={};value.mcp.servers??={};value.mcp.servers.context7??={type:'remote',url:'https://mcp.context7.com/mcp',enabled:true};});
|
|
126
|
+
return {message:'Context7 documentation MCP configured. Any existing configuration was preserved.'};
|
|
127
|
+
}
|
|
128
|
+
throw Error('Unknown dependency.');
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export async function registerSettings(ctx,options){
|
|
133
|
+
const service=createSettingsService(ctx,options);
|
|
134
|
+
// Expected setup errors must reach the dialog, not become opaque HTTP defects.
|
|
135
|
+
const handlers=Object.fromEntries(Object.entries(service).map(([name,fn])=>[name,async(...args)=>{try{return await fn(...args);}catch(error){return {error:{message:error.message}};}}]));
|
|
136
|
+
await ctx.rpc.register(SettingsRPC,handlers);return service;
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rockysoft/opencode-hybrid-workflow",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Portable configurable-reviewer planning, design, implementation checkpoints and usage sidebar for OpenCode 2",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"author": "RockySoft",
|
|
8
|
-
"keywords": [
|
|
9
|
-
"opencode",
|
|
10
|
-
"opencode-plugin",
|
|
11
|
-
"agents",
|
|
12
|
-
"code-review",
|
|
13
|
-
"workflow"
|
|
14
|
-
],
|
|
15
|
-
"publishConfig": {
|
|
16
|
-
"access": "public",
|
|
17
|
-
"tag": "latest"
|
|
18
|
-
},
|
|
19
|
-
"exports": {
|
|
20
|
-
".": "./index.js",
|
|
21
|
-
"./tui": "./tui.js"
|
|
22
|
-
},
|
|
23
|
-
"bin": {
|
|
24
|
-
"opencode-hybrid": "./cli.mjs"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"index.js",
|
|
28
|
-
"tui.js",
|
|
29
|
-
"cli.mjs",
|
|
30
|
-
"lib/",
|
|
31
|
-
"core/",
|
|
32
|
-
"sidebar/",
|
|
33
|
-
"assets/",
|
|
34
|
-
"README.md",
|
|
35
|
-
"THIRD_PARTY_NOTICES.md",
|
|
36
|
-
"LICENSE",
|
|
37
|
-
"docs/"
|
|
38
|
-
],
|
|
39
|
-
"engines": {
|
|
40
|
-
"node": ">=22"
|
|
41
|
-
},
|
|
42
|
-
"scripts": {
|
|
43
|
-
"build": "node scripts/build.mjs",
|
|
44
|
-
"prepack": "npm run build",
|
|
45
|
-
"test": "node --test test/*.test.mjs",
|
|
46
|
-
"doctor": "node cli.mjs doctor"
|
|
47
|
-
},
|
|
48
|
-
"devDependencies": {
|
|
49
|
-
"@babel/core": "7.29.7",
|
|
50
|
-
"@babel/preset-typescript": "7.27.1",
|
|
51
|
-
"babel-preset-solid": "1.9.12"
|
|
52
|
-
},
|
|
53
|
-
"peerDependencies": {
|
|
54
|
-
"@opencode/plugin": ">=2.1.2 <3",
|
|
55
|
-
"@opentui/core": ">=0.6.6",
|
|
56
|
-
"@opentui/solid": ">=0.6.6",
|
|
57
|
-
"solid-js": ">=1.9.0"
|
|
58
|
-
},
|
|
59
|
-
"peerDependenciesMeta": {
|
|
60
|
-
"@opencode/plugin": {
|
|
61
|
-
"optional": true
|
|
62
|
-
},
|
|
63
|
-
"@opentui/core": {
|
|
64
|
-
"optional": true
|
|
65
|
-
},
|
|
66
|
-
"@opentui/solid": {
|
|
67
|
-
"optional": true
|
|
68
|
-
},
|
|
69
|
-
"solid-js": {
|
|
70
|
-
"optional": true
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
"dependencies": {
|
|
74
|
-
"diff": "9.0.0"
|
|
75
|
-
}
|
|
76
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@rockysoft/opencode-hybrid-workflow",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Portable configurable-reviewer planning, design, implementation checkpoints and usage sidebar for OpenCode 2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "RockySoft",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"opencode",
|
|
10
|
+
"opencode-plugin",
|
|
11
|
+
"agents",
|
|
12
|
+
"code-review",
|
|
13
|
+
"workflow"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"tag": "latest"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./index.js",
|
|
21
|
+
"./tui": "./tui.js"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"opencode-hybrid": "./cli.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"index.js",
|
|
28
|
+
"tui.js",
|
|
29
|
+
"cli.mjs",
|
|
30
|
+
"lib/",
|
|
31
|
+
"core/",
|
|
32
|
+
"sidebar/",
|
|
33
|
+
"assets/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"THIRD_PARTY_NOTICES.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"docs/"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "node scripts/build.mjs",
|
|
44
|
+
"prepack": "npm run build",
|
|
45
|
+
"test": "node --test test/*.test.mjs",
|
|
46
|
+
"doctor": "node cli.mjs doctor"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@babel/core": "7.29.7",
|
|
50
|
+
"@babel/preset-typescript": "7.27.1",
|
|
51
|
+
"babel-preset-solid": "1.9.12"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@opencode/plugin": ">=2.1.2 <3",
|
|
55
|
+
"@opentui/core": ">=0.6.6",
|
|
56
|
+
"@opentui/solid": ">=0.6.6",
|
|
57
|
+
"solid-js": ">=1.9.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@opencode/plugin": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@opentui/core": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@opentui/solid": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"solid-js": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"diff": "9.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|