Skip to main content

Relay: local-first remote execution over SSH with managed workspace sync

Project description

Relay

Relay is a local-first remote execution tool for any SSH-accessible host. It makes a remote workspace feel local by handling workspace sync, path mapping, staged transfers, and remote command execution for you.

Use it when you want to keep editing locally but run builds, tests, or tools on a remote Linux machine.

Installation

Relay is a Python-based CLI that depends on several external tools. We recommend installing it using pipx or uv tool to keep it isolated from your system Python.

1. Install external prerequisites

Relay does not install these for you. Ensure they are available in your PATH:

  • Local machine: ssh, mutagen, and rsync.
  • Remote host: /bin/sh, python3, and rsync.

See the Prerequisites section for details.

2. Install Relay

# Using uv (recommended)
uv tool install relayctl

# Using pipx
pipx install relayctl

3. Verify installation

Run the doctor command to check your environment:

relayctl doctor

If you have leftover seas artifacts (like a .seas.toml config or a .seas/ directory), relayctl will report them as cleanup issues. These legacy surfaces are unsupported and must be removed or renamed before you can use Relay.

Quick Start

  1. Ensure prerequisites are met.
  2. Install relayctl via pipx or uv tool.
  3. Run relayctl init in your project root and answer the setup prompt.
  4. Run a command: relayctl -- pwd (the -- separator is required).

By default, relayctl, relayctl --shell, and related top-level commands print live progress updates to stderr as they move through setup, sync, upload, execution, and pullback. Add --verbose to see the individual underlying command steps as well.

Configuration (.relay.toml)

Place a .relay.toml file in your project root to configure the remote host and sync behavior. relayctl init can create it interactively, and you can also start from .relay.example.toml.

[remote]
host = "user@example.com"
workspace_base = "~/.relay/workspaces"
shell = "/bin/sh"
profile = "~/.profile"

[project]
root = "."
exclude = [".git/", ".relay/", ".DS_Store", "__pycache__/", ".pytest_cache/"]

[pull]
enabled = true
conflict_dir = ".relay/conflicts"

[project] controls what gets mirrored into the managed remote workspace. Keep machine-local or generated files in exclude so Mutagen does not try to sync them.

The canonical global config path is ~/.config/relay/config.toml. If an older .seas.toml or ~/.config/seas/config.toml is still present, relayctl fails fast and asks you to rename or delete the legacy file before continuing.

If a stale local .seas/ runtime directory is still present, relayctl warns and ignores it. Relay only creates and uses .relay/ runtime state.

Root Detection

relayctl finds your project root by looking for the nearest .git directory. If no .git directory is found, it defaults to the current working directory. You can override this with the --root flag.

Workspace Synchronization

Relay uses a managed Mutagen workspace for continuous, high-performance file synchronization. This is the only supported synchronization model.

Key Characteristics

  • Automatic Session Management: relayctl automatically creates and manages a labeled Mutagen session for your project on the first run.
  • Derived Remote Workspace: The remote workspace path is derived automatically from remote.workspace_base and a unique workspace identity. You do not need to configure a manual remote directory.
  • Sync Ownership: Mutagen owns the synchronization of all files within the project root. Relay-native conflict handling and pullback do not apply to in-project files.
  • External Path Staging: relayctl still handles the staging of absolute paths from outside your project (e.g., /tmp/data.txt in your command argv) using rsync.
  • Drift Handling: If the managed session's configuration (host, paths, ignores) changes, relayctl will detect the "drift". In interactive terminals, it will prompt to recreate the session; in non-interactive environments, it will fail with a clear error.

Lifecycle Commands

Use relayctl mutagen to inspect and control the managed session:

  • relayctl mutagen status: Show the current session status (absent, healthy, or drifted).
  • relayctl mutagen flush: Force a synchronization cycle (useful before running a command if you just saved a file).
  • relayctl mutagen reset: Manually recreate the session (requires interactive confirmation).
  • relayctl mutagen terminate: Permanently remove the managed session for the current project.

testing

Use the repo's existing commands when validating changes:

uv run pytest -q
just test
just smoke-fake

just smoke-remote is available for a real-host smoke test, but it requires a valid local .relay.toml and a reachable SSH target.

Release automation

GitHub Actions now verifies Relay in two stages:

  • CI runs uv run pytest -q, builds distributions with uv build, and install-smokes the built wheel in a clean virtualenv by invoking the installed relayctl entrypoint.
  • Release re-runs the same verification before any release action.

Use the Release workflow's manual workflow_dispatch path for a non-publishing beta dry run. Actual PyPI publishing is reserved for beta tags matching v*.*.*b*, and the publish job is gated behind the test, build, and install-smoke jobs with trusted publishing enabled through GitHub's pypi environment.

Before the first real beta publish, register .github/workflows/release.yml as a trusted publisher for the relayctl project on PyPI and protect the repository's pypi environment so release approval stays gated.

Usage

Root command model

Use bare relayctl -- ... for argv-safe execution, and explicit top-level subcommands when you want a different flow:

  • relayctl -- CMD [ARG ...]: primary bare argv mode with automatic path rewriting.
  • relayctl --shell -- 'SHELL TEXT': explicit shell-text mode (shorthand -s).
  • relayctl ssh: open an interactive shell in the managed workspace.
  • relayctl init: write project config.
  • relayctl mutagen: inspect and control the managed Mutagen session.
  • relayctl doctor: check local prerequisites and stale legacy artifacts, plus remote prerequisites when a concrete host is configured.

If you type an ambiguous root command like relayctl echo ok, Relay will ask you to choose between relayctl -- echo ok and relayctl --shell -- 'echo ok'.

relayctl doctor

Use relayctl doctor to run local diagnostics before your first real remote run or when troubleshooting an environment. When a concrete host is configured, doctor also checks the remote prerequisite contract.

relayctl doctor
relayctl doctor --json

Doctor always checks local ssh, Mutagen usability, and conditional rsync support. It also reports stale local .seas/ directories and matching active legacy Seas-managed Mutagen sessions as cleanup issues. If your config still uses the placeholder host, doctor stays usable for local-only diagnostics and skips remote checks. If a concrete host is configured, doctor also verifies remote /bin/sh, python3, and staged-transfer rsync support. It never tries to install OS packages for you.

relayctl (Bare Argv Mode)

Use bare relayctl -- ... for standard commands where you want automatic path rewriting (argv).

relayctl requires an explicit -- separator before command argv. If omitted, argparse exits with a plain usage error.

Relative argv paths are resolved against your local cwd first. If the resolved path stays inside the project root, Relay rewrites it into the mirrored remote workspace; if it resolves outside the root, Relay stages it as a local external path. Use remote: explicitly when you want a relative token to stay relative to the remote cwd instead.

relayctl -- make test
relayctl -- python3 -m pytest -q
relayctl --verbose -- make build

Deterministic Path Rules:

Input Pattern Interpretation Remote Result
relative/path Resolve against local CWD first Mirror rewrite if in root; otherwise staged as local external
../outside/path Relative path resolving outside project root Staged to remote slot and rewritten
/abs/path/in/root Inside project root Mapped to remote mirror path
/abs/path/outside Outside project root Staged to remote slot and rewritten
local:relative/or/absolute Force local interpretation Resolve locally, then apply mirror/staging rules
remote:relative/or/absolute Force literal remote path Passed through unchanged
  • Symlinks that escape the project root are rejected for safety.
  • Attached forms (e.g., --flag=/tmp/x) are not rewritten in this version.

Note on Redirection: Shell features like < input.txt or | grep ... are handled by your local shell before relayctl runs. To use these features on the remote host, use relayctl --shell.

relayctl --shell (Shell Mode)

Use relayctl --shell (or -s) when you need complex shell features like pipes, redirects, or multiple commands in a single string (shell-mode).

relayctl --shell requires an explicit -- separator before shell text. If omitted, argparse exits with a plain usage error.

relayctl --shell -- 'g++ -o main main.cpp && ./main < input.txt'
relayctl --shell --verbose -- 'g++ -o main main.cpp && ./main < input.txt'

Guardrails and limitations:

  • No shell-text parsing: relayctl --shell does not look inside your shell string to rewrite paths (no shell-text parsing).
  • Explicit staging: If you need files from outside your project, use the --stage flag (explicit --stage).
  • Environment: relayctl --shell exports RELAY_STAGE_DIR, RELAY_REMOTE_ROOT, RELAY_REMOTE_CWD, and RELAY_RUN_ID to the remote environment.
  • Legacy runtime state: stale local .seas/ directories are ignored with a warning and are never migrated into .relay/.
  • limitations: relayctl --shell does not support automatic path rewriting; all paths must be relative to the project root or explicitly staged.

relayctl ssh (Interactive Mode)

Use relayctl ssh to open an interactive shell in your remote workspace.

relayctl ssh
relayctl ssh --workdir remote:/tmp

Guardrails:

  • Interactive-only: relayctl ssh does not support a command tail or shell-text payload.

Shared Flags

  • --workdir PATH: Set the remote working directory. Supports remote:/abs/path for literal remote paths. Available for bare relayctl, relayctl --shell, and relayctl ssh.
  • --env KEY=VAL: Set a remote environment variable. Can be repeated. Keys starting with RELAY_ are reserved. Available for bare relayctl and relayctl --shell.

Conflict Handling (Staged External Paths)

For absolute paths outside your project root (staged external paths), relayctl automatically pulls changed files back to your local machine after the command finishes.

If a local file was modified while the remote command was running, relayctl will not overwrite it. Instead:

  1. The remote version is saved in .relay/conflicts/<run-id>/.
  2. A conflict summary is printed.
  3. relayctl exits with code 92.

Files within the project root are managed by Mutagen, which handles synchronization and conflict resolution continuously.

exit code

  • 0-255: Remote command exit code is returned unchanged unless a wrapper-reserved condition below applies.
  • 90: Local setup/config/runtime/report error.
  • 91: Workspace lock timeout (prevents concurrent mirror corruption).
  • 92: Pull conflict detected after a successful remote command.

feedback

  • Default live feedback: concise phase updates are printed to stderr so you can see progress before the remote command starts producing output.
  • Verbose mode: pass --verbose to print the underlying command steps in addition to the standard phase updates.
  • Remote command output: stdout and stderr from the remote command still stream normally; progress text stays on stderr.

troubleshooting

  • Locking: If a previous run crashed, you might need to manually remove the .lock file in the remote workspace directory.
  • Excludes: Check your [project].exclude list if files aren't appearing on the remote.
  • SSH/Rsync: Ensure you have SSH keys set up for passwordless login to the remote host. If you use staged external paths or --stage, make sure rsync exists on both the local and remote machines.

workflow

Example workflows:

# Run a normal command with argv-safe path handling
relayctl -- make test

# Run a shell pipeline remotely
relayctl --shell -- 'make build && ./bin/app < input.txt | tee output.txt'

# Stage an external local file for one remote run
relayctl --shell --stage /tmp/data.csv -- 'python3 scripts/process.py "$RELAY_STAGE_DIR/1/data.csv"'

# Interactive session
relayctl ssh

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

relayctl-0.1.0.tar.gz (90.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

relayctl-0.1.0-py3-none-any.whl (52.6 kB view details)

Uploaded Python 3

File details

Details for the file relayctl-0.1.0.tar.gz.

File metadata

  • Download URL: relayctl-0.1.0.tar.gz
  • Upload date:
  • Size: 90.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for relayctl-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f03caa02b66826028f502754503e61e731d0c5e4f399de206cce5091648de99b
MD5 6012826d883902aa4d84a48b7a062e83
BLAKE2b-256 a291b1c413aa8fe3d47722c624f307f335a794f296c8202a0c444ed322a478b3

See more details on using hashes here.

Provenance

The following attestation bundles were made for relayctl-0.1.0.tar.gz:

Publisher: release.yml on SpyicyDev/relayctl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file relayctl-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: relayctl-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 52.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for relayctl-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb9574629b1a6cf1b455d18d0c6642305e1b06131afada1227952ca3e55c47fd
MD5 1a5c9ac493e2542b7447565c882bde4c
BLAKE2b-256 e692fcb8c73d12164755a24b7ff5fe8eaa64501704254ae5ba1352cfe1c8d2c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for relayctl-0.1.0-py3-none-any.whl:

Publisher: release.yml on SpyicyDev/relayctl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page