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)