UNPKG

@xec-sh/core

Version:

Universal shell execution engine

1,089 lines 43.8 kB
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