# Openship CommandExecutor: Local vs Remote Server Operations Explained

> Explore Openship CommandExecutor for seamless local and remote server operations. Learn how LocalExecutor and SshExecutor unify deployments across any infrastructure. Optimize your workflows today.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: deep-dive
- Published: 2026-08-19

---

**Openship unifies host-side actions behind a common CommandExecutor interface, using LocalExecutor for same-machine operations and SshExecutor for remote SSH workflows to enable consistent deployments across any infrastructure.**

Openship deploys applications to diverse targets through a consistent abstraction layer defined in the `oblien/openship` repository. The platform's **CommandExecutor** interface enables the control plane to run shell commands, manage files, and stream logs whether targeting a local development machine or a remote production server. Understanding the `LocalExecutor` and `SshExecutor` implementations is essential for debugging deployment pipelines and extending the framework's capabilities.

## Understanding the CommandExecutor Interface

The `CommandExecutor` interface in [`packages/adapters/src/types.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/types.ts) (lines 715-845) declares the contract that both local and remote executors must fulfill. This TypeScript interface abstracts command execution, file system operations, and network tunneling behind a unified API.

Key methods include:

- `exec(command, opts?)`: Runs commands and returns stdout as a string, throwing on non-zero exits
- `streamExec(command, onLog, opts?)`: Streams real-time output through a callback function
- File helpers: `writeFile`, `readFile`, `exists`, `mkdir`, `rm`, and `rename`
- `transferIn(localPath, remotePath, onLog?, options?)`: Uploads local directory trees to remote hosts
- `dispose()`: Releases resources and closes connections

## LocalExecutor: Same-Machine Operations

The `LocalExecutor` class resides in [`packages/adapters/src/system/local-executor.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/local-executor.ts) and implements host operations using Node.js native APIs. This executor activates when Openship runs in **bare mode**, where the control plane and target host are the same physical machine.

### Command Execution and Streaming

The `exec` method wraps Node's `child_process.exec`, applying a default **30-second timeout** and merging stdout and stderr into error messages when commands fail. According to the source code at lines 25-46, it captures both output streams to ensure error details from tools like `certbot` are not lost.

For streaming operations, `streamExec` (lines 49-78) spawns processes using `child_process.spawn`. It forwards raw byte streams unchanged—including carriage-return characters—and caps retained output to the most recent **200 chunks** to prevent memory leaks during long-running processes.

### Direct File System Access

File operations bypass abstraction layers entirely. The `writeFile`, `readFile`, and `exists` methods directly invoke `fs/promises` APIs (lines 18-44), providing native disk performance without network overhead.

## SshExecutor: Remote Server Operations

When deploying to remote infrastructure, Openship utilizes `SshExecutor` from [`packages/adapters/src/system/ssh-executor.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/ssh-executor.ts). This implementation leverages the **ssh2** library to maintain persistent connections and handles network instability through transparent retry logic.

### Persistent SSH Connection Management

The executor memoizes connection promises via the `connect` method (lines 73-102), clearing the memo on failure to guarantee fresh reconnections after idle timeouts. This lazy-connection pattern ensures SSH channels establish only when needed while maintaining resilience against dropped connections.

### Remote Command Execution with Timeouts

The `exec` method prefixes commands with environment variables like `DEBIAN_FRONTEND=noninteractive` to force non-interactive package manager behavior. As implemented in lines 28-90, it supports per-command timeouts and abort handling for disappeared transports.

For streaming, `streamExec` (lines 92-104) automatically retries on "channel open failure" errors—a common occurrence after SSH idle timeouts—ensuring log streaming resilience without manual reconnection logic.

### Resumable File Transfers and SFTP

All remote file I/O flows through a shared SFTP channel wrapped in `withChannelRetry` logic to recover from stale connections. The `transferIn` method (lines 112-154) implements intelligent upload strategies: it first creates a local tarball, then prefers **rsync** when available on the remote host, falling back to a custom resumable SFTP uploader (`sftpUploadResumable`, lines 74-118) that tracks progress, detects stalled transfers, and retries up to **four times** before failing.

### Reverse and Forward Port Tunnels

The executor supports sophisticated networking scenarios through tunneling methods. `reverseForward` (lines 56-83) creates temporary remote listeners that forward incoming connections to local callbacks, while `forwardPort` (lines 36-48) establishes direct TCP tunnels from the remote host to local addresses—critical for accessing databases and debugging services behind firewalls.

## How Openship Selects the Executor

The factory function in [`packages/adapters/src/system/executor.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/executor.ts) (lines 48-62) determines which implementation to instantiate. For **bare mode** installations without Docker, it returns a singleton `LocalExecutor` because the host is the local machine. For **compose mode** deployments targeting remote Docker hosts, it provisions a new `SshExecutor` configured with the target server's SSH credentials.

## Code Examples for Openship CommandExecutor Operations

### Running One-Off Commands Locally

```typescript
import { LocalExecutor } from "@repo/adapters";

const exec = new LocalExecutor();
const kernelVersion = await exec.exec("uname -a");
console.log(kernelVersion); // "Linux hostname 5.15.0 ..."

```

### Streaming Logs from Long-Running Processes

```typescript
const exec = new LocalExecutor();

await exec.streamExec(
  "npm run build",
  (log) => console.log(`[${log.level}] ${log.text}`),
  { signal: new AbortController().signal }
);

```

### Executing Commands via SSH

```typescript
import { SshExecutor } from "@repo/adapters";
import { readFile } from "fs/promises";

const ssh = new SshExecutor({
  host: "production.example.com",
  username: "deploy",
  privateKey: await readFile("/home/user/.ssh/id_rsa"),
});

const containers = await ssh.exec("docker ps --format '{{.Names}}'");
console.log("Running containers:", containers.split("\n"));

```

### Uploading Directories with Resumable Transfers

```typescript
const ssh = new SshExecutor({ host, username, privateKey });

await ssh.transferIn(
  "./my-project",
  "/var/www/my-project",
  (log) => process.stdout.write(log.text),
  { excludes: ["node_modules", ".git", "*.tmp"] }
);

```

### Interactive Shells and Reverse Tunnels

```typescript
// Interactive shell for debugging
const shell = await ssh.openShell({ cols: 120, rows: 30 });
shell.stdin.write("tail -f /var/log/nginx/error.log\n");
shell.stdout.on("data", (data) => process.stdout.write(data));

// Reverse tunnel for remote service access
const { port, close } = await ssh.reverseForward((stream) => {
  // Forward remote connections to local service
  stream.pipe(localService).pipe(stream);
});
console.log(`Tunnel active on remote port ${port}`);
// Cleanup when done: await close();

```

## Summary

- Openship abstracts deployment operations through the **CommandExecutor** interface defined in [`packages/adapters/src/types.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/types.ts)
- **LocalExecutor** handles same-machine operations using Node.js `child_process` and `fs/promises` for command execution and file management
- **SshExecutor** manages remote hosts via SSH2, featuring automatic reconnection, resumable uploads with rsync fallback, and port forwarding capabilities
- Both executors implement identical method signatures for `exec`, `streamExec`, file operations, and `transferIn`, ensuring deployment logic remains agnostic to target location
- The factory in [`packages/adapters/src/system/executor.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/executor.ts) automatically selects the appropriate implementation based on bare mode versus compose mode deployment configuration

## Frequently Asked Questions

### What is the difference between LocalExecutor and SshExecutor in Openship?

**LocalExecutor** runs commands on the same machine as the Openship control plane using Node.js native APIs like `child_process.exec` and `fs/promises`, while **SshExecutor** connects to remote servers via the ssh2 library to execute commands, transfer files via SFTP, and manage network tunnels. Both implement the same `CommandExecutor` interface defined in [`packages/adapters/src/types.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/types.ts), allowing Openship to deploy consistently across local development environments and remote production servers without changing deployment logic.

### How does Openship handle file uploads to remote servers?

The `SshExecutor.transferIn` method first creates a local tarball of the project directory, then attempts to use **rsync** for efficient transmission if available on the remote host. If rsync is unavailable, it falls back to a custom resumable SFTP uploader that detects stalled transfers and retries up to four times, ensuring reliable deployment even over unstable connections or large file transfers.

### What happens when an SSH connection drops during command execution?

`SshExecutor` implements automatic retry logic through the `withChannelRetry` wrapper and connection memoization in the `connect` method. If a "channel open failure" occurs—typically after SSH idle timeouts—the executor clears the stale connection promise and establishes a fresh SSH session before retrying the operation, allowing long-running deployments to recover without manual intervention.

### How do I choose between exec and streamExec when running commands?

Use `exec` for short-lived commands that return finite output, as it buffers the entire response and applies a 30-second timeout by default. Use `streamExec` for long-running processes like build scripts or development servers, as it provides real-time log entries through a callback interface and supports cancellation via `AbortSignal` for graceful shutdowns.