rrun-cli 0.2.3__tar.gz → 0.2.4__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.
- {rrun_cli-0.2.3/src/rrun_cli.egg-info → rrun_cli-0.2.4}/PKG-INFO +2 -2
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/README.md +1 -1
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/pyproject.toml +2 -2
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/__init__.py +1 -1
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/config_cmd.py +27 -16
- rrun_cli-0.2.4/src/rrun/machines.d.README.md +16 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/machines.template.json +1 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/registry.py +19 -6
- {rrun_cli-0.2.3 → rrun_cli-0.2.4/src/rrun_cli.egg-info}/PKG-INFO +2 -2
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/SOURCES.txt +1 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_config_cmd.py +69 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/LICENSE +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/setup.cfg +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/__main__.py +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/doctor.py +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/executor.py +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/remote-requirements.txt +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/setup.py +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/dependency_links.txt +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/entry_points.txt +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/requires.txt +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/top_level.txt +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_cli.py +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_executor.py +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_registry.py +0 -0
- {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rrun-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -134,7 +134,7 @@ Sources are merged by machine name, highest priority first (all optional, failur
|
|
|
134
134
|
2. `$REMOTE_MACHINE_CONFIG` (legacy name, still honored)
|
|
135
135
|
3. `./machines.json` (current working directory)
|
|
136
136
|
4. `~/.rrun/machines.json`
|
|
137
|
-
5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink)
|
|
137
|
+
5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink; `rrun config init` creates this directory with a README inside)
|
|
138
138
|
6. Legacy: `~/.remote-machine/machines.json` (POSIX) / `C:\tools\remote-machine\machines.json` (Windows)
|
|
139
139
|
|
|
140
140
|
Per-file `defaults` sections (keyed by `os`, lowercased) are applied before merging. Inspect the chain with `rrun config`; list machines (redacted) with `rrun machines`.
|
|
@@ -106,7 +106,7 @@ Sources are merged by machine name, highest priority first (all optional, failur
|
|
|
106
106
|
2. `$REMOTE_MACHINE_CONFIG` (legacy name, still honored)
|
|
107
107
|
3. `./machines.json` (current working directory)
|
|
108
108
|
4. `~/.rrun/machines.json`
|
|
109
|
-
5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink)
|
|
109
|
+
5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink; `rrun config init` creates this directory with a README inside)
|
|
110
110
|
6. Legacy: `~/.remote-machine/machines.json` (POSIX) / `C:\tools\remote-machine\machines.json` (Windows)
|
|
111
111
|
|
|
112
112
|
Per-file `defaults` sections (keyed by `os`, lowercased) are applied before merging. Inspect the chain with `rrun config`; list machines (redacted) with `rrun machines`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rrun-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
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"
|
|
@@ -45,7 +45,7 @@ package-dir = { "" = "src" }
|
|
|
45
45
|
where = ["src"]
|
|
46
46
|
|
|
47
47
|
[tool.setuptools.package-data]
|
|
48
|
-
rrun = ["remote-requirements.txt", "machines.template.json"]
|
|
48
|
+
rrun = ["remote-requirements.txt", "machines.template.json", "machines.d.README.md"]
|
|
49
49
|
|
|
50
50
|
[tool.pytest.ini_options]
|
|
51
51
|
testpaths = ["tests"]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""`rrun config` 子命令组:机器清单的初始化 / 追加 / 编辑 + 来源链诊断。
|
|
2
2
|
|
|
3
3
|
设计要点:
|
|
4
|
-
- init/add/edit
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
4
|
+
- init/add/edit 只操作「用户级主清单」,路径取 registry.user_inventory_path()(单一
|
|
5
|
+
真相源);不得在来源链上按 label 反查——cwd/env 来源与之同路径时去重会丢弃
|
|
6
|
+
user 条目。其他来源(env/cwd/machines.d/legacy)只读不动。
|
|
7
|
+
- init 顺带创建 machines.d/ 并内置 README.md(.d 模式的可发现性传统:目录自带说明)。
|
|
8
|
+
- 模板与 README 内嵌为 package data,离线可用、随发行版本锁定;写盘即收紧权限
|
|
9
|
+
(目录 700 / 文件 600,明文密码场景,Windows chmod 语义有限跳过)。
|
|
9
10
|
- add 双模式:不给 --name/--ip → 交互向导(getpass 隐藏密码,旗帜值作预填默认);
|
|
10
11
|
给了 → 纯旗帜模式(可脚本化);-i 强制向导。
|
|
11
12
|
- edit 走 $VISUAL/$EDITOR(Windows 回退 notepad,POSIX 回退 vi),退出后校验 JSON;
|
|
@@ -24,13 +25,14 @@ from pathlib import Path
|
|
|
24
25
|
from .registry import (
|
|
25
26
|
EMPTY_INVENTORY_HINT,
|
|
26
27
|
_load_file,
|
|
27
|
-
candidate_sources,
|
|
28
28
|
load_machines,
|
|
29
29
|
resolve_machine,
|
|
30
30
|
scan_sources,
|
|
31
|
+
user_inventory_path,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
TEMPLATE_FILE = Path(__file__).resolve().with_name("machines.template.json")
|
|
35
|
+
MACHINES_D_README = Path(__file__).resolve().with_name("machines.d.README.md")
|
|
34
36
|
|
|
35
37
|
_OS_ALIASES = {
|
|
36
38
|
"w": "Windows", "windows": "Windows",
|
|
@@ -39,14 +41,6 @@ _OS_ALIASES = {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def user_inventory_path() -> Path:
|
|
43
|
-
"""用户级主清单路径(从来源链派生,不写死 ~/.rrun)。"""
|
|
44
|
-
for label, p in candidate_sources():
|
|
45
|
-
if label == "user":
|
|
46
|
-
return p
|
|
47
|
-
raise RuntimeError("unreachable: candidate_sources() always includes the user source") # pragma: no cover
|
|
48
|
-
|
|
49
|
-
|
|
50
44
|
def _shorten(path: Path) -> str:
|
|
51
45
|
"""显示用:home 前缀折叠为 ~。"""
|
|
52
46
|
s = str(path)
|
|
@@ -125,15 +119,30 @@ def _report_added(path: Path, name: str) -> None:
|
|
|
125
119
|
print(f"[config] next: rrun doctor {name} # connectivity + python check")
|
|
126
120
|
|
|
127
121
|
|
|
122
|
+
def _ensure_machines_d() -> Path:
|
|
123
|
+
"""创建 machines.d/ 并内置 README(幂等;README 已存在不覆盖——用户可能改过)。"""
|
|
124
|
+
d = user_inventory_path().parent / "machines.d"
|
|
125
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
if os.name != "nt":
|
|
127
|
+
os.chmod(d, 0o700)
|
|
128
|
+
readme = d / "README.md"
|
|
129
|
+
if not readme.exists():
|
|
130
|
+
_write_secure(readme, MACHINES_D_README.read_text(encoding="utf-8"))
|
|
131
|
+
return d
|
|
132
|
+
|
|
133
|
+
|
|
128
134
|
def cmd_config_init(ns) -> int:
|
|
129
|
-
"""
|
|
135
|
+
"""按内嵌模板创建用户级主清单 + machines.d/;已存在时拒绝覆盖(--force 除外)。"""
|
|
130
136
|
path = user_inventory_path()
|
|
131
137
|
if path.exists() and not ns.force:
|
|
132
138
|
print(f"[config] {_shorten(path)} already exists (use --force to overwrite)", file=sys.stderr)
|
|
133
139
|
return 1
|
|
134
140
|
_write_secure(path, TEMPLATE_FILE.read_text(encoding="utf-8"))
|
|
141
|
+
d = _ensure_machines_d()
|
|
135
142
|
perms = "" if os.name == "nt" else ", mode 600"
|
|
136
143
|
print(f"[config] created {_shorten(path)} from the bundled template{perms}")
|
|
144
|
+
print(f"[config] machines.d ready at {_shorten(d)}/ — drop extra *.json inventories there"
|
|
145
|
+
" (see its README.md)")
|
|
137
146
|
print("[config] next: replace the my-* example entries with your machines"
|
|
138
147
|
" — 'rrun config edit' or 'rrun config add'")
|
|
139
148
|
print("[config] then: rrun machines # verify the inventory is picked up (redacted)")
|
|
@@ -257,7 +266,9 @@ def cmd_config_chain(ns) -> int:
|
|
|
257
266
|
print("overridden by higher-priority sources):")
|
|
258
267
|
raw_total = 0
|
|
259
268
|
for i, info in enumerate(infos, 1):
|
|
260
|
-
if
|
|
269
|
+
if info.path.is_dir():
|
|
270
|
+
state = "dir exists, no *.json files"
|
|
271
|
+
elif not info.exists:
|
|
261
272
|
state = "- missing"
|
|
262
273
|
elif info.error:
|
|
263
274
|
state = f"x read failed: {info.error}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# machines.d — additional machine inventories
|
|
2
|
+
|
|
3
|
+
Drop extra inventories here as `*.json` files (same format as
|
|
4
|
+
`~/.rrun/machines.json`). They are merged with the main inventory
|
|
5
|
+
by machine name:
|
|
6
|
+
|
|
7
|
+
- `~/.rrun/machines.json` wins on name conflicts (higher priority).
|
|
8
|
+
- Among `machines.d` files, filename order decides
|
|
9
|
+
(`10-*.json` beats `90-*.json`).
|
|
10
|
+
|
|
11
|
+
Symlinks are welcome — one inventory per file, e.g.:
|
|
12
|
+
|
|
13
|
+
ln -s ~/src/bot_home/config/machines.json ~/.rrun/machines.d/bot_home.json
|
|
14
|
+
|
|
15
|
+
Only `*.json` files are read; this README is ignored.
|
|
16
|
+
Full format: https://github.com/waqiju/rrun#configuration-machinesjson
|
|
@@ -76,6 +76,15 @@ class SourceInfo:
|
|
|
76
76
|
error: str = ""
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
def user_inventory_path() -> Path:
|
|
80
|
+
"""用户级主清单路径(单一真相源:candidate_sources 的 user 条目、config init/add/edit 共用)。
|
|
81
|
+
|
|
82
|
+
注意:不要从 candidate_sources() 的结果里按 label 反查这个路径——当 cwd 恰好是
|
|
83
|
+
~/.rrun 或 $RRUN_CONFIG 指向同一文件时,去重会丢弃 user 条目(v0.2.3 的教训)。
|
|
84
|
+
"""
|
|
85
|
+
return Path.home() / ".rrun" / "machines.json"
|
|
86
|
+
|
|
87
|
+
|
|
79
88
|
def candidate_sources() -> "list[tuple[str, Path]]":
|
|
80
89
|
"""返回 (标签, 路径) 有序来源链(高→低优先级),按 resolved path 去重。"""
|
|
81
90
|
cands: list[tuple[str, Path]] = []
|
|
@@ -86,12 +95,14 @@ def candidate_sources() -> "list[tuple[str, Path]]":
|
|
|
86
95
|
label = f"${env_name}" if i == 0 else f"${env_name}[{i}]"
|
|
87
96
|
cands.append((label, Path(p).expanduser()))
|
|
88
97
|
cands.append(("cwd", Path.cwd() / "machines.json"))
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
machines_d = rrun_home / "machines.d"
|
|
98
|
+
cands.append(("user", user_inventory_path()))
|
|
99
|
+
machines_d = user_inventory_path().parent / "machines.d"
|
|
92
100
|
if machines_d.is_dir():
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
files = sorted(machines_d.glob("*.json"))
|
|
102
|
+
if files:
|
|
103
|
+
cands.extend(("user.d", p) for p in files)
|
|
104
|
+
else:
|
|
105
|
+
cands.append(("user.d", machines_d)) # 空目录也入链:让诊断能看到它存在(merge 时 is_file 跳过)
|
|
95
106
|
if os.name == "nt":
|
|
96
107
|
cands.append(("legacy", Path(r"C:\tools\remote-machine\machines.json")))
|
|
97
108
|
else:
|
|
@@ -150,7 +161,9 @@ def scan_sources() -> "list[SourceInfo]":
|
|
|
150
161
|
infos = []
|
|
151
162
|
for label, p in candidate_sources():
|
|
152
163
|
info = SourceInfo(label=label, path=p)
|
|
153
|
-
if p.
|
|
164
|
+
if p.is_dir():
|
|
165
|
+
info.exists = True # 目录型来源(空 machines.d):无可读机器,仅示意存在
|
|
166
|
+
elif p.is_file():
|
|
154
167
|
info.exists = True
|
|
155
168
|
try:
|
|
156
169
|
info.machine_count = len(_load_file(p))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rrun-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -134,7 +134,7 @@ Sources are merged by machine name, highest priority first (all optional, failur
|
|
|
134
134
|
2. `$REMOTE_MACHINE_CONFIG` (legacy name, still honored)
|
|
135
135
|
3. `./machines.json` (current working directory)
|
|
136
136
|
4. `~/.rrun/machines.json`
|
|
137
|
-
5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink)
|
|
137
|
+
5. `~/.rrun/machines.d/*.json` (sorted by filename — point each inventory at its own file/symlink; `rrun config init` creates this directory with a README inside)
|
|
138
138
|
6. Legacy: `~/.remote-machine/machines.json` (POSIX) / `C:\tools\remote-machine\machines.json` (Windows)
|
|
139
139
|
|
|
140
140
|
Per-file `defaults` sections (keyed by `os`, lowercased) are applied before merging. Inspect the chain with `rrun config`; list machines (redacted) with `rrun machines`.
|
|
@@ -245,3 +245,72 @@ class TestEmptyStateHints:
|
|
|
245
245
|
def test_resolve_machine_hint(self, isolated):
|
|
246
246
|
with pytest.raises(KeyError, match="rrun config init"):
|
|
247
247
|
resolve_machine("nope")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class TestInventoryPathCollision:
|
|
251
|
+
"""v0.2.3 回归:cwd == ~/.rrun 或 $RRUN_CONFIG 指向用户清单时,来源链去重会丢弃
|
|
252
|
+
user 标签——user_inventory_path() 不得受此影响。"""
|
|
253
|
+
|
|
254
|
+
def test_cwd_inside_rrun_home(self, isolated, monkeypatch):
|
|
255
|
+
cmd_config_init(_ns(force=False))
|
|
256
|
+
monkeypatch.chdir(isolated / "home" / ".rrun")
|
|
257
|
+
assert cmd_config_add(_flag_ns(name="x", ip="1.1.1.1", user="u")) == 0
|
|
258
|
+
assert resolve_machine("x").ip == "1.1.1.1"
|
|
259
|
+
assert cmd_config_init(_ns(force=False)) == 1 # 仍能正确识别已存在
|
|
260
|
+
|
|
261
|
+
def test_env_points_at_user_file(self, isolated, monkeypatch):
|
|
262
|
+
cmd_config_init(_ns(force=False))
|
|
263
|
+
monkeypatch.setenv("RRUN_CONFIG", str(user_inventory_path()))
|
|
264
|
+
assert cmd_config_add(_flag_ns(name="x", ip="1.1.1.1", user="u")) == 0
|
|
265
|
+
assert resolve_machine("x").ip == "1.1.1.1"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TestMachinesD:
|
|
269
|
+
def test_init_creates_dir_and_readme(self, isolated):
|
|
270
|
+
cmd_config_init(_ns(force=False))
|
|
271
|
+
d = user_inventory_path().parent / "machines.d"
|
|
272
|
+
readme = d / "README.md"
|
|
273
|
+
assert readme.is_file() and "ln -s" in readme.read_text(encoding="utf-8")
|
|
274
|
+
if os.name != "nt":
|
|
275
|
+
assert stat.S_IMODE(d.stat().st_mode) == 0o700
|
|
276
|
+
|
|
277
|
+
def test_readme_not_treated_as_inventory(self, isolated):
|
|
278
|
+
cmd_config_init(_ns(force=False))
|
|
279
|
+
assert all("README" not in m.name for m in load_machines())
|
|
280
|
+
|
|
281
|
+
def test_ensure_machines_d_idempotent(self, isolated):
|
|
282
|
+
cmd_config_init(_ns(force=False))
|
|
283
|
+
readme = user_inventory_path().parent / "machines.d" / "README.md"
|
|
284
|
+
readme.write_text("user customized", encoding="utf-8")
|
|
285
|
+
cmd_config_init(_ns(force=True))
|
|
286
|
+
assert readme.read_text(encoding="utf-8") == "user customized" # 不覆盖用户改动
|
|
287
|
+
|
|
288
|
+
def test_chain_shows_empty_dir(self, isolated, capsys):
|
|
289
|
+
cmd_config_init(_ns(force=False))
|
|
290
|
+
capsys.readouterr()
|
|
291
|
+
cmd_config_chain(_ns())
|
|
292
|
+
assert "no *.json files" in capsys.readouterr().out
|
|
293
|
+
|
|
294
|
+
def test_chain_lists_json_files_when_present(self, isolated, capsys):
|
|
295
|
+
cmd_config_init(_ns(force=False))
|
|
296
|
+
d = user_inventory_path().parent / "machines.d"
|
|
297
|
+
(d / "10-x.json").write_text(
|
|
298
|
+
json.dumps({"machines": [{"name": "deep", "ip": "1.1.1.1", "user": "u"}]}),
|
|
299
|
+
encoding="utf-8")
|
|
300
|
+
capsys.readouterr()
|
|
301
|
+
cmd_config_chain(_ns())
|
|
302
|
+
out = capsys.readouterr().out
|
|
303
|
+
assert "10-x.json" in out and "no *.json" not in out
|
|
304
|
+
assert "deep" in [m.name for m in load_machines()]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TestTemplateHint:
|
|
308
|
+
def test_hint_present_and_ignored_by_loader(self, isolated):
|
|
309
|
+
cmd_config_init(_ns(force=False))
|
|
310
|
+
assert "_hint" in _read_inventory()
|
|
311
|
+
assert load_machines() # _hint 不影响加载
|
|
312
|
+
|
|
313
|
+
def test_hint_preserved_after_add(self, isolated):
|
|
314
|
+
cmd_config_init(_ns(force=False))
|
|
315
|
+
cmd_config_add(_flag_ns(name="z", ip="1.1.1.1", user="u"))
|
|
316
|
+
assert "_hint" in _read_inventory()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|