clonebox 0.1.19__py3-none-any.whl → 0.1.21__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
@@ -653,6 +653,7 @@ def interactive_mode():
653
653
  summary_table.add_row("Name", vm_name)
654
654
  summary_table.add_row("RAM", f"{ram_mb} MB")
655
655
  summary_table.add_row("vCPUs", str(vcpus))
656
+ summary_table.add_row("Disk", f"{20 if enable_gui else 10} GB")
656
657
  summary_table.add_row("Services", ", ".join(selected_services) or "None")
657
658
  summary_table.add_row(
658
659
  "Packages",
@@ -684,6 +685,7 @@ def interactive_mode():
684
685
  name=vm_name,
685
686
  ram_mb=ram_mb,
686
687
  vcpus=vcpus,
688
+ disk_size_gb=20 if enable_gui else 10,
687
689
  gui=enable_gui,
688
690
  base_image=base_image if base_image else None,
689
691
  paths=paths_mapping,
@@ -732,6 +734,7 @@ def cmd_create(args):
732
734
  name=args.name,
733
735
  ram_mb=args.ram,
734
736
  vcpus=args.vcpus,
737
+ disk_size_gb=getattr(args, "disk_size_gb", 10),
735
738
  gui=not args.no_gui,
736
739
  base_image=args.base_image,
737
740
  paths=config_data.get("paths", {}),
@@ -919,6 +922,8 @@ def cmd_delete(args):
919
922
  ).ask():
920
923
  console.print("[yellow]Cancelled.[/]")
921
924
  return
925
+
926
+
922
927
  def cmd_list(args):
923
928
  """List all VMs."""
924
929
  cloner = SelectiveVMCloner(user_session=getattr(args, "user", False))
@@ -1027,6 +1032,19 @@ def cmd_container_down(args):
1027
1032
  cloner.rm(args.name, force=True)
1028
1033
 
1029
1034
 
1035
+ def cmd_dashboard(args):
1036
+ """Run the local CloneBox dashboard."""
1037
+ try:
1038
+ from clonebox.dashboard import run_dashboard
1039
+ except Exception as e:
1040
+ console.print("[red]❌ Dashboard dependencies are not installed.[/]")
1041
+ console.print("[dim]Install with: pip install 'clonebox[dashboard]'[/]")
1042
+ console.print(f"[dim]{e}[/]")
1043
+ return
1044
+
1045
+ run_dashboard(port=getattr(args, "port", 8080))
1046
+
1047
+
1030
1048
  def cmd_diagnose(args):
1031
1049
  """Run detailed VM diagnostics (standalone)."""
1032
1050
  name = args.name
@@ -1693,6 +1711,7 @@ def generate_clonebox_yaml(
1693
1711
  vm_name: str = None,
1694
1712
  network_mode: str = "auto",
1695
1713
  base_image: Optional[str] = None,
1714
+ disk_size_gb: Optional[int] = None,
1696
1715
  ) -> str:
1697
1716
  """Generate YAML config from system snapshot."""
1698
1717
  sys_info = detector.get_system_info()
@@ -1772,6 +1791,21 @@ def generate_clonebox_yaml(
1772
1791
  guest_path = f"/home/ubuntu/{rel_path}"
1773
1792
  app_data_mapping[host_path] = guest_path
1774
1793
 
1794
+ post_commands = []
1795
+
1796
+ chrome_profile = home_dir / ".config" / "google-chrome"
1797
+ if chrome_profile.exists():
1798
+ host_path = str(chrome_profile)
1799
+ if host_path not in paths_mapping and host_path not in app_data_mapping:
1800
+ app_data_mapping[host_path] = "/home/ubuntu/.config/google-chrome"
1801
+
1802
+ post_commands.append(
1803
+ "command -v google-chrome >/dev/null 2>&1 || ("
1804
+ "curl -fsSL -o /tmp/google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && "
1805
+ "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y /tmp/google-chrome.deb"
1806
+ ")"
1807
+ )
1808
+
1775
1809
  # Determine VM name
1776
1810
  if not vm_name:
1777
1811
  if target_path:
@@ -1783,6 +1817,9 @@ def generate_clonebox_yaml(
1783
1817
  ram_mb = min(4096, int(sys_info["memory_available_gb"] * 1024 * 0.5))
1784
1818
  vcpus = max(2, sys_info["cpu_count"] // 2)
1785
1819
 
1820
+ if disk_size_gb is None:
1821
+ disk_size_gb = 20
1822
+
1786
1823
  # Auto-detect packages from running applications and services
1787
1824
  app_packages = detector.suggest_packages_for_apps(snapshot.applications)
1788
1825
  service_packages = detector.suggest_packages_for_services(snapshot.running_services)
@@ -1805,6 +1842,34 @@ def generate_clonebox_yaml(
1805
1842
  if deduplicate:
1806
1843
  all_snap_packages = deduplicate_list(all_snap_packages)
1807
1844
 
1845
+ if chrome_profile.exists() and "google-chrome" not in [d.get("app", "") for d in app_data_dirs]:
1846
+ if "chromium" not in all_snap_packages:
1847
+ all_snap_packages.append("chromium")
1848
+
1849
+ if "pycharm-community" in all_snap_packages:
1850
+ remapped = {}
1851
+ for host_path, guest_path in app_data_mapping.items():
1852
+ if guest_path == "/home/ubuntu/.config/JetBrains":
1853
+ remapped[host_path] = "/home/ubuntu/snap/pycharm-community/common/.config/JetBrains"
1854
+ elif guest_path == "/home/ubuntu/.local/share/JetBrains":
1855
+ remapped[host_path] = "/home/ubuntu/snap/pycharm-community/common/.local/share/JetBrains"
1856
+ elif guest_path == "/home/ubuntu/.cache/JetBrains":
1857
+ remapped[host_path] = "/home/ubuntu/snap/pycharm-community/common/.cache/JetBrains"
1858
+ else:
1859
+ remapped[host_path] = guest_path
1860
+ app_data_mapping = remapped
1861
+
1862
+ if "firefox" in all_apt_packages:
1863
+ remapped = {}
1864
+ for host_path, guest_path in app_data_mapping.items():
1865
+ if guest_path == "/home/ubuntu/.mozilla/firefox":
1866
+ remapped[host_path] = "/home/ubuntu/snap/firefox/common/.mozilla/firefox"
1867
+ elif guest_path == "/home/ubuntu/.cache/mozilla/firefox":
1868
+ remapped[host_path] = "/home/ubuntu/snap/firefox/common/.cache/mozilla/firefox"
1869
+ else:
1870
+ remapped[host_path] = guest_path
1871
+ app_data_mapping = remapped
1872
+
1808
1873
  # Build config
1809
1874
  config = {
1810
1875
  "version": "1",
@@ -1813,6 +1878,7 @@ def generate_clonebox_yaml(
1813
1878
  "name": vm_name,
1814
1879
  "ram_mb": ram_mb,
1815
1880
  "vcpus": vcpus,
1881
+ "disk_size_gb": disk_size_gb,
1816
1882
  "gui": True,
1817
1883
  "base_image": base_image,
1818
1884
  "network_mode": network_mode,
@@ -1822,7 +1888,7 @@ def generate_clonebox_yaml(
1822
1888
  "services": services,
1823
1889
  "packages": all_apt_packages,
1824
1890
  "snap_packages": all_snap_packages,
1825
- "post_commands": [], # User can add custom commands to run after setup
1891
+ "post_commands": post_commands,
1826
1892
  "paths": paths_mapping,
1827
1893
  "app_data_paths": app_data_mapping, # App-specific config/data directories
1828
1894
  "detected": {
@@ -1966,6 +2032,7 @@ def create_vm_from_config(
1966
2032
  name=config["vm"]["name"],
1967
2033
  ram_mb=config["vm"].get("ram_mb", 4096),
1968
2034
  vcpus=config["vm"].get("vcpus", 4),
2035
+ disk_size_gb=config["vm"].get("disk_size_gb", 10),
1969
2036
  gui=config["vm"].get("gui", True),
1970
2037
  base_image=config["vm"].get("base_image"),
1971
2038
  paths=all_paths,
@@ -2045,6 +2112,7 @@ def cmd_clone(args):
2045
2112
  vm_name=vm_name,
2046
2113
  network_mode=args.network,
2047
2114
  base_image=getattr(args, "base_image", None),
2115
+ disk_size_gb=getattr(args, "disk_size_gb", None),
2048
2116
  )
2049
2117
 
2050
2118
  profile_name = getattr(args, "profile", None)
@@ -2287,6 +2355,12 @@ def main():
2287
2355
  )
2288
2356
  create_parser.add_argument("--ram", type=int, default=4096, help="RAM in MB")
2289
2357
  create_parser.add_argument("--vcpus", type=int, default=4, help="Number of vCPUs")
2358
+ create_parser.add_argument(
2359
+ "--disk-size-gb",
2360
+ type=int,
2361
+ default=10,
2362
+ help="Root disk size in GB (default: 10)",
2363
+ )
2290
2364
  create_parser.add_argument("--base-image", help="Path to base qcow2 image")
2291
2365
  create_parser.add_argument("--no-gui", action="store_true", help="Disable SPICE graphics")
2292
2366
  create_parser.add_argument("--start", "-s", action="store_true", help="Start VM after creation")
@@ -2368,6 +2442,12 @@ def main():
2368
2442
  container_sub = container_parser.add_subparsers(dest="container_command", help="Container commands")
2369
2443
 
2370
2444
  container_up = container_sub.add_parser("up", help="Start container")
2445
+ container_up.add_argument(
2446
+ "--engine",
2447
+ choices=["auto", "podman", "docker"],
2448
+ default=argparse.SUPPRESS,
2449
+ help="Container engine: auto (default), podman, docker",
2450
+ )
2371
2451
  container_up.add_argument("path", nargs="?", default=".", help="Workspace path")
2372
2452
  container_up.add_argument("--name", help="Container name")
2373
2453
  container_up.add_argument("--image", default="ubuntu:22.04", help="Container image")
@@ -2402,23 +2482,52 @@ def main():
2402
2482
  container_up.set_defaults(func=cmd_container_up)
2403
2483
 
2404
2484
  container_ps = container_sub.add_parser("ps", aliases=["ls"], help="List containers")
2485
+ container_ps.add_argument(
2486
+ "--engine",
2487
+ choices=["auto", "podman", "docker"],
2488
+ default=argparse.SUPPRESS,
2489
+ help="Container engine: auto (default), podman, docker",
2490
+ )
2405
2491
  container_ps.add_argument("-a", "--all", action="store_true", help="Show all containers")
2406
2492
  container_ps.add_argument("--json", action="store_true", help="Output JSON")
2407
2493
  container_ps.set_defaults(func=cmd_container_ps)
2408
2494
 
2409
2495
  container_stop = container_sub.add_parser("stop", help="Stop container")
2496
+ container_stop.add_argument(
2497
+ "--engine",
2498
+ choices=["auto", "podman", "docker"],
2499
+ default=argparse.SUPPRESS,
2500
+ help="Container engine: auto (default), podman, docker",
2501
+ )
2410
2502
  container_stop.add_argument("name", help="Container name")
2411
2503
  container_stop.set_defaults(func=cmd_container_stop)
2412
2504
 
2413
2505
  container_rm = container_sub.add_parser("rm", help="Remove container")
2506
+ container_rm.add_argument(
2507
+ "--engine",
2508
+ choices=["auto", "podman", "docker"],
2509
+ default=argparse.SUPPRESS,
2510
+ help="Container engine: auto (default), podman, docker",
2511
+ )
2414
2512
  container_rm.add_argument("name", help="Container name")
2415
2513
  container_rm.add_argument("-f", "--force", action="store_true", help="Force remove")
2416
2514
  container_rm.set_defaults(func=cmd_container_rm)
2417
2515
 
2418
2516
  container_down = container_sub.add_parser("down", help="Stop and remove container")
2517
+ container_down.add_argument(
2518
+ "--engine",
2519
+ choices=["auto", "podman", "docker"],
2520
+ default=argparse.SUPPRESS,
2521
+ help="Container engine: auto (default), podman, docker",
2522
+ )
2419
2523
  container_down.add_argument("name", help="Container name")
2420
2524
  container_down.set_defaults(func=cmd_container_down)
2421
2525
 
2526
+ # Dashboard command
2527
+ dashboard_parser = subparsers.add_parser("dashboard", help="Run local dashboard")
2528
+ dashboard_parser.add_argument("--port", type=int, default=8080, help="Port to bind (default: 8080)")
2529
+ dashboard_parser.set_defaults(func=cmd_dashboard)
2530
+
2422
2531
  # Detect command
2423
2532
  detect_parser = subparsers.add_parser("detect", help="Detect system state")
2424
2533
  detect_parser.add_argument("--json", action="store_true", help="Output as JSON")
@@ -2458,6 +2567,12 @@ def main():
2458
2567
  "--base-image",
2459
2568
  help="Path to a bootable qcow2 image to use as a base disk",
2460
2569
  )
2570
+ clone_parser.add_argument(
2571
+ "--disk-size-gb",
2572
+ type=int,
2573
+ default=None,
2574
+ help="Root disk size in GB (default: 20 for generated configs)",
2575
+ )
2461
2576
  clone_parser.add_argument(
2462
2577
  "--profile",
2463
2578
  help="Profile name (loads ~/.clonebox.d/<name>.yaml, .clonebox.d/<name>.yaml, or built-in templates)",
clonebox/cloner.py CHANGED
@@ -18,6 +18,16 @@ try:
18
18
  except ImportError:
19
19
  libvirt = None
20
20
 
21
+ SNAP_INTERFACES = {
22
+ 'pycharm-community': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'network-bind', 'cups-control', 'removable-media'],
23
+ 'chromium': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'audio-playback', 'camera'],
24
+ 'firefox': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'audio-playback', 'removable-media'],
25
+ 'code': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'ssh-keys'],
26
+ 'slack': ['desktop', 'desktop-legacy', 'x11', 'wayland', 'home', 'network', 'audio-playback'],
27
+ 'spotify': ['desktop', 'x11', 'wayland', 'home', 'network', 'audio-playback'],
28
+ }
29
+ DEFAULT_SNAP_INTERFACES = ['desktop', 'desktop-legacy', 'x11', 'home', 'network']
30
+
21
31
 
22
32
  @dataclass
23
33
  class VMConfig:
@@ -26,7 +36,7 @@ class VMConfig:
26
36
  name: str = "clonebox-vm"
27
37
  ram_mb: int = 4096
28
38
  vcpus: int = 4
29
- disk_size_gb: int = 10
39
+ disk_size_gb: int = 20
30
40
  gui: bool = True
31
41
  base_image: Optional[str] = None
32
42
  paths: dict = field(default_factory=dict)
@@ -680,7 +690,7 @@ fi
680
690
 
681
691
  # User-data
682
692
  # Add desktop environment if GUI is enabled
683
- base_packages = ["qemu-guest-agent"]
693
+ base_packages = ["qemu-guest-agent", "cloud-guest-utils"]
684
694
  if config.gui:
685
695
  base_packages.extend([
686
696
  "ubuntu-desktop-minimal",
@@ -696,6 +706,8 @@ fi
696
706
  runcmd_lines = []
697
707
 
698
708
  runcmd_lines.append(" - systemctl enable --now qemu-guest-agent || true")
709
+ runcmd_lines.append(" - systemctl enable --now snapd || true")
710
+ runcmd_lines.append(" - timeout 300 snap wait system seed.loaded || true")
699
711
 
700
712
  # Add service enablement
701
713
  for svc in config.services:
@@ -717,6 +729,15 @@ fi
717
729
  runcmd_lines.append(" - echo 'Installing snap packages...'")
718
730
  for snap_pkg in config.snap_packages:
719
731
  runcmd_lines.append(f" - snap install {snap_pkg} --classic || snap install {snap_pkg} || true")
732
+
733
+ # Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
734
+ runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
735
+ for snap_pkg in config.snap_packages:
736
+ interfaces = SNAP_INTERFACES.get(snap_pkg, DEFAULT_SNAP_INTERFACES)
737
+ for iface in interfaces:
738
+ runcmd_lines.append(f" - snap connect {snap_pkg}:{iface} :{iface} 2>/dev/null || true")
739
+
740
+ runcmd_lines.append(" - systemctl restart snapd || true")
720
741
 
721
742
  # Add GUI setup if enabled - runs AFTER package installation completes
722
743
  if config.gui:
@@ -760,7 +781,7 @@ users:
760
781
  sudo: ALL=(ALL) NOPASSWD:ALL
761
782
  shell: /bin/bash
762
783
  lock_passwd: false
763
- groups: sudo,adm,dialout,cdrom,floppy,audio,dip,video,plugdev,netdev
784
+ groups: sudo,adm,dialout,cdrom,floppy,audio,dip,video,plugdev,netdev,docker
764
785
  plain_text_passwd: {config.password}
765
786
 
766
787
  # Allow password authentication
@@ -768,6 +789,13 @@ ssh_pwauth: true
768
789
  chpasswd:
769
790
  expire: false
770
791
 
792
+ # Make sure root partition + filesystem grows to fill the qcow2 disk size
793
+ growpart:
794
+ mode: auto
795
+ devices: ["/"]
796
+ ignore_growroot_disabled: false
797
+ resize_rootfs: true
798
+
771
799
  # Update package cache and upgrade
772
800
  package_update: true
773
801
  package_upgrade: false
clonebox/dashboard.py ADDED
@@ -0,0 +1,133 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from typing import Any, List
5
+
6
+ from fastapi import FastAPI
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
+
9
+ app = FastAPI(title="CloneBox Dashboard")
10
+
11
+
12
+ def _run_clonebox(args: List[str]) -> subprocess.CompletedProcess:
13
+ return subprocess.run(
14
+ [sys.executable, "-m", "clonebox"] + args,
15
+ capture_output=True,
16
+ text=True,
17
+ )
18
+
19
+
20
+ def _render_table(title: str, headers: List[str], rows: List[List[str]]) -> str:
21
+ head_html = "".join(f"<th>{h}</th>" for h in headers)
22
+ body_html = "".join(
23
+ "<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>" for row in rows
24
+ )
25
+
26
+ return (
27
+ f"<h2>{title}</h2>"
28
+ "<table>"
29
+ f"<thead><tr>{head_html}</tr></thead>"
30
+ f"<tbody>{body_html}</tbody>"
31
+ "</table>"
32
+ )
33
+
34
+
35
+ @app.get("/", response_class=HTMLResponse)
36
+ async def dashboard() -> str:
37
+ return """
38
+ <!DOCTYPE html>
39
+ <html>
40
+ <head>
41
+ <title>CloneBox Dashboard</title>
42
+ <script src="https://unpkg.com/htmx.org@1.9.10"></script>
43
+ <style>
44
+ body { font-family: system-ui, -apple-system, sans-serif; margin: 20px; }
45
+ table { border-collapse: collapse; width: 100%; margin-bottom: 24px; }
46
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
47
+ th { background: #f6f6f6; }
48
+ code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; }
49
+ </style>
50
+ </head>
51
+ <body>
52
+ <h1>CloneBox Dashboard</h1>
53
+ <p>Auto-refresh every 5s. JSON endpoints: <code>/api/vms.json</code>, <code>/api/containers.json</code></p>
54
+
55
+ <div id="vms" hx-get="/api/vms" hx-trigger="load, every 5s">Loading VMs...</div>
56
+ <div id="containers" hx-get="/api/containers" hx-trigger="load, every 5s">Loading containers...</div>
57
+ </body>
58
+ </html>
59
+ """
60
+
61
+
62
+ @app.get("/api/vms", response_class=HTMLResponse)
63
+ async def api_vms() -> str:
64
+ proc = _run_clonebox(["list", "--json"])
65
+ if proc.returncode != 0:
66
+ return f"<pre>clonebox list failed:\n{proc.stderr}</pre>"
67
+
68
+ try:
69
+ items: List[dict[str, Any]] = json.loads(proc.stdout or "[]")
70
+ except json.JSONDecodeError:
71
+ return f"<pre>Invalid JSON from clonebox list:\n{proc.stdout}</pre>"
72
+
73
+ if not items:
74
+ return "<h2>VMs</h2><p><em>No VMs found.</em></p>"
75
+
76
+ rows = [[str(i.get("name", "")), str(i.get("state", "")), str(i.get("uuid", ""))] for i in items]
77
+ return _render_table("VMs", ["Name", "State", "UUID"], rows)
78
+
79
+
80
+ @app.get("/api/containers", response_class=HTMLResponse)
81
+ async def api_containers() -> str:
82
+ proc = _run_clonebox(["container", "ps", "--json", "-a"])
83
+ if proc.returncode != 0:
84
+ return f"<pre>clonebox container ps failed:\n{proc.stderr}</pre>"
85
+
86
+ try:
87
+ items: List[dict[str, Any]] = json.loads(proc.stdout or "[]")
88
+ except json.JSONDecodeError:
89
+ return f"<pre>Invalid JSON from clonebox container ps:\n{proc.stdout}</pre>"
90
+
91
+ if not items:
92
+ return "<h2>Containers</h2><p><em>No containers found.</em></p>"
93
+
94
+ rows = [
95
+ [
96
+ str(i.get("name", "")),
97
+ str(i.get("image", "")),
98
+ str(i.get("status", "")),
99
+ str(i.get("ports", "")),
100
+ ]
101
+ for i in items
102
+ ]
103
+ return _render_table("Containers", ["Name", "Image", "Status", "Ports"], rows)
104
+
105
+
106
+ @app.get("/api/vms.json")
107
+ async def api_vms_json() -> JSONResponse:
108
+ proc = _run_clonebox(["list", "--json"])
109
+ if proc.returncode != 0:
110
+ return JSONResponse({"error": proc.stderr, "stdout": proc.stdout}, status_code=500)
111
+
112
+ try:
113
+ return JSONResponse(json.loads(proc.stdout or "[]"))
114
+ except json.JSONDecodeError:
115
+ return JSONResponse({"error": "invalid_json", "stdout": proc.stdout}, status_code=500)
116
+
117
+
118
+ @app.get("/api/containers.json")
119
+ async def api_containers_json() -> JSONResponse:
120
+ proc = _run_clonebox(["container", "ps", "--json", "-a"])
121
+ if proc.returncode != 0:
122
+ return JSONResponse({"error": proc.stderr, "stdout": proc.stdout}, status_code=500)
123
+
124
+ try:
125
+ return JSONResponse(json.loads(proc.stdout or "[]"))
126
+ except json.JSONDecodeError:
127
+ return JSONResponse({"error": "invalid_json", "stdout": proc.stdout}, status_code=500)
128
+
129
+
130
+ def run_dashboard(port: int = 8080) -> None:
131
+ import uvicorn
132
+
133
+ uvicorn.run(app, host="127.0.0.1", port=port)
clonebox/detector.py CHANGED
@@ -275,12 +275,24 @@ class SystemDetector:
275
275
  # Browsers - profiles, extensions, bookmarks, passwords
276
276
  "chrome": [".config/google-chrome", ".config/chromium"],
277
277
  "chromium": [".config/chromium"],
278
- "firefox": [".mozilla/firefox", ".cache/mozilla/firefox"],
278
+ "firefox": [
279
+ "snap/firefox/common/.mozilla/firefox",
280
+ "snap/firefox/common/.cache/mozilla/firefox",
281
+ ".mozilla/firefox",
282
+ ".cache/mozilla/firefox",
283
+ ],
279
284
 
280
285
  # IDEs and editors - settings, extensions, projects history
281
286
  "code": [".config/Code", ".vscode", ".vscode-server"],
282
287
  "vscode": [".config/Code", ".vscode", ".vscode-server"],
283
- "pycharm": [".config/JetBrains", ".local/share/JetBrains", ".cache/JetBrains"],
288
+ "pycharm": [
289
+ "snap/pycharm-community/common/.config/JetBrains",
290
+ "snap/pycharm-community/common/.local/share/JetBrains",
291
+ "snap/pycharm-community/common/.cache/JetBrains",
292
+ ".config/JetBrains",
293
+ ".local/share/JetBrains",
294
+ ".cache/JetBrains",
295
+ ],
284
296
  "idea": [".config/JetBrains", ".local/share/JetBrains"],
285
297
  "webstorm": [".config/JetBrains", ".local/share/JetBrains"],
286
298
  "goland": [".config/JetBrains", ".local/share/JetBrains"],
@@ -354,27 +366,40 @@ class SystemDetector:
354
366
  app_data_paths = []
355
367
  seen_paths = set()
356
368
 
369
+ matched_patterns = set()
370
+
357
371
  for app in applications:
358
372
  app_name = app.name.lower()
359
-
360
- # Check each known app pattern
361
- for pattern, dirs in self.APP_DATA_DIRS.items():
373
+
374
+ for pattern in self.APP_DATA_DIRS:
362
375
  if pattern in app_name:
363
- for dir_name in dirs:
364
- full_path = self.home / dir_name
365
- if full_path.exists() and str(full_path) not in seen_paths:
366
- seen_paths.add(str(full_path))
367
- # Calculate size
368
- try:
369
- size = self._get_dir_size(full_path, max_depth=2)
370
- except:
371
- size = 0
372
- app_data_paths.append({
373
- "path": str(full_path),
374
- "app": app.name,
375
- "type": "app_data",
376
- "size_mb": round(size / 1024 / 1024, 1)
377
- })
376
+ matched_patterns.add(pattern)
377
+
378
+ for pattern in ("firefox", "chrome", "chromium", "pycharm"):
379
+ matched_patterns.add(pattern)
380
+
381
+ for pattern in sorted(matched_patterns):
382
+ dirs = self.APP_DATA_DIRS.get(pattern, [])
383
+ if not dirs:
384
+ continue
385
+
386
+ snap_dirs = [d for d in dirs if d.startswith("snap/")]
387
+ preferred_dirs = snap_dirs if any((self.home / d).exists() for d in snap_dirs) else dirs
388
+
389
+ for dir_name in preferred_dirs:
390
+ full_path = self.home / dir_name
391
+ if full_path.exists() and str(full_path) not in seen_paths:
392
+ seen_paths.add(str(full_path))
393
+ try:
394
+ size = self._get_dir_size(full_path, max_depth=2)
395
+ except Exception:
396
+ size = 0
397
+ app_data_paths.append({
398
+ "path": str(full_path),
399
+ "app": pattern,
400
+ "type": "app_data",
401
+ "size_mb": round(size / 1024 / 1024, 1),
402
+ })
378
403
 
379
404
  return app_data_paths
380
405
 
clonebox/models.py CHANGED
@@ -16,7 +16,7 @@ class VMSettings(BaseModel):
16
16
  name: str = Field(default="clonebox-vm", description="VM name")
17
17
  ram_mb: int = Field(default=4096, ge=512, le=131072, description="RAM in MB")
18
18
  vcpus: int = Field(default=4, ge=1, le=128, description="Number of vCPUs")
19
- disk_size_gb: int = Field(default=10, ge=1, le=2048, description="Disk size in GB")
19
+ disk_size_gb: int = Field(default=20, ge=1, le=2048, description="Disk size in GB")
20
20
  gui: bool = Field(default=True, description="Enable SPICE graphics")
21
21
  base_image: Optional[str] = Field(default=None, description="Path to base qcow2 image")
22
22
  network_mode: str = Field(default="auto", description="Network mode: auto|default|user")
@@ -107,6 +107,10 @@ class CloneBoxConfig(BaseModel):
107
107
  """Convert to legacy VMConfig dataclass for compatibility."""
108
108
  from clonebox.cloner import VMConfig as VMConfigDataclass
109
109
 
110
+ # Merge paths and app_data_paths
111
+ all_paths = dict(self.paths)
112
+ all_paths.update(self.app_data_paths)
113
+
110
114
  return VMConfigDataclass(
111
115
  name=self.vm.name,
112
116
  ram_mb=self.vm.ram_mb,
@@ -114,7 +118,7 @@ class CloneBoxConfig(BaseModel):
114
118
  disk_size_gb=self.vm.disk_size_gb,
115
119
  gui=self.vm.gui,
116
120
  base_image=self.vm.base_image,
117
- paths=self.paths,
121
+ paths=all_paths,
118
122
  packages=self.packages,
119
123
  snap_packages=self.snap_packages,
120
124
  services=self.services,
clonebox/profiles.py CHANGED
@@ -7,11 +7,17 @@ import yaml
7
7
 
8
8
  def load_profile(profile_name: str, search_paths: list[Path]) -> Optional[Dict[str, Any]]:
9
9
  """Load profile YAML from ~/.clonebox.d/, .clonebox.d/, templates/profiles/"""
10
+ search_paths = search_paths or []
11
+
10
12
  profile_paths = [
11
13
  Path.home() / ".clonebox.d" / f"{profile_name}.yaml",
12
14
  Path.cwd() / ".clonebox.d" / f"{profile_name}.yaml",
13
15
  ]
14
16
 
17
+ for base in search_paths:
18
+ profile_paths.insert(0, base / "templates" / "profiles" / f"{profile_name}.yaml")
19
+ profile_paths.insert(0, base / f"{profile_name}.yaml")
20
+
15
21
  for profile_path in profile_paths:
16
22
  if profile_path.exists():
17
23
  return yaml.safe_load(profile_path.read_text())
@@ -37,13 +43,24 @@ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any
37
43
  return merged
38
44
 
39
45
 
40
- def merge_with_profile(base_config: Dict[str, Any], profile_name: Optional[str] = None) -> Dict[str, Any]:
46
+ def merge_with_profile(
47
+ base_config: Dict[str, Any],
48
+ profile_name: Optional[str] = None,
49
+ *,
50
+ profile: Optional[Dict[str, Any]] = None,
51
+ search_paths: Optional[list[Path]] = None,
52
+ ) -> Dict[str, Any]:
41
53
  """Merge profile OVER base config (profile wins)."""
54
+ if profile is not None:
55
+ if not isinstance(profile, dict):
56
+ return base_config
57
+ return _deep_merge(base_config, profile)
58
+
42
59
  if not profile_name:
43
60
  return base_config
44
61
 
45
- profile = load_profile(profile_name, [])
46
- if not profile or not isinstance(profile, dict):
62
+ loaded = load_profile(profile_name, search_paths or [])
63
+ if not loaded or not isinstance(loaded, dict):
47
64
  return base_config
48
65
 
49
- return _deep_merge(base_config, profile)
66
+ return _deep_merge(base_config, loaded)
clonebox/validator.py CHANGED
@@ -24,6 +24,7 @@ class VMValidator:
24
24
  "packages": {"passed": 0, "failed": 0, "total": 0, "details": []},
25
25
  "snap_packages": {"passed": 0, "failed": 0, "total": 0, "details": []},
26
26
  "services": {"passed": 0, "failed": 0, "total": 0, "details": []},
27
+ "apps": {"passed": 0, "failed": 0, "total": 0, "details": []},
27
28
  "overall": "unknown"
28
29
  }
29
30
 
@@ -254,68 +255,155 @@ class VMValidator:
254
255
  if not services:
255
256
  self.console.print("[dim]No services configured[/]")
256
257
  return self.results["services"]
257
-
258
- # Initialize skipped counter
258
+
259
259
  if "skipped" not in self.results["services"]:
260
260
  self.results["services"]["skipped"] = 0
261
-
261
+
262
262
  svc_table = Table(title="Service Validation", border_style="cyan")
263
263
  svc_table.add_column("Service", style="bold")
264
264
  svc_table.add_column("Enabled", justify="center")
265
265
  svc_table.add_column("Running", justify="center")
266
266
  svc_table.add_column("Note", style="dim")
267
-
267
+
268
268
  for service in services:
269
- # Check if service should be skipped (host-specific)
270
269
  if service in self.VM_EXCLUDED_SERVICES:
271
270
  svc_table.add_row(service, "[dim]—[/]", "[dim]—[/]", "host-only")
272
271
  self.results["services"]["skipped"] += 1
273
- self.results["services"]["details"].append({
274
- "service": service,
275
- "enabled": None,
276
- "running": None,
277
- "skipped": True,
278
- "reason": "host-specific service"
279
- })
272
+ self.results["services"]["details"].append(
273
+ {
274
+ "service": service,
275
+ "enabled": None,
276
+ "running": None,
277
+ "skipped": True,
278
+ "reason": "host-specific service",
279
+ }
280
+ )
280
281
  continue
281
-
282
+
282
283
  self.results["services"]["total"] += 1
283
-
284
- # Check if enabled
284
+
285
285
  enabled_cmd = f"systemctl is-enabled {service} 2>/dev/null"
286
286
  enabled_status = self._exec_in_vm(enabled_cmd)
287
287
  is_enabled = enabled_status == "enabled"
288
-
289
- # Check if running
288
+
290
289
  running_cmd = f"systemctl is-active {service} 2>/dev/null"
291
290
  running_status = self._exec_in_vm(running_cmd)
292
291
  is_running = running_status == "active"
293
-
292
+
294
293
  enabled_icon = "[green]✅[/]" if is_enabled else "[yellow]⚠️[/]"
295
294
  running_icon = "[green]✅[/]" if is_running else "[red]❌[/]"
296
-
295
+
297
296
  svc_table.add_row(service, enabled_icon, running_icon, "")
298
-
297
+
299
298
  if is_enabled and is_running:
300
299
  self.results["services"]["passed"] += 1
301
300
  else:
302
301
  self.results["services"]["failed"] += 1
303
-
304
- self.results["services"]["details"].append({
305
- "service": service,
306
- "enabled": is_enabled,
307
- "running": is_running,
308
- "skipped": False
309
- })
310
-
302
+
303
+ self.results["services"]["details"].append(
304
+ {
305
+ "service": service,
306
+ "enabled": is_enabled,
307
+ "running": is_running,
308
+ "skipped": False,
309
+ }
310
+ )
311
+
311
312
  self.console.print(svc_table)
312
313
  skipped = self.results["services"].get("skipped", 0)
313
314
  msg = f"{self.results['services']['passed']}/{self.results['services']['total']} services active"
314
315
  if skipped > 0:
315
316
  msg += f" ({skipped} host-only skipped)"
316
317
  self.console.print(f"[dim]{msg}[/]")
317
-
318
+
318
319
  return self.results["services"]
320
+
321
+ def validate_apps(self) -> Dict:
322
+ packages = self.config.get("packages", [])
323
+ snap_packages = self.config.get("snap_packages", [])
324
+ app_data_paths = self.config.get("app_data_paths", {})
325
+
326
+ expected = []
327
+
328
+ if "firefox" in packages:
329
+ expected.append("firefox")
330
+ if "pycharm-community" in snap_packages:
331
+ expected.append("pycharm-community")
332
+
333
+ for _, guest_path in app_data_paths.items():
334
+ if guest_path == "/home/ubuntu/.config/google-chrome":
335
+ expected.append("google-chrome")
336
+ break
337
+
338
+ expected = sorted(set(expected))
339
+ if not expected:
340
+ return self.results["apps"]
341
+
342
+ self.console.print("\n[bold]🧩 Validating Apps...[/]")
343
+ table = Table(title="App Validation", border_style="cyan")
344
+ table.add_column("App", style="bold")
345
+ table.add_column("Installed", justify="center")
346
+ table.add_column("Profile", justify="center")
347
+
348
+ def _check_dir_nonempty(path: str) -> bool:
349
+ out = self._exec_in_vm(
350
+ f"test -d {path} && [ $(ls -A {path} 2>/dev/null | wc -l) -gt 0 ] && echo yes || echo no",
351
+ timeout=10,
352
+ )
353
+ return out == "yes"
354
+
355
+ for app in expected:
356
+ self.results["apps"]["total"] += 1
357
+ installed = False
358
+ profile_ok = False
359
+
360
+ if app == "firefox":
361
+ installed = (
362
+ self._exec_in_vm("command -v firefox >/dev/null 2>&1 && echo yes || echo no")
363
+ == "yes"
364
+ )
365
+ if _check_dir_nonempty("/home/ubuntu/snap/firefox/common/.mozilla/firefox"):
366
+ profile_ok = True
367
+ elif _check_dir_nonempty("/home/ubuntu/.mozilla/firefox"):
368
+ profile_ok = True
369
+
370
+ elif app == "pycharm-community":
371
+ installed = (
372
+ self._exec_in_vm(
373
+ "snap list pycharm-community >/dev/null 2>&1 && echo yes || echo no"
374
+ )
375
+ == "yes"
376
+ )
377
+ profile_ok = _check_dir_nonempty(
378
+ "/home/ubuntu/snap/pycharm-community/common/.config/JetBrains"
379
+ )
380
+
381
+ elif app == "google-chrome":
382
+ installed = (
383
+ self._exec_in_vm(
384
+ "(command -v google-chrome >/dev/null 2>&1 || command -v google-chrome-stable >/dev/null 2>&1) && echo yes || echo no"
385
+ )
386
+ == "yes"
387
+ )
388
+ profile_ok = _check_dir_nonempty("/home/ubuntu/.config/google-chrome")
389
+
390
+ table.add_row(
391
+ app,
392
+ "[green]✅[/]" if installed else "[red]❌[/]",
393
+ "[green]✅[/]" if profile_ok else "[red]❌[/]",
394
+ )
395
+
396
+ if installed and profile_ok:
397
+ self.results["apps"]["passed"] += 1
398
+ else:
399
+ self.results["apps"]["failed"] += 1
400
+
401
+ self.results["apps"]["details"].append(
402
+ {"app": app, "installed": installed, "profile": profile_ok}
403
+ )
404
+
405
+ self.console.print(table)
406
+ return self.results["apps"]
319
407
 
320
408
  def validate_all(self) -> Dict:
321
409
  """Run all validations and return comprehensive results."""
@@ -344,27 +432,31 @@ class VMValidator:
344
432
  self.validate_packages()
345
433
  self.validate_snap_packages()
346
434
  self.validate_services()
435
+ self.validate_apps()
347
436
 
348
437
  # Calculate overall status
349
438
  total_checks = (
350
439
  self.results["mounts"]["total"] +
351
440
  self.results["packages"]["total"] +
352
441
  self.results["snap_packages"]["total"] +
353
- self.results["services"]["total"]
442
+ self.results["services"]["total"] +
443
+ self.results["apps"]["total"]
354
444
  )
355
445
 
356
446
  total_passed = (
357
447
  self.results["mounts"]["passed"] +
358
448
  self.results["packages"]["passed"] +
359
449
  self.results["snap_packages"]["passed"] +
360
- self.results["services"]["passed"]
450
+ self.results["services"]["passed"] +
451
+ self.results["apps"]["passed"]
361
452
  )
362
453
 
363
454
  total_failed = (
364
455
  self.results["mounts"]["failed"] +
365
456
  self.results["packages"]["failed"] +
366
457
  self.results["snap_packages"]["failed"] +
367
- self.results["services"]["failed"]
458
+ self.results["services"]["failed"] +
459
+ self.results["apps"]["failed"]
368
460
  )
369
461
 
370
462
  # Get skipped services count
@@ -392,6 +484,9 @@ class VMValidator:
392
484
  str(self.results["services"]["failed"]),
393
485
  str(skipped_services),
394
486
  str(self.results["services"]["total"]))
487
+ summary_table.add_row("Apps", str(self.results["apps"]["passed"]),
488
+ str(self.results["apps"]["failed"]), "—",
489
+ str(self.results["apps"]["total"]))
395
490
  summary_table.add_row("[bold]TOTAL", f"[bold green]{total_passed}",
396
491
  f"[bold red]{total_failed}",
397
492
  f"[dim]{skipped_services}[/]",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.19
3
+ Version: 0.1.21
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
@@ -85,6 +85,22 @@ CloneBox lets you create isolated virtual machines with only the applications, d
85
85
  - 🧪 **Configuration testing** - Validate VM settings and functionality
86
86
  - 📁 **App data sync** - Include browser profiles, IDE settings, and app configs
87
87
 
88
+ ## Use Cases
89
+
90
+ CloneBox excels in scenarios where developers need:
91
+ - Isolated sandbox environments for testing AI agents, edge computing simulations, or integration workflows without risking host system stability
92
+ - Reproducible development setups that can be quickly spun up with identical configurations across different machines
93
+ - Safe experimentation with system-level changes that can be discarded by simply deleting the VM
94
+ - Quick onboarding for new team members who need a fully configured development environment
95
+
96
+ ## What's Next
97
+
98
+ Project roadmap includes:
99
+ - Container runtime integration (Podman/Docker lightweight mode)
100
+ - Local dashboard for VM and container management
101
+ - Profile system for reusable configuration presets
102
+ - Proxmox export capabilities for production migration
103
+
88
104
 
89
105
 
90
106
 
@@ -95,6 +111,8 @@ Kluczowe komendy:
95
111
  - `clonebox` – interaktywny wizard (detect + create + start)
96
112
  - `clonebox detect` – skanuje usługi/apps/ścieżki
97
113
  - `clonebox clone . --user --run` – szybki klon bieżącego katalogu z użytkownikiem i autostartem
114
+ - `clonebox container up|ps|stop|rm` – lekki runtime kontenerowy (podman/docker)
115
+ - `clonebox dashboard` – lokalny dashboard (VM + containers)
98
116
 
99
117
  ### Dlaczego wirtualne klony workstation mają sens?
100
118
 
@@ -167,6 +185,11 @@ pip install -e .
167
185
  # Or directly
168
186
  pip install clonebox
169
187
  ```
188
+
189
+ Dashboard ma opcjonalne zależności:
190
+ ```bash
191
+ pip install "clonebox[dashboard]"
192
+ ```
170
193
  lub
171
194
  ```bash
172
195
  # Aktywuj venv
@@ -247,7 +270,34 @@ Simply run `clonebox` to start the interactive wizard:
247
270
 
248
271
  ```bash
249
272
  clonebox
250
- clonebox clone . --user --run --replace --base-image ~/ubuntu-22.04-cloud.qcow2
273
+
274
+ clonebox clone . --user --run --replace --base-image ~/ubuntu-22.04-cloud.qcow2 --disk-size-gb 30
275
+
276
+ clonebox test . --user --validate
277
+ ```
278
+
279
+ ### Profiles (Reusable presets)
280
+
281
+ Profiles pozwalają trzymać gotowe presety dla VM/container (np. `ml-dev`, `web-dev`) i nakładać je na bazową konfigurację.
282
+
283
+ ```bash
284
+ # Przykład: uruchom kontener z profilem
285
+ clonebox container up . --profile ml-dev --engine podman
286
+
287
+ # Przykład: generuj VM config z profilem
288
+ clonebox clone . --profile ml-dev --user --run
289
+ ```
290
+
291
+ Domyślne lokalizacje profili:
292
+ - `~/.clonebox.d/<name>.yaml`
293
+ - `./.clonebox.d/<name>.yaml`
294
+ - wbudowane: `src/clonebox/templates/profiles/<name>.yaml`
295
+
296
+ ### Dashboard
297
+
298
+ ```bash
299
+ clonebox dashboard --port 8080
300
+ # http://127.0.0.1:8080
251
301
  ```
252
302
 
253
303
  The wizard will:
@@ -268,7 +318,10 @@ clonebox create --name my-dev-vm --config '{
268
318
  },
269
319
  "packages": ["python3", "nodejs", "docker.io"],
270
320
  "services": ["docker"]
271
- }' --ram 4096 --vcpus 4 --start
321
+ }' --ram 4096 --vcpus 4 --disk-size-gb 20 --start
322
+
323
+ # Create VM with larger root disk
324
+ clonebox create --name my-dev-vm --disk-size-gb 30 --config '{"paths": {}, "packages": [], "services": []}'
272
325
 
273
326
  # List VMs
274
327
  clonebox list
@@ -325,10 +378,45 @@ clonebox clone . --user --run
325
378
 
326
379
  # Access in VM:
327
380
  ls ~/.config/google-chrome # Chrome profile
328
- ls ~/.mozilla/firefox # Firefox profile
329
- ls ~/.config/JetBrains # PyCharm settings
381
+
382
+ # Firefox profile (Ubuntu często używa snap):
383
+ ls ~/snap/firefox/common/.mozilla/firefox
384
+ ls ~/.mozilla/firefox
385
+
386
+ # PyCharm profile (snap):
387
+ ls ~/snap/pycharm-community/common/.config/JetBrains
388
+ ls ~/.config/JetBrains
389
+ ```
390
+
391
+ ### Container workflow (podman/docker)
392
+
393
+ ```bash
394
+ # Start a dev container (auto-detect engine if not specified)
395
+ clonebox container up . --engine podman --detach
396
+
397
+ # List running containers
398
+ clonebox container ps
399
+
400
+ # Stop/remove
401
+ clonebox container stop <name>
402
+ clonebox container rm <name>
403
+ ```
404
+
405
+ ### Full validation (VM)
406
+
407
+ `clonebox test` weryfikuje, że VM faktycznie ma zamontowane ścieżki i spełnia wymagania z `.clonebox.yaml`.
408
+
409
+ ```bash
410
+ clonebox test . --user --validate
330
411
  ```
331
412
 
413
+ Walidowane kategorie:
414
+ - **Mounts** (9p)
415
+ - **Packages** (apt)
416
+ - **Snap packages**
417
+ - **Services** (enabled + running)
418
+ - **Apps** (instalacja + dostępność profilu: Firefox/PyCharm/Chrome)
419
+
332
420
  ### Testing and Validating VM Configuration
333
421
 
334
422
  ```bash
@@ -557,6 +645,9 @@ The fastest way to clone your current working directory:
557
645
  # Base OS image is automatically downloaded to ~/Downloads on first run
558
646
  clonebox clone .
559
647
 
648
+ # Increase VM disk size (recommended for GUI + large tooling)
649
+ clonebox clone . --user --disk-size-gb 30
650
+
560
651
  # Clone specific path
561
652
  clonebox clone ~/projects/my-app
562
653
 
@@ -687,8 +778,10 @@ clonebox clone . --network auto
687
778
  | `clonebox clone . --replace` | Replace existing VM (stop, delete, recreate) |
688
779
  | `clonebox clone . --user` | Clone in user session (no root) |
689
780
  | `clonebox clone . --base-image <path>` | Use custom base image |
781
+ | `clonebox clone . --disk-size-gb <gb>` | Set root disk size in GB (generated configs default to 20GB) |
690
782
  | `clonebox clone . --network user` | Use user-mode networking (slirp) |
691
783
  | `clonebox clone . --network auto` | Auto-detect network mode (default) |
784
+ | `clonebox create --config <json> --disk-size-gb <gb>` | Create VM from JSON config with specified disk size |
692
785
  | `clonebox start .` | Start VM from `.clonebox.yaml` in current dir |
693
786
  | `clonebox start . --viewer` | Start VM and open GUI window |
694
787
  | `clonebox start <name>` | Start existing VM by name |
@@ -701,6 +794,11 @@ clonebox clone . --network auto
701
794
  | `clonebox detect --yaml` | Output as YAML config |
702
795
  | `clonebox detect --yaml --dedupe` | YAML with duplicates removed |
703
796
  | `clonebox detect --json` | Output as JSON |
797
+ | `clonebox container up .` | Start a dev container for given path |
798
+ | `clonebox container ps` | List containers |
799
+ | `clonebox container stop <name>` | Stop a container |
800
+ | `clonebox container rm <name>` | Remove a container |
801
+ | `clonebox dashboard` | Run local dashboard (VM + containers) |
704
802
  | `clonebox status . --user` | Check VM health, cloud-init, IP, and mount status |
705
803
  | `clonebox status . --user --health` | Check VM status and run full health check |
706
804
  | `clonebox test . --user` | Test VM configuration (basic checks) |
@@ -721,6 +819,97 @@ clonebox clone . --network auto
721
819
 
722
820
  ## Troubleshooting
723
821
 
822
+ ### Critical: Insufficient Disk Space
823
+
824
+ If you install a full desktop environment and large development tools (e.g. `ubuntu-desktop-minimal`, `docker.io`, large snaps like `pycharm-community`/`chromium`), you may hit low disk space warnings inside the VM.
825
+
826
+ Recommended fix:
827
+ - Set a larger root disk in `.clonebox.yaml`:
828
+
829
+ ```yaml
830
+ vm:
831
+ disk_size_gb: 30
832
+ ```
833
+
834
+ You can also set it during config generation:
835
+ ```bash
836
+ clonebox clone . --user --disk-size-gb 30
837
+ ```
838
+
839
+ Notes:
840
+ - New configs generated by `clonebox clone` default to `disk_size_gb: 20`.
841
+ - You can override this by setting `vm.disk_size_gb` in `.clonebox.yaml`.
842
+
843
+ Workaround for an existing VM (host-side resize + guest filesystem grow):
844
+ ```bash
845
+ clonebox stop . --user
846
+ qemu-img resize ~/.local/share/libvirt/images/<vm-name>/root.qcow2 +10G
847
+ clonebox start . --user
848
+ ```
849
+
850
+ Inside the VM:
851
+ ```bash
852
+ sudo growpart /dev/vda 1
853
+ sudo resize2fs /dev/vda1
854
+ df -h /
855
+ ```
856
+
857
+ ### Known Issue: IBus Preferences crash
858
+
859
+ During validation you may occasionally see a crash dialog from **IBus Preferences** in the Ubuntu desktop environment.
860
+ This is an upstream issue related to the input method daemon (`ibus-daemon`) and obsolete system packages (e.g. `libglib2.0`, `libssl3`, `libxml2`, `openssl`).
861
+ It does **not** affect CloneBox functionality and the VM operates normally.
862
+
863
+ Workaround:
864
+ - Dismiss the crash dialog
865
+ - Or run `sudo apt upgrade` inside the VM to update system packages
866
+
867
+ ### Snap Apps Not Launching (PyCharm, Chromium, Firefox)
868
+
869
+ If snap-installed applications (e.g., PyCharm, Chromium) are installed but don't launch when clicked, the issue is usually **disconnected snap interfaces**. This happens because snap interfaces are not auto-connected when installing via cloud-init.
870
+
871
+ **New VMs created with updated CloneBox automatically connect snap interfaces**, but for older VMs or manual installs:
872
+
873
+ ```bash
874
+ # Check snap interface connections
875
+ snap connections pycharm-community
876
+
877
+ # If you see "-" instead of ":desktop", interfaces are NOT connected
878
+
879
+ # Connect required interfaces
880
+ sudo snap connect pycharm-community:desktop :desktop
881
+ sudo snap connect pycharm-community:desktop-legacy :desktop-legacy
882
+ sudo snap connect pycharm-community:x11 :x11
883
+ sudo snap connect pycharm-community:wayland :wayland
884
+ sudo snap connect pycharm-community:home :home
885
+ sudo snap connect pycharm-community:network :network
886
+
887
+ # Restart snap daemon and try again
888
+ sudo systemctl restart snapd
889
+ snap run pycharm-community
890
+ ```
891
+
892
+ **For Chromium/Firefox:**
893
+ ```bash
894
+ sudo snap connect chromium:desktop :desktop
895
+ sudo snap connect chromium:x11 :x11
896
+ sudo snap connect firefox:desktop :desktop
897
+ sudo snap connect firefox:x11 :x11
898
+ ```
899
+
900
+ **Debug launch:**
901
+ ```bash
902
+ PYCHARM_DEBUG=true snap run pycharm-community 2>&1 | tee /tmp/pycharm-debug.log
903
+ ```
904
+
905
+ **Nuclear option (reinstall):**
906
+ ```bash
907
+ snap remove pycharm-community
908
+ rm -rf ~/snap/pycharm-community
909
+ sudo snap install pycharm-community --classic
910
+ sudo snap connect pycharm-community:desktop :desktop
911
+ ```
912
+
724
913
  ### Network Issues
725
914
 
726
915
  If you encounter "Network not found" or "network 'default' is not active" errors:
@@ -1069,4 +1258,4 @@ qm set 9000 --boot c --bootdisk scsi0
1069
1258
 
1070
1259
  ## License
1071
1260
 
1072
- MIT License - see [LICENSE](LICENSE) file.
1261
+ Apache License - see [LICENSE](LICENSE) file.
@@ -0,0 +1,17 @@
1
+ clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
2
+ clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
+ clonebox/cli.py,sha256=ItXPNJmQeWbv-Bf9IxYi4e6AsX04pJwRk8fBXP0MglA,104376
4
+ clonebox/cloner.py,sha256=jR-pIBewc8zNE_h_sTcID1uZYKeQjNc6VInhE3aikfQ,34156
5
+ clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
6
+ clonebox/dashboard.py,sha256=RhSPvR6kWglqXeLkCWesBZQid7wv2WpJa6w78mXbPjY,4268
7
+ clonebox/detector.py,sha256=aS_QlbG93-DE3hsjRt88E7O-PGC2TUBgUbP9wqT9g60,23221
8
+ clonebox/models.py,sha256=yBRUlJejpeJHZjvCYMGq1nXPFcmhLFxN-LqkEyveWsA,7913
9
+ clonebox/profiles.py,sha256=VaKVuxCrgyMxx-8_WOTcw7E8irwGxUPhZHVY6RxYYiE,2034
10
+ clonebox/validator.py,sha256=LnQSZEdJXFGcJrTPxzS2cQUmAXucGeHDKwxrX632h_s,21188
11
+ clonebox/templates/profiles/ml-dev.yaml,sha256=MT7Wu3xGBnYIsO5mzZ2GDI4AAEFGOroIx0eU3XjNARg,140
12
+ clonebox-0.1.21.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
13
+ clonebox-0.1.21.dist-info/METADATA,sha256=xvEQjhq84KYnf5oveMK8GRcfZXoBB54sZ9CrcRfTRpg,41534
14
+ clonebox-0.1.21.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ clonebox-0.1.21.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
16
+ clonebox-0.1.21.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
17
+ clonebox-0.1.21.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
2
- clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
- clonebox/cli.py,sha256=WnAlgR3h0gIMwkc0uHAr44E4eS25GTBsNbKbuumrUug,99790
4
- clonebox/cloner.py,sha256=OXytYy4K4sr-ytP55HZK9frt1vU5Kf5EOCIKYhpdJtQ,32516
5
- clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
6
- clonebox/detector.py,sha256=FM8QJ6RTO0jXyUJIcAJivLwUh82xm287ayFD_VIuvgs,22521
7
- clonebox/models.py,sha256=Uxz9eHov2epJpNYbl0ejaOX91iMSjqdHskGdC8-smVk,7789
8
- clonebox/profiles.py,sha256=ZyuPct9Jg5m2ImnSEbHvBMp3x7Ufj2420D84rXt2T1Y,1537
9
- clonebox/validator.py,sha256=qIkFMkWmpx4k2-vPkLigZ6pK5wtl748CmpuNLB6yOIM,17645
10
- clonebox/templates/profiles/ml-dev.yaml,sha256=MT7Wu3xGBnYIsO5mzZ2GDI4AAEFGOroIx0eU3XjNARg,140
11
- clonebox-0.1.19.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- clonebox-0.1.19.dist-info/METADATA,sha256=HXLOv0aaY7OlXGJKtk8LSzlbqeQwgHp9Bax29KzQLV4,35353
13
- clonebox-0.1.19.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
- clonebox-0.1.19.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
15
- clonebox-0.1.19.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
16
- clonebox-0.1.19.dist-info/RECORD,,