relayctl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. relayctl-0.1.0/.github/workflows/ci.yml +94 -0
  2. relayctl-0.1.0/.github/workflows/release.yml +145 -0
  3. relayctl-0.1.0/.gitignore +11 -0
  4. relayctl-0.1.0/.relay.example.toml +15 -0
  5. relayctl-0.1.0/PKG-INFO +258 -0
  6. relayctl-0.1.0/README.md +250 -0
  7. relayctl-0.1.0/justfile +16 -0
  8. relayctl-0.1.0/pyproject.toml +29 -0
  9. relayctl-0.1.0/pyrightconfig.json +4 -0
  10. relayctl-0.1.0/scripts/smoke_fake.py +468 -0
  11. relayctl-0.1.0/src/relayctl/__init__.py +3 -0
  12. relayctl-0.1.0/src/relayctl/_remote_runner.py +254 -0
  13. relayctl-0.1.0/src/relayctl/argv_paths.py +300 -0
  14. relayctl-0.1.0/src/relayctl/cli.py +1212 -0
  15. relayctl-0.1.0/src/relayctl/config.py +689 -0
  16. relayctl-0.1.0/src/relayctl/conflict_policy.py +121 -0
  17. relayctl-0.1.0/src/relayctl/feedback.py +75 -0
  18. relayctl-0.1.0/src/relayctl/identity.py +43 -0
  19. relayctl-0.1.0/src/relayctl/locking.py +111 -0
  20. relayctl-0.1.0/src/relayctl/mutagen.py +425 -0
  21. relayctl-0.1.0/src/relayctl/pullback_plan.py +148 -0
  22. relayctl-0.1.0/src/relayctl/remote_execution.py +1337 -0
  23. relayctl-0.1.0/src/relayctl/remote_runner_manifest.py +207 -0
  24. relayctl-0.1.0/src/relayctl/root.py +73 -0
  25. relayctl-0.1.0/src/relayctl/setup_checks.py +422 -0
  26. relayctl-0.1.0/src/relayctl/ssh.py +172 -0
  27. relayctl-0.1.0/src/relayctl/startup_cache.py +289 -0
  28. relayctl-0.1.0/src/relayctl/transport.py +184 -0
  29. relayctl-0.1.0/src/relayctl/workspace.py +127 -0
  30. relayctl-0.1.0/tests/conftest.py +23 -0
  31. relayctl-0.1.0/tests/integration/test_fake_transport.py +1594 -0
  32. relayctl-0.1.0/tests/integration/test_init_cli.py +105 -0
  33. relayctl-0.1.0/tests/test_cli_help.py +309 -0
  34. relayctl-0.1.0/tests/test_config.py +429 -0
  35. relayctl-0.1.0/tests/test_config_errors.py +280 -0
  36. relayctl-0.1.0/tests/test_conflict_policy.py +87 -0
  37. relayctl-0.1.0/tests/test_docs_contract.py +90 -0
  38. relayctl-0.1.0/tests/test_exit_codes.py +103 -0
  39. relayctl-0.1.0/tests/test_init.py +569 -0
  40. relayctl-0.1.0/tests/test_locking.py +113 -0
  41. relayctl-0.1.0/tests/test_path_classification.py +186 -0
  42. relayctl-0.1.0/tests/test_path_rewrite.py +327 -0
  43. relayctl-0.1.0/tests/test_pullback_plan.py +103 -0
  44. relayctl-0.1.0/tests/test_relay_surface_audit.py +98 -0
  45. relayctl-0.1.0/tests/test_remote_execution.py +1986 -0
  46. relayctl-0.1.0/tests/test_remote_runner.py +81 -0
  47. relayctl-0.1.0/tests/test_remote_runner_manifest.py +188 -0
  48. relayctl-0.1.0/tests/test_root_detection.py +84 -0
  49. relayctl-0.1.0/tests/test_setup_checks.py +473 -0
  50. relayctl-0.1.0/tests/test_shell_mode.py +344 -0
  51. relayctl-0.1.0/tests/test_smoke_fake.py +339 -0
  52. relayctl-0.1.0/tests/test_ssh.py +230 -0
  53. relayctl-0.1.0/tests/test_symlink_policy.py +197 -0
  54. relayctl-0.1.0/tests/test_transport.py +70 -0
  55. relayctl-0.1.0/tests/test_workspace_identity.py +82 -0
  56. relayctl-0.1.0/uv.lock +117 -0
@@ -0,0 +1,94 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ name: Test
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Check out repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+
26
+ - name: Set up uv
27
+ uses: astral-sh/setup-uv@v5
28
+
29
+ - name: Sync development environment
30
+ run: uv sync --group dev
31
+
32
+ - name: Run pytest
33
+ run: uv run pytest -q
34
+
35
+ build:
36
+ name: Build distributions
37
+ runs-on: ubuntu-latest
38
+
39
+ steps:
40
+ - name: Check out repository
41
+ uses: actions/checkout@v4
42
+
43
+ - name: Set up Python
44
+ uses: actions/setup-python@v5
45
+ with:
46
+ python-version: "3.12"
47
+
48
+ - name: Set up uv
49
+ uses: astral-sh/setup-uv@v5
50
+
51
+ - name: Build wheel and sdist
52
+ run: uv build
53
+
54
+ - name: Upload distributions
55
+ if: always()
56
+ uses: actions/upload-artifact@v4
57
+ with:
58
+ name: relayctl-distributions
59
+ path: dist/
60
+ if-no-files-found: ignore
61
+
62
+ install-smoke:
63
+ name: Install smoke
64
+ needs:
65
+ - test
66
+ - build
67
+ runs-on: ubuntu-latest
68
+
69
+ steps:
70
+ - name: Set up Python
71
+ uses: actions/setup-python@v5
72
+ with:
73
+ python-version: "3.12"
74
+
75
+ - name: Download distributions
76
+ uses: actions/download-artifact@v4
77
+ with:
78
+ name: relayctl-distributions
79
+ path: dist/
80
+
81
+ - name: Install built wheel in clean virtualenv
82
+ run: python -m venv .venv-smoke
83
+
84
+ - name: Upgrade pip in clean virtualenv
85
+ run: .venv-smoke/bin/python -m pip install --upgrade pip
86
+
87
+ - name: Install built wheel
88
+ run: .venv-smoke/bin/python -m pip install dist/*.whl
89
+
90
+ - name: Verify wheel entrypoint help
91
+ run: .venv-smoke/bin/relayctl --help
92
+
93
+ - name: Verify doctor help from installed CLI
94
+ run: .venv-smoke/bin/relayctl doctor --help
@@ -0,0 +1,145 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - "v*.*.*b*"
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ name: Test
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Check out repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+
26
+ - name: Set up uv
27
+ uses: astral-sh/setup-uv@v5
28
+
29
+ - name: Sync development environment
30
+ run: uv sync --group dev
31
+
32
+ - name: Run pytest
33
+ run: uv run pytest -q
34
+
35
+ build:
36
+ name: Build distributions
37
+ runs-on: ubuntu-latest
38
+
39
+ steps:
40
+ - name: Check out repository
41
+ uses: actions/checkout@v4
42
+
43
+ - name: Set up Python
44
+ uses: actions/setup-python@v5
45
+ with:
46
+ python-version: "3.12"
47
+
48
+ - name: Set up uv
49
+ uses: astral-sh/setup-uv@v5
50
+
51
+ - name: Build wheel and sdist
52
+ run: uv build
53
+
54
+ - name: Upload distributions
55
+ if: always()
56
+ uses: actions/upload-artifact@v4
57
+ with:
58
+ name: relayctl-distributions
59
+ path: dist/
60
+ if-no-files-found: ignore
61
+
62
+ install-smoke:
63
+ name: Install smoke
64
+ needs:
65
+ - test
66
+ - build
67
+ runs-on: ubuntu-latest
68
+
69
+ steps:
70
+ - name: Set up Python
71
+ uses: actions/setup-python@v5
72
+ with:
73
+ python-version: "3.12"
74
+
75
+ - name: Download distributions
76
+ uses: actions/download-artifact@v4
77
+ with:
78
+ name: relayctl-distributions
79
+ path: dist/
80
+
81
+ - name: Install built wheel in clean virtualenv
82
+ run: python -m venv .venv-smoke
83
+
84
+ - name: Upgrade pip in clean virtualenv
85
+ run: .venv-smoke/bin/python -m pip install --upgrade pip
86
+
87
+ - name: Install built wheel
88
+ run: .venv-smoke/bin/python -m pip install dist/*.whl
89
+
90
+ - name: Verify wheel entrypoint help
91
+ run: .venv-smoke/bin/relayctl --help
92
+
93
+ - name: Verify doctor help from installed CLI
94
+ run: .venv-smoke/bin/relayctl doctor --help
95
+
96
+ release-dry-run:
97
+ name: Release dry run
98
+ if: github.event_name == 'workflow_dispatch'
99
+ needs:
100
+ - test
101
+ - build
102
+ - install-smoke
103
+ runs-on: ubuntu-latest
104
+
105
+ steps:
106
+ - name: Set up Python
107
+ uses: actions/setup-python@v5
108
+ with:
109
+ python-version: "3.12"
110
+
111
+ - name: Set up uv
112
+ uses: astral-sh/setup-uv@v5
113
+
114
+ - name: Download distributions
115
+ uses: actions/download-artifact@v4
116
+ with:
117
+ name: relayctl-distributions
118
+ path: dist/
119
+
120
+ - name: Validate release artifacts without publishing
121
+ run: uvx twine check dist/*
122
+
123
+ publish-pypi:
124
+ name: Publish beta to PyPI
125
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && contains(github.ref_name, 'b')
126
+ needs:
127
+ - test
128
+ - build
129
+ - install-smoke
130
+ runs-on: ubuntu-latest
131
+ environment:
132
+ name: pypi
133
+ url: https://pypi.org/p/relayctl
134
+ permissions:
135
+ id-token: write
136
+
137
+ steps:
138
+ - name: Download distributions
139
+ uses: actions/download-artifact@v4
140
+ with:
141
+ name: relayctl-distributions
142
+ path: dist/
143
+
144
+ - name: Publish distributions to PyPI
145
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ /.venv/
2
+ /.desloppify/
3
+ /__pycache__/
4
+ __pycache__/
5
+ *.py[cod]
6
+ /.pytest_cache/
7
+ /dist/
8
+ /build/
9
+ /.seas/
10
+ /.sisyphus/
11
+ /.seas.toml
@@ -0,0 +1,15 @@
1
+ [remote]
2
+ host = "user@example.com"
3
+ workspace_base = "~/.relay/workspaces"
4
+ shell = "/bin/sh"
5
+ profile = "~/.profile"
6
+
7
+ [project]
8
+ root = "."
9
+ exclude = [".git/", ".relay/", ".DS_Store", "__pycache__/", ".pytest_cache/"]
10
+
11
+ [pull]
12
+ enabled = true
13
+ conflict_dir = ".relay/conflicts"
14
+
15
+ # Relay uses .relay/ local runtime state only. Any stale .seas/ directory is ignored.
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: relayctl
3
+ Version: 0.1.0
4
+ Summary: Relay: local-first remote execution over SSH with managed workspace sync
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: rich>=13.9.0
7
+ Description-Content-Type: text/markdown
8
+
9
+ # Relay
10
+
11
+ `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.
12
+
13
+ Use it when you want to keep editing locally but run builds, tests, or tools on a remote Linux machine.
14
+
15
+ ## Installation
16
+
17
+ 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.
18
+
19
+ ### 1. Install external prerequisites
20
+
21
+ Relay does not install these for you. Ensure they are available in your `PATH`:
22
+
23
+ - **Local machine**: `ssh`, `mutagen`, and `rsync`.
24
+ - **Remote host**: `/bin/sh`, `python3`, and `rsync`.
25
+
26
+ See the [Prerequisites](#prerequisites) section for details.
27
+
28
+ ### 2. Install Relay
29
+
30
+ ```bash
31
+ # Using uv (recommended)
32
+ uv tool install relayctl
33
+
34
+ # Using pipx
35
+ pipx install relayctl
36
+ ```
37
+
38
+ ### 3. Verify installation
39
+
40
+ Run the doctor command to check your environment:
41
+
42
+ ```bash
43
+ relayctl doctor
44
+ ```
45
+
46
+ 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.
47
+
48
+ ## Quick Start
49
+
50
+ 1. Ensure [prerequisites](#prerequisites) are met.
51
+ 2. Install `relayctl` via `pipx` or `uv tool`.
52
+ 3. Run `relayctl init` in your project root and answer the setup prompt.
53
+ 4. Run a command: `relayctl -- pwd` (the `--` separator is required).
54
+
55
+ 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.
56
+
57
+ ## Configuration (`.relay.toml`)
58
+
59
+ 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`.
60
+
61
+ ```toml
62
+ [remote]
63
+ host = "user@example.com"
64
+ workspace_base = "~/.relay/workspaces"
65
+ shell = "/bin/sh"
66
+ profile = "~/.profile"
67
+
68
+ [project]
69
+ root = "."
70
+ exclude = [".git/", ".relay/", ".DS_Store", "__pycache__/", ".pytest_cache/"]
71
+
72
+ [pull]
73
+ enabled = true
74
+ conflict_dir = ".relay/conflicts"
75
+ ```
76
+
77
+ `[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.
78
+
79
+ 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.
80
+
81
+ If a stale local `.seas/` runtime directory is still present, `relayctl` warns and ignores it. Relay only creates and uses `.relay/` runtime state.
82
+
83
+ ### Root Detection
84
+ `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.
85
+
86
+ ## Workspace Synchronization
87
+
88
+ `Relay` uses a managed Mutagen workspace for continuous, high-performance file synchronization. This is the only supported synchronization model.
89
+
90
+ ### Key Characteristics
91
+ - **Automatic Session Management**: `relayctl` automatically creates and manages a labeled Mutagen session for your project on the first run.
92
+ - **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.
93
+ - **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.
94
+ - **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`.
95
+ - **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.
96
+
97
+ ### Lifecycle Commands
98
+ Use `relayctl mutagen` to inspect and control the managed session:
99
+ - `relayctl mutagen status`: Show the current session status (absent, healthy, or drifted).
100
+ - `relayctl mutagen flush`: Force a synchronization cycle (useful before running a command if you just saved a file).
101
+ - `relayctl mutagen reset`: Manually recreate the session (requires interactive confirmation).
102
+ - `relayctl mutagen terminate`: Permanently remove the managed session for the current project.
103
+
104
+ ## testing
105
+
106
+ Use the repo's existing commands when validating changes:
107
+
108
+ ```bash
109
+ uv run pytest -q
110
+ just test
111
+ just smoke-fake
112
+ ```
113
+
114
+ `just smoke-remote` is available for a real-host smoke test, but it requires a valid local `.relay.toml` and a reachable SSH target.
115
+
116
+ ## Release automation
117
+
118
+ GitHub Actions now verifies Relay in two stages:
119
+
120
+ - `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.
121
+ - `Release` re-runs the same verification before any release action.
122
+
123
+ 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.
124
+
125
+ 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.
126
+
127
+ ## Usage
128
+
129
+ ### Root command model
130
+ Use bare `relayctl -- ...` for argv-safe execution, and explicit top-level subcommands when you want a different flow:
131
+
132
+ - `relayctl -- CMD [ARG ...]`: primary bare argv mode with automatic path rewriting.
133
+ - `relayctl --shell -- 'SHELL TEXT'`: explicit shell-text mode (shorthand `-s`).
134
+ - `relayctl ssh`: open an interactive shell in the managed workspace.
135
+ - `relayctl init`: write project config.
136
+ - `relayctl mutagen`: inspect and control the managed Mutagen session.
137
+ - `relayctl doctor`: check local prerequisites and stale legacy artifacts, plus remote prerequisites when a concrete host is configured.
138
+
139
+ 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'`.
140
+
141
+ ### `relayctl doctor`
142
+ 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.
143
+
144
+ ```bash
145
+ relayctl doctor
146
+ relayctl doctor --json
147
+ ```
148
+
149
+ 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.
150
+
151
+ ### `relayctl` (Bare Argv Mode)
152
+ Use bare `relayctl -- ...` for standard commands where you want automatic path rewriting (argv).
153
+
154
+ `relayctl` requires an explicit `--` separator before command argv. If omitted, argparse exits with a plain usage error.
155
+
156
+ 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.
157
+
158
+ ```bash
159
+ relayctl -- make test
160
+ relayctl -- python3 -m pytest -q
161
+ relayctl --verbose -- make build
162
+ ```
163
+
164
+ **Deterministic Path Rules:**
165
+
166
+ | Input Pattern | Interpretation | Remote Result |
167
+ | :--- | :--- | :--- |
168
+ | `relative/path` | Resolve against local CWD first | Mirror rewrite if in root; otherwise staged as local external |
169
+ | `../outside/path` | Relative path resolving outside project root | Staged to remote slot and rewritten |
170
+ | `/abs/path/in/root` | Inside project root | Mapped to remote mirror path |
171
+ | `/abs/path/outside` | Outside project root | Staged to remote slot and rewritten |
172
+ | `local:relative/or/absolute` | Force local interpretation | Resolve locally, then apply mirror/staging rules |
173
+ | `remote:relative/or/absolute` | Force literal remote path | Passed through unchanged |
174
+
175
+ - **Symlinks** that escape the project root are rejected for safety.
176
+ - **Attached forms** (e.g., `--flag=/tmp/x`) are not rewritten in this version.
177
+
178
+ > **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`.
179
+
180
+ ### `relayctl --shell` (Shell Mode)
181
+ Use `relayctl --shell` (or `-s`) when you need complex shell features like pipes, redirects, or multiple commands in a single string (shell-mode).
182
+
183
+ `relayctl --shell` requires an explicit `--` separator before shell text. If omitted, argparse exits with a plain usage error.
184
+
185
+ ```bash
186
+ relayctl --shell -- 'g++ -o main main.cpp && ./main < input.txt'
187
+ relayctl --shell --verbose -- 'g++ -o main main.cpp && ./main < input.txt'
188
+ ```
189
+
190
+ **Guardrails and limitations:**
191
+ - **No shell-text parsing**: `relayctl --shell` does not look inside your shell string to rewrite paths (no shell-text parsing).
192
+ - **Explicit staging**: If you need files from outside your project, use the `--stage` flag (explicit --stage).
193
+ - **Environment**: `relayctl --shell` exports `RELAY_STAGE_DIR`, `RELAY_REMOTE_ROOT`, `RELAY_REMOTE_CWD`, and `RELAY_RUN_ID` to the remote environment.
194
+ - **Legacy runtime state**: stale local `.seas/` directories are ignored with a warning and are never migrated into `.relay/`.
195
+ - **limitations**: `relayctl --shell` does not support automatic path rewriting; all paths must be relative to the project root or explicitly staged.
196
+
197
+ ### `relayctl ssh` (Interactive Mode)
198
+ Use `relayctl ssh` to open an interactive shell in your remote workspace.
199
+
200
+ ```bash
201
+ relayctl ssh
202
+ relayctl ssh --workdir remote:/tmp
203
+ ```
204
+
205
+ **Guardrails:**
206
+ - **Interactive-only**: `relayctl ssh` does not support a command tail or shell-text payload.
207
+
208
+ ### Shared Flags
209
+ - `--workdir PATH`: Set the remote working directory. Supports `remote:/abs/path` for literal remote paths. Available for bare `relayctl`, `relayctl --shell`, and `relayctl ssh`.
210
+ - `--env KEY=VAL`: Set a remote environment variable. Can be repeated. Keys starting with `RELAY_` are reserved. Available for bare `relayctl` and `relayctl --shell`.
211
+
212
+ ## Conflict Handling (Staged External Paths)
213
+
214
+ For absolute paths outside your project root (staged external paths), `relayctl` automatically pulls changed files back to your local machine after the command finishes.
215
+
216
+ If a local file was modified while the remote command was running, `relayctl` will not overwrite it. Instead:
217
+ 1. The remote version is saved in `.relay/conflicts/<run-id>/`.
218
+ 2. A conflict summary is printed.
219
+ 3. `relayctl` exits with code `92`.
220
+
221
+ Files within the project root are managed by Mutagen, which handles synchronization and conflict resolution continuously.
222
+
223
+ ## exit code
224
+
225
+ - `0-255`: Remote command exit code is returned unchanged unless a wrapper-reserved condition below applies.
226
+ - `90`: Local setup/config/runtime/report error.
227
+ - `91`: Workspace lock timeout (prevents concurrent mirror corruption).
228
+ - `92`: Pull conflict detected after a successful remote command.
229
+
230
+ ## feedback
231
+
232
+ - **Default live feedback**: concise phase updates are printed to stderr so you can see progress before the remote command starts producing output.
233
+ - **Verbose mode**: pass `--verbose` to print the underlying command steps in addition to the standard phase updates.
234
+ - **Remote command output**: stdout and stderr from the remote command still stream normally; progress text stays on stderr.
235
+
236
+ ## troubleshooting
237
+
238
+ - **Locking**: If a previous run crashed, you might need to manually remove the `.lock` file in the remote workspace directory.
239
+ - **Excludes**: Check your `[project].exclude` list if files aren't appearing on the remote.
240
+ - **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.
241
+
242
+
243
+ ## workflow
244
+
245
+ **Example workflows:**
246
+ ```bash
247
+ # Run a normal command with argv-safe path handling
248
+ relayctl -- make test
249
+
250
+ # Run a shell pipeline remotely
251
+ relayctl --shell -- 'make build && ./bin/app < input.txt | tee output.txt'
252
+
253
+ # Stage an external local file for one remote run
254
+ relayctl --shell --stage /tmp/data.csv -- 'python3 scripts/process.py "$RELAY_STAGE_DIR/1/data.csv"'
255
+
256
+ # Interactive session
257
+ relayctl ssh
258
+ ```