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.
Files changed (26) hide show
  1. {rrun_cli-0.2.3/src/rrun_cli.egg-info → rrun_cli-0.2.4}/PKG-INFO +2 -2
  2. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/README.md +1 -1
  3. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/pyproject.toml +2 -2
  4. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/__init__.py +1 -1
  5. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/config_cmd.py +27 -16
  6. rrun_cli-0.2.4/src/rrun/machines.d.README.md +16 -0
  7. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/machines.template.json +1 -0
  8. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/registry.py +19 -6
  9. {rrun_cli-0.2.3 → rrun_cli-0.2.4/src/rrun_cli.egg-info}/PKG-INFO +2 -2
  10. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/SOURCES.txt +1 -0
  11. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_config_cmd.py +69 -0
  12. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/LICENSE +0 -0
  13. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/setup.cfg +0 -0
  14. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/__main__.py +0 -0
  15. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/doctor.py +0 -0
  16. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/executor.py +0 -0
  17. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/remote-requirements.txt +0 -0
  18. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun/setup.py +0 -0
  19. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/dependency_links.txt +0 -0
  20. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/entry_points.txt +0 -0
  21. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/requires.txt +0 -0
  22. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/src/rrun_cli.egg-info/top_level.txt +0 -0
  23. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_cli.py +0 -0
  24. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_executor.py +0 -0
  25. {rrun_cli-0.2.3 → rrun_cli-0.2.4}/tests/test_registry.py +0 -0
  26. {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
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.3"
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"]
@@ -11,7 +11,7 @@ try:
11
11
 
12
12
  __version__ = _pkg_version("rrun-cli")
13
13
  except Exception: # noqa: BLE001 - 未安装(源码直跑)时回退
14
- __version__ = "0.2.3"
14
+ __version__ = "0.2.4"
15
15
 
16
16
  from .doctor import DoctorResult, check_machine
17
17
  from .executor import (
@@ -1,11 +1,12 @@
1
1
  """`rrun config` 子命令组:机器清单的初始化 / 追加 / 编辑 + 来源链诊断。
2
2
 
3
3
  设计要点:
4
- - init/add/edit 只操作「用户级主清单」(registry 来源链中的 user 位),路径从
5
- candidate_sources() 派生而非写死 ~/.rrun,保证写入位置就是加载位置;
6
- 其他来源(env/cwd/machines.d/legacy)只读不动。
7
- - 模板内嵌为 package data(src/rrun/machines.template.json),离线可用、随发行版本
8
- 锁定;写盘即收紧权限(目录 700 / 文件 600,明文密码场景,Windows chmod 语义有限跳过)。
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
- """按内嵌模板创建用户级主清单;已存在时拒绝覆盖(--force 除外)。"""
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 not info.exists:
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
@@ -4,6 +4,7 @@
4
4
  "mac": { "os": "Mac" },
5
5
  "linux": { "os": "Linux" }
6
6
  },
7
+ "_hint": "more inventories: drop *.json into ~/.rrun/machines.d/ (see its README.md); docs: https://github.com/waqiju/rrun",
7
8
  "machines": [
8
9
  {
9
10
  "name": "my-win-box",
@@ -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
- rrun_home = Path.home() / ".rrun"
90
- cands.append(("user", rrun_home / "machines.json"))
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
- for p in sorted(machines_d.glob("*.json")):
94
- cands.append(("user.d", p))
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.is_file():
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
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`.
@@ -6,6 +6,7 @@ src/rrun/__main__.py
6
6
  src/rrun/config_cmd.py
7
7
  src/rrun/doctor.py
8
8
  src/rrun/executor.py
9
+ src/rrun/machines.d.README.md
9
10
  src/rrun/machines.template.json
10
11
  src/rrun/registry.py
11
12
  src/rrun/remote-requirements.txt
@@ -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