@xec-sh/core
Version:
Universal shell execution engine
391 lines • 16.3 kB
JavaScript
import { platform } from 'node:os';
import { Readable } from 'node:stream';
import { spawn, spawnSync } from 'node:child_process';
import { StreamHandler } from '../utils/stream.js';
import { RuntimeDetector } from '../utils/runtime-detect.js';
import { CommandError, AdapterError } from '../core/error.js';
import { BaseAdapter } from './base-adapter.js';
export class LocalAdapter extends BaseAdapter {
constructor(config = {}) {
super(config);
this.adapterName = 'local';
this.name = this.adapterName;
this.localConfig = config;
}
async isAvailable() {
return true;
}
async execute(command) {
const mergedCommand = this.mergeCommand(command);
const startTime = Date.now();
try {
const implementation = this.getImplementation();
let result;
if (implementation === 'bun' && RuntimeDetector.isBun()) {
result = await this.executeBun(mergedCommand);
}
else {
result = await this.executeNode(mergedCommand);
}
const endTime = Date.now();
return await this.createResult(result.stdout, result.stderr, result.exitCode ?? 0, result.signal ?? undefined, this.buildCommandString(mergedCommand), startTime, endTime, { originalCommand: mergedCommand });
}
catch (error) {
if (error instanceof CommandError || error instanceof AdapterError) {
throw error;
}
throw new AdapterError(this.adapterName, 'execute', error instanceof Error ? error : new Error(String(error)));
}
}
executeSync(command) {
const mergedCommand = this.mergeCommand(command);
const startTime = Date.now();
try {
const implementation = this.getImplementation();
let result;
if (implementation === 'bun' && RuntimeDetector.isBun()) {
result = this.executeBunSync(mergedCommand);
}
else {
result = this.executeNodeSync(mergedCommand);
}
const endTime = Date.now();
return this.createResultSync(result.stdout, result.stderr, result.exitCode ?? 0, result.signal ?? undefined, this.buildCommandString(mergedCommand), startTime, endTime, { originalCommand: mergedCommand });
}
catch (error) {
if (error instanceof CommandError || error instanceof AdapterError) {
throw error;
}
throw new AdapterError(this.adapterName, 'executeSync', error instanceof Error ? error : new Error(String(error)));
}
}
getImplementation() {
if (this.localConfig.forceImplementation) {
return this.localConfig.forceImplementation;
}
if (this.localConfig.preferBun && RuntimeDetector.isBun()) {
return 'bun';
}
return 'node';
}
async executeNode(command) {
if (!('stdout' in command) || command.stdout == null)
command.stdout = 'pipe';
if (!('stderr' in command) || command.stderr == null)
command.stderr = 'pipe';
const progressReporter = this.createProgressReporter(command);
const stdoutHandler = new StreamHandler({
encoding: this.config.encoding,
maxBuffer: this.config.maxBuffer,
onData: progressReporter ? (data) => progressReporter.reportOutput(data) : undefined
});
const stderrHandler = new StreamHandler({
encoding: this.config.encoding,
maxBuffer: this.config.maxBuffer
});
const spawnOptions = this.buildNodeSpawnOptions(command);
if (progressReporter) {
progressReporter.start(`Executing: ${this.buildCommandString(command)}`);
}
let child;
if (command.shell === true) {
const shellCommand = this.buildCommandString(command);
child = spawn(shellCommand, [], { ...spawnOptions, shell: true });
}
else if (typeof command.shell === 'string') {
const shellCommand = this.buildCommandString(command);
child = spawn(command.shell, ['-c', shellCommand], { ...spawnOptions, shell: false });
}
else {
child = spawn(command.command, command.args || [], spawnOptions);
}
if (command.stdin) {
if (typeof command.stdin === 'string' || Buffer.isBuffer(command.stdin)) {
child.stdin?.write(command.stdin);
child.stdin?.end();
}
else if (command.stdin instanceof Readable) {
command.stdin.pipe(child.stdin);
}
}
if (command.signal) {
const cleanup = () => child.kill(this.localConfig.killSignal);
await this.handleAbortSignal(command.signal, cleanup);
}
let stdoutTransform = null;
let stderrTransform = null;
if (child.stdout) {
if (command.stdout === 'pipe') {
stdoutTransform = stdoutHandler.createTransform();
child.stdout.pipe(stdoutTransform);
}
else if (command.stdout && typeof command.stdout === 'object' && typeof command.stdout.write === 'function') {
child.stdout.pipe(command.stdout);
}
}
if (child.stderr) {
if (command.stderr === 'pipe') {
stderrTransform = stderrHandler.createTransform();
child.stderr.pipe(stderrTransform);
}
else if (command.stderr && typeof command.stderr === 'object' && typeof command.stderr.write === 'function') {
child.stderr.pipe(command.stderr);
}
}
const processPromise = new Promise((resolve, reject) => {
child.on('error', (err) => {
if (err.code === 'ENOENT') {
if (err.syscall === 'spawn /bin/sh' || err.syscall === 'spawn') {
if (command.cwd) {
err.message = `spawn ${err.path || '/bin/sh'} ENOENT: No such file or directory (cwd: ${command.cwd})`;
}
else {
err.message = `spawn ${err.path || '/bin/sh'} ENOENT: No such file or directory`;
}
}
}
if (stdoutTransform) {
stdoutTransform.destroy();
}
if (stderrTransform) {
stderrTransform.destroy();
}
if (progressReporter) {
progressReporter.error(err);
}
reject(err);
});
child.on('exit', (code, signal) => {
if (child.stdout && stdoutTransform) {
child.stdout.unpipe(stdoutTransform);
}
if (child.stderr && stderrTransform) {
child.stderr.unpipe(stderrTransform);
}
if (stdoutTransform) {
stdoutTransform.end();
stdoutTransform.destroy();
}
if (stderrTransform) {
stderrTransform.end();
stderrTransform.destroy();
}
if (child.stdout && !child.stdout.destroyed) {
child.stdout.destroy();
}
if (child.stderr && !child.stderr.destroyed) {
child.stderr.destroy();
}
if (child.stdin && !child.stdin.destroyed) {
child.stdin.destroy();
}
if (progressReporter) {
if (code === 0) {
progressReporter.complete('Command completed successfully');
}
else {
progressReporter.error(new Error(`Command failed with exit code ${code}`));
}
}
resolve({
stdout: stdoutHandler.getContent(),
stderr: stderrHandler.getContent(),
exitCode: code,
signal
});
});
});
const timeout = command.timeout ?? this.config.defaultTimeout;
const result = await this.handleTimeout(processPromise, timeout, this.buildCommandString(command), () => child.kill(this.localConfig.killSignal));
return result;
}
async executeBun(command) {
const Bun = globalThis.Bun;
if (!Bun || !Bun.spawn) {
throw new AdapterError(this.adapterName, 'execute', new Error('Bun.spawn is not available'));
}
const progressReporter = this.createProgressReporter(command);
const stdoutHandler = new StreamHandler({
encoding: this.config.encoding,
maxBuffer: this.config.maxBuffer,
onData: progressReporter ? (data) => progressReporter.reportOutput(data) : undefined
});
const stderrHandler = new StreamHandler({
encoding: this.config.encoding,
maxBuffer: this.config.maxBuffer
});
if (progressReporter) {
progressReporter.start(`Executing: ${this.buildCommandString(command)}`);
}
const proc = Bun.spawn({
cmd: [command.command, ...(command.args || [])],
cwd: command.cwd,
env: this.createCombinedEnv(this.config.defaultEnv, command.env),
stdin: this.mapBunStdin(command.stdin),
stdout: command.stdout === 'pipe' ? 'pipe' : command.stdout,
stderr: command.stderr === 'pipe' ? 'pipe' : command.stderr
});
if (command.stdin && (typeof command.stdin === 'string' || Buffer.isBuffer(command.stdin))) {
const writer = proc.stdin.getWriter();
await writer.write(typeof command.stdin === 'string' ? new TextEncoder().encode(command.stdin) : command.stdin);
await writer.close();
}
const stdoutPromise = command.stdout === 'pipe' && proc.stdout
? this.streamBunReadable(proc.stdout, stdoutHandler)
: Promise.resolve();
const stderrPromise = command.stderr === 'pipe' && proc.stderr
? this.streamBunReadable(proc.stderr, stderrHandler)
: Promise.resolve();
const exitPromise = proc.exited;
const timeout = command.timeout ?? this.config.defaultTimeout;
const exitCode = await this.handleTimeout(exitPromise, timeout, this.buildCommandString(command), () => proc.kill());
await Promise.all([stdoutPromise, stderrPromise]);
if (progressReporter) {
if (exitCode === 0) {
progressReporter.complete('Command completed successfully');
}
else {
progressReporter.error(new Error(`Command failed with exit code ${exitCode}`));
}
}
return {
stdout: stdoutHandler.getContent(),
stderr: stderrHandler.getContent(),
exitCode,
signal: null
};
}
buildNodeSpawnOptions(command) {
const options = {
cwd: command.cwd,
env: this.createCombinedEnv(this.config.defaultEnv, command.env),
detached: command.detached,
windowsHide: true
};
if (command.cwd) {
try {
require('fs').accessSync(command.cwd, require('fs').constants.F_OK);
}
catch (err) {
}
}
if (this.localConfig.uid !== undefined) {
options.uid = this.localConfig.uid;
}
if (this.localConfig.gid !== undefined) {
options.gid = this.localConfig.gid;
}
if (command.shell === true) {
if (platform() === 'win32') {
options.shell = 'cmd.exe';
}
else {
const availableShells = ['/bin/bash', '/bin/sh', '/usr/bin/bash', '/usr/bin/sh'];
let shellFound = false;
for (const shell of availableShells) {
try {
require('fs').accessSync(shell, require('fs').constants.F_OK);
options.shell = shell;
shellFound = true;
break;
}
catch {
}
}
if (!shellFound) {
options.shell = true;
}
}
}
else if (typeof command.shell === 'string') {
options.shell = command.shell;
}
else {
options.shell = command.shell;
}
const isStream = (value) => value && typeof value === 'object' && typeof value.write === 'function';
options.stdio = [
command.stdin ? 'pipe' : 'ignore',
(typeof command.stdout === 'string' ? command.stdout : 'pipe') || 'pipe',
(typeof command.stderr === 'string' ? command.stderr : 'pipe') || 'pipe'
];
return options;
}
mapBunStdin(stdin) {
if (!stdin)
return 'ignore';
if (stdin instanceof Readable)
return stdin;
if (typeof stdin === 'string' || Buffer.isBuffer(stdin))
return 'pipe';
return 'ignore';
}
async streamBunReadable(readable, handler) {
const reader = readable.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
const chunk = Buffer.from(value);
const transform = handler.createTransform();
await new Promise((resolve, reject) => {
transform.on('error', reject);
transform.on('finish', resolve);
transform.write(chunk);
transform.end();
});
}
}
finally {
reader.releaseLock();
}
}
executeNodeSync(command) {
if (!('stdout' in command) || command.stdout == null)
command.stdout = 'pipe';
if (!('stderr' in command) || command.stderr == null)
command.stderr = 'pipe';
const spawnOptions = this.buildNodeSpawnOptions(command);
spawnOptions.encoding = this.config.encoding;
let result;
if (command.shell === true) {
const shellCommand = this.buildCommandString(command);
result = spawnSync(shellCommand, [], { ...spawnOptions, shell: true });
}
else if (typeof command.shell === 'string') {
const shellCommand = this.buildCommandString(command);
result = spawnSync(command.shell, ['-c', shellCommand], { ...spawnOptions, shell: false });
}
else {
result = spawnSync(command.command, command.args || [], spawnOptions);
}
return {
stdout: result.stdout?.toString() || '',
stderr: result.stderr?.toString() || '',
exitCode: result.status,
signal: result.signal
};
}
executeBunSync(command) {
const proc = globalThis.Bun.spawnSync({
cmd: [command.command, ...(command.args || [])],
cwd: command.cwd,
env: this.createCombinedEnv(this.config.defaultEnv, command.env),
stdin: command.stdin && (typeof command.stdin === 'string' || Buffer.isBuffer(command.stdin))
? command.stdin
: undefined,
stdout: command.stdout === 'pipe' ? 'pipe' : command.stdout,
stderr: command.stderr === 'pipe' ? 'pipe' : command.stderr
});
return {
stdout: proc.stdout ? new TextDecoder().decode(proc.stdout) : '',
stderr: proc.stderr ? new TextDecoder().decode(proc.stderr) : '',
exitCode: proc.exitCode,
signal: null
};
}
async dispose() {
}
}
//# sourceMappingURL=local-adapter.js.map