runspec-linux 0.1.3__tar.gz → 0.3.0__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 (28) hide show
  1. {runspec_linux-0.1.3 → runspec_linux-0.3.0}/.gitignore +3 -0
  2. runspec_linux-0.3.0/CHANGELOG.md +99 -0
  3. {runspec_linux-0.1.3 → runspec_linux-0.3.0}/PKG-INFO +2 -1
  4. {runspec_linux-0.1.3 → runspec_linux-0.3.0}/pyproject.toml +8 -1
  5. runspec_linux-0.3.0/runspec_linux/__init__.py +70 -0
  6. runspec_linux-0.3.0/runspec_linux/containers.py +42 -0
  7. runspec_linux-0.3.0/runspec_linux/files.py +28 -0
  8. runspec_linux-0.3.0/runspec_linux/logs.py +52 -0
  9. runspec_linux-0.3.0/runspec_linux/nc_command.py +38 -0
  10. runspec_linux-0.3.0/runspec_linux/network.py +34 -0
  11. runspec_linux-0.3.0/runspec_linux/packages.py +74 -0
  12. runspec_linux-0.3.0/runspec_linux/power.py +20 -0
  13. {runspec_linux-0.1.3 → runspec_linux-0.3.0}/runspec_linux/runspec.toml +73 -0
  14. runspec_linux-0.3.0/runspec_linux/security.py +24 -0
  15. runspec_linux-0.3.0/runspec_linux/services.py +40 -0
  16. runspec_linux-0.3.0/runspec_linux/system_info.py +43 -0
  17. runspec_linux-0.1.3/CHANGELOG.md +0 -51
  18. runspec_linux-0.1.3/runspec_linux/__init__.py +0 -3
  19. runspec_linux-0.1.3/runspec_linux/containers.py +0 -117
  20. runspec_linux-0.1.3/runspec_linux/files.py +0 -72
  21. runspec_linux-0.1.3/runspec_linux/logs.py +0 -81
  22. runspec_linux-0.1.3/runspec_linux/nc_command.py +0 -65
  23. runspec_linux-0.1.3/runspec_linux/network.py +0 -159
  24. runspec_linux-0.1.3/runspec_linux/security.py +0 -70
  25. runspec_linux-0.1.3/runspec_linux/services.py +0 -119
  26. runspec_linux-0.1.3/runspec_linux/system_info.py +0 -140
  27. {runspec_linux-0.1.3 → runspec_linux-0.3.0}/tests/__init__.py +0 -0
  28. {runspec_linux-0.1.3 → runspec_linux-0.3.0}/tests/test_nc_send.py +0 -0
@@ -56,3 +56,6 @@ chainlit-docs/
56
56
 
57
57
  # Claude Code local config (machine-specific)
58
58
  .claude/launch.json
59
+
60
+ # Stray committed test venv (removed from tracking)
61
+ .venv-test/
@@ -0,0 +1,99 @@
1
+ # runspec-linux Changelog
2
+
3
+ ## [0.3.0] — 2026-06-20
4
+
5
+ Add 6 runnables for managing a fleet's packages and power state from
6
+ runspec-console — thin wrappers over the new `runspec-linux-core` 0.2.0 helpers
7
+ (dependency floor bumped to `>=0.2.0`). They work across the common distro
8
+ families (apt/dnf/yum/zypper/pacman).
9
+
10
+ **Packages**
11
+
12
+ - `list-packages` (autonomous) — list installed packages, optional `--filter`
13
+ substring and `--limit`.
14
+ - `list-upgrades` (autonomous) — list packages with an available upgrade;
15
+ refreshes the index first, or pass `--no-refresh` to read the cache without
16
+ root.
17
+ - `upgrade-packages` (confirm) — refresh the index and upgrade everything.
18
+ - `install-package` (confirm) — install one or more packages (`--package`,
19
+ space-separated for several).
20
+ - `remove-package` (confirm) — remove one or more packages.
21
+
22
+ **Power**
23
+
24
+ - `reboot-host` (confirm) — reboot the host now, or schedule with
25
+ `--delay-minutes`.
26
+
27
+ The read-only runnables are `autonomous`; the state-changing ones are `confirm`,
28
+ matching `restart-service` / `restart-container`. Missing package manager / a
29
+ non-root failure returns structured JSON (`{... false, "error": ...}`) and a
30
+ non-zero exit rather than a stack trace; package names are validated before they
31
+ reach the package manager.
32
+
33
+ ## [0.2.0] — 2026-06-09
34
+
35
+ Split the pure logic out into a new dependency-free package,
36
+ **`runspec-linux-core`**, and make `runspec-linux` depend on it. Every runnable
37
+ module is now a thin wrapper: it parses args via `runspec`, calls the matching
38
+ `runspec_linux_core` function, and renders the JSON/exit behaviour. The helper
39
+ functions are re-exported from `runspec_linux` for back-compat
40
+ (`from runspec_linux import nc_send` and friends still work), and
41
+ `runspec_linux.nc_command.nc_send` is preserved.
42
+
43
+ Why: importing a helper (e.g. `nc_send`) previously required installing
44
+ `runspec-linux`, which put all 21 console scripts on PATH and surfaced all 21
45
+ runnables in `runspec local` / `runspec serve`. Code that only wants the
46
+ importable helpers can now depend on `runspec-linux-core` instead — it has no
47
+ `runspec` dependency, no `runspec.toml`, and no entry points, so it surfaces
48
+ nothing. No behaviour change for the runnables themselves; JSON output and exit
49
+ codes are unchanged.
50
+
51
+ ## [0.1.3] — 2026-06-04
52
+
53
+ Switch `[config.logging]` to `store = "per-run"` so every runnable writes one
54
+ log file per invocation (`{runnable}.{utc-ts}.{run_id}.log`) instead of a single
55
+ shared rotating `{runnable}.log`. These runnables are meant to be installed into
56
+ shared venvs and run by multiple users (and in parallel); per-invocation files
57
+ are multi-writer safe — no in-process rotation and no contention over one file.
58
+ Use `runspec logs` to view, compact, and prune. The `rotate`/`keep` keys (inert
59
+ under per-run) are dropped. Bumps the `runspec` dependency floor to `>=0.27.0`,
60
+ where `store = "per-run"` was introduced.
61
+
62
+ ## [0.1.2] — 2026-06-02
63
+
64
+ Lower the Python floor to 3.10 to match the core `runspec` library (no
65
+ 3.11-only features are used). Bump the `runspec` dependency floor to
66
+ `>=0.23.0`.
67
+
68
+ ## [0.1.1] — 2026-05-28
69
+
70
+ Enable `[config.logging]` for all 21 runnables. Each invocation now writes a
71
+ JSON audit record to `{venv}/logs/{runnable}.log` (rotates at 10 MB, keeps 14
72
+ backups) and emits a one-line run summary to stderr. The auto-added `--debug`
73
+ and `--no-summary` flags are available on every runnable. No code changes —
74
+ existing `print(json.dumps(...))` payloads are captured into the audit log via
75
+ the print-capture mechanism; stdout stays clean for pipe consumers.
76
+
77
+ ## [0.1.0] — 2026-05-27
78
+
79
+ Initial release.
80
+
81
+ 21 runnables covering Linux system administration:
82
+
83
+ **System monitoring** — `system-info`, `disk-usage`, `check-memory`, `list-processes`
84
+
85
+ **Services** — `check-service`, `list-services`, `restart-service`
86
+
87
+ **Logs** — `tail-log`, `search-log`, `journalctl`
88
+
89
+ **Network** — `ping-host`, `check-port`, `show-connections`
90
+
91
+ **Files** — `find-large-files`, `backup-files`
92
+
93
+ **Security** — `last-logins`, `who`
94
+
95
+ **Containers** — `list-containers`, `container-logs`, `restart-container`
96
+
97
+ **TCP interfaces** — `nc-command` (also exports `nc_send()` as a public Python API for wrapper runnables)
98
+
99
+ All read-only runnables are `autonomy = "autonomous"`. State-changing runnables (`restart-service`, `backup-files`, `restart-container`) are `autonomy = "confirm"`. Missing tools (`docker`, `systemctl`, `journalctl`) return a structured JSON error rather than a stack trace.
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runspec-linux
3
- Version: 0.1.3
3
+ Version: 0.3.0
4
4
  Summary: Linux system admin runnables for runspec
5
5
  Requires-Python: >=3.10
6
+ Requires-Dist: runspec-linux-core>=0.2.0
6
7
  Requires-Dist: runspec>=0.27.0
7
8
  Provides-Extra: dev
8
9
  Requires-Dist: mypy; extra == 'dev'
@@ -4,11 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-linux"
7
- version = "0.1.3"
7
+ version = "0.3.0"
8
8
  requires-python = ">=3.10"
9
9
  description = "Linux system admin runnables for runspec"
10
10
  dependencies = [
11
11
  "runspec>=0.27.0",
12
+ "runspec-linux-core>=0.2.0",
12
13
  ]
13
14
 
14
15
  [project.scripts]
@@ -32,6 +33,12 @@ who = "runspec_linux.security:main_who"
32
33
  list-containers = "runspec_linux.containers:main_list_containers"
33
34
  container-logs = "runspec_linux.containers:main_container_logs"
34
35
  restart-container = "runspec_linux.containers:main_restart_container"
36
+ list-packages = "runspec_linux.packages:main_list_packages"
37
+ list-upgrades = "runspec_linux.packages:main_list_upgrades"
38
+ upgrade-packages = "runspec_linux.packages:main_upgrade_packages"
39
+ install-package = "runspec_linux.packages:main_install_package"
40
+ remove-package = "runspec_linux.packages:main_remove_package"
41
+ reboot-host = "runspec_linux.power:main_reboot_host"
35
42
  nc-command = "runspec_linux.nc_command:main"
36
43
 
37
44
  [project.optional-dependencies]
@@ -0,0 +1,70 @@
1
+ """runspec-linux — Linux system-admin runnables.
2
+
3
+ The pure logic lives in the dependency-free ``runspec-linux-core`` package; the
4
+ modules here wrap each helper in a runspec runnable. The helper functions are
5
+ re-exported for convenience and back-compat (e.g. ``from runspec_linux import
6
+ nc_send``). To import the helpers *without* surfacing any runnables in a venv,
7
+ depend on ``runspec-linux-core`` directly instead.
8
+ """
9
+
10
+ from runspec_linux_core import (
11
+ backup_files,
12
+ check_memory,
13
+ check_port,
14
+ check_service,
15
+ container_logs,
16
+ detect_manager,
17
+ disk_usage,
18
+ find_large_files,
19
+ install_package,
20
+ journalctl,
21
+ last_logins,
22
+ list_containers,
23
+ list_packages,
24
+ list_processes,
25
+ list_services,
26
+ list_upgrades,
27
+ nc_send,
28
+ ping_host,
29
+ reboot_host,
30
+ remove_package,
31
+ restart_container,
32
+ restart_service,
33
+ search_log,
34
+ show_connections,
35
+ system_info,
36
+ tail_log,
37
+ upgrade_packages,
38
+ who,
39
+ )
40
+
41
+ __all__ = [
42
+ "backup_files",
43
+ "check_memory",
44
+ "check_port",
45
+ "check_service",
46
+ "container_logs",
47
+ "detect_manager",
48
+ "disk_usage",
49
+ "find_large_files",
50
+ "install_package",
51
+ "journalctl",
52
+ "last_logins",
53
+ "list_containers",
54
+ "list_packages",
55
+ "list_processes",
56
+ "list_services",
57
+ "list_upgrades",
58
+ "nc_send",
59
+ "ping_host",
60
+ "reboot_host",
61
+ "remove_package",
62
+ "restart_container",
63
+ "restart_service",
64
+ "search_log",
65
+ "show_connections",
66
+ "system_info",
67
+ "tail_log",
68
+ "upgrade_packages",
69
+ "who",
70
+ ]
@@ -0,0 +1,42 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import container_logs, list_containers, restart_container
6
+ from runspec_linux_core.errors import CommandError
7
+
8
+
9
+ def main_list_containers() -> None:
10
+ spec = rs.parse("list-containers")
11
+ include_all = spec.all.value
12
+ try:
13
+ print(json.dumps(list_containers(include_all)))
14
+ except Exception as e:
15
+ # Includes ToolNotFoundError (docker absent) and CommandError (docker ps failed).
16
+ print(json.dumps({"error": str(e)}))
17
+ sys.exit(1)
18
+
19
+
20
+ def main_container_logs() -> None:
21
+ spec = rs.parse("container-logs")
22
+ container = spec.container.value
23
+ lines = spec.lines.value
24
+ try:
25
+ print(json.dumps(container_logs(container, lines)))
26
+ except Exception as e:
27
+ print(json.dumps({"error": str(e)}))
28
+ sys.exit(1)
29
+
30
+
31
+ def main_restart_container() -> None:
32
+ spec = rs.parse("restart-container")
33
+ container = spec.container.value
34
+ try:
35
+ print(json.dumps(restart_container(container)))
36
+ except CommandError as e:
37
+ print(json.dumps({"container": container, "restarted": False, "error": str(e)}))
38
+ sys.exit(1)
39
+ except Exception as e:
40
+ # Includes ToolNotFoundError (docker absent).
41
+ print(json.dumps({"error": str(e)}))
42
+ sys.exit(1)
@@ -0,0 +1,28 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import backup_files, find_large_files
6
+
7
+
8
+ def main_find_large_files() -> None:
9
+ spec = rs.parse("find-large-files")
10
+ search_path = str(spec.path.value)
11
+ min_mb = spec.min_mb.value
12
+ limit = spec.limit.value
13
+ try:
14
+ print(json.dumps(find_large_files(search_path, min_mb, limit)))
15
+ except Exception as e:
16
+ print(json.dumps({"error": str(e)}))
17
+ sys.exit(1)
18
+
19
+
20
+ def main_backup_files() -> None:
21
+ spec = rs.parse("backup-files")
22
+ source = str(spec.source.value)
23
+ destination = str(spec.destination.value)
24
+ try:
25
+ print(json.dumps(backup_files(source, destination)))
26
+ except Exception as e:
27
+ print(json.dumps({"error": str(e)}))
28
+ sys.exit(1)
@@ -0,0 +1,52 @@
1
+ import json
2
+ import re
3
+ import subprocess
4
+ import sys
5
+
6
+ import runspec as rs
7
+ from runspec_linux_core import journalctl, search_log, tail_log
8
+ from runspec_linux_core.errors import ToolNotFoundError
9
+
10
+
11
+ def main_tail_log() -> None:
12
+ spec = rs.parse("tail-log")
13
+ file_path = str(spec.file.value)
14
+ lines = spec.lines.value
15
+ try:
16
+ print(json.dumps(tail_log(file_path, lines)))
17
+ except OSError as e:
18
+ print(json.dumps({"error": str(e), "file": file_path}))
19
+ sys.exit(1)
20
+
21
+
22
+ def main_search_log() -> None:
23
+ spec = rs.parse("search-log")
24
+ file_path = str(spec.file.value)
25
+ pattern = spec.pattern.value
26
+ limit = spec.limit.value
27
+ try:
28
+ print(json.dumps(search_log(file_path, pattern, limit)))
29
+ except OSError as e:
30
+ print(json.dumps({"error": str(e), "file": file_path}))
31
+ sys.exit(1)
32
+ except re.error as e:
33
+ print(json.dumps({"error": f"Invalid regex: {e}", "pattern": pattern}))
34
+ sys.exit(1)
35
+
36
+
37
+ def main_journalctl() -> None:
38
+ spec = rs.parse("journalctl")
39
+ unit = spec.unit.value
40
+ lines = spec.lines.value
41
+ since = spec.since.value
42
+ try:
43
+ print(json.dumps(journalctl(unit, lines, since)))
44
+ except ToolNotFoundError as e:
45
+ print(json.dumps({"error": str(e)}))
46
+ sys.exit(1)
47
+ except subprocess.CalledProcessError as e:
48
+ print(json.dumps({"error": e.stderr.strip(), "unit": unit}))
49
+ sys.exit(1)
50
+ except Exception as e:
51
+ print(json.dumps({"error": str(e)}))
52
+ sys.exit(1)
@@ -0,0 +1,38 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+
6
+ # Re-exported for back-compat: callers historically did
7
+ # `from runspec_linux.nc_command import nc_send`. The implementation now lives
8
+ # in the dependency-free runspec-linux-core package.
9
+ from runspec_linux_core import nc_send
10
+
11
+ __all__ = ["main", "nc_send"]
12
+
13
+
14
+ def main() -> None:
15
+ spec = rs.parse("nc-command")
16
+ host = spec.host.value
17
+ port = spec.port.value
18
+ command = spec.command.value
19
+ wait = spec.wait.value
20
+ read_timeout = spec.read_timeout.value
21
+
22
+ try:
23
+ response = nc_send(host, port, command, wait=wait, read_timeout=read_timeout)
24
+ lines = response.splitlines()
25
+ print(
26
+ json.dumps(
27
+ {
28
+ "host": host,
29
+ "port": port,
30
+ "command": command,
31
+ "stdout": response,
32
+ "lines": lines,
33
+ }
34
+ )
35
+ )
36
+ except OSError as e:
37
+ print(json.dumps({"error": str(e), "host": host, "port": port}))
38
+ sys.exit(1)
@@ -0,0 +1,34 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import check_port, ping_host, show_connections
6
+
7
+
8
+ def main_ping_host() -> None:
9
+ spec = rs.parse("ping-host")
10
+ host = spec.host.value
11
+ count = spec.count.value
12
+ try:
13
+ print(json.dumps(ping_host(host, count)))
14
+ except Exception as e:
15
+ print(json.dumps({"error": str(e), "host": host}))
16
+ sys.exit(1)
17
+
18
+
19
+ def main_check_port() -> None:
20
+ spec = rs.parse("check-port")
21
+ host = spec.host.value
22
+ port = spec.port.value
23
+ timeout = spec.timeout.value
24
+ print(json.dumps(check_port(host, port, timeout)))
25
+
26
+
27
+ def main_show_connections() -> None:
28
+ spec = rs.parse("show-connections")
29
+ state_filter = spec.state.value
30
+ try:
31
+ print(json.dumps(show_connections(state_filter)))
32
+ except Exception as e:
33
+ print(json.dumps({"error": str(e)}))
34
+ sys.exit(1)
@@ -0,0 +1,74 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import (
6
+ install_package,
7
+ list_packages,
8
+ list_upgrades,
9
+ remove_package,
10
+ upgrade_packages,
11
+ )
12
+ from runspec_linux_core.errors import CommandError
13
+
14
+
15
+ def main_list_packages() -> None:
16
+ spec = rs.parse("list-packages")
17
+ name_filter = spec.filter.value
18
+ limit = spec.limit.value
19
+ try:
20
+ print(json.dumps(list_packages(name_filter, limit)))
21
+ except Exception as e:
22
+ # Includes ToolNotFoundError (no package manager) and CommandError.
23
+ print(json.dumps({"error": str(e)}))
24
+ sys.exit(1)
25
+
26
+
27
+ def main_list_upgrades() -> None:
28
+ spec = rs.parse("list-upgrades")
29
+ try:
30
+ print(json.dumps(list_upgrades(refresh=not spec.no_refresh.value)))
31
+ except Exception as e:
32
+ print(json.dumps({"error": str(e)}))
33
+ sys.exit(1)
34
+
35
+
36
+ def main_upgrade_packages() -> None:
37
+ rs.parse("upgrade-packages")
38
+ try:
39
+ print(json.dumps(upgrade_packages()))
40
+ except CommandError as e:
41
+ print(json.dumps({"upgraded": False, "error": str(e)}))
42
+ sys.exit(1)
43
+ except Exception as e:
44
+ # Includes ToolNotFoundError (no package manager).
45
+ print(json.dumps({"error": str(e)}))
46
+ sys.exit(1)
47
+
48
+
49
+ def main_install_package() -> None:
50
+ spec = rs.parse("install-package")
51
+ package = spec.package.value
52
+ try:
53
+ print(json.dumps(install_package(package)))
54
+ except CommandError as e:
55
+ print(json.dumps({"package": package, "installed": False, "error": str(e)}))
56
+ sys.exit(1)
57
+ except Exception as e:
58
+ # Includes ValueError (bad name) and ToolNotFoundError (no package manager).
59
+ print(json.dumps({"error": str(e)}))
60
+ sys.exit(1)
61
+
62
+
63
+ def main_remove_package() -> None:
64
+ spec = rs.parse("remove-package")
65
+ package = spec.package.value
66
+ try:
67
+ print(json.dumps(remove_package(package)))
68
+ except CommandError as e:
69
+ print(json.dumps({"package": package, "removed": False, "error": str(e)}))
70
+ sys.exit(1)
71
+ except Exception as e:
72
+ # Includes ValueError (bad name) and ToolNotFoundError (no package manager).
73
+ print(json.dumps({"error": str(e)}))
74
+ sys.exit(1)
@@ -0,0 +1,20 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import reboot_host
6
+ from runspec_linux_core.errors import CommandError
7
+
8
+
9
+ def main_reboot_host() -> None:
10
+ spec = rs.parse("reboot-host")
11
+ delay_minutes = spec.delay_minutes.value
12
+ try:
13
+ print(json.dumps(reboot_host(delay_minutes)))
14
+ except CommandError as e:
15
+ print(json.dumps({"scheduled": False, "error": str(e)}))
16
+ sys.exit(1)
17
+ except Exception as e:
18
+ # Includes ToolNotFoundError (no shutdown/systemctl).
19
+ print(json.dumps({"error": str(e)}))
20
+ sys.exit(1)
@@ -299,6 +299,79 @@ type = "str"
299
299
  short = "-c"
300
300
  description = "Container name or ID"
301
301
 
302
+ # ── Packages ──────────────────────────────────────────────────────────────────
303
+
304
+ [list-packages]
305
+ serve = ["local"]
306
+ description = "List installed packages (apt/dnf/yum/zypper/pacman), optionally filtered by name"
307
+ autonomy = "autonomous"
308
+ output = "json"
309
+
310
+ [list-packages.args.filter]
311
+ type = "str"
312
+ short = "-f"
313
+ description = "Only list packages whose name contains this string"
314
+ required = false
315
+
316
+ [list-packages.args.limit]
317
+ type = "int"
318
+ short = "-n"
319
+ description = "Maximum number of packages to return (sorted by name)"
320
+ default = 100
321
+
322
+ [list-upgrades]
323
+ serve = ["local"]
324
+ description = "List packages with an available upgrade (refreshes the package index first)"
325
+ autonomy = "autonomous"
326
+ output = "json"
327
+
328
+ [list-upgrades.args.no-refresh]
329
+ type = "flag"
330
+ description = "Read the cached package index instead of refreshing it first (no root needed)"
331
+ default = false
332
+
333
+ [upgrade-packages]
334
+ serve = ["local"]
335
+ description = "Refresh the package index and upgrade all installed packages (needs root)"
336
+ autonomy = "confirm"
337
+ output = "json"
338
+
339
+ [install-package]
340
+ serve = ["local"]
341
+ description = "Install one or more packages by name (needs root)"
342
+ autonomy = "confirm"
343
+ output = "json"
344
+
345
+ [install-package.args.package]
346
+ type = "str"
347
+ short = "-p"
348
+ description = "Package name to install (space-separated for several)"
349
+
350
+ [remove-package]
351
+ serve = ["local"]
352
+ description = "Remove one or more packages by name (needs root)"
353
+ autonomy = "confirm"
354
+ output = "json"
355
+
356
+ [remove-package.args.package]
357
+ type = "str"
358
+ short = "-p"
359
+ description = "Package name to remove (space-separated for several)"
360
+
361
+ # ── Power ─────────────────────────────────────────────────────────────────────
362
+
363
+ [reboot-host]
364
+ serve = ["local"]
365
+ description = "Reboot the host, optionally after a delay in minutes (needs root)"
366
+ autonomy = "confirm"
367
+ output = "json"
368
+
369
+ [reboot-host.args.delay-minutes]
370
+ type = "int"
371
+ short = "-d"
372
+ description = "Minutes to wait before rebooting (0 = immediately)"
373
+ default = 0
374
+
302
375
  # ── TCP interfaces (nc-command) ────────────────────────────────────────────────
303
376
 
304
377
  [nc-command]
@@ -0,0 +1,24 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import last_logins, who
6
+
7
+
8
+ def main_last_logins() -> None:
9
+ spec = rs.parse("last-logins")
10
+ limit = spec.limit.value
11
+ try:
12
+ print(json.dumps(last_logins(limit)))
13
+ except Exception as e:
14
+ print(json.dumps({"error": str(e)}))
15
+ sys.exit(1)
16
+
17
+
18
+ def main_who() -> None:
19
+ rs.parse("who")
20
+ try:
21
+ print(json.dumps(who()))
22
+ except Exception as e:
23
+ print(json.dumps({"error": str(e)}))
24
+ sys.exit(1)
@@ -0,0 +1,40 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import check_service, list_services, restart_service
6
+ from runspec_linux_core.errors import CommandError
7
+
8
+
9
+ def main_check_service() -> None:
10
+ spec = rs.parse("check-service")
11
+ service = spec.service.value
12
+ try:
13
+ print(json.dumps(check_service(service)))
14
+ except Exception as e:
15
+ print(json.dumps({"error": str(e)}))
16
+ sys.exit(1)
17
+
18
+
19
+ def main_list_services() -> None:
20
+ spec = rs.parse("list-services")
21
+ state_filter = spec.state.value
22
+ try:
23
+ print(json.dumps(list_services(state_filter)))
24
+ except Exception as e:
25
+ print(json.dumps({"error": str(e)}))
26
+ sys.exit(1)
27
+
28
+
29
+ def main_restart_service() -> None:
30
+ spec = rs.parse("restart-service")
31
+ service = spec.service.value
32
+ try:
33
+ print(json.dumps(restart_service(service)))
34
+ except CommandError as e:
35
+ print(json.dumps({"service": service, "restarted": False, "error": str(e)}))
36
+ sys.exit(1)
37
+ except Exception as e:
38
+ # Includes ToolNotFoundError (systemctl absent).
39
+ print(json.dumps({"error": str(e)}))
40
+ sys.exit(1)