hud-python 0.4.10__py3-none-any.whl → 0.4.12__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 +106 -51
- hud/cli/build.py +121 -71
- hud/cli/debug.py +2 -2
- hud/cli/{mcp_server.py → dev.py} +60 -25
- hud/cli/eval.py +148 -68
- hud/cli/init.py +0 -1
- 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 +7 -5
- hud/clients/mcp_use.py +8 -6
- hud/server/server.py +34 -4
- 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 +15 -4
- 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.10.dist-info → hud_python-0.4.12.dist-info}/METADATA +14 -10
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.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.10.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/licenses/LICENSE +0 -0
hud/cli/eval.py
CHANGED
|
@@ -6,18 +6,13 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Any, Literal
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
13
|
import hud
|
|
14
14
|
from hud.utils.design import HUDDesign
|
|
15
15
|
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
from datasets import Dataset
|
|
18
|
-
from hud.agents import ClaudeAgent, OperatorAgent
|
|
19
|
-
from hud.agents.misc.response_agent import ResponseAgent
|
|
20
|
-
|
|
21
16
|
logger = logging.getLogger(__name__)
|
|
22
17
|
design = HUDDesign()
|
|
23
18
|
|
|
@@ -29,7 +24,7 @@ def build_agent(
|
|
|
29
24
|
allowed_tools: list[str] | None = None,
|
|
30
25
|
) -> Any:
|
|
31
26
|
"""Create and return the requested agent type."""
|
|
32
|
-
|
|
27
|
+
|
|
33
28
|
# Import agents lazily to avoid dependency issues
|
|
34
29
|
try:
|
|
35
30
|
from hud.agents.misc.response_agent import ResponseAgent
|
|
@@ -39,7 +34,7 @@ def build_agent(
|
|
|
39
34
|
"Please install with: pip install 'hud-python[agent]'"
|
|
40
35
|
)
|
|
41
36
|
raise typer.Exit(1) from e
|
|
42
|
-
|
|
37
|
+
|
|
43
38
|
if agent_type == "openai":
|
|
44
39
|
try:
|
|
45
40
|
from hud.agents import OperatorAgent
|
|
@@ -49,14 +44,14 @@ def build_agent(
|
|
|
49
44
|
"Please install with: pip install 'hud-python[agent]'"
|
|
50
45
|
)
|
|
51
46
|
raise typer.Exit(1) from e
|
|
52
|
-
|
|
47
|
+
|
|
53
48
|
allowed_tools = allowed_tools or ["openai_computer"]
|
|
54
|
-
|
|
49
|
+
|
|
55
50
|
return OperatorAgent(
|
|
56
51
|
allowed_tools=allowed_tools,
|
|
57
52
|
response_agent=ResponseAgent(),
|
|
58
53
|
)
|
|
59
|
-
|
|
54
|
+
|
|
60
55
|
# Fallback Claude agent (Anthropic)
|
|
61
56
|
try:
|
|
62
57
|
from hud.agents import ClaudeAgent
|
|
@@ -66,10 +61,10 @@ def build_agent(
|
|
|
66
61
|
"Please install with: pip install 'hud-python[agent]'"
|
|
67
62
|
)
|
|
68
63
|
raise typer.Exit(1) from e
|
|
69
|
-
|
|
64
|
+
|
|
70
65
|
model = model or "claude-sonnet-4-20250514"
|
|
71
66
|
allowed_tools = allowed_tools or ["anthropic_computer"]
|
|
72
|
-
|
|
67
|
+
|
|
73
68
|
return ClaudeAgent(
|
|
74
69
|
model=model,
|
|
75
70
|
allowed_tools=allowed_tools,
|
|
@@ -85,26 +80,82 @@ async def run_single_task(
|
|
|
85
80
|
allowed_tools: list[str] | None = None,
|
|
86
81
|
max_steps: int = 10,
|
|
87
82
|
) -> None:
|
|
88
|
-
"""Load one task and execute it."""
|
|
89
|
-
|
|
83
|
+
"""Load one task and execute it, or detect if JSON contains a list and run as dataset."""
|
|
84
|
+
|
|
90
85
|
design.info("📊 Loading dataset…")
|
|
91
|
-
|
|
92
|
-
# Import Task lazily
|
|
86
|
+
|
|
87
|
+
# Import Task and run_dataset lazily
|
|
93
88
|
try:
|
|
94
|
-
from hud.datasets import Task
|
|
89
|
+
from hud.datasets import Task, run_dataset
|
|
95
90
|
except ImportError as e:
|
|
96
91
|
design.error(
|
|
97
92
|
"Dataset dependencies are not installed. "
|
|
98
93
|
"Please install with: pip install 'hud-python[agent]'"
|
|
99
94
|
)
|
|
100
95
|
raise typer.Exit(1) from e
|
|
101
|
-
|
|
102
|
-
# Check if it's a
|
|
96
|
+
|
|
97
|
+
# Check if it's a JSON file
|
|
103
98
|
path = Path(source)
|
|
104
99
|
if path.exists() and path.suffix == ".json":
|
|
105
|
-
with open(path
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
with open(path) as f: # noqa: ASYNC230
|
|
101
|
+
json_data = json.load(f)
|
|
102
|
+
|
|
103
|
+
# Check if JSON contains a list of tasks
|
|
104
|
+
if isinstance(json_data, list):
|
|
105
|
+
design.info(f"Found {len(json_data)} tasks in JSON file, running as dataset…")
|
|
106
|
+
|
|
107
|
+
# Build agent class and config for run_dataset
|
|
108
|
+
if agent_type == "openai":
|
|
109
|
+
try:
|
|
110
|
+
from hud.agents import OperatorAgent
|
|
111
|
+
|
|
112
|
+
agent_class = OperatorAgent
|
|
113
|
+
except ImportError as e:
|
|
114
|
+
design.error(
|
|
115
|
+
"OpenAI agent dependencies are not installed. "
|
|
116
|
+
"Please install with: pip install 'hud-python[agent]'"
|
|
117
|
+
)
|
|
118
|
+
raise typer.Exit(1) from e
|
|
119
|
+
|
|
120
|
+
agent_config: dict[str, Any] = {
|
|
121
|
+
"allowed_tools": allowed_tools or ["openai_computer"],
|
|
122
|
+
}
|
|
123
|
+
else:
|
|
124
|
+
try:
|
|
125
|
+
from hud.agents import ClaudeAgent
|
|
126
|
+
|
|
127
|
+
agent_class = ClaudeAgent
|
|
128
|
+
except ImportError as e:
|
|
129
|
+
design.error(
|
|
130
|
+
"Claude agent dependencies are not installed. "
|
|
131
|
+
"Please install with: pip install 'hud-python[agent]'"
|
|
132
|
+
)
|
|
133
|
+
raise typer.Exit(1) from e
|
|
134
|
+
|
|
135
|
+
agent_config = {
|
|
136
|
+
"model": model or "claude-sonnet-4-20250514",
|
|
137
|
+
"allowed_tools": allowed_tools or ["anthropic_computer"],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Run as dataset with single-task concurrency to maintain debug behavior
|
|
141
|
+
results = await run_dataset(
|
|
142
|
+
name=f"JSON Dataset: {path.name}",
|
|
143
|
+
dataset=json_data, # Pass the list directly
|
|
144
|
+
agent_class=agent_class,
|
|
145
|
+
agent_config=agent_config,
|
|
146
|
+
max_concurrent=1, # Run sequentially for debug mode
|
|
147
|
+
metadata={"source": str(path)},
|
|
148
|
+
max_steps=max_steps,
|
|
149
|
+
auto_respond=True,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Display summary
|
|
153
|
+
successful = sum(1 for r in results if getattr(r, "reward", 0) > 0)
|
|
154
|
+
design.success(f"Completed {len(results)} tasks: {successful} successful")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Single task JSON
|
|
158
|
+
task = Task(**json_data)
|
|
108
159
|
else:
|
|
109
160
|
# Load from HuggingFace dataset
|
|
110
161
|
try:
|
|
@@ -115,15 +166,15 @@ async def run_single_task(
|
|
|
115
166
|
"Please install with: pip install 'hud-python[agent]'"
|
|
116
167
|
)
|
|
117
168
|
raise typer.Exit(1) from e
|
|
118
|
-
|
|
169
|
+
|
|
119
170
|
dataset = load_dataset(source, split="train")
|
|
120
|
-
|
|
171
|
+
|
|
121
172
|
# Get first task from dataset
|
|
122
173
|
sample_task = dataset[0] # type: ignore[index]
|
|
123
174
|
task = Task(**sample_task) # type: ignore[arg-type]
|
|
124
|
-
|
|
175
|
+
|
|
125
176
|
task_prompt = task.prompt[:50] + "..." if len(task.prompt) > 50 else task.prompt
|
|
126
|
-
|
|
177
|
+
|
|
127
178
|
with hud.trace(name=task_prompt):
|
|
128
179
|
agent = build_agent(
|
|
129
180
|
agent_type,
|
|
@@ -145,7 +196,7 @@ async def run_full_dataset(
|
|
|
145
196
|
max_steps: int = 50,
|
|
146
197
|
) -> list[Any]:
|
|
147
198
|
"""Run evaluation across the entire dataset using hud.datasets.run_dataset."""
|
|
148
|
-
|
|
199
|
+
|
|
149
200
|
# Import run_dataset lazily
|
|
150
201
|
try:
|
|
151
202
|
from hud.datasets import run_dataset
|
|
@@ -155,11 +206,29 @@ async def run_full_dataset(
|
|
|
155
206
|
"Please install with: pip install 'hud-python[agent]'"
|
|
156
207
|
)
|
|
157
208
|
raise typer.Exit(1) from e
|
|
158
|
-
|
|
209
|
+
|
|
210
|
+
# Check if source is a JSON file with list of tasks
|
|
211
|
+
path = Path(source)
|
|
212
|
+
dataset_or_tasks = source
|
|
213
|
+
dataset_name = source.split("/")[-1]
|
|
214
|
+
|
|
215
|
+
if path.exists() and path.suffix == ".json":
|
|
216
|
+
with open(path) as f: # noqa: ASYNC230
|
|
217
|
+
json_data = json.load(f)
|
|
218
|
+
|
|
219
|
+
if isinstance(json_data, list):
|
|
220
|
+
dataset_or_tasks = json_data
|
|
221
|
+
dataset_name = f"JSON Dataset: {path.name}"
|
|
222
|
+
design.info(f"Found {len(json_data)} tasks in JSON file")
|
|
223
|
+
else:
|
|
224
|
+
design.error("JSON file must contain a list of tasks when using --full flag")
|
|
225
|
+
raise typer.Exit(1)
|
|
226
|
+
|
|
159
227
|
# Build agent class + config for run_dataset
|
|
160
228
|
if agent_type == "openai":
|
|
161
229
|
try:
|
|
162
230
|
from hud.agents import OperatorAgent
|
|
231
|
+
|
|
163
232
|
agent_class = OperatorAgent
|
|
164
233
|
except ImportError as e:
|
|
165
234
|
design.error(
|
|
@@ -167,13 +236,14 @@ async def run_full_dataset(
|
|
|
167
236
|
"Please install with: pip install 'hud-python[agent]'"
|
|
168
237
|
)
|
|
169
238
|
raise typer.Exit(1) from e
|
|
170
|
-
|
|
239
|
+
|
|
171
240
|
agent_config: dict[str, Any] = {
|
|
172
241
|
"allowed_tools": allowed_tools or ["openai_computer"],
|
|
173
242
|
}
|
|
174
243
|
else:
|
|
175
244
|
try:
|
|
176
245
|
from hud.agents import ClaudeAgent
|
|
246
|
+
|
|
177
247
|
agent_class = ClaudeAgent
|
|
178
248
|
except ImportError as e:
|
|
179
249
|
design.error(
|
|
@@ -181,16 +251,16 @@ async def run_full_dataset(
|
|
|
181
251
|
"Please install with: pip install 'hud-python[agent]'"
|
|
182
252
|
)
|
|
183
253
|
raise typer.Exit(1) from e
|
|
184
|
-
|
|
254
|
+
|
|
185
255
|
agent_config = {
|
|
186
256
|
"model": model or "claude-sonnet-4-20250514",
|
|
187
257
|
"allowed_tools": allowed_tools or ["anthropic_computer"],
|
|
188
258
|
}
|
|
189
|
-
|
|
259
|
+
|
|
190
260
|
design.info("🚀 Running evaluation…")
|
|
191
261
|
return await run_dataset(
|
|
192
|
-
name=f"Evaluation {
|
|
193
|
-
dataset=
|
|
262
|
+
name=f"Evaluation {dataset_name}",
|
|
263
|
+
dataset=dataset_or_tasks,
|
|
194
264
|
agent_class=agent_class,
|
|
195
265
|
agent_config=agent_config,
|
|
196
266
|
max_concurrent=max_concurrent,
|
|
@@ -203,7 +273,7 @@ async def run_full_dataset(
|
|
|
203
273
|
def eval_command(
|
|
204
274
|
source: str = typer.Argument(
|
|
205
275
|
...,
|
|
206
|
-
help="HuggingFace dataset identifier (e.g. 'hud-evals/SheetBench-50')
|
|
276
|
+
help="HuggingFace dataset identifier (e.g. 'hud-evals/SheetBench-50'), single task JSON file, or JSON file with list of tasks", # noqa: E501
|
|
207
277
|
),
|
|
208
278
|
full: bool = typer.Option(
|
|
209
279
|
False,
|
|
@@ -237,66 +307,76 @@ def eval_command(
|
|
|
237
307
|
),
|
|
238
308
|
) -> None:
|
|
239
309
|
"""🚀 Run evaluation on datasets or individual tasks with agents.
|
|
240
|
-
|
|
310
|
+
|
|
241
311
|
Examples:
|
|
242
312
|
# Evaluate a single task from SheetBench
|
|
243
313
|
hud eval hud-evals/SheetBench-50
|
|
244
|
-
|
|
314
|
+
|
|
245
315
|
# Evaluate the FULL SheetBench dataset with Claude
|
|
246
316
|
hud eval hud-evals/SheetBench-50 --full --agent claude
|
|
247
|
-
|
|
317
|
+
|
|
248
318
|
# Run a single task from a JSON file
|
|
249
319
|
hud eval task.json
|
|
250
|
-
|
|
320
|
+
|
|
321
|
+
# Run multiple tasks from a JSON file (auto-detects list)
|
|
322
|
+
hud eval tasks.json # If tasks.json contains a list, runs all tasks
|
|
323
|
+
|
|
324
|
+
# Run JSON list with full dataset mode and concurrency
|
|
325
|
+
hud eval tasks.json --full --max-concurrent 10
|
|
326
|
+
|
|
251
327
|
# Run with OpenAI Operator agent
|
|
252
328
|
hud eval hud-evals/OSWorld-Gold-Beta --agent openai
|
|
253
329
|
"""
|
|
254
|
-
from hud.settings import settings
|
|
255
330
|
import os
|
|
256
|
-
|
|
331
|
+
|
|
332
|
+
from hud.settings import settings
|
|
333
|
+
|
|
257
334
|
# Check for required API keys
|
|
258
335
|
if agent == "claude":
|
|
259
336
|
if not settings.anthropic_api_key or not os.environ.get("ANTHROPIC_API_KEY"):
|
|
260
337
|
design.error("ANTHROPIC_API_KEY is required for Claude agent")
|
|
261
338
|
design.info("Set it in your environment or .env file: ANTHROPIC_API_KEY=your-key-here")
|
|
262
339
|
raise typer.Exit(1)
|
|
263
|
-
elif agent == "openai"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
340
|
+
elif agent == "openai" and (
|
|
341
|
+
not settings.openai_api_key or not os.environ.get("OPENAI_API_KEY")
|
|
342
|
+
):
|
|
343
|
+
design.error("OPENAI_API_KEY is required for OpenAI agent")
|
|
344
|
+
design.info("Set it in your environment or .env file: OPENAI_API_KEY=your-key-here")
|
|
345
|
+
raise typer.Exit(1)
|
|
346
|
+
|
|
269
347
|
# Check for HUD_API_KEY if using HUD services
|
|
270
348
|
if not settings.api_key or not os.environ.get("HUD_API_KEY"):
|
|
271
349
|
design.warning("HUD_API_KEY not set. Some features may be limited.")
|
|
272
350
|
design.info("Get your API key at: https://app.hud.so")
|
|
273
|
-
|
|
351
|
+
|
|
274
352
|
# Parse allowed tools
|
|
275
353
|
allowed_tools_list = (
|
|
276
|
-
[t.strip() for t in allowed_tools.split(",") if t.strip()]
|
|
277
|
-
if allowed_tools
|
|
278
|
-
else None
|
|
354
|
+
[t.strip() for t in allowed_tools.split(",") if t.strip()] if allowed_tools else None
|
|
279
355
|
)
|
|
280
|
-
|
|
356
|
+
|
|
281
357
|
# Set default max_steps if not provided
|
|
282
358
|
if max_steps is None:
|
|
283
359
|
max_steps = 50 if full else 10
|
|
284
|
-
|
|
360
|
+
|
|
285
361
|
# Run evaluation
|
|
286
362
|
if full:
|
|
287
|
-
asyncio.run(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
363
|
+
asyncio.run(
|
|
364
|
+
run_full_dataset(
|
|
365
|
+
source,
|
|
366
|
+
agent_type=agent,
|
|
367
|
+
model=model,
|
|
368
|
+
allowed_tools=allowed_tools_list,
|
|
369
|
+
max_concurrent=max_concurrent,
|
|
370
|
+
max_steps=max_steps,
|
|
371
|
+
)
|
|
372
|
+
)
|
|
295
373
|
else:
|
|
296
|
-
asyncio.run(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
374
|
+
asyncio.run(
|
|
375
|
+
run_single_task(
|
|
376
|
+
source,
|
|
377
|
+
agent_type=agent,
|
|
378
|
+
model=model,
|
|
379
|
+
allowed_tools=allowed_tools_list,
|
|
380
|
+
max_steps=max_steps,
|
|
381
|
+
)
|
|
382
|
+
)
|
hud/cli/init.py
CHANGED
|
@@ -173,7 +173,6 @@ def sanitize_name(name: str) -> str:
|
|
|
173
173
|
|
|
174
174
|
def create_environment(name: str | None, directory: str, force: bool) -> None:
|
|
175
175
|
"""Create a new HUD environment from templates."""
|
|
176
|
-
from hud.utils.design import HUDDesign
|
|
177
176
|
|
|
178
177
|
design = HUDDesign()
|
|
179
178
|
|
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(str(table))
|
|
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:
|