Extensions
The Xec execution engine provides powerful extension mechanisms to add custom functionality, create new adapters, and integrate with external systems.
Overview
Extension support (packages/core/src/core/extensions.ts
) provides:
- Custom adapter creation for new environments
- Plugin system for modular features
- Hook system for lifecycle events
- Custom commands and methods
- Middleware integration for cross-cutting concerns
- Type-safe extensions with TypeScript
Custom Adapters
Creating an Adapter
import { BaseAdapter, AdapterConfig, ExecutionResult } from '@xec-sh/core';
// Custom adapter for a new environment
class CustomAdapter extends BaseAdapter {
constructor(config: AdapterConfig) {
super(config);
}
async execute(command: string, options: any): Promise<ExecutionResult> {
// Custom execution logic
const result = await this.runCommand(command, options);
return {
stdout: result.output,
stderr: result.errors,
exitCode: result.code,
duration: result.time
};
}
async connect(): Promise<void> {
// Establish connection to custom environment
await this.establishConnection();
}
async disconnect(): Promise<void> {
// Clean up resources
await this.cleanup();
}
async healthCheck(): Promise<boolean> {
// Check if adapter is healthy
return await this.ping();
}
}
// Register adapter
$.registerAdapter('custom', CustomAdapter);
// Use custom adapter
await $.custom({ config: 'value' })`command`;
Adapter with Connection Pooling
class PooledAdapter extends BaseAdapter {
private pool: ConnectionPool;
constructor(config: AdapterConfig) {
super(config);
this.pool = new ConnectionPool({
min: 2,
max: 10,
idleTimeout: 60000
});
}
async execute(command: string, options: any): Promise<ExecutionResult> {
const connection = await this.pool.acquire();
try {
return await connection.execute(command, options);
} finally {
await this.pool.release(connection);
}
}
async getPoolStats() {
return this.pool.getStats();
}
}
Plugin System
Creating Plugins
// Plugin interface
interface XecPlugin {
name: string;
version: string;
initialize(engine: ExecutionEngine): void;
destroy?(): Promise<void>;
}
// Example plugin
class LoggingPlugin implements XecPlugin {
name = 'logging';
version = '1.0.0';
initialize(engine: ExecutionEngine) {
// Add logging to all commands
engine.on('command:start', this.logStart);
engine.on('command:complete', this.logComplete);
engine.on('command:error', this.logError);
// Add custom method
engine.addMethod('logged', (command: any) => {
return command
.on('output', (data: any) => console.log('Output:', data));
});
}
private logStart = (event: any) => {
console.log(`[${event.id}] Starting: ${event.command}`);
};
private logComplete = (event: any) => {
console.log(`[${event.id}] Completed in ${event.duration}ms`);
};
private logError = (event: any) => {
console.error(`[${event.id}] Error:`, event.error);
};
async destroy() {
console.log('Logging plugin destroyed');
}
}
// Register and use plugin
$.use(new LoggingPlugin());
// Now all commands have logging
await $`command`.logged();
Plugin with Configuration
class MetricsPlugin implements XecPlugin {
name = 'metrics';
version = '1.0.0';
private metrics: Map<string, any> = new Map();
constructor(private config: {
endpoint: string;
interval: number;
tags: Record<string, string>;
}) {}
initialize(engine: ExecutionEngine) {
// Collect metrics
engine.on('command:complete', (event) => {
this.recordMetric({
command: event.command,
duration: event.duration,
exitCode: event.exitCode,
timestamp: Date.now()
});
});
// Send metrics periodically
setInterval(() => this.sendMetrics(), this.config.interval);
}
private recordMetric(metric: any) {
const key = `${metric.command}:${Date.now()}`;
this.metrics.set(key, metric);
}
private async sendMetrics() {
const batch = Array.from(this.metrics.values());
this.metrics.clear();
await fetch(this.config.endpoint, {
method: 'POST',
body: JSON.stringify({
metrics: batch,
tags: this.config.tags
})
});
}
}
// Use configured plugin
$.use(new MetricsPlugin({
endpoint: 'https://metrics.example.com/api',
interval: 60000,
tags: { service: 'xec', environment: 'production' }
}));
Custom Commands
Adding Global Commands
// Add custom command method
$.addCommand('deploy', async function(this: ExecutionEngine, target: string) {
const config = await this`cat deploy.json`.json();
const steps = [
this`git pull`,
this`npm install`,
this`npm run build`,
this`rsync -av dist/ ${target}:/var/www/`
];
for (const step of steps) {
await step;
}
return { success: true, target };
});
// Use custom command
await $.deploy('user@server.com');
Command with Options
interface DeployOptions {
environment: 'dev' | 'staging' | 'prod';
version?: string;
skipTests?: boolean;
}
$.addCommand('smartDeploy', async function(
this: ExecutionEngine,
options: DeployOptions
) {
// Validate options
if (!options.environment) {
throw new Error('Environment is required');
}
// Build command based on options
let pipeline = [
this`git fetch`,
this`git checkout ${options.version || 'main'}`
];
if (!options.skipTests) {
pipeline.push(this`npm test`);
}
pipeline.push(
this`npm run build:${options.environment}`,
this`deploy-${options.environment}.sh`
);
// Execute pipeline
for (const cmd of pipeline) {
await cmd;
}
});
// Use with options
await $.smartDeploy({
environment: 'staging',
version: 'v1.2.3',
skipTests: false
});
Middleware System
Creating Middleware
// Middleware interface
type Middleware = (context: ExecutionContext, next: () => Promise<any>) => Promise<any>;
// Timing middleware
const timingMiddleware: Middleware = async (context, next) => {
const start = Date.now();
try {
const result = await next();
console.log(`Command took ${Date.now() - start}ms`);
return result;
} catch (error) {
console.log(`Command failed after ${Date.now() - start}ms`);
throw error;
}
};
// Auth middleware
const authMiddleware: Middleware = async (context, next) => {
// Add authentication headers
context.env.AUTH_TOKEN = await getAuthToken();
return next();
};
// Retry middleware
const retryMiddleware: Middleware = async (context, next) => {
let lastError;
for (let i = 0; i < 3; i++) {
try {
return await next();
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1} failed, retrying...`);
}
}
throw lastError;
};
// Apply middleware
$.useMiddleware(timingMiddleware);
$.useMiddleware(authMiddleware);
$.useMiddleware(retryMiddleware);
Conditional Middleware
// Middleware that applies conditionally
const conditionalMiddleware: Middleware = async (context, next) => {
// Only apply to production
if (context.env.NODE_ENV === 'production') {
// Add production-specific behavior
context.timeout = 300000;
context.retries = 5;
}
return next();
};
// Environment-specific middleware
const environmentMiddleware: Middleware = async (context, next) => {
const env = process.env.NODE_ENV || 'development';
switch (env) {
case 'production':
context.env.LOG_LEVEL = 'error';
break;
case 'staging':
context.env.LOG_LEVEL = 'warning';
break;
default:
context.env.LOG_LEVEL = 'debug';
}
return next();
};
Hook System
Lifecycle Hooks
// Register lifecycle hooks
$.addHook('beforeExecute', async (context) => {
console.log('Before executing:', context.command);
// Modify context if needed
context.env.TIMESTAMP = Date.now().toString();
});
$.addHook('afterExecute', async (context, result) => {
console.log('After executing:', context.command);
console.log('Result:', result.exitCode);
// Can modify result
result.metadata = { executedAt: Date.now() };
});
$.addHook('onError', async (context, error) => {
console.error('Error in command:', context.command);
console.error('Error details:', error);
// Can transform error
error.context = context;
});
Async Hooks
// Hooks can be async
$.addHook('beforeExecute', async (context) => {
// Fetch configuration from remote
const config = await fetch('https://api.example.com/config')
.then(r => r.json());
// Apply configuration
Object.assign(context.env, config.environment);
});
// Chain multiple hooks
$.addHook('afterExecute', async (context, result) => {
// Log to remote service
await logToService({
command: context.command,
result: result.exitCode,
duration: result.duration
});
});
$.addHook('afterExecute', async (context, result) => {
// Send metrics
await sendMetrics({
command: context.command,
success: result.exitCode === 0
});
});
Type Extensions
Extending Types
// Extend ExecutionEngine with custom methods
declare module '@xec-sh/core' {
interface ExecutionEngine {
deploy(target: string): Promise<any>;
backup(destination: string): Promise<any>;
healthCheck(): Promise<boolean>;
}
}
// Implement extensions
ExecutionEngine.prototype.deploy = async function(target: string) {
return this`deploy.sh ${target}`;
};
ExecutionEngine.prototype.backup = async function(destination: string) {
return this`backup.sh ${destination}`;
};
ExecutionEngine.prototype.healthCheck = async function() {
const result = await this`health-check.sh`.nothrow();
return result.ok;
};
// Now available with type safety
await $.deploy('production');
await $.backup('/backups/daily');
const healthy = await $.healthCheck();
Custom Result Types
// Extend result types
interface CustomResult extends ExecutionResult {
metrics?: {
cpuUsage: number;
memoryUsage: number;
networkIO: number;
};
logs?: string[];
}
// Custom adapter returning extended result
class MetricsAdapter extends BaseAdapter {
async execute(command: string, options: any): Promise<CustomResult> {
const result = await super.execute(command, options);
// Add custom metrics
return {
...result,
metrics: await this.collectMetrics(),
logs: await this.collectLogs()
};
}
private async collectMetrics() {
// Collect system metrics
return {
cpuUsage: 45.2,
memoryUsage: 1024,
networkIO: 5000
};
}
private async collectLogs() {
// Collect relevant logs
return ['Log entry 1', 'Log entry 2'];
}
}
Integration Examples
Database Integration
// Database command extension
class DatabaseExtension {
constructor(private dbConfig: any) {}
register(engine: ExecutionEngine) {
engine.addCommand('dbQuery', async function(this: ExecutionEngine, query: string) {
const result = await this`psql -c "${query}" ${this.dbConfig.connectionString}`;
return result.stdout;
});
engine.addCommand('dbBackup', async function(this: ExecutionEngine) {
const timestamp = new Date().toISOString();
const filename = `backup-${timestamp}.sql`;
await this`pg_dump ${this.dbConfig.connectionString} > ${filename}`;
return filename;
});
}
}
// Register extension
const dbExt = new DatabaseExtension({
connectionString: 'postgresql://localhost/mydb'
});
dbExt.register($);
// Use database commands
const users = await $.dbQuery('SELECT * FROM users');
const backupFile = await $.dbBackup();
Cloud Provider Integration
// AWS integration extension
class AWSExtension {
register(engine: ExecutionEngine) {
// Add S3 commands
engine.addCommand('s3Upload', async function(
this: ExecutionEngine,
local: string,
bucket: string
) {
return this`aws s3 cp ${local} s3://${bucket}/`;
});
// Add EC2 commands
engine.addCommand('ec2List', async function(this: ExecutionEngine) {
const result = await this`aws ec2 describe-instances --output json`;
return JSON.parse(result.stdout);
});
// Add Lambda commands
engine.addCommand('lambdaInvoke', async function(
this: ExecutionEngine,
functionName: string,
payload: any
) {
const payloadStr = JSON.stringify(payload);
return this`aws lambda invoke --function-name ${functionName} --payload '${payloadStr}' output.json`;
});
}
}
// Use AWS commands
await $.s3Upload('file.txt', 'my-bucket');
const instances = await $.ec2List();
await $.lambdaInvoke('my-function', { key: 'value' });
Best Practices
Do's ✅
// ✅ Keep extensions focused
class SinglePurposeExtension {
register(engine: ExecutionEngine) {
// Do one thing well
}
}
// ✅ Provide cleanup methods
class CleanableExtension {
async destroy() {
// Clean up resources
}
}
// ✅ Document extension behavior
/**
* Adds retry capability to all commands
*/
class RetryExtension { /* ... */ }
// ✅ Make extensions configurable
class ConfigurableExtension {
constructor(private options: ExtensionOptions) {}
}
Don'ts ❌
// ❌ Don't modify global state
class BadExtension {
register() {
process.env.NODE_ENV = 'production'; // Bad!
}
}
// ❌ Don't create naming conflicts
$.addCommand('exec', () => {}); // Conflicts with built-in
// ❌ Don't leak resources
class LeakyExtension {
register() {
setInterval(() => {}, 1000); // Never cleaned up
}
}
// ❌ Don't throw in initialization
class ThrowingExtension {
register() {
throw new Error('Failed'); // Will break everything
}
}
Implementation Details
Extensions are implemented in:
packages/core/src/core/extensions.ts
- Extension systempackages/core/src/core/plugin-manager.ts
- Plugin managementpackages/core/src/core/hook-system.ts
- Hook systempackages/core/src/core/middleware.ts
- Middleware support