@lardexx/arenacode 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -22
- package/package.json +8 -4
- package/src/automation/ffi.js +62 -0
- package/src/automation/linux.js +771 -0
- package/src/automation/mac.js +657 -0
- package/src/automation/png.js +47 -0
- package/src/automation/win-dll.js +493 -0
- package/src/automation.js +88 -0
- package/src/cli.js +5 -2
- package/src/config.js +1 -0
- package/src/local.js +106 -11
- package/src/tui/app.js +40 -2
- package/vendor/windows-use/windows-use.dll +0 -0
package/src/local.js
CHANGED
|
@@ -48,22 +48,110 @@ function relativize(root, abs) {
|
|
|
48
48
|
// Local terminal (local-exec)
|
|
49
49
|
// --------------------------------------------------------------------------- //
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Every shell the client can drive. The bridge may ask for one by name (`shell` in the
|
|
52
|
+
// local-exec request), the user can pin one (`arenacode config set shell zsh`, `/shell`),
|
|
53
|
+
// and otherwise the client takes the user's login shell, like a terminal would.
|
|
54
|
+
const UNIX_SHELLS = {
|
|
55
|
+
sh: '/bin/sh',
|
|
56
|
+
bash: '/bin/bash',
|
|
57
|
+
zsh: '/bin/zsh',
|
|
58
|
+
fish: '/usr/bin/fish',
|
|
59
|
+
dash: '/bin/dash',
|
|
60
|
+
ksh: '/bin/ksh',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const WINDOWS_SHELLS = ['powershell', 'pwsh', 'cmd', 'wsl', 'bash'];
|
|
64
|
+
|
|
65
|
+
function shellExists(file) {
|
|
66
|
+
try {
|
|
67
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** sh/bash/zsh/dash/ksh: -l login, -c command. fish spells the same as -l -c. */
|
|
75
|
+
function unixShell(name) {
|
|
76
|
+
const wanted = String(name || '').toLowerCase().replace(/\.exe$/, '');
|
|
77
|
+
const alias = { 'git-bash': 'bash', gitbash: 'bash' }[wanted] || wanted;
|
|
78
|
+
let file = UNIX_SHELLS[alias];
|
|
79
|
+
if (!file && (alias === 'sh' || alias.startsWith('/'))) file = alias === 'sh' ? '/bin/sh' : alias;
|
|
80
|
+
if (!file) file = whichPath(alias);
|
|
81
|
+
if (!file) file = UNIX_SHELLS[alias] || null;
|
|
82
|
+
if (!file) return null;
|
|
83
|
+
const base = path.basename(file);
|
|
84
|
+
const args = base === 'fish' ? ['-l', '-c', null] : ['-lc', null];
|
|
85
|
+
return { file, args };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function whichPath(name) {
|
|
89
|
+
for (const dir of ['/bin', '/usr/bin', '/usr/local/bin', '/opt/homebrew/bin']) {
|
|
90
|
+
const file = path.join(dir, name);
|
|
91
|
+
if (shellExists(file)) return file;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The shell to use when nobody asked for a specific one. */
|
|
97
|
+
function detectShell(platform = process.platform) {
|
|
98
|
+
if (platform === 'win32') {
|
|
99
|
+
if (shellExists('pwsh.exe') || whichPath('pwsh')) return 'pwsh';
|
|
100
|
+
return 'powershell';
|
|
101
|
+
}
|
|
102
|
+
const login = path.basename(process.env.SHELL || '');
|
|
103
|
+
if (login && (UNIX_SHELLS[login] || whichPath(login))) return login;
|
|
104
|
+
return 'sh';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** What the client can offer on this machine — for `sysinfo`, `/shell` and the help. */
|
|
108
|
+
function availableShells(platform = process.platform) {
|
|
109
|
+
if (platform === 'win32') {
|
|
110
|
+
return WINDOWS_SHELLS.filter((name) => name === 'powershell' || name === 'cmd' || Boolean(whichPath(name)));
|
|
111
|
+
}
|
|
112
|
+
const found = Object.keys(UNIX_SHELLS).filter((name) => shellExists(UNIX_SHELLS[name]) || Boolean(whichPath(name)));
|
|
113
|
+
return found.length ? found : ['sh'];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Command template for one shell. `command` is passed as its own argv entry, never
|
|
118
|
+
* through a second round of quoting, so quotes and `$` survive.
|
|
119
|
+
*/
|
|
120
|
+
function shellCommand(shell, command, platform = process.platform) {
|
|
121
|
+
const name = String(shell || detectShell(platform)).toLowerCase().replace(/\.exe$/, '');
|
|
122
|
+
if (platform === 'win32') {
|
|
123
|
+
switch (name) {
|
|
54
124
|
case 'cmd':
|
|
55
125
|
return { file: 'cmd.exe', args: ['/d', '/s', '/c', command] };
|
|
56
126
|
case 'wsl':
|
|
57
127
|
return { file: 'wsl.exe', args: ['sh', '-lc', command] };
|
|
128
|
+
case 'bash': {
|
|
129
|
+
// Git Bash when it is installed, otherwise the WSL bash
|
|
130
|
+
const gitBash = [
|
|
131
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
132
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
133
|
+
].find((file) => {
|
|
134
|
+
try {
|
|
135
|
+
return fs.existsSync(file);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return gitBash
|
|
141
|
+
? { file: gitBash, args: ['-lc', command] }
|
|
142
|
+
: { file: 'wsl.exe', args: ['bash', '-lc', command] };
|
|
143
|
+
}
|
|
144
|
+
case 'pwsh':
|
|
145
|
+
return { file: 'pwsh.exe', args: ['-NoProfile', '-NonInteractive', '-Command', command] };
|
|
58
146
|
case 'powershell':
|
|
147
|
+
case 'ps':
|
|
59
148
|
default:
|
|
60
|
-
return {
|
|
61
|
-
file: shell === 'pwsh' ? 'pwsh.exe' : 'powershell.exe',
|
|
62
|
-
args: ['-NoProfile', '-NonInteractive', '-Command', command],
|
|
63
|
-
};
|
|
149
|
+
return { file: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command', command] };
|
|
64
150
|
}
|
|
65
151
|
}
|
|
66
|
-
|
|
152
|
+
const template = unixShell(name) || unixShell('sh');
|
|
153
|
+
const args = template.args.map((arg) => (arg === null ? command : arg));
|
|
154
|
+
return { file: template.file, args };
|
|
67
155
|
}
|
|
68
156
|
|
|
69
157
|
/** Runs a command and collects stdout+stderr and the exit code. */
|
|
@@ -96,9 +184,12 @@ async function handleLocalExec(bridge, taskId, request, opts) {
|
|
|
96
184
|
const { rid, command, cwd: reqCwd, shell, admin, sandbox_reason: sandboxReason } = request;
|
|
97
185
|
const lang = opts.lang;
|
|
98
186
|
const dir = reqCwd ? resolveInRoot(opts.folder, reqCwd) : opts.folder;
|
|
187
|
+
// the bridge names the shell it wrote the command for; otherwise the user's choice
|
|
188
|
+
// from `arenacode config set shell …` / `/shell`, otherwise the login shell
|
|
189
|
+
const chosenShell = shell || opts.shell || detectShell();
|
|
99
190
|
|
|
100
191
|
write('');
|
|
101
|
-
write(` ${c.bold(c.yellow('⌘ terminal'))} ${c.dim('·')} ${c.cyan(
|
|
192
|
+
write(` ${c.bold(c.yellow('⌘ terminal'))} ${c.dim('·')} ${c.cyan(chosenShell)} ${c.dim('·')} ${c.gray(dir)}`);
|
|
102
193
|
write(indentLines(command, ' '));
|
|
103
194
|
if (admin) write(` ${c.yellow('!')} ${t(lang, 'Richiede i permessi di amministratore: qui il comando gira senza UAC.', 'Needs administrator rights: here it runs without UAC.', 'Necesita permisos de administrador: aquí se ejecuta sin UAC.')}`);
|
|
104
195
|
if (sandboxReason) write(` ${c.yellow('!')} ${t(lang, 'L’agente chiede di installare sul PC', 'The agent asks to install on the PC', 'El agente pide instalar en el PC')}: ${c.gray(sandboxReason)}`);
|
|
@@ -125,7 +216,7 @@ async function handleLocalExec(bridge, taskId, request, opts) {
|
|
|
125
216
|
return true;
|
|
126
217
|
}
|
|
127
218
|
|
|
128
|
-
const { output, code } = await runShell(command, { cwd: dir, shell });
|
|
219
|
+
const { output, code } = await runShell(command, { cwd: dir, shell: chosenShell });
|
|
129
220
|
await bridge.localResult(taskId, { rid, output, code });
|
|
130
221
|
write(` ${code === 0 ? c.green('✓') : c.red('✗')} ${c.gray(`exit ${code}`)}`);
|
|
131
222
|
return true;
|
|
@@ -495,7 +586,8 @@ async function handleLocalUi(bridge, taskId, request, opts) {
|
|
|
495
586
|
cpus: os.cpus().length,
|
|
496
587
|
memory_mb: Math.round(os.totalmem() / 1048576),
|
|
497
588
|
node: process.version,
|
|
498
|
-
shell:
|
|
589
|
+
shell: detectShell(),
|
|
590
|
+
shells: availableShells(),
|
|
499
591
|
};
|
|
500
592
|
await bridge.localResult(taskId, { rid, output: JSON.stringify(info), code: 0 });
|
|
501
593
|
write(` ${c.green('✓')} ${c.gray('sysinfo')}`);
|
|
@@ -683,6 +775,9 @@ async function handleLocalFile(bridge, taskId, request, opts) {
|
|
|
683
775
|
|
|
684
776
|
module.exports = {
|
|
685
777
|
runUiAction,
|
|
778
|
+
shellCommand,
|
|
779
|
+
detectShell,
|
|
780
|
+
availableShells,
|
|
686
781
|
REFUSED,
|
|
687
782
|
resolveInRoot,
|
|
688
783
|
runShell,
|
package/src/tui/app.js
CHANGED
|
@@ -14,7 +14,7 @@ const { getTheme, themeNames } = require('./theme');
|
|
|
14
14
|
|
|
15
15
|
const { Bridge } = require('../bridge');
|
|
16
16
|
const { buildMeta, readProjectContext } = require('../meta');
|
|
17
|
-
const { runShell, runFsOp, resolveInRoot } = require('../local');
|
|
17
|
+
const { runShell, runFsOp, resolveInRoot, detectShell, availableShells, shellCommand } = require('../local');
|
|
18
18
|
const { readConfig, patchConfig, deviceId, resolveUrl, resolveKey, openFolder, VERSION } = require('../config');
|
|
19
19
|
|
|
20
20
|
const { MODE_LINES, modeLabel, nextMode, normaliseMode } = require('../modes');
|
|
@@ -49,6 +49,7 @@ class App {
|
|
|
49
49
|
this.autoApprove = Boolean(opts.yes);
|
|
50
50
|
this.confirmWrites = Boolean(opts.confirmWrites);
|
|
51
51
|
this.approval = normaliseApproval(opts.approval);
|
|
52
|
+
this.shell = String(opts.shell || cfg.shell || '').trim() || detectShell();
|
|
52
53
|
|
|
53
54
|
this.state = {
|
|
54
55
|
mode: normaliseMode(opts.mode || cfg.mode || 'agent'),
|
|
@@ -493,6 +494,7 @@ class App {
|
|
|
493
494
|
{ slash: '/ask', title: 'Ask and Proceed: ask before commands, files and plans', hint: '', group: 'Session', run: () => this.setApproval('ask') },
|
|
494
495
|
{ slash: '/proceed', title: 'Proceed: approve the plan and run everything without asking', hint: '', group: 'Session', run: () => this.setApproval('proceed') },
|
|
495
496
|
{ slash: '/theme', title: 'Cycle the colour theme', hint: 'ctrl+x t', group: 'Session', run: by('Theme') },
|
|
497
|
+
{ slash: '/shell', title: 'Show or change the shell commands run in', hint: '', group: 'Session', run: () => this.pickShell() },
|
|
496
498
|
{ slash: '/status', title: 'Ping the bridge', hint: '', group: 'Session', run: by('Bridge status') },
|
|
497
499
|
{ slash: '/help', title: 'Every keybinding', hint: 'f1', group: 'Session', run: () => this.openHelp() },
|
|
498
500
|
{ slash: '/quit', title: 'Leave arenacode', hint: 'ctrl+x q', group: 'Session', run: () => this.quit() },
|
|
@@ -538,6 +540,42 @@ class App {
|
|
|
538
540
|
this.toast(`approval: ${next === 'proceed' ? 'Proceed' : 'Ask and Proceed'}`, next === 'proceed' ? 'warn' : 'ok');
|
|
539
541
|
}
|
|
540
542
|
|
|
543
|
+
/** `/shell` (and the double-click on a card's shell line): pick from the shells here. */
|
|
544
|
+
async pickShell(name = '') {
|
|
545
|
+
const shells = availableShells();
|
|
546
|
+
const wanted = String(name || '').trim().toLowerCase();
|
|
547
|
+
if (wanted) {
|
|
548
|
+
if (!shells.includes(wanted)) {
|
|
549
|
+
this.toast(`shell: ${wanted} not found (${shells.join(', ')})`, 'warn');
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
this.setShell(wanted);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// a free-text prompt instead of a dialog: esc keeps the current shell
|
|
556
|
+
this.toast(`shell: ${this.shell} (${shells.join(', ')})`);
|
|
557
|
+
const current = shellCommand(this.shell).file;
|
|
558
|
+
const answer = String(await this.prompt(`shell [${current}]`)).trim().toLowerCase();
|
|
559
|
+
if (!answer) return;
|
|
560
|
+
if (!shells.includes(answer)) {
|
|
561
|
+
this.toast(`shell: ${answer} not found (${shells.join(', ')})`, 'warn');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.setShell(answer);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
setShell(name) {
|
|
568
|
+
this.shell = name;
|
|
569
|
+
try {
|
|
570
|
+
patchConfig({ shell: name });
|
|
571
|
+
} catch {
|
|
572
|
+
/* a read-only HOME keeps the choice in memory only */
|
|
573
|
+
}
|
|
574
|
+
const { file } = shellCommand(name);
|
|
575
|
+
this.push({ kind: 'system', text: `shell: ${name} · ${file}` });
|
|
576
|
+
this.toast(`shell: ${name}`, 'ok');
|
|
577
|
+
}
|
|
578
|
+
|
|
541
579
|
onOverlayKey(key) {
|
|
542
580
|
const ov = this.state.overlay;
|
|
543
581
|
if (key.name === 'escape') {
|
|
@@ -672,7 +710,7 @@ class App {
|
|
|
672
710
|
const cmd = text.slice(1).trim();
|
|
673
711
|
this.push({ kind: 'user', text: cmd, shell: true });
|
|
674
712
|
const item = this.push({ kind: 'tool', icon: 'terminal', label: cmd, state: 'running', detail: '' });
|
|
675
|
-
const { output, code } = await runShell(cmd, { cwd: this.folder, shell:
|
|
713
|
+
const { output, code } = await runShell(cmd, { cwd: this.folder, shell: this.shell, quiet: true });
|
|
676
714
|
item.state = code === 0 ? 'done' : 'failed';
|
|
677
715
|
item.detail = output.trim();
|
|
678
716
|
item.secs = undefined;
|
|
Binary file
|