Skip to main content

Local Target Overview

Implementation Reference​

Source Files:

  • packages/core/src/adapters/local-adapter.ts - Local execution adapter implementation
  • packages/core/src/utils/shell.ts - Shell detection and escaping utilities
  • apps/xec/src/config/types.ts - Target type definitions (lines 45-52)
  • packages/core/src/core/execution-engine.ts - Execution engine integration

Key Classes:

  • LocalAdapter - Implements local command execution
  • ExecutionEngine - Core execution orchestrator

Key Functions:

  • LocalAdapter.execute() - Main execution method (lines 25-68)
  • LocalAdapter.spawn() - Process spawning (lines 70-112)
  • detectShell() - Shell detection logic
  • escapeShellArg() - Shell argument escaping

Overview​

Local targets execute commands directly on the machine where Xec is running. This is the default and simplest execution environment, providing direct access to the local filesystem, processes, and system resources.

Target Configuration​

Basic Configuration​

# .xec/config.yaml
targets:
local:
type: local
shell: /bin/bash # Optional: override default shell
env: # Optional: environment variables
NODE_ENV: development
PATH: /usr/local/bin:/usr/bin:/bin
cwd: /project # Optional: working directory

Default Local Target​

When no target is specified, Xec uses an implicit local target:

// Implicit local target (from LocalAdapter constructor)
{
type: 'local',
shell: process.env.SHELL || '/bin/sh',
env: process.env,
cwd: process.cwd()
}

Execution Model​

Process Spawning​

Local execution uses Node.js child_process.spawn() internally:

// From LocalAdapter.spawn() implementation
const child = spawn(command, args, {
cwd: options.cwd || process.cwd(),
env: { ...process.env, ...options.env },
shell: options.shell || true,
stdio: options.stdio || 'pipe',
detached: options.detached || false
});

Shell Detection​

The adapter automatically detects the available shell (from utils/shell.ts):

  1. Environment Variable: $SHELL environment variable
  2. Windows Detection: process.platform === 'win32' β†’ cmd.exe or PowerShell
  3. Unix Fallback: /bin/sh as universal fallback
// Shell detection priority
const shell = options.shell
|| process.env.SHELL
|| (isWindows ? 'cmd.exe' : '/bin/sh');

Command Execution​

Template Literal API​

import { $ } from '@xec-sh/core';

// Simple command
const result = await $`ls -la`;
console.log(result.stdout);

// With error handling
const { ok, stdout, stderr, exitCode } = await $`test -f file.txt`.nothrow();
if (!ok) {
console.error('File not found');
}

// Piping and chaining
await $`cat file.txt`.pipe($`grep pattern`).pipe($`wc -l`);

Direct Execution​

const adapter = new LocalAdapter();

// Execute with options
const result = await adapter.execute('npm install', {
cwd: '/project',
env: { NODE_ENV: 'production' },
timeout: 60000
});

Features​

Environment Variables​

Local execution inherits and can override environment variables:

// Inherits process.env by default
await $`echo $HOME`; // Uses current HOME

// Override specific variables
await $.env({ NODE_ENV: 'production' })`npm run build`;

// Clear environment
await $.env({})`printenv`; // Empty environment

Working Directory​

Control the execution directory:

// Change directory for execution
await $.cwd('/tmp')`pwd`; // Outputs: /tmp

// Temporary directory change
await $.within(async () => {
await $.cd('/project');
await $`npm install`;
await $`npm test`;
}); // Returns to original directory

Input/Output Handling​

Stream Processing​

// Capture output
const { stdout, stderr } = await $`ls -la`;

// Stream to console
await $`npm install`.pipe(process.stdout);

// Redirect to file
await $`echo "content"`.pipe(fs.createWriteStream('output.txt'));

// Process line by line
await $`tail -f log.txt`.lines(async (line) => {
console.log(`Log: ${line}`);
});

Input Redirection​

// From string
await $`cat`.stdin('Hello, World\n');

// From file
await $`wc -l`.stdin(fs.createReadStream('input.txt'));

// From another command
await $`echo "test"`.pipe($`cat`);

Signal Handling​

// Handle process signals
const proc = $`sleep 100`;

// Send signal
setTimeout(() => proc.kill('SIGTERM'), 1000);

// Handle termination
proc.on('exit', (code, signal) => {
console.log(`Process exited: ${code || signal}`);
});

Performance Characteristics​

Execution Overhead​

Based on implementation analysis:

  • Process Spawn: 5-10ms (Node.js child_process overhead)
  • Shell Invocation: +2-5ms (shell interpreter startup)
  • Environment Setup: <1ms (environment variable copying)
  • Working Directory Change: <1ms (process.chdir)

Memory Usage​

  • Per Process: 5-10MB (child process overhead)
  • Output Buffering: Variable (depends on stdout/stderr size)
  • Stream Mode: Constant memory (no buffering)

Optimization Strategies​

  1. Avoid Shell When Possible:
// Slower (invokes shell)
await $`echo hello`;

// Faster (direct execution)
await $.noshell()`echo`, ['hello']);
  1. Use Streaming for Large Output:
// Memory intensive (buffers all output)
const { stdout } = await $`find / -type f`;

// Memory efficient (streams output)
await $`find / -type f`.pipe(process.stdout);
  1. Batch Operations:
// Inefficient (multiple shell invocations)
await $`mkdir dir1`;
await $`mkdir dir2`;
await $`mkdir dir3`;

// Efficient (single shell invocation)
await $`mkdir dir1 dir2 dir3`;

Error Handling​

Exit Codes​

Local adapter preserves process exit codes:

try {
await $`exit 42`;
} catch (error) {
console.log(error.exitCode); // 42
}

Error Types​

Error ClassConditionExit Code
ExecutionErrorNon-zero exit codeProcess exit code
TimeoutErrorExecution timeout10
FileSystemErrorCommand not found127
PermissionErrorPermission denied126

Signal Handling​

// Handle specific signals
const proc = $`long-running-process`;

process.on('SIGINT', () => {
proc.kill('SIGTERM');
process.exit(130); // Standard SIGINT exit code
});

Security Considerations​

Command Injection Prevention​

The adapter provides automatic escaping:

const userInput = "'; rm -rf /";

// Safe - automatically escaped
await $`echo ${userInput}`;
// Executes: echo ''"'"'; rm -rf /'

// Manual escaping
const escaped = escapeShellArg(userInput);
await $`echo ${escaped}`;

Environment Isolation​

// Sanitize environment
const cleanEnv = {
PATH: '/usr/local/bin:/usr/bin:/bin',
HOME: process.env.HOME,
USER: process.env.USER
};

await $.env(cleanEnv)`sensitive-command`;

Platform Differences​

Unix/Linux/macOS​

  • Default Shell: /bin/sh or $SHELL
  • Path Separator: :
  • Null Device: /dev/null
  • Temp Directory: /tmp or $TMPDIR

Windows​

  • Default Shell: cmd.exe or PowerShell
  • Path Separator: ;
  • Null Device: NUL
  • Temp Directory: %TEMP% or %TMP%
// Platform-aware execution
const isWindows = process.platform === 'win32';

if (isWindows) {
await $`dir`; // Windows
} else {
await $`ls`; // Unix-like
}

Best Practices​

1. Use Appropriate Shell​

// For simple commands, avoid shell overhead
await $.noshell()`node`, ['script.js'];

// For complex pipelines, use shell
await $`cat file | grep pattern | sort | uniq`;

2. Handle Errors Gracefully​

const result = await $`command`.nothrow();
if (!result.ok) {
// Handle error without throwing
console.error(`Command failed: ${result.stderr}`);
}

3. Set Timeouts for Long Operations​

await $`npm install`.timeout(60000); // 60 seconds

4. Use Streaming for Large Data​

// Stream large files
await $`cat large-file.txt`
.pipe($`grep pattern`)
.pipe(fs.createWriteStream('filtered.txt'));

5. Clean Up Resources​

const proc = $`tail -f log.txt`;

// Ensure cleanup
process.on('exit', () => proc.kill());

Troubleshooting​

See Local Troubleshooting Guide for common issues and solutions.