Adapter Concept
Adapters are a key component of the Xec architecture, providing command execution in various environments through a unified API. Each adapter encapsulates the specifics of a particular environment while providing a universal interface.
Adapter System Architecture
┌─────────────────────────────────────── ──────┐
│ ExecutionEngine │
│ │
│ • Adapter management │
│ • Command routing │
│ • Configuration and context │
└──────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ BaseAdapter │
│ │
│ • Base functionality │
│ • Stream processing │
│ • Data masking │
│ • Error handling │
└──────┬──────┬──────┬───────┬───────┬────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐
│Local ││ SSH ││Docker││ K8s ││Remote│
└──────┘└──────┘└──────┘└──────┘└──────┘
Base Adapter Class
All adapters inherit from BaseAdapter
:
export abstract class BaseAdapter extends EnhancedEventEmitter {
protected config: BaseAdapterConfig;
protected abstract readonly adapterName: string;
// Main execution method
abstract execute(command: Command): Promise<ExecutionResult>;
// Availability check
abstract isAvailable(): Promise<boolean>;
// Resource cleanup
abstract dispose(): Promise<void>;
// Optional synchronous version
executeSync?(command: Command): ExecutionResult;
}
Adapter Configuration
interface BaseAdapterConfig {
defaultTimeout?: number; // Default timeout
defaultCwd?: string; // Working directory
defaultEnv?: Record<string, string>; // Environment variables
defaultShell?: string | boolean; // Shell for execution
encoding?: BufferEncoding; // Output encoding
maxBuffer?: number; // Maximum buffer size
throwOnNonZeroExit?: boolean; // Throw exception on error
sensitiveDataMasking?: { // Data masking
enabled: boolean;
patterns: RegExp[];
replacement: string;
};
}
Execution Lifecycle
1. Adapter Selection
// Explicit selection
await $.ssh({ host: 'server' })`ls`;
// Through configuration
await $.with({
adapter: 'docker',
adapterOptions: { container: 'app' }
})`ls`;
// Automatic selection
await $`ls`; // Uses LocalAdapter
2. Command Preparation
// Adapter merges settings
protected mergeCommand(command: Command): Command {
return {
...command,
cwd: command.cwd ?? this.config.defaultCwd,
env: { ...this.config.defaultEnv, ...command.env },
timeout: command.timeout ?? this.config.defaultTimeout,
shell: command.shell ?? this.config.defaultShell
};
}
3. Execution
// Each adapter implements its own logic
async execute(command: Command): Promise<ExecutionResult> {
const merged = this.mergeCommand(command);
// Adapter-specific implementation
const result = await this.runInEnvironment(merged);
// Creating unified result
return this.createResult(
result.stdout,
result.stderr,
result.exitCode,
result.signal,
merged
);
}
4. Result Processing
interface ExecutionResult {
stdout: string; // Standard output
stderr: string; // Error output
exitCode: number; // Exit code
signal?: string; // Termination signal
duration: number; // Execution time
startTime: Date; // Start time
endTime: Date; // End time
adapter: string; // Used adapter
host?: string; // Host (for SSH)
container?: string; // Container (for Docker)
}
Adapter Types
LocalAdapter
Command execution in the local system:
const local = $.local();
await local`ls -la`;
Features:
- Direct execution via child_process
- Bun runtime support
- Synchronous execution
- Minimal overhead
SSHAdapter
Command execution on remote servers:
const ssh = $.ssh({
host: 'server.com',
username: 'user',
privateKey: '/path/to/key'
});
await ssh`ls -la`;
Features:
- SSH connection pool
- SSH tunnels
- File transfer (SCP/SFTP)
- Sudo support
DockerAdapter
Command execution in Docker containers:
const docker = $.docker({
container: 'my-app'
});
await docker`ls -la`;
Features:
- Container lifecycle management
- Log streaming
- Volume mounting
- Docker Compose integration
KubernetesAdapter
Command execution in Kubernetes pods:
const k8s = $.k8s().pod('my-pod');
await k8s`ls -la`;
Features:
- Port forwarding
- Container logs
- File copying
- Namespace support
RemoteDockerAdapter
Docker via SSH connection:
const remote = $.remoteDocker({
ssh: { host: 'server', username: 'user' },
docker: { container: 'app' }
});
await remote`ls -la`;
Features:
- SSH and Docker combination
- Remote container management
- Docker API tunneling
Common Adapter Capabilities
Stream Processing
// StreamHandler for all adapters
protected createStreamHandler(options?: {
onData?: (chunk: string) => void
}): StreamHandler {
return new StreamHandler({
encoding: this.config.encoding,
maxBuffer: this.config.maxBuffer,
onData: options?.onData
});
}
Sensitive Data Masking
// Automatic password and key hiding
protected maskSensitiveData(text: string): string {
if (!this.config.sensitiveDataMasking.enabled) {
return text;
}
for (const pattern of this.config.sensitiveDataMasking.patterns) {
text = text.replace(pattern, this.config.sensitiveDataMasking.replacement);
}
return text;
}
Masking Examples:
// Passwords
"password=secret123" → "password=[REDACTED]"
// API keys
"api_key: abc123" → "api_key: [REDACTED]"
// Bearer tokens
"Authorization: Bearer xyz789" → "Authorization: Bearer [REDACTED]"
// SSH keys
"-----BEGIN RSA PRIVATE KEY-----..." → "[REDACTED]"
Timeout Handling
protected async handleTimeout(
promise: Promise<any>,
timeout: number,
command: string,
cleanup?: () => void
): Promise<any> {
if (timeout <= 0) return promise;
const timeoutPromise = new Promise((_, reject) => {
const timer = setTimeout(() => {
if (cleanup) cleanup();
reject(new TimeoutError(command, timeout));
}, timeout);
promise.finally(() => clearTimeout(timer));
});
return Promise.race([promise, timeoutPromise]);
}
Adapter Events
// Each adapter can generate events
adapter.on('connection:established', ({ host }) => {
console.log(`Connected to ${host}`);
});
adapter.on('transfer:progress', ({ bytes, total }) => {
console.log(`Transfer: ${bytes}/${total}`);
});
adapter.on('container:created', ({ id, name }) => {
console.log(`Container ${name} created: ${id}`);
});
Creating Your Own Adapter
Step 1: Inherit from BaseAdapter
import { BaseAdapter, BaseAdapterConfig } from '@xec-sh/core';
interface CustomAdapterConfig extends BaseAdapterConfig {
customOption?: string;
}
export class CustomAdapter extends BaseAdapter {
protected readonly adapterName = 'custom';
private customConfig: CustomAdapterConfig;
constructor(config: CustomAdapterConfig = {}) {
super(config);
this.name = this.adapterName;
this.customConfig = config;
}
}
Step 2: Implement execute
async execute(command: Command): Promise<ExecutionResult> {
const merged = this.mergeCommand(command);
const startTime = Date.now();
try {
// Your execution logic
const result = await this.runCustomCommand(merged);
return this.createResult(
result.stdout,
result.stderr,
result.exitCode,
result.signal,
this.buildCommandString(merged),
startTime,
Date.now()
);
} catch (error) {
throw new AdapterError(
this.adapterName,
'execute',
error instanceof Error ? error : new Error(String(error))
);
}
}
Step 3: Availability Check
async isAvailable(): Promise<boolean> {
try {
// Check that environment is available
await this.checkEnvironment();
return true;
} catch {
return false;
}
}
Step 4: Resource Cleanup
async dispose(): Promise<void> {
// Close connections
await this.closeConnections();
// Clean up temporary files
await this.cleanupTemp();
// Remove event listeners
this.removeAllListeners();
}
Step 5: Register the Adapter
import { ExecutionEngine } from '@xec-sh/core';
import { CustomAdapter } from './custom-adapter';
const $ = new ExecutionEngine();
$.registerAdapter('custom', new CustomAdapter({
customOption: 'value'
}));
// Usage
await $.with({ adapter: 'custom' })`custom-command`;
Resource Management
Connection Pools
SSH and other network adapters use pools:
class ConnectionPool {
private connections = new Map<string, Connection>();
private maxConnections = 10;
private ttl = 300000; // 5 minutes
async getConnection(key: string): Promise<Connection> {
// Reuse existing
if (this.connections.has(key)) {
return this.connections.get(key)!;
}
// Create new
const conn = await this.createConnection();
this.connections.set(key, conn);
// Auto-cleanup by TTL
setTimeout(() => {
this.closeConnection(key);
}, this.ttl);
return conn;
}
}
Lazy Initialization
Adapters are created only when needed:
class ExecutionEngine {
private adapters = new Map<string, BaseAdapter>();
private async selectAdapter(command: Command): Promise<BaseAdapter> {
const type = command.adapter || 'local';
// Create on first use
if (!this.adapters.has(type)) {
this.adapters.set(type, this.createAdapter(type));
}
return this.adapters.get(type)!;
}
}
Error Handling
Error Types
// Adapter error
class AdapterError extends Error {
constructor(
public adapter: string,
public operation: string,
public cause: Error
) {
super(`${adapter} adapter failed during ${operation}: ${cause.message}`);
}
}
// Command error
class CommandError extends Error {
constructor(
public command: string,
public exitCode: number,
public stderr: string
) {
super(`Command failed with exit code ${exitCode}: ${stderr}`);
}
}
// Timeout error
class TimeoutError extends Error {
constructor(
public command: string,
public timeout: number
) {
super(`Command timed out after ${timeout}ms: ${command}`);
}
}
Handling Strategies
// Automatic retry
async executeWithRetry(command: Command): Promise<ExecutionResult> {
let lastError;
for (let i = 0; i < 3; i++) {
try {
return await this.execute(command);
} catch (error) {
lastError = error;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
}
throw lastError;
}
// Fallback to another adapter
async executeWithFallback(command: Command): Promise<ExecutionResult> {
try {
return await this.primaryAdapter.execute(command);
} catch {
return await this.fallbackAdapter.execute(command);
}
}
Performance
Adapter Metrics
interface AdapterMetrics {
totalExecutions: number;
averageDuration: number;
errorRate: number;
activeConnections: number;
cacheHitRate: number;
}
// Metric collection
adapter.on('command:complete', ({ duration }) => {
metrics.totalExecutions++;
metrics.averageDuration =
(metrics.averageDuration * (metrics.totalExecutions - 1) + duration) /
metrics.totalExecutions;
});
Optimizations
- Result caching - for idempotent commands
- Connection pools - connection reuse
- Stream processing - for large outputs
- Parallel execution - for independent commands
- Lazy loading - creation on demand
Conclusion
The adapter system in Xec provides:
- Universality: unified API for all environments
- Extensibility: easy addition of new adapters
- Security: sensitive data masking
- Performance: optimizations for each environment
- Reliability: error handling and recovery
Adapters are the foundation for creating powerful automation tools that work in any environment.