msdev 0.15.0__tar.gz → 0.15.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.
Files changed (113) hide show
  1. {msdev-0.15.0 → msdev-0.15.2}/PKG-INFO +14 -6
  2. {msdev-0.15.0 → msdev-0.15.2}/README.md +13 -5
  3. {msdev-0.15.0 → msdev-0.15.2}/pyproject.toml +1 -4
  4. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/__init__.py +1 -1
  5. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/cli.py +60 -29
  6. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/config.py +0 -17
  7. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/guides.py +12 -0
  8. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/inventory.py +1 -1
  9. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/limits.py +19 -1
  10. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/resources.py +50 -6
  11. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/context.py +1 -6
  12. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/environment.py +5 -6
  13. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/node.py +94 -36
  14. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/ssh_config.py +42 -0
  15. msdev-0.15.2/src/msdev/core/transport.py +1021 -0
  16. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/skills/msdev-cli/SKILL.md +20 -5
  17. {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/PKG-INFO +14 -6
  18. {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/SOURCES.txt +3 -36
  19. {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/entry_points.txt +0 -1
  20. {msdev-0.15.0 → msdev-0.15.2}/tests/test_cli.py +59 -16
  21. {msdev-0.15.0 → msdev-0.15.2}/tests/test_cli_only_architecture.py +1 -1
  22. {msdev-0.15.0 → msdev-0.15.2}/tests/test_compat.py +0 -12
  23. {msdev-0.15.0 → msdev-0.15.2}/tests/test_config.py +0 -18
  24. {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_transfer.py +5 -6
  25. msdev-0.15.2/tests/test_live_dev122_dev186_scenario.py +196 -0
  26. msdev-0.15.2/tests/test_local_docker_ssh_smoke_scenario.py +1078 -0
  27. {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_bootstrap_version_scenario.py +2 -3
  28. {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_connect_provision_scenario.py +1 -1
  29. {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_connect_save_password_scenario.py +1 -1
  30. {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_env_config.py +65 -0
  31. {msdev-0.15.0 → msdev-0.15.2}/tests/test_ssh_auth.py +9 -63
  32. {msdev-0.15.0 → msdev-0.15.2}/tests/test_target_exec_services.py +70 -22
  33. msdev-0.15.2/tests/test_transport.py +379 -0
  34. msdev-0.15.2/tests/test_version_scenario.py +47 -0
  35. {msdev-0.15.0 → msdev-0.15.2}/tests/test_windows_local_scenario.py +2 -19
  36. msdev-0.15.0/src/msdev/core/container_helper.py +0 -251
  37. msdev-0.15.0/src/msdev/core/daemon_deploy.py +0 -49
  38. msdev-0.15.0/src/msdev/core/daemon_version.py +0 -26
  39. msdev-0.15.0/src/msdev/core/helper_deploy.py +0 -143
  40. msdev-0.15.0/src/msdev/core/host_files.py +0 -92
  41. msdev-0.15.0/src/msdev/core/host_mutations.py +0 -332
  42. msdev-0.15.0/src/msdev/core/local_rpc.py +0 -85
  43. msdev-0.15.0/src/msdev/core/multiplex.py +0 -143
  44. msdev-0.15.0/src/msdev/core/request_client.py +0 -174
  45. msdev-0.15.0/src/msdev/core/ssh_tunnel.py +0 -561
  46. msdev-0.15.0/src/msdev/core/stream_limits.py +0 -17
  47. msdev-0.15.0/src/msdev/core/stream_server.py +0 -606
  48. msdev-0.15.0/src/msdev/core/stream_transport.py +0 -376
  49. msdev-0.15.0/src/msdev/core/streamproto.py +0 -129
  50. msdev-0.15.0/src/msdev/core/transport.py +0 -1498
  51. msdev-0.15.0/src/msdev/daemon.py +0 -1499
  52. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
  53. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -1
  54. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
  55. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -1
  56. msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64 +0 -0
  57. msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
  58. msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64 +0 -0
  59. msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
  60. msdev-0.15.0/tests/test_container_helper.py +0 -234
  61. msdev-0.15.0/tests/test_daemon.py +0 -1092
  62. msdev-0.15.0/tests/test_daemon_container_stream.py +0 -84
  63. msdev-0.15.0/tests/test_daemon_deploy.py +0 -150
  64. msdev-0.15.0/tests/test_docker_stream_dispatch.py +0 -237
  65. msdev-0.15.0/tests/test_go_msdevd_scenario.py +0 -209
  66. msdev-0.15.0/tests/test_helper_deploy.py +0 -185
  67. msdev-0.15.0/tests/test_multiplex.py +0 -199
  68. msdev-0.15.0/tests/test_request_client.py +0 -241
  69. msdev-0.15.0/tests/test_ssh_tunnel.py +0 -57
  70. msdev-0.15.0/tests/test_stream_server.py +0 -510
  71. msdev-0.15.0/tests/test_stream_transport.py +0 -314
  72. msdev-0.15.0/tests/test_streamproto.py +0 -65
  73. msdev-0.15.0/tests/test_transport.py +0 -1252
  74. {msdev-0.15.0 → msdev-0.15.2}/LICENSE +0 -0
  75. {msdev-0.15.0 → msdev-0.15.2}/setup.cfg +0 -0
  76. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/__init__.py +0 -0
  77. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/compat.py +0 -0
  78. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/config_bundle.py +0 -0
  79. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/direct_exec.py +0 -0
  80. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/env_exec.py +0 -0
  81. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/env_transfer.py +0 -0
  82. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/exec_argv.py +0 -0
  83. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/init_setup.py +0 -0
  84. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/__init__.py +0 -0
  85. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/capabilities.py +0 -0
  86. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/execution.py +0 -0
  87. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/model.py +0 -0
  88. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/npu.py +0 -0
  89. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/results.py +0 -0
  90. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/sftp_transfer.py +0 -0
  91. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/skill_install.py +0 -0
  92. {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/ssh_auth.py +0 -0
  93. {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/dependency_links.txt +0 -0
  94. {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/top_level.txt +0 -0
  95. {msdev-0.15.0 → msdev-0.15.2}/tests/test_config_bundle.py +0 -0
  96. {msdev-0.15.0 → msdev-0.15.2}/tests/test_connect_env_scenario.py +0 -0
  97. {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_exec_scenario.py +0 -0
  98. {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_put_get_scenario.py +0 -0
  99. {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_read_write_scenario.py +0 -0
  100. {msdev-0.15.0 → msdev-0.15.2}/tests/test_exec_argv.py +0 -0
  101. {msdev-0.15.0 → msdev-0.15.2}/tests/test_exec_command_scenario.py +0 -0
  102. {msdev-0.15.0 → msdev-0.15.2}/tests/test_exec_default_timeout_scenario.py +0 -0
  103. {msdev-0.15.0 → msdev-0.15.2}/tests/test_init_skill.py +0 -0
  104. {msdev-0.15.0 → msdev-0.15.2}/tests/test_inventory.py +0 -0
  105. {msdev-0.15.0 → msdev-0.15.2}/tests/test_model_cli.py +0 -0
  106. {msdev-0.15.0 → msdev-0.15.2}/tests/test_model_services.py +0 -0
  107. {msdev-0.15.0 → msdev-0.15.2}/tests/test_msdev_cli_skill.py +0 -0
  108. {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_env_services.py +0 -0
  109. {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_sync_ssh_scenario.py +0 -0
  110. {msdev-0.15.0 → msdev-0.15.2}/tests/test_sftp_transfer_scenario.py +0 -0
  111. {msdev-0.15.0 → msdev-0.15.2}/tests/test_ssh_config.py +0 -0
  112. {msdev-0.15.0 → msdev-0.15.2}/tests/test_target_guides.py +0 -0
  113. {msdev-0.15.0 → msdev-0.15.2}/tests/test_windows_git_bash_ssh_scenario.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msdev
3
- Version: 0.15.0
3
+ Version: 0.15.2
4
4
  Summary: Portable nodes and execution environments for msModelSlim development
5
5
  Author: msModelSlim Agent Contributors
6
6
  License-Expression: MulanPSL-2.0
@@ -44,6 +44,7 @@ git clone https://gitcode.com/rookie_hongchuan/msdev.git
44
44
  cd msdev
45
45
  python3 -m pip install -e .
46
46
  msdev --help
47
+ msdev --version
47
48
  ```
48
49
 
49
50
  The package installs the `msdev` CLI.
@@ -115,13 +116,20 @@ Connect atomically creates a same-named host Env on first use. There is no
115
116
  remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
116
117
  re-check SSH readiness after connect problems.
117
118
 
118
- Build optional static helpers before release or local testing:
119
+ Inspect connectivity for one node or for every registered node (probes run in
120
+ parallel; partial failures exit non-zero):
119
121
 
120
122
  ```bash
121
- bash file-helper/build-static.sh
123
+ msdev node status
124
+ msdev node status dev-122
122
125
  ```
123
126
 
124
- Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
127
+ Rename a node together with its same-named host Env, guide files, and SSH
128
+ `Host` alias:
129
+
130
+ ```bash
131
+ msdev node rename dev-122 lab-122
132
+ ```
125
133
 
126
134
  Persistent OpenSSH masters are optional. All SSH from msdev is
127
135
  non-interactive: host keys use `accept-new`, and password/passphrase prompts
@@ -171,11 +179,11 @@ msdev env add dev-122-root \
171
179
  msdev env open dev-122-root
172
180
  ```
173
181
 
174
- Open a human-operated interactive shell in an Env:
182
+ Open an interactive shell in an env. Docker envs enter the container:
175
183
 
176
184
  ```bash
177
185
  msdev env shell dev-122
178
- msdev env shell dev-122 --cwd /srv/project
186
+ msdev env shell dev-122-docker --cwd /srv/project
179
187
  ```
180
188
 
181
189
  Agent automation should use `msdev exec`, not `env shell`.
@@ -19,6 +19,7 @@ git clone https://gitcode.com/rookie_hongchuan/msdev.git
19
19
  cd msdev
20
20
  python3 -m pip install -e .
21
21
  msdev --help
22
+ msdev --version
22
23
  ```
23
24
 
24
25
  The package installs the `msdev` CLI.
@@ -90,13 +91,20 @@ Connect atomically creates a same-named host Env on first use. There is no
90
91
  remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
91
92
  re-check SSH readiness after connect problems.
92
93
 
93
- Build optional static helpers before release or local testing:
94
+ Inspect connectivity for one node or for every registered node (probes run in
95
+ parallel; partial failures exit non-zero):
94
96
 
95
97
  ```bash
96
- bash file-helper/build-static.sh
98
+ msdev node status
99
+ msdev node status dev-122
97
100
  ```
98
101
 
99
- Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
102
+ Rename a node together with its same-named host Env, guide files, and SSH
103
+ `Host` alias:
104
+
105
+ ```bash
106
+ msdev node rename dev-122 lab-122
107
+ ```
100
108
 
101
109
  Persistent OpenSSH masters are optional. All SSH from msdev is
102
110
  non-interactive: host keys use `accept-new`, and password/passphrase prompts
@@ -146,11 +154,11 @@ msdev env add dev-122-root \
146
154
  msdev env open dev-122-root
147
155
  ```
148
156
 
149
- Open a human-operated interactive shell in an Env:
157
+ Open an interactive shell in an env. Docker envs enter the container:
150
158
 
151
159
  ```bash
152
160
  msdev env shell dev-122
153
- msdev env shell dev-122 --cwd /srv/project
161
+ msdev env shell dev-122-docker --cwd /srv/project
154
162
  ```
155
163
 
156
164
  Agent automation should use `msdev exec`, not `env shell`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "msdev"
7
- version = "0.15.0"
7
+ version = "0.15.2"
8
8
  description = "Portable nodes and execution environments for msModelSlim development"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -35,7 +35,6 @@ Repository = "https://gitcode.com/rookie_hongchuan/msdev"
35
35
 
36
36
  [project.scripts]
37
37
  msdev = "msdev.cli:main"
38
- msdevd = "msdev.daemon:main"
39
38
 
40
39
  [tool.setuptools]
41
40
  package-dir = { "" = "src" }
@@ -45,8 +44,6 @@ where = ["src"]
45
44
 
46
45
  [tool.setuptools.package-data]
47
46
  msdev = [
48
- "helpers/msdev-file-helper-*",
49
- "helpers/msdevd-linux-*",
50
47
  "skills/msdev-cli/SKILL.md",
51
48
  ]
52
49
 
@@ -1,3 +1,3 @@
1
1
  """Portable node and execution-environment tooling for msModelSlim."""
2
2
 
3
- __version__ = "0.15.0"
3
+ __version__ = "0.15.2"
@@ -154,8 +154,6 @@ def _provision_request_for_connect(
154
154
  if ssh_host_defined(args.name):
155
155
  return NodeAddRequest(
156
156
  name=args.name,
157
- remote_bin=args.remote_bin,
158
- auto_bootstrap=not args.no_auto_bootstrap,
159
157
  force=args.force,
160
158
  )
161
159
  raise ValueError(
@@ -169,8 +167,6 @@ def _provision_request_for_connect(
169
167
  port=args.port,
170
168
  identity_file=args.identity_file,
171
169
  ssh_host=args.ssh_host,
172
- remote_bin=args.remote_bin,
173
- auto_bootstrap=not args.no_auto_bootstrap,
174
170
  force=bool(
175
171
  args.force or has_params
176
172
  ),
@@ -204,9 +200,7 @@ def command_node(
204
200
  print("local\tregistered\t-")
205
201
  for node in service.list().nodes:
206
202
  print(
207
- f"{node.name}\tregistered\tssh={node.ssh_host}\t"
208
- f"auto-bootstrap={'on' if node.auto_bootstrap else 'off'}\t"
209
- f"bin={node.remote_bin}"
203
+ f"{node.name}\tregistered\tssh={node.ssh_host}"
210
204
  )
211
205
  if args.ssh:
212
206
  for host in service.list_ssh_hosts():
@@ -236,12 +230,39 @@ def command_node(
236
230
  if removed:
237
231
  print("removed environments: " + ", ".join(removed))
238
232
  return 0
239
- if action == "status":
240
- print(f"node: {args.name}")
241
- _print_json(service.status(args.name))
233
+ if action == "rename":
234
+ node = service.rename(args.name, args.new_name)
235
+ print(f"renamed node {args.name} -> {node.name} (ssh={node.ssh_host})")
242
236
  return 0
237
+ if action == "status":
238
+ if args.name:
239
+ print(f"node: {args.name}")
240
+ _print_json(service.status(args.name))
241
+ return 0
242
+ result = service.status_all()
243
+ for item in result.items:
244
+ if item.error is not None:
245
+ print(
246
+ f"{item.node} ({item.ssh_host}): unreachable: {item.error}",
247
+ file=sys.stderr,
248
+ )
249
+ else:
250
+ info = item.info if isinstance(item.info, dict) else {}
251
+ parts: list[str] = []
252
+ hostname = str(info.get("hostname") or "")
253
+ ip = str(info.get("ip") or "")
254
+ target = str(info.get("target") or "")
255
+ if hostname:
256
+ parts.append(f"hostname={hostname}")
257
+ if ip:
258
+ parts.append(f"ip={ip}")
259
+ if target and target not in {hostname, ip, item.ssh_host}:
260
+ parts.append(f"target={target}")
261
+ detail = (" " + " ".join(parts)) if parts else ""
262
+ print(f"{item.node} ({item.ssh_host}): reachable{detail}")
263
+ return result.exit_code
243
264
  if action == "bootstrap":
244
- result = service.bootstrap(args.name, package_path=args.package)
265
+ result = service.bootstrap(args.name)
245
266
  _print_json(result)
246
267
  return 0
247
268
  if action in {"connect", "disconnect"}:
@@ -281,8 +302,8 @@ def command_node(
281
302
  key = getattr(item, "key", None)
282
303
  if key:
283
304
  suffix = f"(persist={item.persist}, key={key})"
284
- if item.agent_ready:
285
- suffix = suffix[:-1] + ", agent=ready)"
305
+ if item.ssh_ready:
306
+ suffix = suffix[:-1] + ", ssh=ready)"
286
307
  print(
287
308
  f"{item.node}: {item.state} to {item.ssh_host} "
288
309
  f"{suffix}"
@@ -573,6 +594,12 @@ def build_parser() -> argparse.ArgumentParser:
573
594
  default=None,
574
595
  help="override local node/environment registry path",
575
596
  )
597
+ parser.add_argument(
598
+ "--version",
599
+ action="version",
600
+ version=f"%(prog)s {__version__}",
601
+ help="print the msdev version and exit",
602
+ )
576
603
  sub = parser.add_subparsers(dest="command", required=True)
577
604
 
578
605
  node = sub.add_parser("node", help="manage SSH connection nodes")
@@ -615,19 +642,26 @@ def build_parser() -> argparse.ArgumentParser:
615
642
  action="store_true",
616
643
  help="also remove every environment on this node",
617
644
  )
618
- node_status = node_sub.add_parser("status")
619
- node_status.add_argument("name")
645
+ node_rename = node_sub.add_parser(
646
+ "rename",
647
+ help="rename a node, its host environment, and a matching SSH Host alias",
648
+ )
649
+ node_rename.add_argument("name")
650
+ node_rename.add_argument("new_name")
651
+ node_status = node_sub.add_parser(
652
+ "status",
653
+ help="check node connectivity (every registered SSH node by default)",
654
+ )
655
+ node_status.add_argument(
656
+ "name",
657
+ nargs="?",
658
+ help="check one node; omit to check every registered SSH node",
659
+ )
620
660
  node_bootstrap = node_sub.add_parser(
621
661
  "bootstrap",
622
662
  help="ensure SSH reachability / ControlMaster for the node",
623
663
  )
624
664
  node_bootstrap.add_argument("name")
625
- node_bootstrap.add_argument(
626
- "--package",
627
- type=Path,
628
- default=None,
629
- help="ignored (kept for compatibility; remote msdevd is no longer deployed)",
630
- )
631
665
  node_connect = node_sub.add_parser(
632
666
  "connect",
633
667
  help=(
@@ -659,17 +693,11 @@ def build_parser() -> argparse.ArgumentParser:
659
693
  "--ssh-host",
660
694
  help="legacy shorthand for user@hostname when writing ~/.ssh/config",
661
695
  )
662
- node_connect.add_argument("--remote-bin", default="~/.local/bin/msdevd")
663
696
  node_connect.add_argument(
664
697
  "--force",
665
698
  action="store_true",
666
699
  help="replace an existing node and its same-named host environment",
667
700
  )
668
- node_connect.add_argument(
669
- "--no-auto-bootstrap",
670
- action="store_true",
671
- help="ignored (kept for compatibility; no remote agent deploy)",
672
- )
673
701
  node_connect.add_argument(
674
702
  "--password",
675
703
  default=None,
@@ -749,7 +777,7 @@ def build_parser() -> argparse.ArgumentParser:
749
777
  environment_inspect.add_argument("name")
750
778
  environment_open = environment_sub.add_parser(
751
779
  "open",
752
- help="establish the shared environment session (runs setup once)",
780
+ help="check that the environment is ready (setup runs on each exec)",
753
781
  )
754
782
  environment_open.add_argument("name")
755
783
  environment_guide = environment_sub.add_parser("guide")
@@ -757,7 +785,10 @@ def build_parser() -> argparse.ArgumentParser:
757
785
  environment_guide_action = environment_guide.add_mutually_exclusive_group()
758
786
  environment_guide_action.add_argument("--write", metavar="FILE")
759
787
  environment_guide_action.add_argument("--delete", action="store_true")
760
- environment_shell = environment_sub.add_parser("shell")
788
+ environment_shell = environment_sub.add_parser(
789
+ "shell",
790
+ help="open an interactive shell in the environment (including Docker)",
791
+ )
761
792
  environment_shell.add_argument("name")
762
793
  environment_shell.add_argument("--cwd")
763
794
  environment_put = environment_sub.add_parser(
@@ -354,20 +354,3 @@ def config_home() -> Path:
354
354
  return Path(override).expanduser()
355
355
  base = os.environ.get("XDG_CONFIG_HOME")
356
356
  return Path(base).expanduser() / "msdev" if base else Path.home() / ".config" / "msdev"
357
-
358
-
359
- def data_home() -> Path:
360
- override = os.environ.get("MSDEV_DATA_HOME")
361
- if override:
362
- return Path(override).expanduser()
363
- base = os.environ.get("XDG_DATA_HOME")
364
- return Path(base).expanduser() / "msdevd" if base else Path.home() / ".local" / "share" / "msdevd"
365
-
366
-
367
- def default_socket_path() -> Path:
368
- override = os.environ.get("MSDEVD_SOCKET")
369
- if override:
370
- return Path(override).expanduser()
371
- # SSH sessions frequently lack XDG_RUNTIME_DIR while systemd user services
372
- # set it to /run/user/<uid>. A stable per-user path keeps both sides aligned.
373
- return data_home() / "msdevd.sock"
@@ -89,6 +89,18 @@ class ResourceGuideStore:
89
89
  except FileNotFoundError:
90
90
  return False
91
91
 
92
+ def rename(self, old: str, new: str) -> bool:
93
+ """Move a guide file. Return whether one existed."""
94
+ source = self.path(old)
95
+ if not source.exists():
96
+ return False
97
+ destination = self.path(new)
98
+ if destination.exists():
99
+ raise ValueError(f"guide already exists for {new!r}")
100
+ destination.parent.mkdir(parents=True, exist_ok=True)
101
+ os.replace(source, destination)
102
+ return True
103
+
92
104
 
93
105
  class NodeGuideStore(ResourceGuideStore):
94
106
  def __init__(self, root: Path | None = None):
@@ -44,7 +44,7 @@ def _utc_now() -> str:
44
44
 
45
45
 
46
46
  class Inventory:
47
- """SQLite inventory owned by one user-level daemon."""
47
+ """SQLite inventory for model artifacts and replicas."""
48
48
 
49
49
  def __init__(self, data_dir: Path):
50
50
  self.data_dir = data_dir.expanduser()
@@ -10,9 +10,15 @@ MAX_RPC_RESPONSE_BYTES = 32 * 1024 * 1024
10
10
  MAX_RPC_ERROR_BYTES = 1 * 1024 * 1024
11
11
 
12
12
  DEFAULT_EXEC_TIMEOUT_SECONDS = -1.0
13
- EXEC_RPC_COMPLETION_GRACE_SECONDS = 10.0
14
13
  RPC_PROCESS_CLEANUP_GRACE_SECONDS = 2.0
15
14
 
15
+ # Infrastructure budget for ControlMaster probe / discard / (re)connect.
16
+ # Separate from exec command deadline (DEFAULT_EXEC_TIMEOUT_SECONDS = -1).
17
+ DEFAULT_SSH_SETUP_TIMEOUT_SECONDS = 15.0
18
+ DEFAULT_SSH_MUX_PROBE_TIMEOUT_SECONDS = 5.0
19
+ # Per-node budget for ``msdev node status`` (parallel; no ControlMaster setup).
20
+ DEFAULT_SSH_STATUS_TIMEOUT_SECONDS = 3.0
21
+
16
22
 
17
23
  def validate_exec_timeout_seconds(value: Any) -> float:
18
24
  if (
@@ -26,6 +32,18 @@ def validate_exec_timeout_seconds(value: Any) -> float:
26
32
  )
27
33
  return float(value)
28
34
 
35
+
36
+ def validate_positive_timeout_seconds(value: Any, *, label: str) -> float:
37
+ if (
38
+ isinstance(value, bool)
39
+ or not isinstance(value, (int, float))
40
+ or not math.isfinite(value)
41
+ or value <= 0
42
+ ):
43
+ raise ValueError(f"{label} must be a positive finite number")
44
+ return float(value)
45
+
46
+
29
47
  # Budget for conservative six-byte JSON escaping:
30
48
  # 8 MiB content * 6 + 64 maps * 2 paths * 4 KiB * 6 = 51 MiB.
31
49
  # The remaining 13 MiB covers the JSON-RPC envelope and structural overhead.
@@ -21,19 +21,17 @@ def _required(value: str, label: str) -> str:
21
21
  return value
22
22
 
23
23
 
24
+ _NODE_LEGACY_FIELDS = frozenset({"remote_bin", "auto_bootstrap"})
25
+
26
+
24
27
  @dataclass(frozen=True)
25
28
  class Node:
26
29
  name: str
27
30
  ssh_host: str
28
- remote_bin: str = "~/.local/bin/msdevd"
29
- auto_bootstrap: bool = True
30
31
 
31
32
  def __post_init__(self) -> None:
32
33
  _required(self.name, "node name")
33
34
  _required(self.ssh_host, "SSH host")
34
- _required(self.remote_bin, "remote binary")
35
- if type(self.auto_bootstrap) is not bool:
36
- raise ValueError("auto_bootstrap must be a boolean")
37
35
 
38
36
 
39
37
  @dataclass(frozen=True)
@@ -124,7 +122,12 @@ class _ResourceStore:
124
122
  environments: dict[str, dict[str, Any]] = {}
125
123
  try:
126
124
  for name, raw in value["nodes"].items():
127
- node = Node(**raw)
125
+ node_raw = {
126
+ key: item
127
+ for key, item in dict(raw).items()
128
+ if key not in _NODE_LEGACY_FIELDS
129
+ }
130
+ node = Node(**node_raw)
128
131
  if node.name != name:
129
132
  raise ValueError(f"node key {name!r} does not match its name")
130
133
  nodes[name] = asdict(node)
@@ -248,6 +251,47 @@ class NodeStore(_ResourceStore):
248
251
  self._save(self._normalize_saved(value))
249
252
  return references
250
253
 
254
+ def rename(self, old: str, new: str) -> Node:
255
+ """Rename a node and retarget environments that reference it.
256
+
257
+ The same-named host environment is renamed with the node. ``ssh_host``
258
+ is updated only when it was the old node name.
259
+ """
260
+ if old == "local" or new == "local":
261
+ raise ValueError("node name 'local' is reserved")
262
+ _required(old, "node name")
263
+ _required(new, "node name")
264
+ if old == new:
265
+ return self.get(old)
266
+ with _exclusive_file_lock(self.lock_path):
267
+ value = self._load_unlocked()
268
+ if old not in value["nodes"]:
269
+ raise KeyError(f"unknown node: {old}")
270
+ if new in value["nodes"]:
271
+ raise ValueError(f"node {new!r} already exists")
272
+ environments = value["environments"]
273
+ if old in environments and new in environments:
274
+ raise ValueError(f"environment {new!r} already exists")
275
+ raw = dict(value["nodes"].pop(old))
276
+ ssh_host = raw["ssh_host"]
277
+ if ssh_host == old:
278
+ ssh_host = new
279
+ node = Node(name=new, ssh_host=ssh_host)
280
+ value["nodes"][new] = asdict(node)
281
+ rewritten: dict[str, Any] = {}
282
+ for name, environment in environments.items():
283
+ updated = dict(environment)
284
+ if updated.get("node") == old:
285
+ updated["node"] = new
286
+ if name == old:
287
+ updated["name"] = new
288
+ rewritten[new] = updated
289
+ else:
290
+ rewritten[name] = updated
291
+ value["environments"] = rewritten
292
+ self._save(self._normalize_saved(value))
293
+ return node
294
+
251
295
 
252
296
  class EnvironmentStore(_ResourceStore):
253
297
  def add(self, environment: Environment, *, force: bool = False) -> None:
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
- from pathlib import Path
7
6
  from typing import Any, Protocol
8
7
 
9
8
  from ..resources import Environment, EnvironmentStore, Node, NodeStore
@@ -24,11 +23,7 @@ class TransportFactory(Protocol):
24
23
 
25
24
 
26
25
  class BootstrapNode(Protocol):
27
- def __call__(
28
- self,
29
- node: Node,
30
- package_path: Path | None = None,
31
- ) -> dict[str, Any]: ...
26
+ def __call__(self, node: Node) -> dict[str, Any]: ...
32
27
 
33
28
 
34
29
  def create_default_transport(
@@ -189,13 +189,12 @@ class EnvironmentService:
189
189
  def open(self, name: str) -> dict[str, Any]:
190
190
  environment = self._environment(name)
191
191
  transport = self.context.transport_for_environment(environment.name)
192
- open_env = getattr(transport, "_ensure_env_open", None)
193
- if open_env is None:
194
- raise TypeError("environment transport does not support env.open")
195
- open_env()
196
- result = transport.call("env.status", {"_env": environment.to_agent_spec()})
192
+ connect = getattr(transport, "connect", None)
193
+ if connect is not None:
194
+ connect()
195
+ result = transport.call("env.open", {"_env": environment.to_agent_spec()})
197
196
  if not isinstance(result, dict):
198
- raise RuntimeError("env.status returned a non-object result")
197
+ raise RuntimeError("env.open returned a non-object result")
199
198
  return result
200
199
 
201
200
  def remove(self, name: str) -> str: