@yunhozz/arc 0.1.5 → 0.1.6

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 CHANGED
@@ -1,4 +1,5 @@
1
- cbd254348f60c8610d0e5c4b8216c9772c0bd5dbfef324796da516b87054b06e README.md
1
+ 85cea3cfddbf92091b1eec35feaea339e70fc2a1faa65099c99cc6318aec1cc2 README.md
2
+ d41edeb8c6631dee30c67c705d4edad977aa1631627ec9de59c013f8990f4018 bin/arc-upgrade.mjs
2
3
  484d9b2002ce542c87589bb719d644d3762706aad28a6ee010024875f8cc00e2 bin/arc.mjs
3
4
  d853d31ba0d1a04661adf8cae1df11058455cb5d366c52312888c9dd2e9a1fb8 claude/agents/arc-coder.md
4
5
  91a2c62f4ea3ff00145407411fbbe62aa02fb7608af8f33bbd3d1719fc9d2f13 claude/agents/arc-planner.md
@@ -64,13 +65,16 @@ aa1361419c0137f74793847805a3abc076a8d782c6f590e2d28b8eee5c35065d codex/skill/ru
64
65
  57e93213dc97465757294cb9d0ae2f4eb2f1384b955c4ca2a39f2fa38f7a9bd7 codex/skill/scripts/apply-models.py
65
66
  bce13feeea37c78e5b0ddaf6bdd5ffd2fd0162eabc8c468667133edbabebe944 codex/skill/scripts/model_config.py
66
67
  800964b417c573aa7b2a07a76759a4750652222de63a985bc133d50194739358 install.command
67
- 92e329fcfb26899f524c31ce6cdb08997c274560b271d80d230ae303d6987f17 lib/cli/choices.mjs
68
- 7833879c00f296248bb37a367564ad0c5db07e2c441873a0b4e6a3c3733ca137 lib/cli/completion.mjs
69
- 6655cf91a3af427378755299433feb3f0086d43b047c4f6cd5a2c429f2812dba lib/cli/index.mjs
68
+ d8ed829763544877436f0e6bf349c5460db0c44cce73b75d38c5661c902536c4 lib/cli/choices.mjs
69
+ 640d1bfdda880c2f17149209ce35933ee9f4318b35166df2e86ff49e271d8fc5 lib/cli/completion.mjs
70
+ 0e0d5135fce87d00bda6a92c03ca39ad542841f80a785674d3f7ac446dbce014 lib/cli/index.mjs
71
+ fe34003f994ab962f60f6ec6e04e6c5f812ceb00936cef3d25365bc4c672707b lib/cli/postinstall.mjs
70
72
  515132cf88973720bb6197a849d896face56c23f3c7fc4681ae9123ffc576318 lib/cli/preset.mjs
71
73
  a575a94adc9461a7c5c051983b47d1a40a88fb8ee90fa469041cd99165cbb80f lib/cli/progress.mjs
72
- ad8ad2f47da6e1d3160d786628d768e8d95108f1a1c113a6a5305e7c85c438db lib/cli/root.mjs
74
+ b5497f312d0ad84efc5328def88197adffa239a18f4746ef12ea20059e36d6a8 lib/cli/root.mjs
75
+ 034a93e4127e303880dc347488e46214702b343da06f9ba7a7a6c757473622a5 lib/cli/terminal.mjs
73
76
  85f25429f208a4b45a36e66f89933d2bbfb470d6f52f032f70971817063e45f0 lib/cli/uninstall.mjs
77
+ 4fadbd52e4ecd0d95c7240037c84aa74714e1f9f764440e31d3ce3402c23b5b6 lib/cli/upgrade.mjs
74
78
  7719a16f6327ea3ba35206ee41c829ab967f865df9ebbd161129ca28babceb3d lib/installer/acquisition.mjs
75
79
  66bc9a18523f70ddbb1d9a0844173d311aabd2d00d9ed5c06ca6679179ba8b06 lib/installer/global-config.mjs
76
80
  061ff82e0c655f47b872610b6ad7bde5d1941eaf7c563491a44291c86b16a349 lib/installer/install.mjs
@@ -78,7 +82,7 @@ f9a0958fc424742f49ce6e2e0a49c7cf2bbe288e0cf3551d69a8a912c8f2709f lib/installer/
78
82
  64713575291e51971abc542df716baa99e9a530a489e92b2f8345b782e0ce4cd lib/installer/limits.mjs
79
83
  cec99b4e9c6e6b6c634c84fba978b93484011b6357b652b99b31612ac522a22d lib/installer/manifest.mjs
80
84
  1e6667f21dfa6eb427d7447df98038bbe534345ebb57d6b401f51efe1077f1ed lib/installer/package-references.mjs
81
- 92065a84b0f252cc8e90a76b24f110c615c11035e45e073fb00bb7cb5de83bdf lib/installer/plan.mjs
85
+ 383390bb5a73d3db85d4cfe7f8ae0502925552eacc66c849c0061cd82e487ebd lib/installer/plan.mjs
82
86
  5a7860583cadb57295bdac8296a6c8caa4cedf0d1823e7d08c57846ed8342ba5 lib/installer/preset.mjs
83
87
  4892d61cfe498a85f47f4a8044a831c853660365c98543eb14f4f4449b727c2f lib/installer/runtime-config.mjs
84
88
  e37be4e8b6f366201f42aeab5d7973baa8303b1f96461ee2ced6ce2e73da3bd2 lib/installer/shell-completion.mjs
@@ -87,14 +91,20 @@ c2ab7eda987cf62bba4dce1b7eb302f350a6c67679b0aaf063b3eea11eb010c5 lib/installer/
87
91
  07805edb471c54b87dcc827cf24264571a6427961989d508773b9751a39dc1f0 lib/installer/targets.mjs
88
92
  65ba12b4786b8b8c2cd65731a96475a64d1888644e8c68825bc369d74c266ab8 lib/installer/transaction.mjs
89
93
  f0606cd2cd4871d11e544b5ad8221e4d9050761d1d80ab94c9219eb7e494b375 lib/installer/uninstall.mjs
94
+ b83205b8244edd9868c91ffbf19864f84a51556b16e87c219b39916b8c31ed51 lib/installer/upgrade-state.mjs
95
+ 6274809b326a65bfc6cb5062226b276ef95cc386a5c90c0e70537930c7fd1822 lib/installer/upgrade.mjs
90
96
  2946f055d61fbe2ffd6af1c182096e01d8c1a4bfb0aa274547ce5e147dccfd19 lib/limits.mjs
91
97
  94c3315811e9694db4423c851437975e658949edb7706cf8124d6e1a4b60d0e5 lib/models/config.mjs
92
98
  a7afd79d4620abcf85f4954aab4599870d8e47b897490d1c5c774f256c5e8800 lib/models/index.mjs
93
99
  cc205194c9aa90887bb74c11cb0f5adb63f9da8afe660755a73c62d090309385 lib/models/profiles.mjs
100
+ acf28d52dac6a097e577da79ac8b9d2b30872a8f0ea66fc855db4b3f4863be5d lib/npm/context.mjs
101
+ ee81dee01b1623d95dda9b1d195ce2008811dfb54af3da8324a9e538238ac84e lib/npm/process.mjs
102
+ 8945c12db082a4a93bc71943d698cf60326e0ba45ae2a1caf6488a1dbc9d7cc7 lib/npm/upgrade.mjs
94
103
  7c36313221f36a9d08964017c79e8c7b3c848655f6a338f71a24ce508ac6e3ed lib/policy/claude-hook.mjs
95
104
  f32fcb03201c6a73d658a6b3107376f13d03f21503c568a7b4e71ff9b71dd426 lib/policy/placement.mjs
96
105
  09b5851cdbcb88b9d4d027e2d96aadb275660354ea64eef0aaa7583ec8090c3e lib/policy/render.mjs
97
106
  5fd21f28b7983a89de8d52033ea61003462586ae679dbf87843b8291a2a29237 lib/policy/role-policy.json
107
+ 80e270d526e9a9c5cac23fe6ae293c2fd25a0176e4a8060987fd19dafe2a4594 lib/release/artifact.mjs
98
108
  30d8cdcb9d7ab6396cab09cd9f5cf6b8003f5490fb5c0ffe9f5acffbb0534978 lib/runtime/adapters/claude.mjs
99
109
  d30d118d3b42b805195ed05a3dea0ed3cf939eac811cf0228ca0b0cafeda7969 lib/runtime/adapters/codex.mjs
100
110
  c5a56ac147f13e9f946f673ac57a5dadbb4560d1f9deaa7c3b60e703f39b2d83 lib/runtime/capabilities.mjs
@@ -110,7 +120,7 @@ aa1361419c0137f74793847805a3abc076a8d782c6f590e2d28b8eee5c35065d lib/runtime/st
110
120
  4174dfcfcd4d69a5486b91108ec03279cfd50644109c7457aa87f9ac877fb1ac lib/safe-fs/native.mjs
111
121
  ff9c33c8b3c2bd466d54ffbe4c7139ccd87c79c7de34a6c4cde92e9f6e9b42d2 native/binding.c
112
122
  205bedc1dee1a9ffa58124c6e21256a1f0cbe24f06f7c8d7cb0b99524cfab9d0 native/binding.gyp
113
- b4aa1c8a1b3226485c90ca31841b57559150ab2a83db5764bf11ad84083236dd package.json
123
+ 837e5eb730cc792d694939fd8cd5a5db41d6308052ac3fdae32bb0a9756ab51c package.json
114
124
  208c65cfde244d39ce45c3cf1b0acb128f04d21199b6b3fb5466e62db5b64af9 prebuilds/darwin-arm64/arc_safe_fs.node
115
125
  572887ac794d10a8b921d8168ea2328e31f05b45987611ff02c83114dba901af prebuilds/darwin-x64/arc_safe_fs.node
116
126
  2bb9443021f9d873712b26321de733eddd6794c45f0bc79e2d5dc98669196635 prebuilds/index.json
@@ -118,12 +128,15 @@ b98bb4ac757ebd6e8f8fc1eb7a6a4389e3a8199984baf6bb1c327a6f952c2c0d prebuilds/linu
118
128
  9f8c2c1e328ee287355bc36afc3173c00ed062611fbd1262e4d30fca83e979dc prebuilds/linux-x64-gnu/arc_safe_fs.node
119
129
  0750a576f8042248a35b55559ccc52ef787a7706de7901169ec929622f256643 scripts/build-native.mjs
120
130
  aa394307b1bba68ea7146f76bec479a21f2cbeff9ea18d0786516c677a2d9fbb scripts/check-installer-parity.mjs
131
+ de88500b49726973d1de71c0bbdf9c993e28720a5b3f28751992c023b617e04c scripts/pack-release.mjs
132
+ 21cb8f3c35ee69ee31e9243f5b8844ca48f1351e77368071f8d4de0116dd1dd7 scripts/postinstall.mjs
121
133
  52aa4334c02e1877822304e66959b12f98a1649a547d51b9271623221ef3573a scripts/probe-runtime-hooks.mjs
122
134
  9c6bce1c1ab29ba75b70742e4d67c1eb0d0a0cab31e06b8abd28b057253f32d0 scripts/update-manifest.mjs
123
135
  e99ca8fd7dbde12840097e3d68bef06037f04284474fc5006976da40758cf6bb scripts/update-role-policy.mjs
124
136
  38163bb8828e4b4ab6bedfad719dba744ef0d99a09c9b23a85b1076d8375f181 scripts/update-runtime-assets.mjs
125
137
  f7c5d8288905ed7a6f0e99e3668a34ed43a3c1a7a3157bc883f22ec94fe22e98 scripts/update-superpowers.mjs
126
138
  40f41cc52354e02d9743c7b8d270c187856aac02cd9d7996d19b1f1089355d88 scripts/vendor-toml.mjs
139
+ a8884682a56e563b64931a2e0b025ec50a18bc758b0de7f5a92b7890ac36c4a5 scripts/verify-release.mjs
127
140
  fa5659948374d4f555594f47f6da073b40dc503e921aeeece30df4362b3051a5 vendor/toml/LICENSE
128
141
  ef924a2a42c7f16761e7098c3ed8e04195ecbcb02d5c369db0aee76f251a409c vendor/toml/dist/date.js
129
142
  76f8e47f5fc57273748242d59570656d7c921a80bceb2291ca5a4d7a723e9f29 vendor/toml/dist/error.js
package/README.md CHANGED
@@ -53,6 +53,17 @@ npx --yes @yunhozz/arc preset smart --target codex
53
53
  npx --yes @yunhozz/arc uninstall --target codex
54
54
  ```
55
55
 
56
+ ## 🧰 Optional: use the global CLI
57
+
58
+ If you run ARC management commands regularly, you can install the CLI globally and keep using the `arc` command:
59
+
60
+ ```sh
61
+ npm i -g @yunhozz/arc
62
+ arc
63
+ ```
64
+
65
+ The first command installs the CLI. Run `arc` to install the ARC harness. Afterward, use `arc preset` or `arc uninstall` to manage it. `arc uninstall` removes the harness; `npm uninstall -g @yunhozz/arc` removes the CLI and leaves the harness installed.
66
+
56
67
  ## 🎛️ Models and language
57
68
 
58
69
  The Orchestrator uses the current conversation's model. The five specialist roles use the selected preset from their platform model file: [Codex](codex/skill/models.toml) · [Claude Code](claude/skill/models.toml). The configured language applies across ARC; an explicit user language request takes precedence.
@@ -0,0 +1,3 @@
1
+ import {runChild} from '../lib/cli/upgrade.mjs';
2
+ try{let bytes=Buffer.alloc(0);for await(const chunk of process.stdin){bytes=Buffer.concat([bytes,chunk]);if(bytes.length>4096)throw Error('Invalid private upgrade request');}process.exitCode=await runChild(JSON.parse(bytes.toString('utf8')));}
3
+ catch{process.stderr.write('Error: Invalid private upgrade request\n');process.exitCode=2;}
@@ -13,10 +13,10 @@ export async function selectChoices(options,{input=process.stdin,output=process.
13
13
  try{return await new Promise((resolve,reject)=>{
14
14
  abort=()=>reject(cancelled());onEnd=abort;onError=reject;
15
15
  onKey=(str,key={})=>{try{if(key.name==='c'&&key.ctrl||key.name==='d'&&key.ctrl)return abort();if(key.name==='up'||key.name==='down'){index=(index+(key.name==='up'?-1:1)+menus[menu][1].length)%menus[menu][1].length;draw(true);}else if(key.name==='return'||key.name==='enter'){const [name,,values]=menus[menu];options[name]=values[index];output.write('\n');if(++menu===menus.length){keys.removeListener('keypress',onKey);resolve(options);}else{index=menus[menu][3];draw();}}}catch(error){reject(error);}};
16
- keys.on('keypress',onKey);readline.emitKeypressEvents(keys);onData=chunk=>keys.emit('data',chunk);input.on('data',onData);input.once('end',onEnd);input.once('error',onError);process.once('SIGINT',abort);
16
+ keys.on('keypress',onKey);readline.emitKeypressEvents(keys);onData=chunk=>keys.emit('data',chunk);input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);
17
17
  if(input.setRawMode)input.setRawMode(true);input.resume();draw();
18
18
  });}finally{
19
- keys.removeAllListeners();input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);
19
+ keys.removeAllListeners();input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
20
20
  for(const [event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
21
21
  if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
22
22
  }
@@ -36,9 +36,9 @@ export async function selectLimits(options,selections,{input=process.stdin,outpu
36
36
  if(name==='maxConcurrentSubagents')options.concurrentSelected=true;output.write('\n');value='';if(++index===fields.length){input.removeListener('keypress',onKey);resolve(options);}else draw();
37
37
  }else if(key.name==='backspace'){value=value.slice(0,-1);output.write('\b \b');}else if(str&&!key.ctrl&&!key.meta){value+=str;output.write(str);}};
38
38
  onData=chunk=>{for(const character of chunk.toString()){if(index===fields.length)break;const name=character==='\r'||character==='\n'?'return':character==='\x03'?'c':character==='\x04'?'d':character==='\x7f'||character==='\b'?'backspace':undefined;onKey(character,{name,ctrl:character==='\x03'||character==='\x04'});}};
39
- input.on('data',onData);input.once('end',onEnd);input.once('error',onError);process.once('SIGINT',abort);if(input.setRawMode)input.setRawMode(true);input.resume();draw();
39
+ input.on('data',onData);input.once('end',onEnd);input.once('close',onEnd);input.once('error',onError);process.once('SIGINT',abort);process.once('SIGTERM',abort);if(input.setRawMode)input.setRawMode(true);input.resume();draw();
40
40
  });}finally{
41
- input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);
41
+ input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('close',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
42
42
  for(const[event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
43
43
  if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
44
44
  }
@@ -2,17 +2,18 @@ export function renderCompletion(shell){
2
2
  if(shell==='bash')return `_arc() {
3
3
  local cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}" command="\${COMP_WORDS[1]}" choices=""
4
4
  case "$prev" in
5
- --target) choices="claude codex all" ;;
6
- --preset) choices="economy balanced smart" ;;
7
- --language) choices="en ko" ;;
5
+ --target) [[ $command == -* || $command == preset || $command == uninstall ]] && choices="claude codex all" ;;
6
+ --preset) [[ $command == -* ]] && choices="economy balanced smart" ;;
7
+ --language) [[ $command == -* ]] && choices="en ko" ;;
8
8
  *)
9
- if (( COMP_CWORD == 1 )) && [[ $cur != -* ]]; then choices="preset uninstall"
9
+ if (( COMP_CWORD == 1 )) && [[ $cur != -* ]]; then choices="preset uninstall completion upgrade"
10
10
  else
11
11
  case "$command" in
12
12
  preset)
13
13
  if (( COMP_CWORD == 2 )) && [[ $cur != -* ]]; then choices="economy balanced smart"
14
14
  else choices="--target --check --dry-run --help"; fi ;;
15
15
  uninstall) choices="--target --dry-run --skip-global --help --version" ;;
16
+ upgrade) choices="--help" ;;
16
17
  completion) (( COMP_CWORD == 2 )) && choices="zsh bash" ;;
17
18
  -*) choices="--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version" ;;
18
19
  esac
@@ -25,17 +26,18 @@ complete -F _arc arc
25
26
  if(shell==='zsh')return `_arc() {
26
27
  local -a candidates
27
28
  if (( CURRENT == 2 )) && [[ \${words[CURRENT]} != -* ]]; then
28
- candidates=(preset uninstall)
29
+ candidates=(preset uninstall completion upgrade)
29
30
  else
30
31
  case "\${words[CURRENT-1]}" in
31
- --target) candidates=(claude codex all) ;;
32
- --preset) candidates=(economy balanced smart) ;;
33
- --language) candidates=(en ko) ;;
32
+ --target) [[ \${words[2]} == -* || \${words[2]} == preset || \${words[2]} == uninstall ]] && candidates=(claude codex all) ;;
33
+ --preset) [[ \${words[2]} == -* ]] && candidates=(economy balanced smart) ;;
34
+ --language) [[ \${words[2]} == -* ]] && candidates=(en ko) ;;
34
35
  *) case "\${words[2]}" in
35
36
  preset)
36
37
  if (( CURRENT == 3 )) && [[ \${words[CURRENT]} != -* ]]; then candidates=(economy balanced smart)
37
38
  else candidates=(--target --check --dry-run --help); fi ;;
38
39
  uninstall) candidates=(--target --dry-run --skip-global --help --version) ;;
40
+ upgrade) candidates=(--help) ;;
39
41
  completion) (( CURRENT == 3 )) && candidates=(zsh bash) ;;
40
42
  -*) candidates=(--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version) ;;
41
43
  esac ;;
package/lib/cli/index.mjs CHANGED
@@ -13,7 +13,7 @@ export function parseArgs(args){
13
13
  if(options.check&&options.dryRun)throw inputError('--check and --dry-run cannot be combined');return options;
14
14
  }
15
15
  export async function selectChoices(options,io){return(await import('./choices.mjs')).selectChoices(options,io);}
16
- export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr}={}){
16
+ export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr,cwd=process.cwd(),env=process.env}={}){
17
17
  let controller,onInterrupt,progress;
18
18
  try{
19
19
  const options=parseArgs(args);if(options.help){output.write(usage+'\n');return 0;}if(options.version){output.write(JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version+'\n');return 0;}
@@ -22,12 +22,12 @@ export async function runCli(args=process.argv.slice(2),{input=process.stdin,out
22
22
  const {startProgress}=await import('./progress.mjs');
23
23
  const interactive=input.isTTY&&output.isTTY,label=options.check?'Checking ARC…':options.dryRun?'Preparing ARC dry run…':'Installing ARC…';
24
24
  if(!interactive)progress=startProgress({output,label});
25
- controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);
25
+ controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
26
26
  const {loadNative}=await import('../safe-fs/native.mjs');const native=loadNative();
27
27
  const [{openRoot},{verifyPayload},{resolveTarget},{buildInstallPlan,validatePlanSet}]=await Promise.all([import('../safe-fs/index.mjs'),import('../installer/manifest.mjs'),import('../installer/targets.mjs'),import('../installer/plan.mjs')]);
28
28
  const safeFs={openRoot:p=>openRoot(p,{native})},payload=verifyPayload({packageRoot,safeFs});
29
29
  const platforms=options.target==='all'?['codex','claude']:[options.target];
30
- const {previewLimits,previewLanguage}=await import('../installer/plan.mjs');const{defaultLanguage}=await import('../installer/language.mjs');const selections=platforms.map(platform=>{const target=resolveTarget(platform,{userHome:os.homedir(),env:process.env});return{target,preview:previewLimits({target,safeFs}),languagePreview:previewLanguage({target,safeFs})};});
30
+ const {previewLimits,previewLanguage}=await import('../installer/plan.mjs');const{defaultLanguage}=await import('../installer/language.mjs');const selections=platforms.map(platform=>{const target=resolveTarget(platform,{userHome:os.homedir(),env,cwd});return{target,preview:previewLimits({target,safeFs}),languagePreview:previewLanguage({target,safeFs})};});
31
31
  if(interactive){const{selectLimits,selectLanguage}=await import('./choices.mjs');await selectLimits(options,selections,{input,output});await selectLanguage(options,defaultLanguage(selections.map(item=>item.languagePreview.existing)),{input,output});}
32
32
  progress??=startProgress({output,label});
33
33
  const plans=selections.map(({target,preview,languagePreview})=>buildInstallPlan({payload,target,language:options.language,languagePreview,preset:options.preset,skipGlobal:options.skipGlobal,safeFs,limitsPreview:preview,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)}));const {attachShellCompletion}=await import('../installer/shell-completion.mjs');attachShellCompletion(plans,{safeFs});validatePlanSet(plans);
@@ -48,8 +48,8 @@ export async function runCli(args=process.argv.slice(2),{input=process.stdin,out
48
48
  progress.stop();
49
49
  const reportResults=()=>{for(const item of result.results){output.write(`${item.platform}: ${item.status}; rollback: ${item.rollback}\n`);for(const recovery of item.residualPaths)output.write(`Residual path: ${recovery}\n`);if(item.error)error.write(`Error: ${item.error.message}\n`);}};
50
50
  if(result.interrupted){reportResults();return 130;}
51
- if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write(`ARC installed! [${result.results.map(item=>item.platform).join('/')}]\n`);return 0;}
51
+ if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write(`ARC version ${JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version} installed! [${result.results.map(item=>item.platform).join('/')}]\n`);return 0;}
52
52
  reportResults();return 1;
53
53
  }catch(cause){progress?.stop();error.write(`Error: ${cause.message}\n`);if(cause.code==='ARC_CLI_INPUT'){error.write(usage+'\n');return 2;}if(cause.code==='ARC_CANCELLED')return 130;return ['ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE','ARC_TARGET_CONFIGURATION'].includes(cause.code)?2:1;
54
- }finally{progress?.stop();if(onInterrupt)process.removeListener('SIGINT',onInterrupt);}
54
+ }finally{progress?.stop();if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
55
55
  }
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+ import {inspectNpmContext,resolveInvocationCwd} from '../npm/context.mjs';
3
+ import {openInstallerTerminal} from './terminal.mjs';
4
+ import {runCli} from './index.mjs';
5
+ export async function runPostinstall({packageRoot,env=process.env,cwd=process.cwd(),input=process.stdin,output=process.stdout,error=process.stderr,deps={}}){
6
+ let terminal;
7
+ try{
8
+ if(env.npm_lifecycle_event!=='postinstall'){output.write('Run arc to install the ARC harness.\n');return 0;}
9
+ const context=await(deps.inspectNpmContext??inspectNpmContext)({packageRoot,entryPath:path.join(packageRoot,'scripts/postinstall.mjs'),env,cwd});
10
+ if(context.kind!=='global'){output.write('Run arc to install the ARC harness.\n');return 0;}
11
+ terminal=(deps.openInstallerTerminal??openInstallerTerminal)({env,input,output,error});
12
+ if(terminal.kind==='skip'){output.write('Run arc to install the ARC harness.\n');return 0;}
13
+ const invocationCwd=resolveInvocationCwd({env,cwd,lifecycle:true});
14
+ return await(deps.runCli??runCli)([],{...terminal.io,cwd:invocationCwd,env});
15
+ }catch(cause){(terminal?.io.error??error).write(`Error: ${cause.message}\n`);return 1;}
16
+ finally{terminal?.dispose?.();}
17
+ }
package/lib/cli/root.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import {renderCompletion} from './completion.mjs';
3
- const usage='Usage: arc [installation options]\n arc preset [economy|balanced|smart] --target <codex|claude|all>\n arc uninstall --target <codex|claude|all>\n arc completion <zsh|bash> (shell setup)\n arc --help | --version';
3
+ const usage='Usage: arc [installation options]\n arc preset [economy|balanced|smart] --target <codex|claude|all>\n arc uninstall --target <codex|claude|all>\n arc upgrade [--help]\n arc completion <zsh|bash> (shell setup)\n arc --help | --version';
4
4
  const completionUsage='Usage: arc completion <zsh|bash>';
5
5
  export async function runRoot(args=process.argv.slice(2),io={}){
6
6
  const output=io.output??process.stdout,error=io.error??process.stderr;
@@ -8,6 +8,7 @@ export async function runRoot(args=process.argv.slice(2),io={}){
8
8
  if(command==='--help'&&!rest.length){output.write(usage+'\n\n');return(await import('./index.mjs')).runCli(args,io);}
9
9
  if(command==='--version'&&!rest.length){output.write(JSON.parse(fs.readFileSync(new URL('../../package.json',import.meta.url))).version+'\n');return 0;}
10
10
  if(command==='preset')return(await import('./preset.mjs')).runCli(rest,io);
11
+ if(command==='upgrade')return(await import('./upgrade.mjs')).runCli(rest,io);
11
12
  if(command==='install'){error.write('Error: The install subcommand was removed. Run npx --yes @yunhozz/arc [installation options].\n');return 2;}
12
13
  if(command===undefined||command.startsWith('--'))return(await import('./index.mjs')).runCli(args,io);
13
14
  if(command==='uninstall')return(await import('./uninstall.mjs')).runCli(rest,io);
@@ -0,0 +1,19 @@
1
+ import fs from 'node:fs';
2
+ import tty from 'node:tty';
3
+ function openControllingTerminal(){
4
+ let inputFd,outputFd,input,output;
5
+ try{
6
+ inputFd=fs.openSync('/dev/tty','r+');outputFd=fs.openSync('/dev/tty','r+');
7
+ input=new tty.ReadStream(inputFd);inputFd=undefined;
8
+ output=new tty.WriteStream(outputFd);outputFd=undefined;
9
+ return{io:{input,output,error:output},dispose(){input.destroy();output.destroy();}};
10
+ }catch(cause){input?.destroy();output?.destroy();if(inputFd!==undefined)fs.closeSync(inputFd);if(outputFd!==undefined)fs.closeSync(outputFd);throw cause;}
11
+ }
12
+ export function openInstallerTerminal({platform=process.platform,env=process.env,input=process.stdin,output=process.stdout,error=process.stderr,openTty=openControllingTerminal}={}){
13
+ if(!['darwin','linux'].includes(platform))return{kind:'skip',reason:'unsupported platform'};
14
+ if(['CI','CONTINUOUS_INTEGRATION','BUILD_NUMBER','RUN_ID'].some(name=>env[name]&&env[name]!=='false'&&env[name]!=='0'))return{kind:'skip',reason:'non-interactive environment'};
15
+ let owned,io;
16
+ try{if(input.isTTY&&output.isTTY)io={input,output,error};else{owned=openTty();io=owned.io;}if(!io.input.isTTY||!io.output.isTTY)throw Error('Terminal is not interactive');}catch{owned?.dispose();return{kind:'skip',reason:'no interactive terminal'};}
17
+ const wasRaw=Boolean(io.input.isRaw),wasFlowing=io.input.readableFlowing;let disposed=false;
18
+ return{kind:'interactive',io,dispose(){if(disposed)return;disposed=true;try{if(io.input.setRawMode&&!io.input.destroyed)io.input.setRawMode(wasRaw);if(wasFlowing!==true)io.input.pause();}finally{owned?.dispose();}}};
19
+ }
@@ -0,0 +1,57 @@
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 usage='Usage: arc upgrade [--help]';
7
+ const inputError=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
8
+ const configurationError=message=>Object.assign(Error(message),{code:'ARC_TARGET_CONFIGURATION'});
9
+ 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;}
10
+ async function runtime(deps){
11
+ 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')]);
12
+ 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})};}
13
+ return{...context,...state,...upgrade,...npm,...deps,safeFs};
14
+ }
15
+ function report(result,output,error){
16
+ 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');}
17
+ if(result.interrupted)return 130;
18
+ if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write('ARC version '+result.version+' harness upgraded! Previous harness version: not recorded.\nOpen a new shell session to use updated completion.\nzsh completion: eval "$(arc completion zsh)" for this session\n');return 0;}
19
+ error.write('Harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return 1;
20
+ }
21
+ 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;}
22
+ export async function runCli(args=process.argv.slice(2),io={},deps={}){
23
+ const output=io.output??process.stdout,error=io.error??process.stderr;let controller,onInterrupt,packageStarted=false;
24
+ try{
25
+ if(args.length===1&&args[0]==='--help'){output.write(usage+'\nUpdates the global CLI and installed harness; npm exec updates the harness only.\n');return 0;}
26
+ if(args.length)throw inputError('Expected no arguments or --help');
27
+ const root=deps.packageRoot??packageRoot,pkg=identity(root),env=io.env??process.env,cwd=fs.realpathSync(io.cwd??process.cwd()),api=await runtime(deps);
28
+ const context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
29
+ if(!['global','npx'].includes(context.kind))throw configurationError('Run upgrade from a verified global ARC installation or npx --yes @yunhozz/arc@latest upgrade');
30
+ const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});
31
+ if(!state.targets.length)throw configurationError('No installed ARC harness found. Run npx --yes @yunhozz/arc to install it first');
32
+ controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
33
+ if(context.kind==='npx')return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
34
+ const latest=await api.resolveLatest({context,env,cwd,signal:controller.signal});packageStarted=true;
35
+ const replaced=await api.upgradeGlobal({context,expectedVersion:latest.version,env,cwd,signal:controller.signal});
36
+ if(replaced.packageStatus!=='updated')throw replaced.error??Error('CLI replacement failed');
37
+ output.write('CLI: updated to '+latest.version+' (previous '+pkg.version+').\n');
38
+ const result=await api.runUpgradeChild({entryPath:path.join(context.packageRoot,'bin/arc-upgrade.mjs'),request:{protocol:1,expectedVersion:latest.version,invocationCwd:cwd,expectedStateToken:state.stateToken},env,cwd,signal:controller.signal});
39
+ if(result.stdout)output.write(result.stdout);if(result.stderr)error.write(result.stderr);
40
+ if(result.code===0&&!result.signal)return 0;
41
+ error.write('CLI was updated but the harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return result.code===130?130:1;
42
+ }catch(cause){const code=failure(cause,error);if(cause.code==='ARC_CLI_INPUT')error.write(usage+'\n');if(packageStarted)error.write('CLI state may have changed. Retry arc upgrade; if it is unavailable, run npx --yes @yunhozz/arc@latest upgrade.\n');return code;}
43
+ finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
44
+ }
45
+ export async function runChild(request,io={},deps={}){
46
+ const output=io.output??process.stdout,error=io.error??process.stderr;let onInterrupt;
47
+ try{
48
+ 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');
49
+ 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');
50
+ 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});
51
+ if(context.kind!=='global'||context.packageRoot!==root)throw configurationError('Cannot verify the replaced global ARC package');
52
+ 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');
53
+ if(!state.targets.length)throw configurationError('No installed ARC harness found');
54
+ const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
55
+ return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
56
+ }catch(cause){return failure(cause,error);}finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
57
+ }
@@ -2,6 +2,7 @@ import {installClaudeHook} from '../policy/claude-hook.mjs';
2
2
  import {validateGitPolicy} from '../../claude/skill/hooks/role-git-guard.mjs';
3
3
  import{parseLanguage,serializeLanguage,renderLanguagePolicy,validateLanguage}from'./language.mjs';
4
4
  import{resolveLimits}from'../limits.mjs';import{parseLimits,serializeLimits}from'./limits.mjs';import{readRuntimeConcurrency,renderCodexConcurrency,renderClaudeConcurrency}from'./runtime-config.mjs';import{assertPlanSnapshot}from'./transaction.mjs';
5
+ import{validateModelConfig}from'../models/config.mjs';
5
6
  import path from'node:path';import{loadModelConfig,applyPreset,serializeModelConfig,renderProfiles,resolveModelSettings}from'../models/index.mjs';import{inspectPath,TargetConfigurationError}from'./targets.mjs';import{renderManagedBlock,renderWaitSettings,agentsBlockBody}from'./global-config.mjs';
6
7
  function fail(message){throw new TargetConfigurationError(message);}
7
8
  export function previewLimits({target,safeFs}){
@@ -16,7 +17,7 @@ export function previewLanguage({target,safeFs}){
16
17
  let existing=null;try{if(current.exists)existing=parseLanguage(current.bytes).language;}catch(cause){fail(`Invalid ARC language settings: ${cause.message}`);}
17
18
  return{existing,snapshot};
18
19
  }
19
- export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview}){
20
+ export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,limits,concurrentExplicit=false,limitsPreview,language='en',languagePreview,modelConfig}){
20
21
  validateLanguage(language);
21
22
  if(languagePreview)assertPlanSnapshot({snapshot:languagePreview.snapshot},{safeFs});
22
23
  const languageState=languagePreview??previewLanguage({target,safeFs});
@@ -24,7 +25,7 @@ export function buildInstallPlan({payload,target,preset,skipGlobal=false,safeFs,
24
25
  const preview=limitsPreview??previewLimits({target,safeFs});
25
26
  limits=resolveLimits({existing:preview.existing,overrides:limits??{}});
26
27
  const input=payload.modelInputs[target.platform];if(!input)fail('Selected payload platform is unavailable');
27
- const config=applyPreset(loadModelConfig(input.models,target.platform),preset),profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
28
+ const config=modelConfig===undefined?applyPreset(loadModelConfig(input.models,target.platform),preset):validateModelConfig(modelConfig);if(config.platform!==target.platform)fail('Model config platform does not match target');const profiles=renderProfiles(input.profiles,resolveModelSettings(config));for(const[name,bytes]of profiles)profiles.set(name,renderLanguagePolicy(bytes,language));const modelBytes=Buffer.from(serializeModelConfig(config));
28
29
  if(target.platform==='claude'){try{const script=payload.files.get('claude/skill/hooks/role-git-guard.mjs'),policy=payload.files.get('claude/skill/hooks/git-policy.json');if(!Buffer.isBuffer(script)||!script.length||!Buffer.isBuffer(policy))throw Error('Missing hook assets');validateGitPolicy(JSON.parse(policy.toString('utf8')));for(const[name,bytes]of profiles)profiles.set(name,installClaudeHook(bytes,path.join(target.home,'skills/arc')));}catch(cause){fail(`Invalid Claude Git guard: ${cause.message}`);}}
29
30
  const skill=new Map();for(const[name,bytes]of payload.files)if(name.startsWith(target.payloadPrefix+'/skill/'))skill.set(name.slice((target.payloadPrefix+'/skill/').length),Buffer.from(bytes));skill.set('SKILL.md',renderLanguagePolicy(skill.get('SKILL.md'),language));skill.set('settings.toml',serializeLanguage(language));skill.set('models.toml',modelBytes);skill.set('limits.toml',serializeLimits(limits));
30
31
  const snapshot=new Map(),home=target.home;const skills=path.join(home,'skills'),agents=path.join(home,'agents'),arc=path.join(skills,'arc');
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ import {createHash} from 'node:crypto';
3
+ import {resolveTarget,inspectPath,TargetConfigurationError} from './targets.mjs';
4
+ import {loadModelConfig} from '../models/index.mjs';
5
+ import {parseLanguage} from './language.mjs';
6
+ import {parseLimits} from './limits.mjs';
7
+ import {removeManagedBlock} from './global-config.mjs';
8
+ import {renderShellCompletion} from './shell-completion.mjs';
9
+ import {readRuntimeConcurrency} from './runtime-config.mjs';
10
+ const fail=message=>{throw new TargetConfigurationError(message);};
11
+ export function collectInstalledState({safeFs,userHome,env={},cwd}){
12
+ if(env.CODEX_HOME!==undefined&&!path.isAbsolute(env.CODEX_HOME)&&env.CODEX_HOME!=='~'&&!env.CODEX_HOME.startsWith('~/')&&(!cwd||!path.isAbsolute(cwd)))fail('A verified invocation cwd is required for relative CODEX_HOME');
13
+ const targets=[];
14
+ for(const platform of ['codex','claude']){
15
+ const target=resolveTarget(platform,{userHome,env,cwd}),snapshot=new Map(),arc=path.join(target.home,'skills/arc');
16
+ const skill=inspectPath(safeFs,arc,{kind:'directory',tree:true,allowLinks:true,snapshot});
17
+ const profiles=target.profileNames.map(name=>inspectPath(safeFs,path.join(target.home,'agents',name),{kind:'file',read:true,snapshot}));
18
+ const instruction=inspectPath(safeFs,target.instructionPath,{kind:'file',read:true,snapshot}),managed=instruction.exists?removeManagedBlock(instruction.bytes):{changed:false};
19
+ if(!skill.exists){if(profiles.some(item=>item.exists)||managed.changed)fail(`Incomplete ARC installation: ${target.home}`);continue;}
20
+ function mandatory(name){const found=inspectPath(safeFs,path.join(arc,name),{kind:'file',read:true,snapshot});if(!found.exists||!found.bytes.length)fail(`Incomplete ARC installation: missing ${name} at ${target.home}`);return found.bytes;}
21
+ mandatory('SKILL.md');if(profiles.some(item=>!item.exists||!item.bytes.length))fail(`Incomplete ARC installation: missing profile at ${target.home}`);
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
+ 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)renderShellCompletion(shell.bytes);
25
+ targets.push({target,language,modelConfig,limits,snapshot});
26
+ }
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));}
28
+ return{targets,stateToken:hash.digest('hex')};
29
+ }
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import {buildInstallPlan,validatePlanSet} from './plan.mjs';
5
+ import {assertPlanSnapshot} from './transaction.mjs';
6
+ import {inspectPath} from './targets.mjs';
7
+ import {renderManagedBlock,removeManagedBlock,agentsBlockBody} from './global-config.mjs';
8
+ import {renderShellCompletion} from './shell-completion.mjs';
9
+ import {verifyPayload} from './manifest.mjs';
10
+ import {installTargets} from './install.mjs';
11
+ import {acquireExternalSkills} from './acquisition.mjs';
12
+ export function buildUpgradePlans({payload,state,safeFs}){
13
+ for(const entry of state.targets)assertPlanSnapshot(entry,{safeFs});
14
+ const plans=state.targets.map(entry=>{
15
+ const {target,language,modelConfig,limits}=entry;
16
+ const plan=buildInstallPlan({payload,target,language,modelConfig,preset:modelConfig.preset,limits,skipGlobal:true,safeFs});
17
+ for(const [name,value]of entry.snapshot)plan.snapshot.set(name,value);
18
+ const current=inspectPath(safeFs,target.instructionPath,{kind:'file',read:true,snapshot:plan.snapshot});
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
+ return plan;
21
+ });
22
+ const first=plans[0];if(first){const filename=path.join(first.target.userHome,'.zshrc'),current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot:first.snapshot});
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
+ }
26
+ validatePlanSet(plans);return plans;
27
+ }
28
+ export async function upgradeInstalledTargets({packageRoot,state,safeFs,signal,acquisition}){
29
+ const payload=verifyPayload({packageRoot,safeFs}),version=JSON.parse(payload.files.get('package.json').toString('utf8')).version,plans=buildUpgradePlans({payload,state,safeFs});
30
+ const acquire=acquisition??(async()=>{const work=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-acquire-'));let acquired;
31
+ try{acquired=await acquireExternalSkills({work,safeFs,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;}
32
+ return{...acquired,dispose:async()=>{await acquired.dispose();fs.rmdirSync(work);}};});
33
+ return{version,...await installTargets(plans,{safeFs,signal,acquisition:acquire})};
34
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {runNpm as defaultRunNpm} from './process.mjs';
4
+ const unsupported=reason=>({kind:'unsupported',reason});
5
+ function realDirectory(value){if(typeof value!=='string'||!path.isAbsolute(value))throw Error('Expected an absolute directory');const real=fs.realpathSync(value);if(!fs.statSync(real).isDirectory())throw Error('Expected a directory');return real;}
6
+ function npmExecutable(env){
7
+ const candidates=env.npm_execpath?[env.npm_execpath]:(env.PATH??'').split(path.delimiter).filter(p=>path.isAbsolute(p)).map(p=>path.join(p,'npm'));
8
+ for(const candidate of candidates)try{if(!path.isAbsolute(candidate))continue;const cli=fs.realpathSync(candidate),root=path.dirname(path.dirname(cli));if(path.basename(cli)==='npm-cli.js'&&path.basename(path.dirname(cli))==='bin'&&JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8')).name==='npm'&&fs.statSync(cli).isFile())return cli;}catch{}
9
+ throw Error('Cannot verify npm CLI');
10
+ }
11
+ export function resolveInvocationCwd({env={},cwd,lifecycle=false}){
12
+ if(lifecycle){
13
+ if(env.INIT_CWD!==undefined)return realDirectory(env.INIT_CWD);
14
+ if(env.CODEX_HOME!==undefined&&!path.isAbsolute(env.CODEX_HOME)&&env.CODEX_HOME!=='~'&&!env.CODEX_HOME.startsWith('~/'))throw Error('Original invocation directory (INIT_CWD) is required for relative CODEX_HOME');
15
+ }
16
+ return realDirectory(cwd);
17
+ }
18
+ export async function inspectNpmContext({packageRoot,entryPath,env=process.env,cwd=process.cwd(),execPath=process.execPath,runNpm=defaultRunNpm}){
19
+ try{
20
+ if(fs.realpathSync(execPath)!==fs.realpathSync(process.execPath))return unsupported('Node executable does not match this runtime');
21
+ const root=realDirectory(packageRoot),entry=fs.realpathSync(entryPath);
22
+ if(!entry.startsWith(root+path.sep)||!fs.statSync(entry).isFile()||JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8')).name!=='@yunhozz/arc')return unsupported('ARC entry is outside the package');
23
+ const npmCli=npmExecutable(env);
24
+ async function query(args){const result=await runNpm(npmCli,args,{cwd,env});if(result.code!==0||result.signal)throw Error('npm context query failed');const value=result.stdout.trim();if(!path.isAbsolute(value)||value.includes('\n'))throw Error('npm returned an invalid context path');try{return fs.realpathSync(value);}catch(error){if(error.code!=='ENOENT')throw error;return path.resolve(value);}}
25
+ const prefix=await query(['prefix','--global']),npmRoot=await query(['root','--global']);
26
+ if([path.join(prefix,'lib/node_modules'),path.join(prefix,'node_modules')].includes(npmRoot)&&root===path.join(npmRoot,'@yunhozz/arc'))return{kind:'global',packageRoot:root,entryPath:entry,prefix,npmCli};
27
+ if(env.npm_command==='exec'){
28
+ const cache=realDirectory(await query(['config','get','cache'])),relative=path.relative(path.join(cache,'_npx'),root),parts=relative.split(path.sep);
29
+ if(parts.length===4&&parts[0]&&parts[0]!=='.'&&parts[0]!=='..'&&parts.slice(1).join('/')==='node_modules/@yunhozz/arc'){
30
+ const bin=path.join(cache,'_npx',parts[0],'node_modules/.bin'),paths=(env.PATH??'').split(path.delimiter);
31
+ if(paths.some(value=>{try{return realDirectory(value)===realDirectory(bin);}catch{return false;}})&&realDirectory(env.INIT_CWD))return{kind:'npx',packageRoot:root,entryPath:entry,npmCli};
32
+ }
33
+ }
34
+ return unsupported('ARC is not in a verified global or npm exec installation');
35
+ }catch{return unsupported('Cannot verify npm installation context');}
36
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawn} from 'node:child_process';
4
+ function failure(message,code,cause){return Object.assign(new Error(message,{cause}),{code});}
5
+ export async function runNpm(npmCli,args,{cwd,env,signal}={}){
6
+ if(signal?.aborted)throw failure('npm cancelled','ARC_NPM_CANCELLED');
7
+ if(!path.isAbsolute(npmCli)||!Array.isArray(args)||args.some(arg=>typeof arg!=='string'))throw failure('Invalid npm invocation','ARC_NPM_START');
8
+ let cli;try{cli=fs.realpathSync(npmCli);if(!fs.statSync(cli).isFile())throw Error('Not a regular file');}catch(cause){throw failure('Cannot resolve npm CLI','ARC_NPM_START',cause);}
9
+ return new Promise((resolve,reject)=>{
10
+ const child=spawn(process.execPath,[cli,...args],{cwd,env,stdio:['ignore','pipe','pipe'],shell:false});
11
+ const stdout=[],stderr=[];let startError,cancelled=false;
12
+ child.stdout.on('data',bytes=>stdout.push(bytes));child.stderr.on('data',bytes=>stderr.push(bytes));
13
+ const abort=()=>{cancelled=true;child.kill('SIGTERM');};signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();
14
+ child.once('error',error=>{startError=error;});
15
+ child.once('close',(code,killedBy)=>{signal?.removeEventListener('abort',abort);if(cancelled)reject(failure('npm cancelled','ARC_NPM_CANCELLED'));else if(startError)reject(failure('Cannot start npm','ARC_NPM_START',startError));else resolve({code,signal:killedBy,stdout:Buffer.concat(stdout).toString('utf8'),stderr:Buffer.concat(stderr).toString('utf8')});});
16
+ });
17
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawn} from 'node:child_process';
4
+ import {runNpm as defaultRunNpm} from './process.mjs';
5
+ const validVersion=value=>typeof value==='string'&&/^\d+\.\d+\.\d+$/.test(value);
6
+ function cancelled(){return Object.assign(Error('Upgrade cancelled'),{code:'ARC_CANCELLED'});}
7
+ export async function resolveLatest({context,env,cwd,signal,runNpm=defaultRunNpm}){
8
+ const result=await runNpm(context.npmCli,['view','@yunhozz/arc@latest','--json'],{env,cwd,signal});
9
+ let value;try{value=JSON.parse(result.stdout);}catch{}
10
+ if(result.code!==0||result.signal||value?.name!=='@yunhozz/arc'||!validVersion(value.version))throw Error('Cannot resolve a valid latest ARC package');
11
+ return{name:'@yunhozz/arc',version:value.version};
12
+ }
13
+ export async function upgradeGlobal({context,expectedVersion,env,cwd,signal,runNpm=defaultRunNpm}){
14
+ if(context.kind!=='global'||!path.isAbsolute(context.prefix)||!validVersion(expectedVersion))throw Error('Invalid global upgrade version or prefix');
15
+ const result=await runNpm(context.npmCli,['install','--global','@yunhozz/arc@'+expectedVersion,'--prefix',context.prefix,'--ignore-scripts'],{env,cwd,signal});
16
+ return result.code===0&&!result.signal?{packageStatus:'updated',version:expectedVersion}:{packageStatus:'failed',error:Error('CLI replacement failed; the package may have changed')};
17
+ }
18
+ export async function runUpgradeChild({entryPath,request,env,cwd,signal}){
19
+ if(signal?.aborted)throw cancelled();
20
+ if(!path.isAbsolute(entryPath))throw Error('Invalid upgrade child path');
21
+ const entry=fs.realpathSync(entryPath);if(!fs.statSync(entry).isFile())throw Error('Invalid upgrade child');
22
+ return new Promise((resolve,reject)=>{
23
+ const child=spawn(process.execPath,[entry],{cwd,env,stdio:['pipe','pipe','pipe'],shell:false});let stdout='',stderr='',startError,aborted=false;
24
+ child.stdout.on('data',bytes=>{stdout+=bytes;});child.stderr.on('data',bytes=>{stderr+=bytes;});child.stdin.on('error',()=>{});
25
+ const abort=()=>{aborted=true;child.kill('SIGTERM');};signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();
26
+ child.once('error',error=>{startError=error;});child.once('close',(code,killedBy)=>{signal?.removeEventListener('abort',abort);if(aborted)resolve({code:130,signal:killedBy,stdout,stderr});else if(startError)reject(Error('Cannot start the new ARC CLI',{cause:startError}));else resolve({code,signal:killedBy,stdout,stderr});});
27
+ child.stdin.end(JSON.stringify(request));
28
+ });
29
+ }
@@ -0,0 +1,163 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import {gunzipSync} from 'node:zlib';
6
+ import {isDeepStrictEqual} from 'node:util';
7
+ import {spawnSync} from 'node:child_process';
8
+ import {fileURLToPath} from 'node:url';
9
+ import {openRoot} from '../safe-fs/index.mjs';
10
+ import {verifyPayload} from '../installer/manifest.mjs';
11
+ import {readCheckedFile,typeOf,sameIdentity} from '../installer/targets.mjs';
12
+ import {runNpm} from '../npm/process.mjs';
13
+
14
+ const HOOKS=['preinstall','install','postinstall','prepublish','prepare','prepublishOnly','prepack','postpack','publish','postpublish','preuninstall','uninstall','postuninstall'];
15
+ const NAME='@yunhozz/arc',HOOK='node scripts/postinstall.mjs';
16
+ const same=(a,b)=>a.dev===b.dev&&a.ino===b.ino;
17
+ const digest=(bytes,algorithm='sha256')=>crypto.createHash(algorithm).update(bytes).digest('hex');
18
+ const json=file=>JSON.parse(fs.readFileSync(file,'utf8'));
19
+ function abort(signal){if(signal?.aborted)throw new Error('Release artifact generation cancelled');}
20
+ function inside(parent,child){const relative=path.relative(parent,child);return relative===''||(!relative.startsWith('..'+path.sep)&&relative!=='..'&&!path.isAbsolute(relative));}
21
+ function regular(file){const stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink())throw new Error('Expected regular release input: '+file);return stat;}
22
+ function metadata(pkg,lock,{artifact=false}={}){
23
+ if(pkg.name!==NAME||!/^\d+\.\d+\.\d+$/.test(pkg.version)||pkg.private===true)throw new Error('Invalid release package identity');
24
+ for(const hook of HOOKS)if(pkg.scripts?.[hook]!==undefined&&!(artifact&&hook==='postinstall'&&pkg.scripts[hook]===HOOK))throw new Error('Unapproved package lifecycle: '+hook);
25
+ if(artifact&&pkg.scripts?.postinstall!==HOOK)throw new Error('Artifact postinstall is missing');
26
+ if(Object.hasOwn(pkg.scripts??{},'build')||Object.hasOwn(pkg,'workspaces'))throw new Error('Source Git preparation trigger is not allowed');
27
+ if(lock){
28
+ const root=lock.packages?.[''];if(lock.name!==pkg.name||lock.version!==pkg.version||root?.name!==pkg.name||root?.version!==pkg.version)throw new Error('Package lock identity mismatch');
29
+ for(const key of ['bin','dependencies','optionalDependencies','devDependencies','peerDependencies','engines'])if(!isDeepStrictEqual(root[key],pkg[key]))throw new Error('Package lock metadata mismatch: '+key);
30
+ if(artifact?root.hasInstallScript!==true:Object.hasOwn(root,'hasInstallScript'))throw new Error('Package lock install script metadata mismatch');
31
+ }
32
+ }
33
+ function inputNames(pkg){const names=new Set(['README.md','package.json','package-lock.json','MANIFEST.sha256']);for(const entry of pkg.files??[]){const name=entry.replace(/\/$/,'');if(!name||name.startsWith('/')||name.includes('\\')||name.split('/').some(part=>!part||part==='.'||part==='..')||/[*?\[\]]/.test(name))throw new Error('Unsupported package files entry');names.add(name);}return [...names].sort();}
34
+ function sourceMetadata(root){
35
+ const directory=openRoot(root);
36
+ try{const result={};for(const name of ['package.json','package-lock.json']){const stat=directory.lstat(name);if(typeOf(stat)!==0o100000)throw new Error('Expected regular source metadata: '+name);result[name]=readCheckedFile(directory,name,stat).bytes;}return result;}
37
+ finally{directory.close();}
38
+ }
39
+ function inventory(root,pkg){
40
+ const files=new Map(),directory=openRoot(root);
41
+ function visit(parent,name,relative){
42
+ const stat=parent.lstat(name),type=typeOf(stat);
43
+ if(type===0o040000){
44
+ const child=parent.openDir(name);try{if(!sameIdentity(stat,child.stat()))throw new Error('Release source directory changed');
45
+ 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');
46
+ }finally{child.close();}
47
+ }else if(type===0o100000){const item=readCheckedFile(parent,name,stat);files.set(relative,{bytes:item.bytes,mode:Number(stat.mode&0o777n)});}
48
+ else throw new Error('Unsafe release source file: '+relative);
49
+ }
50
+ try{
51
+ for(const name of inputNames(pkg)){
52
+ const parts=name.split('/'),opened=[];let parent=directory;
53
+ try{for(const part of parts.slice(0,-1)){const stat=parent.lstat(part);if(typeOf(stat)!==0o040000)throw new Error('Unsafe release source parent');const child=parent.openDir(part);opened.push(child);if(!sameIdentity(stat,child.stat()))throw new Error('Release source parent changed');parent=child;}visit(parent,parts.at(-1),name);}
54
+ finally{for(const child of opened.reverse())child.close();}
55
+ }
56
+ }finally{directory.close();}
57
+ const snapshot=digest([...files].sort(([a],[b])=>a<b?-1:a>b?1:0).map(([name,{bytes,mode}])=>name+'\0'+mode+'\0'+bytes.length+'\0'+digest(bytes)+'\n').join(''));
58
+ return{files,snapshot};
59
+ }
60
+ function refresh(root){
61
+ const result=spawnSync(process.execPath,[path.join(root,'scripts/update-manifest.mjs'),'--root',root],{encoding:'utf8',env:{PATH:process.env.PATH}});
62
+ if(result.status!==0)throw new Error('Staging manifest generation failed: '+result.stderr);
63
+ }
64
+ function safeOutput(sourceRoot,outDir){
65
+ if(!path.isAbsolute(outDir)||path.normalize(outDir)!==outDir||inside(sourceRoot,outDir))throw new Error('Release output must be an absolute directory outside source');
66
+ let current=path.parse(outDir).root;for(const component of outDir.slice(current.length).split(path.sep).filter(Boolean)){
67
+ current=path.join(current,component);try{const stat=fs.lstatSync(current);if(stat.isSymbolicLink()||!stat.isDirectory())throw new Error('Unsafe release output path');}catch(error){if(error.code!=='ENOENT')throw error;if(current!==outDir)throw new Error('Release output parent is missing');}
68
+ }
69
+ const parent=fs.realpathSync(path.dirname(outDir));if(inside(sourceRoot,parent))throw new Error('Release output resolves inside source');
70
+ try{fs.mkdirSync(outDir,{mode:0o700});}catch(error){if(error.code!=='EEXIST')throw error;}
71
+ if(fs.readdirSync(outDir).length)throw new Error('Release output is occupied or incomplete; use a new output directory');
72
+ return openRoot(outDir);
73
+ }
74
+ function readText(bytes,start,length){return bytes.subarray(start,start+length).toString('utf8').replace(/\0.*$/s,'');}
75
+ function number(bytes,start,length){const text=readText(bytes,start,length).trim();if(!/^[0-7]*$/.test(text))throw new Error('Unsupported tar number');const value=parseInt(text||'0',8);if(!Number.isSafeInteger(value))throw new Error('Invalid tar size');return value;}
76
+ function pax(bytes){
77
+ const result={};let offset=0;while(offset<bytes.length){const space=bytes.indexOf(32,offset);if(space<0)throw new Error('Malformed tar metadata');const size=Number(bytes.subarray(offset,space).toString());if(!Number.isSafeInteger(size)||size<=space-offset+1||offset+size>bytes.length)throw new Error('Malformed tar metadata');const record=bytes.subarray(space+1,offset+size).toString('utf8');if(!record.endsWith('\n'))throw new Error('Malformed tar metadata');const equal=record.indexOf('=');if(equal<1)throw new Error('Malformed tar metadata');result[record.slice(0,equal)]=record.slice(equal+1,-1);offset+=size;}return result;
78
+ }
79
+ function archive(bytes){
80
+ const raw=gunzipSync(bytes,{maxOutputLength:256*1024*1024}),files=new Map();let extended={},global={};
81
+ for(let offset=0;offset+512<=raw.length;){
82
+ const header=raw.subarray(offset,offset+512);if(header.every(byte=>byte===0))break;
83
+ let sum=0;for(let i=0;i<512;i++)sum+=i>=148&&i<156?32:header[i];if(sum!==number(header,148,8))throw new Error('Invalid tar header checksum');
84
+ const size=number(header,124,12),type=String.fromCharCode(header[156]||48),end=offset+512+size;if(end>raw.length)throw new Error('Truncated tar archive');
85
+ const data=raw.subarray(offset+512,end);offset+=512+Math.ceil(size/512)*512;
86
+ if(type==='x'||type==='g'){const values=pax(data);if(type==='g')global={...global,...values};else extended=values;continue;}
87
+ const attrs={...global,...extended};extended={};if(attrs.linkpath!==undefined)throw new Error('Archive links are forbidden');
88
+ let name=attrs.path??[readText(header,345,155),readText(header,0,100)].filter(Boolean).join('/');
89
+ if(name.endsWith('/'))name=name.slice(0,-1);
90
+ if(name!=='package'&&(!name.startsWith('package/')||name.split('/').some(part=>!part||part==='.'||part==='..')||name.includes('\\')||name.includes('\0')))throw new Error('Unsafe archive path');
91
+ if(type==='5')continue;if(type!=='0')throw new Error('Archive links and special files are forbidden');
92
+ if(name==='package'||files.has(name))throw new Error('Duplicate archive path');
93
+ if(attrs.size!==undefined&&Number(attrs.size)!==size)throw new Error('Unsupported tar size override');
94
+ files.set(name.slice(8),{bytes:Buffer.from(data),mode:number(header,100,8)&0o777});
95
+ }
96
+ if(!files.size)throw new Error('Empty release archive');return files;
97
+ }
98
+ function validateTarball(bytes,{expectedName,expectedVersion}){
99
+ const files=archive(bytes),pkgFile=files.get('package.json');if(!pkgFile)throw new Error('Artifact package metadata is missing');
100
+ const pkg=JSON.parse(pkgFile.bytes);metadata(pkg,undefined,{artifact:true});
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/cli/terminal.mjs'])if(!files.has(name))throw new Error('Artifact runtime asset missing: '+name);
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
+ const root=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-release-verify-'));
105
+ try{
106
+ 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}});
108
+ }finally{fs.rmSync(root,{recursive:true,force:true});}
109
+ return pkg;
110
+ }
111
+ async function pack({stagingRoot,workRoot,npmCli,signal}){
112
+ const packDir=path.join(workRoot,'packed'),home=path.join(workRoot,'home');fs.mkdirSync(packDir);fs.mkdirSync(home);
113
+ const env={PATH:process.env.PATH,HOME:home,npm_config_userconfig:path.join(home,'user.npmrc'),npm_config_globalconfig:path.join(home,'global.npmrc'),npm_config_cache:path.join(workRoot,'cache'),npm_config_offline:'true',npm_config_ignore_scripts:'true',npm_config_audit:'false',npm_config_fund:'false',npm_config_update_notifier:'false'};
114
+ const result=await runNpm(npmCli,['pack','--ignore-scripts','--offline','--json','--pack-destination',packDir],{cwd:stagingRoot,env,signal});
115
+ if(result.code!==0)throw new Error('Release npm pack failed: '+result.stderr);
116
+ const records=JSON.parse(result.stdout),record=Array.isArray(records)?records[0]:records[NAME];
117
+ if(!record||typeof record.filename!=='string'||path.basename(record.filename)!==record.filename)throw new Error('Invalid npm pack result');
118
+ const filename=path.join(packDir,record.filename);regular(filename);return fs.readFileSync(filename);
119
+ }
120
+ export async function buildReleaseArtifact({sourceRoot,outDir,npmCli,signal,deps={}}){
121
+ if(!path.isAbsolute(sourceRoot)||fs.realpathSync(sourceRoot)!==sourceRoot)throw new Error('Source root must be an absolute real path');
122
+ abort(signal);const inputs=sourceMetadata(sourceRoot),pkg=JSON.parse(inputs['package.json']),lock=JSON.parse(inputs['package-lock.json']);metadata(pkg,lock);
123
+ 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');
125
+ let output,lockIdentity,workRoot,workIdentity;const id=crypto.randomUUID(),partial='.arc-release-'+id+'.partial';let partialExists=false;
126
+ try{
127
+ 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();}
128
+ workRoot=fs.mkdtempSync(path.join(fs.realpathSync(path.dirname(outDir)),'.arc-release-stage-'));workIdentity=fs.lstatSync(workRoot);
129
+ const stagingRoot=path.join(workRoot,'package');fs.mkdirSync(stagingRoot,{mode:0o700});
130
+ for(const[name,item]of source.files){const filename=path.join(stagingRoot,name);fs.mkdirSync(path.dirname(filename),{recursive:true,mode:0o700});fs.writeFileSync(filename,item.bytes,{flag:'wx',mode:item.mode});}
131
+ const stagedPkg={...pkg,scripts:{...pkg.scripts,postinstall:HOOK}},stagedLock=structuredClone(lock);stagedLock.packages[''].hasInstallScript=true;
132
+ 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
+ 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);
135
+ const bytes=await(deps.pack??pack)({stagingRoot,workRoot,npmCli,signal});abort(signal);validateTarball(bytes,{expectedName:pkg.name,expectedVersion:pkg.version});
136
+ if(inventory(sourceRoot,pkg).snapshot!==source.snapshot)throw new Error('Release source snapshot changed during generation');abort(signal);
137
+ const tarballPath=path.join(outDir,'arc-release.tgz'),receiptPath=path.join(outDir,'arc-release.json');
138
+ const receipt={schema:1,name:pkg.name,version:pkg.version,tarballPath,sha512:digest(bytes,'sha512'),sourceSnapshot:source.snapshot,verified:true};
139
+ const file=output.openFile(partial,{createExclusive:true,read:false});partialExists=true;try{file.writeAll(bytes);file.sync();}finally{file.close();}
140
+ output.renameNoReplaceTo(partial,output,'arc-release.tgz');partialExists=false;
141
+ const receiptFile=output.openFile(partial,{createExclusive:true,read:false});partialExists=true;try{receiptFile.writeAll(JSON.stringify(receipt,null,2)+'\n');receiptFile.sync();}finally{receiptFile.close();}
142
+ output.renameNoReplaceTo(partial,output,'arc-release.json');partialExists=false;output.sync();
143
+ if(!same(output.stat(),fs.lstatSync(outDir,{bigint:true}))||fs.lstatSync(outDir).isSymbolicLink())throw new Error('Release output directory changed');
144
+ return{...receipt,receiptPath};
145
+ }finally{
146
+ if(workRoot){try{if(same(workIdentity,fs.lstatSync(workRoot)))fs.rmSync(workRoot,{recursive:true,force:true});}catch(error){if(error.code!=='ENOENT')throw error;}}
147
+ if(output){try{if(partialExists)output.unlink(partial);if(lockIdentity&&same(lockIdentity,output.lstat('.arc-release.lock')))output.unlink('.arc-release.lock');}finally{output.close();}}
148
+ }
149
+ }
150
+ export async function verifyReleaseArtifact({outDir,expectedName,expectedVersion}){
151
+ if(!path.isAbsolute(outDir)||fs.realpathSync(outDir)!==outDir)throw new Error('Unsafe release output path');
152
+ if(fs.readdirSync(outDir).includes('.arc-release.lock'))throw new Error('Release output is locked or incomplete');
153
+ regular(path.join(outDir,'arc-release.json'));const receipt=json(path.join(outDir,'arc-release.json'));
154
+ if(receipt.schema!==1||receipt.verified!==true||receipt.name!==expectedName||receipt.version!==expectedVersion||receipt.tarballPath!==path.join(outDir,'arc-release.tgz')||!/^[a-f0-9]{128}$/.test(receipt.sha512)||!/^[a-f0-9]{64}$/.test(receipt.sourceSnapshot))throw new Error('Invalid release receipt');
155
+ regular(receipt.tarballPath);const bytes=fs.readFileSync(receipt.tarballPath);if(digest(bytes,'sha512')!==receipt.sha512)throw new Error('Release tarball digest mismatch');
156
+ validateTarball(bytes,{expectedName,expectedVersion});return receipt;
157
+ }
158
+ export function resolveNpmCli(){
159
+ const executable=process.env.npm_execpath;if(executable&&path.isAbsolute(executable)&&fs.existsSync(executable))return fs.realpathSync(executable);
160
+ const candidate=path.resolve(path.dirname(process.execPath),'../lib/node_modules/npm/bin/npm-cli.js');
161
+ if(fs.existsSync(candidate))return fs.realpathSync(candidate);throw new Error('Cannot resolve npm CLI; use npm run pack:release');
162
+ }
163
+ export const releaseSourceRoot=path.resolve(fileURLToPath(new URL('../../',import.meta.url)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunhozz/arc",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -14,7 +14,8 @@
14
14
  "build:native": "node scripts/build-native.mjs",
15
15
  "check:manifest": "node scripts/update-manifest.mjs --check",
16
16
  "update:role-policy": "node scripts/update-role-policy.mjs",
17
- "check:role-policy": "node scripts/update-role-policy.mjs --check"
17
+ "check:role-policy": "node scripts/update-role-policy.mjs --check",
18
+ "pack:release": "node scripts/pack-release.mjs"
18
19
  },
19
20
  "devDependencies": {
20
21
  "node-gyp": "11.5.0",
@@ -0,0 +1,9 @@
1
+ import {buildReleaseArtifact,resolveNpmCli,releaseSourceRoot} from '../lib/release/artifact.mjs';
2
+ const args=process.argv.slice(2),controller=new AbortController();
3
+ const cancel=()=>controller.abort();process.once('SIGINT',cancel);process.once('SIGTERM',cancel);
4
+ try{
5
+ if(args.length!==2||args[0]!=='--out-dir')throw Error('Usage: npm run pack:release -- --out-dir <absolute-output-directory>');
6
+ const result=await buildReleaseArtifact({sourceRoot:releaseSourceRoot,outDir:args[1],npmCli:resolveNpmCli(),signal:controller.signal});
7
+ console.log(JSON.stringify(result,null,2));
8
+ }catch(error){console.error('Release artifact error: '+error.message);process.exitCode=controller.signal.aborted?130:1;}
9
+ finally{process.removeListener('SIGINT',cancel);process.removeListener('SIGTERM',cancel);}
@@ -0,0 +1,4 @@
1
+ import {fileURLToPath} from 'node:url';
2
+ import {runPostinstall} from '../lib/cli/postinstall.mjs';
3
+ const packageRoot=fileURLToPath(new URL('../',import.meta.url));
4
+ process.exitCode=await runPostinstall({packageRoot,cwd:packageRoot});
@@ -0,0 +1,6 @@
1
+ import {verifyReleaseArtifact} from '../lib/release/artifact.mjs';
2
+ const args=process.argv.slice(2);
3
+ try{
4
+ if(args.length!==6||args[0]!=='--out-dir'||args[2]!=='--name'||args[4]!=='--version')throw Error('Usage: node scripts/verify-release.mjs --out-dir <absolute-output-directory> --name <package-name> --version <version>');
5
+ console.log(JSON.stringify(await verifyReleaseArtifact({outDir:args[1],expectedName:args[3],expectedVersion:args[5]}),null,2));
6
+ }catch(error){console.error('Release verification error: '+error.message);process.exitCode=1;}