@yunhozz/arc 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.sha256 +36 -27
- package/README.md +18 -30
- package/claude/skill/references/handoff.md +11 -1
- package/claude/skill/references/planning.md +6 -1
- 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/skill/references/handoff.md +11 -1
- package/codex/skill/references/planning.md +6 -1
- 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/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 +41 -0
- package/lib/tui/render.mjs +21 -0
- package/lib/tui/setup.mjs +20 -0
- package/lib/tui/terminal.mjs +71 -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
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {applyTransaction} from './transaction.mjs';
|
|
3
2
|
import {inspectPath,TargetConfigurationError} from './targets.mjs';
|
|
4
3
|
const start='# >>> arc completion >>>',end='# <<< arc completion <<<';
|
|
5
|
-
|
|
6
|
-
export function renderShellCompletion(original){
|
|
4
|
+
export function removeShellCompletion(original){
|
|
7
5
|
let text;try{text=original===null?'':new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(original);}catch{throw new TargetConfigurationError('.zshrc must be valid UTF-8');}
|
|
8
6
|
const starts=[...text.matchAll(/# >>> arc completion >>>/g)],ends=[...text.matchAll(/# <<< arc completion <<</g)];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if(starts.length){
|
|
12
|
-
|
|
7
|
+
const malformed=()=>{throw new TargetConfigurationError('Malformed ARC completion markers');};
|
|
8
|
+
if(starts.length!==ends.length||starts.length>1)malformed();
|
|
9
|
+
if(!starts.length)return{changed:false,content:original};
|
|
10
|
+
const first=starts[0].index,last=ends[0].index;let stop=last+end.length;
|
|
11
|
+
if(first>=last||(first>0&&text[first-1]!=='\n')||(last>0&&text[last-1]!=='\n')||!['','\n','\r\n'].includes(text.slice(stop,stop+2).startsWith('\r')?text.slice(stop,stop+2):text.slice(stop,stop+1)))malformed();
|
|
12
|
+
if(text.slice(stop,stop+2)==='\r\n')stop+=2;else if(text[stop]==='\n')stop++;
|
|
13
|
+
return{changed:true,content:Buffer.from(text.slice(0,first)+text.slice(stop))};
|
|
13
14
|
}
|
|
14
|
-
export function
|
|
15
|
-
const plan=plans[0];if(!plan
|
|
15
|
+
export function attachShellCompletionCleanup(plans,{safeFs}){
|
|
16
|
+
const plan=plans[0];if(!plan)return;
|
|
16
17
|
const filename=path.join(plan.target.userHome,'.zshrc'),current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot:plan.snapshot}),original=current.bytes??null;
|
|
18
|
+
const removed=removeShellCompletion(original);
|
|
19
|
+
for(const item of plans){item.managedPaths=item.managedPaths.filter(name=>name!==filename);item.globalChanges=item.globalChanges.filter(change=>change.path!==filename);if(item!==plan)item.snapshot.delete(filename);}
|
|
20
|
+
if(!removed.changed)return;
|
|
17
21
|
const metadata=plan.snapshot.get(filename);if(metadata&&(metadata.mode&0o222n)===0n)throw new TargetConfigurationError('.zshrc is not writable');
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
plan.summary.push(`${filename}: zsh completion; opens automatically in a new zsh session. For this session: eval "$(arc completion zsh)"`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function registerShellCompletion(userHome,{safeFs,hooks={}}){
|
|
24
|
-
const plan={target:{userHome},globalChanges:[],managedPaths:[],externalPaths:[],snapshot:new Map(),summary:[]};
|
|
25
|
-
attachShellCompletion([plan],{safeFs});
|
|
26
|
-
if(!plan.globalChanges.length)return{status:'unchanged',residualPaths:[]};
|
|
27
|
-
const result=applyTransaction(plan,{safeFs,hooks,specs:plan.globalChanges.map(change=>({path:change.path,bytes:change.content}))});
|
|
28
|
-
return{...result,status:result.cleanupError?'failed':'registered'};
|
|
22
|
+
plan.managedPaths.push(filename);plan.globalChanges.push({path:filename,original,content:removed.content,action:'remove managed completion'});
|
|
23
|
+
plan.summary.push(`${filename}: remove managed ARC completion block`);
|
|
29
24
|
}
|
|
@@ -3,6 +3,7 @@ import {inspectPath,sameIdentity,TargetConfigurationError} from './targets.mjs';
|
|
|
3
3
|
import {SKILLS,snapshotSkill} from './superpowers.mjs';
|
|
4
4
|
import {validatePlanSet} from './plan.mjs';
|
|
5
5
|
import {removeManagedBlock} from './global-config.mjs';
|
|
6
|
+
import {attachShellCompletionCleanup} from './shell-completion.mjs';
|
|
6
7
|
export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
|
|
7
8
|
const snapshot=new Map(),removals=[],managedPaths=[path.join(target.home,'skills/arc'),...target.profileNames.map(name=>path.join(target.home,'agents',name))],globalChanges=[];
|
|
8
9
|
for(const filename of managedPaths){const directory=filename===managedPaths[0];const found=inspectPath(safeFs,filename,{kind:directory?'directory':'file',tree:directory,allowLinks:directory,snapshot});if(found.exists)removals.push(filename);}
|
|
@@ -14,11 +15,14 @@ export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
|
|
|
14
15
|
}
|
|
15
16
|
for(const skill of linkedSkills){const root=arc+'/references/superpowers/'+skill,directory=safeFs.openRoot(root);try{if(!sameIdentity(snapshot.get(root),directory.stat()))throw new TargetConfigurationError('Removal reference root changed');snapshotSkill(directory,root,[]);}finally{directory.close();}}
|
|
16
17
|
if(!skipGlobal){const filename=target.instructionPath??target.globalPaths[0],found=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});managedPaths.push(filename);if(found.exists){const removed=removeManagedBlock(found.bytes);if(removed.changed)globalChanges.push({path:filename,content:removed.content});}}
|
|
17
|
-
|
|
18
|
+
const plan={target,skipGlobal,snapshot,removals,managedPaths,externalPaths:[],globalChanges,summary:[target.platform==='codex'?'Codex config.toml wait settings and native concurrency key are preserved.':'Claude settings.json native concurrency key is preserved.']};
|
|
19
|
+
attachShellCompletionCleanup([plan],{safeFs});return plan;
|
|
18
20
|
}
|
|
19
21
|
import {assertPlanSnapshot,applyUninstallTransaction} from './transaction.mjs';
|
|
20
22
|
export async function uninstallTargets(plans,{safeFs,signal,hooks={}}){
|
|
21
23
|
const results=plans.map(plan=>({platform:plan.target.platform,status:'not-started',rollback:'not-needed',residualPaths:[],error:null}));let invalid=false;
|
|
24
|
+
// Each standalone preview retains its snapshot; only the first plan owns the shared shell write.
|
|
25
|
+
for(const plan of plans.slice(1)){const filename=path.join(plans[0].target.userHome,'.zshrc');plan.managedPaths=plan.managedPaths.filter(name=>name!==filename);plan.globalChanges=plan.globalChanges.filter(change=>change.path!==filename);plan.snapshot.delete(filename);}
|
|
22
26
|
try{validatePlanSet(plans);}catch(error){return{results:results.map(item=>({...item,status:'failed',error})),interrupted:false};}
|
|
23
27
|
for(let i=0;i<plans.length;i++)try{assertPlanSnapshot(plans[i],{safeFs});}catch(error){results[i]={...results[i],status:'failed',error};invalid=true;}
|
|
24
28
|
if(invalid)return{results,interrupted:false};
|
|
@@ -5,7 +5,7 @@ import {loadModelConfig} from '../models/index.mjs';
|
|
|
5
5
|
import {parseLanguage} from './language.mjs';
|
|
6
6
|
import {parseLimits} from './limits.mjs';
|
|
7
7
|
import {removeManagedBlock} from './global-config.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {removeShellCompletion} from './shell-completion.mjs';
|
|
9
9
|
import {readRuntimeConcurrency} from './runtime-config.mjs';
|
|
10
10
|
const fail=message=>{throw new TargetConfigurationError(message);};
|
|
11
11
|
export function collectInstalledState({safeFs,userHome,env={},cwd}){
|
|
@@ -21,7 +21,7 @@ export function collectInstalledState({safeFs,userHome,env={},cwd}){
|
|
|
21
21
|
mandatory('SKILL.md');if(profiles.some(item=>!item.exists||!item.bytes.length))fail(`Incomplete ARC installation: missing profile at ${target.home}`);
|
|
22
22
|
let language,modelConfig,limits;try{language=parseLanguage(mandatory('settings.toml')).language;modelConfig=loadModelConfig(mandatory('models.toml'),platform);limits=parseLimits(mandatory('limits.toml'));}catch(cause){throw new TargetConfigurationError(`Invalid installed ARC settings at ${target.home}: ${cause.message}`,{cause});}
|
|
23
23
|
const native=inspectPath(safeFs,target.runtimeConfigPath,{kind:'file',read:true,snapshot});if(native.exists)try{readRuntimeConcurrency(platform,native.bytes);}catch(cause){throw new TargetConfigurationError(`Invalid native configuration at ${target.runtimeConfigPath}: ${cause.message}`,{cause});}
|
|
24
|
-
const shell=inspectPath(safeFs,path.join(userHome,'.zshrc'),{kind:'file',read:true,snapshot});if(shell.exists)
|
|
24
|
+
const shell=inspectPath(safeFs,path.join(userHome,'.zshrc'),{kind:'file',read:true,snapshot});if(shell.exists)removeShellCompletion(shell.bytes);
|
|
25
25
|
targets.push({target,language,modelConfig,limits,snapshot});
|
|
26
26
|
}
|
|
27
27
|
const hash=createHash('sha256');for(const entry of targets){hash.update(entry.target.platform+'\0'+entry.target.home+'\0');for(const [name,value]of [...entry.snapshot].sort(([a],[b])=>a.localeCompare(b)))hash.update(JSON.stringify([name,value],(key,value)=>key==='atimeNs'?undefined:typeof value==='bigint'?value.toString():value));}
|
|
@@ -5,7 +5,7 @@ import {buildInstallPlan,validatePlanSet} from './plan.mjs';
|
|
|
5
5
|
import {assertPlanSnapshot} from './transaction.mjs';
|
|
6
6
|
import {inspectPath} from './targets.mjs';
|
|
7
7
|
import {renderManagedBlock,removeManagedBlock,agentsBlockBody} from './global-config.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {attachShellCompletionCleanup} from './shell-completion.mjs';
|
|
9
9
|
import {verifyPayload} from './manifest.mjs';
|
|
10
10
|
import {installTargets} from './install.mjs';
|
|
11
11
|
import {acquireExternalSkills} from './acquisition.mjs';
|
|
@@ -19,10 +19,7 @@ export function buildUpgradePlans({payload,state,safeFs}){
|
|
|
19
19
|
if(current.exists&&removeManagedBlock(current.bytes).changed){const rendered=renderManagedBlock(current.bytes,agentsBlockBody(target.home,target.userHome,language,target.platform));plan.managedPaths.push(target.instructionPath);if(!rendered.content.equals(current.bytes))plan.globalChanges.push({path:target.instructionPath,original:current.bytes,...rendered});}
|
|
20
20
|
return plan;
|
|
21
21
|
});
|
|
22
|
-
|
|
23
|
-
if(current.exists){const content=renderShellCompletion(current.bytes);if(current.bytes.includes(Buffer.from('# >>> arc completion >>>'))){first.managedPaths.push(filename);if(!content.equals(current.bytes))first.globalChanges.push({path:filename,original:current.bytes,content,action:'update completion'});}}
|
|
24
|
-
for(const plan of plans.slice(1))plan.snapshot.delete(filename);
|
|
25
|
-
}
|
|
22
|
+
attachShellCompletionCleanup(plans,{safeFs});
|
|
26
23
|
validatePlanSet(plans);return plans;
|
|
27
24
|
}
|
|
28
25
|
export async function upgradeInstalledTargets({packageRoot,state,safeFs,signal,acquisition}){
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
import {resolveTarget} from '../installer/targets.mjs';
|
|
6
|
+
import {buildInstallPlan,validatePlanSet} from '../installer/plan.mjs';
|
|
7
|
+
import {verifyPayload} from '../installer/manifest.mjs';
|
|
8
|
+
import {buildPresetPlan,switchPresetTargets} from '../installer/preset.mjs';
|
|
9
|
+
import {buildUninstallPlan,uninstallTargets} from '../installer/uninstall.mjs';
|
|
10
|
+
import {installTargets} from '../installer/install.mjs';
|
|
11
|
+
import {attachShellCompletionCleanup} from '../installer/shell-completion.mjs';
|
|
12
|
+
import {acquireExternalSkills} from '../installer/acquisition.mjs';
|
|
13
|
+
/** @typedef {{packageRoot:string,userHome:string,cwd:string,env:object,safeFs:object,signal?:AbortSignal,acquisition?:Function}} ManagementContext */
|
|
14
|
+
/** @typedef {{kind:string,plans:object[],options:object}} PreparedOperation */
|
|
15
|
+
/** @typedef {{code:number,exitSession:boolean,results:object[],interrupted:boolean}} OperationResult */
|
|
16
|
+
export async function createManagementContext(options={}){
|
|
17
|
+
let safeFs=options.safeFs;if(!safeFs){const[{loadNative},{openRoot}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs')]);const native=loadNative();safeFs={openRoot:p=>openRoot(p,{native})};}
|
|
18
|
+
return{packageRoot:fileURLToPath(new URL('../../',import.meta.url)),userHome:os.homedir(),cwd:process.cwd(),env:process.env,...options,safeFs};
|
|
19
|
+
}
|
|
20
|
+
export async function prepareOperation(context,options,{selections=[]}={}){
|
|
21
|
+
if(options.kind==='update'){const{prepareUpdate}=await import('./update.mjs');return prepareUpdate(context);}
|
|
22
|
+
if(!['install','preset','uninstall'].includes(options.kind))throw Object.assign(Error('Unknown management operation'),{code:'ARC_CLI_INPUT'});
|
|
23
|
+
const platforms=options.target==='all'?['codex','claude']:[options.target],payload=options.kind==='install'?verifyPayload(context):null;
|
|
24
|
+
const plans=platforms.map(platform=>{const target=resolveTarget(platform,context),selection=selections.find(item=>item.target.platform===platform&&item.target.home===target.home);if(options.kind==='preset')return buildPresetPlan({target,preset:options.preset,safeFs:context.safeFs});if(options.kind==='uninstall')return buildUninstallPlan({target,safeFs:context.safeFs,skipGlobal:options.skipGlobal});return buildInstallPlan({payload,target,limitsPreview:selection?.preview,languagePreview:selection?.languagePreview,preset:options.preset,language:options.language,skipGlobal:options.skipGlobal,safeFs:context.safeFs,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)});});
|
|
25
|
+
if(options.kind==='install')attachShellCompletionCleanup(plans,context);
|
|
26
|
+
// Uninstall plans initially share the completion snapshot; its executor assigns one owner.
|
|
27
|
+
if(options.kind!=='uninstall')validatePlanSet(plans);
|
|
28
|
+
return{kind:options.kind,plans,options:{...options}};
|
|
29
|
+
}
|
|
30
|
+
export function operationResult(result,successStatuses=['installed']){return{...result,code:result.interrupted?130:result.results.every(item=>successStatuses.includes(item.status)&&!item.error)?0:1,exitSession:false};}
|
|
31
|
+
export function acquisitionFor(context){return context.acquisition??(async()=>{const work=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-acquire-'));let acquired;try{acquired=await acquireExternalSkills({work,safeFs:context.safeFs,signal:context.signal});}catch(error){if(!error.residualPaths?.length)try{fs.rmdirSync(work);}catch(cleanup){throw Object.assign(new AggregateError([error,cleanup],'Cannot clean private acquisition work'),{code:'ARC_ACQUISITION_CLEANUP',residualPaths:[work]});}throw error;}return{...acquired,dispose:async()=>{await acquired.dispose();fs.rmdirSync(work);}};});}
|
|
32
|
+
export async function executeOperation(context,prepared){
|
|
33
|
+
if(prepared.kind==='update'){const{executeUpdate}=await import('./update.mjs');return executeUpdate(context,prepared);}
|
|
34
|
+
const options={safeFs:context.safeFs,signal:context.signal,hooks:context.hooks};
|
|
35
|
+
if(prepared.kind==='install')return operationResult(await installTargets(prepared.plans,{...options,acquisition:acquisitionFor(context)}));
|
|
36
|
+
if(prepared.kind==='preset')return operationResult(await switchPresetTargets(prepared.plans,options),['updated','unchanged']);
|
|
37
|
+
if(prepared.kind==='uninstall')return operationResult(await uninstallTargets(prepared.plans,options),['uninstalled']);
|
|
38
|
+
throw Object.assign(Error('Unknown prepared operation'),{code:'ARC_CLI_INPUT'});
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {resolveTarget,inspectPath} from '../installer/targets.mjs';
|
|
3
|
+
import {parseLanguage} from '../installer/language.mjs';
|
|
4
|
+
import {parseLimits} from '../installer/limits.mjs';
|
|
5
|
+
import {loadModelConfig} from '../models/index.mjs';
|
|
6
|
+
import {removeManagedBlock} from '../installer/global-config.mjs';
|
|
7
|
+
import {readRuntimeConcurrency} from '../installer/runtime-config.mjs';
|
|
8
|
+
import {removeShellCompletion} from '../installer/shell-completion.mjs';
|
|
9
|
+
/** @typedef {{platform:string,target?:object,state:'absent'|'installed'|'incomplete',settings?:object,diagnostics:Error[]}} TargetStatus */
|
|
10
|
+
export async function collectStatus(context){return ['codex','claude'].map(platform=>{
|
|
11
|
+
let target;try{target=resolveTarget(platform,context);const snapshot=new Map(),read=(name,kind='file')=>inspectPath(context.safeFs,name,{kind,read:true,snapshot,tree:kind==='directory',allowLinks:kind==='directory'}),arc=path.join(target.home,'skills/arc'),skill=read(arc,'directory'),profiles=target.profileNames.map(name=>read(path.join(target.home,'agents',name))),instruction=read(target.instructionPath),managed=instruction.exists&&removeManagedBlock(instruction.bytes).changed;
|
|
12
|
+
if(!skill.exists&&!profiles.some(x=>x.exists)&&!managed)return{platform,target,state:'absent',diagnostics:[]};
|
|
13
|
+
const mandatory=name=>{const found=read(path.join(arc,name));if(!found.exists||!found.bytes.length)throw Error(`Incomplete ARC installation: missing ${name} at ${target.home}`);return found.bytes;};
|
|
14
|
+
mandatory('SKILL.md');if(profiles.some(x=>!x.exists||!x.bytes.length))throw Error(`Incomplete ARC installation: missing profile at ${target.home}`);
|
|
15
|
+
const settings={language:parseLanguage(mandatory('settings.toml')).language,modelConfig:loadModelConfig(mandatory('models.toml'),platform),limits:parseLimits(mandatory('limits.toml'))};const native=read(target.runtimeConfigPath);if(native.exists)readRuntimeConcurrency(platform,native.bytes);const shell=read(path.join(context.userHome,'.zshrc'));if(shell.exists)removeShellCompletion(shell.bytes);
|
|
16
|
+
return{platform,target,state:'installed',settings,snapshot,diagnostics:[]};
|
|
17
|
+
}catch(error){return{platform,target,state:'incomplete',diagnostics:[error]};}
|
|
18
|
+
});}
|
|
@@ -0,0 +1,58 @@
|
|
|
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=path.resolve(fileURLToPath(new URL('../../',import.meta.url)));
|
|
6
|
+
const inputError=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
|
|
7
|
+
const configurationError=message=>Object.assign(Error(message),{code:'ARC_TARGET_CONFIGURATION'});
|
|
8
|
+
function identity(root){const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));if(pkg.name!=='@yunhozz/arc'||!/^\d+\.\d+\.\d+$/.test(pkg.version))throw configurationError('Invalid ARC package identity');return pkg;}
|
|
9
|
+
export async function runtime(deps){
|
|
10
|
+
const [context,state,upgrade,npm]=await Promise.all([import('../npm/context.mjs'),import('../installer/upgrade-state.mjs'),import('../installer/upgrade.mjs'),import('../npm/upgrade.mjs')]);
|
|
11
|
+
let safeFs=deps.safeFs;if(!safeFs){const[{loadNative},{openRoot}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs')]);const native=loadNative();safeFs={openRoot:root=>openRoot(root,{native})};}
|
|
12
|
+
return{...context,...state,...upgrade,...npm,...deps,safeFs};
|
|
13
|
+
}
|
|
14
|
+
export function report(result,output,error){
|
|
15
|
+
for(const item of result.results){output.write(item.platform+': '+item.status+'; rollback: '+item.rollback+'\n');for(const filename of item.residualPaths??[])output.write('Residual path: '+filename+'\n');if(item.error)error.write('Error: '+item.error.message+'\n');}
|
|
16
|
+
if(result.interrupted)return 130;
|
|
17
|
+
if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write('ARC version '+result.version+' harness upgraded! Previous harness version: not recorded.\n');return 0;}
|
|
18
|
+
error.write('Harness update did not complete. Run arc again and choose Update Harness, or run npx --yes @yunhozz/arc@latest and choose Update Harness.\n');return 1;
|
|
19
|
+
}
|
|
20
|
+
export function failure(cause,error){error.write('Error: '+cause.message+'\n');return ['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(cause.code)?130:['ARC_CLI_INPUT','ARC_TARGET_CONFIGURATION','ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE'].includes(cause.code)?2:1;}
|
|
21
|
+
export async function runChild(request,io={},deps={}){
|
|
22
|
+
const output=io.output??process.stdout,error=io.error??process.stderr;let onInterrupt;
|
|
23
|
+
try{
|
|
24
|
+
if(!request||Object.keys(request).sort().join(',')!=='expectedStateToken,expectedVersion,invocationCwd,protocol'||request.protocol!==1||typeof request.expectedVersion!=='string'||!/^\d+\.\d+\.\d+$/.test(request.expectedVersion)||typeof request.expectedStateToken!=='string'||!/^[a-f0-9]{64}$/.test(request.expectedStateToken)||typeof request.invocationCwd!=='string'||!path.isAbsolute(request.invocationCwd)||fs.realpathSync(request.invocationCwd)!==request.invocationCwd)throw inputError('Invalid private upgrade request');
|
|
25
|
+
const root=deps.packageRoot??packageRoot,pkg=identity(root);if(pkg.version!==request.expectedVersion)throw configurationError('The new ARC package version does not match the requested upgrade');
|
|
26
|
+
const api=await runtime(deps),env=io.env??process.env,cwd=request.invocationCwd,context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc-upgrade.mjs'),env,cwd});
|
|
27
|
+
if(context.kind!=='global'||context.packageRoot!==root)throw configurationError('Cannot verify the replaced global ARC package');
|
|
28
|
+
const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});if(state.stateToken!==request.expectedStateToken)throw configurationError('Installed ARC settings changed during CLI replacement; retry upgrade');
|
|
29
|
+
if(!state.targets.length)throw configurationError('No installed ARC harness found');
|
|
30
|
+
const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
|
|
31
|
+
return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
|
|
32
|
+
}catch(cause){return failure(cause,error);}finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function prepareUpdate(context){
|
|
36
|
+
const root=context.packageRoot??packageRoot,pkg=identity(root),api=await runtime(context),cwd=fs.realpathSync(context.cwd??process.cwd()),env=context.env??process.env;
|
|
37
|
+
const state=api.collectInstalledState({safeFs:api.safeFs,userHome:context.userHome??os.homedir(),env,cwd});
|
|
38
|
+
if(!state.targets.length)throw configurationError('No installed ARC harness found. Run npx --yes @yunhozz/arc to install it first');
|
|
39
|
+
let plans,version;
|
|
40
|
+
if(!context.upgradeInstalledTargets){const[{verifyPayload},{buildUpgradePlans}]=await Promise.all([import('../installer/manifest.mjs'),import('../installer/upgrade.mjs')]);const payload=verifyPayload({packageRoot:root,safeFs:api.safeFs});plans=buildUpgradePlans({payload,state,safeFs:api.safeFs});version=JSON.parse(payload.files.get('package.json')).version;}
|
|
41
|
+
return{kind:'update',root,pkg,cwd,env,state,plans,version};
|
|
42
|
+
}
|
|
43
|
+
export async function executeUpdate(context,prepared){
|
|
44
|
+
let packageStarted=false;const messages=[];try{
|
|
45
|
+
const api=await runtime(context),{root,pkg,cwd,env,state}=prepared;
|
|
46
|
+
const{assertPlanSnapshot}=await import('../installer/transaction.mjs');for(const entry of state.targets)assertPlanSnapshot(entry,{safeFs:api.safeFs});
|
|
47
|
+
const npmContext=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
|
|
48
|
+
if(!['global','npx'].includes(npmContext.kind))throw configurationError('Run arc from a verified global ARC installation or npx --yes @yunhozz/arc@latest, then choose Update Harness');
|
|
49
|
+
if(npmContext.kind==='npx'){
|
|
50
|
+
let result;if(prepared.plans){const[{installTargets},{acquisitionFor,operationResult}]=await Promise.all([import('../installer/install.mjs'),import('./operations.mjs')]);result=operationResult({version:prepared.version,...await installTargets(prepared.plans,{safeFs:api.safeFs,signal:context.signal,acquisition:acquisitionFor({...context,safeFs:api.safeFs})})});}else{const{operationResult}=await import('./operations.mjs');result=operationResult(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:context.signal}));}return result;
|
|
51
|
+
}
|
|
52
|
+
const latest=await api.resolveLatest({context:npmContext,env,cwd,signal:context.signal});packageStarted=true;
|
|
53
|
+
const replaced=await api.upgradeGlobal({context:npmContext,expectedVersion:latest.version,env,cwd,signal:context.signal});if(replaced.packageStatus!=='updated')throw replaced.error??Error('CLI replacement failed');
|
|
54
|
+
messages.push('CLI: updated to '+latest.version+' (previous '+pkg.version+').\n');
|
|
55
|
+
const child=await api.runUpgradeChild({entryPath:path.join(npmContext.packageRoot,'bin/arc-upgrade.mjs'),request:{protocol:1,expectedVersion:latest.version,invocationCwd:cwd,expectedStateToken:state.stateToken},env,cwd,signal:context.signal});
|
|
56
|
+
return{...child,code:child.code===0&&!child.signal?0:child.code===130?130:1,exitSession:true,results:[],messages};
|
|
57
|
+
}catch(error){return{code:['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(error.code)?130:['ARC_CLI_INPUT','ARC_TARGET_CONFIGURATION','ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE'].includes(error.code)?2:1,exitSession:packageStarted,results:[],error,messages};}
|
|
58
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
import {absolute,inspectPath,observeCooperativeTarget,workflowStatePath,validateWorkflowId} from '../runtime/scope.mjs';
|
|
5
|
+
import {validateCooperativeCommand} from '../runtime/cooperative-schema.mjs';
|
|
6
|
+
import {runWorkflowCommand} from '../runtime/command.mjs';
|
|
7
|
+
const invalid=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
|
|
8
|
+
export async function observeWorkspace(cwd){
|
|
9
|
+
await inspectPath(cwd,{directory:true});
|
|
10
|
+
const env=Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.toUpperCase().startsWith('GIT_')));
|
|
11
|
+
Object.assign(env,{GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_OPTIONAL_LOCKS:'0',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
|
|
12
|
+
const result=spawnSync('git',['--no-pager','-c','core.fsmonitor=false','rev-parse','--show-toplevel'],{cwd,env,encoding:'utf8',timeout:10000,maxBuffer:1024*1024});
|
|
13
|
+
if(result.status!==0||result.error||!result.stdout.endsWith('\n')||result.stdout.slice(0,-1).includes('\n'))throw invalid('Cannot observe repository root.');
|
|
14
|
+
return observeCooperativeTarget(result.stdout.slice(0,-1));
|
|
15
|
+
}
|
|
16
|
+
export async function readWorkflowJson(filename){
|
|
17
|
+
if(!absolute(filename))throw invalid('An absolute JSON file path is required.');
|
|
18
|
+
await inspectPath(filename);let input;try{input=JSON.parse(await fs.readFile(filename,'utf8'));}catch{throw invalid('Cannot parse workflow JSON file.');}
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
export async function listWorkflows(cwd){
|
|
22
|
+
const target=await observeWorkspace(cwd),directory=path.join(target.gitCommonDir,'arc','workflows');
|
|
23
|
+
try{await inspectPath(directory,{directory:true});}catch(error){if(error.code==='ENOENT')return[];throw error;}
|
|
24
|
+
const entries=await fs.readdir(directory,{withFileTypes:true}),items=[];
|
|
25
|
+
for(const entry of entries){if(!entry.isDirectory())continue;try{validateWorkflowId(entry.name);}catch{continue;}
|
|
26
|
+
const filename=await workflowStatePath(target,entry.name);try{const state=JSON.parse(await fs.readFile(filename,'utf8'));items.push({id:entry.name,revision:state.revision,target:state.target,foreign:!sameTarget(target,state.target)});}catch(error){if(error.code==='ENOENT')continue;items.push({id:entry.name,invalid:true});}
|
|
27
|
+
}
|
|
28
|
+
return items.sort((a,b)=>a.id.localeCompare(b.id));
|
|
29
|
+
}
|
|
30
|
+
function sameTarget(a,b){return b&&['realpath','gitCommonDir','gitDir','branch','head'].every(key=>a[key]===b[key]);}
|
|
31
|
+
export async function prepareWorkflow({cwd,workflowId,command,input,jsonPath}){
|
|
32
|
+
validateWorkflowId(workflowId);const target=await observeWorkspace(cwd);cwd=target.realpath;const filename=await workflowStatePath(target,workflowId);
|
|
33
|
+
if(jsonPath!==undefined)input=await readWorkflowJson(jsonPath);
|
|
34
|
+
if(command!=='status'){try{validateCooperativeCommand({command,input});}catch{throw invalid('Invalid workflow input. Check the command schema; payload is not displayed.');}}
|
|
35
|
+
else if(input!==undefined)throw invalid('Status takes no input.');
|
|
36
|
+
if(!['init','status','record','prepare','result','reconcile'].includes(command))throw invalid('Invalid workflow command.');
|
|
37
|
+
if(!['status','init'].includes(command)){const state=JSON.parse(await fs.readFile(filename,'utf8'));if(!sameTarget(target,state.target))throw invalid('Workflow target differs from this checkout or HEAD. Use the matching target before mutation.');}
|
|
38
|
+
return{cwd,workflowId,command,input:input===undefined?undefined:structuredClone(input),target};
|
|
39
|
+
}
|
|
40
|
+
// Only expose structural status fields. Evidence payloads, operator statements,
|
|
41
|
+
// request arguments and runtime error text may contain secrets and stay private.
|
|
42
|
+
export function workflowView(body,code){
|
|
43
|
+
const state=body.state;
|
|
44
|
+
return{code,decision:body.decision,workflowId:body.workflowId,revision:body.revision,persisted:body.persisted,ruleIds:body.ruleIds??[],requiredEvidence:(body.requiredEvidence??[]).map(value=>typeof value==='string'?value:'Additional runtime evidence required'),nextActions:(body.nextActions??[]).map(value=>typeof value==='string'?value:'Consult installed runtime instructions'),...(body.error?{error:'Workflow command rejected. Check runtime inputs and target.'}:{}),...(state?{target:state.target,tasks:(state.tasks??[]).map(t=>({id:t.id,status:t.status})),unresolved:(state.unresolved??[]).map(()=> 'Unresolved runtime condition')}: {})};
|
|
45
|
+
}
|
|
46
|
+
export async function executeWorkflow(prepared){
|
|
47
|
+
const current=await observeWorkspace(prepared.cwd);if(!sameTarget(current,prepared.target))return{code:3,decision:'hold',workflowId:prepared.workflowId,ruleIds:['cooperative.target'],error:'Workspace target changed. Review again.',nextActions:[]};
|
|
48
|
+
let text='';const args=[prepared.command,'--cwd',prepared.cwd,'--workflow',prepared.workflowId];if(prepared.command!=='status')args.push('--input','-');
|
|
49
|
+
const code=await runWorkflowCommand(args,{input:JSON.stringify(prepared.input),output:{write(chunk){text+=chunk;}}});
|
|
50
|
+
return workflowView(JSON.parse(text),code);
|
|
51
|
+
}
|
package/lib/release/artifact.mjs
CHANGED
|
@@ -99,7 +99,7 @@ function validateTarball(bytes,{expectedName,expectedVersion}){
|
|
|
99
99
|
const files=archive(bytes),pkgFile=files.get('package.json');if(!pkgFile)throw new Error('Artifact package metadata is missing');
|
|
100
100
|
const pkg=JSON.parse(pkgFile.bytes);metadata(pkg,undefined,{artifact:true});
|
|
101
101
|
if(pkg.name!==expectedName||pkg.version!==expectedVersion)throw new Error('Artifact package identity mismatch');
|
|
102
|
-
for(const name of ['scripts/postinstall.mjs','lib/cli/postinstall.mjs','lib/
|
|
102
|
+
for(const name of ['scripts/postinstall.mjs','lib/cli/postinstall.mjs','lib/tui/app.mjs','lib/tui/terminal.mjs','lib/cli/root.mjs','lib/cli/upgrade.mjs','bin/arc-upgrade.mjs'])if(!files.has(name))throw new Error('Artifact runtime asset missing: '+name);
|
|
103
103
|
if(files.has('package-lock.json')||[...files.keys()].some(name=>name.split('/').some(part=>part==='.git'||part.startsWith('.arc-release'))))throw new Error('Private staging data in artifact');
|
|
104
104
|
const root=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-release-verify-'));
|
|
105
105
|
try{
|
|
@@ -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,41 @@
|
|
|
1
|
+
import {createTerminalSession} from './terminal.mjs';
|
|
2
|
+
import {escapeTerminalText} from './render.mjs';
|
|
3
|
+
import {createManagementContext,prepareOperation,executeOperation} from '../management/operations.mjs';
|
|
4
|
+
import {collectStatus} from '../management/status.mjs';
|
|
5
|
+
import {runSetup,summary} from './setup.mjs';
|
|
6
|
+
import {runWorkflow,workflowRows} from './workflow.mjs';
|
|
7
|
+
const home=['Install / Reconfigure','Change Preset','Workflow','Update Harness','Uninstall Harness','Exit'];
|
|
8
|
+
function interrupted(){return Object.assign(Error('Session interrupted'),{code:'ARC_CANCELLED'});}
|
|
9
|
+
function operationRows(prepared){
|
|
10
|
+
if(prepared.kind==='workflow')return['Command: '+prepared.command,'Workspace: '+prepared.cwd,'Workflow: '+prepared.workflowId,'Confirmation does not supply runtime approval evidence.'];
|
|
11
|
+
const rows=['Operation: '+prepared.kind];if(prepared.options)rows.push('Target: '+prepared.options.target,...(prepared.kind==='install'?['Settings: '+summary(prepared.options),'External references must be downloaded after applying.']:[]));
|
|
12
|
+
for(const plan of prepared.plans??[]){rows.push('Target: '+(plan.target?.platform??plan.platform??'harness'),'Destination: '+(plan.target?.home??plan.home??''));for(const key of ['managedPaths','writes','deletes','removals','preserved','preserve']){const values=plan[key];if(Array.isArray(values))for(const item of values)rows.push(key+': '+(typeof item==='string'?item:item.path??item.destination??item.relativePath??'managed file'));}for(const change of plan.globalChanges??[])rows.push('Global: '+change.path+' — '+(change.action??'managed ARC update'));for(const note of plan.summary??[])rows.push(note);}
|
|
13
|
+
if(prepared.kind==='uninstall')rows.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.');
|
|
14
|
+
if(prepared.kind==='update')rows.push('Installed settings are preserved.','Global CLI replacement ends this session.');
|
|
15
|
+
return rows;
|
|
16
|
+
}
|
|
17
|
+
export function createTuiInterface(session,{input,controller}={}){
|
|
18
|
+
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;}
|
|
19
|
+
async function menu(title,rows,{selected=0,info=[],requiresConfirmation=false}={}){
|
|
20
|
+
while(true){const offset=Math.max(0,selected-7),visible=rows.slice(offset,offset+8);const screen={title,rows:[...info,...visible],selected:info.length+selected-offset,requiresConfirmation,hint:'Up/Down: select | Enter: continue | Esc: back'+(rows.length>8?' | '+(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'&&(!requiresConfirmation||rendered.applyEnabled))return selected;}
|
|
21
|
+
}
|
|
22
|
+
async function text(title,initial=''){let value=initial;while(true){session.render({title,rows:[value],hint:'Type or paste | Left: delete | Enter: accept | Esc/Backspace: back'});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;}}
|
|
23
|
+
async function page(title,rows,{requireRead=false}={}){let offset=0;while(true){const size=8,slice=rows.slice(offset,offset+size);const rendered=session.render({title,rows:slice,requiresConfirmation:requireRead,hint:`Rows ${offset+1}-${Math.min(offset+size,rows.length)} of ${rows.length} | Up/Down: scroll | Enter/Esc: back`});const e=await event();if(e.name==='down')offset=Math.min(Math.max(0,rows.length-size),offset+1);else if(e.name==='up')offset=Math.max(0,offset-1);else if(e.name==='back')return false;else if(e.name==='enter'&&(!requireRead||rendered.applyEnabled)){if(requireRead&&offset+size<rows.length){offset=Math.min(rows.length-size,offset+size);}else return true;}}}
|
|
24
|
+
async function review(title,prepared){const rows=operationRows(prepared);while(true){const choice=await menu('Review '+title,['Cancel','Edit','Apply','View change details','Validate (read only)'],{info:rows.slice(0,8),requiresConfirmation:true});if(choice===null||choice===0)return'cancel';if(choice===1)return'edit';if(choice===2){if(rows.length>8){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';}if(choice===3)await page('Change details',rows);if(choice===4)await page('Validation',['Prepared plans validated without changes or downloads.','Execution rechecks the reviewed snapshot.','External downloads are checked only after applying.']);}}
|
|
25
|
+
async function busy(action){session.render({title:'Working',rows:['Operation in progress. Please wait.'],hint:'Ctrl+C: interrupt'});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();}}
|
|
26
|
+
return{menu,text,page,review,busy};
|
|
27
|
+
}
|
|
28
|
+
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.']:[])];}
|
|
29
|
+
export async function runTui(options={}){
|
|
30
|
+
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);
|
|
31
|
+
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;
|
|
32
|
+
while(true){const statuses=await api.collectStatus(context);const info=statuses.map(s=>s.platform+': '+s.state+(s.diagnostics?.length?' — '+s.diagnostics.map(e=>e.message).join('; '):''));const choice=await ui.menu('ARC Management',home,{selected,info});if(choice===null)continue;selected=choice;if(choice===5)return code;
|
|
33
|
+
try{if(choice===2){await runWorkflow({ui,cwd:context.cwd??process.cwd(),services:options.services,onResult:result=>{code=result.code;if(code===130){finalReport=workflowRows(result);throw interrupted();}}});continue;}
|
|
34
|
+
let prepared;if(choice===0||choice===1)prepared=await runSetup({ui,statuses,draft,kind:choice===0?'install':'preset',prepare:settings=>api.prepareOperation(context,settings)});
|
|
35
|
+
else if(choice===3){prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'update'}));const decision=await ui.review('Update Harness',prepared);if(decision!=='apply')prepared=null;}
|
|
36
|
+
else{const target=await ui.menu('Uninstall target',['Codex','Claude','Both targets']);if(target===null)continue;const global=await ui.menu('Global ARC instructions',['Remove ARC blocks','Preserve ARC blocks']);if(global===null)continue;prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude','all'][target],skipGlobal:global===1}));const decision=await ui.review('Uninstall Harness',prepared);if(decision!=='apply')prepared=null;}
|
|
37
|
+
if(!prepared)continue;const result=await ui.busy(()=>api.executeOperation(context,prepared));code=result.code;if(code===130){finalReport=resultRows(result);return 130;}if(result.exitSession){finalReport=resultRows(result);session.render({title:'Result',rows:finalReport,hint:'Session ended'});return code;}await ui.page('Result',resultRows(result));
|
|
38
|
+
}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.']);}
|
|
39
|
+
}
|
|
40
|
+
}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');}
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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: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 renderScreen(screen={}, {columns=80,rows=24}={}){
|
|
10
|
+
// Leave the last column unused to avoid terminal autowrap at the right edge.
|
|
11
|
+
columns=Math.max(1,Math.floor(Number(columns)||80)-1);rows=Math.max(1,Math.floor(Number(rows)||24));
|
|
12
|
+
const entries=screen.rows??[],hint=screen.hint??'Up/Down: select | Enter: continue | Esc: back';
|
|
13
|
+
const lines=[escapeTerminalText(screen.title),...entries.map((row,index)=>(index===screen.selected?'> ':' ')+escapeTerminalText(row)),escapeTerminalText(hint)];
|
|
14
|
+
const fitted=lines.map(line=>fit(line,columns));
|
|
15
|
+
const required=screen.requiredRows??entries.map((_,index)=>index);
|
|
16
|
+
const requiredVisible=required.every(index=>Number.isInteger(index)&&index>=0&&index<entries.length&&index+1<rows-1&&fitted[index+1].complete);
|
|
17
|
+
const applyEnabled=!screen.requiresConfirmation||(requiredVisible&&fitted[0].complete&&lines.length<=rows&&fitted.at(-1).complete);
|
|
18
|
+
const visible=fitted.slice(0,rows).map(line=>line.text);
|
|
19
|
+
if(!applyEnabled)visible[visible.length-1]=fit('Resize terminal to review before applying.',columns).text;
|
|
20
|
+
return{text:visible.join('\n'),applyEnabled};
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Reconfigure',['Codex','Claude','Both targets']);if(targetIndex===null)return null;const target=['codex','claude','all'][targetIndex];
|
|
7
|
+
if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude)){
|
|
8
|
+
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]};
|
|
9
|
+
}
|
|
10
|
+
const settings=draft[target==='all'?'codex':target];let editing=true;
|
|
11
|
+
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);if(field===null)break;
|
|
12
|
+
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];}
|
|
13
|
+
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];}
|
|
14
|
+
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.']);}}
|
|
15
|
+
else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
|
|
16
|
+
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;}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function summary(s){return`preset=${s.preset}, language=${s.language}, total=${s.maxTotalSubagents}, concurrent=${s.maxConcurrentSubagents}, global=${!s.skipGlobal}`;}
|
|
@@ -0,0 +1,71 @@
|
|
|
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}={}){
|
|
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
|
+
let disposed=false,busy=false,parsing=false,pending='',paste=null,sequence=0,escapeTimer,waiter,queue=[],screen,lastLines=0,applyEnabled=true;
|
|
17
|
+
let decoder=new StringDecoder('utf8'),lastWasCR=false;
|
|
18
|
+
const clearTimer=()=>{clearTimeout(escapeTimer);escapeTimer=undefined;};
|
|
19
|
+
function deliver(name,detail={}){
|
|
20
|
+
const event={name,sequence,...detail};if(waiter&&!parsing){const resolve=waiter;waiter=undefined;resolve(event);}else queue.push(event);
|
|
21
|
+
}
|
|
22
|
+
function discardInput(){clearTimer();pending='';paste=null;decoder=new StringDecoder('utf8');queue=[];}
|
|
23
|
+
function dispose(){
|
|
24
|
+
if(disposed)return;disposed=true;clearTimer();pending='';paste=null;queue=[];
|
|
25
|
+
input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);
|
|
26
|
+
output.removeListener('resize',onResize);output.removeListener('error',onError);signals.removeListener('SIGINT',onSigint);signals.removeListener('SIGTERM',onSigterm);
|
|
27
|
+
try{if(input.setRawMode&&!input.destroyed)input.setRawMode(wasRaw);}finally{
|
|
28
|
+
try{if(wasFlowing!==true)input.pause();}finally{
|
|
29
|
+
try{output.write('\x1b[?2004l\x1b[?25h\n');}finally{if(waiter){const resolve=waiter;waiter=undefined;resolve({name:'eof',sequence});}}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function finish(name,detail){const resolve=waiter;waiter=undefined;dispose();if(resolve)resolve({name,sequence,...detail});else deliver(name,detail);}
|
|
34
|
+
function onEnd(){if(!disposed)finish('eof');}
|
|
35
|
+
function onError(error){if(!disposed)finish('error',{error});}
|
|
36
|
+
function onSigint(){if(!disposed)finish('interrupt',{signal:'SIGINT'});}
|
|
37
|
+
function onSigterm(){if(!disposed)finish('interrupt',{signal:'SIGTERM'});}
|
|
38
|
+
function draw(){
|
|
39
|
+
const result=renderScreen(screen,{columns:output.columns,rows:output.rows});applyEnabled=result.applyEnabled;
|
|
40
|
+
if(lastLines)output.write(`\x1b[${lastLines}A\r`);
|
|
41
|
+
const lines=result.text.split('\n');
|
|
42
|
+
for(let index=0;index<Math.max(lastLines,lines.length);index++)output.write('\x1b[2K'+(lines[index]??'')+'\n');
|
|
43
|
+
if(lastLines>lines.length)output.write(`\x1b[${lastLines-lines.length}A\r`);
|
|
44
|
+
lastLines=lines.length;return result;
|
|
45
|
+
}
|
|
46
|
+
function render(value){if(disposed)return{applyEnabled:false,text:''};discardInput();screen=value;try{return draw();}catch(error){dispose();throw error;}}
|
|
47
|
+
function onResize(){if(disposed)return;discardInput();try{if(screen)draw();deliver('resize',{columns:output.columns,rows:output.rows});}catch(error){onError(error);}}
|
|
48
|
+
function parse(){
|
|
49
|
+
while(pending&&!disposed){
|
|
50
|
+
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;}
|
|
51
|
+
if(pending.startsWith('\x1b[200~')){pending=pending.slice(6);paste='';continue;}
|
|
52
|
+
const arrow=/^\x1b(?:\[|O)([ABCD])/.exec(pending);
|
|
53
|
+
if(arrow){pending=pending.slice(arrow[0].length);deliver({A:'up',B:'down',C:'right',D:'left'}[arrow[1]]);continue;}
|
|
54
|
+
if(pending[0]==='\x1b'){
|
|
55
|
+
if(pending==='\x1b'||['\x1b[','\x1bO','\x1b[2','\x1b[20','\x1b[200'].includes(pending)){escapeTimer=setTimeout(()=>{escapeTimer=undefined;pending='';deliver('back');},35);return;}
|
|
56
|
+
// Unknown escape sequences are consumed as text, never executed as keys.
|
|
57
|
+
const unknown=/^\x1b\[[0-9;?]*[ -/]*[@-~]/.exec(pending);if(unknown){pending=pending.slice(unknown[0].length);continue;}
|
|
58
|
+
pending=pending.slice(1);deliver('back');continue;
|
|
59
|
+
}
|
|
60
|
+
const character=[...pending][0];pending=pending.slice(character.length);const skipLF=lastWasCR&&character==='\n';lastWasCR=character==='\r';if(skipLF)continue;
|
|
61
|
+
if(character==='\x03'){onSigint();return;}if(character==='\x04'){onEnd();return;}
|
|
62
|
+
if(character==='\r'||character==='\n')deliver(applyEnabled?'enter':'blocked');
|
|
63
|
+
else if(character==='\x7f'||character==='\b')deliver('back');
|
|
64
|
+
else if(!/[\p{Cc}]/u.test(character))deliver('text',{text:character});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
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());}}}
|
|
68
|
+
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);
|
|
69
|
+
try{input.setRawMode?.(true);input.resume();output.write('\x1b[?25l\x1b[?2004h');}catch(error){dispose();throw error;}
|
|
70
|
+
return{render,discardInput,dispose,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;});}};
|
|
71
|
+
}
|
|
@@ -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({});
|