wasm-cli 0.14.4__tar.gz → 0.15.1__tar.gz

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 (155) hide show
  1. {wasm_cli-0.14.4/src/wasm_cli.egg-info → wasm_cli-0.15.1}/PKG-INFO +1 -1
  2. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/pyproject.toml +1 -1
  3. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/setup.py +25 -2
  4. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/__init__.py +1 -1
  5. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/backup.py +115 -2
  6. wasm_cli-0.15.1/src/wasm/cli/commands/env.py +220 -0
  7. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/webapp.py +132 -9
  8. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/parser.py +158 -3
  9. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/dependencies.py +36 -0
  10. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/exceptions.py +10 -0
  11. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/logger.py +20 -2
  12. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/store.py +1 -0
  13. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/__init__.py +2 -0
  14. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/base.py +109 -7
  15. wasm_cli-0.15.1/src/wasm/deployers/docker_compose.py +848 -0
  16. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/__init__.py +4 -0
  17. wasm_cli-0.15.1/src/wasm/deployers/helpers/env_manager.py +525 -0
  18. wasm_cli-0.15.1/src/wasm/deployers/helpers/nginx_config.py +289 -0
  19. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/monorepo.py +20 -1
  20. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/python.py +3 -2
  21. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/registry.py +2 -0
  22. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/main.py +8 -0
  23. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/backup_manager.py +175 -8
  24. wasm_cli-0.15.1/src/wasm/managers/backup_scheduler.py +266 -0
  25. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/base_manager.py +4 -8
  26. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/postgres.py +152 -2
  27. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/redis.py +88 -2
  28. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/nginx_manager.py +26 -1
  29. wasm_cli-0.15.1/src/wasm/templates/nginx/advanced.conf.j2 +117 -0
  30. wasm_cli-0.15.1/src/wasm/templates/systemd/backup-service.j2 +9 -0
  31. wasm_cli-0.15.1/src/wasm/templates/systemd/backup-timer.j2 +13 -0
  32. wasm_cli-0.15.1/src/wasm/templates/systemd/docker-compose.service.j2 +39 -0
  33. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/jobs.py +26 -19
  34. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/server.py +12 -8
  35. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/css/main.css +28 -1
  36. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/index.html +6 -2
  37. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/api.js +2 -2
  38. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/router.js +1 -0
  39. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/ui.js +1 -0
  40. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/main.js +2 -2
  41. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/apps.js +1 -1
  42. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/databases.js +1 -10
  43. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/jobs.js +19 -4
  44. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/logs.js +11 -1
  45. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/monitor.js +12 -2
  46. {wasm_cli-0.14.4 → wasm_cli-0.15.1/src/wasm_cli.egg-info}/PKG-INFO +1 -1
  47. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/SOURCES.txt +9 -0
  48. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/LICENSE +0 -0
  49. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/MANIFEST.in +0 -0
  50. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/README.md +0 -0
  51. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/MONITOR.md +0 -0
  52. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/OBS_SETUP.md +0 -0
  53. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/assets/logo.png +0 -0
  54. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/assets/logo_bg.png +0 -0
  55. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/man/wasm.1 +0 -0
  56. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/setup.cfg +0 -0
  57. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/__main__.py +0 -0
  58. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/__init__.py +0 -0
  59. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/__init__.py +0 -0
  60. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/cert.py +0 -0
  61. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/config.py +0 -0
  62. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/db.py +0 -0
  63. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/health.py +0 -0
  64. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/monitor.py +0 -0
  65. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/service.py +0 -0
  66. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/setup.py +0 -0
  67. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/site.py +0 -0
  68. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/store.py +0 -0
  69. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/version.py +0 -0
  70. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/web.py +0 -0
  71. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/interactive.py +0 -0
  72. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/__init__.py +0 -0
  73. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/_wasm +0 -0
  74. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/wasm.bash +0 -0
  75. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/wasm.fish +0 -0
  76. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/__init__.py +0 -0
  77. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/config.py +0 -0
  78. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/update_checker.py +0 -0
  79. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/utils.py +0 -0
  80. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/package_manager.py +0 -0
  81. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/path_resolver.py +0 -0
  82. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/prisma.py +0 -0
  83. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/turbo.py +0 -0
  84. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/workspace.py +0 -0
  85. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/nextjs.py +0 -0
  86. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/nodejs.py +0 -0
  87. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/static.py +0 -0
  88. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/vite.py +0 -0
  89. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/__init__.py +0 -0
  90. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/apache_manager.py +0 -0
  91. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/cert_manager.py +0 -0
  92. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/__init__.py +0 -0
  93. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/base.py +0 -0
  94. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/mongodb.py +0 -0
  95. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/mysql.py +0 -0
  96. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/registry.py +0 -0
  97. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/service_manager.py +0 -0
  98. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/source_manager.py +0 -0
  99. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/__init__.py +0 -0
  100. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/ai_analyzer.py +0 -0
  101. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/email_notifier.py +0 -0
  102. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/process_monitor.py +0 -0
  103. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/threat_store.py +0 -0
  104. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/__init__.py +0 -0
  105. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
  106. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/apache/static.conf.j2 +0 -0
  107. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
  108. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
  109. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/nginx/static.conf.j2 +0 -0
  110. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/systemd/app.service.j2 +0 -0
  111. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/__init__.py +0 -0
  112. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/domain.py +0 -0
  113. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/port.py +0 -0
  114. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/source.py +0 -0
  115. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/ssh.py +0 -0
  116. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/__init__.py +0 -0
  117. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/__init__.py +0 -0
  118. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/apps.py +0 -0
  119. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/auth.py +0 -0
  120. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/backups.py +0 -0
  121. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/certs.py +0 -0
  122. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/config.py +0 -0
  123. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/databases.py +0 -0
  124. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/jobs.py +0 -0
  125. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/monitor.py +0 -0
  126. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/router.py +0 -0
  127. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/services.py +0 -0
  128. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/sites.py +0 -0
  129. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/system.py +0 -0
  130. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/auth.py +0 -0
  131. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/cards.js +0 -0
  132. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/jobs.js +0 -0
  133. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/metrics.js +0 -0
  134. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/skeleton.js +0 -0
  135. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/dialogs.js +0 -0
  136. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/notifications.js +0 -0
  137. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/search.js +0 -0
  138. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/shortcuts.js +0 -0
  139. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/theme.js +0 -0
  140. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/websocket.js +0 -0
  141. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/backups.js +0 -0
  142. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/certs.js +0 -0
  143. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/config.js +0 -0
  144. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/dashboard.js +0 -0
  145. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/services.js +0 -0
  146. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/sites.js +0 -0
  147. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/login.html +0 -0
  148. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/logo.png +0 -0
  149. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/logo.webp +0 -0
  150. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/websockets/__init__.py +0 -0
  151. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/websockets/router.py +0 -0
  152. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
  153. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/entry_points.txt +0 -0
  154. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/requires.txt +0 -0
  155. {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wasm-cli
3
- Version: 0.14.4
3
+ Version: 0.15.1
4
4
  Summary: Web App System Management - Deploy and manage web applications on Linux servers
5
5
  Author: Yago López Prado
6
6
  Author-email: Yago López Prado <yago.lopez.adeje@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wasm-cli"
7
- version = "0.14.4"
7
+ version = "0.15.1"
8
8
  description = "Web App System Management - Deploy and manage web applications on Linux servers"
9
9
  readme = "README.md"
10
10
  license = { text = "WASM-NCSAL" }
@@ -9,7 +9,7 @@ from setuptools import setup, find_packages
9
9
 
10
10
  setup(
11
11
  name="wasm-cli",
12
- version="0.14.4",
12
+ version="0.15.1",
13
13
  description="Web App System Management - Deploy and manage web applications on Linux servers",
14
14
  author="Yago López Prado",
15
15
  author_email="yago.lopez.adeje@gmail.com",
@@ -24,10 +24,33 @@ setup(
24
24
  ],
25
25
  extras_require={
26
26
  "interactive": ["inquirer>=3.1.0"],
27
+ "web": [
28
+ "fastapi>=0.109.0",
29
+ "uvicorn[standard]>=0.27.0",
30
+ "python-jose[cryptography]>=3.3.0",
31
+ "passlib[bcrypt]>=1.7.4",
32
+ "psutil>=5.9.0",
33
+ "aiofiles>=23.0.0",
34
+ ],
35
+ "monitor": [
36
+ "psutil>=5.9.0",
37
+ "httpx>=0.25.0",
38
+ ],
39
+ "all": [
40
+ "inquirer>=3.1.0",
41
+ "rich>=13.0",
42
+ "psutil>=5.9.0",
43
+ "httpx>=0.25.0",
44
+ "fastapi>=0.109.0",
45
+ "uvicorn[standard]>=0.27.0",
46
+ "python-jose[cryptography]>=3.3.0",
47
+ "passlib[bcrypt]>=1.7.4",
48
+ "aiofiles>=23.0.0",
49
+ ],
27
50
  },
28
51
  entry_points={
29
52
  "console_scripts": [
30
- "wasm=wasm.main:main",
53
+ "wasm=wasm.main:cli",
31
54
  ],
32
55
  },
33
56
  data_files=[
@@ -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.14.4"
11
+ __version__ = "0.15.1"
12
12
  __author__ = "Yago López Prado"
13
13
  __license__ = "WASM-NCSAL"
14
14
 
@@ -29,6 +29,8 @@ def handle_backup(args: Namespace) -> int:
29
29
  return _backup_info(args, verbose)
30
30
  elif action == "storage":
31
31
  return _backup_storage(args, verbose)
32
+ elif action == "schedule":
33
+ return _backup_schedule(args, verbose)
32
34
  else:
33
35
  logger = Logger(verbose=verbose)
34
36
  logger.error(f"Unknown backup action: {action}")
@@ -480,9 +482,120 @@ def _backup_storage(args: Namespace, verbose: bool) -> int:
480
482
  app_str = f"{app_bytes:.1f} TB"
481
483
 
482
484
  logger.info(f" {app_name}: {app_str} ({app_usage['count']} backups)")
483
-
485
+
484
486
  return 0
485
-
487
+
486
488
  except Exception as e:
487
489
  logger.error(f"Error: {e}")
488
490
  return 1
491
+
492
+
493
+ def _backup_schedule(args: Namespace, verbose: bool) -> int:
494
+ """Handle backup schedule subcommands."""
495
+ logger = Logger(verbose=verbose)
496
+ schedule_action = getattr(args, "schedule_action", None)
497
+
498
+ if not schedule_action:
499
+ logger.error("Schedule requires an action: create, list, or delete")
500
+ return 1
501
+
502
+ if schedule_action == "create":
503
+ return _backup_schedule_create(args, verbose)
504
+ elif schedule_action in ("list", "ls"):
505
+ return _backup_schedule_list(args, verbose)
506
+ elif schedule_action in ("delete", "remove", "rm"):
507
+ return _backup_schedule_delete(args, verbose)
508
+ else:
509
+ logger.error(f"Unknown schedule action: {schedule_action}")
510
+ return 1
511
+
512
+
513
+ def _backup_schedule_create(args: Namespace, verbose: bool) -> int:
514
+ """Create a backup schedule."""
515
+ logger = Logger(verbose=verbose)
516
+
517
+ domain = getattr(args, "domain", None)
518
+ schedule = getattr(args, "schedule", "daily")
519
+ retention_count = getattr(args, "retention_count", 7)
520
+ retention_days = getattr(args, "retention_days", 30)
521
+
522
+ if not domain:
523
+ logger.error("Domain is required")
524
+ return 1
525
+
526
+ try:
527
+ from wasm.managers.backup_scheduler import BackupScheduler, BackupSchedule
528
+ from wasm.core.utils import domain_to_app_name
529
+
530
+ scheduler = BackupScheduler(verbose=verbose)
531
+ backup_schedule = BackupSchedule(
532
+ domain=domain,
533
+ app_name=domain_to_app_name(domain),
534
+ schedule=schedule,
535
+ retention_count=retention_count,
536
+ retention_days=retention_days,
537
+ )
538
+
539
+ scheduler.create_schedule(backup_schedule)
540
+ logger.success(f"Backup schedule created for {domain}")
541
+ logger.info(f" Schedule: {backup_schedule.on_calendar}")
542
+ logger.info(f" Retention: {retention_count} backups / {retention_days} days")
543
+ return 0
544
+
545
+ except Exception as e:
546
+ logger.error(f"Failed to create schedule: {e}")
547
+ if verbose:
548
+ import traceback
549
+ traceback.print_exc()
550
+ return 1
551
+
552
+
553
+ def _backup_schedule_list(args: Namespace, verbose: bool) -> int:
554
+ """List backup schedules."""
555
+ logger = Logger(verbose=verbose)
556
+
557
+ try:
558
+ from wasm.managers.backup_scheduler import BackupScheduler
559
+
560
+ scheduler = BackupScheduler(verbose=verbose)
561
+ schedules = scheduler.list_schedules()
562
+
563
+ if not schedules:
564
+ logger.info("No backup schedules found")
565
+ return 0
566
+
567
+ logger.header("Backup Schedules")
568
+ for sched in schedules:
569
+ logger.info(
570
+ f" {sched['app_name']}: "
571
+ f"next={sched.get('next_run', '?')} "
572
+ f"last={sched.get('last_run', 'never')}"
573
+ )
574
+
575
+ return 0
576
+
577
+ except Exception as e:
578
+ logger.error(f"Failed to list schedules: {e}")
579
+ return 1
580
+
581
+
582
+ def _backup_schedule_delete(args: Namespace, verbose: bool) -> int:
583
+ """Delete a backup schedule."""
584
+ logger = Logger(verbose=verbose)
585
+
586
+ domain = getattr(args, "domain", None)
587
+ if not domain:
588
+ logger.error("Domain is required")
589
+ return 1
590
+
591
+ try:
592
+ from wasm.managers.backup_scheduler import BackupScheduler
593
+
594
+ scheduler = BackupScheduler(verbose=verbose)
595
+ scheduler.remove_schedule(domain)
596
+ logger.success(f"Backup schedule removed for {domain}")
597
+ return 0
598
+
599
+ except Exception as e:
600
+ logger.error(f"Failed to remove schedule: {e}")
601
+ return 1
@@ -0,0 +1,220 @@
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
+ Environment variable CLI commands for WASM.
7
+
8
+ Provides CLI handlers for managing application environment variables:
9
+ - Interactive configuration from .env.example templates
10
+ - Viewing current values with secret masking
11
+ - Exporting variables to files
12
+ """
13
+
14
+ import sys
15
+ from argparse import Namespace
16
+ from pathlib import Path
17
+
18
+ from wasm.core.config import Config
19
+ from wasm.core.logger import Logger
20
+ from wasm.core.exceptions import WASMError
21
+ from wasm.core.utils import domain_to_app_name
22
+ from wasm.deployers.helpers.env_manager import EnvManager, EnvConfig
23
+
24
+
25
+ def handle_env(args: Namespace) -> int:
26
+ """
27
+ Handle wasm env <action> commands.
28
+
29
+ Routes to the appropriate sub-handler based on the action.
30
+
31
+ Args:
32
+ args: Parsed command-line arguments.
33
+
34
+ Returns:
35
+ Exit code (0 for success, non-zero for error).
36
+ """
37
+ action = getattr(args, "action", None)
38
+ verbose = getattr(args, "verbose", False)
39
+
40
+ if not action:
41
+ print("Error: env requires an action", file=sys.stderr)
42
+ print("Use: wasm env --help", file=sys.stderr)
43
+ return 1
44
+
45
+ handlers = {
46
+ "configure": _env_configure,
47
+ "config": _env_configure,
48
+ "setup": _env_configure,
49
+ "show": _env_show,
50
+ "list": _env_show,
51
+ "ls": _env_show,
52
+ "export": _env_export,
53
+ }
54
+
55
+ handler = handlers.get(action)
56
+ if not handler:
57
+ print(f"Unknown env action: {action}", file=sys.stderr)
58
+ return 1
59
+
60
+ try:
61
+ return handler(args, verbose)
62
+ except WASMError as e:
63
+ logger = Logger(verbose=verbose)
64
+ logger.error(e.message)
65
+ if e.details:
66
+ print(f" {e.details}")
67
+ return 1
68
+
69
+
70
+ def _env_configure(args: Namespace, verbose: bool) -> int:
71
+ """
72
+ Run interactive environment configuration.
73
+
74
+ Discovers variables from .env.example files, prompts the user
75
+ for values, auto-generates secrets, and writes .env files.
76
+
77
+ Args:
78
+ args: Parsed arguments (requires domain).
79
+ verbose: Whether to enable verbose output.
80
+
81
+ Returns:
82
+ Exit code.
83
+ """
84
+ logger = Logger(verbose=verbose)
85
+ config = Config()
86
+
87
+ domain = getattr(args, "domain", None)
88
+ if not domain:
89
+ logger.error("Domain is required")
90
+ return 1
91
+
92
+ app_name = domain_to_app_name(domain)
93
+ app_path = config.apps_directory / app_name
94
+
95
+ if not app_path.exists():
96
+ logger.error(f"Application not found: {domain}")
97
+ return 1
98
+
99
+ manager = EnvManager(verbose=verbose)
100
+
101
+ logger.header(f"Environment Configuration: {domain}")
102
+
103
+ # Discover variables
104
+ variables = manager.discover(app_path)
105
+ if not variables:
106
+ logger.info("No .env.example files found")
107
+ return 0
108
+
109
+ logger.info(f"Found {len(variables)} variables")
110
+
111
+ # Load existing values
112
+ existing = manager.get_current_values(app_path)
113
+
114
+ # Prompt for values
115
+ values = manager.prompt_variables(variables, existing)
116
+
117
+ # Write .env file
118
+ written = manager.write_env_files(app_path, values)
119
+ for path in written:
120
+ logger.success(f"Written: {path}")
121
+
122
+ # Save config
123
+ env_config = EnvConfig(variables=variables)
124
+ manager.save_config(app_path, env_config)
125
+
126
+ return 0
127
+
128
+
129
+ def _env_show(args: Namespace, verbose: bool) -> int:
130
+ """
131
+ Display current environment variables.
132
+
133
+ Shows all variables from the application's .env file, with
134
+ secret values masked by default.
135
+
136
+ Args:
137
+ args: Parsed arguments (requires domain, optional --unmask).
138
+ verbose: Whether to enable verbose output.
139
+
140
+ Returns:
141
+ Exit code.
142
+ """
143
+ logger = Logger(verbose=verbose)
144
+ config = Config()
145
+
146
+ domain = getattr(args, "domain", None)
147
+ if not domain:
148
+ logger.error("Domain is required")
149
+ return 1
150
+
151
+ unmask = getattr(args, "unmask", False)
152
+
153
+ app_name = domain_to_app_name(domain)
154
+ app_path = config.apps_directory / app_name
155
+
156
+ if not app_path.exists():
157
+ logger.error(f"Application not found: {domain}")
158
+ return 1
159
+
160
+ manager = EnvManager(verbose=verbose)
161
+ values = manager.get_current_values(app_path)
162
+
163
+ if not values:
164
+ logger.info(f"No environment variables found for {domain}")
165
+ return 0
166
+
167
+ logger.header(f"Environment: {domain}")
168
+
169
+ for key in sorted(values.keys()):
170
+ value = values[key]
171
+ if not unmask:
172
+ value = manager.mask_value(key, value)
173
+ logger.key_value(f" {key}", value)
174
+
175
+ return 0
176
+
177
+
178
+ def _env_export(args: Namespace, verbose: bool) -> int:
179
+ """
180
+ Export environment variables to a file.
181
+
182
+ Reads the application's .env file and writes it to the
183
+ specified output path.
184
+
185
+ Args:
186
+ args: Parsed arguments (requires domain, optional --output).
187
+ verbose: Whether to enable verbose output.
188
+
189
+ Returns:
190
+ Exit code.
191
+ """
192
+ logger = Logger(verbose=verbose)
193
+ config = Config()
194
+
195
+ domain = getattr(args, "domain", None)
196
+ output = getattr(args, "output", ".env")
197
+
198
+ if not domain:
199
+ logger.error("Domain is required")
200
+ return 1
201
+
202
+ app_name = domain_to_app_name(domain)
203
+ app_path = config.apps_directory / app_name
204
+
205
+ if not app_path.exists():
206
+ logger.error(f"Application not found: {domain}")
207
+ return 1
208
+
209
+ manager = EnvManager(verbose=verbose)
210
+ values = manager.get_current_values(app_path)
211
+
212
+ if not values:
213
+ logger.info(f"No environment variables found for {domain}")
214
+ return 0
215
+
216
+ output_path = Path(output)
217
+ manager._write_single_env_file(output_path, values)
218
+ logger.success(f"Exported {len(values)} variables to {output_path}")
219
+
220
+ return 0
@@ -207,6 +207,10 @@ def _handle_create(args: Namespace) -> int:
207
207
  if app_type == "monorepo":
208
208
  return _handle_monorepo_create(args, domain, env_vars, logger)
209
209
 
210
+ # Handle docker-compose deployments specially
211
+ if app_type == "docker-compose":
212
+ return _handle_docker_compose_create(args, domain, env_vars, logger)
213
+
210
214
  # Get deployer
211
215
  deployer = get_deployer(app_type, verbose=args.verbose)
212
216
 
@@ -269,6 +273,31 @@ def _handle_monorepo_create(args: Namespace, domain: str, env_vars: Dict, logger
269
273
  return 0
270
274
 
271
275
 
276
+ def _handle_docker_compose_create(
277
+ args: Namespace, domain: str, env_vars: Dict, logger: Logger
278
+ ) -> int:
279
+ """Handle Docker Compose deployment."""
280
+ from wasm.deployers.docker_compose import DockerComposeDeployer
281
+
282
+ deployer = DockerComposeDeployer(verbose=args.verbose)
283
+
284
+ deployer.configure(
285
+ domain=domain,
286
+ source=args.source,
287
+ webserver=args.webserver,
288
+ ssl=not args.no_ssl,
289
+ branch=args.branch,
290
+ env_vars=env_vars,
291
+ compose_file=getattr(args, "compose_file", None),
292
+ compose_profiles=getattr(args, "compose_profiles", None),
293
+ port=args.port,
294
+ )
295
+
296
+ deployer.deploy()
297
+
298
+ return 0
299
+
300
+
272
301
  def _handle_list(args: Namespace) -> int:
273
302
  """Handle webapp list command."""
274
303
  logger = Logger(verbose=args.verbose)
@@ -587,12 +616,19 @@ def _handle_update(args: Namespace) -> int:
587
616
 
588
617
  # Step 3: Detect app type and configure deployer
589
618
  logger.step(3, total_steps, "Detecting application type")
590
- app_type = detect_app_type(app_path, verbose=args.verbose)
591
- if not app_type:
592
- app_type = "nodejs"
593
- logger.substep(f"Using default: {app_type}")
594
- else:
619
+
620
+ # Prefer stored app type from database (initial deploy already determined it correctly)
621
+ stored_type = app.app_type if app else None
622
+ if stored_type and stored_type != "unknown":
623
+ app_type = stored_type
595
624
  logger.substep(f"Detected: {app_type}")
625
+ else:
626
+ app_type = detect_app_type(app_path, verbose=args.verbose)
627
+ if not app_type:
628
+ app_type = "nodejs"
629
+ logger.substep(f"Using default: {app_type}")
630
+ else:
631
+ logger.substep(f"Detected: {app_type}")
596
632
 
597
633
  # Handle monorepo updates with dedicated flow
598
634
  if app_type == "monorepo":
@@ -600,6 +636,12 @@ def _handle_update(args: Namespace) -> int:
600
636
  args, app_path, app_name, domain, logger, total_steps
601
637
  )
602
638
 
639
+ # Handle docker-compose updates
640
+ if app_type == "docker-compose":
641
+ return _handle_docker_compose_update(
642
+ args, app_path, app_name, domain, logger
643
+ )
644
+
603
645
  deployer = get_deployer(app_type, verbose=args.verbose)
604
646
  deployer.app_path = app_path
605
647
  deployer.app_name = app_name
@@ -734,6 +776,38 @@ def _handle_monorepo_update(args, app_path, app_name, domain, logger, total_step
734
776
  return 0
735
777
 
736
778
 
779
+ def _handle_docker_compose_update(args, app_path, app_name, domain, logger):
780
+ """Handle update for Docker Compose applications."""
781
+ from wasm.deployers.docker_compose import DockerComposeDeployer
782
+
783
+ deployer = DockerComposeDeployer(verbose=args.verbose)
784
+ deployer.app_path = app_path
785
+ deployer.app_name = app_name
786
+ deployer.domain = domain
787
+
788
+ # Discover compose file
789
+ deployer._discover_compose_file()
790
+
791
+ logger.step(4, 5, "Rebuilding Docker images")
792
+ deployer._build_images()
793
+
794
+ logger.step(5, 5, "Restarting containers")
795
+ from wasm.core.utils import run_command
796
+ cmd = ["docker", "compose"]
797
+ if deployer.compose_path:
798
+ cmd.extend(["-f", str(deployer.compose_path)])
799
+ cmd.extend(["up", "-d", "--remove-orphans"])
800
+ result = run_command(cmd, cwd=app_path, timeout=300000)
801
+
802
+ if result.success:
803
+ logger.success(f"Docker Compose app updated: {domain}")
804
+ else:
805
+ logger.warning("Update may have issues. Check with: docker compose ps")
806
+ logger.warning(result.stderr)
807
+
808
+ return 0
809
+
810
+
737
811
  def _handle_delete(args: Namespace) -> int:
738
812
  """Handle webapp delete command."""
739
813
  logger = Logger(verbose=args.verbose)
@@ -807,8 +881,22 @@ def _handle_delete(args: Namespace) -> int:
807
881
 
808
882
  total_steps = 5
809
883
 
810
- # Stop and delete service
811
- logger.step(1, total_steps, "Stopping service")
884
+ # Stop Docker Compose containers if applicable
885
+ if app and app.app_type == "docker-compose":
886
+ logger.step(1, total_steps, "Stopping Docker Compose containers")
887
+ from wasm.core.utils import run_command as _run_cmd
888
+ for compose_name in ["docker-compose.prod.yml", "docker-compose.yml", "compose.yml"]:
889
+ compose_file = app_path / compose_name
890
+ if compose_file.exists():
891
+ _run_cmd(
892
+ ["docker", "compose", "-f", str(compose_file), "down", "--remove-orphans"],
893
+ cwd=app_path,
894
+ )
895
+ break
896
+ else:
897
+ logger.step(1, total_steps, "Stopping service")
898
+
899
+ # Delete systemd service
812
900
  service_manager = ServiceManager(verbose=args.verbose)
813
901
  try:
814
902
  service_manager.delete_service(app_name)
@@ -858,10 +946,45 @@ def _handle_logs(args: Namespace) -> int:
858
946
  """Handle webapp logs command."""
859
947
  logger = Logger(verbose=args.verbose)
860
948
  service_manager = ServiceManager(verbose=args.verbose)
861
-
949
+
862
950
  domain = validate_domain(args.domain)
863
951
  app_name = domain_to_app_name(domain)
864
-
952
+
953
+ # Check if this is a docker-compose app
954
+ from wasm.core.store import get_store
955
+ store = get_store()
956
+ app = store.get_app(domain)
957
+
958
+ if app and app.app_type == "docker-compose":
959
+ config = Config()
960
+ app_path = Path(app.app_path) if app.app_path else config.apps_directory / app_name
961
+ from wasm.core.utils import run_command as _run_cmd
962
+ import subprocess
963
+
964
+ # Find compose file
965
+ compose_file = None
966
+ for name in ["docker-compose.prod.yml", "docker-compose.yml", "compose.yml"]:
967
+ cf = app_path / name
968
+ if cf.exists():
969
+ compose_file = str(cf)
970
+ break
971
+
972
+ cmd = ["docker", "compose"]
973
+ if compose_file:
974
+ cmd.extend(["-f", compose_file])
975
+ cmd.extend(["logs", "--tail", str(args.lines)])
976
+
977
+ if args.follow:
978
+ cmd.append("-f")
979
+ try:
980
+ subprocess.run(cmd, cwd=app_path)
981
+ except KeyboardInterrupt:
982
+ pass
983
+ else:
984
+ result = _run_cmd(cmd, cwd=app_path)
985
+ print(result.stdout if result.success else result.stderr)
986
+ return 0
987
+
865
988
  # Get resolved service name (handles both legacy wasm-* and new format)
866
989
  service_name = service_manager._resolve_service_name(app_name)
867
990