clonebox 1.1.15__py3-none-any.whl → 1.1.16__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.
clonebox/cli.py CHANGED
@@ -1930,7 +1930,10 @@ def cmd_test(args):
1930
1930
  if is_accessible == "yes":
1931
1931
  console.print(f"[green]✅ {guest_path} (mount)[/]")
1932
1932
  else:
1933
- console.print(f"[red]❌ {guest_path} (mount inaccessible)[/]")
1933
+ if cloud_init_running:
1934
+ console.print(f"[yellow]⏳ {guest_path} (mount pending)[/]")
1935
+ else:
1936
+ console.print(f"[red]❌ {guest_path} (mount inaccessible)[/]")
1934
1937
  except Exception:
1935
1938
  console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1936
1939
 
@@ -1943,7 +1946,10 @@ def cmd_test(args):
1943
1946
  if is_accessible == "yes":
1944
1947
  console.print(f"[green]✅ {guest_path} (copied)[/]")
1945
1948
  else:
1946
- console.print(f"[red]❌ {guest_path} (copy missing)[/]")
1949
+ if cloud_init_running:
1950
+ console.print(f"[yellow]⏳ {guest_path} (copy pending)[/]")
1951
+ else:
1952
+ console.print(f"[red]❌ {guest_path} (copy missing)[/]")
1947
1953
  except Exception:
1948
1954
  console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1949
1955
  else:
@@ -2349,6 +2355,68 @@ def load_clonebox_config(path: Path) -> dict:
2349
2355
  return config
2350
2356
 
2351
2357
 
2358
+ def _exec_in_vm_qga(vm_name: str, conn_uri: str, command: str) -> Optional[str]:
2359
+ """Internal helper to execute command via QGA and get output."""
2360
+ import subprocess
2361
+ import json
2362
+ import base64
2363
+ import time
2364
+
2365
+ try:
2366
+ # 1. Start execution
2367
+ cmd_json = {
2368
+ "execute": "guest-exec",
2369
+ "arguments": {
2370
+ "path": "/bin/sh",
2371
+ "arg": ["-c", command],
2372
+ "capture-output": True
2373
+ }
2374
+ }
2375
+
2376
+ result = subprocess.run(
2377
+ ["virsh", "--connect", conn_uri, "qemu-agent-command", vm_name, json.dumps(cmd_json)],
2378
+ capture_output=True,
2379
+ text=True,
2380
+ timeout=5
2381
+ )
2382
+
2383
+ if result.returncode != 0:
2384
+ return None
2385
+
2386
+ resp = json.loads(result.stdout)
2387
+ pid = resp.get("return", {}).get("pid")
2388
+ if not pid:
2389
+ return None
2390
+
2391
+ # 2. Wait and get status (quick check)
2392
+ status_json = {"execute": "guest-exec-status", "arguments": {"pid": pid}}
2393
+
2394
+ for _ in range(3): # Try a few times
2395
+ status_result = subprocess.run(
2396
+ ["virsh", "--connect", conn_uri, "qemu-agent-command", vm_name, json.dumps(status_json)],
2397
+ capture_output=True,
2398
+ text=True,
2399
+ timeout=5
2400
+ )
2401
+
2402
+ if status_result.returncode != 0:
2403
+ continue
2404
+
2405
+ status_resp = json.loads(status_result.stdout)
2406
+ ret = status_resp.get("return", {})
2407
+
2408
+ if ret.get("exited"):
2409
+ if "out-data" in ret:
2410
+ return base64.b64decode(ret["out-data"]).decode().strip()
2411
+ return ""
2412
+
2413
+ time.sleep(0.5)
2414
+
2415
+ return None
2416
+ except Exception:
2417
+ return None
2418
+
2419
+
2352
2420
  def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout: int = 900):
2353
2421
  """Monitor cloud-init status in VM and show progress."""
2354
2422
  import subprocess
@@ -2358,6 +2426,8 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2358
2426
  start_time = time.time()
2359
2427
  shutdown_count = 0 # Count consecutive shutdown detections
2360
2428
  restart_detected = False
2429
+ last_phases = []
2430
+ seen_lines = set()
2361
2431
 
2362
2432
  with Progress(
2363
2433
  SpinnerColumn(),
@@ -2390,7 +2460,7 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2390
2460
  restart_detected = True
2391
2461
  progress.update(
2392
2462
  task,
2393
- description="[yellow]⟳ VM restarting after package installation...",
2463
+ description="[yellow]⟳ VM restarting after installation...",
2394
2464
  )
2395
2465
  time.sleep(3)
2396
2466
  continue
@@ -2403,7 +2473,7 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2403
2473
  if restart_detected and "running" in vm_state and shutdown_count >= 3:
2404
2474
  # VM restarted successfully - GUI should be ready
2405
2475
  progress.update(
2406
- task, description=f"[green]✓ GUI ready! Total time: {minutes}m {seconds}s"
2476
+ task, description=f"[green]✓ VM ready! Total time: {minutes}m {seconds}s"
2407
2477
  )
2408
2478
  time.sleep(2)
2409
2479
  break
@@ -2420,15 +2490,44 @@ def monitor_cloud_init_status(vm_name: str, user_session: bool = False, timeout:
2420
2490
  else:
2421
2491
  remaining = "almost done"
2422
2492
 
2493
+ # Try to get real-time progress via QGA
2494
+ # Get last 3 unique progress lines
2495
+ raw_info = _exec_in_vm_qga(
2496
+ vm_name,
2497
+ conn_uri,
2498
+ "grep -E '\\[[0-9]/[0-9]\\]|→' /var/log/cloud-init-output.log 2>/dev/null | tail -n 5"
2499
+ )
2500
+
2501
+ if raw_info:
2502
+ lines = [l.strip() for l in raw_info.strip().split('\n') if l.strip()]
2503
+ for line in lines:
2504
+ if line not in seen_lines:
2505
+ # If it's a new phase line, we can log it to console above the progress bar
2506
+ if "[" in line and "/9]" in line:
2507
+ console.print(f"[dim] {line}[/]")
2508
+ seen_lines.add(line)
2509
+ last_phases.append(line)
2510
+
2511
+ # Keep only last 2 for the progress bar description
2512
+ if len(last_phases) > 2:
2513
+ last_phases = last_phases[-2:]
2514
+
2423
2515
  if restart_detected:
2424
2516
  progress.update(
2425
2517
  task,
2426
- description=f"[cyan]Starting GUI... ({minutes}m {seconds}s, {remaining})",
2518
+ description=f"[cyan]Finalizing setup... ({minutes}m {seconds}s, {remaining})",
2519
+ )
2520
+ elif last_phases:
2521
+ # Show the actual phase from logs
2522
+ current_status = last_phases[-1]
2523
+ progress.update(
2524
+ task,
2525
+ description=f"[cyan]{current_status} ({minutes}m {seconds}s, {remaining})",
2427
2526
  )
2428
2527
  else:
2429
2528
  progress.update(
2430
2529
  task,
2431
- description=f"[cyan]Installing desktop packages... ({minutes}m {seconds}s, {remaining})",
2530
+ description=f"[cyan]Installing packages... ({minutes}m {seconds}s, {remaining})",
2432
2531
  )
2433
2532
 
2434
2533
  except (subprocess.TimeoutExpired, Exception) as e:
clonebox/cloner.py CHANGED
@@ -1053,7 +1053,11 @@ if [ -n "$(echo "$COPIED_PATHS" | tr -d '[:space:]')" ]; then
1053
1053
  while IFS= read -r p; do
1054
1054
  [ -z "$p" ] && continue
1055
1055
  if [ -d "$p" ]; then
1056
- ok "$p copied"
1056
+ if [ "$(ls -A "$p" 2>/dev/null | wc -l)" -gt 0 ]; then
1057
+ ok "$p copied"
1058
+ else
1059
+ ok "$p copied (empty)"
1060
+ fi
1057
1061
  else
1058
1062
  fail "$p missing (copy)"
1059
1063
  fi
@@ -1118,6 +1122,12 @@ fi
1118
1122
  mount_checks = []
1119
1123
  for idx, (host_path, guest_path) in enumerate(config.paths.items()):
1120
1124
  mount_checks.append(f'check_mount "{guest_path}" "mount{idx}"')
1125
+
1126
+ # Add copied paths checks
1127
+ copy_paths = config.copy_paths or config.app_data_paths
1128
+ if copy_paths:
1129
+ for idx, (host_path, guest_path) in enumerate(copy_paths.items()):
1130
+ mount_checks.append(f'check_copy_path "{guest_path}"')
1121
1131
 
1122
1132
  apt_checks_str = "\n".join(apt_checks) if apt_checks else "echo 'No apt packages to check'"
1123
1133
  snap_checks_str = (
@@ -1224,6 +1234,30 @@ check_mount() {{
1224
1234
  log "[INFO] Mount point '$path' does not exist yet"
1225
1235
  return 0
1226
1236
  fi
1237
+ }
1238
+
1239
+ check_copy_path() {
1240
+ local path="$1"
1241
+ if [ -d "$path" ]; then
1242
+ if [ "$(ls -A "$path" 2>/dev/null | wc -l)" -gt 0 ]; then
1243
+ log "[PASS] Path '$path' exists and contains data"
1244
+ ((PASSED++))
1245
+ return 0
1246
+ else
1247
+ log "[WARN] Path '$path' exists but is EMPTY"
1248
+ ((WARNINGS++))
1249
+ return 1
1250
+ fi
1251
+ else
1252
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1253
+ log "[INFO] Path '$path' not imported yet"
1254
+ return 0
1255
+ else
1256
+ log "[FAIL] Path '$path' MISSING"
1257
+ ((FAILED++))
1258
+ return 1
1259
+ fi
1260
+ fi
1227
1261
  }}
1228
1262
 
1229
1263
  check_gui() {{
@@ -1408,37 +1442,38 @@ fi
1408
1442
 
1409
1443
  # Handle copy_paths (import then copy)
1410
1444
  all_copy_paths = dict(config.copy_paths) if config.copy_paths else {}
1411
- for idx, (host_path, guest_path) in enumerate(all_copy_paths.items()):
1412
- if Path(host_path).exists():
1413
- tag = f"import{idx}"
1414
- temp_mount_point = f"/mnt/import{idx}"
1415
- # Use regular mount options
1416
- mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000"
1417
-
1418
- # 1. Create temp mount point
1419
- mount_commands.append(f" - mkdir -p {temp_mount_point}")
1420
-
1421
- # 2. Mount the 9p share
1422
- mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {temp_mount_point} || true")
1423
-
1424
- # 3. Ensure target directory exists and permissions are prepared
1425
- if str(guest_path).startswith("/home/ubuntu/"):
1426
- mount_commands.append(f" - mkdir -p {guest_path}")
1427
- mount_commands.append(f" - chown 1000:1000 {guest_path}")
1428
- else:
1429
- mount_commands.append(f" - mkdir -p {guest_path}")
1445
+ existing_copy_paths = {h: g for h, g in all_copy_paths.items() if Path(h).exists()}
1446
+
1447
+ for idx, (host_path, guest_path) in enumerate(existing_copy_paths.items()):
1448
+ tag = f"import{idx}"
1449
+ temp_mount_point = f"/mnt/import{idx}"
1450
+ # Use regular mount options
1451
+ mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000"
1452
+
1453
+ # 1. Create temp mount point
1454
+ mount_commands.append(f" - mkdir -p {temp_mount_point}")
1455
+
1456
+ # 2. Mount the 9p share
1457
+ mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {temp_mount_point} || true")
1458
+
1459
+ # 3. Ensure target directory exists and permissions are prepared
1460
+ if str(guest_path).startswith("/home/ubuntu/"):
1461
+ mount_commands.append(f" - mkdir -p {guest_path}")
1462
+ mount_commands.append(f" - chown 1000:1000 {guest_path}")
1463
+ else:
1464
+ mount_commands.append(f" - mkdir -p {guest_path}")
1430
1465
 
1431
- # 4. Copy contents (cp -rT to copy contents of source to target)
1432
- # We use || true to ensure boot continues even if copy fails
1433
- mount_commands.append(f" - echo 'Importing {host_path} to {guest_path}...'")
1434
- mount_commands.append(f" - cp -rT {temp_mount_point} {guest_path} || true")
1435
-
1436
- # 5. Fix ownership recursively
1437
- mount_commands.append(f" - chown -R 1000:1000 {guest_path}")
1466
+ # 4. Copy contents (cp -rT to copy contents of source to target)
1467
+ # We use || true to ensure boot continues even if copy fails
1468
+ mount_commands.append(f" - echo 'Importing {host_path} to {guest_path}...'")
1469
+ mount_commands.append(f" - cp -rT {temp_mount_point} {guest_path} || true")
1470
+
1471
+ # 5. Fix ownership recursively
1472
+ mount_commands.append(f" - chown -R 1000:1000 {guest_path}")
1438
1473
 
1439
- # 6. Unmount and cleanup
1440
- mount_commands.append(f" - umount {temp_mount_point} || true")
1441
- mount_commands.append(f" - rmdir {temp_mount_point} || true")
1474
+ # 6. Unmount and cleanup
1475
+ mount_commands.append(f" - umount {temp_mount_point} || true")
1476
+ mount_commands.append(f" - rmdir {temp_mount_point} || true")
1442
1477
 
1443
1478
  # User-data
1444
1479
  # Add desktop environment if GUI is enabled
@@ -1457,31 +1492,82 @@ fi
1457
1492
  # Build runcmd - services, mounts, snaps, post_commands
1458
1493
  runcmd_lines = []
1459
1494
 
1495
+ # Add detailed logging header
1496
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1497
+ runcmd_lines.append(" - echo ' CloneBox VM Installation Progress'")
1498
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1499
+ runcmd_lines.append(" - echo ''")
1500
+
1501
+ # Phase 1: APT Packages
1502
+ if all_packages:
1503
+ runcmd_lines.append(f" - echo '[1/9] 📦 Installing APT packages ({len(all_packages)} total)...'")
1504
+ runcmd_lines.append(" - export DEBIAN_FRONTEND=noninteractive")
1505
+ runcmd_lines.append(" - apt-get update")
1506
+ for i, pkg in enumerate(all_packages, 1):
1507
+ runcmd_lines.append(f" - echo ' → [{i}/{len(all_packages)}] Installing {pkg}...'")
1508
+ runcmd_lines.append(f" - apt-get install -y {pkg} || echo ' ⚠️ Failed to install {pkg}'")
1509
+ runcmd_lines.append(" - echo ' ✓ APT packages installed'")
1510
+ runcmd_lines.append(" - echo ''")
1511
+ else:
1512
+ runcmd_lines.append(" - echo '[1/9] 📦 No APT packages to install'")
1513
+ runcmd_lines.append(" - echo ''")
1514
+
1515
+ # Phase 2: Core services
1516
+ runcmd_lines.append(" - echo '[2/9] 🔧 Enabling core services...'")
1517
+ runcmd_lines.append(" - echo ' → qemu-guest-agent'")
1460
1518
  runcmd_lines.append(" - systemctl enable --now qemu-guest-agent || true")
1519
+ runcmd_lines.append(" - echo ' → snapd'")
1461
1520
  runcmd_lines.append(" - systemctl enable --now snapd || true")
1521
+ runcmd_lines.append(" - echo ' → Waiting for snap system seed...'")
1462
1522
  runcmd_lines.append(" - timeout 300 snap wait system seed.loaded || true")
1523
+ runcmd_lines.append(" - echo ' ✓ Core services enabled'")
1524
+ runcmd_lines.append(" - echo ''")
1463
1525
 
1464
- # Add service enablement
1465
- for svc in config.services:
1526
+ # Phase 3: User services
1527
+ runcmd_lines.append(f" - echo '[3/9] 🔧 Enabling user services ({len(config.services)} total)...'")
1528
+ for i, svc in enumerate(config.services, 1):
1529
+ runcmd_lines.append(f" - echo ' → [{i}/{len(config.services)}] {svc}'")
1466
1530
  runcmd_lines.append(f" - systemctl enable --now {svc} || true")
1531
+ runcmd_lines.append(" - echo ' ✓ User services enabled'")
1532
+ runcmd_lines.append(" - echo ''")
1467
1533
 
1468
- # Add fstab entries for persistent mounts after reboot
1534
+ # Phase 4: Filesystem mounts
1535
+ runcmd_lines.append(f" - echo '[4/9] 📁 Mounting shared directories ({len(fstab_entries)} mounts)...'")
1469
1536
  if fstab_entries:
1470
1537
  runcmd_lines.append(
1471
1538
  " - grep -q '^# CloneBox 9p mounts' /etc/fstab || echo '# CloneBox 9p mounts' >> /etc/fstab"
1472
1539
  )
1473
- for entry in fstab_entries:
1540
+ for i, entry in enumerate(fstab_entries, 1):
1541
+ mount_point = entry.split()[1] if len(entry.split()) > 1 else entry
1542
+ runcmd_lines.append(f" - echo ' → [{i}/{len(fstab_entries)}] {mount_point}'")
1474
1543
  runcmd_lines.append(
1475
1544
  f" - grep -qF \"{entry}\" /etc/fstab || echo '{entry}' >> /etc/fstab"
1476
1545
  )
1477
1546
  runcmd_lines.append(" - mount -a || true")
1547
+ runcmd_lines.append(" - echo ' ✓ Mounts configured'")
1548
+ runcmd_lines.append(" - echo ''")
1549
+
1550
+ # Phase 5: Data Import (copied paths)
1551
+ if existing_copy_paths:
1552
+ runcmd_lines.append(f" - echo '[5/9] 📥 Importing data ({len(existing_copy_paths)} paths)...'")
1553
+ # Add mounts (immediate, before reboot)
1554
+ import_count = 0
1555
+ for cmd in mount_commands:
1556
+ if "Importing" in cmd:
1557
+ import_count += 1
1558
+ runcmd_lines.append(cmd.replace("Importing", f" → [{import_count}/{len(existing_copy_paths)}] Importing"))
1559
+ else:
1560
+ runcmd_lines.append(cmd)
1561
+ runcmd_lines.append(" - echo ' ✓ Data import completed'")
1562
+ runcmd_lines.append(" - echo ''")
1563
+ else:
1564
+ runcmd_lines.append(" - echo '[5/9] 📥 No data to import'")
1565
+ runcmd_lines.append(" - echo ''")
1478
1566
 
1479
- # Add mounts (immediate, before reboot)
1480
- for cmd in mount_commands:
1481
- runcmd_lines.append(cmd)
1482
-
1483
- # Create user directories with correct permissions EARLY to avoid race conditions with GDM
1567
+ # Phase 6: GUI Environment Setup
1484
1568
  if config.gui:
1569
+ runcmd_lines.append(" - echo '[6/9] 🖥️ Setting up GUI environment...'")
1570
+ runcmd_lines.append(" - echo ' → Creating user directories'")
1485
1571
  # Create directories that GNOME services need
1486
1572
  runcmd_lines.extend(
1487
1573
  [
@@ -1491,41 +1577,56 @@ fi
1491
1577
  " - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
1492
1578
  " - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
1493
1579
  " - systemctl set-default graphical.target",
1494
- " - systemctl enable --now gdm3 || systemctl enable --now gdm || true",
1495
- " - systemctl start display-manager || true",
1580
+ " - echo ' → Starting display manager'",
1496
1581
  ]
1497
1582
  )
1583
+ runcmd_lines.append(" - systemctl enable --now gdm3 || systemctl enable --now gdm || true")
1584
+ runcmd_lines.append(" - systemctl start display-manager || true")
1585
+ runcmd_lines.append(" - echo ' ✓ GUI environment ready'")
1586
+ runcmd_lines.append(" - echo ''")
1587
+ else:
1588
+ runcmd_lines.append(" - echo '[6/9] 🖥️ No GUI requested'")
1589
+ runcmd_lines.append(" - echo ''")
1498
1590
 
1499
1591
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu || true")
1500
1592
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/snap || true")
1501
1593
 
1502
- # Install snap packages (with retry logic)
1594
+ # Phase 7: Snap packages
1503
1595
  if config.snap_packages:
1504
- runcmd_lines.append(" - echo 'Installing snap packages...'")
1505
- for snap_pkg in config.snap_packages:
1596
+ runcmd_lines.append(f" - echo '[7/9] 📦 Installing snap packages ({len(config.snap_packages)} packages)...'")
1597
+ for i, snap_pkg in enumerate(config.snap_packages, 1):
1598
+ runcmd_lines.append(f" - echo ' → [{i}/{len(config.snap_packages)}] {snap_pkg}'")
1506
1599
  # Try classic first, then strict, with retries
1507
1600
  cmd = (
1508
1601
  f"for i in 1 2 3; do "
1509
- f"snap install {snap_pkg} --classic && break || "
1510
- f"snap install {snap_pkg} && break || "
1511
- f"sleep 10; "
1602
+ f"snap install {snap_pkg} --classic && echo ' ✓ {snap_pkg} installed (classic)' && break || "
1603
+ f"snap install {snap_pkg} && echo ' ✓ {snap_pkg} installed' && break || "
1604
+ f"echo ' ⟳ Retry $i/3...' && sleep 10; "
1512
1605
  f"done"
1513
1606
  )
1514
1607
  runcmd_lines.append(f" - {cmd}")
1608
+ runcmd_lines.append(" - echo ' ✓ Snap packages installed'")
1609
+ runcmd_lines.append(" - echo ''")
1515
1610
 
1516
1611
  # Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
1517
- runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
1612
+ runcmd_lines.append(f" - echo ' 🔌 Connecting snap interfaces...'")
1518
1613
  for snap_pkg in config.snap_packages:
1614
+ runcmd_lines.append(f" - echo ' → {snap_pkg}'")
1519
1615
  interfaces = SNAP_INTERFACES.get(snap_pkg, DEFAULT_SNAP_INTERFACES)
1520
1616
  for iface in interfaces:
1521
1617
  runcmd_lines.append(
1522
1618
  f" - snap connect {snap_pkg}:{iface} :{iface} 2>/dev/null || true"
1523
1619
  )
1524
-
1620
+ runcmd_lines.append(" - echo ' ✓ Snap interfaces connected'")
1525
1621
  runcmd_lines.append(" - systemctl restart snapd || true")
1622
+ runcmd_lines.append(" - echo ''")
1623
+ else:
1624
+ runcmd_lines.append(" - echo '[7/9] 📦 No snap packages to install'")
1625
+ runcmd_lines.append(" - echo ''")
1526
1626
 
1527
1627
  # Add remaining GUI setup if enabled
1528
1628
  if config.gui:
1629
+ runcmd_lines.append(" - echo ' ⚙️ Creating autostart entries...'")
1529
1630
  # Create autostart entries for GUI apps
1530
1631
  autostart_apps = {
1531
1632
  "pycharm-community": (
@@ -1577,15 +1678,28 @@ Comment=CloneBox autostart
1577
1678
 
1578
1679
  # Fix ownership of autostart directory
1579
1680
  runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/.config/autostart")
1681
+ runcmd_lines.append(" - echo ' ✓ Autostart entries created'")
1682
+ runcmd_lines.append(" - echo ''")
1580
1683
 
1581
- # Run user-defined post commands
1684
+ # Phase 8: Post commands
1582
1685
  if config.post_commands:
1583
- runcmd_lines.append(" - echo 'Running post-setup commands...'")
1584
- for cmd in config.post_commands:
1686
+ runcmd_lines.append(f" - echo '[8/9] ⚙️ Running post-setup commands ({len(config.post_commands)} commands)...'")
1687
+ for i, cmd in enumerate(config.post_commands, 1):
1688
+ # Truncate long commands for display
1689
+ display_cmd = cmd[:60] + '...' if len(cmd) > 60 else cmd
1690
+ runcmd_lines.append(f" - echo ' → [{i}/{len(config.post_commands)}] {display_cmd}'")
1585
1691
  runcmd_lines.append(f" - {cmd}")
1692
+ runcmd_lines.append(f" - echo ' ✓ Command {i} completed'")
1693
+ runcmd_lines.append(" - echo ' ✓ Post-setup commands completed'")
1694
+ runcmd_lines.append(" - echo ''")
1695
+ else:
1696
+ runcmd_lines.append(" - echo '[8/9] ⚙️ No post-setup commands'")
1697
+ runcmd_lines.append(" - echo ''")
1586
1698
 
1587
1699
  # Generate health check script
1588
1700
  health_script = self._generate_health_check_script(config)
1701
+ # Phase 9: Health checks and finalization
1702
+ runcmd_lines.append(" - echo '[9/9] 🏥 Running health checks...'")
1589
1703
  runcmd_lines.append(
1590
1704
  f" - echo '{health_script}' | base64 -d > /usr/local/bin/clonebox-health"
1591
1705
  )
@@ -1593,8 +1707,15 @@ Comment=CloneBox autostart
1593
1707
  runcmd_lines.append(
1594
1708
  " - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1 || true"
1595
1709
  )
1710
+ runcmd_lines.append(" - echo ' ✓ Health checks completed'")
1596
1711
  runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
1597
-
1712
+
1713
+ # Final status
1714
+ runcmd_lines.append(" - echo ''")
1715
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1716
+ runcmd_lines.append(" - echo ' ✅ CloneBox VM Installation Complete!'")
1717
+ runcmd_lines.append(" - echo '═══════════════════════════════════════════════════════════'")
1718
+ runcmd_lines.append(" - echo ''")
1598
1719
  # Generate boot diagnostic script (self-healing)
1599
1720
  boot_diag_script = self._generate_boot_diagnostic_script(config)
1600
1721
  runcmd_lines.append(
@@ -2349,7 +2470,8 @@ if __name__ == "__main__":
2349
2470
 
2350
2471
  # Add reboot command at the end if GUI is enabled
2351
2472
  if config.gui:
2352
- runcmd_lines.append(" - echo 'Rebooting in 10 seconds to start GUI...'")
2473
+ runcmd_lines.append(" - echo '🔄 Rebooting in 10 seconds to start GUI...'")
2474
+ runcmd_lines.append(" - echo ' (After reboot, GUI will auto-start)'")
2353
2475
  runcmd_lines.append(" - sleep 10 && reboot")
2354
2476
 
2355
2477
  runcmd_yaml = "\n".join(runcmd_lines) if runcmd_lines else ""
@@ -2397,9 +2519,8 @@ package_update: true
2397
2519
  package_upgrade: false
2398
2520
  {bootcmd_block}
2399
2521
 
2400
- # Install packages (cloud-init waits for completion before runcmd)
2401
- packages:
2402
- {packages_yaml}
2522
+ # Install packages moved to runcmd for better logging
2523
+ packages: []
2403
2524
 
2404
2525
  # Run after packages are installed
2405
2526
  runcmd:
clonebox/validator.py CHANGED
@@ -155,6 +155,7 @@ class VMValidator:
155
155
  mount_table.add_column("Files", justify="right")
156
156
 
157
157
  # Validate bind mounts (paths)
158
+ setup_in_progress = self._setup_in_progress() is True
158
159
  for host_path, guest_path in paths.items():
159
160
  self.results["mounts"]["total"] += 1
160
161
 
@@ -182,6 +183,9 @@ class VMValidator:
182
183
  status_icon = "[red]❌ Inaccessible[/]"
183
184
  self.results["mounts"]["failed"] += 1
184
185
  status = "mounted_but_inaccessible"
186
+ elif setup_in_progress:
187
+ status_icon = "[yellow]⏳ Pending[/]"
188
+ status = "pending"
185
189
  else:
186
190
  status_icon = "[red]❌ Not Mounted[/]"
187
191
  self.results["mounts"]["failed"] += 1
@@ -219,6 +223,9 @@ class VMValidator:
219
223
  status_icon = "[green]✅ Copied[/]"
220
224
  self.results["mounts"]["passed"] += 1
221
225
  status = "pass"
226
+ elif setup_in_progress:
227
+ status_icon = "[yellow]⏳ Pending[/]"
228
+ status = "pending"
222
229
  else:
223
230
  status_icon = "[red]❌ Missing[/]"
224
231
  self.results["mounts"]["failed"] += 1
@@ -255,6 +262,8 @@ class VMValidator:
255
262
  pkg_table.add_column("Status", justify="center")
256
263
  pkg_table.add_column("Version", style="dim")
257
264
 
265
+ setup_in_progress = self._setup_in_progress() is True
266
+
258
267
  for package in packages:
259
268
  self.results["packages"]["total"] += 1
260
269
 
@@ -269,11 +278,17 @@ class VMValidator:
269
278
  {"package": package, "installed": True, "version": version}
270
279
  )
271
280
  else:
272
- pkg_table.add_row(package, "[red]❌ Missing[/]", "")
273
- self.results["packages"]["failed"] += 1
274
- self.results["packages"]["details"].append(
275
- {"package": package, "installed": False, "version": None}
276
- )
281
+ if setup_in_progress:
282
+ pkg_table.add_row(package, "[yellow]⏳ Pending[/]", "")
283
+ self.results["packages"]["details"].append(
284
+ {"package": package, "installed": False, "version": None, "pending": True}
285
+ )
286
+ else:
287
+ pkg_table.add_row(package, "[red]❌ Missing[/]", "")
288
+ self.results["packages"]["failed"] += 1
289
+ self.results["packages"]["details"].append(
290
+ {"package": package, "installed": False, "version": None}
291
+ )
277
292
 
278
293
  self.console.print(pkg_table)
279
294
  self.console.print(
@@ -419,14 +434,14 @@ class VMValidator:
419
434
  else:
420
435
  pid_value = "—"
421
436
 
422
- enabled_icon = "[green]✅[/]" if is_enabled else "[yellow]⚠️[/]"
423
- running_icon = "[green]✅[/]" if is_running else "[red]❌[/]"
437
+ enabled_icon = "[green]✅[/]" if is_enabled else ("[yellow]⏳[/]" if setup_in_progress else "[yellow]⚠️[/]")
438
+ running_icon = "[green]✅[/]" if is_running else ("[yellow]⏳[/]" if setup_in_progress else "[red]❌[/]")
424
439
 
425
440
  svc_table.add_row(service, enabled_icon, running_icon, pid_value, "")
426
441
 
427
442
  if is_enabled and is_running:
428
443
  self.results["services"]["passed"] += 1
429
- else:
444
+ elif not setup_in_progress:
430
445
  self.results["services"]["failed"] += 1
431
446
 
432
447
  self.results["services"]["details"].append(
@@ -994,18 +1009,18 @@ class VMValidator:
994
1009
  return self.results
995
1010
 
996
1011
  ci_status = self._exec_in_vm("cloud-init status --long 2>/dev/null || cloud-init status 2>/dev/null || true", timeout=20)
1012
+ setup_in_progress = False
997
1013
  if ci_status:
998
1014
  ci_lower = ci_status.lower()
999
1015
  if "running" in ci_lower:
1000
- self.console.print("[yellow]⏳ Cloud-init still running - skipping deep validation for now[/]")
1001
- self.results["overall"] = "cloud_init_running"
1002
- return self.results
1016
+ self.console.print("[yellow]⏳ Cloud-init still running - deep validation will show pending states[/]")
1017
+ setup_in_progress = True
1003
1018
 
1004
1019
  ready_msg = self._exec_in_vm(
1005
1020
  "cat /var/log/clonebox-ready 2>/dev/null || true",
1006
1021
  timeout=10,
1007
1022
  )
1008
- if not (ready_msg and "clonebox vm ready" in ready_msg.lower()):
1023
+ if not setup_in_progress and not (ready_msg and "clonebox vm ready" in ready_msg.lower()):
1009
1024
  self.console.print(
1010
1025
  "[yellow]⚠️ CloneBox ready marker not found - provisioning may not have completed[/]"
1011
1026
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.15
3
+ Version: 1.1.16
4
4
  Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
5
5
  Author: CloneBox Team
6
6
  License: Apache-2.0
@@ -1,8 +1,8 @@
1
1
  clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
2
2
  clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
3
  clonebox/audit.py,sha256=1W9vaIjB0A--_p7CgE3cIP5RNckJG1RxJrL-tOb-QmU,14298
4
- clonebox/cli.py,sha256=45-0HJ_T8O5Ggrd42VZTU1LoTOgIidRPqST7jzP23YM,174764
5
- clonebox/cloner.py,sha256=gQeaY_Cu6cKLKh_L0z8yuhB877WWunyKBy3vOzD-IMI,99640
4
+ clonebox/cli.py,sha256=k3q-JrzsiDcGPjhzHbm1jPKajxD2chYJxSGGftCbA20,178481
5
+ clonebox/cloner.py,sha256=ZMeIpEuzT8ZO9fav99JdE3krWT6e06K8pBR7XxDKbos,106958
6
6
  clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
7
7
  clonebox/dashboard.py,sha256=dMY6odvPq3j6FronhRRsX7aY3qdCwznB-aCWKEmHDNw,5768
8
8
  clonebox/detector.py,sha256=vS65cvFNPmUBCX1Y_TMTnSRljw6r1Ae9dlVtACs5XFc,23075
@@ -20,7 +20,7 @@ clonebox/resource_monitor.py,sha256=lDR9KyPbVtImeeOkOBPPVP-5yCgoL5hsVFPZ_UqsY0w,
20
20
  clonebox/resources.py,sha256=IkuM4OdSDV4qhyc0eIynwbAHBTv0aVSxxW-gghsnCAs,6815
21
21
  clonebox/rollback.py,sha256=hpwO-8Ehe1pW0wHuZvJkC_qxZ6yEo9otCJRhGIUArCo,5711
22
22
  clonebox/secrets.py,sha256=l1jwJcEPB1qMoGNLPjyrkKKr1khh9VmftFJI9BWhgK0,10628
23
- clonebox/validator.py,sha256=_glRPL6VlTCnIpPTtFa8_cVApcl1h0jU_uyHCfKraUM,44501
23
+ clonebox/validator.py,sha256=uXBkdrWSk8oBhRnD1y4mwxRdxOPduRraroSIs14bAo8,45342
24
24
  clonebox/backends/libvirt_backend.py,sha256=sIHFIvFO1hIOXEFR_foSkOGBgIzaJVQs-njOU8GdafA,7170
25
25
  clonebox/backends/qemu_disk.py,sha256=YsGjYX5sbEf35Y4yjTpNkZat73a4RGBxY-KTVzJhqIs,1687
26
26
  clonebox/backends/subprocess_runner.py,sha256=c-IyaMxM1cmUu64h654oAvulm83K5Mu-VQxXJ_0BOds,1506
@@ -40,9 +40,9 @@ clonebox/snapshots/manager.py,sha256=hGzM8V6ZJPXjTqj47c4Kr8idlE-c1Q3gPUvuw1HvS1A
40
40
  clonebox/snapshots/models.py,sha256=sRnn3OZE8JG9FZJlRuA3ihO-JXoPCQ3nD3SQytflAao,6206
41
41
  clonebox/templates/profiles/ml-dev.yaml,sha256=w07MToGh31xtxpjbeXTBk9BkpAN8A3gv8HeA3ESKG9M,461
42
42
  clonebox/templates/profiles/web-stack.yaml,sha256=EBnnGMzML5vAjXmIUbCpbTCwmRaNJiuWd3EcL43DOK8,485
43
- clonebox-1.1.15.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
44
- clonebox-1.1.15.dist-info/METADATA,sha256=4gxhtiBARsY1QcnwjPU705upAAXQPvTXNiXHzkZaGpI,49052
45
- clonebox-1.1.15.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
46
- clonebox-1.1.15.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
47
- clonebox-1.1.15.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
48
- clonebox-1.1.15.dist-info/RECORD,,
43
+ clonebox-1.1.16.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
44
+ clonebox-1.1.16.dist-info/METADATA,sha256=mPn_uTUwAXlnakSJc8DLWyg1KlNERP5-oIEyWABxuwA,49052
45
+ clonebox-1.1.16.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
46
+ clonebox-1.1.16.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
47
+ clonebox-1.1.16.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
48
+ clonebox-1.1.16.dist-info/RECORD,,