wasm-cli 0.15.2__tar.gz → 0.15.4__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.15.2/src/wasm_cli.egg-info → wasm_cli-0.15.4}/PKG-INFO +1 -1
  2. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/pyproject.toml +1 -1
  3. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/setup.py +1 -1
  4. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/__init__.py +1 -1
  5. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/cert.py +1 -0
  6. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/webapp.py +6 -0
  7. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/interactive.py +105 -7
  8. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/parser.py +11 -1
  9. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/base.py +27 -7
  10. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/docker_compose.py +49 -26
  11. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/env_manager.py +1 -1
  12. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/cert_manager.py +13 -5
  13. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/service_manager.py +12 -9
  14. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/apache/proxy.conf.j2 +6 -0
  15. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/apache/static.conf.j2 +6 -0
  16. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/nginx/advanced.conf.j2 +2 -2
  17. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/nginx/proxy.conf.j2 +2 -2
  18. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/nginx/static.conf.j2 +2 -2
  19. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/validators/domain.py +22 -0
  20. {wasm_cli-0.15.2 → wasm_cli-0.15.4/src/wasm_cli.egg-info}/PKG-INFO +1 -1
  21. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/LICENSE +0 -0
  22. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/MANIFEST.in +0 -0
  23. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/README.md +0 -0
  24. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/docs/MONITOR.md +0 -0
  25. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/docs/OBS_SETUP.md +0 -0
  26. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/docs/assets/logo.png +0 -0
  27. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/docs/assets/logo_bg.png +0 -0
  28. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/man/wasm.1 +0 -0
  29. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/setup.cfg +0 -0
  30. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/__main__.py +0 -0
  31. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/__init__.py +0 -0
  32. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/__init__.py +0 -0
  33. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/backup.py +0 -0
  34. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/config.py +0 -0
  35. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/db.py +0 -0
  36. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/env.py +0 -0
  37. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/health.py +0 -0
  38. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/monitor.py +0 -0
  39. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/service.py +0 -0
  40. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/setup.py +0 -0
  41. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/site.py +0 -0
  42. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/store.py +0 -0
  43. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/version.py +0 -0
  44. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/cli/commands/web.py +0 -0
  45. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/completions/__init__.py +0 -0
  46. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/completions/_wasm +0 -0
  47. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/completions/wasm.bash +0 -0
  48. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/completions/wasm.fish +0 -0
  49. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/__init__.py +0 -0
  50. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/config.py +0 -0
  51. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/dependencies.py +0 -0
  52. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/exceptions.py +0 -0
  53. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/logger.py +0 -0
  54. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/store.py +0 -0
  55. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/update_checker.py +0 -0
  56. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/core/utils.py +0 -0
  57. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/__init__.py +0 -0
  58. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/__init__.py +0 -0
  59. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/nginx_config.py +0 -0
  60. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/package_manager.py +0 -0
  61. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/path_resolver.py +0 -0
  62. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/prisma.py +0 -0
  63. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/turbo.py +0 -0
  64. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/helpers/workspace.py +0 -0
  65. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/monorepo.py +0 -0
  66. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/nextjs.py +0 -0
  67. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/nodejs.py +0 -0
  68. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/python.py +0 -0
  69. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/registry.py +0 -0
  70. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/static.py +0 -0
  71. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/deployers/vite.py +0 -0
  72. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/main.py +0 -0
  73. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/__init__.py +0 -0
  74. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/apache_manager.py +0 -0
  75. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/backup_manager.py +0 -0
  76. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/backup_scheduler.py +0 -0
  77. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/base_manager.py +0 -0
  78. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/database/__init__.py +0 -0
  79. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/database/base.py +0 -0
  80. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/database/mongodb.py +0 -0
  81. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/database/mysql.py +0 -0
  82. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/database/postgres.py +0 -0
  83. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/database/redis.py +0 -0
  84. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/database/registry.py +0 -0
  85. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/nginx_manager.py +0 -0
  86. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/managers/source_manager.py +0 -0
  87. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/monitor/__init__.py +0 -0
  88. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/monitor/ai_analyzer.py +0 -0
  89. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/monitor/email_notifier.py +0 -0
  90. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/monitor/process_monitor.py +0 -0
  91. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/monitor/threat_store.py +0 -0
  92. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/__init__.py +0 -0
  93. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
  94. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/systemd/app.service.j2 +0 -0
  95. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/systemd/backup-service.j2 +0 -0
  96. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/systemd/backup-timer.j2 +0 -0
  97. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/templates/systemd/docker-compose.service.j2 +0 -0
  98. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/validators/__init__.py +0 -0
  99. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/validators/port.py +0 -0
  100. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/validators/source.py +0 -0
  101. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/validators/ssh.py +0 -0
  102. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/__init__.py +0 -0
  103. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/__init__.py +0 -0
  104. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/apps.py +0 -0
  105. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/auth.py +0 -0
  106. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/backups.py +0 -0
  107. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/certs.py +0 -0
  108. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/config.py +0 -0
  109. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/databases.py +0 -0
  110. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/jobs.py +0 -0
  111. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/monitor.py +0 -0
  112. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/router.py +0 -0
  113. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/services.py +0 -0
  114. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/sites.py +0 -0
  115. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/api/system.py +0 -0
  116. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/auth.py +0 -0
  117. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/jobs.py +0 -0
  118. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/server.py +0 -0
  119. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/css/main.css +0 -0
  120. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/index.html +0 -0
  121. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/components/cards.js +0 -0
  122. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/components/jobs.js +0 -0
  123. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/components/metrics.js +0 -0
  124. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/components/skeleton.js +0 -0
  125. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/api.js +0 -0
  126. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/dialogs.js +0 -0
  127. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/notifications.js +0 -0
  128. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/router.js +0 -0
  129. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/search.js +0 -0
  130. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/shortcuts.js +0 -0
  131. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/theme.js +0 -0
  132. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/ui.js +0 -0
  133. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/core/websocket.js +0 -0
  134. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/main.js +0 -0
  135. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/apps.js +0 -0
  136. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/backups.js +0 -0
  137. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/certs.js +0 -0
  138. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/config.js +0 -0
  139. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/dashboard.js +0 -0
  140. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/databases.js +0 -0
  141. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/jobs.js +0 -0
  142. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/logs.js +0 -0
  143. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/monitor.js +0 -0
  144. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/services.js +0 -0
  145. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/js/pages/sites.js +0 -0
  146. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/login.html +0 -0
  147. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/logo.png +0 -0
  148. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/static/logo.webp +0 -0
  149. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/websockets/__init__.py +0 -0
  150. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm/web/websockets/router.py +0 -0
  151. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm_cli.egg-info/SOURCES.txt +0 -0
  152. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
  153. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm_cli.egg-info/entry_points.txt +0 -0
  154. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/src/wasm_cli.egg-info/requires.txt +0 -0
  155. {wasm_cli-0.15.2 → wasm_cli-0.15.4}/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.15.2
3
+ Version: 0.15.4
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.15.2"
7
+ version = "0.15.4"
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.15.2",
12
+ version="0.15.4",
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",
@@ -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.15.2"
11
+ __version__ = "0.15.4"
12
12
  __author__ = "Yago López Prado"
13
13
  __license__ = "WASM-NCSAL"
14
14
 
@@ -85,6 +85,7 @@ def _handle_create(args: Namespace) -> int:
85
85
  apache=args.apache,
86
86
  dry_run=args.dry_run,
87
87
  additional_domains=additional_domains,
88
+ expand=getattr(args, "expand", False),
88
89
  )
89
90
 
90
91
  if args.dry_run:
@@ -201,6 +201,11 @@ def _handle_create(args: Namespace) -> int:
201
201
  logger.key_value("Port", str(port))
202
202
  logger.key_value("Package Manager", package_manager)
203
203
  logger.key_value("SSL", "Yes" if not args.no_ssl else "No")
204
+ include_www = getattr(args, "www", False)
205
+ if not args.no_ssl and include_www:
206
+ from wasm.validators.domain import should_include_www
207
+ if should_include_www(domain):
208
+ logger.key_value("WWW", f"www.{domain} included")
204
209
  logger.blank()
205
210
 
206
211
  # Handle monorepo deployments specially
@@ -224,6 +229,7 @@ def _handle_create(args: Namespace) -> int:
224
229
  branch=args.branch,
225
230
  env_vars=env_vars,
226
231
  package_manager=package_manager,
232
+ include_www=getattr(args, "www", False),
227
233
  )
228
234
 
229
235
  # Run deployment
@@ -123,13 +123,16 @@ class InteractiveMode:
123
123
  return self._run_webapp_command("list")
124
124
  elif action == "logs":
125
125
  domain = self._prompt_domain("Enter application domain")
126
- return self._run_webapp_command("logs", domain)
127
- elif action in ["status", "update", "restart", "stop", "start"]:
126
+ return self._webapp_logs(domain)
127
+ elif action == "update":
128
+ domain = self._prompt_domain("Enter application domain")
129
+ return self._webapp_update(domain)
130
+ elif action in ["status", "restart", "stop", "start"]:
128
131
  domain = self._prompt_domain("Enter application domain")
129
132
  return self._run_webapp_command(action, domain)
130
133
  elif action == "delete":
131
134
  domain = self._prompt_domain("Enter application domain")
132
- return self._run_webapp_command("delete", domain, force=True)
135
+ return self._run_webapp_command("delete", domain, force=True, keep_files=False)
133
136
 
134
137
  return 0
135
138
 
@@ -216,7 +219,21 @@ class InteractiveMode:
216
219
  answers = inquirer.prompt(questions, theme=GreenPassion())
217
220
  if not answers:
218
221
  return 0
219
-
222
+
223
+ # Ask about www if SSL enabled and domain is a base domain
224
+ from wasm.validators.domain import should_include_www
225
+ include_www = False
226
+ if answers["ssl"] and should_include_www(answers["domain"]):
227
+ www_questions = [
228
+ inquirer.Confirm(
229
+ "include_www",
230
+ message=f"Include www.{answers['domain']} in certificate and web server config?",
231
+ default=True,
232
+ ),
233
+ ]
234
+ www_answers = inquirer.prompt(www_questions, theme=GreenPassion())
235
+ include_www = www_answers.get("include_www", True) if www_answers else False
236
+
220
237
  # Build arguments
221
238
  from argparse import Namespace
222
239
  args = Namespace(
@@ -230,11 +247,59 @@ class InteractiveMode:
230
247
  branch=answers["branch"] or None,
231
248
  no_ssl=not answers["ssl"],
232
249
  env_file=answers["env_file"] or None,
250
+ www=include_www,
233
251
  )
234
-
252
+
235
253
  from wasm.cli.commands.webapp import handle_webapp
236
254
  return handle_webapp(args)
237
255
 
256
+ def _webapp_logs(self, domain: str) -> int:
257
+ """Handle webapp logs flow with interactive prompts."""
258
+ questions = [
259
+ inquirer.Text(
260
+ "lines",
261
+ message="Number of log lines to show",
262
+ default="50",
263
+ validate=lambda _, x: x.isdigit() and int(x) > 0 or "Must be a positive number",
264
+ ),
265
+ inquirer.Confirm(
266
+ "follow",
267
+ message="Follow log output in real time?",
268
+ default=False,
269
+ ),
270
+ ]
271
+
272
+ answers = inquirer.prompt(questions, theme=GreenPassion())
273
+ if not answers:
274
+ return 0
275
+
276
+ return self._run_webapp_command(
277
+ "logs",
278
+ domain,
279
+ lines=int(answers["lines"]),
280
+ follow=answers["follow"],
281
+ )
282
+
283
+ def _webapp_update(self, domain: str) -> int:
284
+ """Handle webapp update flow with interactive prompts."""
285
+ questions = [
286
+ inquirer.Text(
287
+ "branch",
288
+ message="Git branch to update from (leave empty for default)",
289
+ default="",
290
+ ),
291
+ ]
292
+
293
+ answers = inquirer.prompt(questions, theme=GreenPassion())
294
+ if not answers:
295
+ return 0
296
+
297
+ return self._run_webapp_command(
298
+ "update",
299
+ domain,
300
+ branch=answers["branch"] or None,
301
+ )
302
+
238
303
  def _site_flow(self) -> int:
239
304
  """Handle site interactive flow."""
240
305
  questions = [
@@ -348,7 +413,10 @@ class InteractiveMode:
348
413
  return self._service_create()
349
414
  elif action == "list":
350
415
  return self._run_command("service", "list")
351
- elif action in ["status", "start", "stop", "restart", "logs", "delete"]:
416
+ elif action == "logs":
417
+ name = self._prompt_text("Enter service name")
418
+ return self._service_logs(name)
419
+ elif action in ["status", "start", "stop", "restart", "delete"]:
352
420
  name = self._prompt_text("Enter service name")
353
421
  if action == "delete":
354
422
  return self._run_command("service", action, name, force=True)
@@ -395,7 +463,7 @@ class InteractiveMode:
395
463
  verbose=self.verbose,
396
464
  action="create",
397
465
  name=answers["name"],
398
- command=answers["command"],
466
+ exec_command=answers["command"],
399
467
  directory=answers["directory"],
400
468
  user=answers["user"],
401
469
  description=answers["description"] or None,
@@ -404,6 +472,34 @@ class InteractiveMode:
404
472
  from wasm.cli.commands.service import handle_service
405
473
  return handle_service(args)
406
474
 
475
+ def _service_logs(self, name: str) -> int:
476
+ """Handle service logs flow with interactive prompts."""
477
+ questions = [
478
+ inquirer.Text(
479
+ "lines",
480
+ message="Number of log lines to show",
481
+ default="50",
482
+ validate=lambda _, x: x.isdigit() and int(x) > 0 or "Must be a positive number",
483
+ ),
484
+ inquirer.Confirm(
485
+ "follow",
486
+ message="Follow log output in real time?",
487
+ default=False,
488
+ ),
489
+ ]
490
+
491
+ answers = inquirer.prompt(questions, theme=GreenPassion())
492
+ if not answers:
493
+ return 0
494
+
495
+ return self._run_command(
496
+ "service",
497
+ "logs",
498
+ name,
499
+ lines=int(answers["lines"]),
500
+ follow=answers["follow"],
501
+ )
502
+
407
503
  def _cert_flow(self) -> int:
408
504
  """Handle cert interactive flow."""
409
505
  questions = [
@@ -437,6 +533,8 @@ class InteractiveMode:
437
533
  domain = self._prompt_domain("Enter domain name")
438
534
  if action == "delete":
439
535
  return self._run_command("cert", action, domain, force=True)
536
+ elif action == "revoke":
537
+ return self._run_command("cert", action, domain, delete=True)
440
538
  return self._run_command("cert", action, domain)
441
539
 
442
540
  return 0
@@ -206,6 +206,11 @@ def _add_webapp_commands(subparsers) -> None:
206
206
  action="store_true",
207
207
  help="Skip SSL certificate configuration",
208
208
  )
209
+ create.add_argument(
210
+ "--www",
211
+ action="store_true",
212
+ help="Include www subdomain in SSL certificate and web server config",
213
+ )
209
214
  create.add_argument(
210
215
  "--env-file",
211
216
  help="Path to environment file",
@@ -659,7 +664,12 @@ def _add_cert_parser(subparsers) -> None:
659
664
  action="store_true",
660
665
  help="Test without obtaining",
661
666
  )
662
-
667
+ create.add_argument(
668
+ "--expand",
669
+ action="store_true",
670
+ help="Expand existing certificate to include additional domains",
671
+ )
672
+
663
673
  # cert list
664
674
  cert_sub.add_parser(
665
675
  "list",
@@ -95,6 +95,7 @@ class BaseDeployer(ABC):
95
95
  self.app_name: Optional[str] = None
96
96
  self.webserver: str = "nginx"
97
97
  self.ssl: bool = True
98
+ self.include_www: bool = False
98
99
  self.branch: Optional[str] = None
99
100
  self.env_vars: Dict[str, str] = {}
100
101
 
@@ -123,10 +124,11 @@ class BaseDeployer(ABC):
123
124
  env_vars: Optional[Dict[str, str]] = None,
124
125
  app_path: Optional[Path] = None,
125
126
  package_manager: PackageManager = "auto",
127
+ include_www: bool = False,
126
128
  ) -> None:
127
129
  """
128
130
  Configure the deployer.
129
-
131
+
130
132
  Args:
131
133
  domain: Target domain.
132
134
  source: Source URL or path.
@@ -137,12 +139,16 @@ class BaseDeployer(ABC):
137
139
  env_vars: Environment variables.
138
140
  app_path: Custom application path.
139
141
  package_manager: Package manager to use (npm/pnpm/bun/auto).
142
+ include_www: Include www subdomain in certificate and web server config.
140
143
  """
144
+ from wasm.validators.domain import should_include_www
145
+
141
146
  self.domain = domain
142
147
  self.source = source
143
148
  self.port = port or self.DEFAULT_PORT
144
149
  self.webserver = webserver
145
150
  self.ssl = ssl
151
+ self.include_www = include_www and should_include_www(domain)
146
152
  self.branch = branch
147
153
  self.env_vars = env_vars or {}
148
154
  self._package_manager = package_manager
@@ -410,16 +416,23 @@ class BaseDeployer(ABC):
410
416
  Returns:
411
417
  Context dictionary.
412
418
  """
419
+ server_names = self.domain
420
+ if self.include_www:
421
+ server_names = f"{self.domain} www.{self.domain}"
422
+
413
423
  if self._nginx_advanced_config is not None:
414
- return self._nginx_config_builder.build_context(
424
+ ctx = self._nginx_config_builder.build_context(
415
425
  self._nginx_advanced_config,
416
426
  self.domain,
417
427
  ssl=self.ssl,
418
428
  app_path=str(self.app_path),
419
429
  )
430
+ ctx["server_names"] = server_names
431
+ return ctx
420
432
 
421
433
  return {
422
434
  "domain": self.domain,
435
+ "server_names": server_names,
423
436
  "port": self.port,
424
437
  "app_path": str(self.app_path),
425
438
  "app_name": self.app_name,
@@ -1025,25 +1038,32 @@ class BaseDeployer(ABC):
1025
1038
  def obtain_certificate(self) -> bool:
1026
1039
  """
1027
1040
  Obtain SSL certificate.
1028
-
1041
+
1029
1042
  Returns:
1030
1043
  True if successful.
1031
1044
  """
1032
1045
  if not self.ssl:
1033
1046
  return True
1034
-
1047
+
1035
1048
  self.logger.substep(f"Domain: {self.domain}")
1036
-
1049
+
1050
+ additional_domains = None
1051
+ if self.include_www:
1052
+ www_domain = f"www.{self.domain}"
1053
+ additional_domains = [www_domain]
1054
+ self.logger.substep(f"Including: {www_domain}")
1055
+
1037
1056
  # Use nginx plugin if using nginx
1038
1057
  nginx = self.webserver == "nginx"
1039
1058
  apache = self.webserver == "apache"
1040
-
1059
+
1041
1060
  self.cert_manager.obtain(
1042
1061
  self.domain,
1043
1062
  nginx=nginx,
1044
1063
  apache=apache,
1064
+ additional_domains=additional_domains,
1045
1065
  )
1046
-
1066
+
1047
1067
  return True
1048
1068
 
1049
1069
  def start(self) -> bool:
@@ -216,11 +216,20 @@ class DockerComposeDeployer:
216
216
  self.compose_profiles = compose_profiles or []
217
217
  self.port = port
218
218
 
219
+ def _is_headless(self) -> bool:
220
+ """
221
+ Check if the application has no web-facing services.
222
+
223
+ Returns:
224
+ True if no services expose ports (headless/worker mode).
225
+ """
226
+ return not any(svc.is_web for svc in self.services)
227
+
219
228
  def deploy(self) -> None:
220
229
  """
221
230
  Execute the full deployment workflow.
222
231
 
223
- Steps:
232
+ For web-facing apps (services with ports):
224
233
  1. Fetch source code
225
234
  2. Discover compose file
226
235
  3. Parse compose services
@@ -231,6 +240,9 @@ class DockerComposeDeployer:
231
240
  8. Create systemd service
232
241
  9. Start and verify
233
242
 
243
+ For headless/worker apps (no exposed ports):
244
+ Steps 6 and 7 are skipped automatically.
245
+
234
246
  Raises:
235
247
  DeploymentError: If any deployment step fails.
236
248
  """
@@ -246,17 +258,26 @@ class DockerComposeDeployer:
246
258
  self.logger.step(3, total_steps, "Parsing compose services")
247
259
  self._parse_compose_services()
248
260
 
261
+ headless = self._is_headless()
262
+
249
263
  self.logger.step(4, total_steps, "Configuring environment")
250
264
  self._configure_environment()
251
265
 
252
266
  self.logger.step(5, total_steps, "Building Docker images")
253
267
  self._build_images()
254
268
 
255
- self.logger.step(6, total_steps, "Creating site configuration")
256
- self._create_site()
269
+ if headless:
270
+ self.logger.step(6, total_steps, "Skipping Nginx (headless worker)")
271
+ self.logger.substep("No services expose ports, web server not needed")
272
+ self.logger.step(7, total_steps, "Skipping SSL (headless worker)")
273
+ self.logger.substep("No web-facing services, certificate not needed")
274
+ self.ssl = False
275
+ else:
276
+ self.logger.step(6, total_steps, "Creating site configuration")
277
+ self._create_site()
257
278
 
258
- self.logger.step(7, total_steps, "Obtaining SSL certificate")
259
- self._obtain_certificate()
279
+ self.logger.step(7, total_steps, "Obtaining SSL certificate")
280
+ self._obtain_certificate()
260
281
 
261
282
  self.logger.step(8, total_steps, "Creating systemd service")
262
283
  self._create_systemd_service()
@@ -273,7 +294,10 @@ class DockerComposeDeployer:
273
294
  self.logger.key_value("Path", str(self.app_path))
274
295
  self.logger.key_value("Compose File", str(self.compose_path.name))
275
296
  self.logger.key_value("Services", str(len(self.services)))
276
- self.logger.key_value("SSL", "Yes" if self.ssl else "No")
297
+ if headless:
298
+ self.logger.key_value("Mode", "Headless worker (no web server)")
299
+ else:
300
+ self.logger.key_value("SSL", "Yes" if self.ssl else "No")
277
301
 
278
302
  except Exception as e:
279
303
  self.logger.error(f"Deployment failed: {e}")
@@ -285,7 +309,7 @@ class DockerComposeDeployer:
285
309
  source_manager = SourceManager(verbose=self.verbose)
286
310
  source_manager.fetch(
287
311
  source=self.source,
288
- target=self.app_path,
312
+ destination=self.app_path,
289
313
  branch=self.branch,
290
314
  )
291
315
  self.logger.substep(f"Source fetched to {self.app_path}")
@@ -407,7 +431,7 @@ class DockerComposeDeployer:
407
431
  if no_cache:
408
432
  cmd.append("--no-cache")
409
433
 
410
- result = run_command(cmd, cwd=self.app_path, timeout=600000)
434
+ result = run_command(cmd, cwd=self.app_path, timeout=600)
411
435
  if not result.success:
412
436
  raise DockerError(
413
437
  "Failed to build Docker images",
@@ -561,14 +585,11 @@ class DockerComposeDeployer:
561
585
 
562
586
  service_manager.create_service(
563
587
  name=self.app_name,
588
+ working_directory=str(self.app_path),
589
+ description=f"Docker Compose app: {self.domain}",
590
+ environment=service_env,
564
591
  template="docker-compose",
565
- context={
566
- "name": self.app_name,
567
- "description": f"Docker Compose app: {self.domain}",
568
- "working_directory": str(self.app_path),
569
- "environment": service_env,
570
- "compose_file": compose_file_rel,
571
- },
592
+ compose_file=compose_file_rel,
572
593
  )
573
594
  self.logger.substep(f"Created systemd service: {self.app_name}")
574
595
 
@@ -638,7 +659,8 @@ class DockerComposeDeployer:
638
659
 
639
660
  def _register_app(self) -> None:
640
661
  """Register the application in the WASM store."""
641
- primary_port = self._get_primary_port()
662
+ headless = self._is_headless()
663
+ primary_port = 0 if headless else self._get_primary_port()
642
664
 
643
665
  app = App(
644
666
  domain=self.domain,
@@ -647,7 +669,7 @@ class DockerComposeDeployer:
647
669
  branch=self.branch,
648
670
  port=primary_port,
649
671
  app_path=str(self.app_path),
650
- webserver=self.webserver,
672
+ webserver="" if headless else self.webserver,
651
673
  ssl_enabled=self.ssl,
652
674
  status=AppStatus.RUNNING.value,
653
675
  env_vars=self.env_vars,
@@ -676,14 +698,15 @@ class DockerComposeDeployer:
676
698
  except Exception:
677
699
  pass
678
700
 
679
- # Remove nginx config
680
- try:
681
- nginx = NginxManager(verbose=self.verbose)
682
- if nginx.site_exists(self.domain):
683
- nginx.delete_site(self.domain)
684
- nginx.reload()
685
- except Exception:
686
- pass
701
+ # Remove nginx config (only if web-facing)
702
+ if not self._is_headless():
703
+ try:
704
+ nginx = NginxManager(verbose=self.verbose)
705
+ if nginx.site_exists(self.domain):
706
+ nginx.delete_site(self.domain)
707
+ nginx.reload()
708
+ except Exception:
709
+ pass
687
710
 
688
711
  # Remove app directory
689
712
  if self.app_path.exists():
@@ -791,7 +814,7 @@ class DockerComposeDeployer:
791
814
  cmd.extend(["-f", str(self.compose_path)])
792
815
  cmd.extend(["up", "-d", "--remove-orphans"])
793
816
 
794
- result = run_command(cmd, cwd=self.app_path, timeout=300000)
817
+ result = run_command(cmd, cwd=self.app_path, timeout=300)
795
818
  if not result.success:
796
819
  raise DockerError("Failed to update containers", result.stderr)
797
820
 
@@ -113,7 +113,7 @@ class EnvManager:
113
113
 
114
114
  # Secret detection patterns
115
115
  SECRET_PATTERNS = [
116
- "PASSWORD", "SECRET", "TOKEN", "API_KEY", "PRIVATE_KEY",
116
+ "PASSWORD", "_PASS", "SECRET", "TOKEN", "API_KEY", "PRIVATE_KEY",
117
117
  "ENCRYPTION_KEY", "SIGNING_KEY", "ACCESS_KEY", "SECRET_KEY",
118
118
  "CLIENT_SECRET", "WEBHOOK_SECRET",
119
119
  ]
@@ -155,10 +155,11 @@ class CertManager(BaseManager):
155
155
  apache: bool = False,
156
156
  dry_run: bool = False,
157
157
  additional_domains: Optional[List[str]] = None,
158
+ expand: bool = False,
158
159
  ) -> bool:
159
160
  """
160
161
  Obtain a new certificate.
161
-
162
+
162
163
  Args:
163
164
  domain: Primary domain name.
164
165
  email: Email for registration and recovery.
@@ -168,16 +169,19 @@ class CertManager(BaseManager):
168
169
  apache: Use apache plugin.
169
170
  dry_run: Test certificate issuance.
170
171
  additional_domains: Additional domains for the certificate.
171
-
172
+ expand: Expand existing certificate to include additional domains.
173
+
172
174
  Returns:
173
175
  True if certificate was obtained successfully.
174
-
176
+
175
177
  Raises:
176
178
  CertificateError: If certificate issuance fails.
177
179
  """
178
180
  if self.cert_exists(domain) and not dry_run:
179
- self.logger.warning(f"Certificate already exists for {domain}")
180
- return True
181
+ if not expand or not additional_domains:
182
+ self.logger.warning(f"Certificate already exists for {domain}")
183
+ return True
184
+ self.logger.info(f"Expanding certificate for {domain}")
181
185
 
182
186
  # Build command
183
187
  cmd = ["certbot", "certonly"]
@@ -244,6 +248,10 @@ class CertManager(BaseManager):
244
248
  for d in additional_domains:
245
249
  cmd.extend(["-d", d])
246
250
 
251
+ # Expand existing certificate
252
+ if expand and self.cert_exists(domain):
253
+ cmd.append("--expand")
254
+
247
255
  # Dry run
248
256
  if dry_run:
249
257
  cmd.append("--dry-run")
@@ -238,41 +238,43 @@ class ServiceManager(BaseManager):
238
238
  def create_service(
239
239
  self,
240
240
  name: str,
241
- command: str,
242
- working_directory: str,
241
+ command: str = "",
242
+ working_directory: str = "",
243
243
  user: Optional[str] = None,
244
244
  group: Optional[str] = None,
245
245
  environment: Optional[Dict[str, str]] = None,
246
246
  description: Optional[str] = None,
247
247
  template: str = "app",
248
+ **kwargs,
248
249
  ) -> bool:
249
250
  """
250
251
  Create a new systemd service.
251
-
252
+
252
253
  Args:
253
254
  name: Service name.
254
- command: Command to execute.
255
+ command: Command to execute (not required for docker-compose template).
255
256
  working_directory: Working directory for the service.
256
257
  user: User to run service as.
257
258
  group: Group to run service as.
258
259
  environment: Environment variables.
259
260
  description: Service description.
260
261
  template: Template name.
261
-
262
+ **kwargs: Extra context variables passed to the template.
263
+
262
264
  Returns:
263
265
  True if service was created successfully.
264
-
266
+
265
267
  Raises:
266
268
  ServiceError: If creation fails.
267
269
  """
268
270
  service_name = self._get_service_name(name)
269
-
271
+
270
272
  if self.service_exists(name):
271
273
  raise ServiceError(f"Service already exists: {service_name}")
272
-
274
+
273
275
  if not self.jinja_env:
274
276
  raise ServiceError("Template environment not initialized")
275
-
277
+
276
278
  # Prepare context
277
279
  ctx = {
278
280
  "name": service_name,
@@ -283,6 +285,7 @@ class ServiceManager(BaseManager):
283
285
  "group": group or self.config.service_group,
284
286
  "environment": environment or {},
285
287
  }
288
+ ctx.update(kwargs)
286
289
 
287
290
  # Render template
288
291
  try:
@@ -3,6 +3,9 @@
3
3
 
4
4
  <VirtualHost *:80>
5
5
  ServerName {{ domain }}
6
+ {% if server_names is defined and server_names != domain %}
7
+ ServerAlias www.{{ domain }}
8
+ {% endif %}
6
9
  ServerAdmin webmaster@{{ domain }}
7
10
 
8
11
  {% if ssl %}
@@ -14,6 +17,9 @@
14
17
 
15
18
  <VirtualHost *:443>
16
19
  ServerName {{ domain }}
20
+ {% if server_names is defined and server_names != domain %}
21
+ ServerAlias www.{{ domain }}
22
+ {% endif %}
17
23
  ServerAdmin webmaster@{{ domain }}
18
24
 
19
25
  # SSL Configuration