hud-python 0.4.8__py3-none-any.whl → 0.4.10__py3-none-any.whl
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.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/agents/base.py +50 -1
- hud/cli/__init__.py +187 -11
- hud/cli/analyze_metadata.py +33 -42
- hud/cli/build.py +7 -0
- hud/cli/debug.py +8 -1
- hud/cli/env_utils.py +133 -0
- hud/cli/eval.py +302 -0
- hud/cli/list_func.py +213 -0
- hud/cli/mcp_server.py +3 -79
- hud/cli/pull.py +20 -15
- hud/cli/push.py +84 -41
- hud/cli/registry.py +155 -0
- hud/cli/remove.py +200 -0
- hud/cli/runner.py +1 -1
- hud/cli/tests/test_analyze_metadata.py +277 -0
- hud/cli/tests/test_build.py +450 -0
- hud/cli/tests/test_list_func.py +288 -0
- hud/cli/tests/test_pull.py +400 -0
- hud/cli/tests/test_push.py +379 -0
- hud/cli/tests/test_registry.py +264 -0
- hud/clients/base.py +13 -1
- hud/tools/__init__.py +2 -0
- hud/tools/response.py +54 -0
- hud/utils/design.py +10 -0
- hud/utils/mcp.py +14 -2
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/METADATA +12 -1
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/RECORD +32 -20
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/WHEEL +0 -0
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.8.dist-info → hud_python-0.4.10.dist-info}/licenses/LICENSE +0 -0
hud/cli/push.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import subprocess
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from urllib.parse import quote
|
|
8
9
|
|
|
9
10
|
import requests
|
|
10
11
|
import typer
|
|
@@ -14,6 +15,12 @@ from hud.settings import settings
|
|
|
14
15
|
from hud.utils.design import HUDDesign
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def _get_response_text(response: requests.Response) -> str:
|
|
19
|
+
try:
|
|
20
|
+
return response.json().get("detail", "No detail available")
|
|
21
|
+
except Exception:
|
|
22
|
+
return response.text
|
|
23
|
+
|
|
17
24
|
def get_docker_username() -> str | None:
|
|
18
25
|
"""Get the current Docker username if logged in."""
|
|
19
26
|
try:
|
|
@@ -312,11 +319,11 @@ def push_environment(
|
|
|
312
319
|
lock_data["image"] = pushed_digest
|
|
313
320
|
|
|
314
321
|
# Add push information
|
|
315
|
-
from datetime import datetime
|
|
322
|
+
from datetime import datetime, UTC
|
|
316
323
|
|
|
317
324
|
lock_data["push"] = {
|
|
318
325
|
"source": local_image,
|
|
319
|
-
"pushedAt": datetime.
|
|
326
|
+
"pushedAt": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
320
327
|
"registry": pushed_digest.split("/")[0] if "/" in pushed_digest else "docker.io",
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -331,52 +338,88 @@ def push_environment(
|
|
|
331
338
|
# Extract org/name:tag from the pushed image
|
|
332
339
|
# e.g., "docker.io/hudpython/test_init:latest@sha256:..." -> "hudpython/test_init:latest"
|
|
333
340
|
# e.g., "hudpython/test_init:v1.0" -> "hudpython/test_init:v1.0"
|
|
334
|
-
|
|
341
|
+
# Use the original image name for the registry path, not the digest
|
|
342
|
+
# The digest might not contain the tag information
|
|
343
|
+
registry_image = image # This is the image we tagged and pushed (e.g., hudpython/hud-text-2048:0.1.2)
|
|
344
|
+
|
|
345
|
+
# Remove any registry prefix for the HUD registry path
|
|
346
|
+
registry_parts = registry_image.split("/")
|
|
335
347
|
if len(registry_parts) >= 2:
|
|
336
348
|
# Handle docker.io/org/name or just org/name
|
|
337
|
-
if registry_parts[0] in ["docker.io", "registry-1.docker.io", "index.docker.io"]:
|
|
338
|
-
# Remove registry prefix
|
|
339
|
-
name_with_tag = "/".join(registry_parts[1:])
|
|
349
|
+
if registry_parts[0] in ["docker.io", "registry-1.docker.io", "index.docker.io", "ghcr.io"]:
|
|
350
|
+
# Remove registry prefix
|
|
351
|
+
name_with_tag = "/".join(registry_parts[1:])
|
|
352
|
+
elif "." in registry_parts[0] or ":" in registry_parts[0]:
|
|
353
|
+
# Likely a registry URL (has dots or port)
|
|
354
|
+
name_with_tag = "/".join(registry_parts[1:])
|
|
340
355
|
else:
|
|
341
|
-
#
|
|
342
|
-
name_with_tag =
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if ":" not in name_with_tag:
|
|
346
|
-
name_with_tag = f"{name_with_tag}:latest"
|
|
347
|
-
|
|
348
|
-
# Upload to HUD registry
|
|
349
|
-
design.progress_message("Uploading metadata to HUD registry...")
|
|
350
|
-
|
|
351
|
-
registry_url = f"{settings.hud_telemetry_url.rstrip('/')}/registry/envs/{name_with_tag}"
|
|
352
|
-
|
|
353
|
-
# Prepare the payload
|
|
354
|
-
payload = {
|
|
355
|
-
"lock": yaml.dump(lock_data, default_flow_style=False, sort_keys=False),
|
|
356
|
-
"digest": pushed_digest.split("@")[-1] if "@" in pushed_digest else "latest",
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
headers = {"Authorization": f"Bearer {settings.api_key}"}
|
|
360
|
-
|
|
361
|
-
response = requests.post(registry_url, json=payload, headers=headers, timeout=10)
|
|
356
|
+
# No registry prefix, use as-is
|
|
357
|
+
name_with_tag = registry_image
|
|
358
|
+
else:
|
|
359
|
+
name_with_tag = registry_image
|
|
362
360
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
361
|
+
# The image variable already has the tag, no need to add :latest
|
|
362
|
+
|
|
363
|
+
# Validate the image format
|
|
364
|
+
if not name_with_tag:
|
|
365
|
+
design.warning("Could not determine image name for registry upload")
|
|
366
|
+
raise typer.Exit(0)
|
|
367
|
+
|
|
368
|
+
# For HUD registry, we need org/name format
|
|
369
|
+
if "/" not in name_with_tag:
|
|
370
|
+
design.warning("Image name must include organization/namespace for HUD registry")
|
|
371
|
+
design.info(f"Current format: {name_with_tag}")
|
|
372
|
+
design.info("Expected format: org/name:tag (e.g., hudpython/myenv:v1.0)")
|
|
373
|
+
design.info("\nYour Docker push succeeded - share hud.lock.yaml manually")
|
|
374
|
+
raise typer.Exit(0)
|
|
375
|
+
|
|
376
|
+
# Upload to HUD registry
|
|
377
|
+
design.progress_message("Uploading metadata to HUD registry...")
|
|
378
|
+
|
|
379
|
+
# URL-encode the path segments to handle special characters in tags
|
|
380
|
+
url_safe_path = "/".join(quote(part, safe="") for part in name_with_tag.split("/"))
|
|
381
|
+
registry_url = f"{settings.hud_telemetry_url.rstrip('/')}/registry/envs/{url_safe_path}"
|
|
382
|
+
|
|
383
|
+
# Prepare the payload
|
|
384
|
+
payload = {
|
|
385
|
+
"lock": yaml.dump(lock_data, default_flow_style=False, sort_keys=False),
|
|
386
|
+
"digest": pushed_digest.split("@")[-1] if "@" in pushed_digest else None,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
headers = {"Authorization": f"Bearer {settings.api_key}"}
|
|
390
|
+
|
|
391
|
+
response = requests.post(registry_url, json=payload, headers=headers, timeout=10)
|
|
392
|
+
|
|
393
|
+
if response.status_code in [200, 201]:
|
|
394
|
+
design.success("Metadata uploaded to HUD registry")
|
|
395
|
+
design.info("Others can now pull with:")
|
|
396
|
+
design.command_example(f"hud pull {name_with_tag}")
|
|
397
|
+
design.info("")
|
|
398
|
+
elif response.status_code == 401:
|
|
399
|
+
design.error("Authentication failed")
|
|
400
|
+
design.info("Check your HUD_API_KEY is valid")
|
|
401
|
+
design.info("Get a new key at: https://hud.so/settings")
|
|
402
|
+
elif response.status_code == 403:
|
|
403
|
+
design.error("Permission denied")
|
|
404
|
+
design.info("You may not have access to push to this namespace")
|
|
405
|
+
elif response.status_code == 409:
|
|
406
|
+
design.warning("This version already exists in the registry")
|
|
407
|
+
design.info("Consider using a different tag if you want to update")
|
|
373
408
|
else:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
design.info("Share hud.lock.yaml
|
|
409
|
+
design.warning(f"Could not upload to registry: {response.status_code}")
|
|
410
|
+
design.warning(_get_response_text(response))
|
|
411
|
+
design.info("Share hud.lock.yaml manually\n")
|
|
412
|
+
except requests.exceptions.Timeout:
|
|
413
|
+
design.warning("Registry upload timed out")
|
|
414
|
+
design.info("The registry might be slow or unavailable")
|
|
415
|
+
design.info("Your Docker push succeeded - share hud.lock.yaml manually")
|
|
416
|
+
except requests.exceptions.ConnectionError:
|
|
417
|
+
design.warning("Could not connect to HUD registry")
|
|
418
|
+
design.info("Check your internet connection")
|
|
419
|
+
design.info("Your Docker push succeeded - share hud.lock.yaml manually")
|
|
377
420
|
except Exception as e:
|
|
378
421
|
design.warning(f"Registry upload failed: {e}")
|
|
379
|
-
design.info("Share hud.lock.yaml manually
|
|
422
|
+
design.info("Share hud.lock.yaml manually")
|
|
380
423
|
|
|
381
424
|
# Show usage examples
|
|
382
425
|
design.section_title("What's Next?")
|
hud/cli/registry.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Local registry management for HUD environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Optional, Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from hud.utils.design import HUDDesign
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_registry_dir() -> Path:
|
|
14
|
+
"""Get the base directory for the local HUD registry."""
|
|
15
|
+
return Path.home() / ".hud" / "envs"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_digest_from_image(image_ref: str) -> str:
|
|
19
|
+
"""Extract a digest identifier from a Docker image reference.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
image_ref: Docker image reference (e.g., "image:tag@sha256:abc123...")
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Digest string for use as directory name (max 12 chars)
|
|
26
|
+
"""
|
|
27
|
+
if "@sha256:" in image_ref:
|
|
28
|
+
# Extract from digest reference: image@sha256:abc123...
|
|
29
|
+
return image_ref.split("@sha256:")[-1][:12]
|
|
30
|
+
elif image_ref.startswith("sha256:"):
|
|
31
|
+
# Direct digest format
|
|
32
|
+
return image_ref.split(":")[-1][:12]
|
|
33
|
+
elif ":" in image_ref and "/" not in image_ref.split(":")[-1]:
|
|
34
|
+
# Has a tag but no slashes after colon (not a port)
|
|
35
|
+
tag = image_ref.split(":")[-1]
|
|
36
|
+
return tag[:12] if tag else "latest"
|
|
37
|
+
else:
|
|
38
|
+
# No tag specified or complex format
|
|
39
|
+
return "latest"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_name_and_tag(image_ref: str) -> tuple[str, str]:
|
|
43
|
+
"""Extract organization/name and tag from Docker image reference.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
image_ref: Docker image reference
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple of (name, tag) where name includes org/repo
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
docker.io/hudpython/test_init:latest@sha256:... -> (hudpython/test_init, latest)
|
|
53
|
+
hudpython/myenv:v1.0 -> (hudpython/myenv, v1.0)
|
|
54
|
+
myorg/myapp -> (myorg/myapp, latest)
|
|
55
|
+
"""
|
|
56
|
+
# Remove digest if present
|
|
57
|
+
if "@" in image_ref:
|
|
58
|
+
image_ref = image_ref.split("@")[0]
|
|
59
|
+
|
|
60
|
+
# Remove registry prefix if present
|
|
61
|
+
if image_ref.startswith(("docker.io/", "registry-1.docker.io/", "index.docker.io/")):
|
|
62
|
+
image_ref = "/".join(image_ref.split("/")[1:])
|
|
63
|
+
|
|
64
|
+
# Extract tag
|
|
65
|
+
if ":" in image_ref:
|
|
66
|
+
name, tag = image_ref.rsplit(":", 1)
|
|
67
|
+
else:
|
|
68
|
+
name = image_ref
|
|
69
|
+
tag = "latest"
|
|
70
|
+
|
|
71
|
+
return name, tag
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_to_registry(
|
|
75
|
+
lock_data: Dict[str, Any],
|
|
76
|
+
image_ref: str,
|
|
77
|
+
verbose: bool = False
|
|
78
|
+
) -> Optional[Path]:
|
|
79
|
+
"""Save environment lock data to the local registry.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
lock_data: The lock file data to save
|
|
83
|
+
image_ref: Docker image reference for digest extraction
|
|
84
|
+
verbose: Whether to show verbose output
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Path to the saved lock file, or None if save failed
|
|
88
|
+
"""
|
|
89
|
+
design = HUDDesign()
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Extract digest for registry storage
|
|
93
|
+
digest = extract_digest_from_image(image_ref)
|
|
94
|
+
|
|
95
|
+
# Store under ~/.hud/envs/<digest>/
|
|
96
|
+
local_env_dir = get_registry_dir() / digest
|
|
97
|
+
local_env_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
local_lock_path = local_env_dir / "hud.lock.yaml"
|
|
100
|
+
with open(local_lock_path, "w") as f:
|
|
101
|
+
yaml.dump(lock_data, f, default_flow_style=False, sort_keys=False)
|
|
102
|
+
|
|
103
|
+
design.success(f"Added to local registry: {digest}")
|
|
104
|
+
if verbose:
|
|
105
|
+
design.info(f"Registry location: {local_lock_path}")
|
|
106
|
+
|
|
107
|
+
return local_lock_path
|
|
108
|
+
except Exception as e:
|
|
109
|
+
if verbose:
|
|
110
|
+
design.warning(f"Failed to save to registry: {e}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_from_registry(digest: str) -> Optional[Dict[str, Any]]:
|
|
115
|
+
"""Load environment lock data from the local registry.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
digest: The digest/identifier of the environment
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Lock data dictionary, or None if not found
|
|
122
|
+
"""
|
|
123
|
+
lock_path = get_registry_dir() / digest / "hud.lock.yaml"
|
|
124
|
+
|
|
125
|
+
if not lock_path.exists():
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with open(lock_path) as f:
|
|
130
|
+
return yaml.safe_load(f)
|
|
131
|
+
except Exception:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def list_registry_entries() -> list[tuple[str, Path]]:
|
|
136
|
+
"""List all entries in the local registry.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of (digest, lock_path) tuples
|
|
140
|
+
"""
|
|
141
|
+
registry_dir = get_registry_dir()
|
|
142
|
+
|
|
143
|
+
if not registry_dir.exists():
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
entries = []
|
|
147
|
+
for digest_dir in registry_dir.iterdir():
|
|
148
|
+
if not digest_dir.is_dir():
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
lock_file = digest_dir / "hud.lock.yaml"
|
|
152
|
+
if lock_file.exists():
|
|
153
|
+
entries.append((digest_dir.name, lock_file))
|
|
154
|
+
|
|
155
|
+
return entries
|
hud/cli/remove.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Remove HUD environments from local registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from hud.utils.design import HUDDesign
|
|
12
|
+
|
|
13
|
+
from .registry import get_registry_dir, list_registry_entries, load_from_registry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def remove_environment(
|
|
17
|
+
target: str,
|
|
18
|
+
yes: bool = False,
|
|
19
|
+
verbose: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Remove an environment from the local registry."""
|
|
22
|
+
design = HUDDesign()
|
|
23
|
+
design.header("HUD Environment Removal")
|
|
24
|
+
|
|
25
|
+
# Find the environment to remove
|
|
26
|
+
found_entry = None
|
|
27
|
+
found_digest = None
|
|
28
|
+
|
|
29
|
+
# First check if target is a digest
|
|
30
|
+
for digest, lock_file in list_registry_entries():
|
|
31
|
+
if digest.startswith(target):
|
|
32
|
+
found_entry = lock_file
|
|
33
|
+
found_digest = digest
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
# If not found by digest, search by name
|
|
37
|
+
if not found_entry:
|
|
38
|
+
for digest, lock_file in list_registry_entries():
|
|
39
|
+
try:
|
|
40
|
+
lock_data = load_from_registry(digest)
|
|
41
|
+
if lock_data and "image" in lock_data:
|
|
42
|
+
image = lock_data["image"]
|
|
43
|
+
# Extract name and tag
|
|
44
|
+
name = image.split("@")[0] if "@" in image else image
|
|
45
|
+
if "/" in name:
|
|
46
|
+
# Match by repo/name or just name
|
|
47
|
+
if target in name or name.endswith(f"/{target}"):
|
|
48
|
+
found_entry = lock_file
|
|
49
|
+
found_digest = digest
|
|
50
|
+
break
|
|
51
|
+
except Exception:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if not found_entry:
|
|
55
|
+
design.error(f"Environment not found: {target}")
|
|
56
|
+
design.info("Use 'hud list' to see available environments")
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
|
|
59
|
+
# Load and display environment info
|
|
60
|
+
try:
|
|
61
|
+
lock_data = load_from_registry(found_digest)
|
|
62
|
+
if lock_data:
|
|
63
|
+
image = lock_data.get("image", "unknown")
|
|
64
|
+
metadata = lock_data.get("metadata", {})
|
|
65
|
+
description = metadata.get("description", "No description")
|
|
66
|
+
|
|
67
|
+
design.section_title("Environment Details")
|
|
68
|
+
design.status_item("Image", image)
|
|
69
|
+
design.status_item("Digest", found_digest)
|
|
70
|
+
design.status_item("Description", description)
|
|
71
|
+
design.status_item("Location", str(found_entry.parent))
|
|
72
|
+
except Exception as e:
|
|
73
|
+
if verbose:
|
|
74
|
+
design.warning(f"Could not read environment details: {e}")
|
|
75
|
+
|
|
76
|
+
# Confirm deletion
|
|
77
|
+
if not yes:
|
|
78
|
+
design.info("")
|
|
79
|
+
if not typer.confirm(f"Remove environment {found_digest}?"):
|
|
80
|
+
design.info("Aborted")
|
|
81
|
+
raise typer.Exit(0)
|
|
82
|
+
|
|
83
|
+
# Remove the environment directory
|
|
84
|
+
try:
|
|
85
|
+
env_dir = found_entry.parent
|
|
86
|
+
shutil.rmtree(env_dir)
|
|
87
|
+
design.success(f"Removed environment: {found_digest}")
|
|
88
|
+
|
|
89
|
+
# Check if the image is still available locally
|
|
90
|
+
if lock_data:
|
|
91
|
+
image = lock_data.get("image", "")
|
|
92
|
+
if image:
|
|
93
|
+
design.info("")
|
|
94
|
+
design.info("Note: The Docker image may still exist locally.")
|
|
95
|
+
design.info(f"To remove it, run: [cyan]docker rmi {image.split('@')[0]}[/cyan]")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
design.error(f"Failed to remove environment: {e}")
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def remove_all_environments(
|
|
102
|
+
yes: bool = False,
|
|
103
|
+
verbose: bool = False,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Remove all environments from the local registry."""
|
|
106
|
+
design = HUDDesign()
|
|
107
|
+
design.header("Remove All HUD Environments")
|
|
108
|
+
|
|
109
|
+
registry_dir = get_registry_dir()
|
|
110
|
+
if not registry_dir.exists():
|
|
111
|
+
design.info("No environments found in local registry.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Count environments
|
|
115
|
+
entries = list(list_registry_entries())
|
|
116
|
+
if not entries:
|
|
117
|
+
design.info("No environments found in local registry.")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
design.warning(f"This will remove {len(entries)} environment(s) from the local registry!")
|
|
121
|
+
|
|
122
|
+
# List environments that will be removed
|
|
123
|
+
design.section_title("Environments to Remove")
|
|
124
|
+
for digest, lock_file in entries:
|
|
125
|
+
try:
|
|
126
|
+
lock_data = load_from_registry(digest)
|
|
127
|
+
if lock_data:
|
|
128
|
+
image = lock_data.get("image", "unknown")
|
|
129
|
+
design.info(f" • {digest[:12]} - {image}")
|
|
130
|
+
except Exception:
|
|
131
|
+
design.info(f" • {digest[:12]}")
|
|
132
|
+
|
|
133
|
+
# Confirm deletion
|
|
134
|
+
if not yes:
|
|
135
|
+
design.info("")
|
|
136
|
+
if not typer.confirm("Remove ALL environments?", default=False):
|
|
137
|
+
design.info("Aborted")
|
|
138
|
+
raise typer.Exit(0)
|
|
139
|
+
|
|
140
|
+
# Remove all environments
|
|
141
|
+
removed = 0
|
|
142
|
+
failed = 0
|
|
143
|
+
|
|
144
|
+
for digest, lock_file in entries:
|
|
145
|
+
try:
|
|
146
|
+
env_dir = lock_file.parent
|
|
147
|
+
shutil.rmtree(env_dir)
|
|
148
|
+
removed += 1
|
|
149
|
+
if verbose:
|
|
150
|
+
design.success(f"Removed: {digest}")
|
|
151
|
+
except Exception as e:
|
|
152
|
+
failed += 1
|
|
153
|
+
if verbose:
|
|
154
|
+
design.error(f"Failed to remove {digest}: {e}")
|
|
155
|
+
|
|
156
|
+
design.info("")
|
|
157
|
+
if failed == 0:
|
|
158
|
+
design.success(f"Successfully removed {removed} environment(s)")
|
|
159
|
+
else:
|
|
160
|
+
design.warning(f"Removed {removed} environment(s), failed to remove {failed}")
|
|
161
|
+
|
|
162
|
+
design.info("")
|
|
163
|
+
design.info("Note: Docker images may still exist locally.")
|
|
164
|
+
design.info("To remove them, use: [cyan]docker image prune[/cyan]")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def remove_command(
|
|
168
|
+
target: str | None = typer.Argument(
|
|
169
|
+
None,
|
|
170
|
+
help="Environment to remove (digest, name, or 'all' for all environments)"
|
|
171
|
+
),
|
|
172
|
+
yes: bool = typer.Option(
|
|
173
|
+
False, "--yes", "-y", help="Skip confirmation prompt"
|
|
174
|
+
),
|
|
175
|
+
verbose: bool = typer.Option(
|
|
176
|
+
False, "--verbose", "-v", help="Show detailed output"
|
|
177
|
+
),
|
|
178
|
+
) -> None:
|
|
179
|
+
"""🗑️ Remove HUD environments from local registry.
|
|
180
|
+
|
|
181
|
+
Removes environment metadata from ~/.hud/envs/
|
|
182
|
+
Note: This does not remove the Docker images.
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
hud remove abc123 # Remove by digest
|
|
186
|
+
hud remove text_2048 # Remove by name
|
|
187
|
+
hud remove hudpython/test_init # Remove by full name
|
|
188
|
+
hud remove all # Remove all environments
|
|
189
|
+
hud remove all --yes # Remove all without confirmation
|
|
190
|
+
"""
|
|
191
|
+
if not target:
|
|
192
|
+
design = HUDDesign()
|
|
193
|
+
design.error("Please specify an environment to remove or 'all'")
|
|
194
|
+
design.info("Use 'hud list' to see available environments")
|
|
195
|
+
raise typer.Exit(1)
|
|
196
|
+
|
|
197
|
+
if target.lower() == "all":
|
|
198
|
+
remove_all_environments(yes, verbose)
|
|
199
|
+
else:
|
|
200
|
+
remove_environment(target, yes, verbose)
|
hud/cli/runner.py
CHANGED
|
@@ -14,7 +14,7 @@ from hud.utils.design import HUDDesign
|
|
|
14
14
|
|
|
15
15
|
def run_stdio_server(image: str, docker_args: list[str], verbose: bool) -> None:
|
|
16
16
|
"""Run Docker image as stdio MCP server (direct passthrough)."""
|
|
17
|
-
design = HUDDesign(
|
|
17
|
+
design = HUDDesign() # Use stderr for stdio mode
|
|
18
18
|
|
|
19
19
|
# Build docker command
|
|
20
20
|
docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
|