helper-cli 0.1.1__py3-none-any.whl → 0.1.11__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.
@@ -0,0 +1 @@
1
+ # Initialize commands package
@@ -0,0 +1,8 @@
1
+ from ..utils import run_cmd
2
+ import click
3
+
4
+ @click.command()
5
+ def arch():
6
+ """Show CPU architecture"""
7
+ cmd = "uname -m"
8
+ run_cmd(cmd)
@@ -0,0 +1,609 @@
1
+ import click
2
+ import subprocess
3
+ import json
4
+ import re
5
+ import logging
6
+ import sys
7
+ from typing import Dict, List, Tuple, Optional, Any
8
+
9
+ # Configure logging
10
+ logging.basicConfig(
11
+ level=logging.WARNING,
12
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
13
+ stream=sys.stderr
14
+ )
15
+ logger = logging.getLogger('docker-helper')
16
+
17
+ class Verbosity:
18
+ """Handle verbosity levels for logging."""
19
+ def __init__(self, verbosity: int = 0):
20
+ self.verbosity = verbosity
21
+ self.set_level()
22
+
23
+ def set_level(self):
24
+ """Set logging level based on verbosity."""
25
+ if self.verbosity >= 3:
26
+ logger.setLevel(logging.DEBUG)
27
+ elif self.verbosity == 2:
28
+ logger.setLevel(logging.INFO)
29
+ elif self.verbosity == 1:
30
+ logger.setLevel(logging.WARNING)
31
+ else:
32
+ logger.setLevel(logging.ERROR)
33
+
34
+ def debug(self, msg: str, *args, **kwargs):
35
+ """Log debug message if verbosity >= 3."""
36
+ if self.verbosity >= 3:
37
+ logger.debug(msg, *args, **kwargs)
38
+
39
+ def info(self, msg: str, *args, **kwargs):
40
+ """Log info message if verbosity >= 2."""
41
+ if self.verbosity >= 2:
42
+ logger.info(msg, *args, **kwargs)
43
+
44
+ def warning(self, msg: str, *args, **kwargs):
45
+ """Log warning message if verbosity >= 1."""
46
+ if self.verbosity >= 1:
47
+ logger.warning(msg, *args, **kwargs)
48
+
49
+ def error(self, msg: str, *args, **kwargs):
50
+ """Log error message regardless of verbosity."""
51
+ logger.error(msg, *args, **kwargs)
52
+
53
+ def get_container_ports(container_id: str, verbosity: Verbosity) -> List[Dict]:
54
+ """Get exposed ports and IPs for a container."""
55
+ verbosity.debug(f"Getting ports for container {container_id}")
56
+ try:
57
+ result = subprocess.run(
58
+ ['docker', 'inspect', '--format',
59
+ '{{range $p, $conf := .NetworkSettings.Ports}}' # noqa: E501
60
+ '{{range $h, $hosts := $conf}}'
61
+ '{{$p}}|{{$hosts.HostIp}}|{{$hosts.HostPort}};'
62
+ '{{end}}{{end}}',
63
+ container_id],
64
+ capture_output=True,
65
+ text=True
66
+ )
67
+
68
+ if result.returncode != 0:
69
+ verbosity.error(f"Failed to get container info: {result.stderr}")
70
+ return []
71
+
72
+ ports = []
73
+ raw_output = result.stdout.strip()
74
+ verbosity.debug(f"Raw port mappings: {raw_output}")
75
+
76
+ if raw_output:
77
+ for mapping in raw_output.split(';'):
78
+ if not mapping:
79
+ continue
80
+ try:
81
+ container_port, host_ip, host_port = mapping.split('|')
82
+ verbosity.debug(f"Processing mapping: container={container_port}, host_ip={host_ip}, host_port={host_port}")
83
+
84
+ if container_port and host_port:
85
+ port_info = {
86
+ 'container_port': container_port.split('/')[0], # Remove /tcp or /udp
87
+ 'host_ip': host_ip if host_ip not in ('0.0.0.0', '') else 'localhost',
88
+ 'host_port': host_port
89
+ }
90
+ verbosity.info(f"Added port mapping: {port_info}")
91
+ ports.append(port_info)
92
+ else:
93
+ verbosity.debug(f"Skipping incomplete mapping: {mapping}")
94
+ except ValueError as e:
95
+ verbosity.warning(f"Failed to parse mapping '{mapping}': {e}")
96
+ continue
97
+ verbosity.debug(f"Final port mappings: {ports}")
98
+ return ports
99
+ except Exception as e:
100
+ verbosity.error(f"Unexpected error in get_container_ports: {str(e)}", exc_info=verbosity.verbosity >= 3)
101
+ return []
102
+
103
+ def check_docker(verbosity: Verbosity) -> bool:
104
+ """Check if Docker is installed and running."""
105
+ verbosity.info("Checking if Docker is installed and running...")
106
+ try:
107
+ result = subprocess.run(['docker', 'info'],
108
+ capture_output=True,
109
+ text=True)
110
+
111
+ verbosity.debug(f"Docker info command output:\n{result.stdout}")
112
+
113
+ if result.returncode != 0:
114
+ verbosity.error(f"Docker is not running or not accessible. Error: {result.stderr}")
115
+ return False
116
+
117
+ verbosity.info("Docker is running and accessible")
118
+ return True
119
+
120
+ except FileNotFoundError:
121
+ verbosity.error("Docker command not found. Is Docker installed?")
122
+ return False
123
+ except Exception as e:
124
+ verbosity.error(f"Unexpected error checking Docker: {str(e)}", exc_info=verbosity.verbosity >= 3)
125
+ return False
126
+
127
+ def format_output(output, output_format='table'):
128
+ """Format command output based on the specified format."""
129
+ if output_format == 'json':
130
+ try:
131
+ return json.dumps(json.loads(output), indent=2)
132
+ except json.JSONDecodeError:
133
+ return output
134
+ return output
135
+
136
+ def get_verbosity(ctx: click.Context) -> Verbosity:
137
+ """Get verbosity level from context."""
138
+ # Count the number of 'v's in the --verbose flag
139
+ verbose = ctx.params.get('verbose', 0)
140
+ verbosity = Verbosity(verbosity=verbose)
141
+ verbosity.info(f"Verbosity level set to {verbose}")
142
+ return verbosity
143
+
144
+ @click.group()
145
+ @click.option('-v', '--verbose', count=True, help='Increase verbosity (can be used multiple times)')
146
+ @click.pass_context
147
+ def docker(ctx, verbose):
148
+ """Docker management commands."""
149
+ ctx.ensure_object(dict)
150
+
151
+ # Get verbosity from parent context if it exists, otherwise use the flag value
152
+ parent_verbosity = ctx.obj.get('verbosity', 0) if hasattr(ctx, 'obj') else 0
153
+ verbosity_level = max(verbose, parent_verbosity)
154
+
155
+ # Initialize verbosity
156
+ verbosity = Verbosity(verbosity=verbosity_level)
157
+ ctx.obj['verbosity'] = verbosity
158
+
159
+ # Set up logging
160
+ logger = logging.getLogger('docker-helper')
161
+ if verbosity_level >= 3:
162
+ logger.setLevel(logging.DEBUG)
163
+ elif verbosity_level == 2:
164
+ logger.setLevel(logging.INFO)
165
+ elif verbosity_level == 1:
166
+ logger.setLevel(logging.WARNING)
167
+ else:
168
+ logger.setLevel(logging.ERROR)
169
+
170
+ logger.debug(f"Docker command group initialized with verbosity level: {verbosity_level}")
171
+
172
+ verbosity.debug("Initializing Docker command group")
173
+ if not check_docker(verbosity):
174
+ click.echo("Error: Docker is not installed or not running. Please start Docker and try again.", err=True)
175
+ ctx.exit(1)
176
+
177
+ @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
178
+ @click.option('--all', '-a', is_flag=True, help='Show all containers (default shows just running)')
179
+ @click.option('--format', type=click.Choice(['table', 'json'], case_sensitive=False),
180
+ default='table', help='Output format')
181
+ @click.pass_context
182
+ def ps(ctx, all, format):
183
+ """List containers."""
184
+ verbosity = ctx.obj['verbosity']
185
+ cmd = ['docker', 'ps']
186
+ if all:
187
+ cmd.append('-a')
188
+
189
+ try:
190
+ verbosity.debug(f"Running command: {' '.join(cmd)}")
191
+ result = subprocess.run(cmd, capture_output=True, text=True)
192
+
193
+ if result.returncode == 0:
194
+ if format == 'json':
195
+ # Try to parse and pretty-print JSON output
196
+ try:
197
+ data = json.loads(result.stdout)
198
+ click.echo(json.dumps(data, indent=2))
199
+ except json.JSONDecodeError:
200
+ # Fall back to raw output if not valid JSON
201
+ click.echo(result.stdout)
202
+ else:
203
+ # For table format, try to align columns
204
+ lines = result.stdout.strip().split('\n')
205
+ if len(lines) > 1:
206
+ # Parse as JSON to handle special characters in values
207
+ try:
208
+ data = [json.loads(line) for line in lines[1:]]
209
+ headers = data[0].keys()
210
+ rows = [[item.get(header, '') for header in headers] for item in data]
211
+
212
+ # Calculate column widths
213
+ col_widths = [max(len(str(header)),
214
+ max((len(str(row[i])) for row in rows), default=0))
215
+ for i, header in enumerate(headers)]
216
+
217
+ # Print header
218
+ header_row = " ".join(header.ljust(width) for header, width in zip(headers, col_widths))
219
+ click.echo(header_row)
220
+ click.echo("-" * len(header_row))
221
+
222
+ # Print rows
223
+ for row in rows:
224
+ click.echo(" ".join(str(cell).ljust(width) for cell, width in zip(row, col_widths)))
225
+ except Exception as e:
226
+ verbosity.debug(f"Error formatting table: {str(e)}")
227
+ # Fall back to raw output if processing fails
228
+ click.echo(result.stdout)
229
+ else:
230
+ click.echo(result.stdout)
231
+ else:
232
+ error_msg = f"Error: {result.stderr}"
233
+ verbosity.error(error_msg)
234
+ click.echo(error_msg, err=True)
235
+ except subprocess.CalledProcessError as e:
236
+ error_msg = f"Command failed: {str(e)}"
237
+ verbosity.error(error_msg)
238
+ click.echo(error_msg, err=True)
239
+ except Exception as e:
240
+ error_msg = f"Unexpected error: {str(e)}"
241
+ verbosity.error(error_msg)
242
+ click.echo(error_msg, err=True)
243
+
244
+ @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
245
+ @click.argument('image', required=False)
246
+ @click.option('--name', help='Assign a name to the container')
247
+ @click.option('--port', '-p', multiple=True, help='Publish a container\'s port(s) to the host')
248
+ @click.option('--detach', '-d', is_flag=True, help='Run container in background and print container ID')
249
+ @click.option('--env', '-e', multiple=True, help='Set environment variables')
250
+ @click.option('--volume', '-v', multiple=True, help='Bind mount a volume')
251
+ @click.pass_context
252
+ def run(ctx, image, name, port, detach, env, volume):
253
+ """Run a command in a new container."""
254
+ verbosity = ctx.obj['verbosity']
255
+ cmd = ['docker', 'run']
256
+
257
+ if name:
258
+ cmd.extend(['--name', name])
259
+ verbosity.debug(f"Setting container name: {name}")
260
+
261
+ for p in port:
262
+ cmd.extend(['-p', p])
263
+ verbosity.debug(f"Adding port mapping: {p}")
264
+
265
+ if detach:
266
+ cmd.append('-d')
267
+ verbosity.debug("Running container in detached mode")
268
+
269
+ for e in env:
270
+ cmd.extend(['-e', e])
271
+ verbosity.debug(f"Setting environment variable: {e}")
272
+
273
+ for v in volume:
274
+ cmd.extend(['-v', v])
275
+ verbosity.debug(f"Mounting volume: {v}")
276
+
277
+ if image:
278
+ cmd.append(image)
279
+ verbosity.debug(f"Using image: {image}")
280
+
281
+ # Add any remaining arguments
282
+ if hasattr(ctx, 'args') and ctx.args:
283
+ cmd.extend(ctx.args)
284
+ verbosity.debug(f"Additional arguments: {' '.join(ctx.args)}")
285
+
286
+ try:
287
+ verbosity.debug(f"Running command: {' '.join(cmd)}")
288
+ result = subprocess.run(cmd, capture_output=True, text=True)
289
+
290
+ if result.returncode == 0:
291
+ if result.stdout:
292
+ click.echo(result.stdout.strip())
293
+ verbosity.info("Container started successfully")
294
+ else:
295
+ error_msg = f"Error: {result.stderr.strip() or 'Unknown error'}"
296
+ verbosity.error(error_msg)
297
+ click.echo(error_msg, err=True)
298
+ ctx.exit(1)
299
+
300
+ except Exception as e:
301
+ error_msg = f"Failed to run container: {str(e)}"
302
+ verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
303
+ click.echo(error_msg, err=True)
304
+ ctx.exit(1)
305
+
306
+ @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
307
+ @click.argument('containers', nargs=-1, required=False)
308
+ @click.option('--force', '-f', is_flag=True, help='Force the removal of a running container (uses SIGKILL)')
309
+ @click.option('--volumes', '-v', is_flag=True, help='Remove anonymous volumes associated with the container')
310
+ @click.pass_context
311
+ def rm(ctx, containers, force, volumes):
312
+ """Remove one or more containers."""
313
+ verbosity = ctx.obj['verbosity']
314
+ cmd = ['docker', 'rm']
315
+
316
+ if force:
317
+ cmd.append('-f')
318
+ verbosity.debug("Force removal enabled")
319
+ if volumes:
320
+ cmd.append('-v')
321
+ verbosity.debug("Volume removal enabled")
322
+
323
+ # Get containers from both the containers argument and any remaining args
324
+ all_containers = list(containers)
325
+ if hasattr(ctx, 'args') and ctx.args:
326
+ all_containers.extend(ctx.args)
327
+
328
+ if not all_containers:
329
+ error_msg = "Error: You must specify at least one container"
330
+ verbosity.error(error_msg)
331
+ click.echo(error_msg, err=True)
332
+ ctx.exit(1)
333
+
334
+ cmd.extend(all_containers)
335
+ verbosity.debug(f"Removing containers: {', '.join(all_containers)}")
336
+
337
+ try:
338
+ verbosity.debug(f"Running command: {' '.join(cmd)}")
339
+ result = subprocess.run(cmd, capture_output=True, text=True)
340
+
341
+ if result.returncode == 0:
342
+ if result.stdout.strip():
343
+ click.echo(result.stdout.strip())
344
+ verbosity.info(f"Successfully removed {len(all_containers)} container(s)")
345
+ else:
346
+ error_msg = f"Error: {result.stderr.strip() or 'Unknown error'}"
347
+ verbosity.error(error_msg)
348
+ click.echo(error_msg, err=True)
349
+ ctx.exit(1)
350
+
351
+ except Exception as e:
352
+ error_msg = f"Failed to remove containers: {str(e)}"
353
+ verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
354
+ click.echo(error_msg, err=True)
355
+ ctx.exit(1)
356
+
357
+ @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
358
+ @click.option('--show-all', '-a', is_flag=True, help='Show all containers (default shows just running)')
359
+ @click.option('--http-only', is_flag=True, help='Show only containers with HTTP/HTTPS ports')
360
+ @click.pass_context
361
+ def url(ctx, show_all, http_only):
362
+ """Show containers with their HTTP/HTTPS URLs."""
363
+ verbosity = ctx.obj['verbosity']
364
+ verbosity.info(f"Starting url command with show_all={show_all}, http_only={http_only}")
365
+
366
+ try:
367
+ # Get all containers
368
+ cmd = ['docker', 'ps', '--format', '{{.ID}}|{{.Names}}|{{.Status}}|{{.Ports}}']
369
+ if show_all:
370
+ cmd.append('-a')
371
+
372
+ verbosity.debug(f"Running command: {' '.join(cmd)}")
373
+ result = subprocess.run(cmd, capture_output=True, text=True)
374
+
375
+ if result.returncode != 0:
376
+ error_msg = f"Error listing containers: {result.stderr}"
377
+ verbosity.error(error_msg)
378
+ click.echo(error_msg, err=True)
379
+ return
380
+
381
+ verbosity.debug(f"Command output: {result.stdout}")
382
+
383
+ running_containers = []
384
+ stopped_containers = []
385
+
386
+ container_lines = result.stdout.strip().split('\n')
387
+ verbosity.info(f"Found {len(container_lines)} container(s)")
388
+
389
+ for line in container_lines:
390
+ if not line.strip():
391
+ verbosity.debug("Skipping empty line")
392
+ continue
393
+
394
+ try:
395
+ verbosity.debug(f"Processing container line: {line}")
396
+ container_id, name, status, ports = line.split('|', 3)
397
+ is_running = 'Up' in status
398
+ verbosity.info(f"Container: ID={container_id[:12]}, Name={name}, Status={status}, Running={is_running}")
399
+
400
+ # Get container details
401
+ container_info = {
402
+ 'id': container_id[:12], # Short ID
403
+ 'name': name,
404
+ 'status': status,
405
+ 'urls': []
406
+ }
407
+
408
+ # Get exposed ports and their mappings
409
+ port_mappings = get_container_ports(container_id, verbosity)
410
+ verbosity.debug(f"Found {len(port_mappings)} port mappings for {name}")
411
+
412
+ for port in port_mappings:
413
+ if not port.get('host_port') or not port.get('container_port'):
414
+ verbosity.debug(f"Skipping incomplete port mapping: {port}")
415
+ continue
416
+
417
+ verbosity.debug(f"Checking port mapping: {port}")
418
+ verbosity.debug(f"Container name: {name}, Port: {port['container_port']}")
419
+
420
+ # Handle different port string formats (e.g., '8069/tcp', '0.0.0.0:8080->80/tcp')
421
+ port_str = port['container_port']
422
+
423
+ # Extract port number and protocol
424
+ port_num = None
425
+ protocol = 'tcp' # default protocol
426
+
427
+ # Handle format like '8069/tcp' or '80/http'
428
+ if '/' in port_str:
429
+ port_num, protocol = port_str.split('/', 1)
430
+ # Handle format like '0.0.0.0:8080->80/tcp'
431
+ elif '->' in port_str:
432
+ _, port_mapping = port_str.split('->', 1)
433
+ if '/' in port_mapping:
434
+ port_num, protocol = port_mapping.split('/', 1)
435
+ else:
436
+ port_num = port_mapping
437
+ else:
438
+ port_num = port_str
439
+
440
+ # Clean up port number (remove any non-numeric characters)
441
+ port_num = ''.join(c for c in port_num if c.isdigit())
442
+
443
+ # Map all ports to HTTP URLs
444
+ if port_num: # Process all ports regardless of protocol
445
+ scheme = 'http'
446
+
447
+ # Handle IPv6 addresses (add brackets if needed)
448
+ host = port['host_ip']
449
+ if ':' in host and not host.startswith('['):
450
+ host = f'[{host}]'
451
+
452
+ url = f"{scheme}://{host}:{port['host_port']}"
453
+
454
+ container_info['urls'].append({
455
+ 'url': url,
456
+ 'port': port_num,
457
+ 'protocol': protocol
458
+ })
459
+ verbosity.info(f"Added URL for {name}: {url} (port {port_num}/{protocol})")
460
+ verbosity.info(f"Added URL for {name}: {url} (port {port_num})")
461
+
462
+ # If http_only is True and no HTTP URLs, skip this container
463
+ if http_only and not container_info['urls']:
464
+ continue
465
+
466
+ if is_running:
467
+ running_containers.append(container_info)
468
+ else:
469
+ stopped_containers.append(container_info)
470
+
471
+ except Exception as e:
472
+ click.echo(f"Error processing container info: {e}", err=True)
473
+ continue
474
+
475
+ # Display running containers
476
+ if running_containers:
477
+ click.secho("\n🚀 Running Containers:", fg='green', bold=True)
478
+ for container in running_containers:
479
+ verbosity.debug(f"Displaying running container: {container['name']}")
480
+ click.echo(f"\n{click.style('●', fg='green')} {click.style(container['name'], bold=True)} ({container['id']})")
481
+
482
+ if container['urls']:
483
+ verbosity.info(f"Found {len(container['urls'])} URLs for {container['name']}")
484
+ for url_info in container['urls']:
485
+ verbosity.debug(f"Displaying URL: {url_info['url']}")
486
+ click.echo(f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True)}")
487
+ else:
488
+ verbosity.debug(f"No URLs found for {container['name']}")
489
+
490
+ # Display stopped containers
491
+ if stopped_containers and (show_all or not http_only):
492
+ click.secho("\n⏸️ Stopped Containers:", fg='yellow', bold=True)
493
+ for container in stopped_containers:
494
+ verbosity.debug(f"Displaying stopped container: {container['name']}")
495
+ click.echo(f"\n{click.style('●', fg='yellow')} {click.style(container['name'], dim=True)} ({container['id']})")
496
+
497
+ if container['urls']:
498
+ verbosity.info(f"Found {len(container['urls'])} URLs for stopped container {container['name']}")
499
+ for url_info in container['urls']:
500
+ verbosity.debug(f"Displaying URL for stopped container: {url_info['url']}")
501
+ click.echo(f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True, dim=True)}")
502
+ else:
503
+ verbosity.debug(f"No URLs found for stopped container {container['name']}")
504
+
505
+ if not running_containers and not stopped_containers:
506
+ msg = "No containers found."
507
+ verbosity.info(msg)
508
+ click.echo(msg)
509
+ else:
510
+ verbosity.info(f"Displayed {len(running_containers)} running and {len(stopped_containers)} stopped containers")
511
+
512
+ except Exception as e:
513
+ error_msg = f"Error in url command: {str(e)}"
514
+ verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
515
+ click.echo(error_msg, err=True)
516
+
517
+ @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
518
+ @click.argument('image', required=False)
519
+ @click.option('--all-tags', '-a', is_flag=True, help='Remove all versions of the image with the given name')
520
+ @click.option('--force', '-f', is_flag=True, help='Force removal of the image')
521
+ @click.option('--no-prune', is_flag=True, help='Do not delete untagged parents')
522
+ @click.pass_context
523
+ def rmi(ctx, image, all_tags, force, no_prune):
524
+ """Remove one or more images."""
525
+ verbosity = ctx.obj['verbosity']
526
+ cmd = ['docker', 'rmi']
527
+
528
+ if force:
529
+ cmd.append('-f')
530
+ verbosity.debug("Force removal enabled")
531
+ if no_prune:
532
+ cmd.append('--no-prune')
533
+ verbosity.debug("Pruning of untagged parents disabled")
534
+
535
+ # Get images from both the image argument and any remaining args
536
+ images = []
537
+ if image:
538
+ images.append(image)
539
+ if hasattr(ctx, 'args') and ctx.args:
540
+ images.extend(ctx.args)
541
+
542
+ if not images and not all_tags:
543
+ error_msg = "Error: You must specify at least one image"
544
+ verbosity.error(error_msg)
545
+ click.echo(error_msg, err=True)
546
+ ctx.exit(1)
547
+
548
+ if all_tags:
549
+ if not images:
550
+ error_msg = "Error: You must specify an image name when using --all-tags"
551
+ verbosity.error(error_msg)
552
+ click.echo(error_msg, err=True)
553
+ ctx.exit(1)
554
+
555
+ # Get all tags for the specified images
556
+ all_tags_to_remove = []
557
+ for img in images:
558
+ verbosity.debug(f"Finding all tags for image: {img}")
559
+ try:
560
+ result = subprocess.run(
561
+ ['docker', 'images', '--format', '{{.Repository}}:{{.Tag}}', img],
562
+ capture_output=True,
563
+ text=True
564
+ )
565
+
566
+ if result.returncode == 0 and result.stdout.strip():
567
+ tags = [line for line in result.stdout.split('\n') if line]
568
+ verbosity.debug(f"Found {len(tags)} tags for {img}")
569
+ all_tags_to_remove.extend(tags)
570
+ else:
571
+ verbosity.warning(f"No images found matching '{img}'")
572
+
573
+ except Exception as e:
574
+ verbosity.error(f"Error finding tags for {img}: {str(e)}", exc_info=verbosity.verbosity >= 3)
575
+ continue
576
+
577
+ if not all_tags_to_remove:
578
+ error_msg = "No matching images found to remove"
579
+ verbosity.error(error_msg)
580
+ click.echo(error_msg, err=True)
581
+ ctx.exit(1)
582
+
583
+ cmd.extend(all_tags_to_remove)
584
+ verbosity.info(f"Removing {len(all_tags_to_remove)} image(s) with all tags")
585
+
586
+ else:
587
+ cmd.extend(images)
588
+ verbosity.info(f"Removing {len(images)} image(s)")
589
+
590
+ try:
591
+ verbosity.debug(f"Running command: {' '.join(cmd)}")
592
+ result = subprocess.run(cmd, capture_output=True, text=True)
593
+
594
+ if result.returncode == 0:
595
+ if result.stdout.strip():
596
+ click.echo(result.stdout.strip())
597
+ removed_count = len(cmd) - 2 # Subtract 'docker rmi' from the command
598
+ verbosity.info(f"Successfully removed {removed_count} image(s)")
599
+ else:
600
+ error_msg = f"Error: {result.stderr.strip() or 'Unknown error'}"
601
+ verbosity.error(error_msg)
602
+ click.echo(error_msg, err=True)
603
+ ctx.exit(1)
604
+
605
+ except Exception as e:
606
+ error_msg = f"Failed to remove images: {str(e)}"
607
+ verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
608
+ click.echo(error_msg, err=True)
609
+ ctx.exit(1)
@@ -0,0 +1,27 @@
1
+ import platform
2
+ import socket
3
+ import shutil
4
+ from ..utils import run_cmd
5
+ import click
6
+
7
+ def get_internal_ip():
8
+ """Get the internal IP address based on the operating system."""
9
+ system = platform.system()
10
+ if system == "Darwin":
11
+ cmd = "ipconfig getifaddr en0"
12
+ elif system == "Linux":
13
+ if shutil.which("ifconfig"):
14
+ cmd = "ifconfig | grep 'inet ' | grep -v 192.168.1.1 | awk '{print $2}' | head -n1"
15
+ else:
16
+ cmd = "hostname -I | awk '{print $1}'"
17
+ else:
18
+ ip = socket.gethostbyname(socket.gethostname())
19
+ print(f"$ python socket.gethostbyname(socket.gethostname())")
20
+ print(ip)
21
+ return
22
+ return run_cmd(cmd)
23
+
24
+ @click.command()
25
+ def internal_ip():
26
+ """Show local/internal IP"""
27
+ get_internal_ip()
@@ -0,0 +1,88 @@
1
+ import click
2
+ import platform
3
+ import subprocess
4
+
5
+ def check_nixos():
6
+ """Check if running on NixOS."""
7
+ try:
8
+ with open('/etc/os-release') as f:
9
+ return 'NixOS' in f.read()
10
+ except FileNotFoundError:
11
+ return False
12
+ except Exception as e:
13
+ click.echo(f"Warning: Could not check if running on NixOS: {e}", err=True)
14
+ return False
15
+
16
+ def get_nixos_version():
17
+ """Get NixOS version information."""
18
+ if not check_nixos():
19
+ return "Not running NixOS"
20
+
21
+ try:
22
+ result = subprocess.run(['nixos-version'],
23
+ capture_output=True,
24
+ text=True)
25
+ if result.returncode == 0:
26
+ return result.stdout.strip()
27
+ return "NixOS version could not be determined"
28
+ except Exception as e:
29
+ return f"Error getting NixOS version: {str(e)}"
30
+
31
+ @click.group()
32
+ def nixos():
33
+ """NixOS related commands."""
34
+ if not check_nixos():
35
+ click.echo("Warning: Not running on NixOS. Some commands may not work as expected.", err=True)
36
+
37
+ @nixos.command()
38
+ def version():
39
+ """Show NixOS version."""
40
+ click.echo(get_nixos_version())
41
+
42
+ @nixos.command()
43
+ @click.argument('package', required=False)
44
+ def search(package):
45
+ """Search for Nix packages."""
46
+ if not package:
47
+ click.echo("Please specify a package to search for")
48
+ return
49
+
50
+ try:
51
+ result = subprocess.run(['nix-env', '-qa', package],
52
+ capture_output=True,
53
+ text=True)
54
+ if result.returncode == 0:
55
+ click.echo(result.stdout)
56
+ else:
57
+ click.echo(f"Error searching for package: {result.stderr}", err=True)
58
+ except Exception as e:
59
+ click.echo(f"Error: {str(e)}", err=True)
60
+
61
+ @nixos.command()
62
+ @click.option('-f', '--force', is_flag=True, help='Force garbage collection and remove all old generations')
63
+ def clean(force):
64
+ """Clean Nix store and perform garbage collection."""
65
+ if not check_nixos():
66
+ click.echo("Error: This command can only be run on NixOS", err=True)
67
+ return
68
+
69
+ try:
70
+ click.echo("Running Nix garbage collection...")
71
+ if force:
72
+ click.echo("Forcing garbage collection and removing all old generations...")
73
+ # Remove all old generations of all profiles
74
+ subprocess.run(['nix-collect-garbage', '-d'], check=True)
75
+ click.echo("✓ Removed all old generations and ran garbage collection")
76
+ else:
77
+ # Regular garbage collection (safe, only removes unreachable paths)
78
+ subprocess.run(['nix-collect-garbage'], check=True)
79
+ click.echo("✓ Garbage collection completed")
80
+
81
+ # Show disk space usage after cleanup
82
+ click.echo("\nDisk space usage after cleanup:")
83
+ subprocess.run(['nix-store', '--query', '--disk-usage', '/nix/store'])
84
+
85
+ except subprocess.CalledProcessError as e:
86
+ click.echo(f"Error during cleanup: {e}", err=True)
87
+ except Exception as e:
88
+ click.echo(f"Unexpected error: {str(e)}", err=True)
@@ -0,0 +1,8 @@
1
+ from ..utils import run_cmd
2
+ import click
3
+
4
+ @click.command()
5
+ def public_ip():
6
+ """Show public IP"""
7
+ cmd = "curl -s ifconfig.me"
8
+ run_cmd(cmd)
helper/main.py CHANGED
@@ -1,63 +1,109 @@
1
1
  import click
2
- import platform
3
- import subprocess
4
- import socket
5
- import shutil
2
+ import logging
3
+ import sys
4
+ from .commands import internal_ip, public_ip, arch, nixos, docker
6
5
 
7
- def run_cmd(cmd):
8
- print(f"$ {cmd}")
9
- try:
10
- result = subprocess.check_output(cmd, shell=True, text=True).strip()
11
- print(result)
12
- except subprocess.CalledProcessError as e:
13
- print(f"Error: {e}")
14
-
15
- @click.group()
16
- def cli():
17
- """Helper CLI - quick system info"""
18
- pass
6
+ class VerbosityCommand(click.Command):
7
+ def parse_args(self, ctx, args):
8
+ # Initialize verbosity from context if it exists
9
+ ctx.ensure_object(dict)
10
+ verbose = ctx.obj.get('verbosity', 0)
11
+
12
+ # Process args for verbosity flags
13
+ new_args = []
14
+ i = 0
15
+ while i < len(args):
16
+ arg = args[i]
17
+ if arg == '--verbose':
18
+ verbose += 1
19
+ elif arg.startswith('-v'):
20
+ verbose += arg.count('v')
21
+ else:
22
+ new_args.append(arg)
23
+ i += 1
24
+
25
+ # Update verbosity in context
26
+ ctx.obj['verbosity'] = verbose
27
+
28
+ # Set up logging
29
+ self._setup_logging(verbose)
30
+
31
+ # Continue with normal argument parsing
32
+ return super().parse_args(ctx, new_args)
33
+
34
+ def _setup_logging(self, verbose):
35
+ logger = logging.getLogger('docker-helper')
36
+ if verbose >= 3:
37
+ logger.setLevel(logging.DEBUG)
38
+ elif verbose == 2:
39
+ logger.setLevel(logging.INFO)
40
+ elif verbose == 1:
41
+ logger.setLevel(logging.WARNING)
42
+ else:
43
+ logger.setLevel(logging.ERROR)
19
44
 
20
- @cli.command()
21
- def internal_ip():
22
- """Show local/internal IP"""
23
- system = platform.system()
24
- if system == "Darwin":
25
- cmd = "ipconfig getifaddr en0"
26
- elif system == "Linux":
27
- # Prefer ifconfig if available, else hostname -I
28
- if shutil.which("ifconfig"):
29
- cmd = "ifconfig | grep 'inet ' | grep -v 192.168.1.1 | awk '{print $2}' | head -n1"
45
+ class VerbosityGroup(click.Group):
46
+ def make_context(self, info_name, args, parent=None, **extra):
47
+ # Pre-process args to find verbosity flags
48
+ verbose = 0
49
+ processed_args = []
50
+
51
+ for arg in args:
52
+ if arg == '--verbose':
53
+ verbose += 1
54
+ elif arg.startswith('-v'):
55
+ verbose += arg.count('v')
56
+ else:
57
+ processed_args.append(arg)
58
+
59
+ # Create context with processed args
60
+ ctx = super().make_context(info_name, processed_args, parent=parent, **extra)
61
+
62
+ # Set verbosity in context
63
+ ctx.ensure_object(dict)
64
+ ctx.obj['verbosity'] = verbose
65
+
66
+ # Set up logging
67
+ logger = logging.getLogger('docker-helper')
68
+ if verbose >= 3:
69
+ logger.setLevel(logging.DEBUG)
70
+ elif verbose == 2:
71
+ logger.setLevel(logging.INFO)
72
+ elif verbose == 1:
73
+ logger.setLevel(logging.WARNING)
30
74
  else:
31
- cmd = "hostname -I | awk '{print $1}'"
32
- else:
33
- ip = socket.gethostbyname(socket.gethostname())
34
- print(f"$ python socket.gethostbyname(socket.gethostname())")
35
- print(ip)
36
- return
37
- run_cmd(cmd)
75
+ logger.setLevel(logging.ERROR)
76
+
77
+ return ctx
38
78
 
39
- @cli.command()
40
- def public_ip():
41
- """Show public IP"""
42
- cmd = "curl -s ifconfig.me"
43
- run_cmd(cmd)
79
+ @click.group(cls=VerbosityGroup)
80
+ def cli():
81
+ """Helper CLI - quick system info"""
82
+ # Set up basic logging
83
+ logging.basicConfig(
84
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
85
+ level=logging.ERROR
86
+ )
44
87
 
45
- @cli.command()
46
- def arch():
47
- """Show CPU architecture"""
48
- cmd = "uname -m"
49
- run_cmd(cmd)
88
+ # Register all commands
89
+ cli.add_command(internal_ip.internal_ip)
90
+ cli.add_command(public_ip.public_ip)
91
+ cli.add_command(arch.arch)
92
+ cli.add_command(nixos.nixos, name="nixos")
93
+ cli.add_command(docker.docker, name="docker")
50
94
 
51
95
  @cli.command()
52
96
  @click.pass_context
53
97
  def all(ctx):
54
98
  """Show all info"""
55
99
  click.echo("=== Internal IP ===")
56
- ctx.invoke(internal_ip)
100
+ ctx.invoke(internal_ip.internal_ip)
57
101
  click.echo("\n=== Public IP ===")
58
- ctx.invoke(public_ip)
102
+ ctx.invoke(public_ip.public_ip)
59
103
  click.echo("\n=== Architecture ===")
60
- ctx.invoke(arch)
104
+ ctx.invoke(arch.arch)
105
+ click.echo("\n=== NixOS ===")
106
+ ctx.invoke(nixos.nixos, 'version')
61
107
 
62
108
  if __name__ == "__main__":
63
109
  cli()
helper/utils.py ADDED
@@ -0,0 +1,12 @@
1
+ import subprocess
2
+
3
+ def run_cmd(cmd):
4
+ """Run a shell command and print the output."""
5
+ print(f"$ {cmd}")
6
+ try:
7
+ result = subprocess.check_output(cmd, shell=True, text=True).strip()
8
+ print(result)
9
+ return result
10
+ except subprocess.CalledProcessError as e:
11
+ print(f"Error: {e}")
12
+ return None
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: helper-cli
3
+ Version: 0.1.11
4
+ Summary: Simple system info CLI
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: click
@@ -0,0 +1,13 @@
1
+ helper/main.py,sha256=39UV2OxIOAgak7ALwGRdK2oeCaaMXQqvXgnrWU77kB4,3247
2
+ helper/utils.py,sha256=JynYacwFA9kgkDuWpGPCj3VxYl_yFJL-FRCKlW1g6Mo,337
3
+ helper/commands/__init__.py,sha256=HFk0MU1U4VqGJUkb9SvCoslqpYSah8KePP9tP8Xzzs4,30
4
+ helper/commands/arch.py,sha256=AREsblUki99P_wVpi44maQd7KA8IlUCJ6ilzJAQODDU,141
5
+ helper/commands/docker.py,sha256=ZmEk3B2xor0p3AQJPkVQLz0biFa1MSsoF-dRxOHDvsI,25349
6
+ helper/commands/internal_ip.py,sha256=ZN7c1HRgB5f-wRk9IwfGQeI3YuZqmPW-UflIpGEImt0,786
7
+ helper/commands/nixos.py,sha256=--Uz2lA-xw6-spp1WBjzzfu4-imFtcziyZuUHZk3Pxs,3113
8
+ helper/commands/public_ip.py,sha256=HS99RDYCaKDZ-AxMQhUwazgR-tT6IGlBb5Qn0f5ITPg,150
9
+ helper_cli-0.1.11.dist-info/METADATA,sha256=6AR44RTeCCTlJvglidOgz754evOnjEhUeWb7yUqvocY,131
10
+ helper_cli-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ helper_cli-0.1.11.dist-info/entry_points.txt,sha256=4EoFQ0yRogLTvStcbjlpkTqayWmiRqbMwkcDBcgQbK8,43
12
+ helper_cli-0.1.11.dist-info/top_level.txt,sha256=VM8lkErPJijbKhnfEGA_hE_YDXde4iizgqWKloZIxW8,7
13
+ helper_cli-0.1.11.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: helper-cli
3
- Version: 0.1.1
4
- Requires-Dist: click
@@ -1,6 +0,0 @@
1
- helper/main.py,sha256=TTB6NuLydg2EcMlV2NxyYwKLag6tGuQhxbwNBBiOanU,1520
2
- helper_cli-0.1.1.dist-info/METADATA,sha256=PBrA7-WIggiEiRpXGrCJNbCCXsKkWKQAyzoJlt1lxBw,75
3
- helper_cli-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- helper_cli-0.1.1.dist-info/entry_points.txt,sha256=4EoFQ0yRogLTvStcbjlpkTqayWmiRqbMwkcDBcgQbK8,43
5
- helper_cli-0.1.1.dist-info/top_level.txt,sha256=VM8lkErPJijbKhnfEGA_hE_YDXde4iizgqWKloZIxW8,7
6
- helper_cli-0.1.1.dist-info/RECORD,,