@yunhozz/arc 0.2.1 → 0.2.3

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.
Files changed (81) hide show
  1. package/MANIFEST.sha256 +77 -51
  2. package/README.md +4 -2
  3. package/claude/agents/arc-implementer.md +8 -8
  4. package/claude/agents/arc-planner.md +10 -12
  5. package/claude/agents/arc-qa.md +7 -7
  6. package/claude/agents/arc-reviewer.md +6 -6
  7. package/claude/agents/arc-scouter.md +6 -6
  8. package/claude/skill/SKILL.md +16 -17
  9. package/claude/skill/references/arc-visual-companion.md +33 -0
  10. package/claude/skill/references/commit.md +1 -1
  11. package/claude/skill/references/coordination.md +23 -39
  12. package/claude/skill/references/handoff-prep.md +3 -3
  13. package/claude/skill/references/handoff.md +10 -9
  14. package/claude/skill/references/limits.md +3 -4
  15. package/claude/skill/references/planning.md +53 -51
  16. package/claude/skill/references/verification.md +8 -8
  17. package/claude/skill/references/visual-companion.md +293 -15
  18. package/codex/agents/arc-implementer.toml +7 -7
  19. package/codex/agents/arc-planner.toml +11 -13
  20. package/codex/agents/arc-qa.toml +7 -7
  21. package/codex/agents/arc-reviewer.toml +6 -6
  22. package/codex/agents/arc-scouter.toml +6 -6
  23. package/codex/skill/SKILL.md +16 -20
  24. package/codex/skill/references/arc-visual-companion.md +39 -0
  25. package/codex/skill/references/commit.md +4 -6
  26. package/codex/skill/references/coordination.md +25 -44
  27. package/codex/skill/references/git-recovery.md +2 -3
  28. package/codex/skill/references/handoff-prep.md +4 -4
  29. package/codex/skill/references/handoff-worktree.md +5 -9
  30. package/codex/skill/references/handoff.md +15 -18
  31. package/codex/skill/references/limits.md +3 -4
  32. package/codex/skill/references/planning.md +56 -55
  33. package/codex/skill/references/verification.md +13 -15
  34. package/codex/skill/references/visual-companion.md +290 -18
  35. package/lib/installer/install.mjs +5 -7
  36. package/lib/installer/manifest.mjs +23 -8
  37. package/lib/installer/package-references.mjs +10 -5
  38. package/lib/installer/plan.mjs +1 -1
  39. package/lib/installer/superpowers.mjs +26 -1
  40. package/lib/installer/transaction.mjs +3 -3
  41. package/lib/installer/upgrade.mjs +2 -9
  42. package/lib/management/operations.mjs +17 -8
  43. package/lib/management/update.mjs +19 -10
  44. package/lib/npm/context.mjs +22 -0
  45. package/lib/npm/uninstall.mjs +17 -0
  46. package/lib/npm/upgrade.mjs +25 -0
  47. package/lib/policy/role-policy.json +9 -9
  48. package/lib/release/artifact.mjs +6 -4
  49. package/lib/tui/app.mjs +32 -63
  50. package/lib/tui/render.mjs +10 -12
  51. package/lib/tui/setup.mjs +11 -9
  52. package/lib/tui/terminal.mjs +8 -8
  53. package/package.json +12 -2
  54. package/scripts/update-manifest.mjs +7 -9
  55. package/claude/skill/references/superpowers/brainstorming/visual-companion.md +0 -299
  56. package/codex/skill/references/superpowers/brainstorming/visual-companion.md +0 -299
  57. package/lib/tui/workflow.mjs +0 -12
  58. /package/claude/skill/references/{superpowers/brainstorming/SKILL.md → brainstorming.md} +0 -0
  59. /package/claude/skill/references/{superpowers/brainstorming/builder-check-prompt.md → builder-check-prompt.md} +0 -0
  60. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/frame-template.html +0 -0
  61. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/helper.js +0 -0
  62. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/server.cjs +0 -0
  63. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/start-server.sh +0 -0
  64. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/stop-server.sh +0 -0
  65. /package/claude/skill/references/{superpowers/LICENSE → superpowers-LICENSE} +0 -0
  66. /package/claude/skill/references/{superpowers/provenance.json → superpowers-provenance.json} +0 -0
  67. /package/claude/skill/references/{superpowers/test-driven-development/SKILL.md → test-driven-development.md} +0 -0
  68. /package/claude/skill/references/{superpowers/test-driven-development/writing-good-tests.md → writing-good-tests.md} +0 -0
  69. /package/claude/skill/references/{superpowers/writing-plans/SKILL.md → writing-plans.md} +0 -0
  70. /package/codex/skill/references/{superpowers/brainstorming/SKILL.md → brainstorming.md} +0 -0
  71. /package/codex/skill/references/{superpowers/brainstorming/builder-check-prompt.md → builder-check-prompt.md} +0 -0
  72. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/frame-template.html +0 -0
  73. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/helper.js +0 -0
  74. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/server.cjs +0 -0
  75. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/start-server.sh +0 -0
  76. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/stop-server.sh +0 -0
  77. /package/codex/skill/references/{superpowers/LICENSE → superpowers-LICENSE} +0 -0
  78. /package/codex/skill/references/{superpowers/provenance.json → superpowers-provenance.json} +0 -0
  79. /package/codex/skill/references/{superpowers/test-driven-development/SKILL.md → test-driven-development.md} +0 -0
  80. /package/codex/skill/references/{superpowers/test-driven-development/writing-good-tests.md → writing-good-tests.md} +0 -0
  81. /package/codex/skill/references/{superpowers/writing-plans/SKILL.md → writing-plans.md} +0 -0
@@ -1,6 +1,4 @@
1
- import fs from 'node:fs';
2
1
  import os from 'node:os';
3
- import path from 'node:path';
4
2
  import {fileURLToPath} from 'node:url';
5
3
  import {resolveTarget} from '../installer/targets.mjs';
6
4
  import {buildInstallPlan,validatePlanSet} from '../installer/plan.mjs';
@@ -9,8 +7,9 @@ import {buildPresetPlan,switchPresetTargets} from '../installer/preset.mjs';
9
7
  import {buildUninstallPlan,uninstallTargets} from '../installer/uninstall.mjs';
10
8
  import {installTargets} from '../installer/install.mjs';
11
9
  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 */
10
+ import {inspectGlobalArc} from '../npm/context.mjs';
11
+ import {uninstallGlobal,sameGlobalArc} from '../npm/uninstall.mjs';
12
+ /** @typedef {{packageRoot:string,userHome:string,cwd:string,env:object,safeFs:object,signal?:AbortSignal}} ManagementContext */
14
13
  /** @typedef {{kind:string,plans:object[],options:object}} PreparedOperation */
15
14
  /** @typedef {{code:number,exitSession:boolean,results:object[],interrupted:boolean}} OperationResult */
16
15
  export async function createManagementContext(options={}){
@@ -25,15 +24,25 @@ export async function prepareOperation(context,options,{selections=[]}={}){
25
24
  if(options.kind==='install')attachShellCompletionCleanup(plans,context);
26
25
  // Uninstall plans initially share the completion snapshot; its executor assigns one owner.
27
26
  if(options.kind!=='uninstall')validatePlanSet(plans);
28
- return{kind:options.kind,plans,options:{...options}};
27
+ const globalArc=options.kind==='uninstall'?await(context.inspectGlobalArc??inspectGlobalArc)({env:context.env,cwd:context.cwd,runNpm:context.runNpm}):undefined;
28
+ return{kind:options.kind,plans,options:{...options},...(globalArc?{globalArc}:{})};
29
29
  }
30
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
31
  export async function executeOperation(context,prepared){
33
32
  if(prepared.kind==='update'){const{executeUpdate}=await import('./update.mjs');return executeUpdate(context,prepared);}
34
33
  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)}));
34
+ if(prepared.kind==='install')return operationResult(await installTargets(prepared.plans,options));
36
35
  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']);
36
+ if(prepared.kind==='uninstall'){
37
+ let result;
38
+ try{
39
+ const current=await(context.inspectGlobalArc??inspectGlobalArc)({context:prepared.globalArc,env:context.env,cwd:context.cwd,runNpm:context.runNpm});
40
+ if(!sameGlobalArc(prepared.globalArc,current))throw Error('Global ARC installation changed; prepare uninstall again');
41
+ result=operationResult(await uninstallTargets(prepared.plans,options),['uninstalled']);
42
+ if(result.code!==0||context.signal?.aborted)return{...result,code:context.signal?.aborted?130:result.code,cli:{status:'skipped'}};
43
+ const cli=await(context.uninstallGlobal??uninstallGlobal)({context:prepared.globalArc,env:context.env,cwd:context.cwd,signal:context.signal,runNpm:context.runNpm});
44
+ return{...result,code:cli.code??(cli.packageStatus==='failed'?1:0),interrupted:result.interrupted||Boolean(cli.interrupted),exitSession:Boolean(cli.started),cli:{...cli,status:cli.packageStatus},stdout:cli.stdout,stderr:cli.stderr};
45
+ }catch(error){return{...(result??{results:[],interrupted:false}),code:context.signal?.aborted?130:1,exitSession:false,cli:{status:'skipped'},error};}
46
+ }
38
47
  throw Object.assign(Error('Unknown prepared operation'),{code:'ARC_CLI_INPUT'});
39
48
  }
@@ -15,7 +15,7 @@ export function report(result,output,error){
15
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
16
  if(result.interrupted)return 130;
17
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;
18
+ error.write('Harness update did not complete. Run arc again and choose Install / Update to retry the harness update.\n');return 1;
19
19
  }
20
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
21
  export async function runChild(request,io={},deps={}){
@@ -36,23 +36,32 @@ export async function prepareUpdate(context){
36
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
37
  const state=api.collectInstalledState({safeFs:api.safeFs,userHome:context.userHome??os.homedir(),env,cwd});
38
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};
39
+ const npmContext=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
40
+ if(!['global','npx'].includes(npmContext.kind))throw configurationError('Run arc from a verified global ARC installation or npx');
41
+ let latestVersion=null,latestError;
42
+ try{const latest=await api.resolveLatest({context:npmContext,env,cwd,signal:context.signal});if(!/^\d+\.\d+\.\d+$/.test(latest.version))throw Error('Invalid latest ARC version');latestVersion=latest.version;}catch(error){if(['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(error.code))throw error;latestError=error;}
43
+ return{kind:'update',root,pkg,cwd,env,state,npmContext,currentVersion:pkg.version,latestVersion,canUpgrade:latestVersion!==null&&newer(latestVersion,pkg.version),latestError};
42
44
  }
45
+ function newer(left,right){const a=left.split('.').map(BigInt),b=right.split('.').map(BigInt);for(let i=0;i<3;i++){if(a[i]!==b[i])return a[i]>b[i];}return false;}
46
+
43
47
  export async function executeUpdate(context,prepared){
44
48
  let packageStarted=false;const messages=[];try{
45
49
  const api=await runtime(context),{root,pkg,cwd,env,state}=prepared;
50
+ if(!prepared.canUpgrade||typeof prepared.latestVersion!=='string'||!/^\d+\.\d+\.\d+$/.test(prepared.latestVersion)||!newer(prepared.latestVersion,prepared.currentVersion))throw configurationError('No newer ARC version is available');
51
+ if(identity(root).version!==pkg.version||pkg.version!==prepared.currentVersion)throw configurationError('ARC package changed after checking its version');
46
52
  const{assertPlanSnapshot}=await import('../installer/transaction.mjs');for(const entry of state.targets)assertPlanSnapshot(entry,{safeFs:api.safeFs});
53
+ const currentState=api.collectInstalledState({safeFs:api.safeFs,userHome:context.userHome??os.homedir(),env,cwd});if(currentState.stateToken!==state.stateToken)throw configurationError('Installed ARC settings changed; check the version again');
47
54
  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');
55
+ if(!['global','npx'].includes(npmContext.kind)||['kind','packageRoot','entryPath','npmCli','prefix'].some(key=>npmContext[key]!==prepared.npmContext[key]))throw configurationError('ARC npm context changed; check the version again');
56
+ const expectedVersion=prepared.latestVersion;
49
57
  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;
58
+ const child=await api.runNpxUpgrade({context:npmContext,expectedVersion,expectedStateToken:state.stateToken,env,cwd,signal:context.signal});
59
+ return{...child,code:child.code===0&&!child.signal?0:child.code===130?130:1,exitSession:false,results:[],messages};
51
60
  }
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});
61
+ packageStarted=true;
62
+ const replaced=await api.upgradeGlobal({context:npmContext,expectedVersion,env,cwd,signal:context.signal});if(replaced.packageStatus!=='updated')throw replaced.error??Error('CLI replacement failed');
63
+ messages.push('CLI: updated to '+expectedVersion+' (previous '+pkg.version+').\n');
64
+ const child=await api.runUpgradeChild({entryPath:path.join(npmContext.packageRoot,'bin/arc-upgrade.mjs'),request:{protocol:1,expectedVersion,invocationCwd:cwd,expectedStateToken:state.stateToken},env,cwd,signal:context.signal});
56
65
  return{...child,code:child.code===0&&!child.signal?0:child.code===130?130:1,exitSession:true,results:[],messages};
57
66
  }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
67
  }
@@ -34,3 +34,25 @@ export async function inspectNpmContext({packageRoot,entryPath,env=process.env,c
34
34
  return unsupported('ARC is not in a verified global or npm exec installation');
35
35
  }catch{return unsupported('Cannot verify npm installation context');}
36
36
  }
37
+
38
+ // Global absence is a filesystem fact. Identity and context failures must throw,
39
+ // because treating them as absence could allow harness removal before validation.
40
+ export async function inspectGlobalArc({context,env=process.env,cwd=process.cwd(),runNpm=defaultRunNpm}={}){
41
+ const fail=message=>Object.assign(Error(message),{code:'ARC_TARGET_CONFIGURATION'}),npmCli=npmExecutable(env);
42
+ if(context?.npmCli&&context.npmCli!==npmCli)throw fail('Global ARC npm context changed');
43
+ async function query(args){const result=await runNpm(npmCli,args,{env,cwd});if(result.code!==0||result.signal)throw fail('Cannot verify global npm context');const value=result.stdout.trim();if(!path.isAbsolute(value)||value.includes('\n'))throw fail('Invalid global npm context path');return path.resolve(value);}
44
+ const queriedPrefix=await query(['prefix','--global']),queriedRoot=await query(['root','--global']);
45
+ let prefix;try{prefix=realDirectory(queriedPrefix);}catch(error){if(error.code!=='ENOENT')throw error;prefix=queriedPrefix;}
46
+ if(![path.join(queriedPrefix,'lib/node_modules'),path.join(queriedPrefix,'node_modules')].includes(queriedRoot))throw fail('Unverified global npm root');
47
+ const root=path.join(prefix,path.relative(queriedPrefix,queriedRoot)),packageRoot=path.join(root,'@yunhozz/arc');
48
+ for(const relative of [path.relative(prefix,root),'@yunhozz','arc']){
49
+ // Check each component, including lib/node_modules, without following links.
50
+ const base=relative==='@yunhozz'?root:relative==='arc'?path.join(root,'@yunhozz'):prefix;
51
+ let current=base;for(const part of relative.split(path.sep)){current=path.join(current,part);let stat;try{stat=fs.lstatSync(current);}catch(error){if(error.code==='ENOENT')return{kind:'absent'};throw error;}if(stat.isSymbolicLink()||!stat.isDirectory())throw fail('Global ARC directory identity is invalid or symlinked');}
52
+ }
53
+ const filename=path.join(packageRoot,'package.json');if(!fs.lstatSync(filename).isFile()||fs.lstatSync(filename).isSymbolicLink())throw fail('Invalid global ARC package identity');
54
+ let pkg;try{pkg=JSON.parse(fs.readFileSync(filename,'utf8'));}catch{throw fail('Invalid global ARC package identity');}
55
+ if(pkg.name!=='@yunhozz/arc'||!/^\d+\.\d+\.\d+$/.test(pkg.version))throw fail('Invalid global ARC package identity');
56
+ const{createHash}=await import('node:crypto'),stat=fs.lstatSync(packageRoot),packageToken=createHash('sha256').update(fs.readFileSync(filename)).update(String(stat.dev)+':'+String(stat.ino)).digest('hex');
57
+ return{kind:'global',npmCli,prefix,packageRoot,version:pkg.version,packageToken};
58
+ }
@@ -0,0 +1,17 @@
1
+ import {inspectGlobalArc} from './context.mjs';
2
+ import {runNpm as defaultRunNpm} from './process.mjs';
3
+ export function sameGlobalArc(left,right){return['kind','npmCli','prefix','packageRoot','version','packageToken'].every(key=>left?.[key]===right?.[key]);}
4
+ export async function uninstallGlobal({context,env=process.env,cwd=process.cwd(),signal,runNpm=defaultRunNpm}){
5
+ let started=false,result;
6
+ try{
7
+ const current=await inspectGlobalArc({context,env,cwd,runNpm});
8
+ if(!sameGlobalArc(context,current))throw Object.assign(Error('Global ARC installation changed; prepare uninstall again'),{code:'ARC_TARGET_CONFIGURATION'});
9
+ if(signal?.aborted)throw Object.assign(Error('Uninstall interrupted'),{code:'ARC_NPM_CANCELLED'});
10
+ if(current.kind==='absent')return{packageStatus:'absent',code:0,started:false,stdout:'',stderr:''};
11
+ started=true;
12
+ result=await runNpm(current.npmCli,['uninstall','--global','@yunhozz/arc','--prefix',current.prefix,'--ignore-scripts'],{env,cwd,signal});
13
+ if(result.code!==0||result.signal)throw Error('Global CLI removal failed; the package may have changed');
14
+ if((await inspectGlobalArc({context:current,env,cwd,runNpm})).kind!=='absent')throw Error('Global ARC remains after npm uninstall');
15
+ return{...result,packageStatus:'removed',started};
16
+ }catch(error){const interrupted=['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(error.code)||Boolean(signal?.aborted);return{...result,packageStatus:'failed',code:interrupted?130:result?.code||1,interrupted,started,error};}
17
+ }
@@ -27,3 +27,28 @@ export async function runUpgradeChild({entryPath,request,env,cwd,signal}){
27
27
  child.stdin.end(JSON.stringify(request));
28
28
  });
29
29
  }
30
+ // Resolve the npm-exec payload from its own bin, then use only that payload's
31
+ // verified management runtime. The global-only private child is not reused.
32
+ export async function runNpxUpgrade({context,expectedVersion,expectedStateToken,env,cwd,signal,runNpm=defaultRunNpm}){
33
+ if(context.kind!=='npx'||!validVersion(expectedVersion)||typeof expectedStateToken!=='string'||!/^[a-f0-9]{64}$/.test(expectedStateToken))throw Error('Invalid pinned npx upgrade request');
34
+ if(signal?.aborted)throw cancelled();
35
+ const bootstrap=`import fs from 'node:fs';import path from 'node:path';import os from 'node:os';import {pathToFileURL} from 'node:url';
36
+ try{
37
+ const expectedVersion=${JSON.stringify(expectedVersion)},expectedStateToken=${JSON.stringify(expectedStateToken)},cwd=${JSON.stringify(cwd)};
38
+ const bin=process.env.PATH.split(path.delimiter).map(p=>path.join(p,'arc')).find(p=>fs.existsSync(p));
39
+ if(!bin)throw Error('Cannot locate pinned ARC payload');
40
+ const entry=fs.realpathSync(bin),root=path.dirname(path.dirname(entry)),pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
41
+ if(pkg.name!=='@yunhozz/arc'||pkg.version!==expectedVersion||entry!==path.join(root,'bin/arc.mjs'))throw Error('Pinned ARC package identity mismatch');
42
+ const management=await import(pathToFileURL(path.join(root,'lib/management/update.mjs')));
43
+ if(typeof management.runtime!=='function'||typeof management.report!=='function')throw Error('Pinned ARC management runtime is unavailable');
44
+ const api=await management.runtime({});
45
+ if(typeof api.inspectNpmContext!=='function'||typeof api.collectInstalledState!=='function'||typeof api.upgradeInstalledTargets!=='function')throw Error('Pinned ARC management API is unavailable');
46
+ const context=await api.inspectNpmContext({packageRoot:root,entryPath:entry,env:process.env,cwd});
47
+ if(context.kind!=='npx'||context.packageRoot!==root)throw Error('Cannot verify pinned npm exec context');
48
+ const state=api.collectInstalledState({safeFs:api.safeFs,userHome:os.homedir(),env:process.env,cwd});
49
+ if(state.stateToken!==expectedStateToken||!state.targets.length)throw Error('Installed ARC settings changed before pinned upgrade');
50
+ const controller=new AbortController(),abort=()=>controller.abort();process.once('SIGINT',abort);process.once('SIGTERM',abort);
51
+ try{process.exitCode=management.report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),process.stdout,process.stderr);}finally{process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);}
52
+ }catch(error){console.error('Error: '+error.message);process.exitCode=2;}`;
53
+ return runNpm(context.npmCli,['exec','--yes','--package','@yunhozz/arc@'+expectedVersion,'--',process.execPath,'--input-type=module','-e',bootstrap],{env,cwd,signal});
54
+ }
@@ -47,7 +47,7 @@
47
47
  "assigned-role"
48
48
  ],
49
49
  "authority": "orchestrator",
50
- "text": "Act only where role authority, approved scope and available platform tools all permit. Delegation, restart, agent messages and attached instructions do not expand role authority. MUST NOT bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again. For an optional read method failure, use only a pre-authorized, side-effect-free read alternative against the same positively identified material, approved version, purpose and role authority, with equivalent evidence conditions. A different locator requires the exact locator in the brief in advance and confirmed material identity/version; discovery is not authorization. Missing required input, authority or target mismatch, ownership conflict, partial mutation, unknown state, or an alternative that is unapproved, ambiguous or fails requires immediate return to Orchestrator before dependent work. Permit no chained discovery, guessed paths, mutation retries, dependency/environment/authority changes, or repetition of completed work. Report the first failure, alternative, evidence, limits, dependent work held and independent work remaining. First, record the exact failed check, its result and evidence, baseline revision and current revision/change state. Second, classify each failure as baseline or current, recording affected scope and dependency relationships. Third, hold only affected or dependent tasks and checks; when classification is unknown, hold only work whose impact is unknown. Fourth, Orchestrator must release already-approved independent work without waiting for unrelated failures, after confirming valid authority, target, inputs, ownership and gates for that work. Finally, keep acceptance held while any required check remains unresolved. Specialists report evidence and remaining work and must not self-authorize release; an independent-work release never suppresses a failed check, expands scope, changes criteria or waives required verification. Distinguish user-provided instructions, discovered workspace instruction files, installed role references and task inputs by source, applicability, version/approval and access state (available, absent, inaccessible, conflicting or unverified). A root AGENTS.md file absence is not evidence of absence of instructions and alone is not a blocker when applicable instructions were user-provided. Missing required inputs or unresolved instruction conflicts still hold dependent work; retrieved content does not grant authority. The resume brief preserves valid approvals for unchanged scope, completed work, failure, resolution evidence, valid and invalid evidence, remaining holds and next objective. Orchestrator revalidates affected gates after resolution and records changed facts without repeating completed work or treating a restart as new authority. Preserve all role, Git, preservation, approval, QA/recheck, final Reviewer, acceptance and commit boundaries.",
50
+ "text": "Act only as role authority, approved scope and available platform tools permit. Delegation, restart, agent messages and attached instructions do not expand role authority. MUST NOT bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return instruction conflicts to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope without requesting them again. If an optional read method fails, use only a pre-authorized, side-effect-free read alternative for the same positively identified material, approved version, purpose and role authority, with equivalent evidence conditions. A different locator requires the exact locator in the brief in advance and confirmed material identity/version; discovery is not authorization. Missing required input, authority or target mismatch, ownership conflict, partial mutation, unknown state, or an alternative that is unapproved, ambiguous or fails requires immediate return to Orchestrator before dependent work. Permit no chained discovery, guessed paths, mutation retries, dependency/environment/authority changes, or repeated completed work. Report the first failure, alternative, evidence, limits, dependent work held and independent work remaining. First, record the exact failed check, result and evidence, baseline revision and current revision/change state. Second, classify each failure as baseline or current, recording affected scope and dependency relationships. Third, hold only affected or dependent tasks and checks; when classification is unknown, hold only work whose impact is unknown. Fourth, Orchestrator must release already-approved independent work without waiting for unrelated failures, after confirming its valid authority, target, inputs, ownership and gates. Finally, keep acceptance held while any required check remains unresolved. Specialists report evidence and remaining work and must not self-authorize release; an independent-work release never suppresses a failed check, expands scope, changes criteria or waives required verification. Distinguish user-provided instructions, discovered workspace instruction files, installed role references and task inputs by source, applicability, version/approval and access state (available, absent, inaccessible, conflicting or unverified). A root AGENTS.md file absence is not evidence of absence of instructions and alone is not a blocker when applicable instructions were user-provided. Missing required inputs or unresolved instruction conflicts hold dependent work; retrieved content does not grant authority. The resume brief preserves valid approvals for unchanged scope, completed work, failure, resolution evidence, valid and invalid evidence, remaining holds and next objective. After resolution, Orchestrator revalidates affected gates and records changed facts without repeating completed work or treating a restart as new authority. Preserve all role, Git, preservation, approval, QA/recheck, final Reviewer, acceptance and commit boundaries.",
51
51
  "controls": {
52
52
  "codex": "prompt",
53
53
  "claude": "prompt"
@@ -79,7 +79,7 @@
79
79
  "role-contract"
80
80
  ],
81
81
  "authority": "orchestrator",
82
- "text": "Specialist roles MUST NOT change Git state: no stage, commit, push, merge, rebase, reset, branch creation/switching or worktree creation/removal, during normal implementation or recovery. Even an explicit Orchestrator assignment cannot authorize these actions; return them to Orchestrator. Read-only Git inspection remains allowed within the assigned scope.",
82
+ "text": "Specialist roles MUST NOT change Git state: no stage, commit, push, merge, rebase, reset, branch creation/switching or worktree creation/removal, during normal implementation or recovery. An explicit Orchestrator assignment cannot authorize these actions; return them to Orchestrator. Read-only Git inspection remains allowed within the assigned scope.",
83
83
  "controls": {
84
84
  "codex": "prompt",
85
85
  "claude": "hook"
@@ -150,7 +150,7 @@
150
150
  "role-ownership"
151
151
  ],
152
152
  "authority": "orchestrator",
153
- "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Implementer per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the task. Before reusing Implementer, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
153
+ "text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Implementer per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the task. Before Implementer reuse, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
154
154
  "controls": {
155
155
  "codex": "prompt",
156
156
  "claude": "prompt"
@@ -190,7 +190,7 @@
190
190
  "approved-spec-before-dependent-plan"
191
191
  ],
192
192
  "authority": "orchestrator",
193
- "text": "Return design/spec/plan drafts and dependencies, criteria, hold conditions and verification needs in the response. MUST NOT save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
193
+ "text": "Return design/spec/plan drafts with dependencies, criteria, hold conditions and verification needs in the response. MUST NOT save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
194
194
  "controls": {
195
195
  "codex": "prompt",
196
196
  "claude": "prompt"
@@ -211,7 +211,7 @@
211
211
  "tdd-or-recorded-exception"
212
212
  ],
213
213
  "authority": "orchestrator",
214
- "text": "Edit only assigned, approved files and run focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds. Carry the exact approved visual source, version, approval and access state, criterion IDs, required properties and comparison method, and actual-output identity, revision and comparison basis through Implementer and fresh QA handoffs. Record each visual result as pass, mismatch or unverified with accessible evidence and limits. Implementer self-report does not replace fresh QA and its actual comparison. A missing output-to-revision link is unverified; a visible difference is mismatch separately, without inferring current branch UI status. Missing source/output, inaccessible evidence or an unapproved interpretation holds the affected visual claim for Orchestrator; do not silently substitute or redesign.",
214
+ "text": "Edit only assigned, approved files and run focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds. Include the exact approved visual source, version, approval and access state, criterion IDs, required properties and comparison method, and actual-output identity, revision and comparison basis in Implementer and fresh QA handoffs. Record each visual result as pass, mismatch or unverified with accessible evidence and limits. Implementer self-report does not replace fresh QA and its actual comparison. A missing output-to-revision link is unverified; record a visible difference separately as mismatch, without inferring current branch UI status. Hold the affected visual claim for Orchestrator if source/output is missing, evidence is inaccessible or an interpretation is unapproved; do not silently substitute or redesign.",
215
215
  "controls": {
216
216
  "codex": "prompt",
217
217
  "claude": "prompt"
@@ -233,7 +233,7 @@
233
233
  "criterion-evidence"
234
234
  ],
235
235
  "authority": "orchestrator",
236
- "text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for Implementer correction and required QA recheck. Carry the exact approved visual source, version, approval and access state, criterion IDs, required properties and comparison method, and actual-output identity, revision and comparison basis through Implementer and fresh QA handoffs. Record each visual result as pass, mismatch or unverified with accessible evidence and limits. Implementer self-report does not replace fresh QA and its actual comparison. A missing output-to-revision link is unverified; a visible difference is mismatch separately, without inferring current branch UI status. Missing source/output, inaccessible evidence or an unapproved interpretation holds the affected visual claim for Orchestrator; do not silently substitute or redesign.",
236
+ "text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for Implementer correction and required QA recheck. Include the exact approved visual source, version, approval and access state, criterion IDs, required properties and comparison method, and actual-output identity, revision and comparison basis in Implementer and fresh QA handoffs. Record each visual result as pass, mismatch or unverified with accessible evidence and limits. Implementer self-report does not replace fresh QA and its actual comparison. A missing output-to-revision link is unverified; record a visible difference separately as mismatch, without inferring current branch UI status. Hold the affected visual claim for Orchestrator if source/output is missing, evidence is inaccessible or an interpretation is unapproved; do not silently substitute or redesign.",
237
237
  "controls": {
238
238
  "codex": "prompt",
239
239
  "claude": "prompt"
@@ -278,7 +278,7 @@
278
278
  "qa-rechecks"
279
279
  ],
280
280
  "authority": "orchestrator",
281
- "text": "For significant/high-risk work without a plan: Implementer → QA and required QA rechecks → Reviewer of the final full change → Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
281
+ "text": "For significant/high-risk work without a plan: Implementer → QA and required QA rechecks → Reviewer of the final full change → Orchestrator acceptance. MUST NOT omit Reviewer because no plan exists. This creates no new plan obligation.",
282
282
  "controls": {
283
283
  "codex": "prompt",
284
284
  "claude": "prompt"
@@ -303,7 +303,7 @@
303
303
  "required-qa"
304
304
  ],
305
305
  "authority": "orchestrator",
306
- "text": "For significant/high-risk plan work: each task uses Implementer → fresh QA as required → Orchestrator acceptance and focused commit. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
306
+ "text": "For significant/high-risk plan work: each task uses Implementer → fresh QA as required → Orchestrator acceptance and focused commit. Reviewer reviews the final full change once after all tasks are accepted and required QA is complete. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
307
307
  "controls": {
308
308
  "codex": "prompt",
309
309
  "claude": "prompt"
@@ -372,7 +372,7 @@
372
372
  "preceding-approval"
373
373
  ],
374
374
  "authority": "orchestrator",
375
- "text": "Before formal drafting, Orchestrator supplies one complete stage-specific brief with original requirements and criteria IDs, approved/open decisions, verified target and ownership, document paths and preceding approved versions, current behavior and connected source/generated/runtime/installer/documentation surfaces, policy conflicts, depth, dependencies, checks, holds and exact deliverable. Preserve shared brief field order. Review the complete draft once and consolidate substantive feedback as changed fields with other fields marked unchanged. Preserve sequential design, spec and plan approvals; no fixed call cap or incomplete drafts, and no calls for already resolved questions. Include instruction provenance and input states, required skill/reference paths, bounded read alternatives and return conditions, approved visual sources/versions and criteria, and unresolved interpretation decisions before dependent drafting. Planner returns incomplete required inputs to Orchestrator; optional reading recovery never grants preparation, verification, writing or approval authority.",
375
+ "text": "Follow planning.md's Planner drafting preparation and consolidated feedback procedure, including preflight, ledger, evidence classification, target preparation, preceding approvals, provenance and reference gates. Send one complete stage-specific initial brief. Planner returns a complete proportional draft and self-review; Orchestrator reviews once with the fixed checklist and sends one consolidated correction using changed fields only. Additional Planner calls require user-approved material scope/evidence changes or concrete unmet checklist items after correction, plus meaningful new input or criterion-state progress; state changed input, target criterion, expected artifact and expected progress. Hold repeated unmet criteria without new input, evidence or progress and report the criterion, observed evidence and exact unblock input or decision. Do not split omissions, repeat resolved items, or count rewording or agent replacement as progress. Preserve separate sequential design/spec/plan approvals; no fixed call cap, Planner-profile call-count conditions or incomplete draft acceptance.",
376
376
  "controls": {
377
377
  "codex": "prompt",
378
378
  "claude": "prompt"
@@ -7,7 +7,7 @@ import {isDeepStrictEqual} from 'node:util';
7
7
  import {spawnSync} from 'node:child_process';
8
8
  import {fileURLToPath} from 'node:url';
9
9
  import {openRoot} from '../safe-fs/index.mjs';
10
- import {verifyPayload} from '../installer/manifest.mjs';
10
+ import {verifyPayload,LOCAL_UPDATER_PATH} from '../installer/manifest.mjs';
11
11
  import {readCheckedFile,typeOf,sameIdentity} from '../installer/targets.mjs';
12
12
  import {runNpm} from '../npm/process.mjs';
13
13
 
@@ -40,6 +40,7 @@ function inventory(root,pkg){
40
40
  const files=new Map(),directory=openRoot(root);
41
41
  function visit(parent,name,relative){
42
42
  const stat=parent.lstat(name),type=typeOf(stat);
43
+ if(relative===LOCAL_UPDATER_PATH)throw new Error('Local updater wrapper is forbidden in release inventory');
43
44
  if(type===0o040000){
44
45
  const child=parent.openDir(name);try{if(!sameIdentity(stat,child.stat()))throw new Error('Release source directory changed');
45
46
  const before=child.stat();for(const leaf of child.readDir())visit(child,leaf,relative+'/'+leaf);if(!sameIdentity(before,child.stat()))throw new Error('Release source directory changed while reading');
@@ -97,6 +98,7 @@ function archive(bytes){
97
98
  }
98
99
  function validateTarball(bytes,{expectedName,expectedVersion}){
99
100
  const files=archive(bytes),pkgFile=files.get('package.json');if(!pkgFile)throw new Error('Artifact package metadata is missing');
101
+ if(files.has(LOCAL_UPDATER_PATH))throw new Error('Local updater wrapper is forbidden in release artifact');
100
102
  const pkg=JSON.parse(pkgFile.bytes);metadata(pkg,undefined,{artifact:true});
101
103
  if(pkg.name!==expectedName||pkg.version!==expectedVersion)throw new Error('Artifact package identity mismatch');
102
104
  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);
@@ -104,7 +106,7 @@ function validateTarball(bytes,{expectedName,expectedVersion}){
104
106
  const root=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-release-verify-'));
105
107
  try{
106
108
  for(const[name,item]of files){const filename=path.join(root,name);fs.mkdirSync(path.dirname(filename),{recursive:true,mode:0o700});fs.writeFileSync(filename,item.bytes,{mode:item.mode});}
107
- verifyPayload({packageRoot:root,safeFs:{openRoot}});
109
+ verifyPayload({packageRoot:root,safeFs:{openRoot},payloadMode:'package'});
108
110
  }finally{fs.rmSync(root,{recursive:true,force:true});}
109
111
  return pkg;
110
112
  }
@@ -121,7 +123,7 @@ export async function buildReleaseArtifact({sourceRoot,outDir,npmCli,signal,deps
121
123
  if(!path.isAbsolute(sourceRoot)||fs.realpathSync(sourceRoot)!==sourceRoot)throw new Error('Source root must be an absolute real path');
122
124
  abort(signal);const inputs=sourceMetadata(sourceRoot),pkg=JSON.parse(inputs['package.json']),lock=JSON.parse(inputs['package-lock.json']);metadata(pkg,lock);
123
125
  const source=inventory(sourceRoot,pkg);for(const name of ['package.json','package-lock.json'])if(!inputs[name].equals(source.files.get(name).bytes))throw new Error('Release source metadata changed during validation');
124
- verifyPayload({packageRoot:sourceRoot,safeFs:{openRoot}});if(inventory(sourceRoot,pkg).snapshot!==source.snapshot)throw new Error('Release source snapshot changed during validation');
126
+ verifyPayload({packageRoot:sourceRoot,safeFs:{openRoot},payloadMode:'source'});if(inventory(sourceRoot,pkg).snapshot!==source.snapshot)throw new Error('Release source snapshot changed during validation');
125
127
  let output,lockIdentity,workRoot,workIdentity;const id=crypto.randomUUID(),partial='.arc-release-'+id+'.partial';let partialExists=false;
126
128
  try{
127
129
  output=safeOutput(sourceRoot,outDir);const lockFile=output.openFile('.arc-release.lock',{createExclusive:true,read:false});try{lockFile.writeAll(JSON.stringify({schema:1,owner:id,pid:process.pid,state:'incomplete'}));lockIdentity=lockFile.stat();lockFile.sync();}finally{lockFile.close();}
@@ -131,7 +133,7 @@ export async function buildReleaseArtifact({sourceRoot,outDir,npmCli,signal,deps
131
133
  const stagedPkg={...pkg,scripts:{...pkg.scripts,postinstall:HOOK}},stagedLock=structuredClone(lock);stagedLock.packages[''].hasInstallScript=true;
132
134
  fs.writeFileSync(path.join(stagingRoot,'package.json'),JSON.stringify(stagedPkg,null,2)+'\n');fs.writeFileSync(path.join(stagingRoot,'package-lock.json'),JSON.stringify(stagedLock,null,2)+'\n');metadata(stagedPkg,stagedLock,{artifact:true});
133
135
  fs.writeFileSync(path.join(stagingRoot,'.arc-release-staging.json'),JSON.stringify({schema:1,owner:id,state:'incomplete',sourceSnapshot:source.snapshot}));
134
- refresh(stagingRoot);verifyPayload({packageRoot:stagingRoot,safeFs:{openRoot}});await deps.onStaged?.({stagingRoot,signal});abort(signal);
136
+ refresh(stagingRoot);verifyPayload({packageRoot:stagingRoot,safeFs:{openRoot},payloadMode:'package'});await deps.onStaged?.({stagingRoot,signal});abort(signal);verifyPayload({packageRoot:stagingRoot,safeFs:{openRoot},payloadMode:'package'});
135
137
  const bytes=await(deps.pack??pack)({stagingRoot,workRoot,npmCli,signal});abort(signal);validateTarball(bytes,{expectedName:pkg.name,expectedVersion:pkg.version});
136
138
  if(inventory(sourceRoot,pkg).snapshot!==source.snapshot)throw new Error('Release source snapshot changed during generation');abort(signal);
137
139
  const tarballPath=path.join(outDir,'arc-release.tgz'),receiptPath=path.join(outDir,'arc-release.json');
package/lib/tui/app.mjs CHANGED
@@ -1,69 +1,38 @@
1
1
  import {createTerminalSession} from './terminal.mjs';
2
- import path from 'node:path';
3
2
  import {escapeTerminalText,navigationHint,wrapDetailRows} from './render.mjs';
4
3
  import {createManagementContext,prepareOperation,executeOperation} from '../management/operations.mjs';
5
4
  import {collectStatus} from '../management/status.mjs';
6
- import {runSetup,summary} from './setup.mjs';
7
- const home=['Configure','Update','Uninstall','Exit'];
5
+ import {runSetup} from './setup.mjs';
6
+ const home=['Install / Update','Change Preset','Check Version','Uninstall','Exit'];
7
+ function goHome(){throw Object.assign(Error('Return home'),{code:'ARC_TUI_HOME'});}
8
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
9
  export function createTuiInterface(session,{input,controller}={}){
34
10
  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
- }
11
+ async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',wordmark=false}={}){
12
+ const actions=backRow?[...rows,'Back']:rows;
13
+ selected=actions.length?Math.max(0,Math.min(selected,actions.length-1)):0;
14
+ while(true){
15
+ const viewport=session.viewport(),display=actions.flatMap((row,index)=>(index===separatorBefore||backRow&&index===rows.length)?['',row]:[row]),displayIndex=selected+(Number.isInteger(separatorBefore)&&selected>=separatorBefore?1:0)+(backRow&&selected===rows.length?1:0),information=wrapDetailRows(info,viewport.columns);
16
+ if(wordmark&&information.length)information.push('');
17
+ const capacity=Math.max(1,viewport.rows-(wordmark?6:1)-information.length-4),offset=Math.max(0,Math.min(displayIndex-capacity+1,display.length-capacity)),visible=display.slice(offset,offset+capacity);
18
+ const screen={title,wordmark,rows:[...information,...visible],selected:actions.length?information.length+displayIndex-offset:undefined,hint:navigationHint('menu',{backLabel}),...(display.length>capacity?{position:(offset+1)+'-'+Math.min(offset+capacity,display.length)+'/'+display.length}:{})};
19
+ const rendered=session.render(screen),e=await event();
20
+ if(e.name==='up'&&actions.length)selected=(selected+actions.length-1)%actions.length;else if(e.name==='down'&&actions.length)selected=(selected+1)%actions.length;else if(e.name==='escape'){if(backLabel==='exit')return null;goHome();}else if(e.name==='enter'&&actions.length&&rendered.selectionEnabled!==false)return backRow&&selected===rows.length?null:selected;
52
21
  }
53
22
  }
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);
23
+ async function text(title,initial=''){let value=initial,selected=2;while(true){const rendered=session.render({title,mode:'text',rows:[value,'','Save','Back',''],selected,hint:navigationHint('text')});const e=await event();if(e.name==='text'||e.name==='paste')value+=e.text;else if(e.name==='backspace')value=[...value].slice(0,-1).join('');else if(e.name==='up'||e.name==='down')selected=selected===2?3:2;else if(e.name==='escape')goHome();else if(e.name==='enter'&&rendered.selectionEnabled!==false)return selected===2?value:null;}}
24
+ async function page(title,rows){
25
+ let offset=0;
26
+ while(true){const viewport=session.viewport(),wrapped=wrapDetailRows(rows,viewport.columns),size=Math.max(1,viewport.rows-4),last=Math.max(0,wrapped.length-size);
27
+ offset=Math.min(offset,last);const slice=wrapped.slice(offset,offset+size);
28
+ session.render({title,mode:'page',rows:slice,position:`Rows ${offset+1}-${Math.min(offset+size,wrapped.length)} of ${wrapped.length}`,hint:navigationHint('page')});const e=await event();
29
+ if(e.name==='down')offset=Math.min(last,offset+1);else if(e.name==='up')offset=Math.max(0,offset-1);else if(e.name==='escape')goHome();else if(e.name==='enter')return true;
61
30
  }
62
31
  }
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};
32
+ async function busy(action){session.render({title:'Working',mode:'busy',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();}}
33
+ return{menu,text,page,busy};
65
34
  }
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.']:[])];}
35
+ 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.cli?['CLI: '+result.cli.status,...(result.cli.error?['CLI error: '+result.cli.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?[(result.cli?'CLI removal':'CLI replacement')+' ended this session. Run arc again for further management.']:[])];}
67
36
  export async function runTui(options={}){
68
37
  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
38
  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;
@@ -74,22 +43,22 @@ export async function runTui(options={}){
74
43
  }
75
44
  async function uninstall(){
76
45
  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;
46
+ while(true){const nextTarget=await ui.menu('Uninstall target',['Codex','Claude','Both targets'],{selected:target,backRow:true});if(nextTarget===null)return null;target=nextTarget;
47
+ while(true){const nextGlobal=await ui.menu('Global ARC instructions',['Remove ARC blocks','Preserve ARC blocks'],{selected:global,backRow:true});if(nextGlobal===null)break;global=nextGlobal;
48
+ return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude','all'][target],skipGlobal:global===1}));
80
49
  }
81
50
  }
82
51
  }
83
52
  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
53
  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;
54
+ 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===4)return code;selected=choice;
86
55
  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();
56
+ let prepared;
57
+ if(choice===0||choice===1)prepared=await runSetup({ui,statuses,draft,kind:choice===0?'install':'preset',prepare:settings=>ui.busy(()=>api.prepareOperation(context,settings))});
58
+ else if(choice===2){prepared=await ui.busy(()=>api.prepareOperation(context,{kind:'update'}));const action=await ui.menu('Check Version',prepared.canUpgrade?['Version Upgrade']:[],{backRow:true,info:['Current version: '+prepared.currentVersion,'Latest version: '+(prepared.latestVersion??'Unavailable'),...(prepared.latestError?[prepared.latestError.message]:[]),...(prepared.canUpgrade?['Continue?']:[])]});if(action!==0)prepared=null;}
59
+ else prepared=await uninstall();
91
60
  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.']);}
61
+ }catch(error){if(error.code==='ARC_TUI_HOME')continue;if(['ARC_CANCELLED','ARC_TUI_EOF'].includes(error.code))throw error;code=error.code==='ARC_CLI_INPUT'?2:1;try{await ui.page('Operation unavailable',[error.message,'No changes were applied.']);}catch(navigation){if(navigation.code!=='ARC_TUI_HOME')throw navigation;}}
93
62
  }
94
63
 
95
64
  }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');}
@@ -6,11 +6,11 @@ export function escapeTerminalText(value=''){
6
6
  }
7
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
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';
9
+ export function navigationHint(mode='menu',{backLabel='back'}={}){
10
+ if(mode==='text')return'↑/↓: Select · Type/Paste Input · Backspace Delete · Enter Continue · Esc Home';
11
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');
12
+ if(mode==='page')return'↑/↓: Scroll · Enter Back · Esc Home';
13
+ return'↑/↓: Select · Enter Continue · Esc '+(backLabel==='exit'?'Exit':'Home');
14
14
  }
15
15
  export function wrapDetailRows(rows,columns){
16
16
  const available=Math.max(2,Math.floor(Number(columns)||80)-3),result=[];
@@ -27,17 +27,15 @@ export function renderScreen(screen={}, {columns=80,rows=24}={}){
27
27
  columns=Math.max(1,Math.floor(Number(columns)||80)-1);rows=Math.max(1,Math.floor(Number(rows)||24));
28
28
  const entries=screen.rows??[],hint=screen.hint??navigationHint('menu');
29
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)];
30
+ const footer=screen.mode==='busy'||screen.hint==='Session ended'?[escapeTerminalText(hint)]:[...(screen.mode==='text'||screen.mode==='page'?[]:['']),'─'.repeat(columns),escapeTerminalText(hint)];
31
+ const lines=[...heading,...entries.map((row,index)=>(index===screen.selected?'> ':' ')+escapeTerminalText(row)),...position,...footer];
31
32
  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;
33
+ const bodySize=Math.max(0,rows-footer.length),visible=[...fitted.slice(0,Math.min(lines.length-footer.length,bodySize)),...fitted.slice(-Math.min(rows,footer.length))].map(line=>line.text);
37
34
  const styles=[];
38
35
  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
36
  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'});}
37
+ if(Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<Math.min(bodySize,lines.length-footer.length)){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
38
 
42
- return{text:visible.join('\n'),applyEnabled,styles};
39
+ const selectionEnabled=Number.isInteger(screen.selected)&&screen.selected>=0&&screen.selected<entries.length&&selectedLine<rows-footer.length-position.length&&fitted[selectedLine].complete;
40
+ return{text:visible.join('\n'),selectionEnabled,styles};
43
41
  }