@yunhozz/arc 0.1.10 → 0.2.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/MANIFEST.sha256 +59 -50
- package/README.md +17 -49
- package/claude/agents/arc-implementer.md +4 -2
- package/claude/agents/arc-planner.md +2 -2
- package/claude/agents/arc-qa.md +3 -3
- package/claude/agents/arc-reviewer.md +2 -2
- package/claude/agents/arc-scouter.md +2 -2
- package/claude/skill/SKILL.md +3 -1
- package/claude/skill/references/commit.md +1 -1
- package/claude/skill/references/coordination.md +6 -2
- package/claude/skill/references/handoff-prep.md +5 -1
- package/claude/skill/references/handoff.md +23 -2
- package/claude/skill/references/planning.md +11 -2
- package/claude/skill/references/verification.md +6 -0
- package/claude/skill/references/visual-companion.md +6 -0
- package/claude/skill/runtime/bootstrap.mjs +1 -1
- package/claude/skill/runtime/cli.mjs +8 -0
- package/{lib/cli/workflow.mjs → claude/skill/runtime/command.mjs} +4 -4
- package/codex/agents/arc-implementer.toml +5 -2
- package/codex/agents/arc-planner.toml +2 -2
- package/codex/agents/arc-qa.toml +3 -3
- package/codex/agents/arc-reviewer.toml +2 -2
- package/codex/agents/arc-scouter.toml +2 -2
- package/codex/skill/SKILL.md +3 -1
- package/codex/skill/references/commit.md +1 -1
- package/codex/skill/references/coordination.md +6 -2
- package/codex/skill/references/handoff-prep.md +5 -1
- package/codex/skill/references/handoff.md +22 -2
- package/codex/skill/references/planning.md +11 -2
- package/codex/skill/references/verification.md +6 -0
- package/codex/skill/references/visual-companion.md +6 -0
- package/codex/skill/runtime/bootstrap.mjs +1 -1
- package/codex/skill/runtime/cli.mjs +8 -0
- package/codex/skill/runtime/command.mjs +21 -0
- package/lib/cli/postinstall.mjs +3 -16
- package/lib/cli/root.mjs +8 -19
- package/lib/cli/upgrade.mjs +1 -57
- package/lib/installer/manifest.mjs +6 -1
- package/lib/installer/runtime-assets.mjs +25 -0
- package/lib/installer/shell-completion.mjs +15 -20
- package/lib/installer/uninstall.mjs +5 -1
- package/lib/installer/upgrade-state.mjs +2 -2
- package/lib/installer/upgrade.mjs +2 -5
- package/lib/management/operations.mjs +39 -0
- package/lib/management/status.mjs +18 -0
- package/lib/management/update.mjs +58 -0
- package/lib/management/workflow.mjs +51 -0
- package/lib/policy/role-policy.json +7 -7
- package/lib/release/artifact.mjs +1 -1
- package/lib/runtime/bootstrap.mjs +1 -1
- package/lib/runtime/cli.mjs +8 -0
- package/lib/runtime/command.mjs +21 -0
- package/lib/tui/app.mjs +96 -0
- package/lib/tui/render.mjs +43 -0
- package/lib/tui/setup.mjs +21 -0
- package/lib/tui/terminal.mjs +81 -0
- package/lib/tui/workflow.mjs +12 -0
- package/package.json +1 -1
- package/scripts/postinstall.mjs +1 -3
- package/scripts/update-runtime-assets.mjs +6 -1
- package/lib/cli/choices.mjs +0 -47
- package/lib/cli/completion.mjs +0 -55
- package/lib/cli/index.mjs +0 -55
- package/lib/cli/preset.mjs +0 -29
- package/lib/cli/terminal.mjs +0 -19
- package/lib/cli/uninstall.mjs +0 -17
|
@@ -5,7 +5,7 @@ import {validateCooperativeCommand,validateCooperativeState} from './cooperative
|
|
|
5
5
|
import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workflowStatePath,inspectPath} from './scope.mjs';
|
|
6
6
|
import {saveDurableState} from './store.mjs';
|
|
7
7
|
export const COOPERATIVE_RUNTIME_MODULES=Object.freeze([
|
|
8
|
-
'bootstrap.mjs','cooperative-schema.mjs','cooperative.mjs','evaluate.mjs',
|
|
8
|
+
'cli.mjs','command.mjs','bootstrap.mjs','cooperative-schema.mjs','cooperative.mjs','evaluate.mjs',
|
|
9
9
|
'scope.mjs','store.mjs','git-evidence.mjs','assignment.mjs','verification.mjs','acceptance.mjs','external.mjs'
|
|
10
10
|
]);
|
|
11
11
|
export const cooperativeCoverage=()=>({enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import {runWorkflowCommand} from './command.mjs';
|
|
4
|
+
export {runWorkflowCommand};
|
|
5
|
+
// Importing the installed entry is safe for callers that own their own IO.
|
|
6
|
+
if(process.argv[1]&&fs.realpathSync(process.argv[1])===fileURLToPath(import.meta.url)){
|
|
7
|
+
process.exitCode=await runWorkflowCommand(process.argv.slice(2));
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {initializeCooperativeWorkflow,commandResult} from './bootstrap.mjs';
|
|
4
|
+
import {runCooperativeCommand} from './cooperative.mjs';
|
|
5
|
+
import {validateWorkflowId} from './scope.mjs';
|
|
6
|
+
export async function runWorkflowCommand(args,io={}){
|
|
7
|
+
const output=io.output??process.stdout;let workflowId=null;
|
|
8
|
+
try{
|
|
9
|
+
const [command,...rest]=args,options={};
|
|
10
|
+
if(!['init','record','status','reconcile','prepare','result'].includes(command))throw Error('Expected workflow command');
|
|
11
|
+
for(let i=0;i<rest.length;i+=2){const key=rest[i];if(!['--cwd','--workflow','--input'].includes(key)||options[key]!==undefined||typeof rest[i+1]!=='string')throw Error('Invalid workflow option');options[key]=rest[i+1];}
|
|
12
|
+
const cwd=options['--cwd'];workflowId=options['--workflow'];validateWorkflowId(workflowId);
|
|
13
|
+
if(typeof cwd!=='string'||!path.isAbsolute(cwd)||path.normalize(cwd)!==cwd)throw Error('Absolute workspace required');
|
|
14
|
+
if(command==='status'?options['--input']!==undefined:options['--input']!=='-')throw Error('Mutations require --input -; status takes no input');
|
|
15
|
+
// Check/review, commit and external intents are recorded for the operator.
|
|
16
|
+
// Actual Git mutations remain Orchestrator actions; reconciliation observes their results.
|
|
17
|
+
const input=command==='status'?undefined:JSON.parse(io.input??fs.readFileSync(0,'utf8'));
|
|
18
|
+
const result=await(command==='init'?initializeCooperativeWorkflow({cwd,workflowId,input}):runCooperativeCommand({cwd,workflowId,command,input}));
|
|
19
|
+
output.write(JSON.stringify(result)+'\n');return result.decision==='deny'?2:result.decision==='hold'?3:0;
|
|
20
|
+
}catch(error){output.write(JSON.stringify(commandResult({workflowId,decision:'hold',ruleId:'cooperative.error',error:error.message}))+'\n');return 1;}
|
|
21
|
+
}
|
package/lib/tui/app.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {createTerminalSession} from './terminal.mjs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {escapeTerminalText,navigationHint,wrapDetailRows} from './render.mjs';
|
|
4
|
+
import {createManagementContext,prepareOperation,executeOperation} from '../management/operations.mjs';
|
|
5
|
+
import {collectStatus} from '../management/status.mjs';
|
|
6
|
+
import {runSetup,summary} from './setup.mjs';
|
|
7
|
+
const home=['Configure','Update','Uninstall','Exit'];
|
|
8
|
+
function interrupted(){return Object.assign(Error('Session interrupted'),{code:'ARC_CANCELLED'});}
|
|
9
|
+
function operationRows(prepared){
|
|
10
|
+
const groups=new Map();
|
|
11
|
+
function groupFor(plan){const target=plan.target??plan,platform=target.platform??'harness',home=target.home??'',key=platform+'\0'+home;if(!groups.has(key))groups.set(key,{platform,home,Changes:new Map(),Deletions:new Map(),Preserved:new Map(),known:new Set(),notes:new Set()});return groups.get(key);}
|
|
12
|
+
function add(group,category,values){if(!Array.isArray(values))return;group.known.add(category);for(const item of values){const filename=typeof item==='string'?item:item.path??item.destination??item.relativePath;const description=typeof item==='object'?(item.action??item.description??item.note):undefined;const key=filename?path.normalize(filename):String(description??'Managed item');if(!group[category].has(key))group[category].set(key,{path:filename??'Managed item',descriptions:new Set()});if(description)group[category].get(key).descriptions.add(description);}}
|
|
13
|
+
for(const plan of prepared.plans??[]){const group=groupFor(plan);if(prepared.kind!=='uninstall')add(group,'Changes',plan.managedPaths);for(const values of [plan.writes,plan.externalPaths,plan.globalChanges])add(group,'Changes',values);for(const values of [plan.deletes,plan.removals])add(group,'Deletions',values);for(const values of [plan.preserved,plan.preserve])add(group,'Preserved',values);for(const note of plan.summary??[])group.notes.add(note);}
|
|
14
|
+
for(const entry of prepared.state?.targets??[])groupFor(entry);
|
|
15
|
+
const targets=[...new Set([...groups.values()].map(group=>group.platform))];
|
|
16
|
+
if(!targets.length&&prepared.options?.target)targets.push(prepared.options.target==='all'?'codex, claude':prepared.options.target);
|
|
17
|
+
const rows=['Operation: '+prepared.kind,'Targets: '+(targets.join(', ')||'not supplied')];
|
|
18
|
+
for(const group of groups.values())rows.push(group.platform+': '+['Changes','Deletions','Preserved'].map(category=>category+': '+(group.known.has(category)?group[category].size+' listed':'not supplied')).join(' | '));
|
|
19
|
+
const impacts=[];
|
|
20
|
+
if(prepared.kind==='install')impacts.push('Settings: '+summary(prepared.options??{}),'External references must be downloaded after applying.');
|
|
21
|
+
if(prepared.kind==='preset')impacts.push('Only the performance preset and managed profiles change. Other settings are preserved.');
|
|
22
|
+
if(prepared.kind==='uninstall')impacts.push('Remove ARC harness and managed profiles.',prepared.options?.skipGlobal?'Preserve ARC global instruction blocks.':'Remove selected ARC global instruction blocks.','Preserve native concurrency, user files, other skills and workflow audit state.','CLI npm package is retained.');
|
|
23
|
+
if(prepared.kind==='update')impacts.push('Installed settings are preserved.','If the global CLI is replaced, this session ends. npx does not replace the global CLI.');
|
|
24
|
+
rows.push(...impacts,'Counts describe unique listed paths, not every affected file.');
|
|
25
|
+
if(!prepared.plans?.length)rows.push('Detailed paths are not supplied by this prepared operation.');
|
|
26
|
+
for(const group of groups.values()){
|
|
27
|
+
rows.push('Target: '+group.platform,...(group.home?['Destination: '+group.home]:[]));
|
|
28
|
+
for(const category of ['Changes','Deletions','Preserved']){rows.push(category+': '+(group.known.has(category)?group[category].size+' listed paths':'not supplied'));for(const item of group[category].values()){rows.push(item.path);for(const description of item.descriptions)rows.push(' '+description);}}
|
|
29
|
+
if(group.notes.size)rows.push('Notes:',...group.notes);
|
|
30
|
+
}
|
|
31
|
+
return rows;
|
|
32
|
+
}
|
|
33
|
+
export function createTuiInterface(session,{input,controller}={}){
|
|
34
|
+
async function event(){const e=await session.nextEvent();if(e.name==='interrupt')throw interrupted();if(e.name==='eof')throw Object.assign(Error('Session closed'),{code:'ARC_TUI_EOF'});if(e.name==='error')throw e.error;return e;}
|
|
35
|
+
async function menu(title,rows,{selected=0,info=[],requiresConfirmation=false,backLabel='back',wordmark=false}={}){
|
|
36
|
+
while(true){const offset=Math.max(0,selected-7),visible=rows.slice(offset,offset+8),information=wrapDetailRows(info,session.viewport().columns);if(wordmark&&information.length)information.push('');const screen={title,wordmark,rows:[...information,...visible],selected:information.length+selected-offset,requiresConfirmation:requiresConfirmation&&rows[selected]==='Apply',hint:navigationHint('menu',{backLabel}),...(rows.length>8?{position:(offset+1)+'-'+Math.min(offset+8,rows.length)+'/'+rows.length}:{})};const rendered=session.render(screen);const e=await event();if(e.name==='up')selected=(selected+rows.length-1)%rows.length;else if(e.name==='down')selected=(selected+1)%rows.length;else if(e.name==='back')return null;else if(e.name==='enter'&&(!screen.requiresConfirmation||rendered.applyEnabled))return selected;}
|
|
37
|
+
}
|
|
38
|
+
async function text(title,initial=''){let value=initial;while(true){session.render({title,rows:[value],hint:navigationHint('text')});const e=await event();if(e.name==='text'||e.name==='paste')value+=e.text;else if(e.name==='left')value=[...value].slice(0,-1).join('');else if(e.name==='back')return null;else if(e.name==='enter')return value;}}
|
|
39
|
+
async function page(title,rows,{requireRead=false}={}){
|
|
40
|
+
let offset=0,confirmed=0,dimensions='';
|
|
41
|
+
while(true){const viewport=session.viewport(),current=viewport.columns+'x'+viewport.rows;if(current!==dimensions){if(requireRead){offset=0;confirmed=0;}dimensions=current;}
|
|
42
|
+
const wrapped=wrapDetailRows(rows,viewport.columns),size=Math.max(1,viewport.rows-3),last=Math.max(0,wrapped.length-size);
|
|
43
|
+
offset=Math.min(offset,last);const slice=wrapped.slice(offset,offset+size),hasNext=offset+size<wrapped.length;
|
|
44
|
+
const rendered=session.render({title,rows:slice,requiresConfirmation:requireRead,position:`Rows ${offset+1}-${Math.min(offset+size,wrapped.length)} of ${wrapped.length}`,hint:navigationHint(requireRead?'reviewPage':'page',{hasNext})});const e=await event();
|
|
45
|
+
if(e.name==='down')offset=Math.min(requireRead?Math.min(last,confirmed):last,offset+1);else if(e.name==='up')offset=Math.max(0,offset-1);else if(e.name==='back')return false;
|
|
46
|
+
else if(e.name==='enter'&&(!requireRead||rendered.applyEnabled)){
|
|
47
|
+
if(!requireRead)return true;
|
|
48
|
+
// Acknowledge only contiguous, fully displayed rows. Scrolling cannot jump ahead.
|
|
49
|
+
if(offset<=confirmed)confirmed=Math.max(confirmed,offset+slice.length);
|
|
50
|
+
if(confirmed>=wrapped.length)return true;offset=Math.min(last,confirmed);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function review(title,prepared){
|
|
55
|
+
const rows=operationRows(prepared),editable=prepared.kind!=='update',actions=editable?['Cancel','Edit','Apply','Details']:['Cancel','Apply','Details'];
|
|
56
|
+
const info=rows.slice(0,2);for(const row of rows.slice(2)){if(row.startsWith('Target:'))break;info.push(row);}
|
|
57
|
+
while(true){const choice=await menu('Review '+title,actions,{info,requiresConfirmation:true});
|
|
58
|
+
if(choice===null)return editable?'edit':'cancel';const action=actions[choice];if(action==='Cancel')return'cancel';if(action==='Edit')return'edit';
|
|
59
|
+
if(action==='Apply'){if(!await page('Review all changes',rows,{requireRead:true}))continue;const confirm=await menu('Confirm '+title,['Cancel','Apply'],{info:['All details reviewed. Apply the prepared snapshot?'],requiresConfirmation:true});if(confirm!==1)continue;return'apply';}
|
|
60
|
+
if(action==='Details')await page('Change details',rows);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function busy(action){session.render({title:'Working',rows:['Operation in progress. Please wait.'],hint:navigationHint('busy')});session.setBusy(true);const wasFlowing=input?.readableFlowing;const onData=chunk=>{if(String(chunk).includes('\x03')||String(chunk).includes('\x04'))controller?.abort();};input?.on('data',onData);try{const result=await action();if(controller?.signal.aborted){if(typeof result?.code!=='number')throw interrupted();return{...result,code:130,interrupted:true};}return result;}finally{input?.removeListener('data',onData);if(wasFlowing!==true)input?.pause?.();session.setBusy(false);session.discardInput();}}
|
|
64
|
+
return{menu,text,page,review,busy};
|
|
65
|
+
}
|
|
66
|
+
export function resultRows(result){return[...(result.messages??[]).map(s=>s.trim()),...[result.stdout,result.stderr].flatMap(text=>text?text.trimEnd().split(/\r?\n/):[]),...(result.results??[]).flatMap(item=>[`${item.platform??item.target?.platform??'Target'}: ${item.status}; rollback: ${item.rollback??'not needed'}`,...(item.residualPaths??[]).map(p=>'Residual path: '+p),...(item.error?['Error: '+item.error.message]:[])]),...(result.error?['Error: '+result.error.message]:[]),'Exit code: '+result.code,...(result.code===0?['Open a new agent session to use changed harness settings.']:['Review the failed target and retry from the menu.']),...(result.exitSession?['CLI replacement ended this session. Run arc again for further management.']:[])];}
|
|
67
|
+
export async function runTui(options={}){
|
|
68
|
+
let session=options.session,code=0,finalReport;const controller=new AbortController(),signals=options.signals??process;const abort=()=>controller.abort();signals.on('SIGINT',abort);signals.on('SIGTERM',abort);
|
|
69
|
+
try{session??=createTerminalSession(options);const ui=createTuiInterface(session,{input:options.input??process.stdin,controller}),api={collectStatus,prepareOperation,executeOperation,...options.services},context=options.context?{...options.context,signal:controller.signal}:await createManagementContext({...options.contextOptions,signal:controller.signal});const draft={};let selected=0;
|
|
70
|
+
async function execute(prepared){
|
|
71
|
+
if(!prepared)return;const result=await ui.busy(()=>api.executeOperation(context,prepared));code=result.code;
|
|
72
|
+
if(code===130||result.exitSession){finalReport=resultRows(result);if(result.exitSession)session.render({title:'Result',rows:finalReport,hint:'Session ended'});return true;}
|
|
73
|
+
await ui.page('Result',resultRows(result));return false;
|
|
74
|
+
}
|
|
75
|
+
async function uninstall(){
|
|
76
|
+
let target=0,global=0;
|
|
77
|
+
while(true){const nextTarget=await ui.menu('Uninstall target',['Codex','Claude','Both targets'],{selected:target});if(nextTarget===null)return null;target=nextTarget;
|
|
78
|
+
while(true){const nextGlobal=await ui.menu('Global ARC instructions',['Remove ARC blocks','Preserve ARC blocks'],{selected:global});if(nextGlobal===null)break;global=nextGlobal;
|
|
79
|
+
const prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude','all'][target],skipGlobal:global===1}));const decision=await ui.review('Uninstall',prepared);if(decision==='apply')return prepared;if(decision==='cancel')return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
while(true){const statuses=await api.collectStatus(context),ordered=[...statuses].sort((a,b)=>({claude:0,codex:1}[a.platform]??2)-({claude:0,codex:1}[b.platform]??2));
|
|
84
|
+
const label=status=>({codex:'Codex',claude:'Claude'}[status.platform]??status.platform),state=status=>({installed:'Installed',absent:'Not installed',incomplete:'Incomplete'}[status.state]??status.state);
|
|
85
|
+
const info=ordered.length?[ordered.map(status=>label(status)+': '+state(status)).join(' / '),...ordered.flatMap(status=>(status.diagnostics??[]).map(error=>label(status)+': '+error.message))]:[];const choice=await ui.menu('ARC',home,{selected,info,backLabel:'exit',wordmark:true});if(choice===null||choice===3)return code;selected=choice;
|
|
86
|
+
try{
|
|
87
|
+
if(choice===0){let configured=0;while(true){const task=await ui.menu('Configure',['Install / Reconfigure','Change Preset'],{selected:configured});if(task===null)break;configured=task;
|
|
88
|
+
try{const prepared=await runSetup({ui,statuses,draft,kind:task===0?'install':'preset',prepare:settings=>api.prepareOperation(context,settings)});if(await execute(prepared))return code;}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;code=error.code==='ARC_CLI_INPUT'?2:1;await ui.page('Operation unavailable',[error.message,'No unreviewed changes were applied.']);}
|
|
89
|
+
}continue;}
|
|
90
|
+
let prepared;if(choice===1){prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'update'}));if(await ui.review('Update',prepared)!=='apply')prepared=null;}else prepared=await uninstall();
|
|
91
|
+
if(await execute(prepared))return code;
|
|
92
|
+
}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;code=error.code==='ARC_CLI_INPUT'?2:1;await ui.page('Operation unavailable',[error.message,'No unreviewed changes were applied.']);}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
}catch(error){if(error.code==='ARC_CANCELLED')return 130;if(error.code==='ARC_TUI_EOF')return code;if(error.code==='ARC_TUI_TERMINAL')return 2;throw error;}finally{signals.removeListener('SIGINT',abort);signals.removeListener('SIGTERM',abort);session?.dispose();if(finalReport)(options.output??process.stdout).write(finalReport.map(row=>escapeTerminalText(row)).join('\n')+'\n');}
|
|
96
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Treat all screen content as data. Only terminal.mjs emits terminal commands.
|
|
2
|
+
export function escapeTerminalText(value=''){
|
|
3
|
+
return String(value).replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu,character=>{
|
|
4
|
+
const code=character.codePointAt(0);return code<=255?'\\x'+code.toString(16).padStart(2,'0'):'\\u{'+code.toString(16)+'}';
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
function width(character){return /[\p{Mark}]/u.test(character)?0:/[\u2190-\u2193\u2500-\u257f\u2588]/u.test(character)?1:character.codePointAt(0)>=0x1100?2:1;}
|
|
8
|
+
function fit(text,columns){let used=0,result='';for(const character of text){const size=width(character);if(used+size>columns)return{text:result,complete:false};result+=character;used+=size;}return{text:result,complete:true};}
|
|
9
|
+
export function navigationHint(mode='menu',{backLabel='back',hasNext=false}={}){
|
|
10
|
+
if(mode==='text')return'Type/Paste Input · ← Delete · Enter Continue · Esc/Backspace Back';
|
|
11
|
+
if(mode==='busy')return'Ctrl+C Interrupt';
|
|
12
|
+
if(mode==='page'||mode==='reviewPage')return'↑/↓: Scroll · Enter '+(mode==='page'?'Back':hasNext?'Next':'Continue')+' · Esc Back';
|
|
13
|
+
return'↑/↓: Select · Enter Continue · Esc '+(backLabel==='exit'?'Exit':'Back');
|
|
14
|
+
}
|
|
15
|
+
export function wrapDetailRows(rows,columns){
|
|
16
|
+
const available=Math.max(2,Math.floor(Number(columns)||80)-3),result=[];
|
|
17
|
+
for(const row of rows){const escaped=escapeTerminalText(row);let line='',used=0;for(const character of escaped){const size=width(character);if(used+size>available){result.push(line);line='';used=0;}line+=character;used+=size;}result.push(line);}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
const wordmark=[' █████ ███████ ███████ ','██ ██ ██ ██ ██ ','█████████ ███████ ██ ','██ ██ ██ ██ ██ ','██ ██ ██ ██ ███████ '];
|
|
21
|
+
const logoStart=Math.min(...wordmark.map(row=>row.indexOf('█'))),logoEnd=Math.max(...wordmark.map(row=>row.lastIndexOf('█')));
|
|
22
|
+
const logoStops=[[183,228,199],[116,198,157],[64,145,108]];
|
|
23
|
+
function logoColor(column){const position=(column-logoStart)/(logoEnd-logoStart)*2,segment=Math.min(1,Math.floor(position)),fraction=position-segment;return logoStops[segment].map((value,index)=>Math.round(value+(logoStops[segment+1][index]-value)*fraction));}
|
|
24
|
+
|
|
25
|
+
export function renderScreen(screen={}, {columns=80,rows=24}={}){
|
|
26
|
+
// Leave the last column unused to avoid terminal autowrap at the right edge.
|
|
27
|
+
columns=Math.max(1,Math.floor(Number(columns)||80)-1);rows=Math.max(1,Math.floor(Number(rows)||24));
|
|
28
|
+
const entries=screen.rows??[],hint=screen.hint??navigationHint('menu');
|
|
29
|
+
const heading=screen.wordmark?[...wordmark,'─'.repeat(Math.min(columns,34))]:[escapeTerminalText(screen.title)],position=screen.position?[escapeTerminalText(screen.position)]:[];
|
|
30
|
+
const lines=[...heading,...entries.map((row,index)=>(index===screen.selected?'> ':' ')+escapeTerminalText(row)),...position,escapeTerminalText(hint)];
|
|
31
|
+
const fitted=lines.map(line=>fit(line,columns));
|
|
32
|
+
const required=screen.requiredRows??entries.map((_,index)=>index);
|
|
33
|
+
const requiredVisible=required.every(index=>Number.isInteger(index)&&index>=0&&index<entries.length&&index+heading.length<rows-1-position.length&&fitted[index+heading.length].complete);
|
|
34
|
+
const applyEnabled=!screen.requiresConfirmation||(requiredVisible&&heading.every((_,index)=>fitted[index].complete)&&lines.length<=rows&&fitted.at(-1).complete&&position.every((_,index)=>fitted[lines.length-2-index].complete));
|
|
35
|
+
const visible=fitted.slice(0,rows).map(line=>line.text);
|
|
36
|
+
if(!applyEnabled)visible[visible.length-1]=fit('Resize terminal to review before applying.',columns).text;
|
|
37
|
+
const styles=[];
|
|
38
|
+
if(screen.wordmark){for(let index=0;index<Math.min(wordmark.length,visible.length);index++){if(visible[index]===fitted[index].text)styles.push({line:index,color:'logo',cells:[...visible[index]].flatMap((character,column)=>character==='█'?[{column,rgb:logoColor(column)}]:[])});}if(visible.length>wordmark.length&&visible[wordmark.length]===fitted[wordmark.length].text)styles.push({line:wordmark.length,color:'green'});}
|
|
39
|
+
const selectedLine=heading.length+screen.selected;
|
|
40
|
+
if(Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<visible.length&&!(screen.requiresConfirmation&&!applyEnabled&&selectedLine===visible.length-1)){const used=[...visible[selectedLine]].reduce((sum,character)=>sum+width(character),0);visible[selectedLine]+=' '.repeat(Math.max(0,Math.min(columns,34)-used));styles.push({line:selectedLine,color:'selected'});}
|
|
41
|
+
|
|
42
|
+
return{text:visible.join('\n'),applyEnabled,styles};
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {parsePositiveLimit} from '../limits.mjs';
|
|
2
|
+
export function initialSettings(statuses){return Object.fromEntries(['codex','claude'].map(platform=>{const settings=statuses.find(s=>s.platform===platform)?.settings;return[platform,{preset:settings?.modelConfig?.preset??'balanced',language:settings?.language??'en',maxTotalSubagents:settings?.limits?.max_total_subagents??100,maxConcurrentSubagents:settings?.limits?.max_concurrent_subagents??5,skipGlobal:false}];}));}
|
|
3
|
+
export function setupOptions(draft,target){if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude))throw Error('Target settings differ. Explicitly select common values before applying to both.');return{...draft[target==='all'?'codex':target],target};}
|
|
4
|
+
export async function runSetup({ui,statuses,draft,kind,prepare}){
|
|
5
|
+
for(const[key,value]of Object.entries(initialSettings(statuses)))draft[key]??=value;
|
|
6
|
+
let selectedTarget=0;
|
|
7
|
+
while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Reconfigure',['Codex','Claude','Both targets'],{selected:selectedTarget});if(targetIndex===null)return null;selectedTarget=targetIndex;const target=['codex','claude','all'][targetIndex];
|
|
8
|
+
if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude)){
|
|
9
|
+
const choice=await ui.menu('Target settings differ',['Back','Use Codex values for both','Use Claude values for both'],{info:['Codex: '+summary(draft.codex),'Claude: '+summary(draft.claude),'Choosing common values replaces the other target values.']});if(choice===null||choice===0)continue;const source=choice===1?'codex':'claude';draft.codex={...draft[source]};draft.claude={...draft[source]};
|
|
10
|
+
}
|
|
11
|
+
const settings=draft[target==='all'?'codex':target];let editing=true,selectedField=0;
|
|
12
|
+
while(editing){const labels=kind==='preset'?['Preset: '+settings.preset,'Review changes']:['Preset: '+settings.preset,'Harness language: '+settings.language,'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Review changes'];const field=await ui.menu('Settings — '+target,labels,{selected:selectedField});if(field===null)break;selectedField=field;
|
|
13
|
+
if(field===0){const value=await ui.menu('Performance preset',['Economy','Balanced','Smart'],{selected:['economy','balanced','smart'].indexOf(settings.preset)});if(value!==null)settings.preset=['economy','balanced','smart'][value];}
|
|
14
|
+
else if(kind==='install'&&field===1){const value=await ui.menu('Harness output language',['English','Korean'],{selected:settings.language==='ko'?1:0});if(value!==null)settings.language=['en','ko'][value];}
|
|
15
|
+
else if(kind==='install'&&(field===2||field===3)){const key=field===2?'maxTotalSubagents':'maxConcurrentSubagents';const raw=await ui.text(field===2?'Maximum total subagents':'Maximum concurrent subagents',String(settings[key]));if(raw!==null)try{settings[key]=parsePositiveLimit(raw);}catch{await ui.page('Invalid limit',['Enter a positive safe ASCII decimal integer.']);}}
|
|
16
|
+
else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
|
|
17
|
+
else{if(target==='all')draft.claude={...settings};const prepared=await prepare({kind,...setupOptions(draft,target)});const action=await ui.review(kind==='preset'?'Change Preset':'Install / Reconfigure',prepared);if(action==='apply'||action===true)return prepared;if(action==='cancel')return null;}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function summary(s){return`preset=${s.preset}, language=${s.language}, total=${s.maxTotalSubagents}, concurrent=${s.maxConcurrentSubagents}, global=${!s.skipGlobal}`;}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {StringDecoder} from 'node:string_decoder';
|
|
2
|
+
import {renderScreen} from './render.mjs';
|
|
3
|
+
|
|
4
|
+
/** Events are {name, sequence, ...detail}. Names: up/down/left/right, enter,
|
|
5
|
+
* back, text/paste (text), resize (columns,rows), blocked, eof, interrupt
|
|
6
|
+
* (signal), error (error). Every data chunk gets a sequence; paste is one event.
|
|
7
|
+
* render/discardInput/setBusy invalidate buffered input. While busy all data is
|
|
8
|
+
* discarded. Only one nextEvent waiter is allowed. dispose is idempotent,
|
|
9
|
+
* restores owned terminal state, and settles a waiting reader with eof. Shutdown
|
|
10
|
+
* discards unread events; a chunk is fully decoded before resolving a reader,
|
|
11
|
+
* so an Enter followed by EOF in that chunk cannot escape shutdown.
|
|
12
|
+
*/
|
|
13
|
+
export function createTerminalSession({input=process.stdin,output=process.stdout,signals=process,env=process.env}={}){
|
|
14
|
+
if(!input.isTTY||!output.isTTY)throw Object.assign(Error('An interactive terminal is required.'),{code:'ARC_TUI_TERMINAL'});
|
|
15
|
+
const wasRaw=Boolean(input.isRaw),wasFlowing=input.readableFlowing;
|
|
16
|
+
const colorEnabled=env.NO_COLOR===undefined&&env.TERM!=='dumb';
|
|
17
|
+
const truecolor=colorEnabled&&(/^(truecolor|24bit)$/i.test(env.COLORTERM??'')||/^(ghostty|cmux)$/i.test(env.TERM_PROGRAM??'')||/-direct$/.test(env.TERM??'')||(output.getColorDepth?.()??0)>=24);
|
|
18
|
+
let disposed=false,busy=false,parsing=false,pending='',paste=null,sequence=0,escapeTimer,waiter,queue=[],screen,lastLines=0,applyEnabled=true;
|
|
19
|
+
let decoder=new StringDecoder('utf8'),lastWasCR=false;
|
|
20
|
+
const clearTimer=()=>{clearTimeout(escapeTimer);escapeTimer=undefined;};
|
|
21
|
+
function deliver(name,detail={}){
|
|
22
|
+
const event={name,sequence,...detail};if(waiter&&!parsing){const resolve=waiter;waiter=undefined;resolve(event);}else queue.push(event);
|
|
23
|
+
}
|
|
24
|
+
function discardInput(){clearTimer();pending='';paste=null;decoder=new StringDecoder('utf8');queue=[];}
|
|
25
|
+
function dispose(){
|
|
26
|
+
if(disposed)return;disposed=true;clearTimer();pending='';paste=null;queue=[];
|
|
27
|
+
input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);
|
|
28
|
+
output.removeListener('resize',onResize);output.removeListener('error',onError);signals.removeListener('SIGINT',onSigint);signals.removeListener('SIGTERM',onSigterm);
|
|
29
|
+
try{if(input.setRawMode&&!input.destroyed)input.setRawMode(wasRaw);}finally{
|
|
30
|
+
try{if(wasFlowing!==true)input.pause();}finally{
|
|
31
|
+
try{output.write('\x1b[0m\x1b[?2004l\x1b[?25h\n');}finally{if(waiter){const resolve=waiter;waiter=undefined;resolve({name:'eof',sequence});}}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function finish(name,detail){const resolve=waiter;waiter=undefined;dispose();if(resolve)resolve({name,sequence,...detail});else deliver(name,detail);}
|
|
36
|
+
function onEnd(){if(!disposed)finish('eof');}
|
|
37
|
+
function onError(error){if(!disposed)finish('error',{error});}
|
|
38
|
+
function onSigint(){if(!disposed)finish('interrupt',{signal:'SIGINT'});}
|
|
39
|
+
function onSigterm(){if(!disposed)finish('interrupt',{signal:'SIGTERM'});}
|
|
40
|
+
function draw(){
|
|
41
|
+
const result=renderScreen(screen,{columns:output.columns,rows:output.rows});applyEnabled=result.applyEnabled;
|
|
42
|
+
if(lastLines)output.write(`\x1b[${lastLines}A\r`);
|
|
43
|
+
const lines=result.text.split('\n');
|
|
44
|
+
for(let index=0;index<Math.max(lastLines,lines.length);index++){
|
|
45
|
+
let line=lines[index]??'';const style=result.styles.find(style=>style.line===index);
|
|
46
|
+
if(style&&colorEnabled){
|
|
47
|
+
if(style.color==='logo'&&truecolor){const cells=new Map(style.cells.map(cell=>[cell.column,cell.rgb]));line=[...line].map((character,column)=>cells.has(column)?'\x1b[38;2;'+cells.get(column).join(';')+'m'+character+'\x1b[0m':character).join('');}
|
|
48
|
+
else{const color=style.color==='logo'?'\x1b[92m':style.color==='selected'?(truecolor?'\x1b[48;2;16;55;26m\x1b[38;2;220;245;224m':'\x1b[42;30m'):'\x1b[32m';line=color+line+'\x1b[0m';}
|
|
49
|
+
}
|
|
50
|
+
output.write('\x1b[2K'+line+'\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if(lastLines>lines.length)output.write(`\x1b[${lastLines-lines.length}A\r`);
|
|
54
|
+
lastLines=lines.length;return result;
|
|
55
|
+
}
|
|
56
|
+
function render(value){if(disposed)return{applyEnabled:false,text:''};discardInput();screen=value;try{return draw();}catch(error){dispose();throw error;}}
|
|
57
|
+
function onResize(){if(disposed)return;discardInput();try{if(screen)draw();deliver('resize',{columns:output.columns,rows:output.rows});}catch(error){onError(error);}}
|
|
58
|
+
function parse(){
|
|
59
|
+
while(pending&&!disposed){
|
|
60
|
+
if(paste!==null){const end=pending.indexOf('\x1b[201~');if(end<0){const tail=pending.lastIndexOf('\x1b');if(tail<0){paste+=pending;pending='';}else{paste+=pending.slice(0,tail);pending=pending.slice(tail);}return;}paste+=pending.slice(0,end);pending=pending.slice(end+6);deliver('paste',{text:paste});paste=null;continue;}
|
|
61
|
+
if(pending.startsWith('\x1b[200~')){pending=pending.slice(6);paste='';continue;}
|
|
62
|
+
const arrow=/^\x1b(?:\[|O)([ABCD])/.exec(pending);
|
|
63
|
+
if(arrow){pending=pending.slice(arrow[0].length);deliver({A:'up',B:'down',C:'right',D:'left'}[arrow[1]]);continue;}
|
|
64
|
+
if(pending[0]==='\x1b'){
|
|
65
|
+
if(pending==='\x1b'||['\x1b[','\x1bO','\x1b[2','\x1b[20','\x1b[200'].includes(pending)){escapeTimer=setTimeout(()=>{escapeTimer=undefined;pending='';deliver('back');},35);return;}
|
|
66
|
+
// Unknown escape sequences are consumed as text, never executed as keys.
|
|
67
|
+
const unknown=/^\x1b\[[0-9;?]*[ -/]*[@-~]/.exec(pending);if(unknown){pending=pending.slice(unknown[0].length);continue;}
|
|
68
|
+
pending=pending.slice(1);deliver('back');continue;
|
|
69
|
+
}
|
|
70
|
+
const character=[...pending][0];pending=pending.slice(character.length);const skipLF=lastWasCR&&character==='\n';lastWasCR=character==='\r';if(skipLF)continue;
|
|
71
|
+
if(character==='\x03'){onSigint();return;}if(character==='\x04'){onEnd();return;}
|
|
72
|
+
if(character==='\r'||character==='\n')deliver(applyEnabled?'enter':'blocked');
|
|
73
|
+
else if(character==='\x7f'||character==='\b')deliver('back');
|
|
74
|
+
else if(!/[\p{Cc}]/u.test(character))deliver('text',{text:character});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function onData(chunk){if(disposed||busy)return;sequence++;clearTimer();pending+=typeof chunk==='string'?chunk:decoder.write(chunk);parsing=true;try{parse();}finally{parsing=false;if(waiter&&queue.length){const resolve=waiter;waiter=undefined;resolve(queue.shift());}}}
|
|
78
|
+
input.on('data',onData);input.on('end',onEnd);input.on('close',onEnd);input.on('error',onError);output.on('resize',onResize);output.on('error',onError);signals.on('SIGINT',onSigint);signals.on('SIGTERM',onSigterm);
|
|
79
|
+
try{input.setRawMode?.(true);input.resume();output.write('\x1b[?25l\x1b[?2004h');}catch(error){dispose();throw error;}
|
|
80
|
+
return{render,discardInput,dispose,viewport(){return{columns:output.columns??80,rows:output.rows??24};},setBusy(value){busy=Boolean(value);discardInput();},nextEvent(){if(queue.length)return Promise.resolve(queue.shift());if(disposed)return Promise.resolve({name:'eof',sequence});if(waiter)return Promise.reject(Error('Only one terminal event reader is allowed.'));return new Promise(resolve=>{waiter=resolve;});}};
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as workflowServices from '../management/workflow.mjs';
|
|
2
|
+
export async function runWorkflow({ui,cwd,services={},onResult=()=>{}}){
|
|
3
|
+
const api={...workflowServices,...services};let workspace=cwd;
|
|
4
|
+
while(true){let observed;try{observed=await api.observeWorkspace(workspace);}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;const selection=await ui.menu('Workflow workspace',['Back','Change workspace'],{info:['Workspace: '+workspace,'Cannot observe this repository. Select a valid repository root.']});if(selection!==1)return;const value=await ui.text('Absolute repository root',workspace);if(value!==null)workspace=value;continue;}workspace=observed.realpath;const selection=await ui.menu('Workflow workspace',['Continue','Change workspace'],{info:['Workspace: '+observed.realpath,'Branch: '+(observed.branch??'(detached)'),'HEAD: '+observed.head]});if(selection===null)return;if(selection===1){const value=await ui.text('Absolute repository root',workspace);if(value!==null)workspace=value;continue;}
|
|
5
|
+
let leave=false;while(!leave){const workflows=await api.listWorkflows(workspace);const pick=await ui.menu('Workflows',['Initialize workflow',...workflows.map(w=>w.id+(w.foreign?' — different target':w.invalid?' — invalid state':` — revision ${w.revision}`))]);if(pick===null)break;
|
|
6
|
+
let workflowId;if(pick===0){workflowId=await ui.text('Workflow ID','');if(workflowId===null)continue;const actor=await ui.text('Actor ID (operator attestation)','');if(actor===null)continue;const messageRef=await ui.text('Target selection message reference','');if(messageRef===null)continue;const contentHash=await ui.text('Target selection content hash (sha256:...)','');if(contentHash===null)continue;await submit('init',{mode:'plan',actor:{id:actor},selection:{messageRef,contentHash}});}
|
|
7
|
+
else{workflowId=workflows[pick-1].id;while(true){const action=await ui.menu('Workflow — '+workflowId,['Refresh status','Record evidence','Prepare task / operation','Record result','Reconcile']);if(action===null)break;const command=['status','record','prepare','result','reconcile'][action];if(command==='status')await submit(command);else{const jsonPath=await ui.text('Absolute regular JSON file (payload stays private)','');if(jsonPath!==null)await submit(command,undefined,jsonPath);}}}
|
|
8
|
+
async function submit(command,input,jsonPath){try{const prepared=await api.prepareWorkflow({cwd:workspace,workflowId,command,input,jsonPath});const review=await ui.review('Workflow '+command,{kind:'workflow',command,cwd:workspace,workflowId});if(review!=='apply'&&review!==true)return;const result=await ui.busy?.(()=>api.executeWorkflow(prepared))??await api.executeWorkflow(prepared);onResult(result);await ui.page('Workflow '+String(result.decision).toUpperCase(),workflowRows(result));}catch(error){if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;await ui.page('Workflow input rejected',['Check absolute paths, JSON schema, revision and matching checkout.','Payload and error details are hidden to protect secrets.']);}}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function workflowRows(result){return['Decision: '+result.decision+' (exit '+result.code+')','Workflow: '+result.workflowId,'Revision: '+result.revision,...(result.target?['Workspace: '+result.target.realpath,'HEAD: '+result.target.head]:[]),...(result.ruleIds??[]).map(r=>'Rule: '+r),...(result.tasks??[]).map(t=>'Task: '+t.id+' — '+t.status),...(result.unresolved??[]),...(result.requiredEvidence??[]).map(r=>'Required: '+r),...(result.nextActions??[]).map(r=>'Next: '+r),...(result.error?[result.error]:[])];}
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
import {fileURLToPath} from 'node:url';
|
|
2
1
|
import {runPostinstall} from '../lib/cli/postinstall.mjs';
|
|
3
|
-
|
|
4
|
-
process.exitCode=await runPostinstall({packageRoot,cwd:packageRoot});
|
|
2
|
+
process.exitCode=await runPostinstall({});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import {RUNTIME_MODULE_PATHS} from '../lib/installer/runtime-assets.mjs';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import {COOPERATIVE_RUNTIME_MODULES} from '../lib/runtime/bootstrap.mjs';
|
|
4
6
|
const args=process.argv.slice(2);const i=args.indexOf('--root');
|
|
5
7
|
const root=i<0?fileURLToPath(new URL('../',import.meta.url)):path.resolve(args[i+1]);
|
|
6
8
|
const check=args.includes('--check');
|
|
@@ -11,7 +13,10 @@ function safe(filename,directory=false){
|
|
|
11
13
|
}
|
|
12
14
|
function sources(relative){const dir=path.join(root,relative);safe(dir,true);const result=[];for(const entry of fs.readdirSync(dir).sort()){const rel=path.join(relative,entry);const file=path.join(root,rel);safe(file,fs.lstatSync(file).isDirectory());if(fs.lstatSync(file).isDirectory())result.push(...sources(rel));else if(entry.endsWith('.mjs'))result.push(rel);else throw Error(`Unexpected canonical runtime file: ${rel}`);}return result;}
|
|
13
15
|
try{
|
|
14
|
-
safe(root,true);const
|
|
16
|
+
safe(root,true);const modules=sources('lib/runtime').map(name=>name.split(path.sep).join('/')).sort();
|
|
17
|
+
for(const name of COOPERATIVE_RUNTIME_MODULES)if(!modules.includes(`lib/runtime/${name}`))throw Error(`Missing runtime module: ${name}`);
|
|
18
|
+
if(modules.length!==RUNTIME_MODULE_PATHS.length||modules.some((name,index)=>name!==RUNTIME_MODULE_PATHS[index]))throw Error('Runtime asset inventory does not match canonical modules');
|
|
19
|
+
const entries=modules.map(name=>[name.slice('lib/runtime/'.length),fs.readFileSync(path.join(root,name))]);
|
|
15
20
|
const policy=path.join(root,'lib/policy/role-policy.json');safe(policy);entries.push(['role-policy.json',fs.readFileSync(policy)]);
|
|
16
21
|
const outputs=[];for(const platform of ['codex','claude'])for(const [name,bytes]of entries){const filename=path.join(root,platform,'skill/runtime',name);safe(filename);outputs.push({filename,bytes});}
|
|
17
22
|
let drift=false;for(const {filename,bytes}of outputs){if(!fs.existsSync(filename)||!fs.readFileSync(filename).equals(bytes)){drift=true;if(!check){fs.mkdirSync(path.dirname(filename),{recursive:true});fs.writeFileSync(filename,bytes);}}}
|
package/lib/cli/choices.mjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import readline from 'node:readline';
|
|
2
|
-
import {EventEmitter} from 'node:events';
|
|
3
|
-
function inputError(message){return Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});}
|
|
4
|
-
function cancelled(){return Object.assign(new Error('Installation cancelled.'),{code:'ARC_CANCELLED'});}
|
|
5
|
-
export async function selectChoices(options,{input=process.stdin,output=process.stdout,targetOnly=false,languageDefault}={}){
|
|
6
|
-
if(languageDefault===undefined&&options.target&&(targetOnly||options.preset)||languageDefault!==undefined&&options.language)return options;
|
|
7
|
-
if(!input.isTTY||!output.isTTY)throw inputError(targetOnly?'Non-interactive execution requires --target':'Non-interactive execution requires --target and --preset');
|
|
8
|
-
const menus=languageDefault!==undefined?[['language',['English','한국어'],['en','ko'],languageDefault==='ko'?1:0]]:[['target',['Claude','Codex','All'],['claude','codex','all'],0],['preset',['Economy','Balanced','Smart'],['economy','balanced','smart'],1]].filter(([key])=>!options[key]&&(!targetOnly||key==='target'));
|
|
9
|
-
const wasRaw=input.isRaw,wasFlowing=input.readableFlowing,listeners=new Map(['data','newListener'].map(event=>[event,input.listeners(event)]));
|
|
10
|
-
let menu=0,index=menus[0][3];
|
|
11
|
-
const draw=(redraw=false)=>{const [key,labels]=menus[menu];if(redraw)output.write(`\x1b[${labels.length}A\r`);else output.write(`Select ${key==='target'?'an installation target':key==='language'?'an output language':'a performance preset'} [Up/Down]:\n`);output.write(labels.map((label,i)=>`\x1b[2K${i===index?'>':' '} ${label}`).join('\n')+'\n');};
|
|
12
|
-
const keys=new EventEmitter();let onKey,onData,onEnd,onError,abort;
|
|
13
|
-
try{return await new Promise((resolve,reject)=>{
|
|
14
|
-
abort=()=>reject(cancelled());onEnd=abort;onError=reject;
|
|
15
|
-
onKey=(str,key={})=>{try{if(key.name==='c'&&key.ctrl||key.name==='d'&&key.ctrl)return abort();if(key.name==='up'||key.name==='down'){index=(index+(key.name==='up'?-1:1)+menus[menu][1].length)%menus[menu][1].length;draw(true);}else if(key.name==='return'||key.name==='enter'){const [name,,values]=menus[menu];options[name]=values[index];output.write('\n');if(++menu===menus.length){keys.removeListener('keypress',onKey);resolve(options);}else{index=menus[menu][3];draw();}}}catch(error){reject(error);}};
|
|
16
|
-
keys.on('keypress',onKey);readline.emitKeypressEvents(keys);onData=chunk=>keys.emit('data',chunk);input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);
|
|
17
|
-
if(input.setRawMode)input.setRawMode(true);input.resume();draw();
|
|
18
|
-
});}finally{
|
|
19
|
-
keys.removeAllListeners();input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
|
|
20
|
-
for(const [event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
|
|
21
|
-
if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function selectLimits(options,selections,{input=process.stdin,output=process.stdout}={}){
|
|
26
|
-
const{parsePositiveLimit}=await import('../limits.mjs');
|
|
27
|
-
const fields=[['maxTotalSubagents','max_total_subagents',100,'total'],['maxConcurrentSubagents','max_concurrent_subagents',5,'concurrent']].filter(([key])=>options[key]===undefined);
|
|
28
|
-
if(!fields.length)return options;
|
|
29
|
-
const wasRaw=input.isRaw,wasFlowing=input.readableFlowing,listeners=new Map(['data','newListener'].map(event=>[event,input.listeners(event)]));
|
|
30
|
-
let index=0,value='',onKey,onData,onEnd,onError,abort;
|
|
31
|
-
const draw=()=>{const[,,fallback,label]=fields[index];output.write(`Maximum ${label} subagents [Default=${fallback}]: `);};
|
|
32
|
-
try{return await new Promise((resolve,reject)=>{
|
|
33
|
-
abort=()=>reject(cancelled());onEnd=abort;onError=reject;
|
|
34
|
-
onKey=(str,key={})=>{if(key.ctrl&&(key.name==='c'||key.name==='d'))return abort();if(key.name==='return'||key.name==='enter'){
|
|
35
|
-
const[name,,,label]=fields[index];if(value.trim()){try{options[name]=parsePositiveLimit(value,label);}catch(cause){output.write(`\n${cause.message}\n`);value='';draw();return;}}
|
|
36
|
-
if(name==='maxConcurrentSubagents')options.concurrentSelected=true;output.write('\n');value='';if(++index===fields.length){input.removeListener('keypress',onKey);resolve(options);}else draw();
|
|
37
|
-
}else if(key.name==='backspace'){value=value.slice(0,-1);output.write('\b \b');}else if(str&&!key.ctrl&&!key.meta){value+=str;output.write(str);}};
|
|
38
|
-
onData=chunk=>{for(const character of chunk.toString()){if(index===fields.length)break;const name=character==='\r'||character==='\n'?'return':character==='\x03'?'c':character==='\x04'?'d':character==='\x7f'||character==='\b'?'backspace':undefined;onKey(character,{name,ctrl:character==='\x03'||character==='\x04'});}};
|
|
39
|
-
input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);if(input.setRawMode)input.setRawMode(true);input.resume();draw();
|
|
40
|
-
});}finally{
|
|
41
|
-
input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
|
|
42
|
-
for(const[event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
|
|
43
|
-
if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function selectLanguage(options,languageDefault,io={}){return selectChoices(options,{...io,languageDefault});}
|
package/lib/cli/completion.mjs
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
export function renderCompletion(shell){
|
|
2
|
-
if(shell==='bash')return `_arc() {
|
|
3
|
-
local cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}" command="\${COMP_WORDS[1]}" choices=""
|
|
4
|
-
case "$prev" in
|
|
5
|
-
--target) [[ $command == -* || $command == preset || $command == uninstall ]] && choices="claude codex all" ;;
|
|
6
|
-
--preset) [[ $command == -* ]] && choices="economy balanced smart" ;;
|
|
7
|
-
--language) [[ $command == -* ]] && choices="en ko" ;;
|
|
8
|
-
*)
|
|
9
|
-
if (( COMP_CWORD == 1 )) && [[ $cur != -* ]]; then choices="preset uninstall completion upgrade"
|
|
10
|
-
else
|
|
11
|
-
case "$command" in
|
|
12
|
-
preset)
|
|
13
|
-
if (( COMP_CWORD == 2 )) && [[ $cur != -* ]]; then choices="economy balanced smart"
|
|
14
|
-
else choices="--target --check --dry-run --help"; fi ;;
|
|
15
|
-
uninstall) choices="--target --dry-run --skip-global --help --version" ;;
|
|
16
|
-
upgrade) choices="--help" ;;
|
|
17
|
-
completion) (( COMP_CWORD == 2 )) && choices="zsh bash" ;;
|
|
18
|
-
-*) choices="--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version" ;;
|
|
19
|
-
esac
|
|
20
|
-
fi ;;
|
|
21
|
-
esac
|
|
22
|
-
COMPREPLY=( $(compgen -W "$choices" -- "$cur") )
|
|
23
|
-
}
|
|
24
|
-
complete -F _arc arc
|
|
25
|
-
`;
|
|
26
|
-
if(shell==='zsh')return `_arc() {
|
|
27
|
-
local -a candidates
|
|
28
|
-
if (( CURRENT == 2 )) && [[ \${words[CURRENT]} != -* ]]; then
|
|
29
|
-
candidates=(preset uninstall completion upgrade)
|
|
30
|
-
else
|
|
31
|
-
case "\${words[CURRENT-1]}" in
|
|
32
|
-
--target) [[ \${words[2]} == -* || \${words[2]} == preset || \${words[2]} == uninstall ]] && candidates=(claude codex all) ;;
|
|
33
|
-
--preset) [[ \${words[2]} == -* ]] && candidates=(economy balanced smart) ;;
|
|
34
|
-
--language) [[ \${words[2]} == -* ]] && candidates=(en ko) ;;
|
|
35
|
-
*) case "\${words[2]}" in
|
|
36
|
-
preset)
|
|
37
|
-
if (( CURRENT == 3 )) && [[ \${words[CURRENT]} != -* ]]; then candidates=(economy balanced smart)
|
|
38
|
-
else candidates=(--target --check --dry-run --help); fi ;;
|
|
39
|
-
uninstall) candidates=(--target --dry-run --skip-global --help --version) ;;
|
|
40
|
-
upgrade) candidates=(--help) ;;
|
|
41
|
-
completion) (( CURRENT == 3 )) && candidates=(zsh bash) ;;
|
|
42
|
-
-*) candidates=(--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version) ;;
|
|
43
|
-
esac ;;
|
|
44
|
-
esac
|
|
45
|
-
fi
|
|
46
|
-
compadd -a candidates
|
|
47
|
-
}
|
|
48
|
-
if (( ! $+functions[compdef] )); then
|
|
49
|
-
autoload -Uz compinit
|
|
50
|
-
compinit -D
|
|
51
|
-
fi
|
|
52
|
-
compdef _arc arc
|
|
53
|
-
`;
|
|
54
|
-
throw new Error('Unsupported completion shell');
|
|
55
|
-
}
|
package/lib/cli/index.mjs
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import {fileURLToPath} from 'node:url';
|
|
5
|
-
const packageRoot=fileURLToPath(new URL('../../',import.meta.url));
|
|
6
|
-
import{parsePositiveLimit}from'../limits.mjs';
|
|
7
|
-
const usage='Usage: arc --target <claude|codex|all> --preset <economy|balanced|smart> --language <en|ko>\n [--max-total-subagents <count>] [--max-concurrent-subagents <count>]\n [--check | --dry-run] [--skip-global] [--help] [--version]\n --max-total-subagents: positive integer (default: 100)\n --max-concurrent-subagents: positive integer (default: 5)';
|
|
8
|
-
function inputError(message){return Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});}
|
|
9
|
-
function cancelled(){return Object.assign(new Error('Installation cancelled.'),{code:'ARC_CANCELLED'});}
|
|
10
|
-
export function parseArgs(args){
|
|
11
|
-
const options={},seen=new Set();const names={'--max-total-subagents':'maxTotalSubagents','--max-concurrent-subagents':'maxConcurrentSubagents','--target':'target','--preset':'preset','--language':'language','--skip-global':'skipGlobal','--check':'check','--dry-run':'dryRun','--help':'help','--version':'version'};
|
|
12
|
-
for(let i=0;i<args.length;i++){const flag=args[i],key=Object.hasOwn(names,flag)?names[flag]:undefined;if(!key)throw inputError(`Unknown argument: ${flag}`);if(seen.has(flag))throw inputError(`Duplicate argument: ${flag}`);seen.add(flag);if(key==='maxTotalSubagents'||key==='maxConcurrentSubagents'){try{options[key]=parsePositiveLimit(args[++i],flag);}catch(cause){throw inputError(cause.message);}}else if(key==='target'||key==='preset'||key==='language'){const value=args[++i],allowed=key==='target'?['claude','codex','all']:key==='preset'?['economy','balanced','smart']:['en','ko'];if(!allowed.includes(value))throw inputError(`Invalid ${key}: ${value??'missing value'}`);options[key]=value;}else options[key]=true;}
|
|
13
|
-
if(options.check&&options.dryRun)throw inputError('--check and --dry-run cannot be combined');return options;
|
|
14
|
-
}
|
|
15
|
-
export async function selectChoices(options,io){return(await import('./choices.mjs')).selectChoices(options,io);}
|
|
16
|
-
export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr,cwd=process.cwd(),env=process.env}={}){
|
|
17
|
-
let controller,onInterrupt,progress;
|
|
18
|
-
try{
|
|
19
|
-
const options=parseArgs(args);if(options.help){output.write(usage+'\n');return 0;}if(options.version){output.write(JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version+'\n');return 0;}
|
|
20
|
-
if((!input.isTTY||!output.isTTY)&&(!options.target||!options.preset||!options.language))throw inputError('Non-interactive execution requires --target, --preset and --language');
|
|
21
|
-
await selectChoices(options,{input,output});
|
|
22
|
-
const {startProgress}=await import('./progress.mjs');
|
|
23
|
-
const interactive=input.isTTY&&output.isTTY,label=options.check?'Checking ARC…':options.dryRun?'Preparing ARC dry run…':'Installing ARC…';
|
|
24
|
-
if(!interactive)progress=startProgress({output,label});
|
|
25
|
-
controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
|
|
26
|
-
const {loadNative}=await import('../safe-fs/native.mjs');const native=loadNative();
|
|
27
|
-
const [{openRoot},{verifyPayload},{resolveTarget},{buildInstallPlan,validatePlanSet}]=await Promise.all([import('../safe-fs/index.mjs'),import('../installer/manifest.mjs'),import('../installer/targets.mjs'),import('../installer/plan.mjs')]);
|
|
28
|
-
const safeFs={openRoot:p=>openRoot(p,{native})},payload=verifyPayload({packageRoot,safeFs});
|
|
29
|
-
const platforms=options.target==='all'?['codex','claude']:[options.target];
|
|
30
|
-
const {previewLimits,previewLanguage}=await import('../installer/plan.mjs');const{defaultLanguage}=await import('../installer/language.mjs');const selections=platforms.map(platform=>{const target=resolveTarget(platform,{userHome:os.homedir(),env,cwd});return{target,preview:previewLimits({target,safeFs}),languagePreview:previewLanguage({target,safeFs})};});
|
|
31
|
-
if(interactive){const{selectLimits,selectLanguage}=await import('./choices.mjs');await selectLimits(options,selections,{input,output});await selectLanguage(options,defaultLanguage(selections.map(item=>item.languagePreview.existing)),{input,output});}
|
|
32
|
-
progress??=startProgress({output,label});
|
|
33
|
-
const plans=selections.map(({target,preview,languagePreview})=>buildInstallPlan({payload,target,language:options.language,languagePreview,preset:options.preset,skipGlobal:options.skipGlobal,safeFs,limitsPreview:preview,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)}));const {attachShellCompletion}=await import('../installer/shell-completion.mjs');attachShellCompletion(plans,{safeFs});validatePlanSet(plans);
|
|
34
|
-
const languageLabel=options.language==='ko'?'한국어':'English';
|
|
35
|
-
progress.write(`Install: ${platforms.join('/')} · ${options.preset} · ${languageLabel}\n`);
|
|
36
|
-
for(const plan of plans)progress.write(`${plan.target.platform}: ${plan.target.home} · total ${plan.limits.max_total_subagents} · concurrent ${plan.limits.max_concurrent_subagents}\n`);
|
|
37
|
-
if(options.skipGlobal)progress.write('Global settings: skipped\n');
|
|
38
|
-
else progress.write('zsh completion: eval "$(arc completion zsh)" for this session\n');
|
|
39
|
-
if(plans.some(plan=>plan.limits.max_concurrent_subagents>plan.limits.max_total_subagents))progress.write(`Note: fresh ARC sessions cap concurrency at ${Math.min(...plans.map(plan=>plan.limits.max_total_subagents))}.\n`);
|
|
40
|
-
if(options.dryRun)for(const plan of plans){
|
|
41
|
-
for(const name of plan.generated.skill.keys())progress.write(`Would write file: ${path.join(plan.target.home,'skills/arc',name)}\n`);
|
|
42
|
-
for(const name of plan.generated.profiles.keys())progress.write(`Would write file: ${path.join(plan.target.home,'agents',name)}\n`);
|
|
43
|
-
for(const change of plan.globalChanges)progress.write(`Would update global file: ${change.path}\n`);
|
|
44
|
-
}
|
|
45
|
-
if(options.check||options.dryRun){progress.stop();output.write(options.check?'Check completed. No changes made.\n':'Dry run completed. No changes made.\n');return 0;}
|
|
46
|
-
const [{installTargets},{acquireExternalSkills}]=await Promise.all([import('../installer/install.mjs'),import('../installer/acquisition.mjs')]);
|
|
47
|
-
const result=await installTargets(plans,{safeFs,signal:controller.signal,acquisition:async()=>{const work=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-acquire-'));const acquired=await acquireExternalSkills({work,safeFs,signal:controller.signal});const dispose=acquired.dispose;return{...acquired,dispose:async()=>{await dispose();fs.rmdirSync(work);}};}});
|
|
48
|
-
progress.stop();
|
|
49
|
-
const reportResults=()=>{for(const item of result.results){output.write(`${item.platform}: ${item.status}; rollback: ${item.rollback}\n`);for(const recovery of item.residualPaths)output.write(`Residual path: ${recovery}\n`);if(item.error)error.write(`Error: ${item.error.message}\n`);}};
|
|
50
|
-
if(result.interrupted){reportResults();return 130;}
|
|
51
|
-
if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write(`ARC version ${JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version} installed! [${result.results.map(item=>item.platform).join('/')}]\n`);return 0;}
|
|
52
|
-
reportResults();return 1;
|
|
53
|
-
}catch(cause){progress?.stop();error.write(`Error: ${cause.message}\n`);if(cause.code==='ARC_CLI_INPUT'){error.write(usage+'\n');return 2;}if(cause.code==='ARC_CANCELLED')return 130;return ['ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE','ARC_TARGET_CONFIGURATION'].includes(cause.code)?2:1;
|
|
54
|
-
}finally{progress?.stop();if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
|
|
55
|
-
}
|
package/lib/cli/preset.mjs
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import {validatePlanSet} from '../installer/plan.mjs';
|
|
3
|
-
import {selectChoices} from './choices.mjs';
|
|
4
|
-
const usage='Usage: arc preset [economy|balanced|smart] --target <codex|claude|all> [--check | --dry-run] [--help]';
|
|
5
|
-
const inputError=message=>Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});
|
|
6
|
-
export function parseArgs(args){
|
|
7
|
-
const options={},seen=new Set();
|
|
8
|
-
for(let i=0;i<args.length;i++){
|
|
9
|
-
const arg=args[i];if(['economy','balanced','smart'].includes(arg)){if(options.preset)throw inputError('Duplicate preset');options.preset=arg;continue;}
|
|
10
|
-
if(!['--target','--check','--dry-run','--help'].includes(arg))throw inputError(`Unknown argument: ${arg}`);if(seen.has(arg))throw inputError(`Duplicate argument: ${arg}`);seen.add(arg);
|
|
11
|
-
if(arg==='--target'){const value=args[++i];if(!['codex','claude','all'].includes(value))throw inputError('Invalid target');options.target=value;}else options[arg==='--dry-run'?'dryRun':arg.slice(2)]=true;
|
|
12
|
-
}
|
|
13
|
-
if(options.check&&options.dryRun)throw inputError('--check and --dry-run cannot be combined');return options;
|
|
14
|
-
}
|
|
15
|
-
export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr}={}){
|
|
16
|
-
let onInterrupt;
|
|
17
|
-
try{
|
|
18
|
-
const options=parseArgs(args);if(options.help){output.write(usage+'\n');return 0;}
|
|
19
|
-
await selectChoices(options,{input,output});
|
|
20
|
-
const[{loadNative},{openRoot},{resolveTarget},{buildPresetPlan,switchPresetTargets}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs'),import('../installer/targets.mjs'),import('../installer/preset.mjs')]);
|
|
21
|
-
const native=loadNative(),safeFs={openRoot:p=>openRoot(p,{native})};
|
|
22
|
-
const plans=(options.target==='all'?['codex','claude']:[options.target]).map(platform=>buildPresetPlan({target:resolveTarget(platform,{userHome:os.homedir(),env:process.env}),preset:options.preset,safeFs}));
|
|
23
|
-
validatePlanSet(plans);
|
|
24
|
-
if(options.check||options.dryRun){for(const plan of plans){output.write(`${plan.target.platform}: ${plan.specs.length} file(s) would change\n`);if(options.dryRun)for(const spec of plan.specs)output.write(`Would write: ${spec.path}\n`);}output.write('Check completed. No changes made.\n');return 0;}
|
|
25
|
-
const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);const result=await switchPresetTargets(plans,{safeFs,signal:controller.signal});
|
|
26
|
-
for(const item of result.results){output.write(`${item.platform}: ${item.status}; rollback: ${item.rollback}\n`);for(const name of item.residualPaths)output.write(`Residual path: ${name}\n`);if(item.error)error.write(`Error: ${item.error.message}\n`);}
|
|
27
|
-
output.write('Open a new session to use the selected preset.\n');return result.interrupted?130:result.results.every(item=>['updated','unchanged'].includes(item.status)&&!item.error)?0:1;
|
|
28
|
-
}catch(cause){error.write(`Error: ${cause.message}\n`);if(cause.code==='ARC_CLI_INPUT'){error.write(usage+'\n');return 2;}return cause.code==='ARC_CANCELLED'?130:['ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE','ARC_TARGET_CONFIGURATION'].includes(cause.code)?2:1;}finally{if(onInterrupt)process.removeListener('SIGINT',onInterrupt);}
|
|
29
|
-
}
|
package/lib/cli/terminal.mjs
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import tty from 'node:tty';
|
|
3
|
-
function openControllingTerminal(){
|
|
4
|
-
let inputFd,outputFd,input,output;
|
|
5
|
-
try{
|
|
6
|
-
inputFd=fs.openSync('/dev/tty','r+');outputFd=fs.openSync('/dev/tty','r+');
|
|
7
|
-
input=new tty.ReadStream(inputFd);inputFd=undefined;
|
|
8
|
-
output=new tty.WriteStream(outputFd);outputFd=undefined;
|
|
9
|
-
return{io:{input,output,error:output},dispose(){input.destroy();output.destroy();}};
|
|
10
|
-
}catch(cause){input?.destroy();output?.destroy();if(inputFd!==undefined)fs.closeSync(inputFd);if(outputFd!==undefined)fs.closeSync(outputFd);throw cause;}
|
|
11
|
-
}
|
|
12
|
-
export function openInstallerTerminal({platform=process.platform,env=process.env,input=process.stdin,output=process.stdout,error=process.stderr,openTty=openControllingTerminal}={}){
|
|
13
|
-
if(!['darwin','linux'].includes(platform))return{kind:'skip',reason:'unsupported platform'};
|
|
14
|
-
if(['CI','CONTINUOUS_INTEGRATION','BUILD_NUMBER','RUN_ID'].some(name=>env[name]&&env[name]!=='false'&&env[name]!=='0'))return{kind:'skip',reason:'non-interactive environment'};
|
|
15
|
-
let owned,io;
|
|
16
|
-
try{if(input.isTTY&&output.isTTY)io={input,output,error};else{owned=openTty();io=owned.io;}if(!io.input.isTTY||!io.output.isTTY)throw Error('Terminal is not interactive');}catch{owned?.dispose();return{kind:'skip',reason:'no interactive terminal'};}
|
|
17
|
-
const wasRaw=Boolean(io.input.isRaw),wasFlowing=io.input.readableFlowing;let disposed=false;
|
|
18
|
-
return{kind:'interactive',io,dispose(){if(disposed)return;disposed=true;try{if(io.input.setRawMode&&!io.input.destroyed)io.input.setRawMode(wasRaw);if(wasFlowing!==true)io.input.pause();}finally{owned?.dispose();}}};
|
|
19
|
-
}
|