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/push.py
CHANGED
|
@@ -1,370 +1,404 @@
|
|
|
1
|
-
"""Push HUD environments to registry."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import subprocess
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
from
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
design.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
design.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
username
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
#
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
design.
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1
|
+
"""Push HUD environments to registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import typer
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from hud.settings import settings
|
|
14
|
+
from hud.utils.design import HUDDesign
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_docker_username() -> str | None:
|
|
18
|
+
"""Get the current Docker username if logged in."""
|
|
19
|
+
try:
|
|
20
|
+
# Docker config locations
|
|
21
|
+
config_paths = [
|
|
22
|
+
Path.home() / ".docker" / "config.json",
|
|
23
|
+
Path.home() / ".docker" / "plaintext-credentials.json", # Alternative location
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
for config_path in config_paths:
|
|
27
|
+
if config_path.exists():
|
|
28
|
+
try:
|
|
29
|
+
with open(config_path) as f:
|
|
30
|
+
config = json.load(f)
|
|
31
|
+
|
|
32
|
+
# Look for auth entries
|
|
33
|
+
auths = config.get("auths", {})
|
|
34
|
+
for registry_url, auth_info in auths.items():
|
|
35
|
+
if (
|
|
36
|
+
any(
|
|
37
|
+
hub in registry_url
|
|
38
|
+
for hub in ["docker.io", "index.docker.io", "registry-1.docker.io"]
|
|
39
|
+
)
|
|
40
|
+
and "auth" in auth_info
|
|
41
|
+
):
|
|
42
|
+
import base64
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
decoded = base64.b64decode(auth_info["auth"]).decode()
|
|
46
|
+
username = decoded.split(":", 1)[0]
|
|
47
|
+
if username and username != "token": # Skip token-based auth
|
|
48
|
+
return username
|
|
49
|
+
except Exception:
|
|
50
|
+
pass # Silent failure, try other methods
|
|
51
|
+
except Exception:
|
|
52
|
+
pass # Silent failure, try other methods
|
|
53
|
+
|
|
54
|
+
# Alternative: Check credsStore/credHelpers
|
|
55
|
+
for config_path in config_paths:
|
|
56
|
+
if config_path.exists():
|
|
57
|
+
try:
|
|
58
|
+
with open(config_path) as f:
|
|
59
|
+
config = json.load(f)
|
|
60
|
+
|
|
61
|
+
# Check if using credential helpers
|
|
62
|
+
if "credsStore" in config:
|
|
63
|
+
# Try to get credentials from helper
|
|
64
|
+
helper = config["credsStore"]
|
|
65
|
+
try:
|
|
66
|
+
result = subprocess.run( # noqa: S603
|
|
67
|
+
[f"docker-credential-{helper}", "list"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
)
|
|
71
|
+
if result.returncode == 0:
|
|
72
|
+
creds = json.loads(result.stdout)
|
|
73
|
+
for url in creds:
|
|
74
|
+
if "docker.io" in url:
|
|
75
|
+
# Try to get the username
|
|
76
|
+
get_result = subprocess.run( # noqa: S603
|
|
77
|
+
[f"docker-credential-{helper}", "get"],
|
|
78
|
+
input=url,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
)
|
|
82
|
+
if get_result.returncode == 0:
|
|
83
|
+
cred_data = json.loads(get_result.stdout)
|
|
84
|
+
username = cred_data.get("Username", "")
|
|
85
|
+
if username and username != "token":
|
|
86
|
+
return username
|
|
87
|
+
except Exception:
|
|
88
|
+
pass # Silent failure, try other methods
|
|
89
|
+
except Exception:
|
|
90
|
+
pass # Silent failure, try other methods
|
|
91
|
+
except Exception:
|
|
92
|
+
pass # Silent failure, try other methods
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_docker_image_labels(image: str) -> dict:
|
|
97
|
+
"""Get labels from a Docker image."""
|
|
98
|
+
try:
|
|
99
|
+
result = subprocess.run( # noqa: S603
|
|
100
|
+
["docker", "inspect", "--format", "{{json .Config.Labels}}", image], # noqa: S607
|
|
101
|
+
capture_output=True,
|
|
102
|
+
text=True,
|
|
103
|
+
check=True,
|
|
104
|
+
)
|
|
105
|
+
return json.loads(result.stdout.strip()) or {}
|
|
106
|
+
except Exception:
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def push_environment(
|
|
111
|
+
directory: str = ".",
|
|
112
|
+
image: str | None = None,
|
|
113
|
+
tag: str | None = None,
|
|
114
|
+
sign: bool = False,
|
|
115
|
+
yes: bool = False,
|
|
116
|
+
verbose: bool = False,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Push HUD environment to registry."""
|
|
119
|
+
design = HUDDesign()
|
|
120
|
+
design.header("HUD Environment Push")
|
|
121
|
+
|
|
122
|
+
# Find hud.lock.yaml in specified directory
|
|
123
|
+
env_dir = Path(directory)
|
|
124
|
+
lock_path = env_dir / "hud.lock.yaml"
|
|
125
|
+
|
|
126
|
+
if not lock_path.exists():
|
|
127
|
+
design.error(f"No hud.lock.yaml found in {directory}")
|
|
128
|
+
design.info("Run 'hud build' first to generate a lock file")
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
|
|
131
|
+
# Check for API key first
|
|
132
|
+
if not settings.api_key:
|
|
133
|
+
design.error("No HUD API key found")
|
|
134
|
+
design.warning("A HUD API key is required to push environments.")
|
|
135
|
+
design.info("\nTo get started:")
|
|
136
|
+
design.info("1. Get your API key at: https://hud.so/settings")
|
|
137
|
+
design.command_example("export HUD_API_KEY=your-key-here", "Set your API key")
|
|
138
|
+
design.command_example("hud push", "Try again")
|
|
139
|
+
design.info("")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
# Load lock file
|
|
143
|
+
with open(lock_path) as f:
|
|
144
|
+
lock_data = yaml.safe_load(f)
|
|
145
|
+
|
|
146
|
+
# Handle both old and new lock file formats
|
|
147
|
+
local_image = lock_data.get("image", "")
|
|
148
|
+
if not local_image and "build" in lock_data:
|
|
149
|
+
# New format might have image elsewhere
|
|
150
|
+
local_image = lock_data.get("image", "")
|
|
151
|
+
|
|
152
|
+
# Get internal version from lock file
|
|
153
|
+
internal_version = lock_data.get("build", {}).get("version", None)
|
|
154
|
+
|
|
155
|
+
# If no image specified, try to be smart
|
|
156
|
+
if not image:
|
|
157
|
+
# Check if user is logged in
|
|
158
|
+
username = get_docker_username()
|
|
159
|
+
if username:
|
|
160
|
+
# Extract image name from lock file (handle @sha256:... format)
|
|
161
|
+
base_image = local_image.split("@")[0] if "@" in local_image else local_image
|
|
162
|
+
|
|
163
|
+
if ":" in base_image:
|
|
164
|
+
base_name = base_image.split(":")[0]
|
|
165
|
+
current_tag = base_image.split(":")[1]
|
|
166
|
+
else:
|
|
167
|
+
base_name = base_image
|
|
168
|
+
current_tag = "latest"
|
|
169
|
+
|
|
170
|
+
# Remove any existing registry prefix
|
|
171
|
+
if "/" in base_name:
|
|
172
|
+
base_name = base_name.split("/")[-1]
|
|
173
|
+
|
|
174
|
+
# Use provided tag, or internal version, or current tag as fallback
|
|
175
|
+
if tag:
|
|
176
|
+
final_tag = tag
|
|
177
|
+
design.info(f"Using specified tag: {tag}")
|
|
178
|
+
elif internal_version:
|
|
179
|
+
final_tag = internal_version
|
|
180
|
+
design.info(f"Using internal version from lock file: {internal_version}")
|
|
181
|
+
else:
|
|
182
|
+
final_tag = current_tag
|
|
183
|
+
design.info(f"Using current tag: {current_tag}")
|
|
184
|
+
|
|
185
|
+
# Suggest a registry image
|
|
186
|
+
image = f"{username}/{base_name}:{final_tag}"
|
|
187
|
+
design.info(f"Auto-detected Docker username: {username}")
|
|
188
|
+
design.info(f"Will push to: {image}")
|
|
189
|
+
|
|
190
|
+
if not yes and not typer.confirm(f"\nPush to {image}?"):
|
|
191
|
+
design.info("Aborted.")
|
|
192
|
+
raise typer.Exit(0)
|
|
193
|
+
else:
|
|
194
|
+
design.error(
|
|
195
|
+
"Not logged in to Docker Hub. Please specify --image or run 'docker login'"
|
|
196
|
+
)
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
elif tag or internal_version:
|
|
199
|
+
# Handle tag when image is provided
|
|
200
|
+
# Prefer explicit tag over internal version
|
|
201
|
+
final_tag = tag if tag else internal_version
|
|
202
|
+
|
|
203
|
+
if ":" in image:
|
|
204
|
+
# Image already has a tag
|
|
205
|
+
existing_tag = image.split(":")[-1]
|
|
206
|
+
if existing_tag != final_tag:
|
|
207
|
+
if tag:
|
|
208
|
+
design.warning(f"Image already has tag '{existing_tag}', overriding with '{final_tag}'")
|
|
209
|
+
else:
|
|
210
|
+
design.info(f"Image has tag '{existing_tag}', but using internal version '{final_tag}'")
|
|
211
|
+
image = image.rsplit(":", 1)[0] + f":{final_tag}"
|
|
212
|
+
# else: tags match, no action needed
|
|
213
|
+
else:
|
|
214
|
+
# Image has no tag, append the appropriate one
|
|
215
|
+
image = f"{image}:{final_tag}"
|
|
216
|
+
|
|
217
|
+
if tag:
|
|
218
|
+
design.info(f"Using specified tag: {tag}")
|
|
219
|
+
else:
|
|
220
|
+
design.info(f"Using internal version from lock file: {internal_version}")
|
|
221
|
+
design.info(f"Will push to: {image}")
|
|
222
|
+
|
|
223
|
+
# Verify local image exists
|
|
224
|
+
# Extract the tag part (before @sha256:...) for Docker operations
|
|
225
|
+
local_tag = local_image.split("@")[0] if "@" in local_image else local_image
|
|
226
|
+
|
|
227
|
+
# Also check for version-tagged image if we have internal version
|
|
228
|
+
version_tag = None
|
|
229
|
+
if internal_version and ":" in local_tag:
|
|
230
|
+
base_name = local_tag.split(":")[0]
|
|
231
|
+
version_tag = f"{base_name}:{internal_version}"
|
|
232
|
+
|
|
233
|
+
# Try to find the image - prefer version tag if it exists
|
|
234
|
+
image_to_push = None
|
|
235
|
+
if version_tag:
|
|
236
|
+
try:
|
|
237
|
+
subprocess.run(["docker", "inspect", version_tag], capture_output=True, check=True) # noqa: S603, S607
|
|
238
|
+
image_to_push = version_tag
|
|
239
|
+
design.info(f"Found version-tagged image: {version_tag}")
|
|
240
|
+
except subprocess.CalledProcessError:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
if not image_to_push:
|
|
244
|
+
try:
|
|
245
|
+
subprocess.run(["docker", "inspect", local_tag], capture_output=True, check=True) # noqa: S603, S607
|
|
246
|
+
image_to_push = local_tag
|
|
247
|
+
except subprocess.CalledProcessError:
|
|
248
|
+
design.error(f"Local image not found: {local_tag}")
|
|
249
|
+
if version_tag:
|
|
250
|
+
design.error(f"Also tried: {version_tag}")
|
|
251
|
+
design.info("Run 'hud build' first to create the image")
|
|
252
|
+
raise typer.Exit(1) # noqa: B904
|
|
253
|
+
|
|
254
|
+
# Check if local image has the expected label
|
|
255
|
+
labels = get_docker_image_labels(image_to_push)
|
|
256
|
+
expected_label = labels.get("org.hud.manifest.head", "")
|
|
257
|
+
version_label = labels.get("org.hud.version", "")
|
|
258
|
+
|
|
259
|
+
# Skip hash verification - the lock file may have been updated with digest after build
|
|
260
|
+
if verbose:
|
|
261
|
+
if expected_label:
|
|
262
|
+
design.info(f"Image label: {expected_label[:12]}...")
|
|
263
|
+
if version_label:
|
|
264
|
+
design.info(f"Version label: {version_label}")
|
|
265
|
+
|
|
266
|
+
# Tag the image for push
|
|
267
|
+
design.progress_message(f"Tagging {image_to_push} as {image}")
|
|
268
|
+
subprocess.run(["docker", "tag", image_to_push, image], check=True) # noqa: S603, S607
|
|
269
|
+
|
|
270
|
+
# Push the image
|
|
271
|
+
design.progress_message(f"Pushing {image} to registry...")
|
|
272
|
+
|
|
273
|
+
# Show push output
|
|
274
|
+
process = subprocess.Popen( # noqa: S603
|
|
275
|
+
["docker", "push", image], # noqa: S607
|
|
276
|
+
stdout=subprocess.PIPE,
|
|
277
|
+
stderr=subprocess.STDOUT,
|
|
278
|
+
text=True,
|
|
279
|
+
encoding="utf-8",
|
|
280
|
+
errors="replace",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
for line in process.stdout or []:
|
|
284
|
+
design.info(line.rstrip())
|
|
285
|
+
|
|
286
|
+
process.wait()
|
|
287
|
+
|
|
288
|
+
if process.returncode != 0:
|
|
289
|
+
design.error("Push failed")
|
|
290
|
+
raise typer.Exit(1)
|
|
291
|
+
|
|
292
|
+
# Get the digest of the pushed image
|
|
293
|
+
result = subprocess.run( # noqa: S603
|
|
294
|
+
["docker", "inspect", "--format", "{{index .RepoDigests 0}}", image], # noqa: S607
|
|
295
|
+
capture_output=True,
|
|
296
|
+
text=True,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
300
|
+
pushed_digest = result.stdout.strip()
|
|
301
|
+
else:
|
|
302
|
+
pushed_digest = image
|
|
303
|
+
|
|
304
|
+
# Success!
|
|
305
|
+
design.success("Push complete!")
|
|
306
|
+
|
|
307
|
+
# Show the final image reference
|
|
308
|
+
design.section_title("Pushed Image")
|
|
309
|
+
design.status_item("Registry", pushed_digest, primary=True)
|
|
310
|
+
|
|
311
|
+
# Update the lock file with registry information
|
|
312
|
+
lock_data["image"] = pushed_digest
|
|
313
|
+
|
|
314
|
+
# Add push information
|
|
315
|
+
from datetime import datetime
|
|
316
|
+
|
|
317
|
+
lock_data["push"] = {
|
|
318
|
+
"source": local_image,
|
|
319
|
+
"pushedAt": datetime.utcnow().isoformat() + "Z",
|
|
320
|
+
"registry": pushed_digest.split("/")[0] if "/" in pushed_digest else "docker.io",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Save updated lock file
|
|
324
|
+
with open(lock_path, "w") as f:
|
|
325
|
+
yaml.dump(lock_data, f, default_flow_style=False, sort_keys=False)
|
|
326
|
+
|
|
327
|
+
design.success("Updated lock file with registry image")
|
|
328
|
+
|
|
329
|
+
# Upload lock file to HUD registry
|
|
330
|
+
try:
|
|
331
|
+
# Extract org/name:tag from the pushed image
|
|
332
|
+
# e.g., "docker.io/hudpython/test_init:latest@sha256:..." -> "hudpython/test_init:latest"
|
|
333
|
+
# e.g., "hudpython/test_init:v1.0" -> "hudpython/test_init:v1.0"
|
|
334
|
+
registry_parts = pushed_digest.split("/")
|
|
335
|
+
if len(registry_parts) >= 2:
|
|
336
|
+
# 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 and get org/name:tag
|
|
339
|
+
name_with_tag = "/".join(registry_parts[1:]).split("@")[0]
|
|
340
|
+
else:
|
|
341
|
+
# Just org/name:tag
|
|
342
|
+
name_with_tag = "/".join(registry_parts[:2]).split("@")[0]
|
|
343
|
+
|
|
344
|
+
# If no tag specified, use "latest"
|
|
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)
|
|
362
|
+
|
|
363
|
+
if response.status_code in [200, 201]:
|
|
364
|
+
design.success("Metadata uploaded to HUD registry")
|
|
365
|
+
design.info("Others can now pull with:")
|
|
366
|
+
design.command_example(f"hud pull {name_with_tag}")
|
|
367
|
+
design.info("")
|
|
368
|
+
else:
|
|
369
|
+
design.warning(f"Could not upload to registry: {response.status_code}")
|
|
370
|
+
if verbose:
|
|
371
|
+
design.info(f"Response: {response.text}")
|
|
372
|
+
design.info("Share hud.lock.yaml manually\n")
|
|
373
|
+
else:
|
|
374
|
+
if verbose:
|
|
375
|
+
design.info("Could not parse registry path for upload")
|
|
376
|
+
design.info("Share hud.lock.yaml to let others reproduce your exact environment\n")
|
|
377
|
+
except Exception as e:
|
|
378
|
+
design.warning(f"Registry upload failed: {e}")
|
|
379
|
+
design.info("Share hud.lock.yaml manually\n")
|
|
380
|
+
|
|
381
|
+
# Show usage examples
|
|
382
|
+
design.section_title("What's Next?")
|
|
383
|
+
|
|
384
|
+
design.info("Test locally:")
|
|
385
|
+
design.command_example(f"hud run {image}")
|
|
386
|
+
design.info("")
|
|
387
|
+
design.info("Share environment:")
|
|
388
|
+
design.info(" Share the updated hud.lock.yaml for others to reproduce your exact environment")
|
|
389
|
+
|
|
390
|
+
# TODO: Upload lock file to HUD registry
|
|
391
|
+
if sign:
|
|
392
|
+
design.warning("Signing not yet implemented")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def push_command(
|
|
396
|
+
directory: str = ".",
|
|
397
|
+
image: str | None = None,
|
|
398
|
+
tag: str | None = None,
|
|
399
|
+
sign: bool = False,
|
|
400
|
+
yes: bool = False,
|
|
401
|
+
verbose: bool = False,
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Push HUD environment to registry."""
|
|
404
|
+
push_environment(directory, image, tag, sign, yes, verbose)
|