wasm-cli 0.13.16__py3-none-any.whl → 0.14.0__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.
Files changed (43) hide show
  1. wasm/__init__.py +1 -1
  2. wasm/cli/commands/config.py +96 -0
  3. wasm/cli/commands/db.py +3 -2
  4. wasm/cli/commands/health.py +231 -0
  5. wasm/cli/commands/monitor.py +8 -3
  6. wasm/cli/commands/setup.py +51 -16
  7. wasm/cli/commands/webapp.py +155 -34
  8. wasm/cli/interactive.py +3 -2
  9. wasm/cli/parser.py +94 -18
  10. wasm/completions/_wasm +438 -25
  11. wasm/completions/wasm.bash +477 -20
  12. wasm/completions/wasm.fish +223 -3
  13. wasm/core/config.py +85 -1
  14. wasm/core/dependencies.py +51 -17
  15. wasm/core/exceptions.py +14 -0
  16. wasm/core/update_checker.py +66 -17
  17. wasm/core/utils.py +47 -0
  18. wasm/deployers/base.py +326 -284
  19. wasm/deployers/helpers/__init__.py +20 -0
  20. wasm/deployers/helpers/package_manager.py +183 -0
  21. wasm/deployers/helpers/path_resolver.py +188 -0
  22. wasm/deployers/helpers/prisma.py +130 -0
  23. wasm/deployers/vite.py +12 -0
  24. wasm/main.py +21 -5
  25. wasm/managers/backup_manager.py +71 -40
  26. wasm/managers/database/mysql.py +135 -47
  27. wasm/managers/database/postgres.py +85 -36
  28. wasm/managers/database/redis.py +14 -10
  29. wasm/monitor/__init__.py +15 -1
  30. wasm/monitor/ai_analyzer.py +1 -3
  31. wasm/monitor/process_monitor.py +135 -36
  32. wasm/monitor/threat_store.py +332 -0
  33. wasm/web/api/monitor.py +164 -10
  34. wasm/web/jobs.py +1 -1
  35. wasm/web/static/js/pages/monitor.js +69 -9
  36. wasm/web/websockets/router.py +5 -5
  37. wasm_cli-0.14.0.data/data/share/man/man1/wasm.1 +521 -0
  38. {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.0.dist-info}/METADATA +4 -2
  39. {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.0.dist-info}/RECORD +43 -35
  40. {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.0.dist-info}/WHEEL +0 -0
  41. {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.0.dist-info}/entry_points.txt +0 -0
  42. {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.0.dist-info}/licenses/LICENSE +0 -0
  43. {wasm_cli-0.13.16.dist-info → wasm_cli-0.14.0.dist-info}/top_level.txt +0 -0
wasm/__init__.py CHANGED
@@ -8,7 +8,7 @@ WASM - Web App System Management
8
8
  A robust CLI tool for deploying and managing web applications on Linux servers.
9
9
  """
10
10
 
11
- __version__ = "0.13.16"
11
+ __version__ = "0.14.0"
12
12
  __author__ = "Yago López Prado"
13
13
  __license__ = "WASM-NCSAL"
14
14
 
@@ -0,0 +1,96 @@
1
+ """
2
+ Config command handlers for WASM.
3
+
4
+ Commands for managing WASM configuration files.
5
+ """
6
+
7
+ from argparse import Namespace
8
+
9
+ from wasm.core.config import Config, DEFAULT_CONFIG_PATH
10
+ from wasm.core.logger import Logger
11
+
12
+
13
+ def handle_config(args: Namespace) -> int:
14
+ """
15
+ Handle config commands.
16
+
17
+ Args:
18
+ args: Parsed arguments.
19
+
20
+ Returns:
21
+ Exit code.
22
+ """
23
+ action = args.action
24
+
25
+ handlers = {
26
+ "upgrade": _handle_upgrade,
27
+ "show": _handle_show,
28
+ "path": _handle_path,
29
+ }
30
+
31
+ handler = handlers.get(action)
32
+ if handler:
33
+ return handler(args)
34
+
35
+ # No action specified, show help
36
+ logger = Logger(verbose=getattr(args, "verbose", False))
37
+ logger.info("Usage: wasm config <command>")
38
+ logger.info("")
39
+ logger.info("Commands:")
40
+ logger.info(" upgrade Upgrade config file with new defaults")
41
+ logger.info(" show Show current configuration")
42
+ logger.info(" path Show config file path")
43
+ return 0
44
+
45
+
46
+ def _handle_upgrade(args: Namespace) -> int:
47
+ """Handle config upgrade command."""
48
+ quiet = getattr(args, "quiet", False)
49
+ logger = Logger(verbose=getattr(args, "verbose", False))
50
+
51
+ if not quiet:
52
+ logger.info("Upgrading configuration file...")
53
+
54
+ config = Config()
55
+ result = config.upgrade()
56
+
57
+ if "error" in result:
58
+ if not quiet:
59
+ logger.error(f"Failed to upgrade config: {result['error']}")
60
+ return 1
61
+
62
+ if result["upgraded"]:
63
+ if not quiet:
64
+ logger.success(f"Configuration upgraded! Added {len(result['added_keys'])} new option(s):")
65
+ for key in result["added_keys"][:10]: # Show first 10
66
+ logger.info(f" + {key}")
67
+ if len(result["added_keys"]) > 10:
68
+ logger.info(f" ... and {len(result['added_keys']) - 10} more")
69
+ else:
70
+ if not quiet:
71
+ logger.success("Configuration is already up to date")
72
+
73
+ return 0
74
+
75
+
76
+ def _handle_show(args: Namespace) -> int:
77
+ """Handle config show command."""
78
+ import yaml
79
+
80
+ logger = Logger(verbose=getattr(args, "verbose", False))
81
+ config = Config()
82
+
83
+ logger.header("Current Configuration")
84
+ print(yaml.dump(config.to_dict(), default_flow_style=False, sort_keys=False))
85
+
86
+ return 0
87
+
88
+
89
+ def _handle_path(args: Namespace) -> int:
90
+ """Handle config path command."""
91
+ logger = Logger(verbose=getattr(args, "verbose", False))
92
+
93
+ logger.key_value("Config file", str(DEFAULT_CONFIG_PATH))
94
+ logger.key_value("Exists", "Yes" if DEFAULT_CONFIG_PATH.exists() else "No")
95
+
96
+ return 0
wasm/cli/commands/db.py CHANGED
@@ -934,9 +934,10 @@ def _db_connect(args: Namespace, verbose: bool) -> int:
934
934
 
935
935
  logger.info(f"Connecting to {manager.DISPLAY_NAME}...")
936
936
  logger.info(f"Command: {' '.join(cmd)}")
937
-
937
+
938
938
  # Execute interactively
939
- return subprocess.call(cmd)
939
+ result = subprocess.run(cmd)
940
+ return result.returncode
940
941
  except Exception as e:
941
942
  logger.error(str(e))
942
943
  return 1
@@ -0,0 +1,231 @@
1
+ # Copyright (c) 2024-2025 Yago López Prado
2
+ # Licensed under WASM-NCSAL 1.0 (Commercial use prohibited)
3
+ # https://github.com/Perkybeet/wasm/blob/main/LICENSE
4
+
5
+ """
6
+ Health check command for WASM.
7
+
8
+ Provides system-wide health diagnostics.
9
+ """
10
+
11
+ import shutil
12
+ from argparse import Namespace
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ from wasm.core.config import Config
17
+ from wasm.core.logger import Logger
18
+
19
+
20
+ def _print_status(logger: Logger, key: str, value: str, status: str) -> None:
21
+ """Print a key-value pair with status indicator."""
22
+ if status == "ok":
23
+ indicator = "\033[32m[OK]\033[0m"
24
+ elif status == "warning":
25
+ indicator = "\033[33m[!]\033[0m"
26
+ elif status == "error":
27
+ indicator = "\033[31m[X]\033[0m"
28
+ else: # info
29
+ indicator = "\033[34m[i]\033[0m"
30
+
31
+ print(f" {indicator} {key}: {value}")
32
+
33
+
34
+ def handle_health(args: Namespace) -> int:
35
+ """
36
+ Handle the health check command.
37
+
38
+ Performs system diagnostics and shows overall health status.
39
+ """
40
+ logger = Logger(verbose=args.verbose)
41
+ config = Config()
42
+
43
+ logger.header("System Health Check")
44
+ logger.blank()
45
+
46
+ issues = []
47
+ warnings = []
48
+
49
+ # 1. Check disk space
50
+ logger.info("Checking disk space...")
51
+ try:
52
+ apps_dir = config.apps_directory
53
+ if apps_dir.exists():
54
+ stat = shutil.disk_usage(str(apps_dir))
55
+ free_gb = stat.free / (1024 ** 3)
56
+ total_gb = stat.total / (1024 ** 3)
57
+ used_percent = ((stat.total - stat.free) / stat.total) * 100
58
+
59
+ if free_gb < 1.0:
60
+ issues.append(f"Low disk space: {free_gb:.1f}GB free")
61
+ _print_status(logger, "Disk Space", f"{free_gb:.1f}GB free / {total_gb:.1f}GB total ({used_percent:.0f}% used)", "error")
62
+ elif free_gb < 5.0:
63
+ warnings.append(f"Disk space is getting low: {free_gb:.1f}GB free")
64
+ _print_status(logger, "Disk Space", f"{free_gb:.1f}GB free / {total_gb:.1f}GB total ({used_percent:.0f}% used)", "warning")
65
+ else:
66
+ _print_status(logger, "Disk Space", f"{free_gb:.1f}GB free / {total_gb:.1f}GB total ({used_percent:.0f}% used)", "ok")
67
+ else:
68
+ _print_status(logger, "Disk Space", "Apps directory not found", "warning")
69
+ except Exception as e:
70
+ warnings.append(f"Could not check disk space: {e}")
71
+
72
+ # 2. Check web servers
73
+ logger.blank()
74
+ logger.info("Checking web servers...")
75
+
76
+ from wasm.managers.nginx_manager import NginxManager
77
+ from wasm.managers.apache_manager import ApacheManager
78
+
79
+ nginx = NginxManager(verbose=args.verbose)
80
+ apache = ApacheManager(verbose=args.verbose)
81
+
82
+ nginx_installed = nginx.is_installed()
83
+ apache_installed = apache.is_installed()
84
+
85
+ if nginx_installed:
86
+ nginx_status = nginx.get_status()
87
+ if nginx_status.get("active"):
88
+ _print_status(logger, "Nginx", "Running", "ok")
89
+ else:
90
+ issues.append("Nginx is installed but not running")
91
+ _print_status(logger, "Nginx", "Stopped", "error")
92
+ else:
93
+ _print_status(logger, "Nginx", "Not installed", "info")
94
+
95
+ if apache_installed:
96
+ apache_status = apache.get_status()
97
+ if apache_status.get("active"):
98
+ _print_status(logger, "Apache", "Running", "ok")
99
+ else:
100
+ warnings.append("Apache is installed but not running")
101
+ _print_status(logger, "Apache", "Stopped", "warning")
102
+ else:
103
+ _print_status(logger, "Apache", "Not installed", "info")
104
+
105
+ if not nginx_installed and not apache_installed:
106
+ issues.append("No web server installed")
107
+
108
+ # 3. Check deployed applications
109
+ logger.blank()
110
+ logger.info("Checking deployed applications...")
111
+
112
+ from wasm.core.store import get_store
113
+ from wasm.managers.service_manager import ServiceManager
114
+
115
+ store = get_store()
116
+ service_manager = ServiceManager(verbose=args.verbose)
117
+
118
+ apps = store.list_apps()
119
+ apps_running = 0
120
+ apps_stopped = 0
121
+ apps_failed = 0
122
+
123
+ for app in apps:
124
+ try:
125
+ status = service_manager.status(app.domain.replace(".", "-"))
126
+ if status.get("active"):
127
+ apps_running += 1
128
+ else:
129
+ apps_stopped += 1
130
+ warnings.append(f"App '{app.domain}' is not running")
131
+ except Exception:
132
+ apps_failed += 1
133
+
134
+ total_apps = len(apps)
135
+ if total_apps > 0:
136
+ if apps_stopped > 0 or apps_failed > 0:
137
+ _print_status(logger, "Applications", f"{apps_running}/{total_apps} running, {apps_stopped} stopped", "warning")
138
+ else:
139
+ _print_status(logger, "Applications", f"{apps_running}/{total_apps} running", "ok")
140
+ else:
141
+ _print_status(logger, "Applications", "No applications deployed", "info")
142
+
143
+ # 4. Check SSL certificates
144
+ logger.blank()
145
+ logger.info("Checking SSL certificates...")
146
+
147
+ from wasm.managers.cert_manager import CertManager
148
+ cert_manager = CertManager(verbose=args.verbose)
149
+
150
+ try:
151
+ certs = cert_manager.list_certificates()
152
+ expiring_soon = []
153
+
154
+ for cert in certs:
155
+ if cert.get("expires"):
156
+ try:
157
+ expires = datetime.fromisoformat(cert["expires"].replace("Z", "+00:00"))
158
+ days_left = (expires - datetime.now(expires.tzinfo)).days
159
+ if days_left < 7:
160
+ issues.append(f"Certificate for {cert['domain']} expires in {days_left} days")
161
+ expiring_soon.append(cert["domain"])
162
+ elif days_left < 30:
163
+ warnings.append(f"Certificate for {cert['domain']} expires in {days_left} days")
164
+ expiring_soon.append(cert["domain"])
165
+ except Exception:
166
+ pass
167
+
168
+ if expiring_soon:
169
+ _print_status(logger, "SSL Certificates", f"{len(certs)} total, {len(expiring_soon)} expiring soon", "warning")
170
+ elif certs:
171
+ _print_status(logger, "SSL Certificates", f"{len(certs)} total, all valid", "ok")
172
+ else:
173
+ _print_status(logger, "SSL Certificates", "None configured", "info")
174
+ except Exception as e:
175
+ _print_status(logger, "SSL Certificates", f"Could not check: {e}", "warning")
176
+
177
+ # 5. Check system resources
178
+ logger.blank()
179
+ logger.info("Checking system resources...")
180
+
181
+ try:
182
+ # Memory check using /proc/meminfo
183
+ with open("/proc/meminfo") as f:
184
+ meminfo = {}
185
+ for line in f:
186
+ parts = line.split(":")
187
+ if len(parts) == 2:
188
+ key = parts[0].strip()
189
+ value = parts[1].strip().split()[0]
190
+ meminfo[key] = int(value)
191
+
192
+ total_mem = meminfo.get("MemTotal", 0) / 1024 / 1024 # GB
193
+ free_mem = (meminfo.get("MemAvailable", 0) or meminfo.get("MemFree", 0)) / 1024 / 1024 # GB
194
+ used_percent = ((total_mem - free_mem) / total_mem) * 100 if total_mem > 0 else 0
195
+
196
+ if used_percent > 90:
197
+ issues.append(f"High memory usage: {used_percent:.0f}%")
198
+ _print_status(logger, "Memory", f"{free_mem:.1f}GB free / {total_mem:.1f}GB total ({used_percent:.0f}% used)", "error")
199
+ elif used_percent > 75:
200
+ warnings.append(f"Memory usage is high: {used_percent:.0f}%")
201
+ _print_status(logger, "Memory", f"{free_mem:.1f}GB free / {total_mem:.1f}GB total ({used_percent:.0f}% used)", "warning")
202
+ else:
203
+ _print_status(logger, "Memory", f"{free_mem:.1f}GB free / {total_mem:.1f}GB total ({used_percent:.0f}% used)", "ok")
204
+ except Exception as e:
205
+ _print_status(logger, "Memory", f"Could not check: {e}", "warning")
206
+
207
+ # Summary
208
+ logger.blank()
209
+ logger.blank()
210
+
211
+ if issues:
212
+ logger.error(f"Health check found {len(issues)} issue(s):")
213
+ for issue in issues:
214
+ logger.error(f" - {issue}")
215
+ logger.blank()
216
+
217
+ if warnings:
218
+ logger.warning(f"Health check found {len(warnings)} warning(s):")
219
+ for warning in warnings:
220
+ logger.warning(f" - {warning}")
221
+ logger.blank()
222
+
223
+ if not issues and not warnings:
224
+ logger.success("All systems healthy!")
225
+ return 0
226
+ elif issues:
227
+ logger.error("System has issues that need attention.")
228
+ return 1
229
+ else:
230
+ logger.warning("System is healthy with minor warnings.")
231
+ return 0
@@ -9,6 +9,11 @@ from argparse import Namespace
9
9
 
10
10
  from wasm.core.logger import Logger
11
11
  from wasm.core.exceptions import WASMError, MonitorError, EmailError
12
+ from wasm.monitor import (
13
+ DEFAULT_SCAN_INTERVAL,
14
+ DEFAULT_CPU_THRESHOLD,
15
+ DEFAULT_MEMORY_THRESHOLD,
16
+ )
12
17
 
13
18
 
14
19
  def handle_monitor(args: Namespace) -> int:
@@ -348,9 +353,9 @@ def _handle_config(args: Namespace) -> int:
348
353
  # General settings
349
354
  logger.info("")
350
355
  logger.key_value("Enabled", str(config.get("monitor.enabled", False)))
351
- logger.key_value("Scan Interval", f"{config.get('monitor.scan_interval', 3600)}s")
352
- logger.key_value("CPU Threshold", f"{config.get('monitor.cpu_threshold', 80.0)}%")
353
- logger.key_value("Memory Threshold", f"{config.get('monitor.memory_threshold', 80.0)}%")
356
+ logger.key_value("Scan Interval", f"{config.get('monitor.scan_interval', DEFAULT_SCAN_INTERVAL)}s")
357
+ logger.key_value("CPU Threshold", f"{config.get('monitor.cpu_threshold', DEFAULT_CPU_THRESHOLD)}%")
358
+ logger.key_value("Memory Threshold", f"{config.get('monitor.memory_threshold', DEFAULT_MEMORY_THRESHOLD)}%")
354
359
 
355
360
  # Actions
356
361
  logger.info("")
@@ -12,7 +12,7 @@ from typing import Dict, List, Optional, Tuple
12
12
  from wasm.core.logger import Logger
13
13
  from wasm.core.exceptions import WASMError
14
14
  from wasm.core.config import DEFAULT_APPS_DIR, DEFAULT_LOG_DIR, DEFAULT_CONFIG_PATH
15
- from wasm.core.utils import command_exists, run_command, run_command_sudo
15
+ from wasm.core.utils import command_exists, run_command, run_command_sudo, run_trusted_installer
16
16
 
17
17
 
18
18
  def handle_setup(args: Namespace) -> int:
@@ -361,20 +361,20 @@ def _handle_init(args: Namespace) -> int:
361
361
 
362
362
  if config_choices.get("install_nodejs") and not command_exists("node"):
363
363
  logger.substep("Installing Node.js 20.x LTS...")
364
-
365
- # Install Node.js from NodeSource
366
- result = run_command(
367
- "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
368
- shell=True
369
- )
370
- if result.success:
371
- result = run_command_sudo(["apt-get", "install", "-y", "nodejs"])
364
+
365
+ # Install Node.js from NodeSource using trusted installer
366
+ try:
367
+ result = run_trusted_installer("https://deb.nodesource.com/setup_20.x")
372
368
  if result.success:
373
- logger.success("Node.js installed")
369
+ result = run_command_sudo(["apt-get", "install", "-y", "nodejs"])
370
+ if result.success:
371
+ logger.success("Node.js installed")
372
+ else:
373
+ logger.warning(f"Failed to install Node.js: {result.stderr}")
374
374
  else:
375
- logger.warning(f"Failed to install Node.js: {result.stderr}")
376
- else:
377
- logger.warning("Failed to setup Node.js repository. Please install manually.")
375
+ logger.warning("Failed to setup Node.js repository. Please install manually.")
376
+ except Exception as e:
377
+ logger.warning(f"Failed to setup Node.js: {e}")
378
378
 
379
379
  # Install package managers
380
380
  selected_pms = config_choices.get("package_managers", ["npm"])
@@ -394,8 +394,12 @@ def _handle_init(args: Namespace) -> int:
394
394
  elif pm == "yarn":
395
395
  result = run_command_sudo(["npm", "install", "-g", "yarn"])
396
396
  elif pm == "bun":
397
- # Bun has its own installer
398
- result = run_command("curl -fsSL https://bun.sh/install | bash", shell=True)
397
+ # Bun has its own trusted installer
398
+ try:
399
+ result = run_trusted_installer("https://bun.sh/install")
400
+ except Exception as e:
401
+ logger.warning(f"Failed to install bun: {e}")
402
+ continue
399
403
  else:
400
404
  result = run_command_sudo(["npm", "install", "-g", pm])
401
405
 
@@ -467,7 +471,38 @@ def _handle_init(args: Namespace) -> int:
467
471
  logger.success(f"Created {DEFAULT_CONFIG_PATH}")
468
472
  except Exception as e:
469
473
  logger.warning(f"Could not save config file: {e}")
470
-
474
+
475
+ # Install man page
476
+ logger.substep("Installing man page...")
477
+ try:
478
+ # Search for man page in multiple locations
479
+ man_source = None
480
+ search_paths = [
481
+ # Development: project root
482
+ Path(__file__).parent.parent.parent.parent.parent / "man" / "wasm.1",
483
+ # Installed via pip with data_files
484
+ Path("/usr/local/share/man/man1/wasm.1"),
485
+ Path("/usr/share/man/man1/wasm.1"),
486
+ ]
487
+
488
+ for path in search_paths:
489
+ if path.exists():
490
+ man_source = path
491
+ break
492
+
493
+ if man_source and man_source != Path("/usr/share/man/man1/wasm.1"):
494
+ man_dest = Path("/usr/share/man/man1/wasm.1")
495
+ shutil.copy(man_source, man_dest)
496
+ os.chmod(man_dest, 0o644)
497
+ run_command(["mandb", "-q"])
498
+ logger.success("Man page installed (man wasm)")
499
+ elif man_source:
500
+ logger.substep("Man page already installed")
501
+ else:
502
+ logger.debug("Man page source not found, skipping")
503
+ except Exception as e:
504
+ logger.debug(f"Could not install man page: {e}")
505
+
471
506
  # =========================================================================
472
507
  # Final Summary
473
508
  # =========================================================================