smanager 0.2.2__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 (71) hide show
  1. smanager-0.3.0/LICENSE.txt +21 -0
  2. smanager-0.3.0/PKG-INFO +120 -0
  3. smanager-0.3.0/README.md +105 -0
  4. {smanager-0.2.2 → smanager-0.3.0}/pyproject.toml +6 -0
  5. {smanager-0.2.2 → smanager-0.3.0}/smanager/__init__.py +1 -1
  6. smanager-0.3.0/smanager/cli/commands/__init__.py +8 -0
  7. smanager-0.3.0/smanager/cli/commands/all.py +132 -0
  8. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/commands/config.py +1 -1
  9. smanager-0.3.0/smanager/cli/commands/dependency.py +93 -0
  10. smanager-0.3.0/smanager/cli/commands/group.py +88 -0
  11. smanager-0.3.0/smanager/cli/commands/info.py +160 -0
  12. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/commands/metadata.py +7 -4
  13. smanager-0.3.0/smanager/cli/commands/service.py +88 -0
  14. smanager-0.3.0/smanager/cli/commands/status.py +50 -0
  15. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/config.py +20 -0
  16. smanager-0.3.0/smanager/cli/formatters/__init__.py +10 -0
  17. smanager-0.3.0/smanager/cli/formatters/info_table.py +77 -0
  18. smanager-0.3.0/smanager/cli/formatters/runtime_style.py +89 -0
  19. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/formatters/status_table.py +13 -5
  20. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/main.py +1 -2
  21. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/metadata_wizard.py +13 -12
  22. {smanager-0.2.2 → smanager-0.3.0}/smanager/crawler/sd_finder.py +10 -0
  23. {smanager-0.2.2 → smanager-0.3.0}/smanager/global_manager/global_manager.py +10 -19
  24. {smanager-0.2.2 → smanager-0.3.0}/smanager/service_managers/__init__.py +8 -0
  25. {smanager-0.2.2 → smanager-0.3.0}/smanager/service_managers/docker_manager.py +3 -0
  26. {smanager-0.2.2 → smanager-0.3.0}/smanager/service_managers/local_manager.py +3 -0
  27. smanager-0.3.0/smanager/service_managers/registry.py +36 -0
  28. {smanager-0.2.2 → smanager-0.3.0}/smanager/vpn/base_vpn.py +5 -0
  29. {smanager-0.2.2 → smanager-0.3.0}/smanager/vpn/providers/tailscale.py +24 -1
  30. smanager-0.3.0/smanager.egg-info/PKG-INFO +120 -0
  31. {smanager-0.2.2 → smanager-0.3.0}/smanager.egg-info/SOURCES.txt +11 -1
  32. {smanager-0.2.2 → smanager-0.3.0}/tests/test_events.py +32 -0
  33. smanager-0.3.0/tests/test_info_output.py +237 -0
  34. smanager-0.3.0/tests/test_runtime_style.py +125 -0
  35. smanager-0.3.0/tests/test_sd_finder.py +87 -0
  36. smanager-0.3.0/tests/test_service_manager_registry.py +65 -0
  37. {smanager-0.2.2 → smanager-0.3.0}/tests/test_status_output.py +53 -3
  38. smanager-0.3.0/tests/test_tailscale_vpn.py +62 -0
  39. smanager-0.2.2/PKG-INFO +0 -8
  40. smanager-0.2.2/README.md +0 -60
  41. smanager-0.2.2/smanager/cli/commands/__init__.py +0 -11
  42. smanager-0.2.2/smanager/cli/commands/all.py +0 -38
  43. smanager-0.2.2/smanager/cli/commands/dependency.py +0 -27
  44. smanager-0.2.2/smanager/cli/commands/group.py +0 -27
  45. smanager-0.2.2/smanager/cli/commands/service.py +0 -27
  46. smanager-0.2.2/smanager/cli/commands/status.py +0 -71
  47. smanager-0.2.2/smanager/cli/formatters/__init__.py +0 -3
  48. smanager-0.2.2/smanager.egg-info/PKG-INFO +0 -8
  49. {smanager-0.2.2 → smanager-0.3.0}/setup.cfg +0 -0
  50. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/__init__.py +0 -0
  51. {smanager-0.2.2 → smanager-0.3.0}/smanager/cli/events.py +0 -0
  52. {smanager-0.2.2 → smanager-0.3.0}/smanager/crawler/__init__.py +0 -0
  53. {smanager-0.2.2 → smanager-0.3.0}/smanager/datatypes/__init__.py +0 -0
  54. {smanager-0.2.2 → smanager-0.3.0}/smanager/datatypes/config.py +0 -0
  55. {smanager-0.2.2 → smanager-0.3.0}/smanager/datatypes/sd_info_v2.py +0 -0
  56. {smanager-0.2.2 → smanager-0.3.0}/smanager/datatypes/service_status.py +0 -0
  57. {smanager-0.2.2 → smanager-0.3.0}/smanager/events.py +0 -0
  58. {smanager-0.2.2 → smanager-0.3.0}/smanager/global_manager/__init__.py +0 -0
  59. {smanager-0.2.2 → smanager-0.3.0}/smanager/global_manager/base_global_manager.py +0 -0
  60. {smanager-0.2.2 → smanager-0.3.0}/smanager/service_managers/base_manager.py +0 -0
  61. {smanager-0.2.2 → smanager-0.3.0}/smanager/service_managers/base_sdv2_manager.py +0 -0
  62. {smanager-0.2.2 → smanager-0.3.0}/smanager/vpn/__init__.py +0 -0
  63. {smanager-0.2.2 → smanager-0.3.0}/smanager/vpn/providers/__init__.py +0 -0
  64. {smanager-0.2.2 → smanager-0.3.0}/smanager/vpn/registry.py +0 -0
  65. {smanager-0.2.2 → smanager-0.3.0}/smanager.egg-info/dependency_links.txt +0 -0
  66. {smanager-0.2.2 → smanager-0.3.0}/smanager.egg-info/entry_points.txt +0 -0
  67. {smanager-0.2.2 → smanager-0.3.0}/smanager.egg-info/requires.txt +0 -0
  68. {smanager-0.2.2 → smanager-0.3.0}/smanager.egg-info/top_level.txt +0 -0
  69. {smanager-0.2.2 → smanager-0.3.0}/tests/test_cli_events.py +0 -0
  70. {smanager-0.2.2 → smanager-0.3.0}/tests/test_metadata_wizard.py +0 -0
  71. {smanager-0.2.2 → smanager-0.3.0}/tests/test_service_manager_events.py +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Pérez Rodríguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: smanager
3
+ Version: 0.3.0
4
+ Summary: A simple server manager for self-hosted services
5
+ Author: Daniel Pérez Rodríguez
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE.txt
10
+ Requires-Dist: PyYAML>=6.0
11
+ Requires-Dist: click>=8.0
12
+ Requires-Dist: rich>=13.0
13
+ Requires-Dist: InquirerPy>=0.3.4
14
+ Dynamic: license-file
15
+
16
+ # SManager
17
+ > A simple server manager for self-hosted services, with VPN integration and support for both local and Docker-based services.
18
+
19
+ When building a self-hosted setup, it’s common to end up with dozens of local services, Docker containers, and maintenance scripts scattered across your system. Tasks as simple as restarting or stopping a service can quickly become frustrating when there’s no clear overview of what is running, how components relate to each other, or how everything is managed.
20
+
21
+ SManager provides a simple and unified CLI for managing both local and Docker services, with built-in support for VPN integration and interactive metadata management.
22
+
23
+ Reseting all your music-related services becomes as simple as:
24
+
25
+ ```bash
26
+ smanager group restart music
27
+ ```
28
+
29
+ ## Features
30
+ - Unified service management for local/systemd and Docker runtimes.
31
+ - VPN integration (Tailscale provider support).
32
+ - Interactive metadata creation and editing.
33
+ - YAML-based global configuration.
34
+ - Rich service status reporting with optional JSON output.
35
+
36
+ ## CLI Usage
37
+ Use this command pattern:
38
+
39
+ ```bash
40
+ smanager <group> <command> [options]
41
+ ```
42
+
43
+ Discover available commands at any level:
44
+
45
+ ```bash
46
+ smanager --help
47
+ smanager <group> --help
48
+ smanager <group> <command> --help
49
+ ```
50
+
51
+ ### Status Commands
52
+
53
+ Status is now a subcommand under each resource group:
54
+
55
+ ```bash
56
+ smanager service status <service_id>
57
+ smanager group status <group_name>
58
+ smanager dependency status <dependency_name>
59
+ smanager all status
60
+ ```
61
+
62
+ Optional JSON output is available for each status command:
63
+
64
+ ```bash
65
+ smanager service status <service_id> --json
66
+ ```
67
+
68
+ ### Info Commands
69
+
70
+ Info is available under each resource group and shows service name plus WebUI URL:
71
+
72
+ ```bash
73
+ smanager service info <service_id>
74
+ smanager group info <group_name>
75
+ smanager dependency info <dependency_name>
76
+ smanager all info
77
+ ```
78
+
79
+ Use `--details` to include extra operational columns:
80
+
81
+ ```bash
82
+ smanager service info <service_id> --details
83
+ ```
84
+
85
+ ## Metadata Commands
86
+ The `metadata` group provides interactive workflows for service metadata files.
87
+
88
+ ### Create Metadata
89
+ ```bash
90
+ smanager metadata init
91
+ ```
92
+
93
+ Optional output override:
94
+
95
+ ```bash
96
+ smanager metadata init --output /path/to/sd_info.json
97
+ ```
98
+
99
+ ### Edit Existing Metadata
100
+ ```bash
101
+ smanager metadata edit
102
+ ```
103
+
104
+ Edit a specific file directly:
105
+
106
+ ```bash
107
+ smanager metadata edit --file /path/to/sd_info.json
108
+ ```
109
+
110
+ Behavior summary:
111
+ - Interactive prompts with inline defaults.
112
+ - Runtime-aware sections (Docker vs Local).
113
+ - Multi-select support for `groups` and `depends_on`.
114
+ - Validation before writing changes.
115
+
116
+ ## Notes
117
+ - `metadata init` creates a new file.
118
+ - `metadata edit` modifies an existing file.
119
+ - Use command-specific `--help` for the latest options and examples.
120
+
@@ -0,0 +1,105 @@
1
+ # SManager
2
+ > A simple server manager for self-hosted services, with VPN integration and support for both local and Docker-based services.
3
+
4
+ When building a self-hosted setup, it’s common to end up with dozens of local services, Docker containers, and maintenance scripts scattered across your system. Tasks as simple as restarting or stopping a service can quickly become frustrating when there’s no clear overview of what is running, how components relate to each other, or how everything is managed.
5
+
6
+ SManager provides a simple and unified CLI for managing both local and Docker services, with built-in support for VPN integration and interactive metadata management.
7
+
8
+ Reseting all your music-related services becomes as simple as:
9
+
10
+ ```bash
11
+ smanager group restart music
12
+ ```
13
+
14
+ ## Features
15
+ - Unified service management for local/systemd and Docker runtimes.
16
+ - VPN integration (Tailscale provider support).
17
+ - Interactive metadata creation and editing.
18
+ - YAML-based global configuration.
19
+ - Rich service status reporting with optional JSON output.
20
+
21
+ ## CLI Usage
22
+ Use this command pattern:
23
+
24
+ ```bash
25
+ smanager <group> <command> [options]
26
+ ```
27
+
28
+ Discover available commands at any level:
29
+
30
+ ```bash
31
+ smanager --help
32
+ smanager <group> --help
33
+ smanager <group> <command> --help
34
+ ```
35
+
36
+ ### Status Commands
37
+
38
+ Status is now a subcommand under each resource group:
39
+
40
+ ```bash
41
+ smanager service status <service_id>
42
+ smanager group status <group_name>
43
+ smanager dependency status <dependency_name>
44
+ smanager all status
45
+ ```
46
+
47
+ Optional JSON output is available for each status command:
48
+
49
+ ```bash
50
+ smanager service status <service_id> --json
51
+ ```
52
+
53
+ ### Info Commands
54
+
55
+ Info is available under each resource group and shows service name plus WebUI URL:
56
+
57
+ ```bash
58
+ smanager service info <service_id>
59
+ smanager group info <group_name>
60
+ smanager dependency info <dependency_name>
61
+ smanager all info
62
+ ```
63
+
64
+ Use `--details` to include extra operational columns:
65
+
66
+ ```bash
67
+ smanager service info <service_id> --details
68
+ ```
69
+
70
+ ## Metadata Commands
71
+ The `metadata` group provides interactive workflows for service metadata files.
72
+
73
+ ### Create Metadata
74
+ ```bash
75
+ smanager metadata init
76
+ ```
77
+
78
+ Optional output override:
79
+
80
+ ```bash
81
+ smanager metadata init --output /path/to/sd_info.json
82
+ ```
83
+
84
+ ### Edit Existing Metadata
85
+ ```bash
86
+ smanager metadata edit
87
+ ```
88
+
89
+ Edit a specific file directly:
90
+
91
+ ```bash
92
+ smanager metadata edit --file /path/to/sd_info.json
93
+ ```
94
+
95
+ Behavior summary:
96
+ - Interactive prompts with inline defaults.
97
+ - Runtime-aware sections (Docker vs Local).
98
+ - Multi-select support for `groups` and `depends_on`.
99
+ - Validation before writing changes.
100
+
101
+ ## Notes
102
+ - `metadata init` creates a new file.
103
+ - `metadata edit` modifies an existing file.
104
+ - Use command-specific `--help` for the latest options and examples.
105
+
@@ -5,6 +5,12 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "smanager"
7
7
  dynamic = ["version"]
8
+ authors = [
9
+ {name = "Daniel Pérez Rodríguez"}
10
+ ]
11
+ description = "A simple server manager for self-hosted services"
12
+ readme = "README.md"
13
+ license = "MIT"
8
14
  requires-python = ">=3.9"
9
15
  dependencies = [
10
16
  "PyYAML>=6.0",
@@ -1,4 +1,4 @@
1
- __version__ = "0.2.2"
1
+ __version__ = "0.3.0"
2
2
 
3
3
  from .global_manager import GlobalManager
4
4
  from .datatypes import GlobalConfig, Service, ServiceStatus, MultiStatus
@@ -0,0 +1,8 @@
1
+ from .service import service_cmd
2
+ from .group import group_cmd
3
+ from .dependency import dependency_cmd
4
+ from .all import all_cmd
5
+ from .config import config_cmd
6
+ from .metadata import metadata_cmd
7
+
8
+ __all__ = ["service_cmd", "group_cmd", "dependency_cmd", "all_cmd", "config_cmd", "metadata_cmd"]
@@ -0,0 +1,132 @@
1
+ import click
2
+
3
+ from ..config import get_manager
4
+ from ..events import attach_cli_event_renderer
5
+ from .info import emit_all_info
6
+ from .status import emit_all_status
7
+
8
+
9
+ @click.group(name="all")
10
+ @click.pass_context
11
+ def all_cmd(ctx: click.Context) -> None:
12
+ """Manage all services.
13
+
14
+ \b
15
+ Actions: start, stop, restart, enable, disable, status, info
16
+
17
+ Management actions require --yes to prevent accidental execution.
18
+ """
19
+
20
+
21
+ def _run_all_action(ctx: click.Context, action: str, yes: bool) -> None:
22
+ if not yes:
23
+ raise click.UsageError(
24
+ f"This will run '{action}' on ALL services. Pass --yes to confirm."
25
+ )
26
+ manager = get_manager(ctx)
27
+ detach_renderer = attach_cli_event_renderer(manager, command_scope="all")
28
+ try:
29
+ manager.manage_all(action)
30
+ except ValueError as e:
31
+ raise click.ClickException(str(e))
32
+ except RuntimeError as e:
33
+ raise click.ClickException(str(e))
34
+ finally:
35
+ detach_renderer()
36
+
37
+
38
+ @all_cmd.command(name="start")
39
+ @click.option(
40
+ "--yes",
41
+ is_flag=True,
42
+ default=False,
43
+ help="Confirm the operation on all services.",
44
+ )
45
+ @click.pass_context
46
+ def all_start(ctx: click.Context, yes: bool) -> None:
47
+ """Start all managed services.
48
+
49
+ Requires --yes to prevent accidental execution.
50
+ """
51
+ _run_all_action(ctx, "start", yes)
52
+
53
+
54
+ @all_cmd.command(name="stop")
55
+ @click.option(
56
+ "--yes",
57
+ is_flag=True,
58
+ default=False,
59
+ help="Confirm the operation on all services.",
60
+ )
61
+ @click.pass_context
62
+ def all_stop(ctx: click.Context, yes: bool) -> None:
63
+ """Stop all managed services.
64
+
65
+ Requires --yes to prevent accidental execution.
66
+ """
67
+ _run_all_action(ctx, "stop", yes)
68
+
69
+
70
+ @all_cmd.command(name="restart")
71
+ @click.option(
72
+ "--yes",
73
+ is_flag=True,
74
+ default=False,
75
+ help="Confirm the operation on all services.",
76
+ )
77
+ @click.pass_context
78
+ def all_restart(ctx: click.Context, yes: bool) -> None:
79
+ """Restart all managed services.
80
+
81
+ Requires --yes to prevent accidental execution.
82
+ """
83
+ _run_all_action(ctx, "restart", yes)
84
+
85
+
86
+ @all_cmd.command(name="enable")
87
+ @click.option(
88
+ "--yes",
89
+ is_flag=True,
90
+ default=False,
91
+ help="Confirm the operation on all services.",
92
+ )
93
+ @click.pass_context
94
+ def all_enable(ctx: click.Context, yes: bool) -> None:
95
+ """Enable all managed services.
96
+
97
+ Requires --yes to prevent accidental execution.
98
+ """
99
+ _run_all_action(ctx, "enable", yes)
100
+
101
+
102
+ @all_cmd.command(name="disable")
103
+ @click.option(
104
+ "--yes",
105
+ is_flag=True,
106
+ default=False,
107
+ help="Confirm the operation on all services.",
108
+ )
109
+ @click.pass_context
110
+ def all_disable(ctx: click.Context, yes: bool) -> None:
111
+ """Disable all managed services.
112
+
113
+ Requires --yes to prevent accidental execution.
114
+ """
115
+ _run_all_action(ctx, "disable", yes)
116
+
117
+
118
+ @all_cmd.command(name="status")
119
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
120
+ @click.pass_context
121
+ def all_status(ctx: click.Context, as_json: bool) -> None:
122
+ """Show status for all managed services."""
123
+ emit_all_status(ctx, as_json)
124
+
125
+
126
+ @all_cmd.command(name="info")
127
+ @click.option("--details", is_flag=True, default=False, help="Include extra metadata columns.")
128
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
129
+ @click.pass_context
130
+ def all_info(ctx: click.Context, details: bool, as_json: bool) -> None:
131
+ """Show info for all managed services."""
132
+ emit_all_info(ctx, as_json, details)
@@ -13,7 +13,7 @@ global:
13
13
  vpn_provider: "tailscale"
14
14
 
15
15
  # Filename for service metadata files. Each service should have a metadata file with this name in its directory.
16
- metadata_filename: "sd_info.yaml"
16
+ metadata_filename: "sd_info.json"
17
17
  """
18
18
 
19
19
 
@@ -0,0 +1,93 @@
1
+ import click
2
+
3
+ from ..config import get_manager
4
+ from ..events import attach_cli_event_renderer
5
+ from .info import emit_dependency_info
6
+ from .status import emit_dependency_status
7
+
8
+
9
+ @click.group(name="dependency")
10
+ @click.pass_context
11
+ def dependency_cmd(ctx: click.Context) -> None:
12
+ """Manage services by dependency.
13
+
14
+ \b
15
+ Actions: start, stop, restart, enable, disable, status, info
16
+ """
17
+
18
+
19
+ def _run_dependency_action(ctx: click.Context, action: str, dependency_name: str) -> None:
20
+ manager = get_manager(ctx)
21
+ detach_renderer = attach_cli_event_renderer(manager, command_scope="dependency")
22
+ try:
23
+ manager.manage_by_dependency(dependency_name, action)
24
+ except ValueError as e:
25
+ raise click.ClickException(str(e))
26
+ except RuntimeError as e:
27
+ raise click.ClickException(str(e))
28
+ finally:
29
+ detach_renderer()
30
+
31
+
32
+ @dependency_cmd.command(name="start")
33
+ @click.argument("dependency_name")
34
+ @click.pass_context
35
+ def dependency_start(ctx: click.Context, dependency_name: str) -> None:
36
+ """Start services that depend on DEPENDENCY_NAME."""
37
+ _run_dependency_action(ctx, "start", dependency_name)
38
+
39
+
40
+ @dependency_cmd.command(name="stop")
41
+ @click.argument("dependency_name")
42
+ @click.pass_context
43
+ def dependency_stop(ctx: click.Context, dependency_name: str) -> None:
44
+ """Stop services that depend on DEPENDENCY_NAME."""
45
+ _run_dependency_action(ctx, "stop", dependency_name)
46
+
47
+
48
+ @dependency_cmd.command(name="restart")
49
+ @click.argument("dependency_name")
50
+ @click.pass_context
51
+ def dependency_restart(ctx: click.Context, dependency_name: str) -> None:
52
+ """Restart services that depend on DEPENDENCY_NAME."""
53
+ _run_dependency_action(ctx, "restart", dependency_name)
54
+
55
+
56
+ @dependency_cmd.command(name="enable")
57
+ @click.argument("dependency_name")
58
+ @click.pass_context
59
+ def dependency_enable(ctx: click.Context, dependency_name: str) -> None:
60
+ """Enable services that depend on DEPENDENCY_NAME."""
61
+ _run_dependency_action(ctx, "enable", dependency_name)
62
+
63
+
64
+ @dependency_cmd.command(name="disable")
65
+ @click.argument("dependency_name")
66
+ @click.pass_context
67
+ def dependency_disable(ctx: click.Context, dependency_name: str) -> None:
68
+ """Disable services that depend on DEPENDENCY_NAME."""
69
+ _run_dependency_action(ctx, "disable", dependency_name)
70
+
71
+
72
+ @dependency_cmd.command(name="status")
73
+ @click.argument("dependency_name")
74
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
75
+ @click.pass_context
76
+ def dependency_status(ctx: click.Context, dependency_name: str, as_json: bool) -> None:
77
+ """Show status for services by dependency."""
78
+ emit_dependency_status(ctx, dependency_name, as_json)
79
+
80
+
81
+ @dependency_cmd.command(name="info")
82
+ @click.argument("dependency_name")
83
+ @click.option("--details", is_flag=True, default=False, help="Include extra metadata columns.")
84
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
85
+ @click.pass_context
86
+ def dependency_info(
87
+ ctx: click.Context,
88
+ dependency_name: str,
89
+ details: bool,
90
+ as_json: bool,
91
+ ) -> None:
92
+ """Show info for services by dependency."""
93
+ emit_dependency_info(ctx, dependency_name, as_json, details)
@@ -0,0 +1,88 @@
1
+ import click
2
+
3
+ from ..config import get_manager
4
+ from ..events import attach_cli_event_renderer
5
+ from .info import emit_group_info
6
+ from .status import emit_group_status
7
+
8
+
9
+ @click.group(name="group")
10
+ @click.pass_context
11
+ def group_cmd(ctx: click.Context) -> None:
12
+ """Manage all services in a group.
13
+
14
+ \b
15
+ Actions: start, stop, restart, enable, disable, status, info
16
+ """
17
+
18
+
19
+ def _run_group_action(ctx: click.Context, action: str, group_name: str) -> None:
20
+ manager = get_manager(ctx)
21
+ detach_renderer = attach_cli_event_renderer(manager, command_scope="group")
22
+ try:
23
+ manager.manage_group(group_name, action)
24
+ except ValueError as e:
25
+ raise click.ClickException(str(e))
26
+ except RuntimeError as e:
27
+ raise click.ClickException(str(e))
28
+ finally:
29
+ detach_renderer()
30
+
31
+
32
+ @group_cmd.command(name="start")
33
+ @click.argument("group_name")
34
+ @click.pass_context
35
+ def group_start(ctx: click.Context, group_name: str) -> None:
36
+ """Start all services in GROUP_NAME."""
37
+ _run_group_action(ctx, "start", group_name)
38
+
39
+
40
+ @group_cmd.command(name="stop")
41
+ @click.argument("group_name")
42
+ @click.pass_context
43
+ def group_stop(ctx: click.Context, group_name: str) -> None:
44
+ """Stop all services in GROUP_NAME."""
45
+ _run_group_action(ctx, "stop", group_name)
46
+
47
+
48
+ @group_cmd.command(name="restart")
49
+ @click.argument("group_name")
50
+ @click.pass_context
51
+ def group_restart(ctx: click.Context, group_name: str) -> None:
52
+ """Restart all services in GROUP_NAME."""
53
+ _run_group_action(ctx, "restart", group_name)
54
+
55
+
56
+ @group_cmd.command(name="enable")
57
+ @click.argument("group_name")
58
+ @click.pass_context
59
+ def group_enable(ctx: click.Context, group_name: str) -> None:
60
+ """Enable all services in GROUP_NAME."""
61
+ _run_group_action(ctx, "enable", group_name)
62
+
63
+
64
+ @group_cmd.command(name="disable")
65
+ @click.argument("group_name")
66
+ @click.pass_context
67
+ def group_disable(ctx: click.Context, group_name: str) -> None:
68
+ """Disable all services in GROUP_NAME."""
69
+ _run_group_action(ctx, "disable", group_name)
70
+
71
+
72
+ @group_cmd.command(name="status")
73
+ @click.argument("group_name")
74
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
75
+ @click.pass_context
76
+ def group_status(ctx: click.Context, group_name: str, as_json: bool) -> None:
77
+ """Show status for all services in GROUP_NAME."""
78
+ emit_group_status(ctx, group_name, as_json)
79
+
80
+
81
+ @group_cmd.command(name="info")
82
+ @click.argument("group_name")
83
+ @click.option("--details", is_flag=True, default=False, help="Include extra metadata columns.")
84
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
85
+ @click.pass_context
86
+ def group_info(ctx: click.Context, group_name: str, details: bool, as_json: bool) -> None:
87
+ """Show info for all services in GROUP_NAME."""
88
+ emit_group_info(ctx, group_name, as_json, details)