clonebox 0.1.6__tar.gz → 0.1.8__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -38,6 +38,7 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
38
38
  Dynamic: license-file
39
39
 
40
40
  # CloneBox 📦
41
+ ![img.png](img.png)
41
42
 
42
43
  ```commandline
43
44
  ╔═══════════════════════════════════════════════════════╗
@@ -1,4 +1,5 @@
1
1
  # CloneBox 📦
2
+ ![img.png](img.png)
2
3
 
3
4
  ```commandline
4
5
  ╔═══════════════════════════════════════════════════════╗
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clonebox"
7
- version = "0.1.6"
7
+ version = "0.1.8"
8
8
  description = "Clone your workstation environment to an isolated VM with selective apps, paths and services"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -592,6 +592,28 @@ def generate_clonebox_yaml(
592
592
  paths_mapping[host_path] = f"/mnt/workdir{idx}"
593
593
  idx += 1
594
594
 
595
+ # Add default user folders (Downloads, Documents)
596
+ home_dir = Path.home()
597
+ default_folders = [
598
+ (home_dir / "Downloads", "/home/ubuntu/Downloads"),
599
+ (home_dir / "Documents", "/home/ubuntu/Documents"),
600
+ ]
601
+ for host_folder, guest_folder in default_folders:
602
+ if host_folder.exists() and str(host_folder) not in paths_mapping:
603
+ paths_mapping[str(host_folder)] = guest_folder
604
+
605
+ # Detect and add app-specific data directories for running applications
606
+ # This includes browser profiles, IDE settings, credentials, extensions, etc.
607
+ app_data_dirs = detector.detect_app_data_dirs(snapshot.applications)
608
+ app_data_mapping = {}
609
+ for app_data in app_data_dirs:
610
+ host_path = app_data["path"]
611
+ if host_path not in paths_mapping:
612
+ # Map to same relative path in VM user home
613
+ rel_path = host_path.replace(str(home_dir), "").lstrip("/")
614
+ guest_path = f"/home/ubuntu/{rel_path}"
615
+ app_data_mapping[host_path] = guest_path
616
+
595
617
  # Determine VM name
596
618
  if not vm_name:
597
619
  if target_path:
@@ -604,10 +626,10 @@ def generate_clonebox_yaml(
604
626
  vcpus = max(2, sys_info["cpu_count"] // 2)
605
627
 
606
628
  # Auto-detect packages from running applications and services
607
- suggested_app_packages = detector.suggest_packages_for_apps(snapshot.applications)
608
- suggested_service_packages = detector.suggest_packages_for_services(snapshot.running_services)
629
+ app_packages = detector.suggest_packages_for_apps(snapshot.applications)
630
+ service_packages = detector.suggest_packages_for_services(snapshot.running_services)
609
631
 
610
- # Combine with base packages
632
+ # Combine with base packages (apt only)
611
633
  base_packages = [
612
634
  "build-essential",
613
635
  "git",
@@ -615,10 +637,15 @@ def generate_clonebox_yaml(
615
637
  "vim",
616
638
  ]
617
639
 
618
- # Merge all packages and deduplicate
619
- all_packages = base_packages + suggested_app_packages + suggested_service_packages
640
+ # Merge apt packages and deduplicate
641
+ all_apt_packages = base_packages + app_packages["apt"] + service_packages["apt"]
642
+ if deduplicate:
643
+ all_apt_packages = deduplicate_list(all_apt_packages)
644
+
645
+ # Merge snap packages and deduplicate
646
+ all_snap_packages = app_packages["snap"] + service_packages["snap"]
620
647
  if deduplicate:
621
- all_packages = deduplicate_list(all_packages)
648
+ all_snap_packages = deduplicate_list(all_snap_packages)
622
649
 
623
650
  # Build config
624
651
  config = {
@@ -635,17 +662,24 @@ def generate_clonebox_yaml(
635
662
  "password": "${VM_PASSWORD}",
636
663
  },
637
664
  "services": services,
638
- "packages": all_packages,
665
+ "packages": all_apt_packages,
666
+ "snap_packages": all_snap_packages,
667
+ "post_commands": [], # User can add custom commands to run after setup
639
668
  "paths": paths_mapping,
669
+ "app_data_paths": app_data_mapping, # App-specific config/data directories
640
670
  "detected": {
641
671
  "running_apps": [
642
- {"name": a.name, "cwd": a.working_dir, "memory_mb": round(a.memory_mb)}
672
+ {"name": a.name, "cwd": a.working_dir or "", "memory_mb": round(a.memory_mb)}
643
673
  for a in snapshot.applications[:10]
644
674
  ],
675
+ "app_data_dirs": [
676
+ {"path": d["path"], "app": d["app"], "size_mb": d["size_mb"]}
677
+ for d in app_data_dirs[:15]
678
+ ],
645
679
  "all_paths": {
646
- "projects": paths_by_type["project"],
647
- "configs": paths_by_type["config"][:5],
648
- "data": paths_by_type["data"][:5],
680
+ "projects": list(paths_by_type["project"]),
681
+ "configs": list(paths_by_type["config"][:5]),
682
+ "data": list(paths_by_type["data"][:5]),
649
683
  },
650
684
  },
651
685
  }
@@ -764,15 +798,21 @@ def create_vm_from_config(
764
798
  replace: bool = False,
765
799
  ) -> str:
766
800
  """Create VM from YAML config dict."""
801
+ # Merge paths and app_data_paths
802
+ all_paths = config.get("paths", {}).copy()
803
+ all_paths.update(config.get("app_data_paths", {}))
804
+
767
805
  vm_config = VMConfig(
768
806
  name=config["vm"]["name"],
769
807
  ram_mb=config["vm"].get("ram_mb", 4096),
770
808
  vcpus=config["vm"].get("vcpus", 4),
771
809
  gui=config["vm"].get("gui", True),
772
810
  base_image=config["vm"].get("base_image"),
773
- paths=config.get("paths", {}),
811
+ paths=all_paths,
774
812
  packages=config.get("packages", []),
813
+ snap_packages=config.get("snap_packages", []),
775
814
  services=config.get("services", []),
815
+ post_commands=config.get("post_commands", []),
776
816
  user_session=user_session,
777
817
  network_mode=config["vm"].get("network_mode", "auto"),
778
818
  username=config["vm"].get("username", "ubuntu"),
@@ -896,16 +936,27 @@ def cmd_clone(args):
896
936
  password = config['vm'].get('password', 'ubuntu')
897
937
  console.print("\n[bold yellow]⏰ GUI Setup Process:[/]")
898
938
  console.print(" [yellow]•[/] Installing desktop environment (~5-10 minutes)")
939
+ console.print(" [yellow]•[/] Running health checks on all components")
899
940
  console.print(" [yellow]•[/] Automatic restart after installation")
900
941
  console.print(" [yellow]•[/] GUI login screen will appear")
901
942
  console.print(f" [yellow]•[/] Login: [cyan]{username}[/] / [cyan]{'*' * len(password)}[/] (from .env)")
902
943
  console.print("\n[dim]💡 Progress will be monitored automatically below[/]")
903
944
 
945
+ # Show health check info
946
+ console.print("\n[bold]📊 Health Check (inside VM):[/]")
947
+ console.print(" [cyan]cat /var/log/clonebox-health.log[/] # View full report")
948
+ console.print(" [cyan]cat /var/log/clonebox-health-status[/] # Quick status")
949
+ console.print(" [cyan]clonebox-health[/] # Re-run health check")
950
+
904
951
  # Show mount instructions
905
- if config.get("paths"):
906
- console.print("\n[bold]Inside VM, mount paths with:[/]")
907
- for idx, (host, guest) in enumerate(config["paths"].items()):
908
- console.print(f" [cyan]sudo mount -t 9p -o trans=virtio mount{idx} {guest}[/]")
952
+ all_paths = config.get("paths", {}).copy()
953
+ all_paths.update(config.get("app_data_paths", {}))
954
+ if all_paths:
955
+ console.print("\n[bold]📁 Mounted paths (automatic):[/]")
956
+ for idx, (host, guest) in enumerate(list(all_paths.items())[:5]):
957
+ console.print(f" [dim]{host}[/] → [cyan]{guest}[/]")
958
+ if len(all_paths) > 5:
959
+ console.print(f" [dim]... and {len(all_paths) - 5} more paths[/]")
909
960
  except PermissionError as e:
910
961
  console.print(f"[red]❌ Permission Error:[/]\n{e}")
911
962
  console.print("\n[yellow]💡 Try running with --user flag:[/]")
@@ -31,7 +31,9 @@ class VMConfig:
31
31
  base_image: Optional[str] = None
32
32
  paths: dict = field(default_factory=dict)
33
33
  packages: list = field(default_factory=list)
34
+ snap_packages: list = field(default_factory=list) # Snap packages to install
34
35
  services: list = field(default_factory=list)
36
+ post_commands: list = field(default_factory=list) # Commands to run after setup
35
37
  user_session: bool = False # Use qemu:///session instead of qemu:///system
36
38
  network_mode: str = "auto" # auto|default|user
37
39
  username: str = "ubuntu" # VM default username
@@ -474,6 +476,179 @@ class SelectiveVMCloner:
474
476
 
475
477
  return ET.tostring(root, encoding="unicode")
476
478
 
479
+ def _generate_health_check_script(self, config: VMConfig) -> str:
480
+ """Generate a health check script that validates all installed components."""
481
+ import base64
482
+
483
+ # Build package check commands
484
+ apt_checks = []
485
+ for pkg in config.packages:
486
+ apt_checks.append(f'check_apt_package "{pkg}"')
487
+
488
+ snap_checks = []
489
+ for pkg in config.snap_packages:
490
+ snap_checks.append(f'check_snap_package "{pkg}"')
491
+
492
+ service_checks = []
493
+ for svc in config.services:
494
+ service_checks.append(f'check_service "{svc}"')
495
+
496
+ mount_checks = []
497
+ for idx, (host_path, guest_path) in enumerate(config.paths.items()):
498
+ mount_checks.append(f'check_mount "{guest_path}" "mount{idx}"')
499
+
500
+ apt_checks_str = "\n".join(apt_checks) if apt_checks else "echo 'No apt packages to check'"
501
+ snap_checks_str = "\n".join(snap_checks) if snap_checks else "echo 'No snap packages to check'"
502
+ service_checks_str = "\n".join(service_checks) if service_checks else "echo 'No services to check'"
503
+ mount_checks_str = "\n".join(mount_checks) if mount_checks else "echo 'No mounts to check'"
504
+
505
+ script = f'''#!/bin/bash
506
+ # CloneBox Health Check Script
507
+ # Generated automatically - validates all installed components
508
+
509
+ REPORT_FILE="/var/log/clonebox-health.log"
510
+ PASSED=0
511
+ FAILED=0
512
+ WARNINGS=0
513
+
514
+ # Colors for output
515
+ RED='\\033[0;31m'
516
+ GREEN='\\033[0;32m'
517
+ YELLOW='\\033[1;33m'
518
+ NC='\\033[0m'
519
+
520
+ log() {{
521
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$REPORT_FILE"
522
+ }}
523
+
524
+ check_apt_package() {{
525
+ local pkg="$1"
526
+ if dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then
527
+ log "[PASS] APT package '$pkg' is installed"
528
+ ((PASSED++))
529
+ return 0
530
+ else
531
+ log "[FAIL] APT package '$pkg' is NOT installed"
532
+ ((FAILED++))
533
+ return 1
534
+ fi
535
+ }}
536
+
537
+ check_snap_package() {{
538
+ local pkg="$1"
539
+ if snap list "$pkg" &>/dev/null; then
540
+ log "[PASS] Snap package '$pkg' is installed"
541
+ ((PASSED++))
542
+ return 0
543
+ else
544
+ log "[FAIL] Snap package '$pkg' is NOT installed"
545
+ ((FAILED++))
546
+ return 1
547
+ fi
548
+ }}
549
+
550
+ check_service() {{
551
+ local svc="$1"
552
+ if systemctl is-enabled "$svc" &>/dev/null; then
553
+ if systemctl is-active "$svc" &>/dev/null; then
554
+ log "[PASS] Service '$svc' is enabled and running"
555
+ ((PASSED++))
556
+ return 0
557
+ else
558
+ log "[WARN] Service '$svc' is enabled but not running"
559
+ ((WARNINGS++))
560
+ return 1
561
+ fi
562
+ else
563
+ log "[INFO] Service '$svc' is not enabled (may be optional)"
564
+ return 0
565
+ fi
566
+ }}
567
+
568
+ check_mount() {{
569
+ local path="$1"
570
+ local tag="$2"
571
+ if mountpoint -q "$path" 2>/dev/null; then
572
+ log "[PASS] Mount '$path' ($tag) is active"
573
+ ((PASSED++))
574
+ return 0
575
+ elif [ -d "$path" ]; then
576
+ log "[WARN] Directory '$path' exists but not mounted"
577
+ ((WARNINGS++))
578
+ return 1
579
+ else
580
+ log "[INFO] Mount point '$path' does not exist yet"
581
+ return 0
582
+ fi
583
+ }}
584
+
585
+ check_gui() {{
586
+ if systemctl get-default | grep -q graphical; then
587
+ log "[PASS] System configured for graphical target"
588
+ ((PASSED++))
589
+ if systemctl is-active gdm3 &>/dev/null || systemctl is-active gdm &>/dev/null; then
590
+ log "[PASS] Display manager (GDM) is running"
591
+ ((PASSED++))
592
+ else
593
+ log "[WARN] Display manager not yet running (may start after reboot)"
594
+ ((WARNINGS++))
595
+ fi
596
+ else
597
+ log "[INFO] System not configured for GUI"
598
+ fi
599
+ }}
600
+
601
+ # Start health check
602
+ log "=========================================="
603
+ log "CloneBox Health Check Report"
604
+ log "VM Name: {config.name}"
605
+ log "Date: $(date)"
606
+ log "=========================================="
607
+
608
+ log ""
609
+ log "--- APT Packages ---"
610
+ {apt_checks_str}
611
+
612
+ log ""
613
+ log "--- Snap Packages ---"
614
+ {snap_checks_str}
615
+
616
+ log ""
617
+ log "--- Services ---"
618
+ {service_checks_str}
619
+
620
+ log ""
621
+ log "--- Mounts ---"
622
+ {mount_checks_str}
623
+
624
+ log ""
625
+ log "--- GUI Status ---"
626
+ check_gui
627
+
628
+ log ""
629
+ log "=========================================="
630
+ log "Health Check Summary"
631
+ log "=========================================="
632
+ log "Passed: $PASSED"
633
+ log "Failed: $FAILED"
634
+ log "Warnings: $WARNINGS"
635
+
636
+ if [ $FAILED -eq 0 ]; then
637
+ log ""
638
+ log "[SUCCESS] All critical checks passed!"
639
+ echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
640
+ exit 0
641
+ else
642
+ log ""
643
+ log "[ERROR] Some checks failed. Review log for details."
644
+ echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
645
+ exit 1
646
+ fi
647
+ '''
648
+ # Encode script to base64 for safe embedding in cloud-init
649
+ encoded = base64.b64encode(script.encode()).decode()
650
+ return encoded
651
+
477
652
  def _create_cloudinit_iso(self, vm_dir: Path, config: VMConfig) -> Path:
478
653
  """Create cloud-init ISO with user-data and meta-data."""
479
654
 
@@ -508,7 +683,7 @@ class SelectiveVMCloner:
508
683
  "\n".join(f" - {pkg}" for pkg in all_packages) if all_packages else ""
509
684
  )
510
685
 
511
- # Build runcmd - services and mounts
686
+ # Build runcmd - services, mounts, snaps, post_commands
512
687
  runcmd_lines = []
513
688
 
514
689
  # Add service enablement
@@ -519,6 +694,12 @@ class SelectiveVMCloner:
519
694
  for cmd in mount_commands:
520
695
  runcmd_lines.append(cmd)
521
696
 
697
+ # Install snap packages
698
+ if config.snap_packages:
699
+ runcmd_lines.append(" - echo 'Installing snap packages...'")
700
+ for snap_pkg in config.snap_packages:
701
+ runcmd_lines.append(f" - snap install {snap_pkg} --classic || snap install {snap_pkg} || true")
702
+
522
703
  # Add GUI setup if enabled - runs AFTER package installation completes
523
704
  if config.gui:
524
705
  runcmd_lines.extend([
@@ -526,6 +707,17 @@ class SelectiveVMCloner:
526
707
  " - systemctl enable gdm3 || systemctl enable gdm || true",
527
708
  ])
528
709
 
710
+ # Run user-defined post commands
711
+ if config.post_commands:
712
+ runcmd_lines.append(" - echo 'Running post-setup commands...'")
713
+ for cmd in config.post_commands:
714
+ runcmd_lines.append(f" - {cmd}")
715
+
716
+ # Generate health check script
717
+ health_script = self._generate_health_check_script(config)
718
+ runcmd_lines.append(f" - echo '{health_script}' | base64 -d > /usr/local/bin/clonebox-health")
719
+ runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
720
+ runcmd_lines.append(" - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1")
529
721
  runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
530
722
 
531
723
  # Add reboot command at the end if GUI is enabled
@@ -144,59 +144,209 @@ class SystemDetector:
144
144
  "esbuild",
145
145
  "tmux",
146
146
  "screen",
147
+ # IDEs and desktop apps
148
+ "pycharm",
149
+ "idea",
150
+ "webstorm",
151
+ "phpstorm",
152
+ "goland",
153
+ "clion",
154
+ "rider",
155
+ "datagrip",
156
+ "sublime",
157
+ "atom",
158
+ "slack",
159
+ "discord",
160
+ "telegram",
161
+ "spotify",
162
+ "vlc",
163
+ "gimp",
164
+ "inkscape",
165
+ "blender",
166
+ "obs",
167
+ "postman",
168
+ "insomnia",
169
+ "dbeaver",
147
170
  ]
148
171
 
149
- # Map process/service names to Ubuntu packages
172
+ # Map process/service names to Ubuntu packages or snap packages
173
+ # Format: "process_name": ("package_name", "install_type") where install_type is "apt" or "snap"
150
174
  APP_TO_PACKAGE_MAP = {
151
- "python": "python3",
152
- "python3": "python3",
153
- "pip": "python3-pip",
154
- "node": "nodejs",
155
- "npm": "npm",
156
- "yarn": "yarnpkg",
157
- "docker": "docker.io",
158
- "dockerd": "docker.io",
159
- "docker-compose": "docker-compose",
160
- "podman": "podman",
161
- "nginx": "nginx",
162
- "apache2": "apache2",
163
- "httpd": "apache2",
164
- "postgres": "postgresql",
165
- "postgresql": "postgresql",
166
- "mysql": "mysql-server",
167
- "mysqld": "mysql-server",
168
- "mongod": "mongodb",
169
- "mongodb": "mongodb",
170
- "redis-server": "redis-server",
171
- "redis": "redis-server",
172
- "vim": "vim",
173
- "nvim": "neovim",
174
- "emacs": "emacs",
175
- "firefox": "firefox",
176
- "chromium": "chromium-browser",
177
- "jupyter": "jupyter-notebook",
178
- "jupyter-lab": "jupyterlab",
179
- "gunicorn": "gunicorn",
180
- "uvicorn": "uvicorn",
181
- "tmux": "tmux",
182
- "screen": "screen",
183
- "git": "git",
184
- "curl": "curl",
185
- "wget": "wget",
186
- "ssh": "openssh-client",
187
- "sshd": "openssh-server",
188
- "go": "golang",
189
- "cargo": "cargo",
190
- "rustc": "rustc",
191
- "java": "default-jdk",
192
- "gradle": "gradle",
193
- "mvn": "maven",
175
+ "python": ("python3", "apt"),
176
+ "python3": ("python3", "apt"),
177
+ "pip": ("python3-pip", "apt"),
178
+ "node": ("nodejs", "apt"),
179
+ "npm": ("npm", "apt"),
180
+ "yarn": ("yarnpkg", "apt"),
181
+ "docker": ("docker.io", "apt"),
182
+ "dockerd": ("docker.io", "apt"),
183
+ "docker-compose": ("docker-compose", "apt"),
184
+ "podman": ("podman", "apt"),
185
+ "nginx": ("nginx", "apt"),
186
+ "apache2": ("apache2", "apt"),
187
+ "httpd": ("apache2", "apt"),
188
+ "postgres": ("postgresql", "apt"),
189
+ "postgresql": ("postgresql", "apt"),
190
+ "mysql": ("mysql-server", "apt"),
191
+ "mysqld": ("mysql-server", "apt"),
192
+ "mongod": ("mongodb", "apt"),
193
+ "mongodb": ("mongodb", "apt"),
194
+ "redis-server": ("redis-server", "apt"),
195
+ "redis": ("redis-server", "apt"),
196
+ "vim": ("vim", "apt"),
197
+ "nvim": ("neovim", "apt"),
198
+ "emacs": ("emacs", "apt"),
199
+ "firefox": ("firefox", "apt"),
200
+ "chromium": ("chromium-browser", "apt"),
201
+ "jupyter": ("jupyter-notebook", "apt"),
202
+ "jupyter-lab": ("jupyterlab", "apt"),
203
+ "gunicorn": ("gunicorn", "apt"),
204
+ "uvicorn": ("uvicorn", "apt"),
205
+ "tmux": ("tmux", "apt"),
206
+ "screen": ("screen", "apt"),
207
+ "git": ("git", "apt"),
208
+ "curl": ("curl", "apt"),
209
+ "wget": ("wget", "apt"),
210
+ "ssh": ("openssh-client", "apt"),
211
+ "sshd": ("openssh-server", "apt"),
212
+ "go": ("golang", "apt"),
213
+ "cargo": ("cargo", "apt"),
214
+ "rustc": ("rustc", "apt"),
215
+ "java": ("default-jdk", "apt"),
216
+ "gradle": ("gradle", "apt"),
217
+ "mvn": ("maven", "apt"),
218
+ # Popular desktop apps (snap packages)
219
+ "chrome": ("chromium", "snap"),
220
+ "google-chrome": ("chromium", "snap"),
221
+ "pycharm": ("pycharm-community", "snap"),
222
+ "idea": ("intellij-idea-community", "snap"),
223
+ "code": ("code", "snap"),
224
+ "vscode": ("code", "snap"),
225
+ "slack": ("slack", "snap"),
226
+ "discord": ("discord", "snap"),
227
+ "spotify": ("spotify", "snap"),
228
+ "vlc": ("vlc", "apt"),
229
+ "gimp": ("gimp", "apt"),
230
+ "inkscape": ("inkscape", "apt"),
231
+ "blender": ("blender", "apt"),
232
+ "obs": ("obs-studio", "apt"),
233
+ "telegram": ("telegram-desktop", "snap"),
234
+ "postman": ("postman", "snap"),
235
+ "insomnia": ("insomnia", "snap"),
236
+ "dbeaver": ("dbeaver-ce", "snap"),
237
+ "sublime": ("sublime-text", "snap"),
238
+ "atom": ("atom", "snap"),
239
+ }
240
+
241
+ # Map applications to their config/data directories for complete cloning
242
+ # These directories contain user settings, extensions, profiles, credentials
243
+ APP_DATA_DIRS = {
244
+ # Browsers - profiles, extensions, bookmarks, passwords
245
+ "chrome": [".config/google-chrome", ".config/chromium"],
246
+ "chromium": [".config/chromium"],
247
+ "firefox": [".mozilla/firefox", ".cache/mozilla/firefox"],
248
+
249
+ # IDEs and editors - settings, extensions, projects history
250
+ "code": [".config/Code", ".vscode", ".vscode-server"],
251
+ "vscode": [".config/Code", ".vscode", ".vscode-server"],
252
+ "pycharm": [".config/JetBrains", ".local/share/JetBrains", ".cache/JetBrains"],
253
+ "idea": [".config/JetBrains", ".local/share/JetBrains"],
254
+ "webstorm": [".config/JetBrains", ".local/share/JetBrains"],
255
+ "goland": [".config/JetBrains", ".local/share/JetBrains"],
256
+ "sublime": [".config/sublime-text", ".config/sublime-text-3"],
257
+ "atom": [".atom"],
258
+ "vim": [".vim", ".vimrc", ".config/nvim"],
259
+ "nvim": [".config/nvim", ".local/share/nvim"],
260
+ "emacs": [".emacs.d", ".emacs"],
261
+ "cursor": [".config/Cursor", ".cursor"],
262
+
263
+ # Development tools
264
+ "docker": [".docker"],
265
+ "git": [".gitconfig", ".git-credentials", ".config/git"],
266
+ "npm": [".npm", ".npmrc"],
267
+ "yarn": [".yarn", ".yarnrc"],
268
+ "pip": [".pip", ".config/pip"],
269
+ "cargo": [".cargo"],
270
+ "rustup": [".rustup"],
271
+ "go": [".go", "go"],
272
+ "gradle": [".gradle"],
273
+ "maven": [".m2"],
274
+
275
+ # Python environments
276
+ "python": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
277
+ "python3": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
278
+ "conda": [".conda", "anaconda3", "miniconda3"],
279
+
280
+ # Node.js
281
+ "node": [".nvm", ".node", ".npm"],
282
+
283
+ # Databases
284
+ "postgres": [".pgpass", ".psqlrc", ".psql_history"],
285
+ "mysql": [".my.cnf", ".mysql_history"],
286
+ "mongodb": [".mongorc.js", ".dbshell"],
287
+ "redis": [".rediscli_history"],
288
+
289
+ # Communication apps
290
+ "slack": [".config/Slack"],
291
+ "discord": [".config/discord"],
292
+ "telegram": [".local/share/TelegramDesktop"],
293
+ "teams": [".config/Microsoft/Microsoft Teams"],
294
+
295
+ # Other tools
296
+ "postman": [".config/Postman"],
297
+ "insomnia": [".config/Insomnia"],
298
+ "dbeaver": [".local/share/DBeaverData"],
299
+ "ssh": [".ssh"],
300
+ "gpg": [".gnupg"],
301
+ "aws": [".aws"],
302
+ "gcloud": [".config/gcloud"],
303
+ "kubectl": [".kube"],
304
+ "terraform": [".terraform.d"],
305
+ "ansible": [".ansible"],
306
+
307
+ # General app data
308
+ "spotify": [".config/spotify"],
309
+ "vlc": [".config/vlc"],
310
+ "gimp": [".config/GIMP", ".gimp-2.10"],
311
+ "obs": [".config/obs-studio"],
194
312
  }
195
313
 
196
314
  def __init__(self):
197
315
  self.user = pwd.getpwuid(os.getuid()).pw_name
198
316
  self.home = Path.home()
199
317
 
318
+ def detect_app_data_dirs(self, applications: list) -> list:
319
+ """Detect config/data directories for running applications.
320
+
321
+ Returns list of paths that contain user data needed by running apps.
322
+ """
323
+ app_data_paths = []
324
+ seen_paths = set()
325
+
326
+ for app in applications:
327
+ app_name = app.name.lower()
328
+
329
+ # Check each known app pattern
330
+ for pattern, dirs in self.APP_DATA_DIRS.items():
331
+ if pattern in app_name:
332
+ for dir_name in dirs:
333
+ full_path = self.home / dir_name
334
+ if full_path.exists() and str(full_path) not in seen_paths:
335
+ seen_paths.add(str(full_path))
336
+ # Calculate size
337
+ try:
338
+ size = self._get_dir_size(full_path, max_depth=2)
339
+ except:
340
+ size = 0
341
+ app_data_paths.append({
342
+ "path": str(full_path),
343
+ "app": app.name,
344
+ "type": "app_data",
345
+ "size_mb": round(size / 1024 / 1024, 1)
346
+ })
347
+
348
+ return app_data_paths
349
+
200
350
  def detect_all(self) -> SystemSnapshot:
201
351
  """Detect all services, applications and paths."""
202
352
  return SystemSnapshot(
@@ -437,29 +587,53 @@ class SystemDetector:
437
587
  pass
438
588
  return containers
439
589
 
440
- def suggest_packages_for_apps(self, applications: list) -> list:
441
- """Suggest Ubuntu packages based on detected applications."""
442
- packages = set()
590
+ def suggest_packages_for_apps(self, applications: list) -> dict:
591
+ """Suggest packages based on detected applications.
592
+
593
+ Returns:
594
+ dict with 'apt' and 'snap' keys containing lists of packages
595
+ """
596
+ apt_packages = set()
597
+ snap_packages = set()
598
+
443
599
  for app in applications:
444
600
  app_name = app.name.lower()
445
- # Check if app name matches any known mapping
446
- for key, package in self.APP_TO_PACKAGE_MAP.items():
601
+ for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
447
602
  if key in app_name:
448
- packages.add(package)
603
+ if install_type == "snap":
604
+ snap_packages.add(package)
605
+ else:
606
+ apt_packages.add(package)
449
607
  break
450
- return sorted(list(packages))
608
+
609
+ return {
610
+ "apt": sorted(list(apt_packages)),
611
+ "snap": sorted(list(snap_packages))
612
+ }
451
613
 
452
- def suggest_packages_for_services(self, services: list) -> list:
453
- """Suggest Ubuntu packages based on detected services."""
454
- packages = set()
614
+ def suggest_packages_for_services(self, services: list) -> dict:
615
+ """Suggest packages based on detected services.
616
+
617
+ Returns:
618
+ dict with 'apt' and 'snap' keys containing lists of packages
619
+ """
620
+ apt_packages = set()
621
+ snap_packages = set()
622
+
455
623
  for service in services:
456
624
  service_name = service.name.lower()
457
- # Check if service name matches any known mapping
458
- for key, package in self.APP_TO_PACKAGE_MAP.items():
625
+ for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
459
626
  if key in service_name:
460
- packages.add(package)
627
+ if install_type == "snap":
628
+ snap_packages.add(package)
629
+ else:
630
+ apt_packages.add(package)
461
631
  break
462
- return sorted(list(packages))
632
+
633
+ return {
634
+ "apt": sorted(list(apt_packages)),
635
+ "snap": sorted(list(snap_packages))
636
+ }
463
637
 
464
638
  def get_system_info(self) -> dict:
465
639
  """Get basic system information."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -38,6 +38,7 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
38
38
  Dynamic: license-file
39
39
 
40
40
  # CloneBox 📦
41
+ ![img.png](img.png)
41
42
 
42
43
  ```commandline
43
44
  ╔═══════════════════════════════════════════════════════╗
@@ -50,10 +50,16 @@ class TestNetworkMode:
50
50
  @patch("clonebox.cloner.libvirt")
51
51
  def test_resolve_network_mode_auto_user_no_default(self, mock_libvirt):
52
52
  """Test auto mode with user session and no default network falls back to user."""
53
- import libvirt as real_libvirt
53
+ # Handle missing libvirt module in test environment
54
+ try:
55
+ import libvirt as real_libvirt
56
+ libvirt_error = real_libvirt.libvirtError
57
+ except ImportError:
58
+ class libvirt_error(Exception):
59
+ pass
54
60
 
55
61
  mock_conn = MagicMock()
56
- mock_conn.networkLookupByName.side_effect = real_libvirt.libvirtError("No network")
62
+ mock_conn.networkLookupByName.side_effect = libvirt_error("No network")
57
63
  mock_libvirt.open.return_value = mock_conn
58
64
 
59
65
  cloner = SelectiveVMCloner(user_session=True)
@@ -126,10 +132,16 @@ class TestNetworkMode:
126
132
  @patch("clonebox.cloner.libvirt")
127
133
  def test_default_network_active_not_found(self, mock_libvirt):
128
134
  """Test _default_network_active returns False when network not found."""
129
- import libvirt as real_libvirt
135
+ # Handle missing libvirt module in test environment
136
+ try:
137
+ import libvirt as real_libvirt
138
+ libvirt_error = real_libvirt.libvirtError
139
+ except ImportError:
140
+ class libvirt_error(Exception):
141
+ pass
130
142
 
131
143
  mock_conn = MagicMock()
132
- mock_conn.networkLookupByName.side_effect = real_libvirt.libvirtError("Not found")
144
+ mock_conn.networkLookupByName.side_effect = libvirt_error("Not found")
133
145
  mock_libvirt.open.return_value = mock_conn
134
146
 
135
147
  cloner = SelectiveVMCloner()
File without changes
File without changes
File without changes
File without changes