rrun-cli 0.1.0__tar.gz → 0.1.2__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.
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: rrun-cli
3
+ Version: 0.1.2
4
+ Summary: Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner.
5
+ Author: waqiju
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/waqiju/rrun
8
+ Project-URL: Repository, https://github.com/waqiju/rrun
9
+ Project-URL: Issues, https://github.com/waqiju/rrun/issues
10
+ Project-URL: Changelog, https://github.com/waqiju/rrun/blob/main/CHANGELOG.md
11
+ Project-URL: Documentation, https://github.com/waqiju/rrun/blob/main/docs/remote-exec-conventions.md
12
+ Keywords: ssh,remote-exec,powershell,windows,ops,automation
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # rrun
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/rrun-cli)](https://pypi.org/project/rrun-cli/)
29
+ [![Python](https://img.shields.io/pypi/pyversions/rrun-cli)](https://pypi.org/project/rrun-cli/)
30
+ [![License](https://img.shields.io/pypi/l/rrun-cli)](https://github.com/waqiju/rrun/blob/main/LICENSE)
31
+ [![publish](https://github.com/waqiju/rrun/actions/workflows/publish.yml/badge.svg)](https://github.com/waqiju/rrun/actions/workflows/publish.yml)
32
+
33
+ **[中文文档](README.zh-CN.md)**
34
+
35
+ Run local scripts on remote machines over SSH — **via stdin pipes, never via command-line arguments** — so quoting, escaping, and CJK/UTF-8 encoding survive the `bash → ssh → cmd/powershell` journey intact.
36
+
37
+ - python / powershell / bash, inferred from the file extension or the remote OS
38
+ - Remote stdout/stderr stream back verbatim; the **exit code passes through**, so pipes and CI just work
39
+ - ssh ControlMaster multiplexing: ~0.5s on first contact, ~0.02s afterwards
40
+ - One-command provisioning of a unified **remote Python 3.12 venv** (`rrun setup`) — the remote never touches the internet
41
+ - Zero third-party Python dependencies
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pipx install rrun-cli # provides the `rrun` command (plus a `remote-machine` alias)
47
+ # or: pip install rrun-cli
48
+ ```
49
+
50
+ > The PyPI distribution is named `rrun-cli` (`rrun` sits on PyPI's prohibited-name list as it
51
+ > is confusable with `run`); the installed command is plain `rrun`.
52
+
53
+ **Control machine:** Linux or macOS (Windows works via WSL), Python ≥ 3.10, plus `ssh` and `sshpass`:
54
+
55
+ ```bash
56
+ sudo apt install sshpass # Debian/Ubuntu
57
+ brew install hudochenkov/sshpass/sshpass # macOS (sshpass is not in homebrew-core)
58
+ ```
59
+
60
+ **Remote machines:** an OpenSSH server with password auth enabled. Windows remotes execute via PowerShell, Mac/Linux remotes via bash.
61
+
62
+ ## Quickstart
63
+
64
+ Create `~/.rrun/machines.json` (full format in [Configuration](#configuration-machinesjson)):
65
+
66
+ ```json
67
+ {
68
+ "machines": [
69
+ { "name": "my-win-box", "ip": "192.168.1.10", "os": "Windows", "user": "admin", "password": "secret" }
70
+ ]
71
+ }
72
+ ```
73
+
74
+ Then:
75
+
76
+ ```bash
77
+ rrun machines # verify the inventory is picked up (redacted)
78
+ printf 'Write-Output "hello 中文"\n' > demo.ps1
79
+ rrun exec my-win-box demo.ps1 # powershell, inferred from .ps1
80
+ ```
81
+
82
+ That's the whole loop: write a script locally → it runs remotely → you get its output and exit code.
83
+
84
+ ## Why
85
+
86
+ Running commands on remote Windows machines from a POSIX shell is a minefield: sshd lands you in `cmd` with a GBK codepage, quotes and `$` get eaten by one of the three shells along the way, and any non-ASCII argument gets mojibake'd. rrun's rules:
87
+
88
+ - Script content (UTF-8, CJK welcome) always travels through **stdin** — never the command line.
89
+ - PowerShell payloads are base64-wrapped into a **single-line pure-ASCII wrapper** (`powershell -Command -` executes stdin line-by-line; multi-line input fails silently), then decoded and invoked as a ScriptBlock remotely.
90
+ - Command-line arguments are restricted to ASCII and passed through safely (`sys.argv` / `$@` / `$args`). Put anything fancier in the script itself or in a JSON file.
91
+
92
+ The full set of hard-won conventions and internals: [docs/remote-exec-conventions.md](docs/remote-exec-conventions.md) ([中文](docs/remote-exec-conventions.zh-CN.md)).
93
+
94
+ ## Subcommands
95
+
96
+ | Command | Purpose |
97
+ |---|---|
98
+ | `rrun exec <host> <script\|-c ...>` | Execute a local script / inline content remotely |
99
+ | `rrun machines [--json]` | List merged machines (redacted, with source) |
100
+ | `rrun config` | Diagnose the machines.json source chain |
101
+ | `rrun setup <host\|--all> [--force]` | Provision the unified remote Python 3.12 venv (idempotent) |
102
+ | `rrun pip <host> -- list` | Run pip inside the remote unified venv |
103
+ | `rrun close [<host>\|--all]` | Close ssh ControlMaster multiplexed connections |
104
+
105
+ Useful `exec` flags: `--lang bash|powershell|python`, `--workdir`, `--env K=V`, `--timeout`, `--python <path>` (skip detection), `--no-mux`, `-q`.
106
+
107
+ ### Exit codes
108
+
109
+ | Code | Meaning |
110
+ |---|---|
111
+ | `0`–`254` | The remote script's own exit code, passed through unchanged |
112
+ | `255` | ssh transport failure (unreachable / auth failure / connection dropped) |
113
+ | `124` | local `--timeout` expired; the local ssh client was killed |
114
+
115
+ ## Configuration: machines.json
116
+
117
+ Credentials live in local `machines.json` files — see [machines.template.json](machines.template.json):
118
+
119
+ ```json
120
+ {
121
+ "defaults": { "windows": { "os": "Windows" } },
122
+ "machines": [
123
+ { "name": "my-win-box", "ip": "192.168.1.10", "os": "Windows",
124
+ "user": "admin", "password": "secret" }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ Sources are merged by machine name, highest priority first (all optional, failures skipped silently):
130
+
131
+ 1. `$RRUN_CONFIG` (os.pathsep-separated, multiple files allowed)
132
+ 2. `$REMOTE_MACHINE_CONFIG` (legacy name, still honored)
133
+ 3. `./machines.json` (current working directory)
134
+ 4. `~/.rrun/machines.json`
135
+ 5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink)
136
+ 6. Legacy: `~/.remote-machine/machines.json` (POSIX) / `C:\tools\remote-machine\machines.json` (Windows)
137
+
138
+ Per-file `defaults` sections (keyed by `os`, lowercased) are applied before merging. Inspect the chain with `rrun config`; list machines (redacted) with `rrun machines`.
139
+
140
+ ## Remote Python: unified 3.12 environment
141
+
142
+ For `python` scripts, rrun requires a **3.12.x** interpreter on the remote, probed in order:
143
+
144
+ 1. Unified venv: `C:\tools\remote-machine\venv\Scripts\python.exe` / `~/.remote-machine/venv/bin/python`
145
+ 2. Standalone base: `...\python312\python.exe` / `~/.remote-machine/python312/bin/python3`
146
+ 3. Existing installs: `C:\Python\Python312\python.exe` / `python3.12` on PATH
147
+
148
+ If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks Python 3.12, a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
149
+
150
+ ## Security
151
+
152
+ - `machines.json` stores **plaintext passwords**. Keep it local, `chmod 600`, never commit it.
153
+ - rrun authenticates with passwords via `sshpass`; **key-based authentication is not supported yet** (on the roadmap).
154
+ - The audit log never records passwords, and `--env` values are logged as keys only.
155
+
156
+ ## Auditing & state
157
+
158
+ - Every execution appends a JSON line to `~/.rrun/log/remote-exec.jsonl` (host, lang, script sha1, exit code, duration).
159
+ - Inline `-c` payloads are archived under `~/.rrun/drops/` for replay.
160
+ - The state root defaults to `~/.rrun/` and is overridable via `$RRUN_HOME`.
161
+
162
+ ## Upgrading / uninstalling
163
+
164
+ ```bash
165
+ pipx upgrade rrun-cli
166
+ pipx uninstall rrun-cli
167
+ ```
168
+
169
+ ## Contributing
170
+
171
+ Issues and PRs are welcome. Development setup: clone → `python3.12 -m venv .venv && .venv/bin/pip install -e .` → hack → smoke-test with `rrun machines`. Releases are cut by pushing a `vX.Y.Z` tag; CI builds and publishes to PyPI via trusted publishing. See [CHANGELOG.md](CHANGELOG.md).
172
+
173
+ ## License
174
+
175
+ [MIT](LICENSE)
@@ -0,0 +1,150 @@
1
+ # rrun
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/rrun-cli)](https://pypi.org/project/rrun-cli/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/rrun-cli)](https://pypi.org/project/rrun-cli/)
5
+ [![License](https://img.shields.io/pypi/l/rrun-cli)](https://github.com/waqiju/rrun/blob/main/LICENSE)
6
+ [![publish](https://github.com/waqiju/rrun/actions/workflows/publish.yml/badge.svg)](https://github.com/waqiju/rrun/actions/workflows/publish.yml)
7
+
8
+ **[中文文档](README.zh-CN.md)**
9
+
10
+ Run local scripts on remote machines over SSH — **via stdin pipes, never via command-line arguments** — so quoting, escaping, and CJK/UTF-8 encoding survive the `bash → ssh → cmd/powershell` journey intact.
11
+
12
+ - python / powershell / bash, inferred from the file extension or the remote OS
13
+ - Remote stdout/stderr stream back verbatim; the **exit code passes through**, so pipes and CI just work
14
+ - ssh ControlMaster multiplexing: ~0.5s on first contact, ~0.02s afterwards
15
+ - One-command provisioning of a unified **remote Python 3.12 venv** (`rrun setup`) — the remote never touches the internet
16
+ - Zero third-party Python dependencies
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pipx install rrun-cli # provides the `rrun` command (plus a `remote-machine` alias)
22
+ # or: pip install rrun-cli
23
+ ```
24
+
25
+ > The PyPI distribution is named `rrun-cli` (`rrun` sits on PyPI's prohibited-name list as it
26
+ > is confusable with `run`); the installed command is plain `rrun`.
27
+
28
+ **Control machine:** Linux or macOS (Windows works via WSL), Python ≥ 3.10, plus `ssh` and `sshpass`:
29
+
30
+ ```bash
31
+ sudo apt install sshpass # Debian/Ubuntu
32
+ brew install hudochenkov/sshpass/sshpass # macOS (sshpass is not in homebrew-core)
33
+ ```
34
+
35
+ **Remote machines:** an OpenSSH server with password auth enabled. Windows remotes execute via PowerShell, Mac/Linux remotes via bash.
36
+
37
+ ## Quickstart
38
+
39
+ Create `~/.rrun/machines.json` (full format in [Configuration](#configuration-machinesjson)):
40
+
41
+ ```json
42
+ {
43
+ "machines": [
44
+ { "name": "my-win-box", "ip": "192.168.1.10", "os": "Windows", "user": "admin", "password": "secret" }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ Then:
50
+
51
+ ```bash
52
+ rrun machines # verify the inventory is picked up (redacted)
53
+ printf 'Write-Output "hello 中文"\n' > demo.ps1
54
+ rrun exec my-win-box demo.ps1 # powershell, inferred from .ps1
55
+ ```
56
+
57
+ That's the whole loop: write a script locally → it runs remotely → you get its output and exit code.
58
+
59
+ ## Why
60
+
61
+ Running commands on remote Windows machines from a POSIX shell is a minefield: sshd lands you in `cmd` with a GBK codepage, quotes and `$` get eaten by one of the three shells along the way, and any non-ASCII argument gets mojibake'd. rrun's rules:
62
+
63
+ - Script content (UTF-8, CJK welcome) always travels through **stdin** — never the command line.
64
+ - PowerShell payloads are base64-wrapped into a **single-line pure-ASCII wrapper** (`powershell -Command -` executes stdin line-by-line; multi-line input fails silently), then decoded and invoked as a ScriptBlock remotely.
65
+ - Command-line arguments are restricted to ASCII and passed through safely (`sys.argv` / `$@` / `$args`). Put anything fancier in the script itself or in a JSON file.
66
+
67
+ The full set of hard-won conventions and internals: [docs/remote-exec-conventions.md](docs/remote-exec-conventions.md) ([中文](docs/remote-exec-conventions.zh-CN.md)).
68
+
69
+ ## Subcommands
70
+
71
+ | Command | Purpose |
72
+ |---|---|
73
+ | `rrun exec <host> <script\|-c ...>` | Execute a local script / inline content remotely |
74
+ | `rrun machines [--json]` | List merged machines (redacted, with source) |
75
+ | `rrun config` | Diagnose the machines.json source chain |
76
+ | `rrun setup <host\|--all> [--force]` | Provision the unified remote Python 3.12 venv (idempotent) |
77
+ | `rrun pip <host> -- list` | Run pip inside the remote unified venv |
78
+ | `rrun close [<host>\|--all]` | Close ssh ControlMaster multiplexed connections |
79
+
80
+ Useful `exec` flags: `--lang bash|powershell|python`, `--workdir`, `--env K=V`, `--timeout`, `--python <path>` (skip detection), `--no-mux`, `-q`.
81
+
82
+ ### Exit codes
83
+
84
+ | Code | Meaning |
85
+ |---|---|
86
+ | `0`–`254` | The remote script's own exit code, passed through unchanged |
87
+ | `255` | ssh transport failure (unreachable / auth failure / connection dropped) |
88
+ | `124` | local `--timeout` expired; the local ssh client was killed |
89
+
90
+ ## Configuration: machines.json
91
+
92
+ Credentials live in local `machines.json` files — see [machines.template.json](machines.template.json):
93
+
94
+ ```json
95
+ {
96
+ "defaults": { "windows": { "os": "Windows" } },
97
+ "machines": [
98
+ { "name": "my-win-box", "ip": "192.168.1.10", "os": "Windows",
99
+ "user": "admin", "password": "secret" }
100
+ ]
101
+ }
102
+ ```
103
+
104
+ Sources are merged by machine name, highest priority first (all optional, failures skipped silently):
105
+
106
+ 1. `$RRUN_CONFIG` (os.pathsep-separated, multiple files allowed)
107
+ 2. `$REMOTE_MACHINE_CONFIG` (legacy name, still honored)
108
+ 3. `./machines.json` (current working directory)
109
+ 4. `~/.rrun/machines.json`
110
+ 5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink)
111
+ 6. Legacy: `~/.remote-machine/machines.json` (POSIX) / `C:\tools\remote-machine\machines.json` (Windows)
112
+
113
+ Per-file `defaults` sections (keyed by `os`, lowercased) are applied before merging. Inspect the chain with `rrun config`; list machines (redacted) with `rrun machines`.
114
+
115
+ ## Remote Python: unified 3.12 environment
116
+
117
+ For `python` scripts, rrun requires a **3.12.x** interpreter on the remote, probed in order:
118
+
119
+ 1. Unified venv: `C:\tools\remote-machine\venv\Scripts\python.exe` / `~/.remote-machine/venv/bin/python`
120
+ 2. Standalone base: `...\python312\python.exe` / `~/.remote-machine/python312/bin/python3`
121
+ 3. Existing installs: `C:\Python\Python312\python.exe` / `python3.12` on PATH
122
+
123
+ If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks Python 3.12, a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
124
+
125
+ ## Security
126
+
127
+ - `machines.json` stores **plaintext passwords**. Keep it local, `chmod 600`, never commit it.
128
+ - rrun authenticates with passwords via `sshpass`; **key-based authentication is not supported yet** (on the roadmap).
129
+ - The audit log never records passwords, and `--env` values are logged as keys only.
130
+
131
+ ## Auditing & state
132
+
133
+ - Every execution appends a JSON line to `~/.rrun/log/remote-exec.jsonl` (host, lang, script sha1, exit code, duration).
134
+ - Inline `-c` payloads are archived under `~/.rrun/drops/` for replay.
135
+ - The state root defaults to `~/.rrun/` and is overridable via `$RRUN_HOME`.
136
+
137
+ ## Upgrading / uninstalling
138
+
139
+ ```bash
140
+ pipx upgrade rrun-cli
141
+ pipx uninstall rrun-cli
142
+ ```
143
+
144
+ ## Contributing
145
+
146
+ Issues and PRs are welcome. Development setup: clone → `python3.12 -m venv .venv && .venv/bin/pip install -e .` → hack → smoke-test with `rrun machines`. Releases are cut by pushing a `vX.Y.Z` tag; CI builds and publishes to PyPI via trusted publishing. See [CHANGELOG.md](CHANGELOG.md).
147
+
148
+ ## License
149
+
150
+ [MIT](LICENSE)
@@ -1,13 +1,13 @@
1
1
  [build-system]
2
- requires = ["setuptools>=68"]
2
+ requires = ["setuptools>=77"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rrun-cli"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner."
9
9
  readme = "README.md"
10
- license = { text = "MIT" }
10
+ license = "MIT"
11
11
  requires-python = ">=3.10"
12
12
  authors = [{ name = "waqiju" }]
13
13
  keywords = ["ssh", "remote-exec", "powershell", "windows", "ops", "automation"]
@@ -16,7 +16,6 @@ classifiers = [
16
16
  "Environment :: Console",
17
17
  "Intended Audience :: Developers",
18
18
  "Intended Audience :: System Administrators",
19
- "License :: OSI Approved :: MIT License",
20
19
  "Operating System :: POSIX :: Linux",
21
20
  "Operating System :: MacOS",
22
21
  "Programming Language :: Python :: 3",
@@ -29,6 +28,8 @@ dependencies = []
29
28
  Homepage = "https://github.com/waqiju/rrun"
30
29
  Repository = "https://github.com/waqiju/rrun"
31
30
  Issues = "https://github.com/waqiju/rrun/issues"
31
+ Changelog = "https://github.com/waqiju/rrun/blob/main/CHANGELOG.md"
32
+ Documentation = "https://github.com/waqiju/rrun/blob/main/docs/remote-exec-conventions.md"
32
33
 
33
34
  [project.scripts]
34
35
  rrun = "rrun.__main__:main"
@@ -12,7 +12,7 @@ try:
12
12
 
13
13
  __version__ = _pkg_version("rrun-cli")
14
14
  except Exception: # noqa: BLE001 - 未安装(源码直跑)时回退
15
- __version__ = "0.1.0"
15
+ __version__ = "0.1.2"
16
16
 
17
17
  from .executor import (
18
18
  AUDIT_LOG,
@@ -1,31 +1,36 @@
1
1
  # -*- coding: utf-8 -*-
2
- """rrun — 远程机器执行器(Run Remote)。
2
+ """rrun CLI — run local scripts on remote machines over SSH stdin pipes.
3
3
 
4
- 核心约定:脚本内容(可含中文,UTF-8)经 **stdin 管道**送远端解释器执行,
5
- 全程不走命令行参数,规避 bash->ssh->cmd 多层转码/转义问题。
4
+ Core contract: script content (UTF-8, CJK welcome) is piped to the remote
5
+ interpreter via stdin, never via command-line arguments — sidestepping the
6
+ bash -> ssh -> cmd quoting/encoding gauntlet.
6
7
 
7
- 用法:
8
+ Usage:
8
9
  rrun exec <host> <script.py|.ps1|.sh> [ascii_args...]
9
- rrun exec <host> -c "Write-Output 中文" # lang 按 OS 推断
10
+ rrun exec <host> -c "Write-Output hello" # lang inferred from remote OS
10
11
  rrun exec pc_build temp/x.py --timeout 60
11
12
  rrun exec mac_mini --lang python temp/x.py
12
- rrun setup <host|--all> [--force] # 初始化统一 python 环境(3.12 venv + 阿里源)
13
- rrun pip <host> -- list # 在统一 venv 中执行 pip
14
- rrun machines # 列出可用机器(脱敏,含来源)
15
- rrun config # 查看 machines.json 来源链解析
16
- rrun close [<host>|--all] # 关闭 ssh 复用连接
17
-
18
- 约定:
19
- - lang 推断:文件扩展名(.py/.ps1/.sh)优先,否则按机器 OS(Windows=powershell,其他=bash)。
20
- - 命令行参数只允许 ASCII,透传远端(python=sys.argv;bash=$@;powershell=$args);
21
- 中文/特殊字符一律写进脚本内容或 JSON 文件。
22
- - python 基线锁 3.12:探测命中统一 venv(优先)/standalone 基座/存量 3.12,
23
- 并校验版本号;全灭则报错提示先跑 setup(exec 热路径不做隐式安装)。
24
- - 退出码:远端脚本退出码原样透传;255=ssh 传输层错误;124=本地超时。
25
- - 内联 -c 内容自动落盘 ~/.rrun/drops/ 留档,可复跑(RRUN_HOME 可改根目录)。
26
- - 审计:每次执行追加 ~/.rrun/log/remote-exec.jsonl。
27
-
28
- 退出码即远端退出码,可直接管道使用:远端 stdout→本机 stdout,stderr→stderr。
13
+ rrun setup <host|--all> [--force] # provision the unified python env (3.12 venv)
14
+ rrun pip <host> -- list # run pip in the unified venv
15
+ rrun machines # list machines (redacted, with source)
16
+ rrun config # diagnose the machines.json source chain
17
+ rrun close [<host>|--all] # close ssh multiplexed connections
18
+
19
+ Conventions:
20
+ - Language inference: file extension (.py/.ps1/.sh) first, then remote OS
21
+ (Windows=powershell, others=bash).
22
+ - Command-line args are ASCII-only, passed through remotely
23
+ (python=sys.argv; bash=$@; powershell=$args); put CJK/special characters
24
+ in the script content or a JSON file instead.
25
+ - Remote python is pinned to 3.12: probes the unified venv (preferred),
26
+ the standalone base, then existing installs; all missing -> run `rrun setup`
27
+ first (exec never installs implicitly on the hot path).
28
+ - Exit codes: the remote exit code passes through unchanged;
29
+ 255=ssh transport error; 124=local timeout.
30
+ - Inline -c content is archived under ~/.rrun/drops/ for replay.
31
+ - Audit: every execution appends to ~/.rrun/log/remote-exec.jsonl.
32
+
33
+ Remote stdout -> local stdout, stderr -> stderr; the exit code is the remote one.
29
34
  """
30
35
 
31
36
  import argparse
@@ -45,7 +50,7 @@ def _parse_env(pairs) -> dict:
45
50
  env = {}
46
51
  for p in pairs or []:
47
52
  if "=" not in p:
48
- raise SystemExit(f"[remote-exec] --env 格式应为 KEY=VAL: {p!r}")
53
+ raise SystemExit(f"[remote-exec] --env expects KEY=VAL, got: {p!r}")
49
54
  k, v = p.split("=", 1)
50
55
  env[k] = v
51
56
  return env
@@ -70,9 +75,9 @@ def cmd_exec(ns) -> int:
70
75
  file = ns.script or ""
71
76
  lang = ns.lang or ""
72
77
  if content and file:
73
- raise SystemExit("[remote-exec] 脚本文件与 -c/--content 只能二选一")
78
+ raise SystemExit("[remote-exec] give either a script file or -c/--content, not both")
74
79
  if not content and not file:
75
- raise SystemExit("[remote-exec] 缺少脚本:给文件路径或 -c/--content")
80
+ raise SystemExit("[remote-exec] nothing to run: pass a script file or -c/--content")
76
81
 
77
82
  # content 模式先确定 lang 再落盘(扩展名需要 lang)
78
83
  script_for_log = file
@@ -81,7 +86,7 @@ def cmd_exec(ns) -> int:
81
86
  lang = machine.default_lang
82
87
  dropped = _drop_inline(machine.name, lang, content)
83
88
  script_for_log = str(dropped)
84
- print(f"[remote-exec] 内联内容已落盘: {dropped}", file=sys.stderr)
89
+ print(f"[remote-exec] inline content archived to {dropped}", file=sys.stderr)
85
90
 
86
91
  args = ns.args
87
92
 
@@ -106,11 +111,11 @@ def cmd_exec(ns) -> int:
106
111
  sys.stderr.buffer.flush()
107
112
 
108
113
  if result.timed_out:
109
- print(f"[remote-exec] 超时({ns.timeout}s),本地已终止", file=sys.stderr)
114
+ print(f"[remote-exec] local timeout ({ns.timeout}s), ssh client killed", file=sys.stderr)
110
115
  elif result.transport_error:
111
- print(f"[remote-exec] ssh 传输层错误(连接失败/认证失败/掉线),exit=255", file=sys.stderr)
116
+ print(f"[remote-exec] ssh transport error (unreachable / auth failure / dropped), exit=255", file=sys.stderr)
112
117
  if not ns.quiet:
113
- print(f"[remote-exec] exit={result.exit_code} 耗时 {result.duration:.1f}s", file=sys.stderr)
118
+ print(f"[remote-exec] exit={result.exit_code} took {result.duration:.1f}s", file=sys.stderr)
114
119
  return result.exit_code
115
120
 
116
121
 
@@ -138,19 +143,20 @@ def cmd_machines(ns) -> int:
138
143
 
139
144
  def cmd_config(ns) -> int:
140
145
  infos = scan_sources()
141
- print("machines.json 来源链(高 → 低优先级,同名机器高优先级覆盖):")
146
+ print("machines.json source chain (high -> low priority; same-name machines are")
147
+ print("overridden by higher-priority sources):")
142
148
  raw_total = 0
143
149
  for i, info in enumerate(infos, 1):
144
150
  if not info.exists:
145
- state = "- 不存在"
151
+ state = "- missing"
146
152
  elif info.error:
147
- state = f"✗ 读取失败: {info.error}"
153
+ state = f"x read failed: {info.error}"
148
154
  else:
149
- state = f"✓ {info.machine_count} 台"
155
+ state = f"ok, {info.machine_count} machines"
150
156
  raw_total += info.machine_count
151
157
  print(f" [{i}] {info.label:<22} {info.path} {state}")
152
158
  merged = load_machines()
153
- print(f"合计 {len(merged)} 台(各来源原始共 {raw_total} 台,同名覆盖后 {len(merged)} 台)")
159
+ print(f"{len(merged)} machines total ({raw_total} across all sources before same-name overrides)")
154
160
  return 0
155
161
 
156
162
 
@@ -160,10 +166,10 @@ def cmd_setup(ns) -> int:
160
166
  elif ns.host:
161
167
  hosts = [ns.host]
162
168
  else:
163
- raise SystemExit("[setup] 需要指定 host 或 --all")
169
+ raise SystemExit("[setup] specify a host or --all")
164
170
  results = []
165
171
  if len(hosts) == 1:
166
- print(f"[setup] {hosts[0]} 初始化中...", file=sys.stderr)
172
+ print(f"[setup] provisioning {hosts[0]} ...", file=sys.stderr)
167
173
  results.append(setup_machine(hosts[0], force=ns.force))
168
174
  else:
169
175
  from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -173,17 +179,17 @@ def cmd_setup(ns) -> int:
173
179
  r = f.result()
174
180
  results.append(r)
175
181
  print(f"[setup] {r.host}: {'ok' if r.ok else 'FAIL'} ({r.duration:.0f}s)", file=sys.stderr)
176
- print(f"\n{'host':<24} {'结果':<6} {'python':<10} {'venv':<46} 备注")
182
+ print(f"\n{'host':<24} {'result':<6} {'python':<10} {'venv':<46} note")
177
183
  for r in sorted(results, key=lambda x: x.host):
178
184
  if r.ok:
179
185
  note = []
180
186
  if r.installed_standalone:
181
- note.append("新装standalone")
187
+ note.append("standalone installed")
182
188
  if r.created_venv:
183
- note.append("新建venv")
189
+ note.append("venv created")
184
190
  if not note:
185
- note.append("已存在,仅校验/补装依赖")
186
- print(f"{r.host:<24} {'ok':<6} {r.version:<10} {r.venv_python:<46} {','.join(note)}")
191
+ note.append("already present, verified/topped-up deps")
192
+ print(f"{r.host:<24} {'ok':<6} {r.version:<10} {r.venv_python:<46} {', '.join(note)}")
187
193
  else:
188
194
  print(f"{r.host:<24} {'FAIL':<6} {'':<10} {'':<46} {r.message[:80]}")
189
195
  return 0 if all(r.ok for r in results) else 1
@@ -196,9 +202,9 @@ def cmd_pip(ns) -> int:
196
202
  raise SystemExit(f"[pip] {e}")
197
203
  args = list(ns.pargs or []) + list(getattr(ns, "args", []) or [])
198
204
  if not args:
199
- raise SystemExit("[pip] 缺少 pip 参数,例:rrun pip <host> -- list")
205
+ raise SystemExit("[pip] missing pip args, e.g.: rrun pip <host> -- list")
200
206
  try:
201
- _check_ascii(args, "pip 参数")
207
+ _check_ascii(args, "pip args")
202
208
  py = venv_python_or_die(ns.host)
203
209
  except (ValueError, RuntimeError) as e:
204
210
  raise SystemExit(f"[pip] {e}")
@@ -213,7 +219,7 @@ def cmd_pip(ns) -> int:
213
219
  sys.stdout.buffer.flush()
214
220
  sys.stderr.buffer.write(err)
215
221
  sys.stderr.buffer.flush()
216
- print(f"[pip] {machine.name} exit={rc} 耗时 {time.time() - t0:.1f}s", file=sys.stderr)
222
+ print(f"[pip] {machine.name} exit={rc} took {time.time() - t0:.1f}s", file=sys.stderr)
217
223
  return rc
218
224
 
219
225
 
@@ -223,7 +229,7 @@ def cmd_close(ns) -> int:
223
229
  elif ns.host:
224
230
  hosts = [ns.host]
225
231
  else:
226
- raise SystemExit("[remote-exec] close 需要指定 host 或 --all")
232
+ raise SystemExit("[remote-exec] close needs a host or --all")
227
233
  rc = 0
228
234
  for h in hosts:
229
235
  try:
@@ -241,47 +247,47 @@ def main() -> None:
241
247
  description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
242
248
  sub = ap.add_subparsers(dest="subcmd", required=True)
243
249
 
244
- ep = sub.add_parser("exec", help="在远端机器执行本地脚本/内联内容")
245
- ep.add_argument("host", help="机器 name/ip/hostname(见 machines 子命令)")
246
- ep.add_argument("script", nargs="?", help="本地脚本路径(.py/.ps1/.sh,UTF-8)")
250
+ ep = sub.add_parser("exec", help="execute a local script / inline content on a remote machine")
251
+ ep.add_argument("host", help="machine name/ip/hostname (see the machines subcommand)")
252
+ ep.add_argument("script", nargs="?", help="local script path (.py/.ps1/.sh, UTF-8)")
247
253
  ep.add_argument("--lang", choices=["bash", "powershell", "python"],
248
- help="远端解释语言(缺省:按扩展名,否则按机器 OS)")
249
- ep.add_argument("-c", "--content", help="内联脚本内容(自动落盘 ~/.rrun/drops/ 留档)")
254
+ help="remote interpreter language (default: by extension, else by remote OS)")
255
+ ep.add_argument("-c", "--content", help="inline script content (archived to ~/.rrun/drops/)")
250
256
  ep.add_argument("args", nargs="*",
251
- help="脚本参数(仅 ASCII;建议用 -- 与选项分隔,选项可放任意位置)")
252
- ep.add_argument("--workdir", help="远端工作目录(bash/powershell 支持)")
257
+ help="script arguments (ASCII only; use -- to separate from options)")
258
+ ep.add_argument("--workdir", help="remote working directory (bash/powershell only)")
253
259
  ep.add_argument("--env", action="append", metavar="KEY=VAL",
254
- help="远端环境变量(可多次;仅 ASCII)")
255
- ep.add_argument("--timeout", type=float, help="本地超时秒数(超时 exit=124)")
256
- ep.add_argument("--python", dest="python", help="远端 python 路径(跳过自动探测)")
257
- ep.add_argument("--no-utf8", action="store_true", help="远端 python 不加 -X utf8")
258
- ep.add_argument("--no-mux", action="store_true", help="禁用 ssh ControlMaster 复用")
259
- ep.add_argument("-q", "--quiet", action="store_true", help="不打印 [remote-exec] 信息行")
260
+ help="remote environment variable (repeatable; ASCII only)")
261
+ ep.add_argument("--timeout", type=float, help="local timeout in seconds (exit=124 on expiry)")
262
+ ep.add_argument("--python", dest="python", help="remote python path (skips auto-detection)")
263
+ ep.add_argument("--no-utf8", action="store_true", help="do not pass -X utf8 to remote python")
264
+ ep.add_argument("--no-mux", action="store_true", help="disable ssh ControlMaster multiplexing")
265
+ ep.add_argument("-q", "--quiet", action="store_true", help="suppress [remote-exec] info lines")
260
266
  ep.set_defaults(func=cmd_exec)
261
267
 
262
- mp = sub.add_parser("machines", help="列出全部来源合并后的机器(脱敏,含来源)")
268
+ mp = sub.add_parser("machines", help="list machines merged from all sources (redacted, with source)")
263
269
  mp.add_argument("--json", action="store_true")
264
270
  mp.set_defaults(func=cmd_machines)
265
271
 
266
- cf = sub.add_parser("config", help="查看 machines.json 来源链解析(哪些文件生效、各贡献几台)")
272
+ cf = sub.add_parser("config", help="show the machines.json source chain (which files apply, how many machines each)")
267
273
  cf.set_defaults(func=cmd_config)
268
274
 
269
- sp = sub.add_parser("setup", help="初始化远端统一 python 环境(3.12 venv + 阿里云源)")
270
- sp.add_argument("host", nargs="?", help="机器 name/ip;--all 表示全部")
271
- sp.add_argument("--all", action="store_true", help="对 machines.json 所有机器执行")
272
- sp.add_argument("--force", action="store_true", help="重建 venv(不动已装的 python 本体)")
273
- sp.add_argument("--jobs", type=int, default=6, help="--all 时的并发数(默认 6)")
275
+ sp = sub.add_parser("setup", help="provision the unified remote python environment (3.12 venv)")
276
+ sp.add_argument("host", nargs="?", help="machine name/ip; use --all for every machine")
277
+ sp.add_argument("--all", action="store_true", help="run against all machines in machines.json")
278
+ sp.add_argument("--force", action="store_true", help="recreate the venv (keeps the python base)")
279
+ sp.add_argument("--jobs", type=int, default=6, help="concurrency for --all (default 6)")
274
280
  sp.set_defaults(func=cmd_setup)
275
281
 
276
- pp = sub.add_parser("pip", help="在远端统一 venv 中执行 pip(ad-hoc 装包)")
277
- pp.add_argument("host", help="机器 name/ip")
278
- pp.add_argument("pargs", nargs="*", help="pip 参数;含 - 开头选项时放 -- 之后")
279
- pp.add_argument("--timeout", type=float, default=300.0, help="本地超时秒数(默认 300)")
282
+ pp = sub.add_parser("pip", help="run pip inside the remote unified venv (ad-hoc installs)")
283
+ pp.add_argument("host", help="machine name/ip")
284
+ pp.add_argument("pargs", nargs="*", help="pip arguments; put options starting with - after --")
285
+ pp.add_argument("--timeout", type=float, default=300.0, help="local timeout in seconds (default 300)")
280
286
  pp.set_defaults(func=cmd_pip)
281
287
 
282
- cp = sub.add_parser("close", help="关闭 ssh ControlMaster 复用连接")
283
- cp.add_argument("host", nargs="?", help="机器 name/ip;省略时需 --all")
284
- cp.add_argument("--all", action="store_true", help="关闭所有机器的复用连接")
288
+ cp = sub.add_parser("close", help="close ssh ControlMaster multiplexed connections")
289
+ cp.add_argument("host", nargs="?", help="machine name/ip; omit with --all")
290
+ cp.add_argument("--all", action="store_true", help="close connections for all machines")
285
291
  cp.set_defaults(func=cmd_close)
286
292
 
287
293
  # argparse 对 -- 的处理与子解析器/位置参数组合有 quirk,手动切分更可靠: