Openship CommandExecutor: Local vs Remote Server Operations Explained
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 (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 exitsstreamExec(command, onLog, opts?): Streams real-time output through a callback function- File helpers:
writeFile,readFile,exists,mkdir,rm, andrename transferIn(localPath, remotePath, onLog?, options?): Uploads local directory trees to remote hostsdispose(): Releases resources and closes connections
LocalExecutor: Same-Machine Operations
The LocalExecutor class resides in 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. 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 (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
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
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
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
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
// 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 - LocalExecutor handles same-machine operations using Node.js
child_processandfs/promisesfor 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, andtransferIn, ensuring deployment logic remains agnostic to target location - The factory in
packages/adapters/src/system/executor.tsautomatically 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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →