@scolastico-dev/orchestrator
v1.3.0
Published
SSH deployment orchestrator with a blessed TUI - upload assets, run scripts across servers in parallel
Downloads
349
Readme
s.Orchestrator
An SSH deployment orchestrator that uploads local assets and runs shell scripts across multiple remote servers in parallel, with a full-featured terminal UI (or plain log output for CI/CD environments).
┌─[web] ───────────────────────────────────────────────────────────────────────┐
│ Checking connection... │
│ Connection OK │
│ Creating remote base directory... │
│ Uploading scripts... │
│ Running: 01-install.sh │
│ [...] │
│ COMPLETED SUCCESSFULLY │
└──────────────────────────────────────────────────────────────────────────────┘
Switch: [TAB] | Scroll: [SHIFT]+[↑/↓] [PGUP/PGDN] | Quit: [CTRL+C] | ...Why s.Orchestrator?
When you need to provision a machine, deploy an app, or apply quick updates across a handful of servers, heavyweight configuration management tools often feel like overkill. s.Orchestrator gets out of your way. There is no complex syntax to learn - just write standard bash scripts, point them at a JSON config, and watch everything execute concurrently.
Compared to Ansible:
| | s.Orchestrator | Ansible |
|----------------------------|-----------------------------------------|-----------------------------------|
| Learning curve | Low - just bash + JSON | Medium - YAML, modules, playbooks |
| Target requirements | SSH only | SSH + Python on target |
| Idempotency | Manual (write your scripts defensively) | Built-in via modules |
| Parallel execution | Always | Configurable (forks) |
| Module ecosystem | None | Large (ansible-galaxy) |
| Live feedback | TUI with per-server log panes | stdout only |
| Config management at scale | Not suitable | Purpose-built |
Use s.Orchestrator when you manage a small cluster of servers, you already know the exact shell commands you want to run, and you want zero tooling overhead.
Use Ansible when you require built-in idempotency, complex configuration templating (Jinja2), or are managing large enterprise infrastructure.
Features
- Parallel deployment - all servers deploy simultaneously; per-server log panes in the TUI
- Host key enforcement - on first contact, fetches all key types offered by the server and prompts you to save them; on subsequent runs, verifies each stored key type against
known_hosts; all mismatches are reported together in a single run - TUI with tab switching - navigate between server log panes, scroll history, forward keystrokes to interactive SSH sessions
- Plain mode (
--ugly) - prefix-based log output suitable for CI/CD pipelines and logging - Dry-run mode - validates config and tests connections without executing any remote commands
- JSON schema export - emit a JSON Schema file for the config so editors can validate it
- Injected environment variables - every script receives a standard set of server variables automatically; additional per-server variables can be added via
envin the config - Cross-server script dependencies (
needs) - a server can wait before one of its scripts until other servers have finished a given script, receiving that script's output as an environment variable; circular dependencies are detected up front and abort the run - Optional key file - specify a per-server SSH private key; falls back to the SSH agent/defaults
- Per-server log files - each server's output is saved to
logs/<name>.log - Single-command exec (
-e/--exec) - run one ad-hoc command on all servers instead of the scripts directory (assets and scripts are still uploaded) - Server filter (
-s/--servers) - target a subset of servers by name; all others are skipped. Names may be given as partial (prefix) matches when unambiguous (an exact name always wins) - Interactive debug session (
--ssh) - upload assets and scripts to a single server (partial name allowed), then drop into an interactive SSH shell in the remote path with the injected environment exported; no scripts are executed and the uploaded files are left in place for debugging
Installation
From npm, installed globally
# Install with npm
npm i -g @scolastico-dev/orchestrator
# Then run it
s-orchestrator [options]
From npm, installed locally
# Install with npm
npm i @scolastico-dev/orchestrator
Then add a script to package.json:
{
"scripts": {
"deploy": "s-orchestrator [options]"
}
}
And run it with:
npm run deploy
From source
# Clone and build
git clone https://github.com/scolastico-dev/orchestrator.git
cd orchestrator
pnpm install
pnpm build
# Then run it
node dist/index.js [options]Quick Start
1. Create config.json
{
"web": {
"ip": "203.0.113.10",
"user": "deploy",
"env": {
"APP_ENV": "production"
}
},
"db": {
"ip": "203.0.113.20",
"port": 2222,
"keyFile": "~/.ssh/db_deploy"
}
}2. Add your scripts
It's important to prefix your scripts with numbers to ensure they run in the correct order, as they are executed in lexicographic order.
scripts/
10-update.sh
20-restart.shEnsure all scripts are written in a "upsert" style, meaning they can be run multiple times without causing issues. s.Orchestrator does not enforce idempotency, so your scripts should be designed to handle repeated executions gracefully.
3. Run
s-orchestratorOn first run, s.Orchestrator will fetch all SSH host key types for each server and ask you to confirm before saving them to config.json. Subsequent runs verify every stored key type and abort if any of them changes (TOFU - Trust On First Use).
See example/ for a complete working example that provisions two Debian servers: apt upgrade, SSH key setup, and Docker installation.
Configuration Reference
The config file is a JSON object whose keys are server names and whose values are server configs:
| Field | Type | Default | Description |
|------------------|-------------------------|----------|----------------------------------------------------------------------------------------------------|
| ip | string (required) | - | IP address or hostname |
| port | number | 22 | SSH port |
| user | string | "root" | SSH username |
| hostKeys | string[] | - | Stored host public keys for all key types (auto-populated on first run) |
| keyFile | string | - | Path to SSH private key file |
| knownHostsFile | string | - | Path to a custom known_hosts file (useful in CI/CD) |
| env | Record<string,string> | - | Extra environment variables injected into each script (merged on top of the default injected vars) |
| importedEnv | string[] | [] | Names of local shell environment variables to read at runtime and forward to each remote script |
| needs | Need[] | [] | Cross-server script dependencies — wait for other servers and receive their script output (see Script Dependencies) |
Export JSON Schema
s-orchestrator --schema # print schema to stdout
s-orchestrator --schema config.schema.json # write to fileAdd to config.json for editor validation:
{
"$schema": "./config.schema.json",
"web": { "ip": "..." }
}Injected Environment Variables
Every script execution receives the following environment variables automatically:
| Variable | Value |
|-------------------|--------------------------------------------------|
| SERVER_NAME | The server's key in config.json |
| SERVER_IP | The server's ip field |
| SERVER_USER | The SSH username (user, default root) |
| SERVER_SSH_PORT | The SSH port (port, default 22) |
| ASSET_DIR | Absolute remote path where assets were uploaded |
| SCRIPT_DIR | Absolute remote path where scripts were uploaded |
Variables defined in the server's env block are merged on top, so they can override any of the above if needed.
Importing local environment variables
importedEnv lets you forward specific variables from the shell that runs s.Orchestrator to each remote script — without hard-coding their values in config.json. Variables not present in the local environment are silently skipped. The import happens before the env block is applied, so env entries take precedence over imported values.
{
"web": {
"ip": "203.0.113.10",
"importedEnv": ["DB_PASSWORD", "API_KEY"]
}
}This is especially useful with dotenvx, which can decrypt an encrypted .env file and inject the secrets into the local shell before s.Orchestrator runs:
# Decrypt .env.vault, inject secrets locally, then forward listed ones to remote servers
dotenvx run --quiet -- sh -c 's-orchestrator'The lite version works the same way:
dotenvx run --quiet -- sh -c 'bash lite-version.sh'dotenvx makes DB_PASSWORD and API_KEY available in the local environment; importedEnv picks them up and forwards only those two to each remote script. Nothing else from the local environment is exposed.
Script Dependencies (needs)
needs lets one server pause before a specific script until other servers have finished a specific script — and receive that script's captured output as an environment variable. The classic use case is a Docker Swarm: workers must wait for the manager to initialize the swarm and need the join token it prints.
Each entry in a server's needs array has these fields:
| Field | Type | Required | Description |
|-------------|------------|----------|----------------------------------------------------------------------------------------------|
| server | string[] | no | Provider server names (exact config keys). Omitted = every other server in the run |
| env | string | no | Environment variable that receives the captured output of the depends script. Omitted = wait only, no output is saved or injected |
| depends | string | yes | Script that must finish on the provider server(s) first |
| depending | string | yes | Script on this server that waits until the dependency is satisfied |
{
"manager.swarm": { "ip": "203.0.113.10" },
"worker-1": {
"ip": "203.0.113.11",
"needs": [
{
"server": ["manager.swarm"],
"env": "TOKEN",
"depends": "20-setup-swarm.sh",
"depending": "25-add-to-swarm.sh"
}
]
}
}scripts/20-setup-swarm.sh — runs on every server, but only does work on the manager. Its output on the manager becomes the token:
#!/bin/sh
[ "$SERVER_NAME" = "manager.swarm" ] || exit 0
docker swarm init --advertise-addr "$SERVER_IP" >/dev/null 2>&1 || true
docker swarm join-token -q workerscripts/25-add-to-swarm.sh — the worker blocks here until the manager finished 20-setup-swarm.sh, then runs with $TOKEN set:
#!/bin/sh
[ "$SERVER_NAME" = "manager.swarm" ] && exit 0
docker swarm join --token "$TOKEN" 203.0.113.10:2377Semantics:
- All servers still run all scripts —
needsonly adds waiting points. Branch on$SERVER_NAMEinside scripts. - The env value is the entire output of the provider's
dependsscript execution (stderr included, since scripts run on a TTY), with trailing whitespace trimmed — keep the output of adependsscript clean and redirect noise to/dev/null. - With multiple providers (or an omitted
serverfield), the run waits for all of them; outputs are joined with newlines in provider order. - Omit
envto use the entry as a pure synchronization barrier — e.g. wait until another server finished restarting a service — without receiving any output. - Needs-injected variables are applied last (they override
envandimportedEnv) and only for thedependingscript's execution. - Validation happens before anything is deployed: unknown scripts or servers, providers excluded by
--servers, and circular dependencies (deadlocks) abort the run with an error instead of hanging. --execignoresneeds(no scripts run). Dry-run validatesneedswithout waiting.- The lite version handles
needssequentially: a blocked server is skipped, requeued at the end of the queue, and later resumes from the blocked script (no scripts are rerun). If the queue stops making progress (e.g. a provider failed), it exits with an error.
CLI Reference
Usage: s-orchestrator [options]
Options:
-v, --version print version and exit
-c, --config <path> path to config JSON file (default: "config.json")
-y, --skip-confirm skip confirmation after connection tests
-n, --dry-run test connections, do not deploy
--schema [path] export JSON schema for config
-u, --ugly plain log output, no TUI
--log-dir <path> directory for per-server log files (default: "logs")
--assets-dir <path> local assets directory to upload (default: "assets")
--scripts-dir <path> local scripts directory (default: "scripts")
--remote-path <path> remote working directory (default: "/tmp/s-orchestrator")
-e, --exec <command> run one command on each server instead of scripts dir
(assets and scripts are still uploaded)
-s, --servers <names> comma-separated server names to target; others are skipped
(partial/prefix names accepted when unambiguous)
--ssh <name> upload to a single server (partial name allowed), then
open an interactive SSH session for debugging (no TUI,
no scripts executed, files left in place)
-h, --help display helpExamples
# Production deploy with a dedicated config
s-orchestrator -c prod.json
# Skip confirmation prompt (for automated pipelines)
s-orchestrator -y
# Dry run - verify config and connections without deploying
s-orchestrator --dry-run --ugly
# CI/CD friendly - ugly mode, skip confirm, custom config
s-orchestrator --ugly -y -c /etc/deploy/config.json
# Export the JSON schema
s-orchestrator --schema > config.schema.json
# Run a single ad-hoc command across all servers
s-orchestrator --exec "systemctl restart nginx" -y
# Deploy only to specific servers (partial names accepted when unambiguous)
s-orchestrator --servers web,db-primary
s-orchestrator -s web # matches "web" exactly, or the only server starting with "web"
# Combine: one command, specific servers, no prompt
s-orchestrator --exec "docker pull myapp:latest" --servers web1,web2 -y
# Upload to a server and open an interactive SSH shell to debug (partial name ok)
s-orchestrator --ssh webDeployment Flow
For each run, s.Orchestrator:
- Validates the config file
- Enforces host keys - fetches all key types on first use (with confirmation), verifies each type on subsequent runs; all mismatches are collected and reported together; any newly confirmed keys are saved to the config even if mismatches are found
- Tests connections to all servers in parallel - aborts immediately if any connection fails
- Prompts to start (unless
--skip-confirm) - Deploys to all servers in parallel:
- Creates
<remote-path>/on the remote - Uploads
assets/(if present) - Uploads
scripts/(if present, even when--execis used) - Executes each
.shscript in lexicographic order or the single--execcommand, with injected environment variables — pausing before any script whoseneedsdependencies are not yet satisfied - Cleans up
<remote-path>/on the remote
- Creates
- Displays final status - Ctrl+C after this point exits cleanly without an abort prompt
Directory Layout
your-project/
├── config.json # server config (managed by s.Orchestrator)
├── assets/ # files uploaded verbatim (optional)
│ └── nginx.conf
└── scripts/ # executed in alphabetical order on each server
├── 01-update.sh
└── 02-restart.shLite Version
For environments where Node.js is unavailable, a self-contained bash implementation is provided in lite-version.sh. It shares the same config format and feature set but deploys servers sequentially and has no TUI.
Requirements: bash, curl, ssh, scp, ssh-keyscan, ssh-keygen, jq
# Run directly (no build or Node.js required)
curl -sSL https://raw.githubusercontent.com/scolastico-dev/orchestrator/main/lite-version.sh | bash -s -- [options]The lite version supports the same options as the full version except --ugly, --schema, and --ssh (it also accepts partial/prefix server names for --servers). It also self-updates: on each run, if it is installed as a file on disk and curl/md5sum are available, it checks the latest version from GitHub, compares checksums, and prompts you to update in place.
Options:
-c, --config <path> path to config JSON file (default: config.json)
-y, --skip-confirm skip confirmation after connection tests
-n, --dry-run test connections, do not deploy
--log-dir <path> directory for per-server log files (default: logs)
--assets-dir <path> local assets directory to upload (default: assets)
--scripts-dir <path> local scripts directory (default: scripts)
--remote-path <path> remote working directory (default: /tmp/s-orchestrator)
-e, --exec <command> run one command instead of the scripts dir
-s, --servers <names> comma-separated list of server names to target
(partial/prefix names accepted when unambiguous)| Feature | Full version | Lite version |
|--------------------------------|--------------|-----------------|
| Parallel deployment | yes | no - sequential |
| TUI / plain mode | yes | plain only |
| Host key enforcement (TOFU) | yes | yes |
| Env injection | yes | yes |
| needs script dependencies | yes | yes - via requeue |
| Dry-run mode | yes | yes |
| Per-server log files | yes | yes |
| keyFile / knownHostsFile | yes | yes |
| --exec / --servers | yes | yes |
| Short aliases (-e/-s/-u) | -e -s -u | -e -s |
| Partial/prefix server names | yes | yes |
| --ssh interactive session | yes | no |
| Self-update check | no | yes |
| JSON schema export | yes | no |
| Node.js required | yes | no |
Development
pnpm install
# Type check
pnpm typecheck
# Run tests (unit tests run always; Docker integration tests require Docker)
pnpm test
pnpm test:watch
# Build (Parcel compiles TypeScript → dist/index.js)
pnpm build
# Run from source (no build needed)
pnpm dev -- --helpProject Structure
src/
├── index.ts entry point, main()
├── cli.ts CLI argument parsing (commander)
├── types.ts shared TypeScript types
├── config.ts Zod schema, load / save / export-schema
├── logger.ts per-server file logger
├── ssh/
│ ├── executor.ts execRemoteStreaming, execRemoteSimple, scpUpload
│ ├── host-keys.ts TOFU host key enforcement
│ ├── connection.ts connection health check
│ └── index.ts barrel export
├── deploy/
│ ├── steps.ts single-server deploy steps (env injection, script execution)
│ └── index.ts multi-server orchestration
└── ui/
├── types.ts OrchestratorUI interface
├── tui.ts blessed TUI implementation
└── plain.ts plain stdout implementation
tests/
├── fixtures/sshd/ Alpine+OpenSSH Docker image for integration tests
├── helpers/docker.ts Docker container lifecycle management
├── config.test.ts config validation unit tests
├── ssh.test.ts SSH executor integration tests (requires Docker)
├── deploy.test.ts deployment integration tests (requires Docker)
└── lite-version.test.ts lite-version.sh integration tests (requires Docker)Testing
# All tests (unit + Docker integration)
pnpm test
# Unit tests only (no Docker required)
pnpm test tests/config.test.tsIntegration tests spin up Alpine+OpenSSH Docker containers automatically, run the deployment against them, and clean up on completion. They are skipped gracefully when Docker is unavailable.
Security
- Host key verification uses SSH's
StrictHostKeyChecking=yes- connections to unknown hosts are rejected - TOFU (Trust On First Use) - the first time a server is seen, the user must explicitly approve all offered key types; they are then stored as an array in
config.json - Key mismatch = hard abort - if any stored key type doesn't match
~/.ssh/known_hosts, s.Orchestrator exits with an error listing all conflicting entries and thessh-keygen -Rcommands needed to resolve them; any newly confirmed keys are saved before aborting so they don't need to be re-confirmed after the conflict is resolved - Scripts run with the permissions of the configured SSH user - use a least-privilege deploy user where possible
License
This project is licensed under the MIT License.
About
MIT
A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code.
What you can do
| Permissions | Conditions | Limitations | |-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| | 🟢 Commercial useThe licensed material and derivatives may be used for commercial purposes. | 🔵 License and copyright noticeA copy of the license and copyright notice must be included with the licensed material. | 🔴 LiabilityThis license includes a limitation of liability. | | 🟢 DistributionThe licensed material may be distributed. | | 🔴 WarrantyThis license explicitly states that it does NOT provide any warranty. | | 🟢 ModificationThe licensed material may be modified. | | | | 🟢 Private useThe licensed material may be used and modified in private. | | |
Information provided by https://choosealicense.com/licenses/mit/
This information is provided for general understanding and is not legal advice.
