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