DEV Community

LuxQuant
LuxQuant

Posted on

Xec - Write Once, Execute Anywhere: Universal Shell Commands in TypeScript

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"`; 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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!'); } 
Enter fullscreen mode Exit fullscreen mode

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}`); 
Enter fullscreen mode Exit fullscreen mode

πŸ›  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!'); 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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'); 
Enter fullscreen mode Exit fullscreen mode

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}`); } 
Enter fullscreen mode Exit fullscreen mode

πŸ— 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 } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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!'); 
Enter fullscreen mode Exit fullscreen mode

Make it executable and run:

chmod +x deploy.ts ./deploy.ts 
Enter fullscreen mode Exit fullscreen mode

🀝 Why Xec Matters

In an era of microservices, containers, and distributed systems, we need tools that match our mental models. Xec brings:

  1. Unified Mental Model: Think about what you want to do, not how to do it in each environment
  2. Type Safety: Catch errors at compile time, not in production
  3. Modern JavaScript: Use async/await, destructuring, and all ES2022+ features
  4. Better Error Handling: No more silent failures or cryptic exit codes
  5. 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:

Star us on GitHub if you find Xec useful! ⭐

Top comments (0)