hud-python 0.4.48__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/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
- await client.initialize()
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 pyproject.toml
290
- pyproject_path = env_dir / "pyproject.toml"
291
- if not pyproject_path.exists():
292
- hud_console.error(f"No pyproject.toml found in {directory}")
293
- raise typer.Exit(1)
294
-
295
- # Read pyproject.toml to get image name
296
- try:
297
- import toml
298
-
299
- pyproject = toml.load(pyproject_path)
300
- default_image = pyproject.get("tool", {}).get("hud", {}).get("image", None)
301
- if not default_image:
302
- # Generate default from directory name
303
- default_image = f"{env_dir.name}:dev"
304
- except Exception:
305
- default_image = f"{env_dir.name}:dev"
306
-
307
- # Determine final image tag to use
308
- image_tag: str = tag if tag else default_image
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
- # Create lock file content - minimal and useful
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.0", # Lock file format version
384
- "image": tag, # Will be updated with ID/digest later
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.utcnow().isoformat() + "Z",
430
+ "generatedAt": datetime.now(UTC).isoformat() + "Z",
387
431
  "hudVersion": hud_version,
388
432
  "directory": str(env_dir.name),
389
- "version": new_version, # Internal environment 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
- # Also tag with version
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
- image_tag,
513
+ version_tag, # Always tag with new version
470
514
  "-t",
471
- version_tag,
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
- # Hide output when not verbose
530
+ # Capture output for error reporting, but don't show unless it fails
483
531
  result = subprocess.run( # noqa: S603
484
- label_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False
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(image_tag)
549
+ image_id = get_docker_image_id(version_tag)
495
550
  if image_id:
496
- # For local builds, store the image ID
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["image"] = f"{image_tag}@{image_id}"
553
+ lock_content["images"]["full"] = f"{version_tag}@{image_id}"
500
554
  else:
501
- lock_content["image"] = f"{image_tag}@sha256:{image_id}"
555
+ lock_content["images"]["full"] = f"{version_tag}@sha256:{image_id}"
502
556
 
503
- # Update the lock file with the new image reference
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 ID")
561
+ hud_console.success("Updated lock file with image digest")
508
562
  else:
509
- hud_console.warning("Could not retrieve image ID for lock file")
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
- save_to_registry(lock_content, lock_content.get("image", tag), verbose)
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
- if image_tag:
525
- hud_console.status_item("Also tagged", image_tag)
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 {image_tag}", "Run the built image")
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}")