wraith-cli 1.3.0__tar.gz → 1.4.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 (38) hide show
  1. wraith_cli-1.4.0/.pre-commit-config.yaml +82 -0
  2. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/CHANGELOG.md +2 -0
  3. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/PKG-INFO +1 -30
  4. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/README.md +0 -29
  5. wraith_cli-1.4.0/docs/usage.md +208 -0
  6. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/pyproject.toml +1 -1
  7. wraith_cli-1.4.0/src/wraith_cli/assets.py +10 -0
  8. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/src/wraith_cli/main.py +76 -27
  9. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/src/wraith_cli/shield.py +49 -25
  10. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/tests/test_cli.py +1 -1
  11. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/tests/test_shield.py +108 -5
  12. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/uv.lock +1 -1
  13. wraith_cli-1.3.0/.pre-commit-config.yaml +0 -46
  14. wraith_cli-1.3.0/docs/usage.md +0 -80
  15. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.cz.yaml +0 -0
  16. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.env.example +0 -0
  17. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.gitea/CODEOWNERS.md +0 -0
  18. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.gitea/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.gitea/workflows/pages.yml +0 -0
  20. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.gitea/workflows/release.yml +0 -0
  21. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.gitea/workflows/test.yml +0 -0
  22. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.gitignore +0 -0
  23. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/.secrets.baseline +0 -0
  24. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/CONTRIBUTING.md +0 -0
  25. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/LICENSE +0 -0
  26. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/bin/build.sh +0 -0
  27. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/bin/run_tests.sh +0 -0
  28. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/bin/setup_venv.sh +0 -0
  29. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/docs/architecture.md +0 -0
  30. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/docs/index.md +0 -0
  31. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/docs/reference.md +0 -0
  32. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/docs/security.md +0 -0
  33. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/mkdocs.yml +0 -0
  34. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/src/wraith_cli/__init__.py +0 -0
  35. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/src/wraith_cli/qol.py +0 -0
  36. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/src/wraith_cli/repo_make.py +0 -0
  37. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/tests/test_qol.py +0 -0
  38. {wraith_cli-1.3.0 → wraith_cli-1.4.0}/tests/test_repo_make.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,5 @@
1
+ ## v1.4.0 (2026-04-05)
2
+
1
3
  ## v1.3.0 (2026-04-04)
2
4
 
3
5
  ## 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.4.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/
@@ -64,35 +64,6 @@ Description-Content-Type: text/markdown
64
64
  high-level container orchestration and bare-metal reality, providing the "Ghost Factory"
65
65
  for instant project scaffolding.
66
66
 
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
67
 
97
68
  ---
98
69
 
@@ -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,208 @@
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
+ * `status`: Check the heartbeat of the OpenViking Stack.
21
+ * `update`: Sync Wraith with the latest PyPI release.
22
+ * `ps`: List running containers with Sovereign...
23
+ * `tail`: Stream live logs from a specific service.
24
+ * `runner-reset`: Repair hung or offline Gitea Action Runners.
25
+ * `spawn`: 🏭 Scaffold a new repository instantly.
26
+ * `logs`: Unified hardware health and logging portal.
27
+ * `mesh`: Map the Tailscale Ghost Mesh network.
28
+ * `audit`: Execute a Sovereign security and...
29
+
30
+ ## `status`
31
+
32
+ Check the heartbeat of the OpenViking Stack.
33
+
34
+ Polls the configured VIKING_BASE_URL health
35
+ nendpoint to verify if the orchestrator API is responsive.
36
+
37
+ **Usage**:
38
+
39
+ ```console
40
+ $ status [OPTIONS]
41
+ ```
42
+
43
+ **Options**:
44
+
45
+ * `--help`: Show this message and exit.
46
+
47
+ ## `update`
48
+
49
+ Sync Wraith with the latest PyPI release.
50
+
51
+ Compares local versioning against the remote registry and
52
+ automatically triggers an upgrade via &#x27;uv tool&#x27; if a newer
53
+ version is detected.
54
+
55
+ **Usage**:
56
+
57
+ ```console
58
+ $ update [OPTIONS]
59
+ ```
60
+
61
+ **Options**:
62
+
63
+ * `--help`: Show this message and exit.
64
+
65
+ ## `ps`
66
+
67
+ List running containers with Sovereign styling.
68
+
69
+ Wraps &#x27;docker ps&#x27; to provide a high-contrast,
70
+ readable table summarising service names,
71
+ container status, and source images.
72
+
73
+ **Usage**:
74
+
75
+ ```console
76
+ $ ps [OPTIONS]
77
+ ```
78
+
79
+ **Options**:
80
+
81
+ * `-a, --all`: Show all containers
82
+ * `--help`: Show this message and exit.
83
+
84
+ ## `tail`
85
+
86
+ Stream live logs from a specific service.
87
+
88
+ Connects to a running container to output real-time logs.
89
+ Resolves Compose paths via CLI flags,
90
+ environment variables, or local directory discovery.
91
+
92
+ Priority:
93
+ 1. Passed flag --path
94
+ 2. Env Var WRAITH_COMPOSE_PATH
95
+ 3. Current Directory
96
+
97
+ **Usage**:
98
+
99
+ ```console
100
+ $ tail [OPTIONS] SERVICE
101
+ ```
102
+
103
+ **Arguments**:
104
+
105
+ * `SERVICE`: Service name (e.g., ollama, gitea) [required]
106
+
107
+ **Options**:
108
+
109
+ * `-p, --path PATH`: Path to directory with docker-compose.yml. [env var: WRAITH_COMPOSE_PATH]
110
+ * `--help`: Show this message and exit.
111
+
112
+ ## `runner-reset`
113
+
114
+ Repair hung or offline Gitea Action Runners.
115
+
116
+ Performs a nuclear reset: stops the container,
117
+ purges the local registration data, and restarts
118
+ the service to force a fresh handshake with Gitea.
119
+
120
+ Requires GITEA_COMPOSE_PATH as an envelope variable
121
+
122
+ **Usage**:
123
+
124
+ ```console
125
+ $ runner-reset [OPTIONS]
126
+ ```
127
+
128
+ **Options**:
129
+
130
+ * `--help`: Show this message and exit.
131
+
132
+ ## `spawn`
133
+
134
+ 🏭 Scaffold a new repository instantly.
135
+
136
+ Atomic scaffolding for new Ghost Stack repositories.
137
+ Clones a template, bleaches git history,
138
+ provisions a remote repository via API, and
139
+ pushes the initial commit in one sequence.
140
+
141
+ **Usage**:
142
+
143
+ ```console
144
+ $ spawn [OPTIONS] REPO_NAME
145
+ ```
146
+
147
+ **Arguments**:
148
+
149
+ * `REPO_NAME`: Name of the new Ghost Stack repository [required]
150
+
151
+ **Options**:
152
+
153
+ * `--help`: Show this message and exit.
154
+
155
+ ## `logs`
156
+
157
+ Unified hardware health and logging portal.
158
+
159
+ Default behavior provides log-tailing instructions.
160
+ Using the --health flag triggers a deep-dive query
161
+ into the Scrutiny API for SMART drive data and disk temps.
162
+
163
+ **Usage**:
164
+
165
+ ```console
166
+ $ logs [OPTIONS]
167
+ ```
168
+
169
+ **Options**:
170
+
171
+ * `--health`: Pull SMART data summary from Scrutiny API
172
+ * `--help`: Show this message and exit.
173
+
174
+ ## `mesh`
175
+
176
+ Map the Tailscale Ghost Mesh network.
177
+
178
+ Interrogates the Tailscale daemon (local or containerised)
179
+ to return a list of active peers, their internal IPs, and
180
+ current online/offline status.
181
+
182
+ **Usage**:
183
+
184
+ ```console
185
+ $ mesh [OPTIONS]
186
+ ```
187
+
188
+ **Options**:
189
+
190
+ * `--help`: Show this message and exit.
191
+
192
+ ## `audit`
193
+
194
+ Execute a Sovereign security and compliance scan.
195
+
196
+ Audits the running stack for common vulnerabilities, including
197
+ containers running with root privileges and exposed ports
198
+ that deviate from the Registry Spec.
199
+
200
+ **Usage**:
201
+
202
+ ```console
203
+ $ audit [OPTIONS]
204
+ ```
205
+
206
+ **Options**:
207
+
208
+ * `--help`: Show this message and exit.
@@ -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.4.0"
8
8
  description = "Sovereign Command Centre for a Ghost Stack"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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
@@ -12,10 +12,10 @@ from rich.markdown import Markdown
12
12
  from rich.panel import Panel
13
13
  from rich.text import Text
14
14
 
15
- from wraith_cli import qol, repo_make, shield
15
+ from wraith_cli import assets, qol, repo_make, shield
16
16
 
17
17
  SOVEREIGN_ADVISORY = """
18
- ## 🛡️ Sovereign Security Advisory – Please Read
18
+ ## Sovereign Security Advisory – Please Read
19
19
 
20
20
  **Wraith-CLI** is your stack’s nervous system and has sharp edges.
21
21
  The commands in this toolkit can wipe runner data (`runner-reset`),
@@ -50,7 +50,12 @@ GITEA_TEMPLATE_URL = os.getenv("GITEA_TEMPLATE_URL")
50
50
 
51
51
  # --- Callbacks ---
52
52
  def version_callback(value: bool):
53
- """Callback to handle the --version flag."""
53
+ """
54
+ Display the current Wraith-CLI version.
55
+
56
+ Checks the local metadata to return the
57
+ installed version string before exiting the application.
58
+ """
54
59
  if value:
55
60
  v = get_local_version("wraith-cli")
56
61
  typer.echo(f"Wraith CLI v{v}")
@@ -62,14 +67,7 @@ def print_wraith_welcome(): # pragma: no cover
62
67
  console = qol.console
63
68
  local_v = get_local_version("wraith-cli")
64
69
 
65
- wraith_pixel_logo = r"""
66
- ▖ ▖▄▖▄▖▄▖▄▖▖▖ ▄▖▖ ▄▖
67
- ▌▞▖▌▙▘▌▌▐ ▐ ▙▌▄▖▌ ▌ ▐
68
- ▛ ▝▌▌▌▛▌▟▖▐ ▌▌ ▙▖▙▖▟▖
69
-
70
- """
71
-
72
- logo_text = Text(wraith_pixel_logo, style="bold black on white")
70
+ logo_text = Text(assets.WRAITH_LOGO, style="bold black on white")
73
71
  console.print(Align.center(logo_text))
74
72
 
75
73
  sub_header_markup = (
@@ -79,11 +77,6 @@ def print_wraith_welcome(): # pragma: no cover
79
77
  console.print(sub_header, justify="center")
80
78
  console.print("\n")
81
79
 
82
- console.print(
83
- Text(f"Wraith Sovereign v{local_v}", style="white"), justify="center"
84
- )
85
- console.print("\n")
86
-
87
80
  md = Markdown(SOVEREIGN_ADVISORY)
88
81
 
89
82
  panel = Panel(
@@ -116,6 +109,9 @@ def main(
116
109
  """
117
110
  [bold green]Wraith-CLI[/bold green]
118
111
  The nervous system of the Sovereign Ghost Stack.
112
+
113
+ Orchestrate your local infrastructure, manage Gitea repositories,
114
+ and monitor system-wide health from a single unified entry point.
119
115
  """
120
116
 
121
117
  if ctx.invoked_subcommand is None and not version:
@@ -127,7 +123,12 @@ def main(
127
123
 
128
124
  @app.command()
129
125
  def status():
130
- """Check the heartbeat of the OpenViking Stack."""
126
+ """
127
+ Check the heartbeat of the OpenViking Stack.
128
+
129
+ Polls the configured VIKING_BASE_URL health
130
+ nendpoint to verify if the orchestrator API is responsive.
131
+ """
131
132
  url = f"{VIKING_URL}/health"
132
133
  try:
133
134
  response = requests.get(url, timeout=3)
@@ -144,7 +145,13 @@ def status():
144
145
 
145
146
  @app.command()
146
147
  def update():
147
- """Check for updates on PyPI and upgrade via uv."""
148
+ """
149
+ Sync Wraith with the latest PyPI release.
150
+
151
+ Compares local versioning against the remote registry and
152
+ automatically triggers an upgrade via 'uv tool' if a newer
153
+ version is detected.
154
+ """
148
155
  try:
149
156
  local_v = get_local_version("wraith-cli")
150
157
  remote_v = qol.get_latest_version()
@@ -166,7 +173,13 @@ def ps(
166
173
  bool, typer.Option("--all", "-a", help="Show all containers")
167
174
  ] = False,
168
175
  ):
169
- """View Docker process list with Sovereign styling."""
176
+ """
177
+ List running containers with Sovereign styling.
178
+
179
+ Wraps 'docker ps' to provide a high-contrast,
180
+ readable table summarising service names,
181
+ container status, and source images.
182
+ """
170
183
  cmd = ["docker", "ps"]
171
184
  if all:
172
185
  cmd.append("-a")
@@ -198,7 +211,13 @@ def tail(
198
211
  ),
199
212
  ] = None,
200
213
  ):
201
- """Tail logs for any service in your stack.
214
+ """
215
+ Stream live logs from a specific service.
216
+
217
+ Connects to a running container to output real-time logs.
218
+ Resolves Compose paths via CLI flags,
219
+ environment variables, or local directory discovery.
220
+
202
221
  Priority:
203
222
  1. Passed flag --path
204
223
  2. Env Var WRAITH_COMPOSE_PATH
@@ -218,7 +237,15 @@ def tail(
218
237
 
219
238
  @app.command()
220
239
  def runner_reset():
221
- """Resets the Gitea Action Runner (Requires GITEA_COMPOSE_PATH)."""
240
+ """
241
+ Repair hung or offline Gitea Action Runners.
242
+
243
+ Performs a nuclear reset: stops the container,
244
+ purges the local registration data, and restarts
245
+ the service to force a fresh handshake with Gitea.
246
+
247
+ Requires GITEA_COMPOSE_PATH as an envelope variable
248
+ """
222
249
  if not GITEA_PATH:
223
250
  typer.secho("❌ Error: GITEA_COMPOSE_PATH not set in .env", fg="red")
224
251
  raise typer.Exit(1)
@@ -249,7 +276,14 @@ def spawn(
249
276
  ..., help="Name of the new Ghost Stack repository"
250
277
  ),
251
278
  ):
252
- """🏭 The Gitea Factory: Scaffold a new repository instantly."""
279
+ """
280
+ 🏭 Scaffold a new repository instantly.
281
+
282
+ Atomic scaffolding for new Ghost Stack repositories.
283
+ Clones a template, bleaches git history,
284
+ provisions a remote repository via API, and
285
+ pushes the initial commit in one sequence.
286
+ """
253
287
  if not all([GITEA_API_URL, GITEA_TOKEN, GITEA_TEMPLATE_URL]):
254
288
  typer.secho(
255
289
  """❌ Missing Environment Variables!
@@ -305,20 +339,32 @@ def logs(
305
339
  ),
306
340
  ] = False,
307
341
  ):
308
- """View stack logs or bare-metal health status."""
342
+ """
343
+ Unified hardware health and logging portal.
344
+
345
+ Default behavior provides log-tailing instructions.
346
+ Using the --health flag triggers a deep-dive query
347
+ into the Scrutiny API for SMART drive data and disk temps.
348
+ """
309
349
  if health:
310
350
  typer.echo("🔍 Polling Bare-Metal SMART data via Scrutiny...")
311
351
  health_table = shield.get_scrutiny_health()
312
352
  qol.console.print(health_table)
313
353
  else:
314
354
  typer.echo(
315
- "Use 'wraith tail <service>' for container logs. Pass --health for drive health."
355
+ "Use 'wraith tail <svc>' for logs. Pass --health for drive health."
316
356
  )
317
357
 
318
358
 
319
359
  @app.command()
320
360
  def mesh():
321
- """Checks Tailscale status and lists active peers in the Ghost Mesh."""
361
+ """
362
+ Map the Tailscale Ghost Mesh network.
363
+
364
+ Interrogates the Tailscale daemon (local or containerised)
365
+ to return a list of active peers, their internal IPs, and
366
+ current online/offline status.
367
+ """
322
368
  typer.echo("🌐 Querying Ghost Mesh (Tailscale)...")
323
369
  mesh_table = shield.get_tailscale_mesh()
324
370
  qol.console.print(mesh_table)
@@ -327,8 +373,11 @@ def mesh():
327
373
  @app.command()
328
374
  def audit():
329
375
  """
330
- Checks for containers running as root or exposed ports not in your
331
- Registry Spec.
376
+ Execute a Sovereign security and compliance scan.
377
+
378
+ Audits the running stack for common vulnerabilities, including
379
+ containers running with root privileges and exposed ports
380
+ that deviate from the Registry Spec.
332
381
  """
333
382
  typer.echo("🛡️ Initiating Sovereign Security Audit...")
334
383
  audit_table = shield.run_security_audit()
@@ -1,12 +1,14 @@
1
1
  import json
2
2
  import os
3
3
  import subprocess
4
+
4
5
  import requests
5
6
  from rich.table import Table
6
7
 
7
8
  # Pull from .env, fallback to the Ghost Stack default port
8
9
  SCRUTINY_URL = os.getenv("SCRUTINY_URL", "http://127.0.0.1:8090")
9
10
 
11
+
10
12
  def get_scrutiny_health() -> Table:
11
13
  """Fetches SMART data from the Scrutiny API and formats it.
12
14
 
@@ -28,26 +30,32 @@ def get_scrutiny_health() -> Table:
28
30
  response = requests.get(f"{SCRUTINY_URL}/api/summary", timeout=5)
29
31
  response.raise_for_status()
30
32
  data = response.json()
31
-
33
+
32
34
  devices = data.get("data", {}).get("summary", {})
33
35
  if not devices:
34
36
  table.add_row("No devices found.", "-", "-", "-")
35
37
  return table
36
38
 
37
- for wwn, device in devices.items():
39
+ for _wwn, device in devices.items():
38
40
  name = device.get("device", {}).get("name", "Unknown")
39
41
  capacity_bytes = device.get("device", {}).get("capacity", 0)
40
- capacity_tb = f"{(capacity_bytes / (10**12)):.1f} TB" if capacity_bytes else "Unknown"
41
-
42
+ capacity_tb = (
43
+ f"{(capacity_bytes / (10**12)):.1f} TB"
44
+ if capacity_bytes
45
+ else "Unknown"
46
+ )
47
+
42
48
  temp = str(device.get("smart", {}).get("temp", "N/A")) + "°C"
43
49
  status = device.get("smart", {}).get("status", "Unknown")
44
-
50
+
45
51
  # Sovereign styling for status
46
52
  status_color = "green" if status.lower() == "passed" else "red"
47
- formatted_status = f"[{status_color}]{status.upper()}[/{status_color}]"
48
-
53
+ formatted_status = (
54
+ f"[{status_color}]{status.upper()}[/{status_color}]"
55
+ )
56
+
49
57
  table.add_row(name, capacity_tb, temp, formatted_status)
50
-
58
+
51
59
  except requests.exceptions.RequestException as e:
52
60
  table.add_row(f"[red]API Error: {e}[/red]", "-", "-", "-")
53
61
 
@@ -55,11 +63,7 @@ def get_scrutiny_health() -> Table:
55
63
 
56
64
 
57
65
  def get_tailscale_mesh() -> Table:
58
- """Pulls Tailscale status via JSON and formats active peers.
59
-
60
- Returns:
61
- Table: A rich Table object containing Tailscale mesh peer status.
62
- """
66
+ """Pulls Tailscale status from a Docker container and formats peers."""
63
67
  table = Table(
64
68
  title="Ghost Mesh (Tailscale)",
65
69
  border_style="bright_black",
@@ -70,30 +74,49 @@ def get_tailscale_mesh() -> Table:
70
74
  table.add_column("OS")
71
75
  table.add_column("Status")
72
76
 
77
+ # The command shifted to target the container
78
+ # docker exec <container_name> tailscale status --json
79
+ cmd = ["docker", "exec", "tailscale", "tailscale", "status", "--json"]
80
+
73
81
  try:
74
82
  result = subprocess.run(
75
- ["tailscale", "status", "--json"],
83
+ cmd,
76
84
  capture_output=True,
77
85
  text=True,
78
86
  check=True,
79
87
  )
88
+
80
89
  data = json.loads(result.stdout)
81
-
82
90
  peers = data.get("Peer", {})
83
- for peer_id, peer_data in peers.items():
91
+
92
+ if not peers:
93
+ table.add_row(
94
+ "No peers found", "-", "-", "[yellow]Solo Node[/yellow]"
95
+ )
96
+ return table
97
+
98
+ for peer_data in peers.values():
84
99
  hostname = peer_data.get("HostName", "Unknown")
85
- ip = peer_data.get("TailscaleIPs", ["Unknown"])[0]
100
+ ips = peer_data.get("TailscaleIPs", ["Unknown"])
101
+ ip = ips[0] if ips else "Unknown"
86
102
  os_name = peer_data.get("OS", "Unknown")
87
- is_online = peer_data.get("Online", False)
88
103
 
89
104
  status_str = (
90
105
  "[green]Online[/green]"
91
- if is_online
106
+ if peer_data.get("Online")
92
107
  else "[bright_black]Offline[/bright_black]"
93
108
  )
94
109
 
95
110
  table.add_row(hostname, ip, os_name, status_str)
96
-
111
+
112
+ except subprocess.CalledProcessError:
113
+ # This triggers if the container 'tailscale' isn't running
114
+ table.add_row(
115
+ "[red]Error: Tailscale container not found or stopped[/red]",
116
+ "-",
117
+ "-",
118
+ "-",
119
+ )
97
120
  except Exception as e:
98
121
  table.add_row(f"[red]Mesh Error: {e}[/red]", "-", "-", "-")
99
122
 
@@ -141,7 +164,7 @@ def run_security_audit() -> Table:
141
164
 
142
165
  for c in containers:
143
166
  name = c["Name"].lstrip("/")
144
-
167
+
145
168
  # Check User
146
169
  user = c["Config"].get("User", "")
147
170
  if not user or user == "0" or user == "root":
@@ -150,7 +173,7 @@ def run_security_audit() -> Table:
150
173
  else:
151
174
  user_disp = f"[green]{user}[/green]"
152
175
  is_root = False
153
-
176
+
154
177
  # Check Ports (Simplified: grabs bindings)
155
178
  ports = c["NetworkSettings"].get("Ports", {})
156
179
  exposed = []
@@ -162,13 +185,14 @@ def run_security_audit() -> Table:
162
185
  # Flag if exposed globally (0.0.0.0) vs. locally.
163
186
  if host_ip == "0.0.0.0" or host_ip == "":
164
187
  exposed.append(
165
- f"[yellow]{host_port}->{port} (Global)[/yellow]"
188
+ f"[yellow]{host_port}->{port} "
189
+ f"(Global)[/yellow]"
166
190
  )
167
191
  else:
168
192
  exposed.append(f"{host_port}->{port}")
169
-
193
+
170
194
  port_disp = ", ".join(exposed) if exposed else "Internal Only"
171
-
195
+
172
196
  # Determine Verdict
173
197
  if is_root and "Global" in port_disp:
174
198
  verdict = "[red]CRITICAL: Root + Global Port[/red]"
@@ -280,7 +280,7 @@ def test_logs_default():
280
280
  """Test logs command with no flags."""
281
281
  result = runner.invoke(app, ["logs"])
282
282
  assert result.exit_code == 0
283
- assert "Use 'wraith tail <service>'" in result.stdout
283
+ assert "Use 'wraith tail <svc>'" in result.stdout
284
284
 
285
285
 
286
286
  @patch("wraith_cli.shield.get_tailscale_mesh")
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import subprocess
2
3
  from unittest.mock import Mock, patch
3
4
 
4
5
  import pytest
@@ -91,6 +92,32 @@ def test_get_scrutiny_health_missing_data(mock_get):
91
92
  assert "FAILED" in str(cells[3])
92
93
 
93
94
 
95
+ @patch("wraith_cli.shield.requests.get")
96
+ def test_get_scrutiny_health_missing_smart_data(mock_get):
97
+ """Test Scrutiny data with missing 'smart' section."""
98
+ mock_response = Mock()
99
+ mock_response.json.return_value = {
100
+ "data": {
101
+ "summary": {
102
+ "WWN1": {
103
+ "device": {"name": "sdb", "capacity": 2 * 10**12},
104
+ # 'smart' key is entirely missing
105
+ }
106
+ }
107
+ }
108
+ }
109
+ mock_response.raise_for_status.return_value = None
110
+ mock_get.return_value = mock_response
111
+
112
+ table = get_scrutiny_health()
113
+ assert len(table.rows) == 1
114
+ cells = [list(col.cells)[0] for col in table.columns]
115
+ assert str(cells[0]) == "sdb"
116
+ assert str(cells[1]) == "2.0 TB"
117
+ assert str(cells[2]) == "N/A°C"
118
+ assert "UNKNOWN" in str(cells[3])
119
+
120
+
94
121
  @patch("wraith_cli.shield.subprocess.run")
95
122
  def test_get_tailscale_mesh_success(mock_run):
96
123
  """Test successful retrieval of Tailscale mesh data."""
@@ -131,14 +158,80 @@ def test_get_tailscale_mesh_success(mock_run):
131
158
 
132
159
 
133
160
  @patch("wraith_cli.shield.subprocess.run")
134
- def test_get_tailscale_mesh_error(mock_run):
135
- """Test handling of an error when running tailscale command."""
136
- mock_run.side_effect = Exception("Command not found")
161
+ def test_get_tailscale_mesh_generic_error(mock_run):
162
+ """Test handling of a generic error when running tailscale command."""
163
+ mock_run.side_effect = Exception("Generic error for tailscale")
164
+
165
+ table = get_tailscale_mesh()
166
+ assert len(table.rows) == 1
167
+ cells = [list(col.cells)[0] for col in table.columns]
168
+ assert "Mesh Error: Generic error for tailscale" in str(cells[0])
169
+
170
+
171
+ @patch("wraith_cli.shield.subprocess.run")
172
+ def test_get_tailscale_mesh_called_process_error(mock_run):
173
+ """Test handling of subprocess.CalledProcessError for tailscale."""
174
+ mock_run.side_effect = subprocess.CalledProcessError(1, ["tailscale"])
175
+
176
+ table = get_tailscale_mesh()
177
+ assert len(table.rows) == 1
178
+ cells = [list(col.cells)[0] for col in table.columns]
179
+ assert ("Error: Tailscale container not found or stopped"
180
+ in str(cells[0]))
181
+
182
+
183
+ @patch("wraith_cli.shield.subprocess.run")
184
+ def test_get_tailscale_mesh_no_peers(mock_run):
185
+ """Test Tailscale mesh when no peers are found."""
186
+ mock_process = Mock()
187
+ mock_process.stdout = json.dumps({"Peer": {}}) # Empty peers
188
+ mock_run.return_value = mock_process
137
189
 
138
190
  table = get_tailscale_mesh()
139
191
  assert len(table.rows) == 1
140
192
  cells = [list(col.cells)[0] for col in table.columns]
141
- assert "Mesh Error: Command not found" in str(cells[0])
193
+ assert str(cells[0]) == "No peers found"
194
+ assert "[yellow]Solo Node[/yellow]" in str(cells[3])
195
+
196
+
197
+ @patch("wraith_cli.shield.subprocess.run")
198
+ def test_get_tailscale_mesh_empty_ips(mock_run):
199
+ """Test Tailscale mesh with empty TailscaleIPs list."""
200
+ mock_process = Mock()
201
+ mock_process.stdout = json.dumps(
202
+ {
203
+ "Peer": {
204
+ "peer1": {
205
+ "HostName": "node1",
206
+ "TailscaleIPs": [], # Empty IPs
207
+ "OS": "Linux",
208
+ "Online": True,
209
+ }
210
+ }
211
+ }
212
+ )
213
+ mock_run.return_value = mock_process
214
+
215
+ table = get_tailscale_mesh()
216
+ assert len(table.rows) == 1
217
+ cells = [list(col.cells)[0] for col in table.columns]
218
+ assert str(cells[0]) == "node1"
219
+ assert str(cells[1]) == "Unknown"
220
+ assert str(cells[2]) == "Linux"
221
+ assert "Online" in str(cells[3])
222
+
223
+
224
+ @patch("wraith_cli.shield.subprocess.run")
225
+ def test_get_tailscale_mesh_malformed_json(mock_run):
226
+ """Test Tailscale mesh with malformed JSON output."""
227
+ mock_process = Mock()
228
+ mock_process.stdout = "this is not json"
229
+ mock_run.return_value = mock_process
230
+
231
+ table = get_tailscale_mesh()
232
+ assert len(table.rows) == 1
233
+ cells = [list(col.cells)[0] for col in table.columns]
234
+ assert "Mesh Error: Expecting value" in str(cells[0])
142
235
 
143
236
 
144
237
  @patch("wraith_cli.shield.subprocess.run")
@@ -155,9 +248,12 @@ def test_run_security_audit_no_containers(mock_run):
155
248
 
156
249
  def mock_container_inspect(user, ports):
157
250
  """Helper to create a mock container inspect JSON."""
251
+ config_data = {}
252
+ if user is not None:
253
+ config_data["User"] = user
158
254
  return {
159
255
  "Name": "/test-container",
160
- "Config": {"User": user},
256
+ "Config": config_data,
161
257
  "NetworkSettings": {"Ports": ports},
162
258
  }
163
259
 
@@ -201,6 +297,13 @@ def mock_container_inspect(user, ports):
201
297
  "SECURE",
202
298
  ),
203
299
  ("testuser", {}, "[green]testuser[/green]", "Internal Only", "SECURE"),
300
+ (
301
+ None, # Simulate missing 'User' key in Config
302
+ {"80/tcp": [{"HostIp": "127.0.0.1", "HostPort": "8080"}]},
303
+ "[red]ROOT[/red]",
304
+ "8080->80/tcp",
305
+ "WARNING: Root Context",
306
+ ),
204
307
  ],
205
308
  )
206
309
  @patch("wraith_cli.shield.subprocess.run")
@@ -1080,7 +1080,7 @@ wheels = [
1080
1080
 
1081
1081
  [[package]]
1082
1082
  name = "wraith-cli"
1083
- version = "1.3.0"
1083
+ version = "1.4.0"
1084
1084
  source = { editable = "." }
1085
1085
  dependencies = [
1086
1086
  { name = "python-dotenv" },
@@ -1,46 +0,0 @@
1
- repos:
2
- - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v4.4.0
4
- hooks:
5
- - id: trailing-whitespace
6
- - id: end-of-file-fixer
7
- - id: check-yaml
8
- - id: check-added-large-files
9
-
10
- - repo: https://github.com/astral-sh/ruff-pre-commit
11
- rev: v0.9.6
12
- hooks:
13
- - id: ruff
14
- args: ["--fix"]
15
- - id: ruff-format
16
-
17
- - repo: local
18
- hooks:
19
- - id: ty
20
- name: ty
21
- entry: uv run ty check
22
- language: system
23
- types: [python]
24
- pass_filenames: false
25
-
26
- - repo: https://github.com/Yelp/detect-secrets
27
- rev: v1.5.0
28
- hooks:
29
- - id: detect-secrets
30
- args: ['--baseline', '.secrets.baseline']
31
- exclude: package-lock.json
32
-
33
- - repo: local
34
- hooks:
35
- - id: pytest-coverage
36
- name: pytest-coverage
37
- entry: ./bin/run_tests.sh
38
- language: system
39
- pass_filenames: false
40
- always_run: true
41
-
42
- - id: commitizen
43
- name: commitizen check
44
- entry: uv run cz check --commit-msg-file
45
- language: system
46
- stages: [commit-msg]
@@ -1,80 +0,0 @@
1
- # 📖 Operational Usage Guide
2
-
3
- **Wraith** is designed to be intuitive and fast. Make a coffee, grab a Hob-Nob, and put down your glass rectangle — it's time to orchestrate.
4
-
5
- ---
6
-
7
- ## 🕹️ Core Commands
8
-
9
- ### 1. The Global Manifest
10
-
11
- To see all available commands, options, and the Sovereign splash screen:
12
-
13
- ```bash
14
- wraith --help
15
- ```
16
-
17
- ### 2. The Ghost Factory (Scaffolding)
18
-
19
- This is the flagship feature. Instead of manual setup, we spawn. This command clones your template, bleaches the history, provisions a Gitea repo, and pushes the initial commit in one atomic action.
20
-
21
- ```bash
22
- wraith spawn <project-name>
23
- ```
24
-
25
- > **Pro Tip:** Ensure your `GITEA_TOKEN` is set in your `.env` to avoid the manual
26
- > password hang.
27
-
28
- ### 3. Stack Observability
29
-
30
- Monitor your Sovereign containers without leaving the terminal.
31
-
32
- - **Process List:** `wraith ps` — Shows names, status, and images in Sovereign Dark style.
33
- - **Live Logs:** `wraith tail <service-name>` — Tails logs for a specific container.
34
- - **Heartbeat:** `wraith status` — Checks if the OpenViking API is responding.
35
-
36
- ---
37
-
38
- ## 🛠️ Maintenance & Defence
39
-
40
- ### 🔄 Staying Current
41
-
42
- Wraith is self-aware. To check for updates on PyPI and upgrade via `uv`:
43
-
44
- ```bash
45
- wraith update
46
- ```
47
-
48
- ### 🧹 Runner Recovery
49
-
50
- If a Gitea Action Runner hangs or goes "Offline" in the UI, use the nuclear option to wipe its registration and restart the service:
51
-
52
- ```bash
53
- wraith runner-reset
54
- ```
55
-
56
- > *Note: Requires `GITEA_COMPOSE_PATH` to be defined.*
57
-
58
- ---
59
-
60
- ## 🔐 Configuration
61
-
62
- Wraith prioritises environment variables for security. You can define these in a `.env` file in your project root or your global shell profile.
63
-
64
- | Variable | Description | Required For |
65
- |------------------------|------------------------------------------------------|----------------|
66
- | `GITEA_API_URL` | Your Gitea instance (e.g., `https://git.domain.com`) | `spawn` |
67
- | `GITEA_TOKEN` | Your Personal Access Token (PAT) | `spawn` |
68
- | `GITEA_TEMPLATE_URL` | HTTPS URL of your source `ghost-template` | `spawn` |
69
- | `WRAITH_COMPOSE_PATH` | Path to your main stack directory | `ps`, `tail` |
70
- | `VIKING_BASE_URL` | Heartbeat monitoring endpoint | `status` |
71
-
72
- ---
73
-
74
- ## ⌨️ Advanced Flags
75
-
76
- | Flag | Description |
77
- |-----------------------------------------|------------------------------------------|
78
- | `wraith --version` | Print the current installed version. |
79
- | `wraith ps --all` | Show all containers, including stopped. |
80
- | `wraith tail <svc> --path /custom/path` | Override the default Compose path. |
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes