Metadata-Version: 2.4
Name: relayctl
Version: 0.1.0
Summary: Relay: local-first remote execution over SSH with managed workspace sync
Requires-Python: >=3.11
Requires-Dist: rich>=13.9.0
Description-Content-Type: text/markdown

# 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](#prerequisites) section for details.

### 2. Install Relay

```bash
# Using uv (recommended)
uv tool install relayctl

# Using pipx
pipx install relayctl
```

### 3. Verify installation

Run the doctor command to check your environment:

```bash
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](#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`.

```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:

```bash
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.

```bash
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.

```bash
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.

```bash
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.

```bash
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:**
```bash
# 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
```
