Have you ever wished you could write shell scripts in TypeScript with the same ease as bash, but with type safety, better error handling, and the ability to seamlessly execute commands across SSH, Docker, and Kubernetes? Meet Xec - the universal command execution system that's changing how we think about shell automation.
The Problem: Context Switching Hell
As developers, we constantly switch between different execution contexts:
- Running commands locally during development
- SSHing into production servers for debugging
- Executing commands in Docker containers
- Managing Kubernetes pods
Each context traditionally requires different tools, APIs, and mental models. What if there was a better way?
Enter Xec: One API to Rule Them All
import { $ } from '@xec-sh/core'; // Same syntax, different contexts await $`echo "Hello from local machine"`; const server = $.ssh({ host: 'prod.example.com' }); await server`echo "Hello from SSH"`; const container = $.docker({ container: 'my-app' }); await container`echo "Hello from Docker"`; const pod = $.k8s({ namespace: 'production' }).pod('web-app'); await pod.exec`echo "Hello from Kubernetes"`;
That's it. Same API, same syntax, different execution environments. No more context switching.
π― Key Features That Make Xec Special
1. Template Literal Magic with Auto-Escaping
Remember the nightmare of escaping shell arguments? Xec handles it automatically:
const filename = "my file with spaces & special chars.txt"; const content = "Hello $USER"; // Variables are automatically escaped await $`echo ${content} > ${filename}`; // Executes: echo 'Hello $USER' > 'my file with spaces & special chars.txt' // Need raw shell interpretation? Use .raw await $.raw`echo $HOME`; // Shell interprets $HOME
2. Intelligent Connection Pooling
Xec automatically manages connections for optimal performance:
const remote = $.ssh({ host: 'server.com', username: 'deploy', privateKey: '~/.ssh/id_rsa' }); // These commands reuse the same SSH connection for (const service of ['nginx', 'app', 'redis']) { const status = await remote`systemctl status ${service}`; console.log(`${service}: ${status.stdout.includes('active') ? 'β
' : 'β'}`); } // Connection automatically closed when done
3. Real-World Error Handling
Unlike traditional shell scripts, Xec provides robust error handling:
// Don't throw on non-zero exit codes const result = await $`grep "pattern" file.txt`.nothrow(); if (!result.isSuccess()) { console.log('Pattern not found, using default...'); await $`echo "default content" > file.txt`; } // Automatic retries for flaky commands await $`curl https://api.example.com/webhook`.retry({ maxAttempts: 3, backoff: 'exponential', initialDelay: 1000 }); // Set timeouts try { await $`npm install`.timeout(30000); } catch (error) { console.error('Installation took too long!'); }
4. Streaming and Real-Time Output
Handle large outputs and long-running processes efficiently:
// Stream output in real-time await $`npm install`.stream(); // Custom stream processing await $`tail -f /var/log/app.log`.pipe(line => { if (line.includes('ERROR')) { console.error(`π¨ ${line}`); } }); // Process large files line by line let errorCount = 0; await $`find . -name "*.log"`.pipe(async (file) => { const errors = await $`grep -c ERROR ${file}`.nothrow(); if (errors.stdout) { errorCount += parseInt(errors.stdout); } }); console.log(`Total errors found: ${errorCount}`);
π Real-World Use Cases
1. Multi-Environment Deployment Script
#!/usr/bin/env xec import { $ } from '@xec-sh/core'; import { confirm, select } from '@clack/prompts'; // Select deployment environment const env = await select({ message: 'Choose deployment environment', options: [ { value: 'staging', label: 'Staging' }, { value: 'production', label: 'Production β οΈ' } ] }); // Run tests first console.log('π§ͺ Running tests...'); await $`npm test`; // Build the application console.log('π¨ Building application...'); await $`npm run build`; // Deploy based on environment if (env === 'staging') { // Direct SSH deployment for staging const staging = $.ssh({ host: 'staging.example.com' }); await staging`cd /app && git pull`; await staging`npm install --production`; await staging`pm2 restart app`; } else { // Kubernetes deployment for production const shouldContinue = await confirm({ message: 'Deploy to PRODUCTION?' }); if (shouldContinue) { const k8s = $.k8s({ namespace: 'production' }); // Build and push Docker image await $`docker build -t myapp:${Date.now()} .`; await $`docker push myapp:latest`; // Update Kubernetes deployment await k8s`kubectl set image deployment/web app=myapp:latest`; await k8s`kubectl rollout status deployment/web`; } } console.log('β
Deployment complete!');
2. Database Backup Across Environments
import { $ } from '@xec-sh/core'; import { parallel } from '@xec-sh/core'; async function backupDatabase(env: string, config: any) { const timestamp = new Date().toISOString().split('T')[0]; const filename = `backup-${env}-${timestamp}.sql`; if (config.type === 'ssh') { // Backup remote PostgreSQL const remote = $.ssh(config); await remote`pg_dump ${config.database} | gzip > /backups/${filename}.gz`; await remote.downloadFile(`/backups/${filename}.gz`, `./backups/${filename}.gz`); } else if (config.type === 'k8s') { // Backup from Kubernetes pod const pod = $.k8s(config).pod(config.podName); await pod.exec`pg_dump ${config.database} > /tmp/${filename}`; await pod.copyFrom(`/tmp/${filename}`, `./backups/${filename}`); await $`gzip ./backups/${filename}`; } return filename; } // Backup all databases in parallel const databases = [ { env: 'prod-us', type: 'ssh', host: 'db1.us.example.com', database: 'app_prod' }, { env: 'prod-eu', type: 'ssh', host: 'db1.eu.example.com', database: 'app_prod' }, { env: 'prod-k8s', type: 'k8s', namespace: 'production', podName: 'postgres-0', database: 'app' } ]; const results = await parallel( databases.map(db => () => backupDatabase(db.env, db)), { maxConcurrent: 2 } ); console.log('Backups completed:', results);
3. Container Management and Debugging
import { $ } from '@xec-sh/core'; class ContainerDebugger { async debug(containerName: string) { const container = $.docker({ container: containerName }); // Check if container is running const isRunning = await $`docker ps --format "{{.Names}}" | grep -q ${containerName}`.nothrow(); if (!isRunning.isSuccess()) { console.log('Container not running, starting it...'); await $`docker start ${containerName}`; } // Gather debug information console.log('\nπ Container Stats:'); await container`df -h`.stream(); console.log('\nπ Running Processes:'); await container`ps aux`.stream(); console.log('\nπ Network Connections:'); await container`netstat -tulpn`.stream(); console.log('\nπ Recent Logs:'); await $`docker logs --tail 50 ${containerName}`.stream(); // Interactive shell if needed const shouldEnterShell = await confirm({ message: 'Enter interactive shell?' }); if (shouldEnterShell) { // This will give you an interactive shell await $`docker exec -it ${containerName} /bin/bash`.stdio('inherit'); } } } const debugger = new ContainerDebugger(); await debugger.debug('my-app');
4. Advanced SSH Tunneling and Port Forwarding
import { $ } from '@xec-sh/core'; // Setup SSH tunnel for database access async function setupDatabaseTunnel() { const jumpHost = $.ssh({ host: 'bastion.example.com', username: 'admin' }); // Create tunnel through bastion to internal database const tunnel = await jumpHost.tunnel({ localPort: 5432, remoteHost: 'postgres.internal', remotePort: 5432 }); console.log(`Database available at localhost:${tunnel.localPort}`); // Now you can connect to the database locally await $`psql -h localhost -p ${tunnel.localPort} -U dbuser -d myapp -c "SELECT version();"`; // Tunnel automatically closes when done return tunnel; } // Kubernetes port forwarding async function debugKubernetesPod() { const k8s = $.k8s({ namespace: 'production' }); const pod = k8s.pod('web-app-7d4b8c-x2kl9'); // Forward multiple ports const forwards = await Promise.all([ pod.portForward(8080, 80), // HTTP pod.portForward(8443, 443), // HTTPS pod.portForward(9229, 9229) // Node.js debugger ]); console.log('Pod ports forwarded:'); console.log(`- HTTP: http://localhost:${forwards[0].localPort}`); console.log(`- HTTPS: https://localhost:${forwards[1].localPort}`); console.log(`- Debugger: chrome://inspect at localhost:${forwards[2].localPort}`); }
π Architecture That Scales
Xec's architecture is built on a simple but powerful adapter pattern:
// Core execution engine interface ExecutionAdapter { execute(command: string, options?: ExecutionOptions): ProcessPromise; dispose(): Promise<void>; } // Specialized adapters extend the base class SSHAdapter extends BaseAdapter { private connectionPool: SSHConnectionPool; // Manages SSH connections, tunnels, and file transfers } class DockerAdapter extends BaseAdapter { private containerManager: ContainerManager; // Handles container lifecycle and Docker operations } class KubernetesAdapter extends BaseAdapter { private k8sClient: K8sClient; // Manages kubectl commands and port forwarding }
This design allows for:
- Easy addition of new execution environments
- Consistent behavior across all adapters
- Optimal resource management per environment
π Getting Started
Installation
# Install the CLI globally npm install -g @xec-sh/cli # Install the core library for your project npm install @xec-sh/core
Your First Xec Script
Create deploy.ts
:
#!/usr/bin/env xec import { $ } from '@xec-sh/core'; // Build and test locally await $`npm test`; await $`npm run build`; // Deploy to staging const staging = $.ssh({ host: 'staging.myapp.com' }); await staging`cd /app && git pull && npm install`; await staging`pm2 restart all`; console.log('β
Deployed to staging!');
Make it executable and run:
chmod +x deploy.ts ./deploy.ts
π€ Why Xec Matters
In an era of microservices, containers, and distributed systems, we need tools that match our mental models. Xec brings:
- Unified Mental Model: Think about what you want to do, not how to do it in each environment
- Type Safety: Catch errors at compile time, not in production
- Modern JavaScript: Use async/await, destructuring, and all ES2022+ features
- Better Error Handling: No more silent failures or cryptic exit codes
- Developer Experience: Auto-completion, inline documentation, and familiar APIs
π― What's Next?
Xec is actively developed and has an exciting roadmap:
- AI Integration: Natural language to command translation
- Workflow Engine: Define complex multi-step operations declaratively
- Cloud Provider Adapters: Native AWS, GCP, Azure command execution
- Performance Monitoring: Built-in metrics and tracing
π Conclusion
Shell scripting doesn't have to be stuck in the 1970s. With Xec, you get the simplicity of shell commands with the power of TypeScript, all wrapped in a universal API that works everywhere.
Whether you're automating deployments, managing infrastructure, or building complex DevOps workflows, Xec provides the foundation for reliable, maintainable, and enjoyable shell automation.
Give it a try and let us know what you think!
Links:
- π GitHub Repository
- π Documentation
- π¦ npm Package
- π¬ Discord Community
Star us on GitHub if you find Xec useful! β
Top comments (0)