helper-cli 0.1.11__py3-none-any.whl → 0.1.16__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.
- helper/commands/docker.py +39 -35
- helper/commands/speed.py +39 -0
- helper/commands/system_info.py +276 -0
- helper/main.py +18 -3
- helper_cli-0.1.16.dist-info/METADATA +61 -0
- helper_cli-0.1.16.dist-info/RECORD +15 -0
- {helper_cli-0.1.11.dist-info → helper_cli-0.1.16.dist-info}/entry_points.txt +1 -0
- helper_cli-0.1.11.dist-info/METADATA +0 -6
- helper_cli-0.1.11.dist-info/RECORD +0 -13
- {helper_cli-0.1.11.dist-info → helper_cli-0.1.16.dist-info}/WHEEL +0 -0
- {helper_cli-0.1.11.dist-info → helper_cli-0.1.16.dist-info}/top_level.txt +0 -0
helper/commands/docker.py
CHANGED
|
@@ -147,15 +147,15 @@ def get_verbosity(ctx: click.Context) -> Verbosity:
|
|
|
147
147
|
def docker(ctx, verbose):
|
|
148
148
|
"""Docker management commands."""
|
|
149
149
|
ctx.ensure_object(dict)
|
|
150
|
-
|
|
150
|
+
|
|
151
151
|
# Get verbosity from parent context if it exists, otherwise use the flag value
|
|
152
152
|
parent_verbosity = ctx.obj.get('verbosity', 0) if hasattr(ctx, 'obj') else 0
|
|
153
153
|
verbosity_level = max(verbose, parent_verbosity)
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
# Initialize verbosity
|
|
156
156
|
verbosity = Verbosity(verbosity=verbosity_level)
|
|
157
157
|
ctx.obj['verbosity'] = verbosity
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
# Set up logging
|
|
160
160
|
logger = logging.getLogger('docker-helper')
|
|
161
161
|
if verbosity_level >= 3:
|
|
@@ -166,7 +166,7 @@ def docker(ctx, verbose):
|
|
|
166
166
|
logger.setLevel(logging.WARNING)
|
|
167
167
|
else:
|
|
168
168
|
logger.setLevel(logging.ERROR)
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
logger.debug(f"Docker command group initialized with verbosity level: {verbosity_level}")
|
|
171
171
|
|
|
172
172
|
verbosity.debug("Initializing Docker command group")
|
|
@@ -176,8 +176,10 @@ def docker(ctx, verbose):
|
|
|
176
176
|
|
|
177
177
|
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
178
178
|
@click.option('--all', '-a', is_flag=True, help='Show all containers (default shows just running)')
|
|
179
|
-
@click.option('--
|
|
179
|
+
@click.option('--all-containers', is_flag=True, help='Show all containers (default shows just running)')
|
|
180
|
+
@click.option('--format', '-f', type=click.Choice(['table', 'json'], case_sensitive=False),
|
|
180
181
|
default='table', help='Output format')
|
|
182
|
+
@click.help_option('--help', '-h')
|
|
181
183
|
@click.pass_context
|
|
182
184
|
def ps(ctx, all, format):
|
|
183
185
|
"""List containers."""
|
|
@@ -189,7 +191,7 @@ def ps(ctx, all, format):
|
|
|
189
191
|
try:
|
|
190
192
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
191
193
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
192
|
-
|
|
194
|
+
|
|
193
195
|
if result.returncode == 0:
|
|
194
196
|
if format == 'json':
|
|
195
197
|
# Try to parse and pretty-print JSON output
|
|
@@ -277,7 +279,7 @@ def run(ctx, image, name, port, detach, env, volume):
|
|
|
277
279
|
if image:
|
|
278
280
|
cmd.append(image)
|
|
279
281
|
verbosity.debug(f"Using image: {image}")
|
|
280
|
-
|
|
282
|
+
|
|
281
283
|
# Add any remaining arguments
|
|
282
284
|
if hasattr(ctx, 'args') and ctx.args:
|
|
283
285
|
cmd.extend(ctx.args)
|
|
@@ -286,7 +288,7 @@ def run(ctx, image, name, port, detach, env, volume):
|
|
|
286
288
|
try:
|
|
287
289
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
288
290
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
289
|
-
|
|
291
|
+
|
|
290
292
|
if result.returncode == 0:
|
|
291
293
|
if result.stdout:
|
|
292
294
|
click.echo(result.stdout.strip())
|
|
@@ -296,12 +298,12 @@ def run(ctx, image, name, port, detach, env, volume):
|
|
|
296
298
|
verbosity.error(error_msg)
|
|
297
299
|
click.echo(error_msg, err=True)
|
|
298
300
|
ctx.exit(1)
|
|
299
|
-
|
|
301
|
+
|
|
300
302
|
except Exception as e:
|
|
301
303
|
error_msg = f"Failed to run container: {str(e)}"
|
|
302
304
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
303
305
|
click.echo(error_msg, err=True)
|
|
304
|
-
ctx.exit(1)
|
|
306
|
+
ctx.exit(1)
|
|
305
307
|
|
|
306
308
|
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
307
309
|
@click.argument('containers', nargs=-1, required=False)
|
|
@@ -324,7 +326,7 @@ def rm(ctx, containers, force, volumes):
|
|
|
324
326
|
all_containers = list(containers)
|
|
325
327
|
if hasattr(ctx, 'args') and ctx.args:
|
|
326
328
|
all_containers.extend(ctx.args)
|
|
327
|
-
|
|
329
|
+
|
|
328
330
|
if not all_containers:
|
|
329
331
|
error_msg = "Error: You must specify at least one container"
|
|
330
332
|
verbosity.error(error_msg)
|
|
@@ -337,7 +339,7 @@ def rm(ctx, containers, force, volumes):
|
|
|
337
339
|
try:
|
|
338
340
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
339
341
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
340
|
-
|
|
342
|
+
|
|
341
343
|
if result.returncode == 0:
|
|
342
344
|
if result.stdout.strip():
|
|
343
345
|
click.echo(result.stdout.strip())
|
|
@@ -347,16 +349,16 @@ def rm(ctx, containers, force, volumes):
|
|
|
347
349
|
verbosity.error(error_msg)
|
|
348
350
|
click.echo(error_msg, err=True)
|
|
349
351
|
ctx.exit(1)
|
|
350
|
-
|
|
352
|
+
|
|
351
353
|
except Exception as e:
|
|
352
354
|
error_msg = f"Failed to remove containers: {str(e)}"
|
|
353
355
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
354
356
|
click.echo(error_msg, err=True)
|
|
355
|
-
ctx.exit(1)
|
|
357
|
+
ctx.exit(1)
|
|
356
358
|
|
|
357
359
|
@docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
358
360
|
@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')
|
|
361
|
+
@click.option('--http-only', '-h', is_flag=True, help='Show only containers with HTTP/HTTPS ports')
|
|
360
362
|
@click.pass_context
|
|
361
363
|
def url(ctx, show_all, http_only):
|
|
362
364
|
"""Show containers with their HTTP/HTTPS URLs."""
|
|
@@ -413,17 +415,17 @@ def url(ctx, show_all, http_only):
|
|
|
413
415
|
if not port.get('host_port') or not port.get('container_port'):
|
|
414
416
|
verbosity.debug(f"Skipping incomplete port mapping: {port}")
|
|
415
417
|
continue
|
|
416
|
-
|
|
418
|
+
|
|
417
419
|
verbosity.debug(f"Checking port mapping: {port}")
|
|
418
420
|
verbosity.debug(f"Container name: {name}, Port: {port['container_port']}")
|
|
419
|
-
|
|
421
|
+
|
|
420
422
|
# Handle different port string formats (e.g., '8069/tcp', '0.0.0.0:8080->80/tcp')
|
|
421
423
|
port_str = port['container_port']
|
|
422
|
-
|
|
424
|
+
|
|
423
425
|
# Extract port number and protocol
|
|
424
426
|
port_num = None
|
|
425
427
|
protocol = 'tcp' # default protocol
|
|
426
|
-
|
|
428
|
+
|
|
427
429
|
# Handle format like '8069/tcp' or '80/http'
|
|
428
430
|
if '/' in port_str:
|
|
429
431
|
port_num, protocol = port_str.split('/', 1)
|
|
@@ -436,21 +438,23 @@ def url(ctx, show_all, http_only):
|
|
|
436
438
|
port_num = port_mapping
|
|
437
439
|
else:
|
|
438
440
|
port_num = port_str
|
|
439
|
-
|
|
441
|
+
|
|
440
442
|
# Clean up port number (remove any non-numeric characters)
|
|
441
443
|
port_num = ''.join(c for c in port_num if c.isdigit())
|
|
442
|
-
|
|
444
|
+
|
|
443
445
|
# Map all ports to HTTP URLs
|
|
444
446
|
if port_num: # Process all ports regardless of protocol
|
|
445
447
|
scheme = 'http'
|
|
446
|
-
|
|
448
|
+
|
|
447
449
|
# Handle IPv6 addresses (add brackets if needed)
|
|
448
450
|
host = port['host_ip']
|
|
449
451
|
if ':' in host and not host.startswith('['):
|
|
450
452
|
host = f'[{host}]'
|
|
451
|
-
|
|
453
|
+
verbosity.info(f"Skipping non-HTTP port: {port['container_port']}")
|
|
454
|
+
continue
|
|
455
|
+
|
|
452
456
|
url = f"{scheme}://{host}:{port['host_port']}"
|
|
453
|
-
|
|
457
|
+
|
|
454
458
|
container_info['urls'].append({
|
|
455
459
|
'url': url,
|
|
456
460
|
'port': port_num,
|
|
@@ -538,7 +542,7 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
538
542
|
images.append(image)
|
|
539
543
|
if hasattr(ctx, 'args') and ctx.args:
|
|
540
544
|
images.extend(ctx.args)
|
|
541
|
-
|
|
545
|
+
|
|
542
546
|
if not images and not all_tags:
|
|
543
547
|
error_msg = "Error: You must specify at least one image"
|
|
544
548
|
verbosity.error(error_msg)
|
|
@@ -551,7 +555,7 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
551
555
|
verbosity.error(error_msg)
|
|
552
556
|
click.echo(error_msg, err=True)
|
|
553
557
|
ctx.exit(1)
|
|
554
|
-
|
|
558
|
+
|
|
555
559
|
# Get all tags for the specified images
|
|
556
560
|
all_tags_to_remove = []
|
|
557
561
|
for img in images:
|
|
@@ -559,30 +563,30 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
559
563
|
try:
|
|
560
564
|
result = subprocess.run(
|
|
561
565
|
['docker', 'images', '--format', '{{.Repository}}:{{.Tag}}', img],
|
|
562
|
-
capture_output=True,
|
|
566
|
+
capture_output=True,
|
|
563
567
|
text=True
|
|
564
568
|
)
|
|
565
|
-
|
|
569
|
+
|
|
566
570
|
if result.returncode == 0 and result.stdout.strip():
|
|
567
571
|
tags = [line for line in result.stdout.split('\n') if line]
|
|
568
572
|
verbosity.debug(f"Found {len(tags)} tags for {img}")
|
|
569
573
|
all_tags_to_remove.extend(tags)
|
|
570
574
|
else:
|
|
571
575
|
verbosity.warning(f"No images found matching '{img}'")
|
|
572
|
-
|
|
576
|
+
|
|
573
577
|
except Exception as e:
|
|
574
578
|
verbosity.error(f"Error finding tags for {img}: {str(e)}", exc_info=verbosity.verbosity >= 3)
|
|
575
579
|
continue
|
|
576
|
-
|
|
580
|
+
|
|
577
581
|
if not all_tags_to_remove:
|
|
578
582
|
error_msg = "No matching images found to remove"
|
|
579
583
|
verbosity.error(error_msg)
|
|
580
584
|
click.echo(error_msg, err=True)
|
|
581
585
|
ctx.exit(1)
|
|
582
|
-
|
|
586
|
+
|
|
583
587
|
cmd.extend(all_tags_to_remove)
|
|
584
588
|
verbosity.info(f"Removing {len(all_tags_to_remove)} image(s) with all tags")
|
|
585
|
-
|
|
589
|
+
|
|
586
590
|
else:
|
|
587
591
|
cmd.extend(images)
|
|
588
592
|
verbosity.info(f"Removing {len(images)} image(s)")
|
|
@@ -590,7 +594,7 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
590
594
|
try:
|
|
591
595
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
592
596
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
593
|
-
|
|
597
|
+
|
|
594
598
|
if result.returncode == 0:
|
|
595
599
|
if result.stdout.strip():
|
|
596
600
|
click.echo(result.stdout.strip())
|
|
@@ -601,9 +605,9 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
601
605
|
verbosity.error(error_msg)
|
|
602
606
|
click.echo(error_msg, err=True)
|
|
603
607
|
ctx.exit(1)
|
|
604
|
-
|
|
608
|
+
|
|
605
609
|
except Exception as e:
|
|
606
610
|
error_msg = f"Failed to remove images: {str(e)}"
|
|
607
611
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
608
612
|
click.echo(error_msg, err=True)
|
|
609
|
-
ctx.exit(1)
|
|
613
|
+
ctx.exit(1)
|
helper/commands/speed.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import subprocess
|
|
3
|
+
from ..utils import run_cmd
|
|
4
|
+
|
|
5
|
+
def check_speedtest_installed():
|
|
6
|
+
"""Check if speedtest-cli is installed."""
|
|
7
|
+
try:
|
|
8
|
+
subprocess.run(
|
|
9
|
+
["which", "speedtest-cli"],
|
|
10
|
+
check=True,
|
|
11
|
+
stdout=subprocess.PIPE,
|
|
12
|
+
stderr=subprocess.PIPE,
|
|
13
|
+
)
|
|
14
|
+
return True
|
|
15
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
@click.command()
|
|
19
|
+
@click.option(
|
|
20
|
+
"--simple", "-s",
|
|
21
|
+
is_flag=True,
|
|
22
|
+
help="Only show basic speed information"
|
|
23
|
+
)
|
|
24
|
+
def speed(simple):
|
|
25
|
+
"""Test internet speed using speedtest-cli"""
|
|
26
|
+
if not check_speedtest_installed():
|
|
27
|
+
click.echo("Error: speedtest-cli is not installed. Please install it first.")
|
|
28
|
+
click.echo("You can install it with: pip install speedtest-cli")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
cmd = "speedtest-cli"
|
|
32
|
+
if simple:
|
|
33
|
+
cmd += " --simple"
|
|
34
|
+
|
|
35
|
+
run_cmd(cmd)
|
|
36
|
+
|
|
37
|
+
# Add aliases for the command
|
|
38
|
+
speed_test = speed
|
|
39
|
+
sp = speed
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import platform
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
def run_command(cmd):
|
|
8
|
+
"""Run a shell command and return its output"""
|
|
9
|
+
try:
|
|
10
|
+
result = subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.STDOUT).strip()
|
|
11
|
+
return result if result else "N/A"
|
|
12
|
+
except subprocess.CalledProcessError as e:
|
|
13
|
+
return f"Error: {e.output}" if e.output else "Command failed"
|
|
14
|
+
|
|
15
|
+
def format_bytes(size_bytes):
|
|
16
|
+
"""Convert bytes to human readable format"""
|
|
17
|
+
if not isinstance(size_bytes, (int, float)):
|
|
18
|
+
try:
|
|
19
|
+
size_bytes = float(size_bytes)
|
|
20
|
+
except (ValueError, TypeError):
|
|
21
|
+
return str(size_bytes)
|
|
22
|
+
|
|
23
|
+
for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']:
|
|
24
|
+
if size_bytes < 1024.0 or unit == 'PB':
|
|
25
|
+
return f"{size_bytes:.1f} {unit}"
|
|
26
|
+
size_bytes /= 1024.0
|
|
27
|
+
return f"{size_bytes:.1f} PB"
|
|
28
|
+
|
|
29
|
+
def parse_vm_stat(output):
|
|
30
|
+
"""Parse macOS vm_stat output and return human readable memory info"""
|
|
31
|
+
if not output or 'Mach Virtual Memory Statistics' not in output:
|
|
32
|
+
return "Memory information not available"
|
|
33
|
+
|
|
34
|
+
# Get physical memory using sysctl
|
|
35
|
+
try:
|
|
36
|
+
total_memory = int(run_command('sysctl -n hw.memsize'))
|
|
37
|
+
except (ValueError, subprocess.CalledProcessError):
|
|
38
|
+
return "Error: Could not determine total physical memory"
|
|
39
|
+
|
|
40
|
+
lines = output.split('\n')
|
|
41
|
+
mem_info = {}
|
|
42
|
+
|
|
43
|
+
for line in lines[1:]: # Skip header
|
|
44
|
+
if ':' in line:
|
|
45
|
+
key, value = line.split(':', 1)
|
|
46
|
+
key = key.strip().strip('.')
|
|
47
|
+
try:
|
|
48
|
+
# Convert pages to bytes (1 page = 4KB on macOS)
|
|
49
|
+
pages = int(value.strip().strip('.'))
|
|
50
|
+
mem_info[key] = pages * 4096 # 4KB per page
|
|
51
|
+
except (ValueError, AttributeError):
|
|
52
|
+
mem_info[key] = value.strip()
|
|
53
|
+
|
|
54
|
+
# Calculate memory usage
|
|
55
|
+
wired_memory = mem_info.get('Pages wired down', 0)
|
|
56
|
+
active_memory = mem_info.get('Pages active', 0)
|
|
57
|
+
inactive_memory = mem_info.get('Pages inactive', 0)
|
|
58
|
+
free_memory = mem_info.get('Pages free', 0)
|
|
59
|
+
|
|
60
|
+
# Calculate used memory (wired + active)
|
|
61
|
+
used_memory = wired_memory + active_memory
|
|
62
|
+
|
|
63
|
+
# Calculate app memory (active + inactive)
|
|
64
|
+
app_memory = active_memory + inactive_memory
|
|
65
|
+
|
|
66
|
+
# Calculate cached files (inactive memory can be purged by the OS)
|
|
67
|
+
cached_files = inactive_memory
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
f"Total: {format_bytes(total_memory)}\n"
|
|
71
|
+
f"Used: {format_bytes(used_memory)} (Apps: {format_bytes(app_memory)}, Wired: {format_bytes(wired_memory)})\n"
|
|
72
|
+
f"Free: {format_bytes(free_memory)}\n"
|
|
73
|
+
f"Cached: {format_bytes(cached_files)}\n"
|
|
74
|
+
f"Usage: {used_memory/total_memory*100:.1f}%"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def parse_df_output(output):
|
|
78
|
+
"""Parse df output and format sizes"""
|
|
79
|
+
if not output:
|
|
80
|
+
return "Disk information not available"
|
|
81
|
+
|
|
82
|
+
lines = output.split('\n')
|
|
83
|
+
if not lines:
|
|
84
|
+
return output
|
|
85
|
+
|
|
86
|
+
# Keep the header
|
|
87
|
+
result = [lines[0]]
|
|
88
|
+
|
|
89
|
+
# Process each line
|
|
90
|
+
for line in lines[1:]:
|
|
91
|
+
if not line.strip():
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
parts = line.split()
|
|
95
|
+
if len(parts) >= 5:
|
|
96
|
+
# Format size columns (assuming standard df -h output)
|
|
97
|
+
parts[1] = format_bytes(parts[1].upper().replace('G', 'GB').replace('M', 'MB')
|
|
98
|
+
.replace('K', 'KB').replace('B', '').replace('I', '').strip())
|
|
99
|
+
parts[2] = format_bytes(parts[2].upper().replace('G', 'GB').replace('M', 'MB')
|
|
100
|
+
.replace('K', 'KB').replace('B', '').replace('I', '').strip())
|
|
101
|
+
parts[3] = format_bytes(parts[3].upper().replace('G', 'GB').replace('M', 'MB')
|
|
102
|
+
.replace('K', 'KB').replace('B', '').replace('I', '').strip())
|
|
103
|
+
|
|
104
|
+
# Reconstruct the line with formatted sizes
|
|
105
|
+
result.append(' '.join(parts))
|
|
106
|
+
else:
|
|
107
|
+
result.append(line)
|
|
108
|
+
|
|
109
|
+
return '\n'.join(result)
|
|
110
|
+
|
|
111
|
+
def get_os_specific_info():
|
|
112
|
+
"""Get OS-specific system information"""
|
|
113
|
+
system = platform.system().lower()
|
|
114
|
+
|
|
115
|
+
if system == 'darwin': # macOS
|
|
116
|
+
return {
|
|
117
|
+
'cpu': 'sysctl -n machdep.cpu.brand_string',
|
|
118
|
+
'cpu_cores': 'sysctl -n hw.ncpu',
|
|
119
|
+
'memory': 'vm_stat',
|
|
120
|
+
'disks': 'df -h',
|
|
121
|
+
'os_version': 'sw_vers',
|
|
122
|
+
'hostname': 'hostname',
|
|
123
|
+
'uptime': 'uptime'
|
|
124
|
+
}
|
|
125
|
+
elif system == 'linux':
|
|
126
|
+
return {
|
|
127
|
+
'cpu': 'cat /proc/cpuinfo | grep "model name" | head -n 1 | cut -d":" -f2',
|
|
128
|
+
'cpu_cores': 'nproc',
|
|
129
|
+
'memory': 'free -h',
|
|
130
|
+
'disks': 'df -h',
|
|
131
|
+
'os_version': 'cat /etc/os-release',
|
|
132
|
+
'hostname': 'hostname',
|
|
133
|
+
'uptime': 'uptime'
|
|
134
|
+
}
|
|
135
|
+
elif system == 'windows':
|
|
136
|
+
return {
|
|
137
|
+
'cpu': 'wmic cpu get name',
|
|
138
|
+
'cpu_cores': 'wmic cpu get NumberOfCores',
|
|
139
|
+
'memory': 'wmic OS get TotalVisibleMemorySize,FreePhysicalMemory /Value',
|
|
140
|
+
'disks': 'wmic logicaldisk get size,freespace,caption',
|
|
141
|
+
'os_version': 'systeminfo | findstr /B /C:"OS Name" /C:"OS Version"',
|
|
142
|
+
'hostname': 'hostname',
|
|
143
|
+
'uptime': 'wmic os get lastbootuptime'
|
|
144
|
+
}
|
|
145
|
+
else:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def format_uptime(uptime_str, system):
|
|
149
|
+
"""Format uptime string based on OS"""
|
|
150
|
+
if system == 'darwin' or system == 'linux':
|
|
151
|
+
# Example: ' 8:53 up 1 day, 3:45, 2 users, load averages: 2.22 2.41 2.35'
|
|
152
|
+
parts = uptime_str.split(',')
|
|
153
|
+
if 'up' in parts[0]:
|
|
154
|
+
return 'Uptime: ' + parts[0].split('up', 1)[1].strip()
|
|
155
|
+
return uptime_str
|
|
156
|
+
|
|
157
|
+
@click.command()
|
|
158
|
+
def system_info():
|
|
159
|
+
"""Display system information including CPU, RAM, and disk usage"""
|
|
160
|
+
system = platform.system().lower()
|
|
161
|
+
commands = get_os_specific_info()
|
|
162
|
+
|
|
163
|
+
if not commands:
|
|
164
|
+
click.echo("Unsupported operating system")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# System Information
|
|
168
|
+
click.echo("="*40 + " System Information " + "="*40)
|
|
169
|
+
click.echo(f"System: {platform.system()} {platform.release()}")
|
|
170
|
+
click.echo(f"Node Name: {run_command(commands['hostname'])}")
|
|
171
|
+
click.echo(f"Machine: {platform.machine()}")
|
|
172
|
+
click.echo(f"Processor: {platform.processor() or run_command(commands['cpu']).strip()}")
|
|
173
|
+
|
|
174
|
+
# OS Version
|
|
175
|
+
click.echo("\n" + "="*40 + " OS Version " + "="*40)
|
|
176
|
+
click.echo(run_command(commands['os_version']))
|
|
177
|
+
|
|
178
|
+
# Uptime
|
|
179
|
+
click.echo("\n" + "="*40 + " Uptime " + "="*40)
|
|
180
|
+
uptime = run_command(commands['uptime'])
|
|
181
|
+
click.echo(format_uptime(uptime, system))
|
|
182
|
+
|
|
183
|
+
# CPU Information
|
|
184
|
+
click.echo("\n" + "="*40 + " CPU Info " + "="*40)
|
|
185
|
+
cpu_cores = run_command(commands['cpu_cores']).strip()
|
|
186
|
+
click.echo(f"CPU Cores: {cpu_cores}")
|
|
187
|
+
|
|
188
|
+
if system == 'darwin' or system == 'linux':
|
|
189
|
+
cpu_info = run_command(commands['cpu']).strip()
|
|
190
|
+
click.echo(f"CPU: {cpu_info}")
|
|
191
|
+
|
|
192
|
+
# CPU Usage (simplified for cross-platform)
|
|
193
|
+
if system == 'darwin':
|
|
194
|
+
load_avg = run_command('sysctl -n vm.loadavg').strip()
|
|
195
|
+
click.echo(f"Load Average: {load_avg}")
|
|
196
|
+
elif system == 'linux':
|
|
197
|
+
load_avg = run_command('cat /proc/loadavg').strip()
|
|
198
|
+
click.echo(f"Load Average: {load_avg}")
|
|
199
|
+
|
|
200
|
+
# Memory Information
|
|
201
|
+
click.echo("\n" + "="*40 + " Memory Information " + "="*40)
|
|
202
|
+
if system == 'darwin':
|
|
203
|
+
mem_info = run_command('vm_stat')
|
|
204
|
+
click.echo(parse_vm_stat(mem_info))
|
|
205
|
+
elif system == 'linux':
|
|
206
|
+
mem_info = run_command('free -b') # Get bytes for consistent formatting
|
|
207
|
+
lines = mem_info.split('\n')
|
|
208
|
+
if len(lines) > 1:
|
|
209
|
+
headers = lines[0].split()
|
|
210
|
+
values = lines[1].split()
|
|
211
|
+
if len(values) >= 7: # For Mem: line
|
|
212
|
+
total = int(values[1])
|
|
213
|
+
used = int(values[2])
|
|
214
|
+
free = int(values[3])
|
|
215
|
+
click.echo(
|
|
216
|
+
f"Total: {format_bytes(total)}\n"
|
|
217
|
+
f"Used: {format_bytes(used)}\n"
|
|
218
|
+
f"Free: {format_bytes(free)}\n"
|
|
219
|
+
f"Usage: {used/total*100:.1f}%"
|
|
220
|
+
)
|
|
221
|
+
elif system == 'windows':
|
|
222
|
+
mem_info = run_command('wmic OS get TotalVisibleMemorySize,FreePhysicalMemory /Value')
|
|
223
|
+
if 'TotalVisibleMemorySize' in mem_info and 'FreePhysicalMemory' in mem_info:
|
|
224
|
+
try:
|
|
225
|
+
total = int(mem_info.split('TotalVisibleMemorySize=')[1].split('\n')[0]) * 1024 # KB to bytes
|
|
226
|
+
free = int(mem_info.split('FreePhysicalMemory=')[1].split('\n')[0]) * 1024 # KB to bytes
|
|
227
|
+
used = total - free
|
|
228
|
+
click.echo(
|
|
229
|
+
f"Total: {format_bytes(total)}\n"
|
|
230
|
+
f"Used: {format_bytes(used)}\n"
|
|
231
|
+
f"Free: {format_bytes(free)}\n"
|
|
232
|
+
f"Usage: {used/total*100:.1f}%"
|
|
233
|
+
)
|
|
234
|
+
except (IndexError, ValueError):
|
|
235
|
+
click.echo(mem_info)
|
|
236
|
+
else:
|
|
237
|
+
click.echo(mem_info)
|
|
238
|
+
|
|
239
|
+
# Disk Information
|
|
240
|
+
click.echo("\n" + "="*40 + " Disk Information " + "="*40)
|
|
241
|
+
if system == 'windows':
|
|
242
|
+
disks = run_command('wmic logicaldisk get size,freespace,caption')
|
|
243
|
+
if 'Caption' in disks:
|
|
244
|
+
lines = disks.split('\n')
|
|
245
|
+
result = ["{:<5} {:<15} {:<15} {:<15} {:<10}".format(
|
|
246
|
+
"Drive", "Total Space", "Free Space", "Used Space", "% Used")]
|
|
247
|
+
for line in lines[1:]:
|
|
248
|
+
parts = line.strip().split()
|
|
249
|
+
if len(parts) >= 3:
|
|
250
|
+
drive = parts[0]
|
|
251
|
+
try:
|
|
252
|
+
size = int(parts[1])
|
|
253
|
+
free = int(parts[2])
|
|
254
|
+
used = size - free
|
|
255
|
+
pct_used = (used / size) * 100 if size > 0 else 0
|
|
256
|
+
result.append(
|
|
257
|
+
"{:<5} {:<15} {:<15} {:<15} {:.1f}%".format(
|
|
258
|
+
drive,
|
|
259
|
+
format_bytes(size),
|
|
260
|
+
format_bytes(free),
|
|
261
|
+
format_bytes(used),
|
|
262
|
+
pct_used
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
except (ValueError, IndexError):
|
|
266
|
+
continue
|
|
267
|
+
click.echo('\n'.join(result))
|
|
268
|
+
else:
|
|
269
|
+
click.echo(disks)
|
|
270
|
+
else:
|
|
271
|
+
disks = run_command('df -h' if system != 'windows' else 'wmic logicaldisk get size,freespace,caption')
|
|
272
|
+
click.echo(parse_df_output(disks))
|
|
273
|
+
|
|
274
|
+
# Add aliases for the command
|
|
275
|
+
sysinfo = system_info
|
|
276
|
+
si = system_info
|
helper/main.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import click
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
|
-
|
|
4
|
+
import subprocess
|
|
5
|
+
from .commands import internal_ip, public_ip, arch, nixos, docker, speed, system_info
|
|
5
6
|
|
|
6
7
|
class VerbosityCommand(click.Command):
|
|
7
8
|
def parse_args(self, ctx, args):
|
|
@@ -76,9 +77,16 @@ class VerbosityGroup(click.Group):
|
|
|
76
77
|
|
|
77
78
|
return ctx
|
|
78
79
|
|
|
79
|
-
@click.group(cls=VerbosityGroup
|
|
80
|
+
@click.group(cls=VerbosityGroup, context_settings={
|
|
81
|
+
'help_option_names': ['-h', '--help'],
|
|
82
|
+
'token_normalize_func': lambda x: 'helper' if x == 'h' else x
|
|
83
|
+
})
|
|
80
84
|
def cli():
|
|
81
|
-
"""Helper CLI - quick system info
|
|
85
|
+
"""Helper CLI - quick system info
|
|
86
|
+
|
|
87
|
+
You can use 'h' as a shortcut for 'helper' command.
|
|
88
|
+
Example: h docker ps
|
|
89
|
+
"""
|
|
82
90
|
# Set up basic logging
|
|
83
91
|
logging.basicConfig(
|
|
84
92
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
@@ -91,6 +99,11 @@ cli.add_command(public_ip.public_ip)
|
|
|
91
99
|
cli.add_command(arch.arch)
|
|
92
100
|
cli.add_command(nixos.nixos, name="nixos")
|
|
93
101
|
cli.add_command(docker.docker, name="docker")
|
|
102
|
+
cli.add_command(speed.speed, name="speed")
|
|
103
|
+
cli.add_command(speed.speed, name="sp")
|
|
104
|
+
cli.add_command(system_info.system_info, name="system-info")
|
|
105
|
+
cli.add_command(system_info.system_info, name="sysinfo")
|
|
106
|
+
cli.add_command(system_info.system_info, name="si")
|
|
94
107
|
|
|
95
108
|
@cli.command()
|
|
96
109
|
@click.pass_context
|
|
@@ -104,6 +117,8 @@ def all(ctx):
|
|
|
104
117
|
ctx.invoke(arch.arch)
|
|
105
118
|
click.echo("\n=== NixOS ===")
|
|
106
119
|
ctx.invoke(nixos.nixos, 'version')
|
|
120
|
+
click.echo("\n=== System Info ===")
|
|
121
|
+
ctx.invoke(system_info.system_info)
|
|
107
122
|
|
|
108
123
|
if __name__ == "__main__":
|
|
109
124
|
cli()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: helper-cli
|
|
3
|
+
Version: 0.1.16
|
|
4
|
+
Summary: Simple system info CLI
|
|
5
|
+
Author-email: Huy Nguyen <huy.ntq02@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: click>=8.0.0
|
|
18
|
+
|
|
19
|
+
# 📦 Helper CLI
|
|
20
|
+
|
|
21
|
+
A simple command-line tool to show system info (IP, CPU arch, etc.) and the commands used to get them.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
### 🚀 Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install helper
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### 🧠 Usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
helper [command]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Available commands:
|
|
40
|
+
|
|
41
|
+
| Command | Description | Example |
|
|
42
|
+
| ------------- | --------------------------------- | -------------------- |
|
|
43
|
+
| `internal-ip` | Show internal IP | `helper internal-ip` |
|
|
44
|
+
| `public-ip` | Show public IP | `helper public-ip` |
|
|
45
|
+
| `cpu-arch` | Show CPU architecture (arm / amd) | `helper cpu-arch` |
|
|
46
|
+
| `all` | Show all info | `helper all` |
|
|
47
|
+
|
|
48
|
+
Each command also prints the shell command it runs to get the result.
|
|
49
|
+
|
|
50
|
+
### 🔄 Upgrade
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install --upgrade helper
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### 🧑💻 Contribute
|
|
59
|
+
|
|
60
|
+
Pull requests are welcome!
|
|
61
|
+
Repo: [https://github.com/nguyenhuy158/helper](https://github.com/nguyenhuy158/helper)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
helper/main.py,sha256=iLoQRR_nTgb9Uv_GqebSQPJluvo7PRwBuXGCD3T5JIs,3840
|
|
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=ktO6v5vEb8glhEZQl5ijMsQ4Kl6UL_FONNK4iCOvwgg,25250
|
|
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/commands/speed.py,sha256=AZ6RoPSi3Al4Z1RsMDIm9pjxYxXXs01qn6Fr7TEW8SM,974
|
|
10
|
+
helper/commands/system_info.py,sha256=QknKrFlknpvAWjHkOoKJkIMQp0S6FT_wecxHgx7Unqs,10716
|
|
11
|
+
helper_cli-0.1.16.dist-info/METADATA,sha256=oY4-b2_Z8Jmgzu0TgUEVFeIt0DsusNdCnCbWYc9LAZg,1619
|
|
12
|
+
helper_cli-0.1.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
helper_cli-0.1.16.dist-info/entry_points.txt,sha256=SeXS_UhbMNq2biiMGgbcJcJfqkhTIDmQ4UID3ZZgj0c,63
|
|
14
|
+
helper_cli-0.1.16.dist-info/top_level.txt,sha256=VM8lkErPJijbKhnfEGA_hE_YDXde4iizgqWKloZIxW8,7
|
|
15
|
+
helper_cli-0.1.16.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|