@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 +20 -7
- package/README.md +11 -0
- package/bin/arc-upgrade.mjs +3 -0
- package/lib/cli/choices.mjs +4 -4
- package/lib/cli/completion.mjs +10 -8
- package/lib/cli/index.mjs +5 -5
- package/lib/cli/postinstall.mjs +17 -0
- package/lib/cli/root.mjs +2 -1
- package/lib/cli/terminal.mjs +19 -0
- package/lib/cli/upgrade.mjs +57 -0
- package/lib/installer/plan.mjs +3 -2
- package/lib/installer/upgrade-state.mjs +29 -0
- package/lib/installer/upgrade.mjs +34 -0
- package/lib/npm/context.mjs +36 -0
- package/lib/npm/process.mjs +17 -0
- package/lib/npm/upgrade.mjs +29 -0
- package/lib/release/artifact.mjs +163 -0
- package/package.json +3 -2
- package/scripts/pack-release.mjs +9 -0
- package/scripts/postinstall.mjs +4 -0
- package/scripts/verify-release.mjs +6 -0
package/MANIFEST.sha256
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;}
|
package/lib/cli/choices.mjs
CHANGED
|
@@ -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
|
}
|
package/lib/cli/completion.mjs
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/lib/installer/plan.mjs
CHANGED
|
@@ -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)
|
|
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.
|
|
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,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;}
|