@xec-sh/core
Version:
Universal shell execution engine
1,089 lines • 43.8 kB
JavaScript
import * as os from 'os';
import * as path from 'path';
import { AdapterError } from './error.js';
import { stream } from '../utils/stream.js';
import { TransferEngine } from '../utils/transfer.js';
import { globalCache } from '../utils/cache.js';
import { DockerFluentAPI } from '../utils/docker-fluent-api.js';
import { EnhancedEventEmitter } from '../utils/event-emitter.js';
import { TempDir, TempFile } from '../utils/temp.js';
import { ExecutionResultImpl } from './result.js';
import { interpolate, interpolateRaw } from '../utils/shell-escape.js';
import { CommandTemplate } from '../utils/templates.js';
import { SSHAdapter } from '../adapters/ssh-adapter.js';
import { within, withinSync, asyncLocalStorage } from '../utils/within.js';
let unhandledRejectionHandler = null;
function setupUnhandledRejectionHandler() {
if (unhandledRejectionHandler)
return;
unhandledRejectionHandler = (reason, promise) => {
const isXecPromise = promise.__isXecPromise ||
(reason && reason.code === 'COMMAND_FAILED') ||
(reason && reason.constructor && reason.constructor.name === 'CommandError');
if (isXecPromise) {
return;
}
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
};
process.on('unhandledRejection', unhandledRejectionHandler);
}
setupUnhandledRejectionHandler();
import { LocalAdapter } from '../adapters/local-adapter.js';
import { DockerAdapter } from '../adapters/docker-adapter.js';
import { createSSHExecutionContext } from '../utils/ssh-api.js';
import { ParallelEngine } from '../utils/parallel.js';
import { select, confirm, Spinner, question, password } from '../utils/interactive.js';
import { RetryError, withExecutionRetry } from '../utils/retry-adapter.js';
import { executePipe } from './pipe-implementation.js';
import { createK8sExecutionContext } from '../utils/kubernetes-api.js';
import { KubernetesAdapter } from '../adapters/kubernetes-adapter.js';
import { RemoteDockerAdapter } from '../adapters/remote-docker-adapter.js';
export class ExecutionEngine extends EnhancedEventEmitter {
get parallel() {
if (!this._parallel) {
this._parallel = new ParallelEngine(this);
}
return this._parallel;
}
get transfer() {
if (!this._transfer) {
this._transfer = new TransferEngine(this);
}
return this._transfer;
}
constructor(config = {}, existingAdapters) {
super();
this.stream = stream;
this.question = question;
this.prompt = question;
this.password = password;
this.confirm = confirm;
this.select = select;
this.spinner = (text) => new Spinner(text);
this.within = within;
this.withinSync = withinSync;
this.adapters = new Map();
this.currentConfig = {};
this._tempTracker = new Set();
this._activeProcesses = new Set();
this._templatesRegistry = new Map();
this.templates = {
render: (templateStr, data, options) => {
const mergedParams = { ...options?.defaults, ...data };
return templateStr.replace(/\{\{(\w+)\}\}/g, (match, key) => {
if (!(key in mergedParams)) {
throw new Error(`Missing required parameter: ${key}`);
}
const value = mergedParams[key];
if (typeof value === 'string') {
if (value.includes(' ') || value.includes('"') || value.includes("'")) {
return '"' + value.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
}
return value;
}
return String(value);
});
},
create: (templateStr, options) => new CommandTemplate(templateStr, options),
parse: (templateStr) => {
const regex = /\{\{(\w+)\}\}/g;
const params = [];
let match;
while ((match = regex.exec(templateStr)) !== null) {
if (match[1]) {
params.push(match[1]);
}
}
return { template: templateStr, params };
},
register: (name, templateStr, options) => {
const template = new CommandTemplate(templateStr, options);
this._templatesRegistry.set(name, template);
},
get: (name) => {
const template = this._templatesRegistry.get(name);
if (!template) {
throw new Error(`Template '${name}' not found`);
}
return template;
}
};
this._config = this.validateConfig(config);
this.setMaxListeners(config.maxEventListeners || 100);
if (config.enableEvents === false) {
this.emit = () => false;
}
if (existingAdapters) {
this.adapters = existingAdapters;
}
else {
this.initializeAdapters();
}
}
emitEvent(event, data) {
if (!this.listenerCount(event))
return;
this.emit(event, {
...data,
timestamp: new Date(),
adapter: this.getCurrentAdapter()?.name || 'local'
});
}
getCurrentAdapter() {
const adapterType = this.currentConfig.adapter || 'local';
return this.adapters.get(adapterType);
}
validateConfig(config) {
const validatedConfig = { ...config };
if (config.defaultTimeout !== undefined && config.defaultTimeout < 0) {
throw new Error(`Invalid timeout value: ${config.defaultTimeout}`);
}
if (config.encoding !== undefined) {
const validEncodings = ['ascii', 'utf8', 'utf-8', 'utf16le', 'ucs2', 'ucs-2', 'base64', 'base64url', 'latin1', 'binary', 'hex'];
if (!validEncodings.includes(config.encoding)) {
throw new Error(`Unsupported encoding: ${config.encoding}`);
}
}
if (config.maxBuffer !== undefined && config.maxBuffer <= 0) {
throw new Error(`Invalid buffer size: ${config.maxBuffer}`);
}
if (config.maxEventListeners !== undefined && config.maxEventListeners <= 0) {
throw new Error(`Invalid max event listeners: ${config.maxEventListeners}`);
}
validatedConfig.defaultTimeout = config.defaultTimeout ?? 30000;
validatedConfig.throwOnNonZeroExit = config.throwOnNonZeroExit ?? true;
validatedConfig.encoding = config.encoding ?? 'utf8';
validatedConfig.maxBuffer = config.maxBuffer ?? 10 * 1024 * 1024;
validatedConfig.enableEvents = config.enableEvents;
validatedConfig.maxEventListeners = config.maxEventListeners;
return validatedConfig;
}
initializeAdapters() {
const localConfig = {
...this.getBaseAdapterConfig(),
...this._config.adapters?.local,
preferBun: this._config.runtime?.preferBun
};
this.adapters.set('local', new LocalAdapter(localConfig));
const sshConfig = {
...this.getBaseAdapterConfig(),
...this._config.adapters?.ssh
};
this.adapters.set('ssh', new SSHAdapter(sshConfig));
const k8sConfig = {
...this.getBaseAdapterConfig(),
...this._config.adapters?.kubernetes
};
this.adapters.set('kubernetes', new KubernetesAdapter(k8sConfig));
const dockerConfig = {
...this.getBaseAdapterConfig(),
...this._config.adapters?.docker
};
this.adapters.set('docker', new DockerAdapter(dockerConfig));
if (this._config.adapters?.remoteDocker) {
const remoteDockerConfig = {
...this.getBaseAdapterConfig(),
...this._config.adapters.remoteDocker
};
this.adapters.set('remote-docker', new RemoteDockerAdapter(remoteDockerConfig));
}
}
getBaseAdapterConfig() {
return {
defaultTimeout: this._config.defaultTimeout,
defaultCwd: this._config.defaultCwd,
defaultEnv: this._config.defaultEnv,
defaultShell: this._config.defaultShell,
encoding: this._config.encoding,
maxBuffer: this._config.maxBuffer,
throwOnNonZeroExit: this._config.throwOnNonZeroExit,
};
}
async execute(command) {
const startTime = Date.now();
const localContext = asyncLocalStorage.getStore();
let contextCommand = command;
if (localContext) {
const { defaultEnv, ...otherContext } = localContext;
contextCommand = {
...otherContext,
...command,
env: {
...(defaultEnv || {}),
...(command.env || {})
}
};
}
const mergedCommand = { ...this.currentConfig, ...contextCommand };
const adapter = await this.selectAdapter(mergedCommand);
if (!adapter) {
throw new AdapterError('unknown', 'execute', new Error('No suitable adapter found'));
}
this.emitEvent('command:start', {
command: mergedCommand.command || '',
args: mergedCommand.args,
cwd: mergedCommand.cwd,
shell: typeof mergedCommand.shell === 'boolean' ? mergedCommand.shell : !!mergedCommand.shell,
env: mergedCommand.env
});
try {
let result;
if (mergedCommand.retry) {
const maxRetries = mergedCommand.retry.maxRetries ?? 0;
if (maxRetries > 0) {
try {
result = await withExecutionRetry(() => adapter.execute(mergedCommand), mergedCommand.retry, this);
}
catch (error) {
if (mergedCommand.nothrow && error instanceof RetryError) {
result = error.lastResult;
}
else {
throw error;
}
}
}
else {
result = await adapter.execute(mergedCommand);
}
}
else {
result = await adapter.execute(mergedCommand);
}
this.emitEvent('command:complete', {
command: mergedCommand.command || '',
exitCode: result.exitCode,
stdout: result.stdout,
stderr: result.stderr,
duration: Date.now() - startTime
});
return result;
}
catch (error) {
this.emitEvent('command:error', {
command: mergedCommand.command || '',
error: error instanceof Error ? error.message : String(error),
duration: Date.now() - startTime
});
throw error;
}
}
async awaitThenables(values) {
const results = [];
for (const value of values) {
if (value && typeof value === 'object' && typeof value.then === 'function') {
results.push(await value);
}
else {
results.push(value);
}
}
return results;
}
run(strings, ...values) {
const deferredCommand = async () => {
const resolvedValues = await this.awaitThenables(values);
const command = interpolate(strings, ...resolvedValues);
return { command, shell: this.currentConfig.shell ?? true };
};
return this.createDeferredProcessPromise(deferredCommand);
}
raw(strings, ...values) {
const deferredCommand = async () => {
const resolvedValues = await this.awaitThenables(values);
const command = interpolateRaw(strings, ...resolvedValues);
return { command, shell: this.currentConfig.shell ?? true };
};
return this.createDeferredProcessPromise(deferredCommand);
}
template(templateStr, options) {
return new CommandTemplate(templateStr, options);
}
tag(strings, ...values) {
return this.run(strings, ...values);
}
createDeferredProcessPromise(commandResolver) {
let pendingModifications = {};
let isQuiet = false;
let abortController;
let cacheOptions;
const executeCommand = async () => {
try {
if (!pendingModifications.signal) {
abortController = new AbortController();
pendingModifications.signal = abortController.signal;
}
const commandParts = await commandResolver();
const globalNothrow = this._config.throwOnNonZeroExit === false;
const currentCommand = {
...this.currentConfig,
...commandParts,
...pendingModifications,
nothrow: pendingModifications.nothrow ?? commandParts.nothrow ?? (globalNothrow ? true : undefined)
};
if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
const cached = globalCache.get(cacheKey);
if (cached) {
return cached;
}
const inflight = globalCache.getInflight(cacheKey);
if (inflight) {
return inflight;
}
}
let result;
let executePromise;
try {
executePromise = this.execute(currentCommand);
if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.setInflight(cacheKey, executePromise);
}
result = await executePromise;
if (cacheOptions && (result.exitCode === 0 || currentCommand.nothrow)) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.set(cacheKey, result, cacheOptions.ttl || 60000);
globalCache.clearInflight(cacheKey);
if (cacheOptions.invalidateOn) {
globalCache.invalidate(cacheOptions.invalidateOn);
}
}
else if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.clearInflight(cacheKey);
}
}
catch (error) {
if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.clearInflight(cacheKey);
}
throw error;
}
return result;
}
catch (error) {
if (pendingModifications.nothrow) {
const errorResult = new ExecutionResultImpl('', error instanceof Error ? error.message : String(error), 1, undefined, pendingModifications.command || '', 0, new Date(), new Date(), 'local');
return errorResult;
}
throw error;
}
};
const promise = executeCommand();
promise.__isXecPromise = true;
this._activeProcesses.add(promise);
promise.finally(() => {
this._activeProcesses.delete(promise);
});
promise.stdin = null;
promise.pipe = (target, optionsOrFirstValue, ...args) => {
let pipeOptions = {};
let templateArgs = args;
if (Array.isArray(target) && 'raw' in target &&
optionsOrFirstValue !== undefined &&
(typeof optionsOrFirstValue !== 'object' || optionsOrFirstValue === null ||
!('throwOnError' in optionsOrFirstValue || 'encoding' in optionsOrFirstValue ||
'lineByLine' in optionsOrFirstValue || 'lineSeparator' in optionsOrFirstValue))) {
templateArgs = [optionsOrFirstValue, ...args];
pipeOptions = {};
}
else if (typeof optionsOrFirstValue === 'object' && optionsOrFirstValue !== null) {
pipeOptions = optionsOrFirstValue;
}
const pipedPromise = (async () => {
const result = await executePipe(promise, target, this, {
throwOnError: !pendingModifications.nothrow,
...pipeOptions
}, ...templateArgs);
return result;
})();
pipedPromise.stdin = null;
pipedPromise.pipe = promise.pipe;
pipedPromise.signal = promise.signal;
pipedPromise.timeout = promise.timeout;
pipedPromise.quiet = promise.quiet;
pipedPromise.nothrow = promise.nothrow;
pipedPromise.interactive = promise.interactive;
pipedPromise.cache = promise.cache;
pipedPromise.env = promise.env;
pipedPromise.cwd = promise.cwd;
pipedPromise.shell = promise.shell;
pipedPromise.stdout = promise.stdout;
pipedPromise.stderr = promise.stderr;
pipedPromise.text = promise.text;
pipedPromise.json = promise.json;
pipedPromise.lines = promise.lines;
pipedPromise.buffer = promise.buffer;
pipedPromise.kill = promise.kill;
return pipedPromise;
};
promise.signal = (signal) => {
pendingModifications = { ...pendingModifications, signal };
return promise;
};
promise.timeout = (ms, timeoutSignal) => {
pendingModifications = { ...pendingModifications, timeout: ms };
if (timeoutSignal) {
pendingModifications.timeoutSignal = timeoutSignal;
}
return promise;
};
promise.quiet = () => {
isQuiet = true;
return promise;
};
promise.nothrow = () => {
pendingModifications = { ...pendingModifications, nothrow: true };
return promise;
};
promise.interactive = () => {
pendingModifications = {
...pendingModifications,
stdout: 'inherit',
stderr: 'inherit',
stdin: process.stdin
};
return promise;
};
promise.cwd = (dir) => {
pendingModifications = { ...pendingModifications, cwd: dir };
return promise;
};
promise.env = (env) => {
pendingModifications = { ...pendingModifications, env: { ...pendingModifications.env, ...env } };
return promise;
};
promise.shell = (shell) => {
pendingModifications = { ...pendingModifications, shell };
return promise;
};
promise.stdout = (stream) => {
pendingModifications = { ...pendingModifications, stdout: stream };
return promise;
};
promise.stderr = (stream) => {
pendingModifications = { ...pendingModifications, stderr: stream };
return promise;
};
promise.text = () => promise.then(result => result.stdout.trim());
promise.json = () => promise.text().then(text => {
try {
return JSON.parse(text);
}
catch (error) {
throw new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}\nOutput: ${text}`);
}
});
promise.lines = async () => {
const result = await promise;
return result.stdout.split('\n').filter(line => line.length > 0);
};
promise.buffer = async () => {
const result = await promise;
return Buffer.from(result.stdout);
};
promise.cache = (options) => {
cacheOptions = options || {};
return promise;
};
promise.kill = (signal = 'SIGTERM') => {
if (abortController && !abortController.signal.aborted) {
abortController.abort();
}
else if (pendingModifications.signal && typeof pendingModifications.signal.dispatchEvent === 'function') {
const event = new Event('abort');
pendingModifications.signal.dispatchEvent(event);
}
};
promise.child = undefined;
Object.defineProperty(promise, 'exitCode', {
get: () => promise.then(result => result.exitCode)
});
return promise;
}
createProcessPromise(command) {
const currentCommand = { ...command };
let isQuiet = false;
let abortController;
let cacheOptions;
const executeCommand = async () => {
try {
if (!currentCommand.signal) {
abortController = new AbortController();
currentCommand.signal = abortController.signal;
}
if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
const cached = globalCache.get(cacheKey);
if (cached) {
return cached;
}
const inflight = globalCache.getInflight(cacheKey);
if (inflight) {
return inflight;
}
}
let result;
let executePromise;
try {
executePromise = this.execute(currentCommand);
if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.setInflight(cacheKey, executePromise);
}
result = await executePromise;
if (cacheOptions && (result.exitCode === 0 || currentCommand.nothrow)) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.set(cacheKey, result, cacheOptions.ttl || 60000);
globalCache.clearInflight(cacheKey);
if (cacheOptions.invalidateOn) {
globalCache.invalidate(cacheOptions.invalidateOn);
}
}
else if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.clearInflight(cacheKey);
}
}
catch (error) {
if (cacheOptions) {
const cacheKey = cacheOptions.key || globalCache.generateKey(currentCommand.command || '', currentCommand.cwd, currentCommand.env);
globalCache.clearInflight(cacheKey);
}
throw error;
}
return result;
}
catch (error) {
if (currentCommand.nothrow) {
const errorResult = new ExecutionResultImpl('', error instanceof Error ? error.message : String(error), 1, undefined, currentCommand.command || '', 0, new Date(), new Date(), 'local');
return errorResult;
}
throw error;
}
};
const promise = executeCommand();
promise.__isXecPromise = true;
this._activeProcesses.add(promise);
promise.finally(() => {
this._activeProcesses.delete(promise);
});
promise.stdin = null;
promise.pipe = (target, optionsOrFirstValue, ...args) => {
let pipeOptions = {};
let templateArgs = args;
if (Array.isArray(target) && 'raw' in target &&
optionsOrFirstValue !== undefined &&
(typeof optionsOrFirstValue !== 'object' || optionsOrFirstValue === null ||
!('throwOnError' in optionsOrFirstValue || 'encoding' in optionsOrFirstValue ||
'lineByLine' in optionsOrFirstValue || 'lineSeparator' in optionsOrFirstValue))) {
templateArgs = [optionsOrFirstValue, ...args];
pipeOptions = {};
}
else if (typeof optionsOrFirstValue === 'object' && optionsOrFirstValue !== null) {
pipeOptions = optionsOrFirstValue;
}
const pipedPromise = (async () => {
const result = await executePipe(promise, target, this, {
throwOnError: !currentCommand.nothrow,
...pipeOptions
}, ...templateArgs);
return result;
})();
pipedPromise.stdin = null;
pipedPromise.pipe = promise.pipe;
pipedPromise.signal = promise.signal;
pipedPromise.timeout = promise.timeout;
pipedPromise.quiet = promise.quiet;
pipedPromise.nothrow = promise.nothrow;
pipedPromise.interactive = promise.interactive;
pipedPromise.cache = promise.cache;
pipedPromise.env = promise.env;
pipedPromise.cwd = promise.cwd;
pipedPromise.shell = promise.shell;
pipedPromise.stdout = promise.stdout;
pipedPromise.stderr = promise.stderr;
pipedPromise.text = promise.text;
pipedPromise.json = promise.json;
pipedPromise.lines = promise.lines;
pipedPromise.buffer = promise.buffer;
pipedPromise.kill = promise.kill;
this._activeProcesses.add(pipedPromise);
pipedPromise.finally(() => {
this._activeProcesses.delete(pipedPromise);
});
return pipedPromise;
};
promise.signal = (signal) => {
currentCommand.signal = signal;
return this.createProcessPromise(currentCommand);
};
promise.timeout = (ms, timeoutSignal) => {
currentCommand.timeout = ms;
if (timeoutSignal) {
currentCommand.timeoutSignal = timeoutSignal;
}
return this.createProcessPromise(currentCommand);
};
promise.quiet = () => {
isQuiet = true;
return this.createProcessPromise(currentCommand);
};
promise.nothrow = () => {
currentCommand.nothrow = true;
return this.createProcessPromise(currentCommand);
};
promise.interactive = () => {
currentCommand.stdout = 'inherit';
currentCommand.stderr = 'inherit';
currentCommand.stdin = process.stdin;
return this.createProcessPromise(currentCommand);
};
promise.cwd = (dir) => {
currentCommand.cwd = dir;
return this.createProcessPromise(currentCommand);
};
promise.env = (env) => {
currentCommand.env = { ...currentCommand.env, ...env };
return this.createProcessPromise(currentCommand);
};
promise.shell = (shell) => {
currentCommand.shell = shell;
return this.createProcessPromise(currentCommand);
};
promise.stdout = (stream) => {
currentCommand.stdout = stream;
return this.createProcessPromise(currentCommand);
};
promise.stderr = (stream) => {
currentCommand.stderr = stream;
return this.createProcessPromise(currentCommand);
};
promise.text = () => promise.then(result => result.stdout.trim());
promise.json = () => promise.text().then(text => {
try {
return JSON.parse(text);
}
catch (error) {
throw new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}\nOutput: ${text}`);
}
});
promise.lines = async () => {
const result = await promise;
return result.stdout.split('\n').filter(line => line.length > 0);
};
promise.buffer = async () => {
const result = await promise;
return Buffer.from(result.stdout);
};
promise.cache = (options) => {
cacheOptions = options || {};
return promise;
};
promise.kill = (signal = 'SIGTERM') => {
if (abortController && !abortController.signal.aborted) {
abortController.abort();
}
else if (currentCommand.signal && typeof currentCommand.signal.dispatchEvent === 'function') {
const event = new Event('abort');
currentCommand.signal.dispatchEvent(event);
}
};
promise.child = undefined;
Object.defineProperty(promise, 'exitCode', {
get: () => promise.then(result => result.exitCode)
});
return promise;
}
async selectAdapter(command) {
if (command.adapter && command.adapter !== 'auto') {
const adapter = this.adapters.get(command.adapter);
if (!adapter) {
throw new AdapterError(command.adapter, 'select', new Error(`Adapter '${command.adapter}' not configured`));
}
return adapter;
}
if (command.adapterOptions) {
switch (command.adapterOptions.type) {
case 'ssh':
return this.adapters.get('ssh') || null;
case 'docker':
if (!this.adapters.has('docker')) {
const dockerConfig = {
...this.getBaseAdapterConfig(),
...this._config.adapters?.docker
};
this.adapters.set('docker', new DockerAdapter(dockerConfig));
}
return this.adapters.get('docker') || null;
case 'kubernetes':
if (!this.adapters.has('kubernetes')) {
const k8sConfig = {
...this.getBaseAdapterConfig(),
...this._config.adapters?.kubernetes
};
this.adapters.set('kubernetes', new KubernetesAdapter(k8sConfig));
}
return this.adapters.get('kubernetes') || null;
case 'remote-docker':
if (!this.adapters.has('remote-docker')) {
const remoteDockerConfig = this._config.adapters?.remoteDocker;
if (!remoteDockerConfig || !remoteDockerConfig.ssh) {
throw new Error('Remote Docker adapter requires SSH configuration');
}
const fullConfig = {
...this.getBaseAdapterConfig(),
...remoteDockerConfig
};
this.adapters.set('remote-docker', new RemoteDockerAdapter(fullConfig));
}
return this.adapters.get('remote-docker') || null;
case 'local':
return this.adapters.get('local') || null;
default:
break;
}
}
return this.adapters.get('local') || null;
}
retry(options = {}) {
const originalExecute = this.execute.bind(this);
const newEngine = Object.create(this);
newEngine.execute = async (cmd) => {
const retryOptions = { ...options, ...cmd.retry };
try {
return await withExecutionRetry(() => originalExecute(cmd), retryOptions, this);
}
catch (error) {
if (cmd.nothrow && error instanceof RetryError) {
return error.lastResult;
}
throw error;
}
};
return newEngine;
}
async tempFile(options) {
const file = new TempFile({ ...options, emitter: this });
await file.create();
this._tempTracker.add(file);
return file;
}
async tempDir(options) {
const dir = new TempDir({ ...options, emitter: this });
await dir.create();
this._tempTracker.add(dir);
return dir;
}
async withTempFile(fn, options) {
const file = new TempFile({ ...options, emitter: this });
try {
await file.create();
return await fn(file.path);
}
finally {
await file.cleanup();
}
}
async withTempDir(fn, options) {
const dir = new TempDir({ ...options, emitter: this });
try {
await dir.create();
return await fn(dir.path);
}
finally {
await dir.cleanup();
}
}
async readFile(path) {
const result = await this.execute({
command: 'cat',
args: [path],
shell: false
});
if (result.exitCode === 0) {
this.emitEvent('file:read', {
path
});
return result.stdout;
}
else {
throw new Error(`Failed to read file ${path}: ${result.stderr}`);
}
}
async writeFile(path, content) {
const result = await this.execute({
command: 'tee',
args: [path],
stdin: content,
shell: false
});
if (result.exitCode === 0) {
this.emitEvent('file:write', {
path,
size: Buffer.byteLength(content, 'utf8')
});
}
else {
throw new Error(`Failed to write file ${path}: ${result.stderr}`);
}
}
async deleteFile(path) {
const result = await this.execute({
command: 'rm',
args: ['-f', path],
shell: false
});
if (result.exitCode === 0) {
this.emitEvent('file:delete', {
path
});
}
else {
throw new Error(`Failed to delete file ${path}: ${result.stderr}`);
}
}
interactive() {
const newEngine = Object.create(this);
newEngine.currentConfig = {
...this.currentConfig,
stdout: 'inherit',
stderr: 'inherit',
stdin: process.stdin
};
return newEngine;
}
async withSpinner(text, fn) {
const s = new Spinner(text);
s.start();
try {
const result = await fn();
s.succeed();
return result;
}
catch (error) {
s.fail();
throw error;
}
}
with(config) {
const localContext = asyncLocalStorage.getStore();
const mergedConfig = localContext
? { ...localContext, ...config }
: config;
const { defaultEnv, defaultCwd, ...commandConfig } = mergedConfig;
const engineConfig = (defaultEnv !== undefined || defaultCwd !== undefined) ? {
...this._config,
defaultEnv: defaultEnv ?? this._config.defaultEnv,
defaultCwd: defaultCwd ?? this._config.defaultCwd
} : this._config;
const newEngine = new ExecutionEngine(engineConfig, this.adapters);
newEngine.currentConfig = { ...this.currentConfig, ...commandConfig };
return newEngine;
}
ssh(options) {
return createSSHExecutionContext(this, options);
}
docker(options) {
if (!options) {
if (!this._dockerFluentAPI) {
this._dockerFluentAPI = new DockerFluentAPI(this);
}
return this._dockerFluentAPI;
}
if ('image' in options) {
const ephemeralOptions = options;
const containerName = this.generateEphemeralContainerName(ephemeralOptions.image);
return this.with({
adapter: 'docker',
adapterOptions: {
type: 'docker',
container: containerName,
runMode: 'run',
image: ephemeralOptions.image,
volumes: ephemeralOptions.volumes,
autoRemove: true,
workdir: ephemeralOptions.workdir,
user: ephemeralOptions.user,
env: ephemeralOptions.env,
}
});
}
else {
const persistentOptions = options;
return this.with({
adapter: 'docker',
adapterOptions: {
type: 'docker',
container: persistentOptions.container,
workdir: persistentOptions.workdir,
user: persistentOptions.user,
env: persistentOptions.env
}
});
}
}
generateEphemeralContainerName(image) {
const imageWithoutTag = image.split(':')[0] || image;
const imageParts = imageWithoutTag.split('/');
const imageName = imageParts[imageParts.length - 1] || 'container';
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `xec-${imageName}-${timestamp}-${random}`;
}
k8s(options) {
return createK8sExecutionContext(this, options || {});
}
remoteDocker(options) {
return this.with({
adapter: 'remote-docker',
adapterOptions: { type: 'remote-docker', ...options }
});
}
local() {
return this.with({
adapter: 'local',
adapterOptions: { type: 'local' }
});
}
cd(dir) {
const currentCwd = this.currentConfig.cwd || this._config.defaultCwd || process.cwd();
let resolvedPath;
if (dir.startsWith('~')) {
const homedir = os.homedir();
resolvedPath = path.join(homedir, dir.slice(1));
}
else if (!path.isAbsolute(dir)) {
resolvedPath = path.resolve(currentCwd, dir);
}
else {
resolvedPath = dir;
}
return this.with({ cwd: resolvedPath });
}
pwd() {
return this.currentConfig.cwd || this._config.defaultCwd || process.cwd();
}
async batch(commands, options = {}) {
const batchOptions = {
...options,
maxConcurrency: options.concurrency || options.maxConcurrency || 5
};
return this.parallel.settled(commands, batchOptions);
}
env(env) {
return this.with({
env: { ...this.currentConfig.env, ...env }
});
}
timeout(ms) {
return this.with({ timeout: ms });
}
shell(shell) {
return this.with({ shell });
}
get config() {
const self = this;
return {
set(updates) {
if (updates.defaultEnv) {
self._config.defaultEnv = { ...self._config.defaultEnv, ...updates.defaultEnv };
delete updates.defaultEnv;
}
Object.assign(self._config, updates);
if (updates.adapters) {
self.updateAdapterConfigs(updates.adapters);
}
},
get() {
return { ...self._config };
}
};
}
defaults(config) {
const newConfig = {};
if (config.defaultEnv) {
newConfig.defaultEnv = { ...this._config.defaultEnv, ...config.defaultEnv };
}
if (config.defaultCwd) {
newConfig.defaultCwd = config.defaultCwd;
}
if (config.timeout !== undefined) {
newConfig.defaultTimeout = config.timeout;
}
if (config.shell !== undefined) {
newConfig.defaultShell = config.shell;
}
const newEngine = new ExecutionEngine({ ...this._config, ...newConfig });
Object.assign(newEngine.currentConfig, this.currentConfig);
const { defaultEnv, defaultCwd, timeout, shell, ...commandDefaults } = config;
Object.assign(newEngine.currentConfig, commandDefaults);
return newEngine;
}
updateAdapterConfigs(adapterConfigs) {
if (!adapterConfigs)
return;
for (const [name, config] of Object.entries(adapterConfigs)) {
const adapter = this.adapters.get(name);
if (adapter && 'updateConfig' in adapter && typeof adapter.updateConfig === 'function') {
adapter.updateConfig(config);
}
}
}
async which(command) {
try {
const result = await this.run `which ${command}`.nothrow();
const path = result.stdout.trim();
return (path && result.exitCode === 0) ? path : null;
}
catch {
return null;
}
}
async isCommandAvailable(command) {
const path = await this.which(command);
return path !== null;
}
async commandExists(command) {
return this.isCommandAvailable(command);
}
async dispose() {
for (const process of this._activeProcesses) {
try {
process.kill('SIGTERM');
}
catch {
}
}
this._activeProcesses.clear();
for (const temp of this._tempTracker) {
try {
await temp.cleanup();
}
catch {
}
}
this._tempTracker.clear();
const disposePromises = [];
for (const adapter of this.adapters.values()) {
if ('dispose' in adapter && typeof adapter.dispose === 'function') {
disposePromises.push(adapter.dispose());
}
}
await Promise.allSettled(disposePromises);
this.adapters.clear();
this._parallel = undefined;
this._transfer = undefined;
this.removeAllListeners();
this.currentConfig = {};
}
getAdapter(name) {
return this.adapters.get(name);
}
registerAdapter(name, adapter) {
this.adapters.set(name, adapter);
}
}
//# sourceMappingURL=execution-engine.js.map