wraith-cli 1.3.0__tar.gz → 1.5.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 (45) hide show
  1. wraith_cli-1.5.0/.pre-commit-config.yaml +82 -0
  2. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/CHANGELOG.md +4 -0
  3. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/PKG-INFO +3 -30
  4. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/README.md +0 -29
  5. wraith_cli-1.5.0/docs/usage.md +245 -0
  6. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/mkdocs.yml +0 -1
  7. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/pyproject.toml +3 -1
  8. wraith_cli-1.5.0/src/wraith_cli/assets.py +10 -0
  9. wraith_cli-1.5.0/src/wraith_cli/config.py +134 -0
  10. wraith_cli-1.5.0/src/wraith_cli/main.py +524 -0
  11. wraith_cli-1.5.0/src/wraith_cli/providers.py +74 -0
  12. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/src/wraith_cli/qol.py +10 -4
  13. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/src/wraith_cli/shield.py +48 -47
  14. wraith_cli-1.5.0/tests/conftest.py +39 -0
  15. wraith_cli-1.5.0/tests/test_cli.py +484 -0
  16. wraith_cli-1.5.0/tests/test_config.py +117 -0
  17. wraith_cli-1.5.0/tests/test_providers.py +126 -0
  18. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/tests/test_repo_make.py +8 -2
  19. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/tests/test_shield.py +134 -42
  20. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/uv.lock +135 -1
  21. wraith_cli-1.3.0/.pre-commit-config.yaml +0 -46
  22. wraith_cli-1.3.0/docs/usage.md +0 -80
  23. wraith_cli-1.3.0/src/wraith_cli/main.py +0 -339
  24. wraith_cli-1.3.0/tests/test_cli.py +0 -301
  25. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.cz.yaml +0 -0
  26. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.env.example +0 -0
  27. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.gitea/CODEOWNERS.md +0 -0
  28. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.gitea/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.gitea/workflows/pages.yml +0 -0
  30. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.gitea/workflows/release.yml +0 -0
  31. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.gitea/workflows/test.yml +0 -0
  32. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.gitignore +0 -0
  33. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/.secrets.baseline +0 -0
  34. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/CONTRIBUTING.md +0 -0
  35. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/LICENSE +0 -0
  36. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/bin/build.sh +0 -0
  37. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/bin/run_tests.sh +0 -0
  38. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/bin/setup_venv.sh +0 -0
  39. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/docs/architecture.md +0 -0
  40. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/docs/index.md +0 -0
  41. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/docs/reference.md +0 -0
  42. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/docs/security.md +0 -0
  43. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/src/wraith_cli/__init__.py +0 -0
  44. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/src/wraith_cli/repo_make.py +0 -0
  45. {wraith_cli-1.3.0 → wraith_cli-1.5.0}/tests/test_qol.py +0 -0
@@ -0,0 +1,82 @@
1
+ minimum_pre_commit_version: 4.0.0
2
+ default_stages: [pre-commit, pre-push]
3
+
4
+ repos:
5
+ - repo: https://github.com/pre-commit/pre-commit-hooks
6
+ rev: v6.0.0
7
+ hooks:
8
+ - id: trailing-whitespace
9
+ args: [--markdown-linebreak-ext=md]
10
+ exclude: ^src/wraith_cli/assets\.py$
11
+ - id: end-of-file-fixer
12
+ - id: check-yaml
13
+ - id: check-toml
14
+ - id: check-added-large-files
15
+ - id: check-case-conflict
16
+
17
+ - repo: https://github.com/astral-sh/ruff-pre-commit
18
+ rev: v0.15.9
19
+ hooks:
20
+ - id: ruff
21
+ args: ["--fix", "--exit-non-zero-on-fix"]
22
+ - id: ruff-format
23
+
24
+ - repo: https://github.com/codespell-project/codespell
25
+ rev: v2.4.2
26
+ hooks:
27
+ - id: codespell
28
+ types_or: [python, markdown, rst]
29
+
30
+ - repo: https://github.com/asottile/pyupgrade
31
+ rev: v3.21.2
32
+ hooks:
33
+ - id: pyupgrade
34
+ args: [--py312-plus]
35
+
36
+ - repo: https://github.com/shellcheck-py/shellcheck-py
37
+ rev: v0.11.0.1
38
+ hooks:
39
+ - id: shellcheck
40
+ args: ["--severity=warning"]
41
+
42
+ - repo: https://github.com/Yelp/detect-secrets
43
+ rev: v1.5.0
44
+ hooks:
45
+ - id: detect-secrets
46
+ args: ["--baseline", ".secrets.baseline"]
47
+ exclude: package-lock.json
48
+
49
+ - repo: local
50
+ hooks:
51
+ - id: generate-docs
52
+ name: Auto-generate CLI Documentation
53
+ entry: uv run typer src/wraith_cli/main.py utils docs --output docs/usage.md
54
+ language: system
55
+ pass_filenames: false
56
+ always_run: true
57
+
58
+ - id: no-raw-print
59
+ name: Ban raw print() in favor of typer.echo/rich
60
+ language: pygrep
61
+ entry: '(?<![\.a-zA-Z0-9_])print\('
62
+ types: [python]
63
+
64
+ - id: ty-check
65
+ name: ty check
66
+ entry: uv run ty check
67
+ language: system
68
+ types: [python]
69
+ pass_filenames: false
70
+
71
+ - id: pytest-coverage
72
+ name: pytest-coverage
73
+ entry: ./bin/run_tests.sh
74
+ language: system
75
+ pass_filenames: false
76
+ always_run: true
77
+
78
+ - id: commitizen
79
+ name: commitizen check
80
+ entry: uv run cz check --commit-msg-file
81
+ language: system
82
+ stages: [commit-msg]
@@ -1,3 +1,7 @@
1
+ ## v1.5.0 (2026-04-06)
2
+
3
+ ## v1.4.0 (2026-04-05)
4
+
1
5
  ## v1.3.0 (2026-04-04)
2
6
 
3
7
  ## v1.2.0 (2026-04-02)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wraith-cli
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Sovereign Command Centre for a Ghost Stack
5
5
  Project-URL: Homepage, https://git.thomaspeoples.com/thomaspeoples/wraith-cli
6
6
  Project-URL: Documentation, https://www.thomaspeoples.com/gitea-repos/wraith-cli/
@@ -32,7 +32,9 @@ Classifier: Programming Language :: Python :: 3
32
32
  Classifier: Programming Language :: Python :: 3.12
33
33
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
34
  Requires-Python: >=3.12
35
+ Requires-Dist: pydantic-settings>=2.0.0
35
36
  Requires-Dist: python-dotenv
37
+ Requires-Dist: pyyaml>=6.0.1
36
38
  Requires-Dist: requests
37
39
  Requires-Dist: rich>=13.0.0
38
40
  Requires-Dist: typer>=0.9.0
@@ -64,35 +66,6 @@ Description-Content-Type: text/markdown
64
66
  high-level container orchestration and bare-metal reality, providing the "Ghost Factory"
65
67
  for instant project scaffolding.
66
68
 
67
- ---
68
-
69
- ## 🏗️ The Ghost Factory (New)
70
-
71
- The `spawn` command allows you to go from **Idea to Code** in under 5 seconds by
72
- bleaching a template and provisioning a private Gitea repository automatically.
73
-
74
- ```bash
75
- wraith spawn my-new-api
76
- ```
77
-
78
- 1. **🧬 Clones** your `GHOST_TEMPLATE_URL`.
79
- 2. **🧹 Bleaches** all previous git history.
80
- 3. **🌐 Provisions** a new private repo via the Gitea API.
81
- 4. **🚀 Pushes** the clean stack to your Sovereign remote.
82
-
83
- ---
84
-
85
- ## 🛠️ Operational Manual
86
-
87
- | Command | Feature | Status |
88
- |--------------------------|------------------------|---------------------------------------------|
89
- | `wraith spawn <name>` | **The Ghost Factory** | 🏗️ Scaffolds new repos via Gitea API. |
90
- | `wraith update` | **Global Update** | 🟢 PyPI-linked & `uv` powered. |
91
- | `wraith ps` | **Rich Observability** | 🟢 Sovereign Dark styling for Docker. |
92
- | `wraith tail <svc>` | **Flexible Logging** | 🟢 Supports `--path` & Env Vars. |
93
- | `wraith status` | **Heartbeat** | 🟢 Monitor OpenViking (Port 1933). |
94
- | `wraith runner-reset` | **Runner Defence** | 🟢 CI/CD maintenance & registration wipe. |
95
- | `wraith --version` | **Self-Identity** | 🟢 Eager callback for versioning. |
96
69
 
97
70
  ---
98
71
 
@@ -11,35 +11,6 @@
11
11
  high-level container orchestration and bare-metal reality, providing the "Ghost Factory"
12
12
  for instant project scaffolding.
13
13
 
14
- ---
15
-
16
- ## 🏗️ The Ghost Factory (New)
17
-
18
- The `spawn` command allows you to go from **Idea to Code** in under 5 seconds by
19
- bleaching a template and provisioning a private Gitea repository automatically.
20
-
21
- ```bash
22
- wraith spawn my-new-api
23
- ```
24
-
25
- 1. **🧬 Clones** your `GHOST_TEMPLATE_URL`.
26
- 2. **🧹 Bleaches** all previous git history.
27
- 3. **🌐 Provisions** a new private repo via the Gitea API.
28
- 4. **🚀 Pushes** the clean stack to your Sovereign remote.
29
-
30
- ---
31
-
32
- ## 🛠️ Operational Manual
33
-
34
- | Command | Feature | Status |
35
- |--------------------------|------------------------|---------------------------------------------|
36
- | `wraith spawn <name>` | **The Ghost Factory** | 🏗️ Scaffolds new repos via Gitea API. |
37
- | `wraith update` | **Global Update** | 🟢 PyPI-linked & `uv` powered. |
38
- | `wraith ps` | **Rich Observability** | 🟢 Sovereign Dark styling for Docker. |
39
- | `wraith tail <svc>` | **Flexible Logging** | 🟢 Supports `--path` & Env Vars. |
40
- | `wraith status` | **Heartbeat** | 🟢 Monitor OpenViking (Port 1933). |
41
- | `wraith runner-reset` | **Runner Defence** | 🟢 CI/CD maintenance & registration wipe. |
42
- | `wraith --version` | **Self-Identity** | 🟢 Eager callback for versioning. |
43
14
 
44
15
  ---
45
16
 
@@ -0,0 +1,245 @@
1
+ # CLI
2
+
3
+ Wraith Sovereign CLI: Ghost Stack Orchestrator
4
+
5
+ **Usage**:
6
+
7
+ ```console
8
+ $ [OPTIONS] COMMAND [ARGS]...
9
+ ```
10
+
11
+ **Options**:
12
+
13
+ * `--version`: Show version and exit.
14
+ * `--install-completion`: Install completion for the current shell.
15
+ * `--show-completion`: Show completion for the current shell, to copy it or customize the installation.
16
+ * `--help`: Show this message and exit.
17
+
18
+ **Commands**:
19
+
20
+ * `init`: Bootstrap a new Wraith environment.
21
+ * `status`: Check the heartbeat of the OpenViking Stack.
22
+ * `health`: Visualise health of all configured services.
23
+ * `update`: Sync Wraith with the latest PyPI release.
24
+ * `ps`: List running containers with Sovereign...
25
+ * `tail`: Stream live logs from a specific service.
26
+ * `runner-reset`: Repair hung or offline Gitea Action Runners.
27
+ * `spawn`: Scaffold a new repository instantly.
28
+ * `logs`: Unified hardware health and logging portal.
29
+ * `mesh`: Map the Tailscale Ghost Mesh network.
30
+ * `audit`: Execute a Sovereign security and...
31
+
32
+ ## `init`
33
+
34
+ Bootstrap a new Wraith environment.
35
+
36
+ Interactive wizard to configure the stack name, API URLs,
37
+ and auto-probe for Docker and Tailscale presence.
38
+
39
+ **Usage**:
40
+
41
+ ```console
42
+ $ init [OPTIONS]
43
+ ```
44
+
45
+ **Options**:
46
+
47
+ * `--help`: Show this message and exit.
48
+
49
+ ## `status`
50
+
51
+ Check the heartbeat of the OpenViking Stack.
52
+
53
+ Polls the configured VIKING_BASE_URL health
54
+ endpoint to verify if the orchestrator API is responsive.
55
+
56
+ **Usage**:
57
+
58
+ ```console
59
+ $ status [OPTIONS]
60
+ ```
61
+
62
+ **Options**:
63
+
64
+ * `--help`: Show this message and exit.
65
+
66
+ ## `health`
67
+
68
+ Visualise health of all configured services.
69
+
70
+ Polls the health_url for each service defined in .wraith.yaml.
71
+ Automatically parses OpenAPI/Swagger specs to find /health endpoints.
72
+
73
+ **Usage**:
74
+
75
+ ```console
76
+ $ health [OPTIONS]
77
+ ```
78
+
79
+ **Options**:
80
+
81
+ * `--help`: Show this message and exit.
82
+
83
+ ## `update`
84
+
85
+ Sync Wraith with the latest PyPI release.
86
+
87
+ Compares local versioning against the remote registry and
88
+ automatically triggers an upgrade via &#x27;uv tool&#x27; if a newer
89
+ version is detected.
90
+
91
+ **Usage**:
92
+
93
+ ```console
94
+ $ update [OPTIONS]
95
+ ```
96
+
97
+ **Options**:
98
+
99
+ * `--help`: Show this message and exit.
100
+
101
+ ## `ps`
102
+
103
+ List running containers with Sovereign styling.
104
+
105
+ Wraps &#x27;docker ps&#x27; to provide a high-contrast,
106
+ readable table summarising service names,
107
+ container status, and source images.
108
+
109
+ **Usage**:
110
+
111
+ ```console
112
+ $ ps [OPTIONS]
113
+ ```
114
+
115
+ **Options**:
116
+
117
+ * `-a, --all`: Show all containers
118
+ * `--help`: Show this message and exit.
119
+
120
+ ## `tail`
121
+
122
+ Stream live logs from a specific service.
123
+
124
+ Connects to a running container to output real-time logs.
125
+ Resolves Compose paths via CLI flags,
126
+ environment variables, or local directory discovery.
127
+
128
+ Priority:
129
+ 1. Passed flag --path
130
+ 2. Configured compose_path
131
+ 3. Discovered via search_roots
132
+ 4. Current Directory
133
+
134
+ **Usage**:
135
+
136
+ ```console
137
+ $ tail [OPTIONS] SERVICE
138
+ ```
139
+
140
+ **Arguments**:
141
+
142
+ * `SERVICE`: Service name (e.g., ollama, gitea) [required]
143
+
144
+ **Options**:
145
+
146
+ * `-p, --path PATH`: Path to directory with docker-compose.yml.
147
+ * `--help`: Show this message and exit.
148
+
149
+ ## `runner-reset`
150
+
151
+ Repair hung or offline Gitea Action Runners.
152
+
153
+ Performs a nuclear reset: stops the container,
154
+ purges the local registration data, and restarts
155
+ the service to force a fresh handshake with Gitea.
156
+
157
+ Requires GITEA_COMPOSE_PATH as an envelope variable
158
+
159
+ **Usage**:
160
+
161
+ ```console
162
+ $ runner-reset [OPTIONS]
163
+ ```
164
+
165
+ **Options**:
166
+
167
+ * `--help`: Show this message and exit.
168
+
169
+ ## `spawn`
170
+
171
+ Scaffold a new repository instantly.
172
+
173
+ Atomic scaffolding for new Ghost Stack repositories.
174
+ Clones a template, bleaches git history,
175
+ provisions a remote repository via API, and
176
+ pushes the initial commit in one sequence.
177
+
178
+ **Usage**:
179
+
180
+ ```console
181
+ $ spawn [OPTIONS] REPO_NAME
182
+ ```
183
+
184
+ **Arguments**:
185
+
186
+ * `REPO_NAME`: Name of the new Ghost Stack repository [required]
187
+
188
+ **Options**:
189
+
190
+ * `--help`: Show this message and exit.
191
+
192
+ ## `logs`
193
+
194
+ Unified hardware health and logging portal.
195
+
196
+ Default behavior provides log-tailing instructions.
197
+ Using the --health flag triggers a deep-dive query
198
+ into the Scrutiny API for SMART drive data and disk temps.
199
+
200
+ **Usage**:
201
+
202
+ ```console
203
+ $ logs [OPTIONS]
204
+ ```
205
+
206
+ **Options**:
207
+
208
+ * `--health`: Pull SMART data summary from Scrutiny API
209
+ * `--help`: Show this message and exit.
210
+
211
+ ## `mesh`
212
+
213
+ Map the Tailscale Ghost Mesh network.
214
+
215
+ Interrogates the Tailscale daemon (local or containerised)
216
+ to return a list of active peers, their internal IPs, and
217
+ current online/offline status.
218
+
219
+ **Usage**:
220
+
221
+ ```console
222
+ $ mesh [OPTIONS]
223
+ ```
224
+
225
+ **Options**:
226
+
227
+ * `--help`: Show this message and exit.
228
+
229
+ ## `audit`
230
+
231
+ Execute a Sovereign security and compliance scan.
232
+
233
+ Audits the running stack for common vulnerabilities, including
234
+ containers running with root privileges and exposed ports
235
+ that deviate from the Registry Spec.
236
+
237
+ **Usage**:
238
+
239
+ ```console
240
+ $ audit [OPTIONS]
241
+ ```
242
+
243
+ **Options**:
244
+
245
+ * `--help`: Show this message and exit.
@@ -35,4 +35,3 @@ extra:
35
35
  - icon: material/home
36
36
  link: https://www.thomaspeoples.com/
37
37
  name: Back to Main Site
38
-
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wraith-cli"
7
- version = "1.3.0"
7
+ version = "1.5.0"
8
8
  description = "Sovereign Command Centre for a Ghost Stack"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -27,6 +27,8 @@ dependencies = [
27
27
  "python-dotenv",
28
28
  "rich>=13.0.0",
29
29
  "typer>=0.9.0",
30
+ "pydantic-settings>=2.0.0",
31
+ "pyyaml>=6.0.1",
30
32
  ]
31
33
 
32
34
  [project.urls]
@@ -0,0 +1,10 @@
1
+ # fmt: off
2
+ # DO NOT EDIT: Trailing whitespace here is structural,
3
+ # not "useless" linting noise.
4
+ WRAITH_LOGO = r"""
5
+ ▖ ▖▄▖▄▖▄▖▄▖▖▖ ▄▖▖ ▄▖
6
+ ▌▞▖▌▙▘▌▌▐ ▐ ▙▌▄▖▌ ▌ ▐
7
+ ▛ ▝▌▌▌▛▌▟▖▐ ▌▌ ▙▖▙▖▟▖
8
+
9
+ """
10
+ # fmt: on
@@ -0,0 +1,134 @@
1
+ from functools import lru_cache
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ import yaml
6
+ from pydantic import AliasChoices, BaseModel, Field, HttpUrl
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+
9
+
10
+ class ServiceModel(BaseModel):
11
+ path: Path
12
+ metadata: dict[str, Any] = Field(default_factory=dict)
13
+ health_url: HttpUrl | None = None
14
+
15
+
16
+ class TemplateModel(BaseModel):
17
+ url: str
18
+
19
+
20
+ class WraithSettings(BaseSettings):
21
+ stack_name: str = "Ghost Stack"
22
+ viking_api: HttpUrl | None = None
23
+ search_roots: list[Path] = Field(default_factory=list)
24
+ services: dict[str, ServiceModel] = Field(default_factory=dict)
25
+ docker_enabled: bool = False
26
+ tailscale_enabled: bool = False
27
+
28
+ # Provider Settings
29
+ forge_api_url: str | None = None
30
+ forge_token: str | None = None
31
+ forge_type: str = "gitea"
32
+ templates: dict[str, TemplateModel] = Field(default_factory=dict)
33
+
34
+ # Legacy .env mappings
35
+ gitea_api_url: str | None = Field(
36
+ default=None,
37
+ validation_alias=AliasChoices("WRAITH_GITEA_API_URL", "GITEA_API_URL"),
38
+ )
39
+ gitea_token: str | None = Field(
40
+ default=None,
41
+ validation_alias=AliasChoices("WRAITH_GITEA_TOKEN", "GITEA_TOKEN"),
42
+ )
43
+ gitea_template_url: str | None = Field(
44
+ default=None,
45
+ validation_alias=AliasChoices(
46
+ "WRAITH_GITEA_TEMPLATE_URL", "GITEA_TEMPLATE_URL"
47
+ ),
48
+ )
49
+ gitea_compose_path: Path | None = Field(
50
+ default=None,
51
+ validation_alias=AliasChoices(
52
+ "WRAITH_GITEA_COMPOSE_PATH", "GITEA_COMPOSE_PATH"
53
+ ),
54
+ )
55
+ compose_path: Path | None = Field(
56
+ default=None,
57
+ validation_alias=AliasChoices("WRAITH_COMPOSE_PATH", "COMPOSE_PATH"),
58
+ )
59
+
60
+ # Ingests .env variables as fallback, prioritizing WRAITH_ prefix
61
+ model_config = SettingsConfigDict(
62
+ env_prefix="WRAITH_",
63
+ env_file=".env",
64
+ env_file_encoding="utf-8",
65
+ extra="ignore",
66
+ populate_by_name=True,
67
+ )
68
+
69
+
70
+ @lru_cache
71
+ def get_settings() -> WraithSettings:
72
+ """
73
+ Load settings with priority:
74
+ 1. Local .wraith.yaml (CWD)
75
+ 2. User ~/.config/wraith/config.yaml
76
+ 3. Environment variables (WRAITH_ prefix & .env fallback)
77
+ """
78
+ yaml_data: dict[str, Any] = {}
79
+
80
+ # 2. User-level config
81
+ user_config = Path.home() / ".config" / "wraith" / "config.yaml"
82
+ if user_config.exists(): # pragma: no cover
83
+ with open(user_config) as f:
84
+ parsed = yaml.safe_load(f)
85
+ if isinstance(parsed, dict):
86
+ yaml_data.update(parsed)
87
+
88
+ # 1. Local config (overrides user-level)
89
+ local_config = Path.cwd() / ".wraith.yaml"
90
+ if local_config.exists(): # pragma: no cover
91
+ with open(local_config) as f:
92
+ parsed = yaml.safe_load(f)
93
+ if isinstance(parsed, dict):
94
+ yaml_data.update(parsed)
95
+
96
+ # Pydantic BaseSettings kwargs override environment variables.
97
+ return WraithSettings(**yaml_data)
98
+
99
+
100
+ def discover_service(
101
+ service_name: str, search_roots: list[Path]
102
+ ) -> Path | None: # pragma: no cover
103
+ """
104
+ Recursively search for docker-compose.yml files in search_roots.
105
+ Matches if the directory name or container_name equals the service_name.
106
+ """
107
+ for root in search_roots:
108
+ if not root.exists() or not root.is_dir():
109
+ continue
110
+
111
+ for compose_file in root.rglob("docker-compose.yml"):
112
+ # Match by directory name
113
+ if compose_file.parent.name == service_name:
114
+ return compose_file
115
+
116
+ # Match by container_name or service key in yaml
117
+ try:
118
+ with open(compose_file) as f:
119
+ compose_data = yaml.safe_load(f)
120
+ if compose_data and isinstance(compose_data, dict):
121
+ services = compose_data.get("services", {})
122
+ for svc_key, svc_def in services.items():
123
+ if svc_key == service_name:
124
+ return compose_file
125
+ if (
126
+ isinstance(svc_def, dict)
127
+ and svc_def.get("container_name")
128
+ == service_name
129
+ ):
130
+ return compose_file
131
+ except Exception:
132
+ pass # Ignore unparsable files during discovery
133
+
134
+ return None