hte-cli 0.2.26__py3-none-any.whl → 0.2.28__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.
- hte_cli/cli.py +93 -8
- hte_cli/image_utils.py +187 -0
- {hte_cli-0.2.26.dist-info → hte_cli-0.2.28.dist-info}/METADATA +1 -1
- {hte_cli-0.2.26.dist-info → hte_cli-0.2.28.dist-info}/RECORD +6 -6
- {hte_cli-0.2.26.dist-info → hte_cli-0.2.28.dist-info}/WHEEL +0 -0
- {hte_cli-0.2.26.dist-info → hte_cli-0.2.28.dist-info}/entry_points.txt +0 -0
hte_cli/cli.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Uses Click for command parsing and Rich for pretty output.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
import webbrowser
|
|
8
9
|
|
|
@@ -173,12 +174,18 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
173
174
|
sys.exit(1)
|
|
174
175
|
|
|
175
176
|
# Check Docker is running before we start (with retry prompt)
|
|
177
|
+
# In non-interactive mode (CI/automation), fail immediately instead of prompting
|
|
178
|
+
non_interactive = os.environ.get("HTE_NON_INTERACTIVE", "").lower() in ("1", "true", "yes")
|
|
179
|
+
|
|
176
180
|
while True:
|
|
177
181
|
docker_ok, docker_error = _check_docker()
|
|
178
182
|
if docker_ok:
|
|
179
183
|
console.print("[dim]✓ Docker running[/dim]")
|
|
180
184
|
break
|
|
181
185
|
console.print(f"[red]{docker_error}[/red]")
|
|
186
|
+
if non_interactive:
|
|
187
|
+
console.print("[dim]Non-interactive mode - exiting[/dim]")
|
|
188
|
+
sys.exit(1)
|
|
182
189
|
console.print()
|
|
183
190
|
if not click.confirm("Start Docker and retry?", default=True):
|
|
184
191
|
sys.exit(1)
|
|
@@ -238,6 +245,10 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
238
245
|
extract_images_from_compose,
|
|
239
246
|
extract_image_platforms_from_compose,
|
|
240
247
|
pull_image_with_progress,
|
|
248
|
+
check_image_architecture_matches_host,
|
|
249
|
+
fix_image_architecture,
|
|
250
|
+
get_host_docker_platform,
|
|
251
|
+
is_running_in_linux_vm_on_arm,
|
|
241
252
|
)
|
|
242
253
|
|
|
243
254
|
# Create event streamer
|
|
@@ -317,6 +328,22 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
317
328
|
if images:
|
|
318
329
|
from hte_cli.image_utils import check_image_exists_locally
|
|
319
330
|
|
|
331
|
+
# Detect host architecture for smart image handling
|
|
332
|
+
is_linux_arm = is_running_in_linux_vm_on_arm()
|
|
333
|
+
host_platform = get_host_docker_platform()
|
|
334
|
+
|
|
335
|
+
if is_linux_arm:
|
|
336
|
+
console.print(
|
|
337
|
+
f"[yellow]![/yellow] Detected [bold]Linux ARM64[/bold] environment"
|
|
338
|
+
)
|
|
339
|
+
console.print(
|
|
340
|
+
f" [dim]Will verify cached images match host architecture ({host_platform})[/dim]"
|
|
341
|
+
)
|
|
342
|
+
console.print(
|
|
343
|
+
f" [dim]Mismatched images will be automatically re-pulled[/dim]"
|
|
344
|
+
)
|
|
345
|
+
console.print()
|
|
346
|
+
|
|
320
347
|
console.print(f"[bold]Step 2:[/bold] Pulling {len(images)} Docker image(s)...")
|
|
321
348
|
pull_start = time.monotonic()
|
|
322
349
|
pull_errors = {}
|
|
@@ -327,13 +354,57 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
327
354
|
|
|
328
355
|
# Check if already cached
|
|
329
356
|
if check_image_exists_locally(img):
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
357
|
+
# Verify architecture matches host (important for Linux ARM64)
|
|
358
|
+
matches, image_arch, host_arch = check_image_architecture_matches_host(img)
|
|
359
|
+
|
|
360
|
+
if matches:
|
|
361
|
+
# Show architecture info on Linux ARM64 for transparency
|
|
362
|
+
if is_linux_arm and image_arch:
|
|
363
|
+
console.print(
|
|
364
|
+
f" [green]✓[/green] {short_name} [dim](cached, arch: {image_arch})[/dim]"
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
console.print(f" [green]✓[/green] {short_name} [dim](cached)[/dim]")
|
|
368
|
+
cached_images.append(img)
|
|
369
|
+
continue
|
|
370
|
+
else:
|
|
371
|
+
# Architecture mismatch detected - this is the key fix for Linux ARM64
|
|
372
|
+
console.print(
|
|
373
|
+
f" [yellow]⚠[/yellow] {short_name} [yellow]architecture mismatch![/yellow]"
|
|
374
|
+
)
|
|
375
|
+
console.print(
|
|
376
|
+
f" [dim]Cached image: {image_arch} | Host: {host_arch}[/dim]"
|
|
377
|
+
)
|
|
378
|
+
console.print(
|
|
379
|
+
f" [dim]Removing cached image and re-pulling correct architecture...[/dim]"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
needed_fix, fix_msg = fix_image_architecture(img)
|
|
383
|
+
if needed_fix:
|
|
384
|
+
console.print(
|
|
385
|
+
f" [green]✓[/green] {short_name} [green]fixed![/green] [dim]({fix_msg})[/dim]"
|
|
386
|
+
)
|
|
387
|
+
pulled_images.append(img)
|
|
388
|
+
continue
|
|
389
|
+
else:
|
|
390
|
+
console.print(f" [red]✗[/red] {short_name} [dim]({fix_msg})[/dim]")
|
|
391
|
+
failed_images.append(img)
|
|
392
|
+
pull_errors[img] = fix_msg
|
|
393
|
+
continue
|
|
333
394
|
|
|
334
395
|
# Need to pull - show progress
|
|
335
396
|
last_status = ["connecting..."]
|
|
336
397
|
last_error = [""]
|
|
398
|
+
|
|
399
|
+
# On Linux ARM64, use host platform if no explicit platform in compose
|
|
400
|
+
# This prevents pulling amd64 images that won't run
|
|
401
|
+
pull_platform = platform
|
|
402
|
+
if not pull_platform and is_linux_arm and host_platform:
|
|
403
|
+
pull_platform = host_platform
|
|
404
|
+
console.print(
|
|
405
|
+
f" [dim]Pulling {short_name} with platform {host_platform}...[/dim]"
|
|
406
|
+
)
|
|
407
|
+
|
|
337
408
|
with console.status(
|
|
338
409
|
f"[yellow]↓[/yellow] {short_name} [dim]connecting...[/dim]"
|
|
339
410
|
) as status:
|
|
@@ -357,14 +428,21 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
357
428
|
last_error[0] = line
|
|
358
429
|
|
|
359
430
|
success = pull_image_with_progress(
|
|
360
|
-
img, platform=
|
|
431
|
+
img, platform=pull_platform, on_progress=show_progress
|
|
361
432
|
)
|
|
362
433
|
|
|
363
434
|
if success:
|
|
364
|
-
|
|
435
|
+
# Show platform info on Linux ARM64 for confirmation
|
|
436
|
+
if is_linux_arm and pull_platform:
|
|
437
|
+
arch_short = pull_platform.split("/")[-1] # e.g., "arm64"
|
|
438
|
+
console.print(
|
|
439
|
+
f" [green]✓[/green] {short_name} [dim](downloaded, arch: {arch_short})[/dim]"
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
console.print(f" [green]✓[/green] {short_name} [dim](downloaded)[/dim]")
|
|
365
443
|
pulled_images.append(img)
|
|
366
444
|
else:
|
|
367
|
-
platform_note = f" (platform: {
|
|
445
|
+
platform_note = f" (platform: {pull_platform})" if pull_platform else ""
|
|
368
446
|
console.print(f" [red]✗[/red] {short_name}{platform_note} [dim](failed)[/dim]")
|
|
369
447
|
if last_error[0]:
|
|
370
448
|
console.print(f" [dim]{last_error[0][:60]}[/dim]")
|
|
@@ -388,8 +466,15 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
388
466
|
console.print()
|
|
389
467
|
console.print("[yellow]Troubleshooting:[/yellow]")
|
|
390
468
|
console.print(" 1. Check Docker is running: docker info")
|
|
391
|
-
|
|
392
|
-
|
|
469
|
+
|
|
470
|
+
# Architecture-specific advice
|
|
471
|
+
if is_linux_arm:
|
|
472
|
+
console.print(f" 2. You're on Linux ARM64 - try: docker pull <image> --platform linux/arm64")
|
|
473
|
+
console.print(" 3. For x86-only images, enable QEMU: docker run --privileged --rm tonistiigi/binfmt --install all")
|
|
474
|
+
else:
|
|
475
|
+
console.print(" 2. Try manual pull: docker pull <image>")
|
|
476
|
+
|
|
477
|
+
console.print(" 4. Check network connectivity")
|
|
393
478
|
console.print()
|
|
394
479
|
console.print("Session remains active - you can retry with: hte-cli session join " + session_id)
|
|
395
480
|
sys.exit(1)
|
hte_cli/image_utils.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import platform
|
|
5
6
|
import pty
|
|
6
7
|
import re
|
|
7
8
|
import select
|
|
@@ -13,6 +14,119 @@ import yaml
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
# Architecture mapping: Python's platform.machine() -> Docker platform
|
|
18
|
+
ARCH_TO_DOCKER_PLATFORM = {
|
|
19
|
+
"x86_64": "linux/amd64",
|
|
20
|
+
"amd64": "linux/amd64",
|
|
21
|
+
"aarch64": "linux/arm64",
|
|
22
|
+
"arm64": "linux/arm64",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Docker image architecture names (from docker inspect)
|
|
26
|
+
DOCKER_ARCH_TO_PLATFORM = {
|
|
27
|
+
"amd64": "linux/amd64",
|
|
28
|
+
"arm64": "linux/arm64",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_host_architecture() -> str:
|
|
33
|
+
"""
|
|
34
|
+
Get the host machine's architecture.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Architecture string (e.g., "x86_64", "arm64", "aarch64")
|
|
38
|
+
"""
|
|
39
|
+
return platform.machine()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_host_docker_platform() -> str | None:
|
|
43
|
+
"""
|
|
44
|
+
Get the Docker platform string for the host architecture.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Docker platform (e.g., "linux/amd64", "linux/arm64") or None if unknown
|
|
48
|
+
"""
|
|
49
|
+
arch = get_host_architecture()
|
|
50
|
+
return ARCH_TO_DOCKER_PLATFORM.get(arch)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_image_architecture(image: str) -> str | None:
|
|
54
|
+
"""
|
|
55
|
+
Get the architecture of a locally cached Docker image.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
image: Image name (e.g., "python:3.12-slim")
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Architecture string (e.g., "amd64", "arm64") or None if not found/error
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["docker", "image", "inspect", image, "--format", "{{.Architecture}}"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=10,
|
|
69
|
+
)
|
|
70
|
+
if result.returncode == 0:
|
|
71
|
+
return result.stdout.strip()
|
|
72
|
+
return None
|
|
73
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_image_architecture_matches_host(image: str) -> tuple[bool, str | None, str | None]:
|
|
78
|
+
"""
|
|
79
|
+
Check if a cached image's architecture matches the host.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
image: Image name to check
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Tuple of (matches, image_arch, host_arch):
|
|
86
|
+
- matches: True if architectures match or image not cached
|
|
87
|
+
- image_arch: The cached image's architecture (None if not cached)
|
|
88
|
+
- host_arch: The host's architecture
|
|
89
|
+
"""
|
|
90
|
+
host_arch = get_host_architecture()
|
|
91
|
+
host_platform = get_host_docker_platform()
|
|
92
|
+
|
|
93
|
+
if not host_platform:
|
|
94
|
+
# Unknown host architecture - can't check
|
|
95
|
+
logger.warning(f"Unknown host architecture: {host_arch}")
|
|
96
|
+
return (True, None, host_arch)
|
|
97
|
+
|
|
98
|
+
image_arch = get_image_architecture(image)
|
|
99
|
+
if not image_arch:
|
|
100
|
+
# Image not cached - nothing to check
|
|
101
|
+
return (True, None, host_arch)
|
|
102
|
+
|
|
103
|
+
image_platform = DOCKER_ARCH_TO_PLATFORM.get(image_arch)
|
|
104
|
+
matches = image_platform == host_platform
|
|
105
|
+
|
|
106
|
+
if not matches:
|
|
107
|
+
logger.info(
|
|
108
|
+
f"Architecture mismatch for {image}: "
|
|
109
|
+
f"cached={image_arch} ({image_platform}), host={host_arch} ({host_platform})"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return (matches, image_arch, host_arch)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_running_in_linux_vm_on_arm() -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Detect if running Linux ARM64 (likely a VM on Apple Silicon).
|
|
118
|
+
|
|
119
|
+
This is a common setup that causes architecture issues because:
|
|
120
|
+
- macOS Docker Desktop handles multi-arch via Rosetta
|
|
121
|
+
- Linux ARM64 in a VM doesn't have that, needs explicit platform handling
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if running Linux on ARM64
|
|
125
|
+
"""
|
|
126
|
+
import sys
|
|
127
|
+
return sys.platform == "linux" and get_host_architecture() in ("aarch64", "arm64")
|
|
128
|
+
|
|
129
|
+
|
|
16
130
|
def extract_images_from_compose(compose_yaml: str) -> list[str]:
|
|
17
131
|
"""
|
|
18
132
|
Extract Docker image names from a compose.yaml string.
|
|
@@ -86,6 +200,79 @@ def check_image_exists_locally(image: str) -> bool:
|
|
|
86
200
|
return False
|
|
87
201
|
|
|
88
202
|
|
|
203
|
+
def remove_image(image: str) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Remove a Docker image from local cache.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
image: Image name to remove
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if removed successfully, False otherwise
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
result = subprocess.run(
|
|
215
|
+
["docker", "rmi", image],
|
|
216
|
+
capture_output=True,
|
|
217
|
+
timeout=30,
|
|
218
|
+
)
|
|
219
|
+
return result.returncode == 0
|
|
220
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def fix_image_architecture(
|
|
225
|
+
image: str,
|
|
226
|
+
on_status: Callable[[str], None] | None = None,
|
|
227
|
+
) -> tuple[bool, str]:
|
|
228
|
+
"""
|
|
229
|
+
Check if a cached image has wrong architecture and fix it if needed.
|
|
230
|
+
|
|
231
|
+
For Linux ARM64 hosts (e.g., VM on Apple Silicon), this:
|
|
232
|
+
1. Checks if the cached image is amd64 when host is arm64
|
|
233
|
+
2. Removes the wrongly-cached image
|
|
234
|
+
3. Re-pulls with explicit --platform linux/arm64
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
image: Image name to check/fix
|
|
238
|
+
on_status: Callback for status updates
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Tuple of (needed_fix, message):
|
|
242
|
+
- needed_fix: True if image was re-pulled
|
|
243
|
+
- message: Description of what happened
|
|
244
|
+
"""
|
|
245
|
+
matches, image_arch, host_arch = check_image_architecture_matches_host(image)
|
|
246
|
+
|
|
247
|
+
if matches:
|
|
248
|
+
if image_arch:
|
|
249
|
+
return (False, f"architecture OK ({image_arch})")
|
|
250
|
+
else:
|
|
251
|
+
return (False, "not cached")
|
|
252
|
+
|
|
253
|
+
# Architecture mismatch detected
|
|
254
|
+
host_platform = get_host_docker_platform()
|
|
255
|
+
if not host_platform:
|
|
256
|
+
return (False, f"unknown host architecture: {host_arch}")
|
|
257
|
+
|
|
258
|
+
if on_status:
|
|
259
|
+
on_status(f"Cached image is {image_arch}, host is {host_arch} - re-pulling...")
|
|
260
|
+
|
|
261
|
+
# Remove the wrongly-cached image
|
|
262
|
+
logger.info(f"Removing wrongly-cached {image_arch} image: {image}")
|
|
263
|
+
if not remove_image(image):
|
|
264
|
+
return (False, f"failed to remove cached {image_arch} image")
|
|
265
|
+
|
|
266
|
+
# Re-pull with correct platform
|
|
267
|
+
logger.info(f"Re-pulling {image} with platform {host_platform}")
|
|
268
|
+
success = pull_image_with_progress(image, platform=host_platform)
|
|
269
|
+
|
|
270
|
+
if success:
|
|
271
|
+
return (True, f"re-pulled as {host_platform.split('/')[-1]}")
|
|
272
|
+
else:
|
|
273
|
+
return (False, f"failed to re-pull with platform {host_platform}")
|
|
274
|
+
|
|
275
|
+
|
|
89
276
|
def pull_image_with_progress(
|
|
90
277
|
image: str,
|
|
91
278
|
platform: str | None = None,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
hte_cli/__init__.py,sha256=fDGXp-r8bIoLtlQnn5xJ_CpwMhonvk9bGjZQsjA2mDI,914
|
|
2
2
|
hte_cli/__main__.py,sha256=63n0gNGfskidWDU0aAIF2N8lylVCLYKVIkrN9QiORoo,107
|
|
3
3
|
hte_cli/api_client.py,sha256=m42kfFZS72Nu_VuDwxRsLNy4ziCcvgk7KNWBh9gwqy0,9257
|
|
4
|
-
hte_cli/cli.py,sha256=
|
|
4
|
+
hte_cli/cli.py,sha256=EgL9nlQ2R0TSp8qUtPe5YwTN3KlrNCQ8tRQnUhnFrP4,30647
|
|
5
5
|
hte_cli/config.py,sha256=42Xv__YMSeRLs2zhGukJkIXFKtnBtYCHnONfViGyt2g,3387
|
|
6
6
|
hte_cli/errors.py,sha256=1J5PpxcUKBu6XjigMMCPOq4Zc12tnv8LhAsiaVFWLQM,2762
|
|
7
7
|
hte_cli/events.py,sha256=oDKCS-a0IZ7bz7xkwQj5eM4DoDCYvnclAGohrMTWf8s,5644
|
|
8
|
-
hte_cli/image_utils.py,sha256=
|
|
8
|
+
hte_cli/image_utils.py,sha256=n4AmbaR9tbH0ahXbTOn7Rr_VeRbhg1RgWknsWwI_83c,13249
|
|
9
9
|
hte_cli/runner.py,sha256=SWl9FF4X3e9eBbZyL0ujhmmSL5OK8J6st-Ty0jD5AWM,14550
|
|
10
10
|
hte_cli/scorers.py,sha256=B0ZjQ3Fh-VDkc_8CDc86yW7vpdimbV3RSqs7l-VeUIg,6629
|
|
11
11
|
hte_cli/version_check.py,sha256=WVZyGy2XfAghQYdd2N9-0Qfg-7pgp9gt4761-PnmacI,1708
|
|
12
|
-
hte_cli-0.2.
|
|
13
|
-
hte_cli-0.2.
|
|
14
|
-
hte_cli-0.2.
|
|
15
|
-
hte_cli-0.2.
|
|
12
|
+
hte_cli-0.2.28.dist-info/METADATA,sha256=cBb8EnxzFdZrsodJ3icqvprVywpeYJDb1__AI6Lm99A,3820
|
|
13
|
+
hte_cli-0.2.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
hte_cli-0.2.28.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
|
|
15
|
+
hte_cli-0.2.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|