hud-python 0.4.47__py3-none-any.whl → 0.4.49__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/agents/base.py +55 -142
- hud/agents/claude.py +5 -6
- hud/agents/grounded_openai.py +1 -1
- hud/agents/misc/integration_test_agent.py +2 -0
- hud/agents/tests/test_base.py +2 -5
- hud/cli/__init__.py +80 -215
- hud/cli/build.py +105 -45
- hud/cli/dev.py +614 -743
- hud/cli/eval.py +14 -9
- hud/cli/flows/tasks.py +100 -21
- hud/cli/init.py +18 -14
- hud/cli/push.py +27 -9
- hud/cli/rl/local_runner.py +28 -16
- hud/cli/rl/vllm.py +2 -0
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_eval.py +574 -0
- hud/cli/tests/test_mcp_server.py +6 -95
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/source_hash.py +1 -1
- hud/datasets/parallel.py +0 -12
- hud/datasets/runner.py +1 -4
- hud/rl/actor.py +4 -2
- hud/rl/distributed.py +1 -1
- hud/rl/learner.py +2 -1
- hud/rl/train.py +1 -1
- hud/server/__init__.py +2 -1
- hud/server/router.py +160 -0
- hud/server/server.py +246 -79
- hud/telemetry/trace.py +1 -1
- hud/tools/base.py +20 -10
- hud/tools/computer/__init__.py +2 -0
- hud/tools/computer/qwen.py +431 -0
- hud/tools/computer/settings.py +16 -0
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/playwright.py +1 -1
- hud/types.py +2 -3
- hud/utils/hud_console.py +43 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/METADATA +1 -1
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/RECORD +45 -42
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/WHEEL +0 -0
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/licenses/LICENSE +0 -0
hud/cli/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import contextlib
|
|
7
6
|
import json
|
|
8
7
|
import sys
|
|
9
8
|
from pathlib import Path
|
|
@@ -39,6 +38,7 @@ app = typer.Typer(
|
|
|
39
38
|
help="🚀 HUD CLI for MCP environment analysis and debugging",
|
|
40
39
|
add_completion=False,
|
|
41
40
|
rich_markup_mode="rich",
|
|
41
|
+
pretty_exceptions_enable=False, # Disable Rich's verbose tracebacks
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
console = Console()
|
|
@@ -352,76 +352,71 @@ def version() -> None:
|
|
|
352
352
|
def dev(
|
|
353
353
|
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
354
354
|
None,
|
|
355
|
-
help="
|
|
355
|
+
help="Module path or extra Docker args (when using --docker)",
|
|
356
356
|
),
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild without cache"),
|
|
362
|
-
transport: str = typer.Option(
|
|
363
|
-
"http", "--transport", "-t", help="Transport protocol: http (default) or stdio"
|
|
357
|
+
docker: bool = typer.Option(
|
|
358
|
+
False,
|
|
359
|
+
"--docker",
|
|
360
|
+
help="Run in Docker with volume mounts for hot-reload (for complex environments)",
|
|
364
361
|
),
|
|
365
|
-
|
|
366
|
-
no_reload: bool = typer.Option(False, "--no-reload", help="Disable hot-reload"),
|
|
367
|
-
full_reload: bool = typer.Option(
|
|
362
|
+
stdio: bool = typer.Option(
|
|
368
363
|
False,
|
|
369
|
-
"--
|
|
370
|
-
help="
|
|
364
|
+
"--stdio",
|
|
365
|
+
help="Use stdio transport (default: HTTP)",
|
|
371
366
|
),
|
|
372
|
-
|
|
367
|
+
port: int = typer.Option(8765, "--port", "-p", help="HTTP server port (ignored for stdio)"),
|
|
368
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed logs"),
|
|
373
369
|
inspector: bool = typer.Option(
|
|
374
370
|
False, "--inspector", help="Launch MCP Inspector (HTTP mode only)"
|
|
375
371
|
),
|
|
376
|
-
no_logs: bool = typer.Option(False, "--no-logs", help="Disable streaming Docker logs"),
|
|
377
372
|
interactive: bool = typer.Option(
|
|
378
373
|
False, "--interactive", help="Launch interactive testing mode (HTTP mode only)"
|
|
379
374
|
),
|
|
375
|
+
watch: list[str] = typer.Option( # noqa: B008
|
|
376
|
+
None,
|
|
377
|
+
"--watch",
|
|
378
|
+
help="Additional directories to watch for changes (default: current directory)",
|
|
379
|
+
),
|
|
380
380
|
) -> None:
|
|
381
|
-
"""🔥 Development mode -
|
|
381
|
+
"""🔥 Development mode - run MCP server with hot-reload.
|
|
382
|
+
|
|
383
|
+
TWO MODES:
|
|
384
|
+
|
|
385
|
+
1. Python Module:
|
|
386
|
+
hud dev # Auto-detects module
|
|
387
|
+
hud dev server.main # Explicit module
|
|
388
|
+
|
|
389
|
+
2. Docker with Volume Mounts (Complex environments like 'browser'):
|
|
390
|
+
hud dev --docker # Auto-detects image from hud.lock.yaml
|
|
391
|
+
hud dev --docker -p 8080:8080 # With extra Docker args
|
|
382
392
|
|
|
383
|
-
|
|
384
|
-
The container's CMD determines reload behavior.
|
|
393
|
+
The server must define 'mcp' in its __init__.py or main.py.
|
|
385
394
|
|
|
386
395
|
Examples:
|
|
387
396
|
hud dev # Auto-detect in current directory
|
|
388
|
-
hud dev
|
|
389
|
-
hud dev
|
|
390
|
-
hud dev
|
|
391
|
-
hud dev
|
|
392
|
-
hud dev
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
hud dev . --no-logs # Disable Docker log streaming
|
|
397
|
-
|
|
398
|
-
# With Docker arguments (after all options):
|
|
399
|
-
hud dev . -e BROWSER_PROVIDER=anchorbrowser -e ANCHOR_API_KEY=xxx
|
|
400
|
-
hud dev . -e API_KEY=secret -v /tmp/data:/data --network host
|
|
401
|
-
hud dev . --build -e DEBUG=true --memory 2g
|
|
397
|
+
hud dev controller # Run specific module
|
|
398
|
+
hud dev --inspector # Launch MCP Inspector
|
|
399
|
+
hud dev --interactive # Launch interactive testing mode
|
|
400
|
+
hud dev --stdio # Use stdio transport
|
|
401
|
+
hud dev --watch ../shared # Watch additional directories
|
|
402
|
+
|
|
403
|
+
For environment backend servers, use uvicorn directly:
|
|
404
|
+
uvicorn server:app --reload
|
|
402
405
|
"""
|
|
403
|
-
#
|
|
404
|
-
if params
|
|
405
|
-
|
|
406
|
-
docker_args = params[1:] if len(params) > 1 else []
|
|
407
|
-
else:
|
|
408
|
-
directory = "."
|
|
409
|
-
docker_args = []
|
|
406
|
+
# Extract module from params if provided (first param when not --docker)
|
|
407
|
+
module = params[0] if params and not docker else None
|
|
408
|
+
docker_args = params if docker else []
|
|
410
409
|
|
|
411
410
|
run_mcp_dev_server(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
build,
|
|
415
|
-
no_cache,
|
|
416
|
-
transport,
|
|
411
|
+
module,
|
|
412
|
+
stdio,
|
|
417
413
|
port,
|
|
418
|
-
no_reload,
|
|
419
|
-
full_reload,
|
|
420
414
|
verbose,
|
|
421
415
|
inspector,
|
|
422
|
-
no_logs,
|
|
423
416
|
interactive,
|
|
424
|
-
|
|
417
|
+
watch,
|
|
418
|
+
docker=docker,
|
|
419
|
+
docker_args=docker_args,
|
|
425
420
|
)
|
|
426
421
|
|
|
427
422
|
|
|
@@ -429,18 +424,14 @@ def dev(
|
|
|
429
424
|
def run(
|
|
430
425
|
params: list[str] = typer.Argument( # type: ignore[arg-type] # noqa: B008
|
|
431
426
|
None,
|
|
432
|
-
help="
|
|
427
|
+
help="Docker image followed by optional Docker run arguments "
|
|
428
|
+
"(e.g., 'my-image:latest -e KEY=value')",
|
|
433
429
|
),
|
|
434
430
|
local: bool = typer.Option(
|
|
435
431
|
False,
|
|
436
432
|
"--local",
|
|
437
433
|
help="Run locally with Docker (default: remote via mcp.hud.so)",
|
|
438
434
|
),
|
|
439
|
-
remote: bool = typer.Option(
|
|
440
|
-
False,
|
|
441
|
-
"--remote",
|
|
442
|
-
help="Run remotely via mcp.hud.so (default)",
|
|
443
|
-
),
|
|
444
435
|
transport: str = typer.Option(
|
|
445
436
|
"stdio",
|
|
446
437
|
"--transport",
|
|
@@ -474,180 +465,54 @@ def run(
|
|
|
474
465
|
"-v",
|
|
475
466
|
help="Show detailed output",
|
|
476
467
|
),
|
|
477
|
-
interactive: bool = typer.Option(
|
|
478
|
-
False,
|
|
479
|
-
"--interactive",
|
|
480
|
-
help="Launch interactive testing mode (HTTP transport only)",
|
|
481
|
-
),
|
|
482
|
-
reload: bool = typer.Option(
|
|
483
|
-
False,
|
|
484
|
-
"--reload",
|
|
485
|
-
help="Enable auto-reload on file changes (local Python files only)",
|
|
486
|
-
),
|
|
487
|
-
watch: list[str] = typer.Option( # noqa: B008
|
|
488
|
-
None,
|
|
489
|
-
"--watch",
|
|
490
|
-
help="Directories to watch for changes (can be used multiple times). Defaults to current directory.", # noqa: E501
|
|
491
|
-
),
|
|
492
|
-
cmd: str | None = typer.Option(
|
|
493
|
-
None,
|
|
494
|
-
"--cmd",
|
|
495
|
-
help="Command to run as MCP server (e.g., 'python -m controller')",
|
|
496
|
-
),
|
|
497
468
|
) -> None:
|
|
498
|
-
"""🚀 Run MCP server.
|
|
469
|
+
"""🚀 Run Docker image as MCP server.
|
|
499
470
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
The module is imported, decorators register implicitly, and the server runs.
|
|
503
|
-
Use --reload to watch the module/package directory.
|
|
471
|
+
A simple wrapper around 'docker run' that can launch images locally or remotely.
|
|
472
|
+
By default, runs remotely via mcp.hud.so. Use --local to run with local Docker.
|
|
504
473
|
|
|
505
|
-
|
|
506
|
-
Works with Docker, binaries, or any executable. Supports --reload.
|
|
474
|
+
For local Python development with hot-reload, use 'hud dev' instead.
|
|
507
475
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
476
|
+
Examples:
|
|
477
|
+
hud run my-image:latest # Run remotely (default)
|
|
478
|
+
hud run my-image:latest --local # Run with local Docker
|
|
479
|
+
hud run my-image:latest -e KEY=value # Remote with env vars
|
|
480
|
+
hud run my-image:latest --local -e KEY=val # Local with env vars
|
|
481
|
+
hud run my-image:latest --transport http # Use HTTP transport
|
|
482
|
+
"""
|
|
483
|
+
if not params:
|
|
484
|
+
console.print("[red]❌ Docker image is required[/red]")
|
|
485
|
+
console.print("\nExamples:")
|
|
486
|
+
console.print(" hud run my-image:latest # Run remotely (default)")
|
|
487
|
+
console.print(" hud run my-image:latest --local # Run with local Docker")
|
|
488
|
+
console.print("\n[yellow]For local Python development:[/yellow]")
|
|
489
|
+
console.print(" hud dev # Run with hot-reload")
|
|
512
490
|
raise typer.Exit(1)
|
|
513
491
|
|
|
514
|
-
|
|
515
|
-
if
|
|
516
|
-
import asyncio
|
|
517
|
-
|
|
518
|
-
from .utils.package_runner import run_package_as_mcp
|
|
519
|
-
|
|
520
|
-
asyncio.run(
|
|
521
|
-
run_package_as_mcp(
|
|
522
|
-
cmd, # Pass command string
|
|
523
|
-
transport=transport,
|
|
524
|
-
port=port,
|
|
525
|
-
verbose=verbose,
|
|
526
|
-
reload=reload,
|
|
527
|
-
watch_paths=watch if watch else None,
|
|
528
|
-
)
|
|
529
|
-
)
|
|
530
|
-
return
|
|
531
|
-
|
|
532
|
-
first_param = params[0]
|
|
533
|
-
extra_args = params[1:] if len(params) > 1 else []
|
|
534
|
-
|
|
535
|
-
# Guard: strip accidental nested 'run' token from positional args,
|
|
536
|
-
# which can happen with nested invocations or reload wrappers.
|
|
537
|
-
if first_param == "run" and extra_args:
|
|
538
|
-
first_param, extra_args = extra_args[0], extra_args[1:]
|
|
539
|
-
|
|
540
|
-
# Try to interpret first_param as module[:attr] or file[:attr]
|
|
541
|
-
target = first_param
|
|
542
|
-
server_attr = "mcp"
|
|
543
|
-
if ":" in target:
|
|
544
|
-
target, server_attr = target.split(":", 1)
|
|
545
|
-
|
|
546
|
-
# Only allow dotted import paths or python files for Python mode
|
|
547
|
-
import importlib.util as _importlib_util
|
|
548
|
-
|
|
549
|
-
# Ensure current working directory is importable for local packages like 'controller'
|
|
550
|
-
try:
|
|
551
|
-
import sys as _sys
|
|
552
|
-
from pathlib import Path as _Path
|
|
553
|
-
|
|
554
|
-
cwd_str = str(_Path.cwd())
|
|
555
|
-
if cwd_str not in _sys.path:
|
|
556
|
-
_sys.path.insert(0, cwd_str)
|
|
557
|
-
except Exception: # noqa: S110
|
|
558
|
-
pass
|
|
559
|
-
try:
|
|
560
|
-
# If given a file path, detect and import via file spec
|
|
561
|
-
from pathlib import Path as _Path
|
|
562
|
-
|
|
563
|
-
if target.endswith(".py") and _Path(target).exists():
|
|
564
|
-
spec = _importlib_util.spec_from_file_location("_hud_module", target)
|
|
565
|
-
else:
|
|
566
|
-
spec = _importlib_util.find_spec(target)
|
|
567
|
-
except Exception:
|
|
568
|
-
spec = None
|
|
569
|
-
|
|
570
|
-
# Fallback: treat a local package directory (e.g. 'controller') as a module target
|
|
571
|
-
from pathlib import Path as _Path
|
|
572
|
-
|
|
573
|
-
pkg_dir = _Path(target)
|
|
574
|
-
is_pkg_dir = pkg_dir.is_dir() and (pkg_dir / "__init__.py").exists()
|
|
575
|
-
|
|
576
|
-
is_python_target = (spec is not None) or is_pkg_dir
|
|
577
|
-
|
|
578
|
-
if is_python_target and not (local or remote):
|
|
579
|
-
# Python file/package mode - use implicit MCP server
|
|
580
|
-
import asyncio
|
|
581
|
-
|
|
582
|
-
from .utils.package_runner import run_package_as_mcp, run_with_reload
|
|
583
|
-
|
|
584
|
-
if reload:
|
|
585
|
-
# Run with watchfiles reload
|
|
586
|
-
# Use user-provided watch paths or compute from module
|
|
587
|
-
if watch:
|
|
588
|
-
watch_paths = watch
|
|
589
|
-
else:
|
|
590
|
-
# Compute a watch path that works for dotted modules as well
|
|
591
|
-
watch_paths = [target]
|
|
592
|
-
if spec is not None:
|
|
593
|
-
origin = getattr(spec, "origin", None)
|
|
594
|
-
sublocs = getattr(spec, "submodule_search_locations", None)
|
|
595
|
-
if origin:
|
|
596
|
-
p = _Path(origin)
|
|
597
|
-
# If package __init__.py, watch the package directory
|
|
598
|
-
watch_paths = [str(p.parent if p.name == "__init__.py" else p)]
|
|
599
|
-
elif sublocs:
|
|
600
|
-
with contextlib.suppress(Exception):
|
|
601
|
-
watch_paths = [next(iter(sublocs))]
|
|
602
|
-
|
|
603
|
-
# Always run as subprocess when using reload to enable proper file watching
|
|
604
|
-
# This ensures the parent process can watch files while the child runs the server
|
|
605
|
-
run_with_reload(
|
|
606
|
-
None, # This forces subprocess mode for both stdio and http
|
|
607
|
-
watch_paths,
|
|
608
|
-
verbose=verbose,
|
|
609
|
-
)
|
|
610
|
-
else:
|
|
611
|
-
# Run normally (but still pass reload=False for consistency)
|
|
612
|
-
asyncio.run(
|
|
613
|
-
run_package_as_mcp(
|
|
614
|
-
target,
|
|
615
|
-
transport=transport,
|
|
616
|
-
port=port,
|
|
617
|
-
verbose=verbose,
|
|
618
|
-
server_attr=server_attr,
|
|
619
|
-
reload=False, # Explicitly pass reload state
|
|
620
|
-
watch_paths=None,
|
|
621
|
-
)
|
|
622
|
-
)
|
|
623
|
-
return
|
|
492
|
+
image = params[0]
|
|
493
|
+
docker_args = params[1:] if len(params) > 1 else []
|
|
624
494
|
|
|
625
|
-
#
|
|
626
|
-
|
|
627
|
-
docker_args = extra_args
|
|
495
|
+
# Check if user accidentally passed a module path
|
|
496
|
+
from pathlib import Path
|
|
628
497
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
498
|
+
if not any(c in image for c in [":", "/"]) and (
|
|
499
|
+
Path(image).is_dir() or Path(image).is_file() or "." in image
|
|
500
|
+
):
|
|
501
|
+
console.print(f"[yellow]⚠️ '{image}' looks like a module path, not a Docker image[/yellow]")
|
|
502
|
+
console.print("\n[green]For local Python development, use:[/green]")
|
|
503
|
+
console.print(f" hud dev {image}")
|
|
504
|
+
console.print("\n[green]For Docker images:[/green]")
|
|
505
|
+
console.print(" hud run my-image:latest")
|
|
632
506
|
raise typer.Exit(1)
|
|
633
507
|
|
|
634
508
|
# Default to remote if not explicitly local
|
|
635
|
-
is_local = local
|
|
636
|
-
|
|
637
|
-
# Check for interactive mode restrictions
|
|
638
|
-
if interactive:
|
|
639
|
-
if transport != "http":
|
|
640
|
-
typer.echo("❌ Interactive mode requires HTTP transport (use --transport http)")
|
|
641
|
-
raise typer.Exit(1)
|
|
642
|
-
if not is_local:
|
|
643
|
-
typer.echo("❌ Interactive mode is only available for local execution (use --local)")
|
|
644
|
-
raise typer.Exit(1)
|
|
509
|
+
is_local = local
|
|
645
510
|
|
|
646
511
|
if is_local:
|
|
647
512
|
# Local Docker execution
|
|
648
513
|
from .utils.runner import run_mcp_server
|
|
649
514
|
|
|
650
|
-
run_mcp_server(image, docker_args, transport, port, verbose, interactive)
|
|
515
|
+
run_mcp_server(image, docker_args, transport, port, verbose, interactive=False)
|
|
651
516
|
else:
|
|
652
517
|
# Remote execution via proxy
|
|
653
518
|
from .utils.remote_runner import run_remote_server
|
|
@@ -935,8 +800,8 @@ def eval(
|
|
|
935
800
|
"--max-concurrent",
|
|
936
801
|
help="Max concurrent tasks (prevents rate limits in both asyncio and parallel modes)",
|
|
937
802
|
),
|
|
938
|
-
max_steps: int = typer.Option(
|
|
939
|
-
|
|
803
|
+
max_steps: int | None = typer.Option(
|
|
804
|
+
None,
|
|
940
805
|
"--max-steps",
|
|
941
806
|
help="Maximum steps per task (default: 10 for single, 50 for full)",
|
|
942
807
|
),
|
hud/cli/build.py
CHANGED
|
@@ -7,7 +7,7 @@ import contextlib
|
|
|
7
7
|
import hashlib
|
|
8
8
|
import subprocess
|
|
9
9
|
import time
|
|
10
|
-
from datetime import datetime
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
@@ -184,7 +184,8 @@ async def analyze_mcp_environment(
|
|
|
184
184
|
if verbose:
|
|
185
185
|
hud_console.info(f"Initializing MCP client with command: {' '.join(docker_cmd)}")
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
# Add timeout to fail fast instead of hanging (30 seconds)
|
|
188
|
+
await asyncio.wait_for(client.initialize(), timeout=60.0)
|
|
188
189
|
initialized = True
|
|
189
190
|
initialize_ms = int((time.time() - start_time) * 1000)
|
|
190
191
|
|
|
@@ -205,6 +206,14 @@ async def analyze_mcp_environment(
|
|
|
205
206
|
"tools": tool_info,
|
|
206
207
|
"success": True,
|
|
207
208
|
}
|
|
209
|
+
except TimeoutError:
|
|
210
|
+
from hud.shared.exceptions import HudException
|
|
211
|
+
|
|
212
|
+
hud_console.error("MCP server initialization timed out after 60 seconds")
|
|
213
|
+
hud_console.info(
|
|
214
|
+
"The server likely crashed during startup - check stderr logs with 'hud debug'"
|
|
215
|
+
)
|
|
216
|
+
raise HudException("MCP server initialization timeout") from None
|
|
208
217
|
except Exception as e:
|
|
209
218
|
from hud.shared.exceptions import HudException
|
|
210
219
|
|
|
@@ -286,26 +295,46 @@ def build_environment(
|
|
|
286
295
|
hud_console.error(f"Directory not found: {directory}")
|
|
287
296
|
raise typer.Exit(1)
|
|
288
297
|
|
|
289
|
-
# Check for
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
298
|
+
# Step 1: Check for hud.lock.yaml (previous build)
|
|
299
|
+
lock_path = env_dir / "hud.lock.yaml"
|
|
300
|
+
base_name = None
|
|
301
|
+
|
|
302
|
+
if lock_path.exists():
|
|
303
|
+
try:
|
|
304
|
+
with open(lock_path) as f:
|
|
305
|
+
lock_data = yaml.safe_load(f)
|
|
306
|
+
# Get base name from lock file (strip version/digest)
|
|
307
|
+
lock_image = lock_data.get("images", {}).get("local") or lock_data.get("image", "")
|
|
308
|
+
if lock_image:
|
|
309
|
+
# Remove @sha256:... digest if present
|
|
310
|
+
if "@" in lock_image:
|
|
311
|
+
lock_image = lock_image.split("@")[0]
|
|
312
|
+
# Extract base name (remove :version tag)
|
|
313
|
+
base_name = lock_image.split(":")[0] if ":" in lock_image else lock_image
|
|
314
|
+
hud_console.info(f"Using base name from lock file: {base_name}")
|
|
315
|
+
except Exception as e:
|
|
316
|
+
hud_console.warning(f"Could not read lock file: {e}")
|
|
317
|
+
|
|
318
|
+
# Step 2: If no lock, check for Dockerfile
|
|
319
|
+
if not base_name:
|
|
320
|
+
dockerfile_path = env_dir / "Dockerfile"
|
|
321
|
+
if not dockerfile_path.exists():
|
|
322
|
+
hud_console.error(f"Not a valid environment directory: {directory}")
|
|
323
|
+
hud_console.info("Expected: Dockerfile or hud.lock.yaml")
|
|
324
|
+
raise typer.Exit(1)
|
|
325
|
+
|
|
326
|
+
# First build - use directory name
|
|
327
|
+
base_name = env_dir.name
|
|
328
|
+
hud_console.info(f"First build - using base name: {base_name}")
|
|
329
|
+
|
|
330
|
+
# If user provides --tag, respect it; otherwise use base name only (version added later)
|
|
331
|
+
if tag:
|
|
332
|
+
# User explicitly provided a tag
|
|
333
|
+
image_tag = tag
|
|
334
|
+
base_name = image_tag.split(":")[0] if ":" in image_tag else image_tag
|
|
335
|
+
else:
|
|
336
|
+
# No tag provided - we'll add version later
|
|
337
|
+
image_tag = None
|
|
309
338
|
|
|
310
339
|
# Build temporary image first
|
|
311
340
|
temp_tag = f"hud-build-temp:{int(time.time())}"
|
|
@@ -333,6 +362,13 @@ def build_environment(
|
|
|
333
362
|
asyncio.set_event_loop(loop)
|
|
334
363
|
try:
|
|
335
364
|
analysis = loop.run_until_complete(analyze_mcp_environment(temp_tag, verbose, env_vars))
|
|
365
|
+
except Exception as e:
|
|
366
|
+
hud_console.error(f"Failed to analyze MCP environment: {e}")
|
|
367
|
+
hud_console.info("")
|
|
368
|
+
hud_console.info("To debug this issue, run:")
|
|
369
|
+
hud_console.command_example(f"hud debug {temp_tag}")
|
|
370
|
+
hud_console.info("")
|
|
371
|
+
raise typer.Exit(1) from e
|
|
336
372
|
finally:
|
|
337
373
|
loop.close()
|
|
338
374
|
|
|
@@ -378,15 +414,23 @@ def build_environment(
|
|
|
378
414
|
new_version = "0.1.0"
|
|
379
415
|
hud_console.info(f"Setting initial version: {new_version}")
|
|
380
416
|
|
|
381
|
-
#
|
|
417
|
+
# Determine base name for image references
|
|
418
|
+
if image_tag:
|
|
419
|
+
base_name = image_tag.split(":")[0] if ":" in image_tag else image_tag
|
|
420
|
+
|
|
421
|
+
# Create lock file content with images subsection at top
|
|
382
422
|
lock_content = {
|
|
383
|
-
"version": "1.
|
|
384
|
-
"
|
|
423
|
+
"version": "1.1", # Lock file format version
|
|
424
|
+
"images": {
|
|
425
|
+
"local": f"{base_name}:{new_version}", # Local tag with version
|
|
426
|
+
"full": None, # Will be set with digest after build
|
|
427
|
+
"pushed": None, # Will be set by hud push
|
|
428
|
+
},
|
|
385
429
|
"build": {
|
|
386
|
-
"generatedAt": datetime.
|
|
430
|
+
"generatedAt": datetime.now(UTC).isoformat() + "Z",
|
|
387
431
|
"hudVersion": hud_version,
|
|
388
432
|
"directory": str(env_dir.name),
|
|
389
|
-
"version": new_version,
|
|
433
|
+
"version": new_version,
|
|
390
434
|
# Fast source fingerprint for change detection
|
|
391
435
|
"sourceHash": compute_source_hash(env_dir),
|
|
392
436
|
},
|
|
@@ -450,9 +494,9 @@ def build_environment(
|
|
|
450
494
|
hud_console.progress_message("Rebuilding with lock file metadata...")
|
|
451
495
|
|
|
452
496
|
# Build final image with label (uses cache from first build)
|
|
453
|
-
#
|
|
454
|
-
base_name = image_tag.split(":")[0] if ":" in image_tag else image_tag
|
|
497
|
+
# Create tags: versioned and latest (and custom tag if provided)
|
|
455
498
|
version_tag = f"{base_name}:{new_version}"
|
|
499
|
+
latest_tag = f"{base_name}:latest"
|
|
456
500
|
|
|
457
501
|
label_cmd = ["docker", "build"]
|
|
458
502
|
# Use same defaulting for the second build step
|
|
@@ -466,12 +510,16 @@ def build_environment(
|
|
|
466
510
|
"--label",
|
|
467
511
|
f"org.hud.version={new_version}",
|
|
468
512
|
"-t",
|
|
469
|
-
|
|
513
|
+
version_tag, # Always tag with new version
|
|
470
514
|
"-t",
|
|
471
|
-
|
|
515
|
+
latest_tag, # Always tag with latest
|
|
472
516
|
]
|
|
473
517
|
)
|
|
474
518
|
|
|
519
|
+
# Add custom tag if user provided one
|
|
520
|
+
if image_tag and image_tag not in [version_tag, latest_tag]:
|
|
521
|
+
label_cmd.extend(["-t", image_tag])
|
|
522
|
+
|
|
475
523
|
label_cmd.append(str(env_dir))
|
|
476
524
|
|
|
477
525
|
# Run rebuild using Docker's native output formatting
|
|
@@ -479,34 +527,40 @@ def build_environment(
|
|
|
479
527
|
# Show Docker's native output when verbose
|
|
480
528
|
result = subprocess.run(label_cmd, check=False) # noqa: S603
|
|
481
529
|
else:
|
|
482
|
-
#
|
|
530
|
+
# Capture output for error reporting, but don't show unless it fails
|
|
483
531
|
result = subprocess.run( # noqa: S603
|
|
484
|
-
label_cmd,
|
|
532
|
+
label_cmd, capture_output=True, text=True, check=False
|
|
485
533
|
)
|
|
486
534
|
|
|
487
535
|
if result.returncode != 0:
|
|
488
536
|
hud_console.error("Failed to rebuild with label")
|
|
537
|
+
if not verbose and result.stderr:
|
|
538
|
+
hud_console.info("Error output:")
|
|
539
|
+
hud_console.info(str(result.stderr))
|
|
540
|
+
if not verbose:
|
|
541
|
+
hud_console.info("")
|
|
542
|
+
hud_console.info("Run with --verbose to see full build output:")
|
|
543
|
+
hud_console.command_example("hud build --verbose")
|
|
489
544
|
raise typer.Exit(1)
|
|
490
545
|
|
|
491
546
|
hud_console.success("Built final image with lock file metadata")
|
|
492
547
|
|
|
493
548
|
# NOW get the image ID after the final build
|
|
494
|
-
image_id = get_docker_image_id(
|
|
549
|
+
image_id = get_docker_image_id(version_tag)
|
|
495
550
|
if image_id:
|
|
496
|
-
#
|
|
497
|
-
# Docker IDs come as sha256:hash, we want tag@sha256:hash
|
|
551
|
+
# Store full reference with digest
|
|
498
552
|
if image_id.startswith("sha256:"):
|
|
499
|
-
lock_content["
|
|
553
|
+
lock_content["images"]["full"] = f"{version_tag}@{image_id}"
|
|
500
554
|
else:
|
|
501
|
-
lock_content["
|
|
555
|
+
lock_content["images"]["full"] = f"{version_tag}@sha256:{image_id}"
|
|
502
556
|
|
|
503
|
-
# Update the lock file with the
|
|
557
|
+
# Update the lock file with the full image reference
|
|
504
558
|
with open(lock_path, "w") as f:
|
|
505
559
|
yaml.dump(lock_content, f, default_flow_style=False, sort_keys=False)
|
|
506
560
|
|
|
507
|
-
hud_console.success("Updated lock file with image
|
|
561
|
+
hud_console.success("Updated lock file with image digest")
|
|
508
562
|
else:
|
|
509
|
-
hud_console.warning("Could not retrieve image
|
|
563
|
+
hud_console.warning("Could not retrieve image digest")
|
|
510
564
|
|
|
511
565
|
# Remove temp image after we're done
|
|
512
566
|
subprocess.run(["docker", "rmi", "-f", temp_tag], capture_output=True) # noqa: S603, S607
|
|
@@ -514,15 +568,21 @@ def build_environment(
|
|
|
514
568
|
# Add to local registry
|
|
515
569
|
if image_id:
|
|
516
570
|
# Save to local registry using the helper
|
|
517
|
-
|
|
571
|
+
local_ref = lock_content.get("images", {}).get("local", version_tag)
|
|
572
|
+
save_to_registry(lock_content, local_ref, verbose)
|
|
518
573
|
|
|
519
574
|
# Print summary
|
|
520
575
|
hud_console.section_title("Build Complete")
|
|
521
576
|
|
|
522
577
|
# Show the version tag as primary since that's what will be pushed
|
|
523
578
|
hud_console.status_item("Built image", version_tag, primary=True)
|
|
524
|
-
|
|
525
|
-
|
|
579
|
+
|
|
580
|
+
# Show additional tags
|
|
581
|
+
additional_tags = [latest_tag]
|
|
582
|
+
if image_tag and image_tag not in [version_tag, latest_tag]:
|
|
583
|
+
additional_tags.append(image_tag)
|
|
584
|
+
hud_console.status_item("Also tagged", ", ".join(additional_tags))
|
|
585
|
+
|
|
526
586
|
hud_console.status_item("Version", new_version)
|
|
527
587
|
hud_console.status_item("Lock file", "hud.lock.yaml")
|
|
528
588
|
hud_console.status_item("Tools found", str(analysis["toolCount"]))
|
|
@@ -534,7 +594,7 @@ def build_environment(
|
|
|
534
594
|
hud_console.section_title("Next Steps")
|
|
535
595
|
hud_console.info("Test locally:")
|
|
536
596
|
hud_console.command_example("hud dev", "Hot-reload development")
|
|
537
|
-
hud_console.command_example(f"hud run {
|
|
597
|
+
hud_console.command_example(f"hud run {latest_tag}", "Run the built image")
|
|
538
598
|
hud_console.info("")
|
|
539
599
|
hud_console.info("Publish to registry:")
|
|
540
600
|
hud_console.command_example("hud push", f"Push as {version_tag}")
|