rrun-cli 0.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rrun-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
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
5
  Author: waqiju
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rrun-cli"
7
- version = "0.1.1"
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
10
  license = "MIT"
@@ -12,7 +12,7 @@ try:
12
12
 
13
13
  __version__ = _pkg_version("rrun-cli")
14
14
  except Exception: # noqa: BLE001 - 未安装(源码直跑)时回退
15
- __version__ = "0.1.1"
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,手动切分更可靠:
@@ -113,7 +113,7 @@ class ExecResult:
113
113
  def _check_ascii(values, what: str) -> None:
114
114
  for v in values:
115
115
  if not all(ord(c) < 128 for c in v):
116
- raise ValueError(f"{what} 含非 ASCII:{v!r};中文/特殊字符请写进脚本内容或 JSON 文件,不要走命令行")
116
+ raise ValueError(f"{what} must be ASCII-only, got: {v!r}; put CJK/special characters in the script content or a JSON file, not on the command line")
117
117
 
118
118
 
119
119
  def _ps_quote(s: str) -> str:
@@ -123,7 +123,7 @@ def _ps_quote(s: str) -> str:
123
123
 
124
124
  def build_ps_wrapper(code: str, args=(), workdir: str = "", env: "dict | None" = None) -> bytes:
125
125
  """把 PowerShell 脚本编码为单行纯 ASCII wrapper(-Command - 按行执行,禁止多行)。"""
126
- _check_ascii(args, "参数")
126
+ _check_ascii(args, "arguments")
127
127
  parts = [
128
128
  "[Console]::OutputEncoding=[Text.Encoding]::UTF8",
129
129
  "$OutputEncoding=[Text.Encoding]::UTF8",
@@ -157,7 +157,7 @@ def _posix_prefix(workdir: str = "", env: "dict | None" = None) -> str:
157
157
 
158
158
 
159
159
  def build_bash_command(args=(), workdir: str = "", env: "dict | None" = None) -> str:
160
- _check_ascii(args, "参数")
160
+ _check_ascii(args, "arguments")
161
161
  cmd = _posix_prefix(workdir, env) + "bash -s"
162
162
  if args:
163
163
  cmd += " -- " + " ".join(shlex.quote(a) for a in args)
@@ -169,7 +169,7 @@ def _ssh_run(machine: Machine, remote_cmd: str, stdin: bytes, timeout: "float |
169
169
  """返回 (exit_code, stdout, stderr, timed_out)。exit 255 即传输层错误。"""
170
170
  sshpass = shutil.which("sshpass")
171
171
  if not sshpass:
172
- raise RuntimeError("本机缺少 sshpass(sudo apt install sshpass)")
172
+ raise RuntimeError("sshpass not found on this machine (install: sudo apt install sshpass / brew install hudochenkov/sshpass/sshpass)")
173
173
  if mux:
174
174
  CONTROL_PATH_DIR.mkdir(mode=0o700, exist_ok=True)
175
175
  args = [sshpass, "-p", machine.password, "ssh", *SSH_BASE_OPTS]
@@ -181,7 +181,7 @@ def _ssh_run(machine: Machine, remote_cmd: str, stdin: bytes, timeout: "float |
181
181
  timeout=timeout if timeout and timeout > 0 else None)
182
182
  return p.returncode, p.stdout, p.stderr, False
183
183
  except subprocess.TimeoutExpired as e:
184
- return EXIT_TIMEOUT, e.stdout or b"", (e.stderr or b"") + f"\n[remote-exec] 本地超时 {timeout}s\n".encode(), True
184
+ return EXIT_TIMEOUT, e.stdout or b"", (e.stderr or b"") + f"\n[remote-exec] local timeout {timeout}s\n".encode(), True
185
185
 
186
186
 
187
187
  # 注意:代码内不得出现任何引号(bash 探测用单引号包裹此代码;chr(46)='.')
@@ -259,8 +259,8 @@ def detect_remote_python(machine: Machine, override: str = "", mux: bool = True)
259
259
  found = probe_python(machine, candidates, mux)
260
260
  if not found:
261
261
  raise RuntimeError(
262
- f"[{machine.name}] 未找到 python {REQUIRED_PY_MAJOR_MINOR[0]}.{REQUIRED_PY_MAJOR_MINOR[1]}"
263
- f"(基线版本已锁定)。请先初始化统一环境:rrun setup {machine.name}"
262
+ f"[{machine.name}] no python {REQUIRED_PY_MAJOR_MINOR[0]}.{REQUIRED_PY_MAJOR_MINOR[1]}"
263
+ f" found (baseline is pinned). Provision first: rrun setup {machine.name}"
264
264
  )
265
265
  result = found
266
266
  _python_cache[key] = result
@@ -278,9 +278,9 @@ def run(host: str, lang: str = "", file: str = "", content: str = "", args=(),
278
278
  """
279
279
  machine = resolve_machine(host)
280
280
  if file and content:
281
- raise ValueError("file 与 content 只能二选一")
281
+ raise ValueError("pass either file or content, not both")
282
282
  if not file and not content:
283
- raise ValueError("必须提供 file 或 content")
283
+ raise ValueError("file or content is required")
284
284
 
285
285
  if file:
286
286
  script_path = str(Path(file).resolve())
@@ -293,8 +293,8 @@ def run(host: str, lang: str = "", file: str = "", content: str = "", args=(),
293
293
  if not lang:
294
294
  lang = EXT_TO_LANG.get(suffix, machine.default_lang)
295
295
  if lang not in LANGS:
296
- raise ValueError(f"不支持的语言: {lang}(可选 {LANGS})")
297
- _check_ascii(args, "参数")
296
+ raise ValueError(f"unsupported language: {lang} (choose from {LANGS})")
297
+ _check_ascii(args, "arguments")
298
298
  _check_ascii([x for kv in (env or {}).items() for x in kv], "env")
299
299
 
300
300
  t0 = time.time()
@@ -313,7 +313,7 @@ def run(host: str, lang: str = "", file: str = "", content: str = "", args=(),
313
313
  py_cmd += " " + " ".join(shlex.quote(a) for a in args)
314
314
  if env or workdir:
315
315
  if machine.is_windows:
316
- raise ValueError("Windows + python 暂不支持 workdir/env,请在脚本内 os.chdir/os.environ 处理")
316
+ raise ValueError("workdir/env are not supported for Windows+python yet; use os.chdir/os.environ inside the script")
317
317
  remote_cmd = _posix_prefix(workdir, env) + py_cmd
318
318
  else:
319
319
  remote_cmd = py_cmd
@@ -158,6 +158,6 @@ def resolve_machine(host: str, path: "str | os.PathLike | None" = None) -> Machi
158
158
  for m in machines:
159
159
  if host and host in (m.name, m.ip, m.hostname):
160
160
  return m
161
- available = ", ".join(f"{m.name}({m.ip})" for m in machines) or "<空>"
162
- sources = ", ".join(str(p) for _lbl, p in candidate_sources() if p.is_file()) or "<无可用来源>"
163
- raise KeyError(f"机器 [{host}] 未找到。已扫描来源: {sources}。可用: {available}")
161
+ available = ", ".join(f"{m.name}({m.ip})" for m in machines) or "<none>"
162
+ sources = ", ".join(str(p) for _lbl, p in candidate_sources() if p.is_file()) or "<no sources available>"
163
+ raise KeyError(f"machine [{host}] not found. Scanned sources: {sources}. Available: {available}")
@@ -103,7 +103,7 @@ def _ensure_local_tarball(os_name: str, arch: str) -> Path:
103
103
  return path
104
104
  CACHE_DIR.mkdir(parents=True, exist_ok=True)
105
105
  url = PBS_DOWNLOAD_BASE + name
106
- print(f"[setup] 本机缓存缺失,下载 {url} ...", file=sys.stderr)
106
+ print(f"[setup] local cache miss, downloading {url} ...", file=sys.stderr)
107
107
  tmp = path.with_suffix(".part")
108
108
  urllib.request.urlretrieve(url, tmp) # noqa: S310 - 固定官方源
109
109
  tmp.rename(path)
@@ -128,7 +128,7 @@ def _install_standalone(machine: Machine, mux: bool, timeout: float) -> str:
128
128
  arch = out.decode("utf-8", "replace").strip()
129
129
  os_name = "Mac"
130
130
  if (os_name, arch) not in PBS_ASSETS:
131
- raise RuntimeError(f"[{machine.name}] 暂不支持的架构: {arch}")
131
+ raise RuntimeError(f"[{machine.name}] unsupported architecture: {arch}")
132
132
  extract_cmd = (
133
133
  'tools="$HOME/.remote-machine"; mkdir -p "$tools" '
134
134
  '&& rm -rf "$tools/python312" '
@@ -137,15 +137,15 @@ def _install_standalone(machine: Machine, mux: bool, timeout: float) -> str:
137
137
  )
138
138
  base_py = BASE_PY_POSIX
139
139
  tarball = _ensure_local_tarball(os_name, arch)
140
- print(f"[setup] [{machine.name}] 推送 standalone python ({tarball.name}, "
140
+ print(f"[setup] [{machine.name}] streaming standalone python ({tarball.name}, "
141
141
  f"{tarball.stat().st_size // 1024 // 1024}MB) ...", file=sys.stderr)
142
142
  rc, _, err, timed_out = _ssh_run(machine, extract_cmd, tarball.read_bytes(), timeout, mux)
143
143
  if rc != 0:
144
- raise RuntimeError(f"[{machine.name}] standalone 推送/解压失败(exit={rc}): "
144
+ raise RuntimeError(f"[{machine.name}] standalone push/extract failed (exit={rc}): "
145
145
  f"{err.decode('utf-8', 'replace')[:300]}")
146
146
  ver = probe_python_version(machine, base_py, mux)
147
147
  if not ver:
148
- raise RuntimeError(f"[{machine.name}] standalone 解压后验证失败: {base_py}")
148
+ raise RuntimeError(f"[{machine.name}] standalone python failed verification after extraction: {base_py}")
149
149
  return base_py
150
150
 
151
151
 
@@ -230,11 +230,11 @@ def setup_machine(host: str, force: bool = False, mux: bool = True,
230
230
  rc, out, err, _ = _ssh_run(machine, "bash -s", payload, timeout, mux)
231
231
  if rc != 0:
232
232
  tail = (err or out).decode("utf-8", "replace").strip()[-400:]
233
- raise RuntimeError(f"setup 脚本失败(exit={rc}): {tail}")
233
+ raise RuntimeError(f"setup script failed (exit={rc}): {tail}")
234
234
 
235
235
  final = probe_python(machine, [venv_py], mux)
236
236
  if not final:
237
- raise RuntimeError("setup 完成但 venv python 探测失败")
237
+ raise RuntimeError("setup finished but the venv python probe failed")
238
238
  result.venv_python, result.version = final
239
239
  result.packages = reqs
240
240
  result.ok = True
@@ -272,5 +272,5 @@ def venv_python_or_die(host: str, mux: bool = True) -> str:
272
272
  venv_py = VENV_PY_WIN if machine.is_windows else VENV_PY_POSIX
273
273
  hit = probe_python(machine, [venv_py], mux)
274
274
  if not hit:
275
- raise RuntimeError(f"[{machine.name}] 统一 venv 未初始化,请先:rrun setup {machine.name}")
275
+ raise RuntimeError(f"[{machine.name}] unified venv not provisioned yet; run first: rrun setup {machine.name}")
276
276
  return hit[0]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rrun-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
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
5
  Author: waqiju
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes