hud-python 0.4.11__py3-none-any.whl → 0.4.13__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/__main__.py +8 -0
- hud/agents/base.py +7 -8
- hud/agents/langchain.py +2 -2
- hud/agents/tests/test_openai.py +3 -1
- hud/cli/__init__.py +114 -52
- hud/cli/build.py +121 -71
- hud/cli/debug.py +2 -2
- hud/cli/{mcp_server.py → dev.py} +101 -38
- hud/cli/eval.py +175 -90
- hud/cli/init.py +442 -64
- hud/cli/list_func.py +72 -71
- hud/cli/pull.py +1 -2
- hud/cli/push.py +35 -23
- hud/cli/remove.py +35 -41
- hud/cli/tests/test_analyze.py +2 -1
- hud/cli/tests/test_analyze_metadata.py +42 -49
- hud/cli/tests/test_build.py +28 -52
- hud/cli/tests/test_cursor.py +1 -1
- hud/cli/tests/test_debug.py +1 -1
- hud/cli/tests/test_list_func.py +75 -64
- hud/cli/tests/test_main_module.py +30 -0
- hud/cli/tests/test_mcp_server.py +3 -3
- hud/cli/tests/test_pull.py +30 -61
- hud/cli/tests/test_push.py +70 -89
- hud/cli/tests/test_registry.py +36 -38
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/utils/__init__.py +1 -0
- hud/cli/{docker_utils.py → utils/docker.py} +36 -0
- hud/cli/{env_utils.py → utils/environment.py} +7 -7
- hud/cli/{interactive.py → utils/interactive.py} +91 -19
- hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
- hud/cli/{registry.py → utils/registry.py} +28 -30
- hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
- hud/cli/utils/runner.py +134 -0
- hud/cli/utils/server.py +250 -0
- hud/clients/base.py +1 -1
- hud/clients/fastmcp.py +5 -13
- hud/clients/mcp_use.py +6 -10
- hud/server/server.py +35 -5
- hud/shared/exceptions.py +11 -0
- hud/shared/tests/test_exceptions.py +22 -0
- hud/telemetry/tests/__init__.py +0 -0
- hud/telemetry/tests/test_replay.py +40 -0
- hud/telemetry/tests/test_trace.py +63 -0
- hud/tools/base.py +20 -3
- hud/tools/computer/hud.py +15 -6
- hud/tools/executors/tests/test_base_executor.py +27 -0
- hud/tools/response.py +12 -8
- hud/tools/tests/test_response.py +60 -0
- hud/tools/tests/test_tools_init.py +49 -0
- hud/utils/design.py +19 -8
- hud/utils/mcp.py +17 -5
- hud/utils/tests/test_mcp.py +112 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/METADATA +16 -13
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/RECORD +62 -52
- hud/cli/runner.py +0 -160
- /hud/cli/{cursor.py → utils/cursor.py} +0 -0
- /hud/cli/{utils.py → utils/logging.py} +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/WHEEL +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/licenses/LICENSE +0 -0
hud/cli/list_func.py
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import re
|
|
6
5
|
from datetime import datetime
|
|
7
|
-
from pathlib import Path
|
|
8
6
|
|
|
9
7
|
import typer
|
|
10
8
|
import yaml
|
|
@@ -12,30 +10,33 @@ from rich.table import Table
|
|
|
12
10
|
|
|
13
11
|
from hud.utils.design import HUDDesign
|
|
14
12
|
|
|
15
|
-
from .registry import get_registry_dir, list_registry_entries
|
|
13
|
+
from .utils.registry import extract_name_and_tag, get_registry_dir, list_registry_entries
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
def format_timestamp(timestamp: float | None) -> str:
|
|
19
17
|
"""Format timestamp to human-readable relative time."""
|
|
20
18
|
if not timestamp:
|
|
21
19
|
return "unknown"
|
|
22
|
-
|
|
20
|
+
|
|
23
21
|
dt = datetime.fromtimestamp(timestamp)
|
|
24
22
|
now = datetime.now()
|
|
25
23
|
delta = now - dt
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
|
|
25
|
+
# Get total seconds to handle edge cases properly
|
|
26
|
+
total_seconds = delta.total_seconds()
|
|
27
|
+
|
|
28
|
+
if total_seconds < 60:
|
|
29
|
+
return "just now"
|
|
30
|
+
elif total_seconds < 3600:
|
|
31
|
+
return f"{int(total_seconds // 60)}m ago"
|
|
32
|
+
elif total_seconds < 86400: # Less than 24 hours
|
|
33
|
+
return f"{int(total_seconds // 3600)}h ago"
|
|
34
|
+
elif delta.days < 30:
|
|
32
35
|
return f"{delta.days}d ago"
|
|
33
|
-
elif delta.
|
|
34
|
-
return f"{delta.
|
|
35
|
-
elif delta.seconds > 60:
|
|
36
|
-
return f"{delta.seconds // 60}m ago"
|
|
36
|
+
elif delta.days < 365:
|
|
37
|
+
return f"{delta.days // 30}mo ago"
|
|
37
38
|
else:
|
|
38
|
-
return "
|
|
39
|
+
return f"{delta.days // 365}y ago"
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def list_environments(
|
|
@@ -46,71 +47,73 @@ def list_environments(
|
|
|
46
47
|
) -> None:
|
|
47
48
|
"""List all HUD environments in the local registry."""
|
|
48
49
|
design = HUDDesign()
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
if not json_output:
|
|
51
52
|
design.header("HUD Environment Registry")
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
# Check for environment directory
|
|
54
55
|
env_dir = get_registry_dir()
|
|
55
56
|
if not env_dir.exists():
|
|
56
57
|
if json_output:
|
|
57
|
-
print("[]")
|
|
58
|
+
print("[]") # noqa: T201
|
|
58
59
|
else:
|
|
59
60
|
design.info("No environments found in local registry.")
|
|
60
61
|
design.info("")
|
|
61
62
|
design.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
|
|
62
63
|
design.info("Build environments with: [cyan]hud build[/cyan]")
|
|
63
64
|
return
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
# Collect all environments using the registry helper
|
|
66
67
|
environments = []
|
|
67
|
-
|
|
68
|
+
|
|
68
69
|
for digest, lock_file in list_registry_entries():
|
|
69
70
|
try:
|
|
70
71
|
# Read lock file
|
|
71
72
|
with open(lock_file) as f:
|
|
72
73
|
lock_data = yaml.safe_load(f)
|
|
73
|
-
|
|
74
|
+
|
|
74
75
|
# Extract metadata
|
|
75
76
|
image = lock_data.get("image", "unknown")
|
|
76
77
|
name, tag = extract_name_and_tag(image)
|
|
77
|
-
|
|
78
|
+
|
|
78
79
|
# Apply filter if specified
|
|
79
80
|
if filter_name and filter_name.lower() not in name.lower():
|
|
80
81
|
continue
|
|
81
|
-
|
|
82
|
+
|
|
82
83
|
# Get additional metadata
|
|
83
84
|
metadata = lock_data.get("metadata", {})
|
|
84
85
|
description = metadata.get("description", "")
|
|
85
86
|
tools_count = len(metadata.get("tools", []))
|
|
86
|
-
|
|
87
|
+
|
|
87
88
|
# Get file modification time as pulled time
|
|
88
89
|
pulled_time = lock_file.stat().st_mtime
|
|
89
|
-
|
|
90
|
-
environments.append(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
|
|
91
|
+
environments.append(
|
|
92
|
+
{
|
|
93
|
+
"name": name,
|
|
94
|
+
"tag": tag,
|
|
95
|
+
"digest": digest,
|
|
96
|
+
"description": description,
|
|
97
|
+
"tools_count": tools_count,
|
|
98
|
+
"pulled_time": pulled_time,
|
|
99
|
+
"image": image,
|
|
100
|
+
"path": str(lock_file),
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
101
104
|
except Exception as e:
|
|
102
105
|
if verbose:
|
|
103
106
|
design.warning(f"Failed to read {lock_file}: {e}")
|
|
104
|
-
|
|
107
|
+
|
|
105
108
|
# Sort by pulled time (newest first)
|
|
106
109
|
environments.sort(key=lambda x: x["pulled_time"], reverse=True)
|
|
107
|
-
|
|
110
|
+
|
|
108
111
|
if json_output:
|
|
109
112
|
# Output as JSON
|
|
110
113
|
import json
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
|
|
115
|
+
json_data = [
|
|
116
|
+
{
|
|
114
117
|
"name": env["name"],
|
|
115
118
|
"tag": env["tag"],
|
|
116
119
|
"digest": env["digest"],
|
|
@@ -118,67 +121,71 @@ def list_environments(
|
|
|
118
121
|
"tools_count": env["tools_count"],
|
|
119
122
|
"pulled_time": env["pulled_time"],
|
|
120
123
|
"image": env["image"],
|
|
121
|
-
"path": env["path"],
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
+
"path": str(env["path"]).replace("\\", "/"), # Normalize path separators for JSON
|
|
125
|
+
}
|
|
126
|
+
for env in environments
|
|
127
|
+
]
|
|
128
|
+
print(json.dumps(json_data, indent=2)) # noqa: T201
|
|
124
129
|
return
|
|
125
|
-
|
|
130
|
+
|
|
126
131
|
if not environments:
|
|
127
132
|
design.info("No environments found matching criteria.")
|
|
128
133
|
design.info("")
|
|
129
134
|
design.info("Pull environments with: [cyan]hud pull <org/name:tag>[/cyan]")
|
|
130
135
|
design.info("Build environments with: [cyan]hud build[/cyan]")
|
|
131
136
|
return
|
|
132
|
-
|
|
137
|
+
|
|
133
138
|
# Create table
|
|
134
|
-
table = Table(
|
|
139
|
+
table = Table(
|
|
140
|
+
title=f"Found {len(environments)} environment{'s' if len(environments) != 1 else ''}"
|
|
141
|
+
)
|
|
135
142
|
table.add_column("Environment", style="cyan", no_wrap=True)
|
|
136
143
|
table.add_column("Description", style="white")
|
|
137
144
|
table.add_column("Tools", justify="right", style="yellow")
|
|
138
145
|
table.add_column("Pulled", style="dim")
|
|
139
|
-
|
|
146
|
+
|
|
140
147
|
if show_all or verbose:
|
|
141
148
|
table.add_column("Digest", style="dim")
|
|
142
|
-
|
|
149
|
+
|
|
143
150
|
# Add rows
|
|
144
151
|
for env in environments:
|
|
145
152
|
# Truncate description if too long
|
|
146
153
|
desc = env["description"]
|
|
147
154
|
if desc and len(desc) > 50 and not verbose:
|
|
148
155
|
desc = desc[:47] + "..."
|
|
149
|
-
|
|
156
|
+
|
|
150
157
|
# Combine name and tag for easy copying
|
|
151
158
|
full_ref = f"{env['name']}:{env['tag']}"
|
|
152
|
-
|
|
159
|
+
|
|
153
160
|
row = [
|
|
154
161
|
full_ref,
|
|
155
162
|
desc or "[dim]No description[/dim]",
|
|
156
163
|
str(env["tools_count"]),
|
|
157
164
|
format_timestamp(env["pulled_time"]),
|
|
158
165
|
]
|
|
159
|
-
|
|
166
|
+
|
|
160
167
|
if show_all or verbose:
|
|
161
168
|
row.append(env["digest"][:12])
|
|
162
|
-
|
|
169
|
+
|
|
163
170
|
table.add_row(*row)
|
|
164
|
-
|
|
165
|
-
design.print(table)
|
|
171
|
+
|
|
172
|
+
design.print(table) # type: ignore
|
|
166
173
|
design.info("")
|
|
167
|
-
|
|
174
|
+
|
|
168
175
|
# Show usage hints
|
|
169
176
|
design.section_title("Usage")
|
|
170
177
|
if environments:
|
|
171
178
|
# Use the most recently pulled environment as example
|
|
172
179
|
example_env = environments[0]
|
|
173
180
|
example_ref = f"{example_env['name']}:{example_env['tag']}"
|
|
174
|
-
|
|
181
|
+
|
|
175
182
|
design.info(f"Run an environment: [cyan]hud run {example_ref}[/cyan]")
|
|
176
183
|
design.info(f"Analyze tools: [cyan]hud analyze {example_ref}[/cyan]")
|
|
177
184
|
design.info(f"Debug server: [cyan]hud debug {example_ref}[/cyan]")
|
|
178
|
-
|
|
185
|
+
|
|
179
186
|
design.info("Pull more environments: [cyan]hud pull <org/name:tag>[/cyan]")
|
|
180
187
|
design.info("Build new environments: [cyan]hud build[/cyan]")
|
|
181
|
-
|
|
188
|
+
|
|
182
189
|
if verbose:
|
|
183
190
|
design.info("")
|
|
184
191
|
design.info(f"[dim]Registry location: {env_dir}[/dim]")
|
|
@@ -188,21 +195,15 @@ def list_command(
|
|
|
188
195
|
filter_name: str | None = typer.Option(
|
|
189
196
|
None, "--filter", "-f", help="Filter environments by name (case-insensitive)"
|
|
190
197
|
),
|
|
191
|
-
json_output: bool = typer.Option(
|
|
192
|
-
|
|
193
|
-
),
|
|
194
|
-
show_all: bool = typer.Option(
|
|
195
|
-
False, "--all", "-a", help="Show all columns including digest"
|
|
196
|
-
),
|
|
197
|
-
verbose: bool = typer.Option(
|
|
198
|
-
False, "--verbose", "-v", help="Show detailed output"
|
|
199
|
-
),
|
|
198
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
199
|
+
show_all: bool = typer.Option(False, "--all", "-a", help="Show all columns including digest"),
|
|
200
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
200
201
|
) -> None:
|
|
201
202
|
"""📋 List all HUD environments in local registry.
|
|
202
|
-
|
|
203
|
+
|
|
203
204
|
Shows environments pulled with 'hud pull' or built with 'hud build',
|
|
204
205
|
stored in ~/.hud/envs/
|
|
205
|
-
|
|
206
|
+
|
|
206
207
|
Examples:
|
|
207
208
|
hud list # List all environments
|
|
208
209
|
hud list --filter text # Filter by name
|
|
@@ -210,4 +211,4 @@ def list_command(
|
|
|
210
211
|
hud list --all # Show digest column
|
|
211
212
|
hud list --verbose # Show full descriptions
|
|
212
213
|
"""
|
|
213
|
-
list_environments(filter_name, json_output, show_all, verbose)
|
|
214
|
+
list_environments(filter_name, json_output, show_all, verbose)
|
hud/cli/pull.py
CHANGED
|
@@ -6,7 +6,6 @@ import subprocess
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from urllib.parse import quote
|
|
8
8
|
|
|
9
|
-
import click
|
|
10
9
|
import requests
|
|
11
10
|
import typer
|
|
12
11
|
import yaml
|
|
@@ -15,7 +14,7 @@ from rich.table import Table
|
|
|
15
14
|
from hud.settings import settings
|
|
16
15
|
from hud.utils.design import HUDDesign
|
|
17
16
|
|
|
18
|
-
from .registry import save_to_registry
|
|
17
|
+
from .utils.registry import save_to_registry
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
def get_docker_manifest(image: str) -> dict | None:
|
hud/cli/push.py
CHANGED
|
@@ -21,6 +21,7 @@ def _get_response_text(response: requests.Response) -> str:
|
|
|
21
21
|
except Exception:
|
|
22
22
|
return response.text
|
|
23
23
|
|
|
24
|
+
|
|
24
25
|
def get_docker_username() -> str | None:
|
|
25
26
|
"""Get the current Docker username if logged in."""
|
|
26
27
|
try:
|
|
@@ -53,10 +54,10 @@ def get_docker_username() -> str | None:
|
|
|
53
54
|
username = decoded.split(":", 1)[0]
|
|
54
55
|
if username and username != "token": # Skip token-based auth
|
|
55
56
|
return username
|
|
56
|
-
except Exception:
|
|
57
|
-
pass
|
|
58
|
-
except Exception:
|
|
59
|
-
pass
|
|
57
|
+
except Exception: # noqa: S110
|
|
58
|
+
pass
|
|
59
|
+
except Exception: # noqa: S110
|
|
60
|
+
pass
|
|
60
61
|
|
|
61
62
|
# Alternative: Check credsStore/credHelpers
|
|
62
63
|
for config_path in config_paths:
|
|
@@ -91,12 +92,12 @@ def get_docker_username() -> str | None:
|
|
|
91
92
|
username = cred_data.get("Username", "")
|
|
92
93
|
if username and username != "token":
|
|
93
94
|
return username
|
|
94
|
-
except Exception:
|
|
95
|
-
pass
|
|
96
|
-
except Exception:
|
|
97
|
-
pass
|
|
98
|
-
except Exception:
|
|
99
|
-
pass
|
|
95
|
+
except Exception: # noqa: S110
|
|
96
|
+
pass
|
|
97
|
+
except Exception: # noqa: S110
|
|
98
|
+
pass
|
|
99
|
+
except Exception: # noqa: S110
|
|
100
|
+
pass
|
|
100
101
|
return None
|
|
101
102
|
|
|
102
103
|
|
|
@@ -155,7 +156,7 @@ def push_environment(
|
|
|
155
156
|
if not local_image and "build" in lock_data:
|
|
156
157
|
# New format might have image elsewhere
|
|
157
158
|
local_image = lock_data.get("image", "")
|
|
158
|
-
|
|
159
|
+
|
|
159
160
|
# Get internal version from lock file
|
|
160
161
|
internal_version = lock_data.get("build", {}).get("version", None)
|
|
161
162
|
|
|
@@ -206,21 +207,25 @@ def push_environment(
|
|
|
206
207
|
# Handle tag when image is provided
|
|
207
208
|
# Prefer explicit tag over internal version
|
|
208
209
|
final_tag = tag if tag else internal_version
|
|
209
|
-
|
|
210
|
+
|
|
210
211
|
if ":" in image:
|
|
211
212
|
# Image already has a tag
|
|
212
213
|
existing_tag = image.split(":")[-1]
|
|
213
214
|
if existing_tag != final_tag:
|
|
214
215
|
if tag:
|
|
215
|
-
design.warning(
|
|
216
|
+
design.warning(
|
|
217
|
+
f"Image already has tag '{existing_tag}', overriding with '{final_tag}'"
|
|
218
|
+
)
|
|
216
219
|
else:
|
|
217
|
-
design.info(
|
|
220
|
+
design.info(
|
|
221
|
+
f"Image has tag '{existing_tag}', but using internal version '{final_tag}'"
|
|
222
|
+
)
|
|
218
223
|
image = image.rsplit(":", 1)[0] + f":{final_tag}"
|
|
219
224
|
# else: tags match, no action needed
|
|
220
225
|
else:
|
|
221
226
|
# Image has no tag, append the appropriate one
|
|
222
227
|
image = f"{image}:{final_tag}"
|
|
223
|
-
|
|
228
|
+
|
|
224
229
|
if tag:
|
|
225
230
|
design.info(f"Using specified tag: {tag}")
|
|
226
231
|
else:
|
|
@@ -230,7 +235,7 @@ def push_environment(
|
|
|
230
235
|
# Verify local image exists
|
|
231
236
|
# Extract the tag part (before @sha256:...) for Docker operations
|
|
232
237
|
local_tag = local_image.split("@")[0] if "@" in local_image else local_image
|
|
233
|
-
|
|
238
|
+
|
|
234
239
|
# Also check for version-tagged image if we have internal version
|
|
235
240
|
version_tag = None
|
|
236
241
|
if internal_version and ":" in local_tag:
|
|
@@ -246,7 +251,7 @@ def push_environment(
|
|
|
246
251
|
design.info(f"Found version-tagged image: {version_tag}")
|
|
247
252
|
except subprocess.CalledProcessError:
|
|
248
253
|
pass
|
|
249
|
-
|
|
254
|
+
|
|
250
255
|
if not image_to_push:
|
|
251
256
|
try:
|
|
252
257
|
subprocess.run(["docker", "inspect", local_tag], capture_output=True, check=True) # noqa: S603, S607
|
|
@@ -319,7 +324,7 @@ def push_environment(
|
|
|
319
324
|
lock_data["image"] = pushed_digest
|
|
320
325
|
|
|
321
326
|
# Add push information
|
|
322
|
-
from datetime import
|
|
327
|
+
from datetime import UTC, datetime
|
|
323
328
|
|
|
324
329
|
lock_data["push"] = {
|
|
325
330
|
"source": local_image,
|
|
@@ -340,13 +345,20 @@ def push_environment(
|
|
|
340
345
|
# e.g., "hudpython/test_init:v1.0" -> "hudpython/test_init:v1.0"
|
|
341
346
|
# Use the original image name for the registry path, not the digest
|
|
342
347
|
# The digest might not contain the tag information
|
|
343
|
-
registry_image =
|
|
344
|
-
|
|
348
|
+
registry_image = (
|
|
349
|
+
image # This is the image we tagged and pushed (e.g., hudpython/hud-text-2048:0.1.2)
|
|
350
|
+
)
|
|
351
|
+
|
|
345
352
|
# Remove any registry prefix for the HUD registry path
|
|
346
353
|
registry_parts = registry_image.split("/")
|
|
347
354
|
if len(registry_parts) >= 2:
|
|
348
355
|
# Handle docker.io/org/name or just org/name
|
|
349
|
-
if registry_parts[0] in [
|
|
356
|
+
if registry_parts[0] in [
|
|
357
|
+
"docker.io",
|
|
358
|
+
"registry-1.docker.io",
|
|
359
|
+
"index.docker.io",
|
|
360
|
+
"ghcr.io",
|
|
361
|
+
]:
|
|
350
362
|
# Remove registry prefix
|
|
351
363
|
name_with_tag = "/".join(registry_parts[1:])
|
|
352
364
|
elif "." in registry_parts[0] or ":" in registry_parts[0]:
|
|
@@ -359,12 +371,12 @@ def push_environment(
|
|
|
359
371
|
name_with_tag = registry_image
|
|
360
372
|
|
|
361
373
|
# The image variable already has the tag, no need to add :latest
|
|
362
|
-
|
|
374
|
+
|
|
363
375
|
# Validate the image format
|
|
364
376
|
if not name_with_tag:
|
|
365
377
|
design.warning("Could not determine image name for registry upload")
|
|
366
378
|
raise typer.Exit(0)
|
|
367
|
-
|
|
379
|
+
|
|
368
380
|
# For HUD registry, we need org/name format
|
|
369
381
|
if "/" not in name_with_tag:
|
|
370
382
|
design.warning("Image name must include organization/namespace for HUD registry")
|
hud/cli/remove.py
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import shutil
|
|
6
|
-
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
import typer
|
|
9
|
-
import yaml
|
|
10
8
|
|
|
11
9
|
from hud.utils.design import HUDDesign
|
|
12
10
|
|
|
13
|
-
from .registry import get_registry_dir, list_registry_entries, load_from_registry
|
|
11
|
+
from .utils.registry import get_registry_dir, list_registry_entries, load_from_registry
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
def remove_environment(
|
|
@@ -21,18 +19,18 @@ def remove_environment(
|
|
|
21
19
|
"""Remove an environment from the local registry."""
|
|
22
20
|
design = HUDDesign()
|
|
23
21
|
design.header("HUD Environment Removal")
|
|
24
|
-
|
|
22
|
+
|
|
25
23
|
# Find the environment to remove
|
|
26
24
|
found_entry = None
|
|
27
25
|
found_digest = None
|
|
28
|
-
|
|
26
|
+
|
|
29
27
|
# First check if target is a digest
|
|
30
28
|
for digest, lock_file in list_registry_entries():
|
|
31
29
|
if digest.startswith(target):
|
|
32
30
|
found_entry = lock_file
|
|
33
31
|
found_digest = digest
|
|
34
32
|
break
|
|
35
|
-
|
|
33
|
+
|
|
36
34
|
# If not found by digest, search by name
|
|
37
35
|
if not found_entry:
|
|
38
36
|
for digest, lock_file in list_registry_entries():
|
|
@@ -42,28 +40,29 @@ def remove_environment(
|
|
|
42
40
|
image = lock_data["image"]
|
|
43
41
|
# Extract name and tag
|
|
44
42
|
name = image.split("@")[0] if "@" in image else image
|
|
45
|
-
if "/" in name:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
except Exception:
|
|
43
|
+
if "/" in name and (target in name or name.endswith(f"/{target}")):
|
|
44
|
+
found_entry = lock_file
|
|
45
|
+
found_digest = digest
|
|
46
|
+
break
|
|
47
|
+
except Exception as e:
|
|
48
|
+
design.error(f"Error loading lock file: {e}")
|
|
52
49
|
continue
|
|
53
|
-
|
|
50
|
+
|
|
54
51
|
if not found_entry:
|
|
55
52
|
design.error(f"Environment not found: {target}")
|
|
56
53
|
design.info("Use 'hud list' to see available environments")
|
|
57
54
|
raise typer.Exit(1)
|
|
58
|
-
|
|
55
|
+
|
|
59
56
|
# Load and display environment info
|
|
60
57
|
try:
|
|
58
|
+
if found_digest is None:
|
|
59
|
+
raise ValueError("Found digest is None")
|
|
61
60
|
lock_data = load_from_registry(found_digest)
|
|
62
61
|
if lock_data:
|
|
63
62
|
image = lock_data.get("image", "unknown")
|
|
64
63
|
metadata = lock_data.get("metadata", {})
|
|
65
64
|
description = metadata.get("description", "No description")
|
|
66
|
-
|
|
65
|
+
|
|
67
66
|
design.section_title("Environment Details")
|
|
68
67
|
design.status_item("Image", image)
|
|
69
68
|
design.status_item("Digest", found_digest)
|
|
@@ -72,20 +71,20 @@ def remove_environment(
|
|
|
72
71
|
except Exception as e:
|
|
73
72
|
if verbose:
|
|
74
73
|
design.warning(f"Could not read environment details: {e}")
|
|
75
|
-
|
|
74
|
+
|
|
76
75
|
# Confirm deletion
|
|
77
76
|
if not yes:
|
|
78
77
|
design.info("")
|
|
79
78
|
if not typer.confirm(f"Remove environment {found_digest}?"):
|
|
80
79
|
design.info("Aborted")
|
|
81
80
|
raise typer.Exit(0)
|
|
82
|
-
|
|
81
|
+
|
|
83
82
|
# Remove the environment directory
|
|
84
83
|
try:
|
|
85
84
|
env_dir = found_entry.parent
|
|
86
85
|
shutil.rmtree(env_dir)
|
|
87
86
|
design.success(f"Removed environment: {found_digest}")
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
# Check if the image is still available locally
|
|
90
89
|
if lock_data:
|
|
91
90
|
image = lock_data.get("image", "")
|
|
@@ -95,7 +94,7 @@ def remove_environment(
|
|
|
95
94
|
design.info(f"To remove it, run: [cyan]docker rmi {image.split('@')[0]}[/cyan]")
|
|
96
95
|
except Exception as e:
|
|
97
96
|
design.error(f"Failed to remove environment: {e}")
|
|
98
|
-
raise typer.Exit(1)
|
|
97
|
+
raise typer.Exit(1) from e
|
|
99
98
|
|
|
100
99
|
|
|
101
100
|
def remove_all_environments(
|
|
@@ -105,23 +104,23 @@ def remove_all_environments(
|
|
|
105
104
|
"""Remove all environments from the local registry."""
|
|
106
105
|
design = HUDDesign()
|
|
107
106
|
design.header("Remove All HUD Environments")
|
|
108
|
-
|
|
107
|
+
|
|
109
108
|
registry_dir = get_registry_dir()
|
|
110
109
|
if not registry_dir.exists():
|
|
111
110
|
design.info("No environments found in local registry.")
|
|
112
111
|
return
|
|
113
|
-
|
|
112
|
+
|
|
114
113
|
# Count environments
|
|
115
114
|
entries = list(list_registry_entries())
|
|
116
115
|
if not entries:
|
|
117
116
|
design.info("No environments found in local registry.")
|
|
118
117
|
return
|
|
119
|
-
|
|
118
|
+
|
|
120
119
|
design.warning(f"This will remove {len(entries)} environment(s) from the local registry!")
|
|
121
|
-
|
|
120
|
+
|
|
122
121
|
# List environments that will be removed
|
|
123
122
|
design.section_title("Environments to Remove")
|
|
124
|
-
for digest,
|
|
123
|
+
for digest, _ in entries:
|
|
125
124
|
try:
|
|
126
125
|
lock_data = load_from_registry(digest)
|
|
127
126
|
if lock_data:
|
|
@@ -129,18 +128,18 @@ def remove_all_environments(
|
|
|
129
128
|
design.info(f" • {digest[:12]} - {image}")
|
|
130
129
|
except Exception:
|
|
131
130
|
design.info(f" • {digest[:12]}")
|
|
132
|
-
|
|
131
|
+
|
|
133
132
|
# Confirm deletion
|
|
134
133
|
if not yes:
|
|
135
134
|
design.info("")
|
|
136
135
|
if not typer.confirm("Remove ALL environments?", default=False):
|
|
137
136
|
design.info("Aborted")
|
|
138
137
|
raise typer.Exit(0)
|
|
139
|
-
|
|
138
|
+
|
|
140
139
|
# Remove all environments
|
|
141
140
|
removed = 0
|
|
142
141
|
failed = 0
|
|
143
|
-
|
|
142
|
+
|
|
144
143
|
for digest, lock_file in entries:
|
|
145
144
|
try:
|
|
146
145
|
env_dir = lock_file.parent
|
|
@@ -152,13 +151,13 @@ def remove_all_environments(
|
|
|
152
151
|
failed += 1
|
|
153
152
|
if verbose:
|
|
154
153
|
design.error(f"Failed to remove {digest}: {e}")
|
|
155
|
-
|
|
154
|
+
|
|
156
155
|
design.info("")
|
|
157
156
|
if failed == 0:
|
|
158
157
|
design.success(f"Successfully removed {removed} environment(s)")
|
|
159
158
|
else:
|
|
160
159
|
design.warning(f"Removed {removed} environment(s), failed to remove {failed}")
|
|
161
|
-
|
|
160
|
+
|
|
162
161
|
design.info("")
|
|
163
162
|
design.info("Note: Docker images may still exist locally.")
|
|
164
163
|
design.info("To remove them, use: [cyan]docker image prune[/cyan]")
|
|
@@ -166,21 +165,16 @@ def remove_all_environments(
|
|
|
166
165
|
|
|
167
166
|
def remove_command(
|
|
168
167
|
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"
|
|
168
|
+
None, help="Environment to remove (digest, name, or 'all' for all environments)"
|
|
177
169
|
),
|
|
170
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
171
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
178
172
|
) -> None:
|
|
179
173
|
"""🗑️ Remove HUD environments from local registry.
|
|
180
|
-
|
|
174
|
+
|
|
181
175
|
Removes environment metadata from ~/.hud/envs/
|
|
182
176
|
Note: This does not remove the Docker images.
|
|
183
|
-
|
|
177
|
+
|
|
184
178
|
Examples:
|
|
185
179
|
hud remove abc123 # Remove by digest
|
|
186
180
|
hud remove text_2048 # Remove by name
|
|
@@ -193,7 +187,7 @@ def remove_command(
|
|
|
193
187
|
design.error("Please specify an environment to remove or 'all'")
|
|
194
188
|
design.info("Use 'hud list' to see available environments")
|
|
195
189
|
raise typer.Exit(1)
|
|
196
|
-
|
|
190
|
+
|
|
197
191
|
if target.lower() == "all":
|
|
198
192
|
remove_all_environments(yes, verbose)
|
|
199
193
|
else:
|
hud/cli/tests/test_analyze.py
CHANGED
|
@@ -82,6 +82,7 @@ class TestAnalyzeEnvironment:
|
|
|
82
82
|
with (
|
|
83
83
|
patch("hud.cli.analyze.MCPClient") as MockClient,
|
|
84
84
|
patch("hud.cli.analyze.console") as mock_console,
|
|
85
|
+
patch("platform.system", return_value="Windows"),
|
|
85
86
|
):
|
|
86
87
|
# Setup mock client that will raise exception during initialization
|
|
87
88
|
mock_client = MagicMock()
|
|
@@ -100,7 +101,7 @@ class TestAnalyzeEnvironment:
|
|
|
100
101
|
mock_client.initialize.assert_called_once()
|
|
101
102
|
mock_client.shutdown.assert_called_once()
|
|
102
103
|
|
|
103
|
-
# Check console printed error hints
|
|
104
|
+
# Check console printed Windows-specific error hints
|
|
104
105
|
calls = mock_console.print.call_args_list
|
|
105
106
|
assert any("Docker logs may not show on Windows" in str(call) for call in calls)
|
|
106
107
|
|