clonebox 0.1.25__py3-none-any.whl → 0.1.27__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/dashboard.py CHANGED
@@ -19,9 +19,7 @@ def _run_clonebox(args: List[str]) -> subprocess.CompletedProcess:
19
19
 
20
20
  def _render_table(title: str, headers: List[str], rows: List[List[str]]) -> str:
21
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
- )
22
+ body_html = "".join("<tr>" + "".join(f"<td>{c}</td>" for c in row) + "</tr>" for row in rows)
25
23
 
26
24
  return (
27
25
  f"<h2>{title}</h2>"
@@ -73,7 +71,9 @@ async def api_vms() -> str:
73
71
  if not items:
74
72
  return "<h2>VMs</h2><p><em>No VMs found.</em></p>"
75
73
 
76
- rows = [[str(i.get("name", "")), str(i.get("state", "")), str(i.get("uuid", ""))] for i in items]
74
+ rows = [
75
+ [str(i.get("name", "")), str(i.get("state", "")), str(i.get("uuid", ""))] for i in items
76
+ ]
77
77
  return _render_table("VMs", ["Name", "State", "UUID"], rows)
78
78
 
79
79
 
clonebox/detector.py CHANGED
@@ -281,7 +281,6 @@ class SystemDetector:
281
281
  ".mozilla/firefox",
282
282
  ".cache/mozilla/firefox",
283
283
  ],
284
-
285
284
  # IDEs and editors - settings, extensions, projects history
286
285
  "code": [".config/Code", ".vscode", ".vscode-server"],
287
286
  "vscode": [".config/Code", ".vscode", ".vscode-server"],
@@ -302,7 +301,6 @@ class SystemDetector:
302
301
  "nvim": [".config/nvim", ".local/share/nvim"],
303
302
  "emacs": [".emacs.d", ".emacs"],
304
303
  "cursor": [".config/Cursor", ".cursor"],
305
-
306
304
  # Development tools
307
305
  "docker": [".docker"],
308
306
  "git": [".gitconfig", ".git-credentials", ".config/git"],
@@ -314,27 +312,22 @@ class SystemDetector:
314
312
  "go": [".go", "go"],
315
313
  "gradle": [".gradle"],
316
314
  "maven": [".m2"],
317
-
318
315
  # Python environments
319
316
  "python": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
320
317
  "python3": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
321
318
  "conda": [".conda", "anaconda3", "miniconda3"],
322
-
323
319
  # Node.js
324
320
  "node": [".nvm", ".node", ".npm"],
325
-
326
321
  # Databases
327
322
  "postgres": [".pgpass", ".psqlrc", ".psql_history"],
328
323
  "mysql": [".my.cnf", ".mysql_history"],
329
324
  "mongodb": [".mongorc.js", ".dbshell"],
330
325
  "redis": [".rediscli_history"],
331
-
332
326
  # Communication apps
333
327
  "slack": [".config/Slack"],
334
328
  "discord": [".config/discord"],
335
329
  "telegram": [".local/share/TelegramDesktop"],
336
330
  "teams": [".config/Microsoft/Microsoft Teams"],
337
-
338
331
  # Other tools
339
332
  "postman": [".config/Postman"],
340
333
  "insomnia": [".config/Insomnia"],
@@ -346,7 +339,6 @@ class SystemDetector:
346
339
  "kubectl": [".kube"],
347
340
  "terraform": [".terraform.d"],
348
341
  "ansible": [".ansible"],
349
-
350
342
  # General app data
351
343
  "spotify": [".config/spotify"],
352
344
  "vlc": [".config/vlc"],
@@ -360,12 +352,12 @@ class SystemDetector:
360
352
 
361
353
  def detect_app_data_dirs(self, applications: list) -> list:
362
354
  """Detect config/data directories for running applications.
363
-
355
+
364
356
  Returns list of paths that contain user data needed by running apps.
365
357
  """
366
358
  app_data_paths = []
367
359
  seen_paths = set()
368
-
360
+
369
361
  matched_patterns = set()
370
362
 
371
363
  for app in applications:
@@ -394,13 +386,15 @@ class SystemDetector:
394
386
  size = self._get_dir_size(full_path, max_depth=2)
395
387
  except Exception:
396
388
  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
- })
403
-
389
+ app_data_paths.append(
390
+ {
391
+ "path": str(full_path),
392
+ "app": pattern,
393
+ "type": "app_data",
394
+ "size_mb": round(size / 1024 / 1024, 1),
395
+ }
396
+ )
397
+
404
398
  return app_data_paths
405
399
 
406
400
  def detect_all(self) -> SystemSnapshot:
@@ -645,13 +639,13 @@ class SystemDetector:
645
639
 
646
640
  def suggest_packages_for_apps(self, applications: list) -> dict:
647
641
  """Suggest packages based on detected applications.
648
-
642
+
649
643
  Returns:
650
644
  dict with 'apt' and 'snap' keys containing lists of packages
651
645
  """
652
646
  apt_packages = set()
653
647
  snap_packages = set()
654
-
648
+
655
649
  for app in applications:
656
650
  app_name = app.name.lower()
657
651
  for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
@@ -661,21 +655,18 @@ class SystemDetector:
661
655
  else:
662
656
  apt_packages.add(package)
663
657
  break
664
-
665
- return {
666
- "apt": sorted(list(apt_packages)),
667
- "snap": sorted(list(snap_packages))
668
- }
658
+
659
+ return {"apt": sorted(list(apt_packages)), "snap": sorted(list(snap_packages))}
669
660
 
670
661
  def suggest_packages_for_services(self, services: list) -> dict:
671
662
  """Suggest packages based on detected services.
672
-
663
+
673
664
  Returns:
674
665
  dict with 'apt' and 'snap' keys containing lists of packages
675
666
  """
676
667
  apt_packages = set()
677
668
  snap_packages = set()
678
-
669
+
679
670
  for service in services:
680
671
  service_name = service.name.lower()
681
672
  for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
@@ -685,11 +676,8 @@ class SystemDetector:
685
676
  else:
686
677
  apt_packages.add(package)
687
678
  break
688
-
689
- return {
690
- "apt": sorted(list(apt_packages)),
691
- "snap": sorted(list(snap_packages))
692
- }
679
+
680
+ return {"apt": sorted(list(apt_packages)), "snap": sorted(list(snap_packages))}
693
681
 
694
682
  def get_system_info(self) -> dict:
695
683
  """Get basic system information."""
clonebox/models.py CHANGED
@@ -76,8 +76,17 @@ class CloneBoxConfig(BaseModel):
76
76
  if isinstance(data, dict):
77
77
  if "vm" in data and isinstance(data["vm"], dict):
78
78
  return data
79
- vm_fields = {"name", "ram_mb", "vcpus", "disk_size_gb", "gui", "base_image",
80
- "network_mode", "username", "password"}
79
+ vm_fields = {
80
+ "name",
81
+ "ram_mb",
82
+ "vcpus",
83
+ "disk_size_gb",
84
+ "gui",
85
+ "base_image",
86
+ "network_mode",
87
+ "username",
88
+ "password",
89
+ }
81
90
  vm_data = {k: v for k, v in data.items() if k in vm_fields}
82
91
  if vm_data:
83
92
  data = {k: v for k, v in data.items() if k not in vm_fields}
@@ -158,6 +167,14 @@ class ContainerConfig(BaseModel):
158
167
  raise ValueError(f"Container path must be absolute: {container_path}")
159
168
  return v
160
169
 
170
+ @field_validator("ports", mode="before")
171
+ @classmethod
172
+ def coerce_ports(cls, v):
173
+ # Accept mapping like {"8080": "80"} -> ["8080:80"]
174
+ if isinstance(v, dict):
175
+ return [f"{k}:{val}" for k, val in v.items()]
176
+ return v
177
+
161
178
  @field_validator("ports")
162
179
  @classmethod
163
180
  def ports_must_be_valid(cls, v: List[str]) -> List[str]:
clonebox/profiles.py CHANGED
@@ -32,11 +32,7 @@ def load_profile(profile_name: str, search_paths: list[Path]) -> Optional[Dict[s
32
32
  def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
33
33
  merged: Dict[str, Any] = dict(base)
34
34
  for key, value in override.items():
35
- if (
36
- key in merged
37
- and isinstance(merged[key], dict)
38
- and isinstance(value, dict)
39
- ):
35
+ if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
40
36
  merged[key] = _deep_merge(merged[key], value)
41
37
  else:
42
38
  merged[key] = value