clonebox 0.1.25__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/__init__.py +14 -0
- clonebox/__main__.py +7 -0
- clonebox/cli.py +2932 -0
- clonebox/cloner.py +2081 -0
- clonebox/container.py +190 -0
- clonebox/dashboard.py +133 -0
- clonebox/detector.py +705 -0
- clonebox/models.py +201 -0
- clonebox/profiles.py +66 -0
- clonebox/templates/profiles/ml-dev.yaml +6 -0
- clonebox/validator.py +841 -0
- clonebox-0.1.25.dist-info/METADATA +1382 -0
- clonebox-0.1.25.dist-info/RECORD +17 -0
- clonebox-0.1.25.dist-info/WHEEL +5 -0
- clonebox-0.1.25.dist-info/entry_points.txt +2 -0
- clonebox-0.1.25.dist-info/licenses/LICENSE +201 -0
- clonebox-0.1.25.dist-info/top_level.txt +1 -0
clonebox/detector.py
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SystemDetector - Detects running services, applications and important paths.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import pwd
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import psutil
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DetectedService:
|
|
17
|
+
"""A detected systemd service."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
status: str # running, stopped, failed
|
|
21
|
+
description: str = ""
|
|
22
|
+
enabled: bool = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DetectedApplication:
|
|
27
|
+
"""A detected running application."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
pid: int
|
|
31
|
+
cmdline: str
|
|
32
|
+
exe: str
|
|
33
|
+
working_dir: str = ""
|
|
34
|
+
memory_mb: float = 0.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class DetectedPath:
|
|
39
|
+
"""A detected important path."""
|
|
40
|
+
|
|
41
|
+
path: str
|
|
42
|
+
type: str # config, data, project, home
|
|
43
|
+
size_mb: float = 0.0
|
|
44
|
+
description: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SystemSnapshot:
|
|
49
|
+
"""Complete snapshot of detected system state."""
|
|
50
|
+
|
|
51
|
+
services: list = field(default_factory=list)
|
|
52
|
+
applications: list = field(default_factory=list)
|
|
53
|
+
paths: list = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def running_services(self) -> list:
|
|
57
|
+
return [s for s in self.services if s.status == "running"]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def running_apps(self) -> list:
|
|
61
|
+
return self.applications
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SystemDetector:
|
|
65
|
+
"""Detects running services, applications and important paths on the system."""
|
|
66
|
+
|
|
67
|
+
# Services that should NOT be cloned to VM (host-specific, hardware-dependent, or hypervisor services)
|
|
68
|
+
VM_EXCLUDED_SERVICES = {
|
|
69
|
+
# Hypervisor/virtualization - no nested virt needed
|
|
70
|
+
"libvirtd",
|
|
71
|
+
"virtlogd",
|
|
72
|
+
"libvirt-guests",
|
|
73
|
+
"qemu-guest-agent", # Host-side, VM has its own
|
|
74
|
+
# Hardware-specific
|
|
75
|
+
"bluetooth",
|
|
76
|
+
"bluez",
|
|
77
|
+
"upower",
|
|
78
|
+
"thermald",
|
|
79
|
+
"tlp",
|
|
80
|
+
"power-profiles-daemon",
|
|
81
|
+
# Display manager (VM has its own)
|
|
82
|
+
"gdm",
|
|
83
|
+
"gdm3",
|
|
84
|
+
"sddm",
|
|
85
|
+
"lightdm",
|
|
86
|
+
# Snap-based duplicates (prefer APT versions)
|
|
87
|
+
"snap.cups.cups-browsed",
|
|
88
|
+
"snap.cups.cupsd",
|
|
89
|
+
# Network hardware
|
|
90
|
+
"ModemManager",
|
|
91
|
+
"wpa_supplicant",
|
|
92
|
+
# Host-specific desktop
|
|
93
|
+
"accounts-daemon",
|
|
94
|
+
"colord",
|
|
95
|
+
"switcheroo-control",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Common development/server services to look for
|
|
99
|
+
INTERESTING_SERVICES = [
|
|
100
|
+
"docker",
|
|
101
|
+
"containerd",
|
|
102
|
+
"podman",
|
|
103
|
+
"nginx",
|
|
104
|
+
"apache2",
|
|
105
|
+
"httpd",
|
|
106
|
+
"caddy",
|
|
107
|
+
"postgresql",
|
|
108
|
+
"mysql",
|
|
109
|
+
"mariadb",
|
|
110
|
+
"mongodb",
|
|
111
|
+
"redis",
|
|
112
|
+
"memcached",
|
|
113
|
+
"elasticsearch",
|
|
114
|
+
"kibana",
|
|
115
|
+
"grafana",
|
|
116
|
+
"prometheus",
|
|
117
|
+
"jenkins",
|
|
118
|
+
"gitlab-runner",
|
|
119
|
+
"sshd",
|
|
120
|
+
"rsync",
|
|
121
|
+
"rabbitmq-server",
|
|
122
|
+
"kafka",
|
|
123
|
+
"nodejs",
|
|
124
|
+
"pm2",
|
|
125
|
+
"supervisor",
|
|
126
|
+
"systemd-resolved",
|
|
127
|
+
"cups",
|
|
128
|
+
"bluetooth",
|
|
129
|
+
"NetworkManager",
|
|
130
|
+
"libvirtd",
|
|
131
|
+
"virtlogd",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
# Interesting process names
|
|
135
|
+
INTERESTING_PROCESSES = [
|
|
136
|
+
"python",
|
|
137
|
+
"python3",
|
|
138
|
+
"node",
|
|
139
|
+
"npm",
|
|
140
|
+
"yarn",
|
|
141
|
+
"pnpm",
|
|
142
|
+
"java",
|
|
143
|
+
"gradle",
|
|
144
|
+
"mvn",
|
|
145
|
+
"go",
|
|
146
|
+
"cargo",
|
|
147
|
+
"rustc",
|
|
148
|
+
"docker",
|
|
149
|
+
"docker-compose",
|
|
150
|
+
"podman",
|
|
151
|
+
"nginx",
|
|
152
|
+
"apache",
|
|
153
|
+
"httpd",
|
|
154
|
+
"postgres",
|
|
155
|
+
"mysql",
|
|
156
|
+
"mongod",
|
|
157
|
+
"redis-server",
|
|
158
|
+
"code",
|
|
159
|
+
"code-server",
|
|
160
|
+
"cursor",
|
|
161
|
+
"vim",
|
|
162
|
+
"nvim",
|
|
163
|
+
"emacs",
|
|
164
|
+
"firefox",
|
|
165
|
+
"chrome",
|
|
166
|
+
"chromium",
|
|
167
|
+
"jupyter",
|
|
168
|
+
"jupyter-lab",
|
|
169
|
+
"gunicorn",
|
|
170
|
+
"uvicorn",
|
|
171
|
+
"flask",
|
|
172
|
+
"django",
|
|
173
|
+
"webpack",
|
|
174
|
+
"vite",
|
|
175
|
+
"esbuild",
|
|
176
|
+
"tmux",
|
|
177
|
+
"screen",
|
|
178
|
+
# IDEs and desktop apps
|
|
179
|
+
"pycharm",
|
|
180
|
+
"idea",
|
|
181
|
+
"webstorm",
|
|
182
|
+
"phpstorm",
|
|
183
|
+
"goland",
|
|
184
|
+
"clion",
|
|
185
|
+
"rider",
|
|
186
|
+
"datagrip",
|
|
187
|
+
"sublime",
|
|
188
|
+
"atom",
|
|
189
|
+
"slack",
|
|
190
|
+
"discord",
|
|
191
|
+
"telegram",
|
|
192
|
+
"spotify",
|
|
193
|
+
"vlc",
|
|
194
|
+
"gimp",
|
|
195
|
+
"inkscape",
|
|
196
|
+
"blender",
|
|
197
|
+
"obs",
|
|
198
|
+
"postman",
|
|
199
|
+
"insomnia",
|
|
200
|
+
"dbeaver",
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
# Map process/service names to Ubuntu packages or snap packages
|
|
204
|
+
# Format: "process_name": ("package_name", "install_type") where install_type is "apt" or "snap"
|
|
205
|
+
APP_TO_PACKAGE_MAP = {
|
|
206
|
+
"python": ("python3", "apt"),
|
|
207
|
+
"python3": ("python3", "apt"),
|
|
208
|
+
"pip": ("python3-pip", "apt"),
|
|
209
|
+
"node": ("nodejs", "apt"),
|
|
210
|
+
"npm": ("npm", "apt"),
|
|
211
|
+
"yarn": ("yarnpkg", "apt"),
|
|
212
|
+
"docker": ("docker.io", "apt"),
|
|
213
|
+
"dockerd": ("docker.io", "apt"),
|
|
214
|
+
"docker-compose": ("docker-compose", "apt"),
|
|
215
|
+
"podman": ("podman", "apt"),
|
|
216
|
+
"nginx": ("nginx", "apt"),
|
|
217
|
+
"apache2": ("apache2", "apt"),
|
|
218
|
+
"httpd": ("apache2", "apt"),
|
|
219
|
+
"postgres": ("postgresql", "apt"),
|
|
220
|
+
"postgresql": ("postgresql", "apt"),
|
|
221
|
+
"mysql": ("mysql-server", "apt"),
|
|
222
|
+
"mysqld": ("mysql-server", "apt"),
|
|
223
|
+
"mongod": ("mongodb", "apt"),
|
|
224
|
+
"mongodb": ("mongodb", "apt"),
|
|
225
|
+
"redis-server": ("redis-server", "apt"),
|
|
226
|
+
"redis": ("redis-server", "apt"),
|
|
227
|
+
"vim": ("vim", "apt"),
|
|
228
|
+
"nvim": ("neovim", "apt"),
|
|
229
|
+
"emacs": ("emacs", "apt"),
|
|
230
|
+
"firefox": ("firefox", "apt"),
|
|
231
|
+
"chromium": ("chromium-browser", "apt"),
|
|
232
|
+
"jupyter": ("jupyter-notebook", "apt"),
|
|
233
|
+
"jupyter-lab": ("jupyterlab", "apt"),
|
|
234
|
+
"gunicorn": ("gunicorn", "apt"),
|
|
235
|
+
"uvicorn": ("uvicorn", "apt"),
|
|
236
|
+
"tmux": ("tmux", "apt"),
|
|
237
|
+
"screen": ("screen", "apt"),
|
|
238
|
+
"git": ("git", "apt"),
|
|
239
|
+
"curl": ("curl", "apt"),
|
|
240
|
+
"wget": ("wget", "apt"),
|
|
241
|
+
"ssh": ("openssh-client", "apt"),
|
|
242
|
+
"sshd": ("openssh-server", "apt"),
|
|
243
|
+
"go": ("golang", "apt"),
|
|
244
|
+
"cargo": ("cargo", "apt"),
|
|
245
|
+
"rustc": ("rustc", "apt"),
|
|
246
|
+
"java": ("default-jdk", "apt"),
|
|
247
|
+
"gradle": ("gradle", "apt"),
|
|
248
|
+
"mvn": ("maven", "apt"),
|
|
249
|
+
# Popular desktop apps (snap packages)
|
|
250
|
+
"chrome": ("chromium", "snap"),
|
|
251
|
+
"google-chrome": ("chromium", "snap"),
|
|
252
|
+
"pycharm": ("pycharm-community", "snap"),
|
|
253
|
+
"idea": ("intellij-idea-community", "snap"),
|
|
254
|
+
"code": ("code", "snap"),
|
|
255
|
+
"vscode": ("code", "snap"),
|
|
256
|
+
"slack": ("slack", "snap"),
|
|
257
|
+
"discord": ("discord", "snap"),
|
|
258
|
+
"spotify": ("spotify", "snap"),
|
|
259
|
+
"vlc": ("vlc", "apt"),
|
|
260
|
+
"gimp": ("gimp", "apt"),
|
|
261
|
+
"inkscape": ("inkscape", "apt"),
|
|
262
|
+
"blender": ("blender", "apt"),
|
|
263
|
+
"obs": ("obs-studio", "apt"),
|
|
264
|
+
"telegram": ("telegram-desktop", "snap"),
|
|
265
|
+
"postman": ("postman", "snap"),
|
|
266
|
+
"insomnia": ("insomnia", "snap"),
|
|
267
|
+
"dbeaver": ("dbeaver-ce", "snap"),
|
|
268
|
+
"sublime": ("sublime-text", "snap"),
|
|
269
|
+
"atom": ("atom", "snap"),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Map applications to their config/data directories for complete cloning
|
|
273
|
+
# These directories contain user settings, extensions, profiles, credentials
|
|
274
|
+
APP_DATA_DIRS = {
|
|
275
|
+
# Browsers - profiles, extensions, bookmarks, passwords
|
|
276
|
+
"chrome": [".config/google-chrome", ".config/chromium"],
|
|
277
|
+
"chromium": [".config/chromium"],
|
|
278
|
+
"firefox": [
|
|
279
|
+
"snap/firefox/common/.mozilla/firefox",
|
|
280
|
+
"snap/firefox/common/.cache/mozilla/firefox",
|
|
281
|
+
".mozilla/firefox",
|
|
282
|
+
".cache/mozilla/firefox",
|
|
283
|
+
],
|
|
284
|
+
|
|
285
|
+
# IDEs and editors - settings, extensions, projects history
|
|
286
|
+
"code": [".config/Code", ".vscode", ".vscode-server"],
|
|
287
|
+
"vscode": [".config/Code", ".vscode", ".vscode-server"],
|
|
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
|
+
],
|
|
296
|
+
"idea": [".config/JetBrains", ".local/share/JetBrains"],
|
|
297
|
+
"webstorm": [".config/JetBrains", ".local/share/JetBrains"],
|
|
298
|
+
"goland": [".config/JetBrains", ".local/share/JetBrains"],
|
|
299
|
+
"sublime": [".config/sublime-text", ".config/sublime-text-3"],
|
|
300
|
+
"atom": [".atom"],
|
|
301
|
+
"vim": [".vim", ".vimrc", ".config/nvim"],
|
|
302
|
+
"nvim": [".config/nvim", ".local/share/nvim"],
|
|
303
|
+
"emacs": [".emacs.d", ".emacs"],
|
|
304
|
+
"cursor": [".config/Cursor", ".cursor"],
|
|
305
|
+
|
|
306
|
+
# Development tools
|
|
307
|
+
"docker": [".docker"],
|
|
308
|
+
"git": [".gitconfig", ".git-credentials", ".config/git"],
|
|
309
|
+
"npm": [".npm", ".npmrc"],
|
|
310
|
+
"yarn": [".yarn", ".yarnrc"],
|
|
311
|
+
"pip": [".pip", ".config/pip"],
|
|
312
|
+
"cargo": [".cargo"],
|
|
313
|
+
"rustup": [".rustup"],
|
|
314
|
+
"go": [".go", "go"],
|
|
315
|
+
"gradle": [".gradle"],
|
|
316
|
+
"maven": [".m2"],
|
|
317
|
+
|
|
318
|
+
# Python environments
|
|
319
|
+
"python": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
|
|
320
|
+
"python3": [".pyenv", ".virtualenvs", ".local/share/virtualenvs"],
|
|
321
|
+
"conda": [".conda", "anaconda3", "miniconda3"],
|
|
322
|
+
|
|
323
|
+
# Node.js
|
|
324
|
+
"node": [".nvm", ".node", ".npm"],
|
|
325
|
+
|
|
326
|
+
# Databases
|
|
327
|
+
"postgres": [".pgpass", ".psqlrc", ".psql_history"],
|
|
328
|
+
"mysql": [".my.cnf", ".mysql_history"],
|
|
329
|
+
"mongodb": [".mongorc.js", ".dbshell"],
|
|
330
|
+
"redis": [".rediscli_history"],
|
|
331
|
+
|
|
332
|
+
# Communication apps
|
|
333
|
+
"slack": [".config/Slack"],
|
|
334
|
+
"discord": [".config/discord"],
|
|
335
|
+
"telegram": [".local/share/TelegramDesktop"],
|
|
336
|
+
"teams": [".config/Microsoft/Microsoft Teams"],
|
|
337
|
+
|
|
338
|
+
# Other tools
|
|
339
|
+
"postman": [".config/Postman"],
|
|
340
|
+
"insomnia": [".config/Insomnia"],
|
|
341
|
+
"dbeaver": [".local/share/DBeaverData"],
|
|
342
|
+
"ssh": [".ssh"],
|
|
343
|
+
"gpg": [".gnupg"],
|
|
344
|
+
"aws": [".aws"],
|
|
345
|
+
"gcloud": [".config/gcloud"],
|
|
346
|
+
"kubectl": [".kube"],
|
|
347
|
+
"terraform": [".terraform.d"],
|
|
348
|
+
"ansible": [".ansible"],
|
|
349
|
+
|
|
350
|
+
# General app data
|
|
351
|
+
"spotify": [".config/spotify"],
|
|
352
|
+
"vlc": [".config/vlc"],
|
|
353
|
+
"gimp": [".config/GIMP", ".gimp-2.10"],
|
|
354
|
+
"obs": [".config/obs-studio"],
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
def __init__(self):
|
|
358
|
+
self.user = pwd.getpwuid(os.getuid()).pw_name
|
|
359
|
+
self.home = Path.home()
|
|
360
|
+
|
|
361
|
+
def detect_app_data_dirs(self, applications: list) -> list:
|
|
362
|
+
"""Detect config/data directories for running applications.
|
|
363
|
+
|
|
364
|
+
Returns list of paths that contain user data needed by running apps.
|
|
365
|
+
"""
|
|
366
|
+
app_data_paths = []
|
|
367
|
+
seen_paths = set()
|
|
368
|
+
|
|
369
|
+
matched_patterns = set()
|
|
370
|
+
|
|
371
|
+
for app in applications:
|
|
372
|
+
app_name = app.name.lower()
|
|
373
|
+
|
|
374
|
+
for pattern in self.APP_DATA_DIRS:
|
|
375
|
+
if pattern in app_name:
|
|
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
|
+
})
|
|
403
|
+
|
|
404
|
+
return app_data_paths
|
|
405
|
+
|
|
406
|
+
def detect_all(self) -> SystemSnapshot:
|
|
407
|
+
"""Detect all services, applications and paths."""
|
|
408
|
+
return SystemSnapshot(
|
|
409
|
+
services=self.detect_services(),
|
|
410
|
+
applications=self.detect_applications(),
|
|
411
|
+
paths=self.detect_paths(),
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def detect_services(self) -> list:
|
|
415
|
+
"""Detect systemd services."""
|
|
416
|
+
services = []
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
# Get all services
|
|
420
|
+
result = subprocess.run(
|
|
421
|
+
["systemctl", "list-units", "--type=service", "--all", "--no-pager", "--plain"],
|
|
422
|
+
capture_output=True,
|
|
423
|
+
text=True,
|
|
424
|
+
timeout=10,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
for line in result.stdout.strip().split("\n")[1:]: # Skip header
|
|
428
|
+
parts = line.split()
|
|
429
|
+
if len(parts) >= 4:
|
|
430
|
+
name = parts[0].replace(".service", "")
|
|
431
|
+
|
|
432
|
+
# Filter to interesting services
|
|
433
|
+
if any(
|
|
434
|
+
interesting in name.lower() for interesting in self.INTERESTING_SERVICES
|
|
435
|
+
):
|
|
436
|
+
status = "running" if parts[3] == "running" else parts[3]
|
|
437
|
+
|
|
438
|
+
# Get description
|
|
439
|
+
desc = " ".join(parts[4:]) if len(parts) > 4 else ""
|
|
440
|
+
|
|
441
|
+
# Check if enabled
|
|
442
|
+
enabled = False
|
|
443
|
+
try:
|
|
444
|
+
en_result = subprocess.run(
|
|
445
|
+
["systemctl", "is-enabled", name],
|
|
446
|
+
capture_output=True,
|
|
447
|
+
text=True,
|
|
448
|
+
timeout=5,
|
|
449
|
+
)
|
|
450
|
+
enabled = en_result.stdout.strip() == "enabled"
|
|
451
|
+
except:
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
services.append(
|
|
455
|
+
DetectedService(
|
|
456
|
+
name=name, status=status, description=desc, enabled=enabled
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
return services
|
|
463
|
+
|
|
464
|
+
def detect_applications(self) -> list:
|
|
465
|
+
"""Detect running applications/processes."""
|
|
466
|
+
applications = []
|
|
467
|
+
seen_names = set()
|
|
468
|
+
|
|
469
|
+
for proc in psutil.process_iter(["pid", "name", "cmdline", "exe", "cwd", "memory_info"]):
|
|
470
|
+
try:
|
|
471
|
+
info = proc.info
|
|
472
|
+
name = info["name"] or ""
|
|
473
|
+
|
|
474
|
+
# Filter to interesting processes
|
|
475
|
+
if not any(
|
|
476
|
+
interesting in name.lower() for interesting in self.INTERESTING_PROCESSES
|
|
477
|
+
):
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
# Skip duplicates by name (keep first)
|
|
481
|
+
if name in seen_names:
|
|
482
|
+
continue
|
|
483
|
+
seen_names.add(name)
|
|
484
|
+
|
|
485
|
+
cmdline = " ".join(info["cmdline"] or [])[:200]
|
|
486
|
+
exe = info["exe"] or ""
|
|
487
|
+
cwd = info["cwd"] or ""
|
|
488
|
+
mem = (info["memory_info"].rss / 1024 / 1024) if info["memory_info"] else 0
|
|
489
|
+
|
|
490
|
+
applications.append(
|
|
491
|
+
DetectedApplication(
|
|
492
|
+
name=name,
|
|
493
|
+
pid=info["pid"],
|
|
494
|
+
cmdline=cmdline,
|
|
495
|
+
exe=exe,
|
|
496
|
+
working_dir=cwd,
|
|
497
|
+
memory_mb=round(mem, 1),
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
# Sort by memory usage
|
|
504
|
+
applications.sort(key=lambda x: x.memory_mb, reverse=True)
|
|
505
|
+
return applications
|
|
506
|
+
|
|
507
|
+
def detect_paths(self) -> list:
|
|
508
|
+
"""Detect important paths (projects, configs, data)."""
|
|
509
|
+
paths = []
|
|
510
|
+
|
|
511
|
+
# User home subdirectories
|
|
512
|
+
important_home_dirs = [
|
|
513
|
+
("projects", "project"),
|
|
514
|
+
("workspace", "project"),
|
|
515
|
+
("code", "project"),
|
|
516
|
+
("dev", "project"),
|
|
517
|
+
("work", "project"),
|
|
518
|
+
("repos", "project"),
|
|
519
|
+
("github", "project"),
|
|
520
|
+
("gitlab", "project"),
|
|
521
|
+
(".config", "config"),
|
|
522
|
+
(".local/share", "data"),
|
|
523
|
+
(".ssh", "config"),
|
|
524
|
+
(".docker", "config"),
|
|
525
|
+
(".kube", "config"),
|
|
526
|
+
(".npm", "config"),
|
|
527
|
+
(".cargo", "config"),
|
|
528
|
+
(".rustup", "data"),
|
|
529
|
+
(".pyenv", "data"),
|
|
530
|
+
(".nvm", "data"),
|
|
531
|
+
(".vscode", "config"),
|
|
532
|
+
("Documents", "data"),
|
|
533
|
+
("Downloads", "data"),
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
for dirname, path_type in important_home_dirs:
|
|
537
|
+
full_path = self.home / dirname
|
|
538
|
+
if full_path.exists() and full_path.is_dir():
|
|
539
|
+
size = self._get_dir_size(full_path, max_depth=1)
|
|
540
|
+
paths.append(
|
|
541
|
+
DetectedPath(
|
|
542
|
+
path=str(full_path),
|
|
543
|
+
type=path_type,
|
|
544
|
+
size_mb=round(size / 1024 / 1024, 1),
|
|
545
|
+
description=f"User {dirname}",
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# System paths that might be interesting
|
|
550
|
+
system_paths = [
|
|
551
|
+
("/var/www", "data", "Web server root"),
|
|
552
|
+
("/var/lib/docker", "data", "Docker data"),
|
|
553
|
+
("/var/lib/postgresql", "data", "PostgreSQL data"),
|
|
554
|
+
("/var/lib/mysql", "data", "MySQL data"),
|
|
555
|
+
("/opt", "data", "Optional software"),
|
|
556
|
+
("/etc/nginx", "config", "Nginx config"),
|
|
557
|
+
("/etc/apache2", "config", "Apache config"),
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
for path, path_type, desc in system_paths:
|
|
561
|
+
p = Path(path)
|
|
562
|
+
if p.exists():
|
|
563
|
+
size = self._get_dir_size(p, max_depth=1)
|
|
564
|
+
paths.append(
|
|
565
|
+
DetectedPath(
|
|
566
|
+
path=path,
|
|
567
|
+
type=path_type,
|
|
568
|
+
size_mb=round(size / 1024 / 1024, 1),
|
|
569
|
+
description=desc,
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Detect project directories (with .git, package.json, etc.)
|
|
574
|
+
project_markers = [
|
|
575
|
+
".git",
|
|
576
|
+
"package.json",
|
|
577
|
+
"Cargo.toml",
|
|
578
|
+
"go.mod",
|
|
579
|
+
"pyproject.toml",
|
|
580
|
+
"setup.py",
|
|
581
|
+
]
|
|
582
|
+
for search_dir in [
|
|
583
|
+
self.home / "projects",
|
|
584
|
+
self.home / "code",
|
|
585
|
+
self.home / "github",
|
|
586
|
+
self.home,
|
|
587
|
+
]:
|
|
588
|
+
if search_dir.exists():
|
|
589
|
+
for item in search_dir.iterdir():
|
|
590
|
+
if item.is_dir() and not item.name.startswith("."):
|
|
591
|
+
for marker in project_markers:
|
|
592
|
+
if (item / marker).exists():
|
|
593
|
+
size = self._get_dir_size(item, max_depth=2)
|
|
594
|
+
if str(item) not in [p.path for p in paths]:
|
|
595
|
+
paths.append(
|
|
596
|
+
DetectedPath(
|
|
597
|
+
path=str(item),
|
|
598
|
+
type="project",
|
|
599
|
+
size_mb=round(size / 1024 / 1024, 1),
|
|
600
|
+
description=f"Project ({marker})",
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
break
|
|
604
|
+
|
|
605
|
+
# Sort by type then path
|
|
606
|
+
paths.sort(key=lambda x: (x.type, x.path))
|
|
607
|
+
return paths
|
|
608
|
+
|
|
609
|
+
def _get_dir_size(self, path: Path, max_depth: int = 2) -> int:
|
|
610
|
+
"""Get approximate directory size in bytes."""
|
|
611
|
+
total = 0
|
|
612
|
+
if not path.exists():
|
|
613
|
+
return 0
|
|
614
|
+
try:
|
|
615
|
+
for item in path.iterdir():
|
|
616
|
+
if item.is_file():
|
|
617
|
+
try:
|
|
618
|
+
total += item.stat().st_size
|
|
619
|
+
except:
|
|
620
|
+
pass
|
|
621
|
+
elif item.is_dir() and max_depth > 0 and not item.is_symlink():
|
|
622
|
+
total += self._get_dir_size(item, max_depth - 1)
|
|
623
|
+
except (PermissionError, FileNotFoundError, OSError):
|
|
624
|
+
pass
|
|
625
|
+
return total
|
|
626
|
+
|
|
627
|
+
def detect_docker_containers(self) -> list:
|
|
628
|
+
"""Detect running Docker containers."""
|
|
629
|
+
containers = []
|
|
630
|
+
try:
|
|
631
|
+
result = subprocess.run(
|
|
632
|
+
["docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Status}}"],
|
|
633
|
+
capture_output=True,
|
|
634
|
+
text=True,
|
|
635
|
+
timeout=10,
|
|
636
|
+
)
|
|
637
|
+
for line in result.stdout.strip().split("\n"):
|
|
638
|
+
if line:
|
|
639
|
+
parts = line.split("\t")
|
|
640
|
+
if len(parts) >= 3:
|
|
641
|
+
containers.append({"name": parts[0], "image": parts[1], "status": parts[2]})
|
|
642
|
+
except:
|
|
643
|
+
pass
|
|
644
|
+
return containers
|
|
645
|
+
|
|
646
|
+
def suggest_packages_for_apps(self, applications: list) -> dict:
|
|
647
|
+
"""Suggest packages based on detected applications.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
dict with 'apt' and 'snap' keys containing lists of packages
|
|
651
|
+
"""
|
|
652
|
+
apt_packages = set()
|
|
653
|
+
snap_packages = set()
|
|
654
|
+
|
|
655
|
+
for app in applications:
|
|
656
|
+
app_name = app.name.lower()
|
|
657
|
+
for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
|
|
658
|
+
if key in app_name:
|
|
659
|
+
if install_type == "snap":
|
|
660
|
+
snap_packages.add(package)
|
|
661
|
+
else:
|
|
662
|
+
apt_packages.add(package)
|
|
663
|
+
break
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
"apt": sorted(list(apt_packages)),
|
|
667
|
+
"snap": sorted(list(snap_packages))
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
def suggest_packages_for_services(self, services: list) -> dict:
|
|
671
|
+
"""Suggest packages based on detected services.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
dict with 'apt' and 'snap' keys containing lists of packages
|
|
675
|
+
"""
|
|
676
|
+
apt_packages = set()
|
|
677
|
+
snap_packages = set()
|
|
678
|
+
|
|
679
|
+
for service in services:
|
|
680
|
+
service_name = service.name.lower()
|
|
681
|
+
for key, (package, install_type) in self.APP_TO_PACKAGE_MAP.items():
|
|
682
|
+
if key in service_name:
|
|
683
|
+
if install_type == "snap":
|
|
684
|
+
snap_packages.add(package)
|
|
685
|
+
else:
|
|
686
|
+
apt_packages.add(package)
|
|
687
|
+
break
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
"apt": sorted(list(apt_packages)),
|
|
691
|
+
"snap": sorted(list(snap_packages))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
def get_system_info(self) -> dict:
|
|
695
|
+
"""Get basic system information."""
|
|
696
|
+
return {
|
|
697
|
+
"hostname": os.uname().nodename,
|
|
698
|
+
"user": self.user,
|
|
699
|
+
"home": str(self.home),
|
|
700
|
+
"cpu_count": psutil.cpu_count(),
|
|
701
|
+
"memory_total_gb": round(psutil.virtual_memory().total / 1024 / 1024 / 1024, 1),
|
|
702
|
+
"memory_available_gb": round(psutil.virtual_memory().available / 1024 / 1024 / 1024, 1),
|
|
703
|
+
"disk_total_gb": round(psutil.disk_usage("/").total / 1024 / 1024 / 1024, 1),
|
|
704
|
+
"disk_free_gb": round(psutil.disk_usage("/").free / 1024 / 1024 / 1024, 1),
|
|
705
|
+
}
|