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/cli.py +391 -230
- clonebox/cloner.py +335 -206
- clonebox/dashboard.py +4 -4
- clonebox/detector.py +19 -31
- clonebox/models.py +19 -2
- clonebox/profiles.py +1 -5
- clonebox/validator.py +275 -145
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/METADATA +1 -1
- clonebox-0.1.27.dist-info/RECORD +17 -0
- clonebox-0.1.25.dist-info/RECORD +0 -17
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/WHEEL +0 -0
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/entry_points.txt +0 -0
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/licenses/LICENSE +0 -0
- {clonebox-0.1.25.dist-info → clonebox-0.1.27.dist-info}/top_level.txt +0 -0
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 = [
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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 = {
|
|
80
|
-
|
|
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
|