hud-python 0.4.1__py3-none-any.whl → 0.4.3__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/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/cli/pull.py
CHANGED
|
@@ -1,336 +1,330 @@
|
|
|
1
|
-
"""Pull HUD environments from registry."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import subprocess
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import click
|
|
9
|
-
import requests
|
|
10
|
-
import typer
|
|
11
|
-
import yaml
|
|
12
|
-
from rich.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from hud.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if lock_path
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if
|
|
146
|
-
design.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
table.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
table.add_row("
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if "
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
#
|
|
239
|
-
if lock_data and
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
design.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
False, "--verify-only", help="Only verify metadata without pulling"
|
|
332
|
-
),
|
|
333
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
334
|
-
) -> None:
|
|
335
|
-
"""Pull HUD environment from registry with metadata preview."""
|
|
336
|
-
pull_environment(target, lock_file, yes, verify_only, verbose)
|
|
1
|
+
"""Pull HUD environments from registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import requests
|
|
10
|
+
import typer
|
|
11
|
+
import yaml
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from hud.settings import settings
|
|
15
|
+
from hud.utils.design import HUDDesign
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_docker_manifest(image: str) -> dict | None:
|
|
19
|
+
"""Get manifest from Docker registry without pulling the image."""
|
|
20
|
+
try:
|
|
21
|
+
# Try docker manifest inspect (requires experimental features)
|
|
22
|
+
result = subprocess.run( # noqa: S603
|
|
23
|
+
["docker", "manifest", "inspect", image], # noqa: S607
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
)
|
|
27
|
+
if result.returncode == 0:
|
|
28
|
+
import json
|
|
29
|
+
|
|
30
|
+
return json.loads(result.stdout)
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_image_size_from_manifest(manifest: dict) -> int | None:
|
|
36
|
+
"""Extract total image size from Docker manifest."""
|
|
37
|
+
try:
|
|
38
|
+
total_size = 0
|
|
39
|
+
|
|
40
|
+
# Handle different manifest formats
|
|
41
|
+
if "layers" in manifest:
|
|
42
|
+
# v2 manifest
|
|
43
|
+
for layer in manifest["layers"]:
|
|
44
|
+
total_size += layer.get("size", 0)
|
|
45
|
+
elif "manifests" in manifest:
|
|
46
|
+
first_manifest = manifest["manifests"][0]
|
|
47
|
+
total_size = first_manifest.get("size", 0)
|
|
48
|
+
|
|
49
|
+
return total_size if total_size > 0 else None
|
|
50
|
+
except Exception:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fetch_lock_from_registry(reference: str) -> dict | None:
|
|
55
|
+
"""Fetch lock file from HUD registry."""
|
|
56
|
+
try:
|
|
57
|
+
# Reference should be org/name:tag format
|
|
58
|
+
# If no tag specified, append :latest
|
|
59
|
+
if "/" in reference and ":" not in reference:
|
|
60
|
+
reference = f"{reference}:latest"
|
|
61
|
+
|
|
62
|
+
registry_url = f"{settings.hud_telemetry_url.rstrip('/')}/registry/envs/{reference}"
|
|
63
|
+
|
|
64
|
+
headers = {}
|
|
65
|
+
if settings.api_key:
|
|
66
|
+
headers["Authorization"] = f"Bearer {settings.api_key}"
|
|
67
|
+
|
|
68
|
+
response = requests.get(registry_url, headers=headers, timeout=10)
|
|
69
|
+
|
|
70
|
+
if response.status_code == 200:
|
|
71
|
+
data = response.json()
|
|
72
|
+
# Parse the lock YAML from the response
|
|
73
|
+
if "lock" in data:
|
|
74
|
+
return yaml.safe_load(data["lock"])
|
|
75
|
+
elif "lock_data" in data:
|
|
76
|
+
return data["lock_data"]
|
|
77
|
+
else:
|
|
78
|
+
# Try to treat the whole response as lock data
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
except Exception:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_size(size_bytes: int) -> str:
|
|
87
|
+
"""Format bytes to human readable size."""
|
|
88
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
89
|
+
if size_bytes < 1024:
|
|
90
|
+
return f"{size_bytes:.1f} {unit}"
|
|
91
|
+
size_bytes //= 1024
|
|
92
|
+
return f"{size_bytes:.1f} TB"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def pull_environment(
|
|
96
|
+
target: str,
|
|
97
|
+
lock_file: str | None = None,
|
|
98
|
+
yes: bool = False,
|
|
99
|
+
verify_only: bool = False,
|
|
100
|
+
verbose: bool = False,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Pull HUD environment from registry."""
|
|
103
|
+
design = HUDDesign()
|
|
104
|
+
design.header("HUD Environment Pull")
|
|
105
|
+
|
|
106
|
+
# Two modes:
|
|
107
|
+
# 1. Pull from lock file (recommended)
|
|
108
|
+
# 2. Pull from image reference directly
|
|
109
|
+
|
|
110
|
+
lock_data = None
|
|
111
|
+
image_ref = target
|
|
112
|
+
|
|
113
|
+
# Mode 1: Lock file provided
|
|
114
|
+
if lock_file or target.endswith((".yaml", ".yml")):
|
|
115
|
+
# If target looks like a lock file, use it
|
|
116
|
+
if target.endswith((".yaml", ".yml")):
|
|
117
|
+
lock_file = target
|
|
118
|
+
|
|
119
|
+
lock_path = Path(lock_file) if lock_file else None
|
|
120
|
+
if lock_path and not lock_path.exists():
|
|
121
|
+
design.error(f"Lock file not found: {lock_file}")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
design.info(f"Reading lock file: {lock_file}")
|
|
125
|
+
if lock_path:
|
|
126
|
+
with open(lock_path) as f:
|
|
127
|
+
lock_data = yaml.safe_load(f)
|
|
128
|
+
|
|
129
|
+
image_ref = lock_data.get("image", "") if lock_data else ""
|
|
130
|
+
|
|
131
|
+
# Mode 2: Direct image reference
|
|
132
|
+
else:
|
|
133
|
+
# First, try to parse as org/env reference for HUD registry
|
|
134
|
+
# Check if it's a simple org/name or org/name:tag format (no @sha256)
|
|
135
|
+
if "/" in target and "@" not in target:
|
|
136
|
+
# Looks like org/env reference, possibly with tag
|
|
137
|
+
design.info(f"Checking HUD registry for: {target}")
|
|
138
|
+
|
|
139
|
+
# Check for API key (not required for pulling, but good to inform)
|
|
140
|
+
if not settings.api_key:
|
|
141
|
+
design.info("No HUD API key set (pulling from public registry)")
|
|
142
|
+
|
|
143
|
+
lock_data = fetch_lock_from_registry(target)
|
|
144
|
+
|
|
145
|
+
if lock_data:
|
|
146
|
+
design.success("Found in HUD registry")
|
|
147
|
+
image_ref = lock_data.get("image", "")
|
|
148
|
+
else:
|
|
149
|
+
# Fall back to treating as Docker image
|
|
150
|
+
if not settings.api_key:
|
|
151
|
+
design.info(
|
|
152
|
+
"Not found in HUD registry (try setting HUD_API_KEY for private environments)" # noqa: E501
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
design.info("Not found in HUD registry, treating as Docker image")
|
|
156
|
+
|
|
157
|
+
# Try to get metadata from Docker registry
|
|
158
|
+
if not lock_data:
|
|
159
|
+
design.info(f"Fetching Docker metadata for: {image_ref}")
|
|
160
|
+
manifest = get_docker_manifest(image_ref)
|
|
161
|
+
|
|
162
|
+
if manifest:
|
|
163
|
+
# Create minimal lock data from manifest
|
|
164
|
+
lock_data = {"image": image_ref, "source": "docker-manifest"}
|
|
165
|
+
|
|
166
|
+
# Try to get size
|
|
167
|
+
size = get_image_size_from_manifest(manifest)
|
|
168
|
+
if size:
|
|
169
|
+
lock_data["size"] = format_size(size)
|
|
170
|
+
|
|
171
|
+
if verbose:
|
|
172
|
+
design.info(
|
|
173
|
+
f"Retrieved manifest (type: {manifest.get('mediaType', 'unknown')})"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Display environment summary
|
|
177
|
+
design.section_title("Environment Details")
|
|
178
|
+
|
|
179
|
+
# Create summary table
|
|
180
|
+
table = Table(show_header=False, box=None)
|
|
181
|
+
table.add_column("Property", style="cyan")
|
|
182
|
+
table.add_column("Value")
|
|
183
|
+
|
|
184
|
+
# Image info - show simple name in table
|
|
185
|
+
display_ref = image_ref.split("@")[0] if ":" in image_ref and "@" in image_ref else image_ref
|
|
186
|
+
table.add_row("Image", display_ref)
|
|
187
|
+
|
|
188
|
+
if lock_data:
|
|
189
|
+
# Show size if available
|
|
190
|
+
if "size" in lock_data:
|
|
191
|
+
table.add_row("Size", lock_data["size"])
|
|
192
|
+
|
|
193
|
+
# Check if this is full lock data or minimal manifest data
|
|
194
|
+
if lock_data.get("source") == "docker-manifest":
|
|
195
|
+
# Minimal data from Docker manifest
|
|
196
|
+
table.add_row("Source", "Docker Registry")
|
|
197
|
+
if not yes:
|
|
198
|
+
design.warning("Note: Limited metadata available from Docker registry.")
|
|
199
|
+
design.info("For full environment details, use a lock file.\n")
|
|
200
|
+
else:
|
|
201
|
+
# Full lock file data
|
|
202
|
+
if "build" in lock_data:
|
|
203
|
+
table.add_row("Built", lock_data["build"].get("generatedAt", "Unknown"))
|
|
204
|
+
table.add_row("HUD Version", lock_data["build"].get("hudVersion", "Unknown"))
|
|
205
|
+
|
|
206
|
+
if "environment" in lock_data:
|
|
207
|
+
env_data = lock_data["environment"]
|
|
208
|
+
table.add_row("Tools", str(env_data.get("toolCount", "Unknown")))
|
|
209
|
+
table.add_row("Init Time", f"{env_data.get('initializeMs', 'Unknown')} ms")
|
|
210
|
+
|
|
211
|
+
if "push" in lock_data:
|
|
212
|
+
push_data = lock_data["push"]
|
|
213
|
+
table.add_row("Registry", push_data.get("registry", "Unknown"))
|
|
214
|
+
table.add_row("Pushed", push_data.get("pushedAt", "Unknown"))
|
|
215
|
+
|
|
216
|
+
# Environment variables
|
|
217
|
+
env_section = lock_data.get("environment", {})
|
|
218
|
+
if "variables" in env_section:
|
|
219
|
+
vars_data = env_section["variables"]
|
|
220
|
+
if vars_data.get("required"):
|
|
221
|
+
table.add_row("Required Env", ", ".join(vars_data["required"]))
|
|
222
|
+
if vars_data.get("optional"):
|
|
223
|
+
table.add_row("Optional Env", ", ".join(vars_data["optional"]))
|
|
224
|
+
|
|
225
|
+
else:
|
|
226
|
+
# No metadata available
|
|
227
|
+
table.add_row("Source", "Unknown")
|
|
228
|
+
|
|
229
|
+
# Use design's console to maintain consistent output
|
|
230
|
+
design.console.print(table)
|
|
231
|
+
|
|
232
|
+
# Tool summary (show after table)
|
|
233
|
+
if lock_data and "tools" in lock_data:
|
|
234
|
+
design.section_title("Available Tools")
|
|
235
|
+
for tool in lock_data["tools"]:
|
|
236
|
+
design.info(f"• {tool['name']}: {tool['description']}")
|
|
237
|
+
|
|
238
|
+
# Show warnings if no metadata
|
|
239
|
+
if not lock_data and not yes:
|
|
240
|
+
design.warning("No metadata available for this image.")
|
|
241
|
+
design.info("The image will be pulled without verification.")
|
|
242
|
+
|
|
243
|
+
# If verify only, stop here
|
|
244
|
+
if verify_only:
|
|
245
|
+
design.success("Verification complete")
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# Ask for confirmation unless --yes
|
|
249
|
+
if not yes:
|
|
250
|
+
design.info("")
|
|
251
|
+
# Show simple name for confirmation, not the full digest
|
|
252
|
+
if ":" in image_ref and "@" in image_ref:
|
|
253
|
+
simple_name = image_ref.split("@")[0]
|
|
254
|
+
else:
|
|
255
|
+
simple_name = image_ref
|
|
256
|
+
if not typer.confirm(f"Pull {simple_name}?"):
|
|
257
|
+
design.info("Aborted")
|
|
258
|
+
raise typer.Exit(0)
|
|
259
|
+
|
|
260
|
+
# Pull the image
|
|
261
|
+
design.progress_message(f"Pulling {image_ref}...")
|
|
262
|
+
|
|
263
|
+
# Run docker pull with progress
|
|
264
|
+
process = subprocess.Popen( # noqa: S603
|
|
265
|
+
["docker", "pull", image_ref], # noqa: S607
|
|
266
|
+
stdout=subprocess.PIPE,
|
|
267
|
+
stderr=subprocess.STDOUT,
|
|
268
|
+
text=True,
|
|
269
|
+
encoding="utf-8",
|
|
270
|
+
errors="replace",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
for line in process.stdout or []:
|
|
274
|
+
if verbose or "Downloading" in line or "Extracting" in line or "Pull complete" in line:
|
|
275
|
+
design.info(line.rstrip())
|
|
276
|
+
|
|
277
|
+
process.wait()
|
|
278
|
+
|
|
279
|
+
if process.returncode != 0:
|
|
280
|
+
design.error("Pull failed")
|
|
281
|
+
raise typer.Exit(1)
|
|
282
|
+
|
|
283
|
+
# Store lock file locally if we have full lock data (not minimal manifest data)
|
|
284
|
+
if lock_data and lock_data.get("source") != "docker-manifest":
|
|
285
|
+
# Extract digest from image ref
|
|
286
|
+
digest = image_ref.split("@sha256:")[-1][:12] if "@sha256:" in image_ref else "latest"
|
|
287
|
+
|
|
288
|
+
# Store under ~/.hud/envs/<digest>/
|
|
289
|
+
local_env_dir = Path.home() / ".hud" / "envs" / digest
|
|
290
|
+
local_env_dir.mkdir(parents=True, exist_ok=True)
|
|
291
|
+
|
|
292
|
+
local_lock_path = local_env_dir / "hud.lock.yaml"
|
|
293
|
+
with open(local_lock_path, "w") as f:
|
|
294
|
+
yaml.dump(lock_data, f, default_flow_style=False, sort_keys=False)
|
|
295
|
+
|
|
296
|
+
if verbose:
|
|
297
|
+
design.info(f"Stored lock file: {local_lock_path}")
|
|
298
|
+
|
|
299
|
+
# Success!
|
|
300
|
+
design.success("Pull complete!")
|
|
301
|
+
|
|
302
|
+
# Show usage
|
|
303
|
+
design.section_title("Next Steps")
|
|
304
|
+
|
|
305
|
+
# Extract simple name for examples
|
|
306
|
+
simple_ref = image_ref.split("@")[0] if ":" in image_ref and "@" in image_ref else image_ref
|
|
307
|
+
|
|
308
|
+
design.info("1. Quick analysis (from metadata):")
|
|
309
|
+
design.command_example(f"hud analyze {simple_ref}")
|
|
310
|
+
design.info("")
|
|
311
|
+
design.info("2. Live analysis (runs container):")
|
|
312
|
+
design.command_example(f"hud analyze {simple_ref} --live")
|
|
313
|
+
design.info("")
|
|
314
|
+
design.info("3. Run the environment:")
|
|
315
|
+
design.command_example(f"hud run {simple_ref}")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def pull_command(
|
|
319
|
+
target: str = typer.Argument(..., help="Image reference or lock file to pull"),
|
|
320
|
+
lock_file: str | None = typer.Option(
|
|
321
|
+
None, "--lock", "-l", help="Path to lock file (if target is image ref)"
|
|
322
|
+
),
|
|
323
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
324
|
+
verify_only: bool = typer.Option(
|
|
325
|
+
False, "--verify-only", help="Only verify metadata without pulling"
|
|
326
|
+
),
|
|
327
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Pull HUD environment from registry with metadata preview."""
|
|
330
|
+
pull_environment(target, lock_file, yes, verify_only, verbose)
|