clonebox 0.1.3__py3-none-any.whl → 0.1.4__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/detector.py CHANGED
@@ -4,11 +4,10 @@ SystemDetector - Detects running services, applications and important paths.
4
4
  """
5
5
 
6
6
  import os
7
- import subprocess
8
7
  import pwd
9
- from pathlib import Path
8
+ import subprocess
10
9
  from dataclasses import dataclass, field
11
- from typing import Optional
10
+ from pathlib import Path
12
11
 
13
12
  import psutil
14
13
 
@@ -16,6 +15,7 @@ import psutil
16
15
  @dataclass
17
16
  class DetectedService:
18
17
  """A detected systemd service."""
18
+
19
19
  name: str
20
20
  status: str # running, stopped, failed
21
21
  description: str = ""
@@ -25,6 +25,7 @@ class DetectedService:
25
25
  @dataclass
26
26
  class DetectedApplication:
27
27
  """A detected running application."""
28
+
28
29
  name: str
29
30
  pid: int
30
31
  cmdline: str
@@ -36,6 +37,7 @@ class DetectedApplication:
36
37
  @dataclass
37
38
  class DetectedPath:
38
39
  """A detected important path."""
40
+
39
41
  path: str
40
42
  type: str # config, data, project, home
41
43
  size_mb: float = 0.0
@@ -45,14 +47,15 @@ class DetectedPath:
45
47
  @dataclass
46
48
  class SystemSnapshot:
47
49
  """Complete snapshot of detected system state."""
50
+
48
51
  services: list = field(default_factory=list)
49
52
  applications: list = field(default_factory=list)
50
53
  paths: list = field(default_factory=list)
51
-
54
+
52
55
  @property
53
56
  def running_services(self) -> list:
54
57
  return [s for s in self.services if s.status == "running"]
55
-
58
+
56
59
  @property
57
60
  def running_apps(self) -> list:
58
61
  return self.applications
@@ -60,43 +63,93 @@ class SystemSnapshot:
60
63
 
61
64
  class SystemDetector:
62
65
  """Detects running services, applications and important paths on the system."""
63
-
66
+
64
67
  # Common development/server services to look for
65
68
  INTERESTING_SERVICES = [
66
- "docker", "containerd", "podman",
67
- "nginx", "apache2", "httpd", "caddy",
68
- "postgresql", "mysql", "mariadb", "mongodb", "redis", "memcached",
69
- "elasticsearch", "kibana", "grafana", "prometheus",
70
- "jenkins", "gitlab-runner",
71
- "sshd", "rsync",
72
- "rabbitmq-server", "kafka",
73
- "nodejs", "pm2",
74
- "supervisor", "systemd-resolved",
75
- "cups", "bluetooth", "NetworkManager",
76
- "libvirtd", "virtlogd",
69
+ "docker",
70
+ "containerd",
71
+ "podman",
72
+ "nginx",
73
+ "apache2",
74
+ "httpd",
75
+ "caddy",
76
+ "postgresql",
77
+ "mysql",
78
+ "mariadb",
79
+ "mongodb",
80
+ "redis",
81
+ "memcached",
82
+ "elasticsearch",
83
+ "kibana",
84
+ "grafana",
85
+ "prometheus",
86
+ "jenkins",
87
+ "gitlab-runner",
88
+ "sshd",
89
+ "rsync",
90
+ "rabbitmq-server",
91
+ "kafka",
92
+ "nodejs",
93
+ "pm2",
94
+ "supervisor",
95
+ "systemd-resolved",
96
+ "cups",
97
+ "bluetooth",
98
+ "NetworkManager",
99
+ "libvirtd",
100
+ "virtlogd",
77
101
  ]
78
-
102
+
79
103
  # Interesting process names
80
104
  INTERESTING_PROCESSES = [
81
- "python", "python3", "node", "npm", "yarn", "pnpm",
82
- "java", "gradle", "mvn",
83
- "go", "cargo", "rustc",
84
- "docker", "docker-compose", "podman",
85
- "nginx", "apache", "httpd",
86
- "postgres", "mysql", "mongod", "redis-server",
87
- "code", "code-server", "cursor",
88
- "vim", "nvim", "emacs",
89
- "firefox", "chrome", "chromium",
90
- "jupyter", "jupyter-lab",
91
- "gunicorn", "uvicorn", "flask", "django",
92
- "webpack", "vite", "esbuild",
93
- "tmux", "screen",
105
+ "python",
106
+ "python3",
107
+ "node",
108
+ "npm",
109
+ "yarn",
110
+ "pnpm",
111
+ "java",
112
+ "gradle",
113
+ "mvn",
114
+ "go",
115
+ "cargo",
116
+ "rustc",
117
+ "docker",
118
+ "docker-compose",
119
+ "podman",
120
+ "nginx",
121
+ "apache",
122
+ "httpd",
123
+ "postgres",
124
+ "mysql",
125
+ "mongod",
126
+ "redis-server",
127
+ "code",
128
+ "code-server",
129
+ "cursor",
130
+ "vim",
131
+ "nvim",
132
+ "emacs",
133
+ "firefox",
134
+ "chrome",
135
+ "chromium",
136
+ "jupyter",
137
+ "jupyter-lab",
138
+ "gunicorn",
139
+ "uvicorn",
140
+ "flask",
141
+ "django",
142
+ "webpack",
143
+ "vite",
144
+ "esbuild",
145
+ "tmux",
146
+ "screen",
94
147
  ]
95
-
148
+
96
149
  def __init__(self):
97
150
  self.user = pwd.getpwuid(os.getuid()).pw_name
98
151
  self.home = Path.home()
99
-
152
+
100
153
  def detect_all(self) -> SystemSnapshot:
101
154
  """Detect all services, applications and paths."""
102
155
  return SystemSnapshot(
@@ -104,95 +157,104 @@ class SystemDetector:
104
157
  applications=self.detect_applications(),
105
158
  paths=self.detect_paths(),
106
159
  )
107
-
160
+
108
161
  def detect_services(self) -> list:
109
162
  """Detect systemd services."""
110
163
  services = []
111
-
164
+
112
165
  try:
113
166
  # Get all services
114
167
  result = subprocess.run(
115
168
  ["systemctl", "list-units", "--type=service", "--all", "--no-pager", "--plain"],
116
- capture_output=True, text=True, timeout=10
169
+ capture_output=True,
170
+ text=True,
171
+ timeout=10,
117
172
  )
118
-
173
+
119
174
  for line in result.stdout.strip().split("\n")[1:]: # Skip header
120
175
  parts = line.split()
121
176
  if len(parts) >= 4:
122
177
  name = parts[0].replace(".service", "")
123
-
178
+
124
179
  # Filter to interesting services
125
- if any(interesting in name.lower() for interesting in self.INTERESTING_SERVICES):
180
+ if any(
181
+ interesting in name.lower() for interesting in self.INTERESTING_SERVICES
182
+ ):
126
183
  status = "running" if parts[3] == "running" else parts[3]
127
-
184
+
128
185
  # Get description
129
186
  desc = " ".join(parts[4:]) if len(parts) > 4 else ""
130
-
187
+
131
188
  # Check if enabled
132
189
  enabled = False
133
190
  try:
134
191
  en_result = subprocess.run(
135
192
  ["systemctl", "is-enabled", name],
136
- capture_output=True, text=True, timeout=5
193
+ capture_output=True,
194
+ text=True,
195
+ timeout=5,
137
196
  )
138
197
  enabled = en_result.stdout.strip() == "enabled"
139
198
  except:
140
199
  pass
141
-
142
- services.append(DetectedService(
143
- name=name,
144
- status=status,
145
- description=desc,
146
- enabled=enabled
147
- ))
200
+
201
+ services.append(
202
+ DetectedService(
203
+ name=name, status=status, description=desc, enabled=enabled
204
+ )
205
+ )
148
206
  except Exception:
149
207
  pass
150
-
208
+
151
209
  return services
152
-
210
+
153
211
  def detect_applications(self) -> list:
154
212
  """Detect running applications/processes."""
155
213
  applications = []
156
214
  seen_names = set()
157
-
158
- for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'exe', 'cwd', 'memory_info']):
215
+
216
+ for proc in psutil.process_iter(["pid", "name", "cmdline", "exe", "cwd", "memory_info"]):
159
217
  try:
160
218
  info = proc.info
161
- name = info['name'] or ""
162
-
219
+ name = info["name"] or ""
220
+
163
221
  # Filter to interesting processes
164
- if not any(interesting in name.lower() for interesting in self.INTERESTING_PROCESSES):
222
+ if not any(
223
+ interesting in name.lower() for interesting in self.INTERESTING_PROCESSES
224
+ ):
165
225
  continue
166
-
226
+
167
227
  # Skip duplicates by name (keep first)
168
228
  if name in seen_names:
169
229
  continue
170
230
  seen_names.add(name)
171
-
172
- cmdline = " ".join(info['cmdline'] or [])[:200]
173
- exe = info['exe'] or ""
174
- cwd = info['cwd'] or ""
175
- mem = (info['memory_info'].rss / 1024 / 1024) if info['memory_info'] else 0
176
-
177
- applications.append(DetectedApplication(
178
- name=name,
179
- pid=info['pid'],
180
- cmdline=cmdline,
181
- exe=exe,
182
- working_dir=cwd,
183
- memory_mb=round(mem, 1)
184
- ))
231
+
232
+ cmdline = " ".join(info["cmdline"] or [])[:200]
233
+ exe = info["exe"] or ""
234
+ cwd = info["cwd"] or ""
235
+ mem = (info["memory_info"].rss / 1024 / 1024) if info["memory_info"] else 0
236
+
237
+ applications.append(
238
+ DetectedApplication(
239
+ name=name,
240
+ pid=info["pid"],
241
+ cmdline=cmdline,
242
+ exe=exe,
243
+ working_dir=cwd,
244
+ memory_mb=round(mem, 1),
245
+ )
246
+ )
185
247
  except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
186
248
  continue
187
-
249
+
188
250
  # Sort by memory usage
189
251
  applications.sort(key=lambda x: x.memory_mb, reverse=True)
190
252
  return applications
191
-
253
+
192
254
  def detect_paths(self) -> list:
193
255
  """Detect important paths (projects, configs, data)."""
194
256
  paths = []
195
-
257
+
196
258
  # User home subdirectories
197
259
  important_home_dirs = [
198
260
  ("projects", "project"),
@@ -217,18 +279,20 @@ class SystemDetector:
217
279
  ("Documents", "data"),
218
280
  ("Downloads", "data"),
219
281
  ]
220
-
282
+
221
283
  for dirname, path_type in important_home_dirs:
222
284
  full_path = self.home / dirname
223
285
  if full_path.exists() and full_path.is_dir():
224
286
  size = self._get_dir_size(full_path, max_depth=1)
225
- paths.append(DetectedPath(
226
- path=str(full_path),
227
- type=path_type,
228
- size_mb=round(size / 1024 / 1024, 1),
229
- description=f"User {dirname}"
230
- ))
231
-
287
+ paths.append(
288
+ DetectedPath(
289
+ path=str(full_path),
290
+ type=path_type,
291
+ size_mb=round(size / 1024 / 1024, 1),
292
+ description=f"User {dirname}",
293
+ )
294
+ )
295
+
232
296
  # System paths that might be interesting
233
297
  system_paths = [
234
298
  ("/var/www", "data", "Web server root"),
@@ -239,21 +303,35 @@ class SystemDetector:
239
303
  ("/etc/nginx", "config", "Nginx config"),
240
304
  ("/etc/apache2", "config", "Apache config"),
241
305
  ]
242
-
306
+
243
307
  for path, path_type, desc in system_paths:
244
308
  p = Path(path)
245
309
  if p.exists():
246
310
  size = self._get_dir_size(p, max_depth=1)
247
- paths.append(DetectedPath(
248
- path=path,
249
- type=path_type,
250
- size_mb=round(size / 1024 / 1024, 1),
251
- description=desc
252
- ))
253
-
311
+ paths.append(
312
+ DetectedPath(
313
+ path=path,
314
+ type=path_type,
315
+ size_mb=round(size / 1024 / 1024, 1),
316
+ description=desc,
317
+ )
318
+ )
319
+
254
320
  # Detect project directories (with .git, package.json, etc.)
255
- project_markers = [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml", "setup.py"]
256
- for search_dir in [self.home / "projects", self.home / "code", self.home / "github", self.home]:
321
+ project_markers = [
322
+ ".git",
323
+ "package.json",
324
+ "Cargo.toml",
325
+ "go.mod",
326
+ "pyproject.toml",
327
+ "setup.py",
328
+ ]
329
+ for search_dir in [
330
+ self.home / "projects",
331
+ self.home / "code",
332
+ self.home / "github",
333
+ self.home,
334
+ ]:
257
335
  if search_dir.exists():
258
336
  for item in search_dir.iterdir():
259
337
  if item.is_dir() and not item.name.startswith("."):
@@ -261,18 +339,20 @@ class SystemDetector:
261
339
  if (item / marker).exists():
262
340
  size = self._get_dir_size(item, max_depth=2)
263
341
  if str(item) not in [p.path for p in paths]:
264
- paths.append(DetectedPath(
265
- path=str(item),
266
- type="project",
267
- size_mb=round(size / 1024 / 1024, 1),
268
- description=f"Project ({marker})"
269
- ))
342
+ paths.append(
343
+ DetectedPath(
344
+ path=str(item),
345
+ type="project",
346
+ size_mb=round(size / 1024 / 1024, 1),
347
+ description=f"Project ({marker})",
348
+ )
349
+ )
270
350
  break
271
-
351
+
272
352
  # Sort by type then path
273
353
  paths.sort(key=lambda x: (x.type, x.path))
274
354
  return paths
275
-
355
+
276
356
  def _get_dir_size(self, path: Path, max_depth: int = 2) -> int:
277
357
  """Get approximate directory size in bytes."""
278
358
  total = 0
@@ -290,28 +370,26 @@ class SystemDetector:
290
370
  except (PermissionError, FileNotFoundError, OSError):
291
371
  pass
292
372
  return total
293
-
373
+
294
374
  def detect_docker_containers(self) -> list:
295
375
  """Detect running Docker containers."""
296
376
  containers = []
297
377
  try:
298
378
  result = subprocess.run(
299
379
  ["docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Status}}"],
300
- capture_output=True, text=True, timeout=10
380
+ capture_output=True,
381
+ text=True,
382
+ timeout=10,
301
383
  )
302
384
  for line in result.stdout.strip().split("\n"):
303
385
  if line:
304
386
  parts = line.split("\t")
305
387
  if len(parts) >= 3:
306
- containers.append({
307
- "name": parts[0],
308
- "image": parts[1],
309
- "status": parts[2]
310
- })
388
+ containers.append({"name": parts[0], "image": parts[1], "status": parts[2]})
311
389
  except:
312
390
  pass
313
391
  return containers
314
-
392
+
315
393
  def get_system_info(self) -> dict:
316
394
  """Get basic system information."""
317
395
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.3
3
+ Version: 0.1.4
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
@@ -291,6 +291,29 @@ clonebox detect --yaml --dedupe
291
291
  clonebox detect --yaml --dedupe -o my-config.yaml
292
292
  ```
293
293
 
294
+ ### User Session & Networking
295
+
296
+ CloneBox supports creating VMs in user session (no root required) with automatic network fallback:
297
+
298
+ ```bash
299
+ # Create VM in user session (uses ~/.local/share/libvirt/images)
300
+ clonebox clone . --user
301
+
302
+ # Explicitly use user-mode networking (slirp) - works without libvirt network
303
+ clonebox clone . --user --network user
304
+
305
+ # Force libvirt default network (may fail in user session)
306
+ clonebox clone . --network default
307
+
308
+ # Auto mode (default): tries libvirt network, falls back to user-mode if unavailable
309
+ clonebox clone . --network auto
310
+ ```
311
+
312
+ **Network modes:**
313
+ - `auto` (default): Uses libvirt default network if available, otherwise falls back to user-mode (slirp)
314
+ - `default`: Forces use of libvirt default network
315
+ - `user`: Uses user-mode networking (slirp) - no bridge setup required
316
+
294
317
  ## Commands Reference
295
318
 
296
319
  | Command | Description |
@@ -299,6 +322,9 @@ clonebox detect --yaml --dedupe -o my-config.yaml
299
322
  | `clonebox clone <path>` | Generate `.clonebox.yaml` from path + running processes |
300
323
  | `clonebox clone . --run` | Clone and immediately start VM |
301
324
  | `clonebox clone . --edit` | Clone, edit config, then create |
325
+ | `clonebox clone . --user` | Clone in user session (no root) |
326
+ | `clonebox clone . --network user` | Use user-mode networking (slirp) |
327
+ | `clonebox clone . --network auto` | Auto-detect network mode (default) |
302
328
  | `clonebox start .` | Start VM from `.clonebox.yaml` in current dir |
303
329
  | `clonebox start <name>` | Start existing VM by name |
304
330
  | `clonebox stop <name>` | Stop a VM (graceful shutdown) |
@@ -324,7 +350,10 @@ clonebox detect --yaml --dedupe -o my-config.yaml
324
350
  If you encounter "Network not found" or "network 'default' is not active" errors:
325
351
 
326
352
  ```bash
327
- # Run the network fix script
353
+ # Option 1: Use user-mode networking (no setup required)
354
+ clonebox clone . --user --network user
355
+
356
+ # Option 2: Run the network fix script
328
357
  ./fix-network.sh
329
358
 
330
359
  # Or manually fix:
@@ -0,0 +1,11 @@
1
+ clonebox/__init__.py,sha256=IOk7G0DiSQ33EGbFC0xbnnFB9aou_6yuyFxvycQEvA0,407
2
+ clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
+ clonebox/cli.py,sha256=gtnFl0Jibfwuy4uMF9_BPTOvbHzTgpXsy71WwLEtY64,31781
4
+ clonebox/cloner.py,sha256=Uoh9mCUX-3p2tFL_3qlf2R2232JCXO5YhWrgKTpEr0s,19369
5
+ clonebox/detector.py,sha256=jkzENmi4720n5e04k6gM7MNvXbQdYX-z1_O3Id0WK9w,12505
6
+ clonebox-0.1.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
7
+ clonebox-0.1.4.dist-info/METADATA,sha256=YQg_Br_7JJcWvhNTKhWokMXftG2Xht_zfPQ_zmJuzSQ,13126
8
+ clonebox-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ clonebox-0.1.4.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
10
+ clonebox-0.1.4.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
11
+ clonebox-0.1.4.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- clonebox/__init__.py,sha256=IOk7G0DiSQ33EGbFC0xbnnFB9aou_6yuyFxvycQEvA0,407
2
- clonebox/cli.py,sha256=tg_tinIH3D6Q1xAjhXu7P4msl2XcxLo8XUHMDxkOFis,31996
3
- clonebox/cloner.py,sha256=bB37BFYY7_xlfOSdk05zrUsrw7ewItRBMb7EJkYFA_0,19671
4
- clonebox/detector.py,sha256=Umg4CRJU61yV3a1AvR_0tOfjBMCCIbiQdDAAhlrOL5k,11916
5
- clonebox-0.1.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
6
- clonebox-0.1.3.dist-info/METADATA,sha256=W6d_Km3nbulNWpl5Z6KctOHciT1o14o4OnAELJMAfbc,11996
7
- clonebox-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
- clonebox-0.1.3.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
9
- clonebox-0.1.3.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
10
- clonebox-0.1.3.dist-info/RECORD,,