wasm-cli 1.0.2__tar.gz → 1.1.0__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 (180) hide show
  1. {wasm_cli-1.0.2/src/wasm_cli.egg-info → wasm_cli-1.1.0}/PKG-INFO +1 -1
  2. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/pyproject.toml +1 -1
  3. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/setup.py +1 -1
  4. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/__init__.py +1 -1
  5. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/health.py +26 -35
  6. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/web.py +47 -0
  7. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/webapp.py +31 -16
  8. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/interactive.py +5 -3
  9. wasm_cli-1.1.0/src/wasm/core/app_state.py +205 -0
  10. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/logger.py +206 -33
  11. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/service_manager.py +8 -0
  12. {wasm_cli-1.0.2 → wasm_cli-1.1.0/src/wasm_cli.egg-info}/PKG-INFO +1 -1
  13. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/SOURCES.txt +1 -0
  14. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/LICENSE +0 -0
  15. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/MANIFEST.in +0 -0
  16. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/README.md +0 -0
  17. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/MONITOR.md +0 -0
  18. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/OBS_SETUP.md +0 -0
  19. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/assets/logo.png +0 -0
  20. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/assets/logo_bg.png +0 -0
  21. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/superpowers/specs/2026-08-12-wasm-panel-design-direction.md +0 -0
  22. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/docs/superpowers/specs/2026-08-12-wasm-v1-refactor-design.md +0 -0
  23. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/man/wasm.1 +0 -0
  24. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/setup.cfg +0 -0
  25. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/__main__.py +0 -0
  26. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/__init__.py +0 -0
  27. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/app.py +0 -0
  28. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/__init__.py +0 -0
  29. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/backup.py +0 -0
  30. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/cert.py +0 -0
  31. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/config.py +0 -0
  32. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/db.py +0 -0
  33. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/env.py +0 -0
  34. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/monitor.py +0 -0
  35. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/service.py +0 -0
  36. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/setup.py +0 -0
  37. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/site.py +0 -0
  38. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/store.py +0 -0
  39. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/commands/version.py +0 -0
  40. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/cli/prompts.py +0 -0
  41. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/README.md +0 -0
  42. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/_wasm +0 -0
  43. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/wasm.bash +0 -0
  44. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/completions/wasm.fish +0 -0
  45. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/__init__.py +0 -0
  46. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/config.py +0 -0
  47. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/dependencies.py +0 -0
  48. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/exceptions.py +0 -0
  49. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/fs.py +0 -0
  50. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/runner.py +0 -0
  51. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/store.py +0 -0
  52. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/update_checker.py +0 -0
  53. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/core/utils.py +0 -0
  54. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/__init__.py +0 -0
  55. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/auto.py +0 -0
  56. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/base.py +0 -0
  57. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/docker_compose.py +0 -0
  58. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/__init__.py +0 -0
  59. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/env_manager.py +0 -0
  60. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/health.py +0 -0
  61. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/nginx_config.py +0 -0
  62. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/package_manager.py +0 -0
  63. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/path_resolver.py +0 -0
  64. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/preflight.py +0 -0
  65. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/prisma.py +0 -0
  66. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/registration.py +0 -0
  67. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/summary.py +0 -0
  68. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/turbo.py +0 -0
  69. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/helpers/workspace.py +0 -0
  70. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/interface.py +0 -0
  71. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/monorepo.py +0 -0
  72. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/nextjs.py +0 -0
  73. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/nodejs.py +0 -0
  74. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/pipeline.py +0 -0
  75. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/python.py +0 -0
  76. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/registry.py +0 -0
  77. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/static.py +0 -0
  78. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/deployers/vite.py +0 -0
  79. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/main.py +0 -0
  80. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/__init__.py +0 -0
  81. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/apache_manager.py +0 -0
  82. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/backup_manager.py +0 -0
  83. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/backup_scheduler.py +0 -0
  84. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/base_manager.py +0 -0
  85. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/cert_manager.py +0 -0
  86. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/__init__.py +0 -0
  87. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/base.py +0 -0
  88. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/mongodb.py +0 -0
  89. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/mysql.py +0 -0
  90. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/postgres.py +0 -0
  91. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/redis.py +0 -0
  92. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/database/registry.py +0 -0
  93. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/nginx_manager.py +0 -0
  94. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/source_manager.py +0 -0
  95. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/managers/webserver.py +0 -0
  96. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/__init__.py +0 -0
  97. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/email_notifier.py +0 -0
  98. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/metrics.py +0 -0
  99. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/models.py +0 -0
  100. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/observation_store.py +0 -0
  101. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/process_monitor.py +0 -0
  102. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/monitor/signals.py +0 -0
  103. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/__init__.py +0 -0
  104. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
  105. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/apache/static.conf.j2 +0 -0
  106. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/advanced.conf.j2 +0 -0
  107. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
  108. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
  109. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/nginx/static.conf.j2 +0 -0
  110. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/_escape.j2 +0 -0
  111. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/app.service.j2 +0 -0
  112. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/backup-service.j2 +0 -0
  113. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/backup-timer.j2 +0 -0
  114. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/templates/systemd/docker-compose.service.j2 +0 -0
  115. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/__init__.py +0 -0
  116. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/domain.py +0 -0
  117. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/environment.py +0 -0
  118. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/names.py +0 -0
  119. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/port.py +0 -0
  120. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/source.py +0 -0
  121. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/validators/ssh.py +0 -0
  122. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/__init__.py +0 -0
  123. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/__init__.py +0 -0
  124. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/apps.py +0 -0
  125. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/auth.py +0 -0
  126. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/backups.py +0 -0
  127. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/certs.py +0 -0
  128. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/config.py +0 -0
  129. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/databases.py +0 -0
  130. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/deps.py +0 -0
  131. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/jobs.py +0 -0
  132. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/monitor.py +0 -0
  133. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/router.py +0 -0
  134. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/services.py +0 -0
  135. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/sites.py +0 -0
  136. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/api/system.py +0 -0
  137. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/auth.py +0 -0
  138. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/jobs.py +0 -0
  139. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/server.py +0 -0
  140. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/app.css +0 -0
  141. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/logo.png +0 -0
  142. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/logo.webp +0 -0
  143. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/panel.js +0 -0
  144. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/alpine.min.js +0 -0
  145. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  146. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  147. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-500-normal.woff2 +0 -0
  148. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-600-normal.woff2 +0 -0
  149. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  150. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  151. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  152. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/htmx.min.js +0 -0
  153. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/uplot.min.css +0 -0
  154. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/uplot.min.js +0 -0
  155. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/xterm.css +0 -0
  156. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/static/vendor/xterm.js +0 -0
  157. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/_macros.html +0 -0
  158. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/base.html +0 -0
  159. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/fragments/machine.html +0 -0
  160. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/login.html +0 -0
  161. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/activity.html +0 -0
  162. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/app.html +0 -0
  163. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/backups.html +0 -0
  164. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/certificates.html +0 -0
  165. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/dashboard.html +0 -0
  166. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/missing.html +0 -0
  167. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/resources.html +0 -0
  168. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/templates/pages/settings.html +0 -0
  169. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/__init__.py +0 -0
  170. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/context.py +0 -0
  171. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/machine.py +0 -0
  172. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/rendering.py +0 -0
  173. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/resources.py +0 -0
  174. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/views/router.py +0 -0
  175. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/websockets/__init__.py +0 -0
  176. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm/web/websockets/router.py +0 -0
  177. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
  178. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/entry_points.txt +0 -0
  179. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/src/wasm_cli.egg-info/requires.txt +0 -0
  180. {wasm_cli-1.0.2 → wasm_cli-1.1.0}/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: 1.0.2
3
+ Version: 1.1.0
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 = "1.0.2"
7
+ version = "1.1.0"
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="1.0.2",
12
+ version="1.1.0",
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",
@@ -15,7 +15,7 @@ from importlib.metadata import version as _installed_version
15
15
  #: as an OBS build directory. The single source of truth is the ``version``
16
16
  #: field of pyproject.toml; ``scripts/release.py`` keeps this literal and the
17
17
  #: distribution packaging files in step with it.
18
- _FALLBACK_VERSION = "1.0.2"
18
+ _FALLBACK_VERSION = "1.1.0"
19
19
 
20
20
  try:
21
21
  __version__ = _installed_version("wasm-cli")
@@ -28,11 +28,11 @@ from datetime import datetime
28
28
  import click
29
29
 
30
30
  from wasm.cli.app import Context, pass_context
31
+ from wasm.core.app_state import RUNNING, STATIC, resolve_states
31
32
  from wasm.core.config import Config
32
- from wasm.core.exceptions import ServiceError, WASMError
33
+ from wasm.core.exceptions import WASMError
33
34
  from wasm.core.logger import Logger
34
35
  from wasm.core.store import get_store
35
- from wasm.core.utils import domain_to_app_name
36
36
  from wasm.managers.apache_manager import ApacheManager
37
37
  from wasm.managers.cert_manager import CertificateInfo, CertManager
38
38
  from wasm.managers.nginx_manager import NginxManager
@@ -50,21 +50,14 @@ def _print_status(logger: Logger, key: str, value: str, status: str) -> None:
50
50
  Print a key-value pair with status indicator.
51
51
 
52
52
  Args:
53
- logger: Logger of the current command, kept for signature stability.
53
+ logger: Logger of the current command. Writing through it is what makes
54
+ ``wasm --no-color health`` colourless; the escape codes used to be
55
+ written to stdout directly, so the flag did nothing here.
54
56
  key: Name of the checked item.
55
57
  value: Human readable result.
56
58
  status: One of "ok", "warning", "error" or "info".
57
59
  """
58
- if status == "ok":
59
- indicator = "\033[32m[OK]\033[0m"
60
- elif status == "warning":
61
- indicator = "\033[33m[!]\033[0m"
62
- elif status == "error":
63
- indicator = "\033[31m[X]\033[0m"
64
- else: # info
65
- indicator = "\033[34m[i]\033[0m"
66
-
67
- print(f" {indicator} {key}: {value}")
60
+ logger.check(key, value, status)
68
61
 
69
62
 
70
63
  def _days_until(expiry: str) -> int | None:
@@ -222,37 +215,35 @@ def run_health_check(verbose: bool = False) -> int:
222
215
  service_manager = ServiceManager(verbose=verbose)
223
216
 
224
217
  apps = store.list_apps()
225
- apps_running = 0
226
- apps_stopped = 0
227
- apps_failed = 0
228
-
229
- for app in apps:
230
- try:
231
- # The unit was created from domain_to_app_name(); a hand-rolled
232
- # replace() disagrees with it for any domain that is not lowercase.
233
- status = service_manager.get_status(domain_to_app_name(app.domain))
234
- except ServiceError as e:
235
- apps_failed += 1
236
- warnings.append(f"App '{app.domain}' could not be queried: {e}")
237
- continue
238
-
239
- if status.get("active"):
240
- apps_running += 1
241
- else:
242
- apps_stopped += 1
243
- warnings.append(f"App '{app.domain}' is not running")
218
+
219
+ # The same resolver `wasm list` uses. When these two commands each decided
220
+ # for themselves what "running" meant, list reported fifteen applications
221
+ # running while this reported seven stopped, and five of the seven were
222
+ # static sites that have no service to run in the first place.
223
+ states = resolve_states(apps, service_manager)
224
+
225
+ apps_running = sum(1 for s in states.values() if s.label == RUNNING)
226
+ apps_static = sum(1 for s in states.values() if s.label == STATIC)
227
+ unhealthy = [(domain, s) for domain, s in states.items() if not s.healthy]
228
+
229
+ for domain, current in unhealthy:
230
+ warnings.append(f"App '{domain}' - {current.detail or current.label.lower()}")
244
231
 
245
232
  total_apps = len(apps)
233
+ served = apps_running + apps_static
246
234
  if total_apps > 0:
247
- if apps_stopped > 0 or apps_failed > 0:
235
+ summary = f"{served}/{total_apps} serving"
236
+ if apps_static:
237
+ summary += f" ({apps_static} static)"
238
+ if unhealthy:
248
239
  _print_status(
249
240
  logger,
250
241
  "Applications",
251
- f"{apps_running}/{total_apps} running, {apps_stopped} stopped",
242
+ f"{summary}, {len(unhealthy)} need attention",
252
243
  "warning",
253
244
  )
254
245
  else:
255
- _print_status(logger, "Applications", f"{apps_running}/{total_apps} running", "ok")
246
+ _print_status(logger, "Applications", summary, "ok")
256
247
  else:
257
248
  _print_status(logger, "Applications", "No applications deployed", "info")
258
249
 
@@ -708,6 +708,38 @@ def _prompt_install(missing_apt: list[str], missing_pip: list[str], verbose: boo
708
708
  # ---------------------------------------------------------------------------
709
709
 
710
710
 
711
+ def _port_in_use(host: str, port: int) -> bool:
712
+ """
713
+ Ask whether the panel's address is already taken.
714
+
715
+ Binding is the only reliable answer: a connection attempt says nothing
716
+ about a socket bound with no backlog, and reading ``ss`` output means
717
+ parsing a different format on every distribution.
718
+
719
+ Args:
720
+ host: Address the panel would bind.
721
+ port: Port the panel would bind.
722
+
723
+ Returns:
724
+ True when binding would fail because something holds the address.
725
+ """
726
+ family = socket.AF_INET6 if ":" in _strip_brackets(host) else socket.AF_INET
727
+ probe = socket.socket(family, socket.SOCK_STREAM)
728
+ try:
729
+ # Deliberately not SO_REUSEADDR: with it the bind succeeds against a
730
+ # socket in TIME_WAIT, which is the case this is meant to catch.
731
+ probe.bind((_strip_brackets(host), port))
732
+ except OSError:
733
+ return True
734
+ except (ValueError, socket.gaierror):
735
+ # An address this process cannot even parse is not this check's problem;
736
+ # the bind that follows will report it properly.
737
+ return False
738
+ finally:
739
+ probe.close()
740
+ return False
741
+
742
+
711
743
  def _start(options: StartOptions, verbose: bool, *, dry_run: bool = False) -> int:
712
744
  """
713
745
  Start the panel, refusing an unsafe exposure first.
@@ -773,6 +805,21 @@ def _start(options: StartOptions, verbose: bool, *, dry_run: bool = False) -> in
773
805
  # Process not running, remove stale PID file
774
806
  get_fs().remove(pid_file, missing_ok=True)
775
807
 
808
+ # The PID file only catches a panel this machine still has a record of. A
809
+ # panel whose PID file was removed, or anything else on the port, walked
810
+ # past that check: the token was printed and then uvicorn failed with a
811
+ # bare OSError, leaving an operator holding a credential for a server that
812
+ # never started.
813
+ if not dry_run and _port_in_use(host, config.port):
814
+ logger.error(f"Something is already listening on {host}:{config.port}")
815
+ logger.info("If it is a panel this machine has forgotten about:")
816
+ logger.info(" wasm web stop")
817
+ logger.info("To see what holds the port:")
818
+ logger.info(f" ss -ltnp 'sport = :{config.port}'")
819
+ logger.info("Or serve somewhere else:")
820
+ logger.info(f" wasm web start --port {config.port + 1}")
821
+ return 1
822
+
776
823
  if config.ip_whitelist:
777
824
  logger.info(f"Only these clients may connect: {', '.join(config.ip_whitelist)}")
778
825
  if config.trusted_proxies:
@@ -33,15 +33,16 @@ from typing import Any, TypeVar
33
33
  import click
34
34
 
35
35
  from wasm.cli.app import Context, enable_dry_run, pass_context
36
+ from wasm.core.app_state import RUNNING, STATIC, resolve_states
36
37
  from wasm.core.config import Config
37
38
  from wasm.core.dependencies import check_deployment_ready
38
39
  from wasm.core.exceptions import DeploymentError, ServiceError, WASMError
39
- from wasm.core.logger import Logger, set_colors_disabled
40
+ from wasm.core.logger import Logger, set_colors_disabled, state, styled
40
41
  from wasm.core.runner import (
41
42
  CommandResult,
42
43
  get_runner,
43
44
  )
44
- from wasm.core.store import AppStatus, get_store
45
+ from wasm.core.store import get_store
45
46
  from wasm.core.utils import domain_to_app_name, remove_directory
46
47
  from wasm.deployers import detect_app_type, get_deployer
47
48
  from wasm.deployers.docker_compose import DockerComposeDeployer
@@ -464,30 +465,44 @@ def _list_apps(logger: Logger) -> int:
464
465
  logger.info(" wasm deploy -d example.com -s https://github.com/user/repo")
465
466
  return 0
466
467
 
468
+ # Asked of systemd and of the port, not read from the status column. That
469
+ # column is written at deploy time and never again, so it called everything
470
+ # Running while health, which did ask, reported half of them stopped.
471
+ states = resolve_states(apps, ServiceManager(verbose=logger.verbose))
472
+
467
473
  headers = ["Domain", "Type", "Status", "Port", "SSL"]
468
474
  rows = []
469
475
 
470
- labels = {
471
- AppStatus.RUNNING.value: "Running",
472
- AppStatus.STOPPED.value: "Stopped",
473
- AppStatus.DEPLOYING.value: "Deploying",
474
- AppStatus.FAILED.value: "Failed",
475
- }
476
-
477
476
  for app in apps:
478
- status_str = labels.get(app.status, "Unknown")
479
- port_str = str(app.port) if app.port else "static"
480
- ssl_str = "yes" if app.ssl_enabled else "no"
477
+ current = states[app.domain]
478
+ rows.append(
479
+ [
480
+ styled(app.domain, "bold"),
481
+ app.app_type,
482
+ state(current.label),
483
+ styled(app.port, "") if app.port else state("static"),
484
+ state("yes") if app.ssl_enabled else state("no"),
485
+ ]
486
+ )
481
487
 
482
- rows.append([app.domain, app.app_type, status_str, port_str, ssl_str])
488
+ logger.table(headers, rows, justify=["left", "left", "left", "right", "left"])
483
489
 
484
- logger.table(headers, rows)
490
+ running = sum(1 for s in states.values() if s.label == RUNNING)
491
+ static = sum(1 for s in states.values() if s.label == STATIC)
492
+ unhealthy = [(domain, s) for domain, s in states.items() if not s.healthy]
485
493
 
486
494
  logger.blank()
487
- running = sum(1 for a in apps if a.status == AppStatus.RUNNING.value)
488
- static = sum(1 for a in apps if a.is_static)
489
495
  logger.info(f"Total: {len(apps)} apps ({running} running, {static} static)")
490
496
 
497
+ # The reason belongs next to the list. Making the operator run a second
498
+ # command to find out why something says Stopped is how the contradiction
499
+ # between these two commands went unnoticed for as long as it did.
500
+ if unhealthy:
501
+ logger.blank()
502
+ logger.warning(f"{len(unhealthy)} need attention:")
503
+ for domain, current in unhealthy:
504
+ logger.list_item(f"{domain} - {current.detail or current.label.lower()}")
505
+
491
506
  return 0
492
507
 
493
508
 
@@ -34,9 +34,11 @@ class InteractiveMode:
34
34
  raise WASMError(
35
35
  "Interactive mode needs questionary, which is missing",
36
36
  details=(
37
- "It is a hard dependency, so this means a broken install.\n"
38
- " pip install --force-reinstall wasm-cli\n"
39
- " or, on a distribution package: apt install python3-questionary"
37
+ "Every other command works without it. To get it:\n"
38
+ " apt install python3-questionary Debian 13, Ubuntu 24.04+\n"
39
+ " dnf install python3-questionary Fedora 42+\n"
40
+ " pip install questionary anywhere else\n"
41
+ "It is not packaged for Debian 12, Ubuntu 22.04 or Leap 15."
40
42
  ),
41
43
  )
42
44
 
@@ -0,0 +1,205 @@
1
+ # Copyright (c) 2024-2026 Yago Lopez Prado
2
+ # Licensed under WASM-NCSAL 1.0 (Commercial use prohibited)
3
+ # https://github.com/Perkybeet/wasm/blob/main/LICENSE
4
+
5
+ """
6
+ One answer to the question "is this application actually working".
7
+
8
+ ``wasm list`` printed the status column from the database and ``wasm health``
9
+ asked systemd, so on the same machine at the same moment list called fifteen
10
+ applications Running while health reported seven of them stopped. Nothing ever
11
+ wrote to that column after a deploy, so list was reading a value that had been
12
+ true once.
13
+
14
+ Asking systemd instead is necessary but not sufficient. A unit is active while
15
+ the process exists, which includes a service that accepts no connections and
16
+ one systemd is restarting every few seconds because it crashes on startup. Both
17
+ report ``active`` if you catch them between restarts. So the state here is
18
+ decided from four signals:
19
+
20
+ - what systemd says the unit is doing (ActiveState and SubState),
21
+ - how many times it has been restarted (NRestarts),
22
+ - whether anything answers on the port the application is supposed to serve,
23
+ - and whether the application is static, in which case there is no unit at all
24
+ and the web server serves the files directly.
25
+
26
+ The last one is why health warned about five sites that were fine: a static
27
+ site has no systemd unit, so querying one always says "not running".
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import socket
33
+ from concurrent.futures import ThreadPoolExecutor
34
+ from dataclasses import dataclass
35
+ from typing import TYPE_CHECKING, Any
36
+
37
+ from wasm.core.exceptions import ValidationError, WASMError
38
+ from wasm.core.utils import domain_to_app_name
39
+
40
+ if TYPE_CHECKING: # pragma: no cover - imported for types only
41
+ from wasm.core.store import App
42
+ from wasm.managers.service_manager import ServiceManager
43
+
44
+ #: How long to wait for a port to accept a connection. The probe runs against
45
+ #: loopback, where anything listening answers immediately; a longer wait would
46
+ #: only make `wasm list` slower on the applications that are already broken.
47
+ PROBE_TIMEOUT = 0.4
48
+
49
+ #: Labels. These are what the Status column shows, and core.logger.STATE_STYLES
50
+ #: gives each of them its colour.
51
+ RUNNING = "Running"
52
+ RESTARTING = "Restarting"
53
+ NOT_RESPONDING = "No answer"
54
+ STOPPED = "Stopped"
55
+ FAILED = "Failed"
56
+ STATIC = "Static"
57
+ UNKNOWN = "Unknown"
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class AppState:
62
+ """
63
+ What is true about one application right now.
64
+
65
+ Attributes:
66
+ label: One word for the Status column.
67
+ healthy: Whether this needs an operator's attention. Static sites are
68
+ healthy: there is nothing to run.
69
+ detail: Why, in a sentence, when the answer is not simply "it works".
70
+ Empty when there is nothing to explain.
71
+ """
72
+
73
+ label: str
74
+ healthy: bool
75
+ detail: str = ""
76
+
77
+
78
+ def port_answers(port: int, host: str = "127.0.0.1", timeout: float = PROBE_TIMEOUT) -> bool:
79
+ """
80
+ Ask whether anything accepts a connection on a port.
81
+
82
+ Args:
83
+ port: The port the application should be serving.
84
+ host: Where to look, loopback by default.
85
+ timeout: Seconds to wait.
86
+
87
+ Returns:
88
+ True when the connection is accepted.
89
+ """
90
+ try:
91
+ with socket.create_connection((host, port), timeout=timeout):
92
+ return True
93
+ except (OSError, ValueError):
94
+ return False
95
+
96
+
97
+ def _restart_count(status: dict[str, Any]) -> int:
98
+ """
99
+ Read NRestarts out of a status dictionary.
100
+
101
+ Args:
102
+ status: What ServiceManager.get_status returned.
103
+
104
+ Returns:
105
+ The restart count, and 0 when systemd did not report one.
106
+ """
107
+ try:
108
+ return int(str(status.get("restarts", "0")).strip() or 0)
109
+ except ValueError:
110
+ return 0
111
+
112
+
113
+ def resolve_state(
114
+ app: App,
115
+ service_manager: ServiceManager,
116
+ *,
117
+ probe: bool = True,
118
+ ) -> AppState:
119
+ """
120
+ Work out what is true about one application.
121
+
122
+ Args:
123
+ app: The application record.
124
+ service_manager: Used to ask systemd about the unit.
125
+ probe: Whether to check that the port answers. Turn it off where the
126
+ extra connection is not wanted; the systemd signals still apply.
127
+
128
+ Returns:
129
+ The state, ready to be shown or counted.
130
+ """
131
+ if app.is_static:
132
+ return AppState(STATIC, healthy=True, detail="served directly by the web server")
133
+
134
+ try:
135
+ status = service_manager.get_status(domain_to_app_name(app.domain))
136
+ except (WASMError, ValidationError) as error:
137
+ return AppState(UNKNOWN, healthy=False, detail=str(error))
138
+
139
+ active_state = str(status.get("active_state", "")).strip()
140
+ sub_state = str(status.get("sub_state", "")).strip()
141
+ restarts = _restart_count(status)
142
+
143
+ if not status.get("exists", True) and not status.get("active"):
144
+ return AppState(STOPPED, healthy=False, detail="there is no systemd unit for it")
145
+
146
+ if active_state == "failed" or sub_state == "failed":
147
+ reason = str(status.get("result", "")).strip()
148
+ detail = f"systemd gave up on it ({reason})" if reason else "systemd gave up on it"
149
+ return AppState(FAILED, healthy=False, detail=detail)
150
+
151
+ # A crash loop: systemd keeps starting it and it keeps dying. Between two
152
+ # restarts the unit reads as active, which is how this got reported as
153
+ # Running for as long as it did.
154
+ if sub_state == "auto-restart" or active_state == "activating":
155
+ detail = f"restarted {restarts} times; check the logs with wasm service logs {app.domain}"
156
+ return AppState(RESTARTING, healthy=False, detail=detail)
157
+
158
+ if not status.get("active"):
159
+ return AppState(STOPPED, healthy=False, detail="the unit is not running")
160
+
161
+ # systemd is satisfied. That only means a process exists, so ask the
162
+ # application itself.
163
+ if probe and app.port and not port_answers(app.port):
164
+ return AppState(
165
+ NOT_RESPONDING,
166
+ healthy=False,
167
+ detail=f"the unit is up but nothing accepts connections on port {app.port}",
168
+ )
169
+
170
+ if restarts:
171
+ return AppState(RUNNING, healthy=True, detail=f"answering, after {restarts} restarts")
172
+
173
+ return AppState(RUNNING, healthy=True)
174
+
175
+
176
+ def resolve_states(
177
+ apps: list[App],
178
+ service_manager: ServiceManager,
179
+ *,
180
+ probe: bool = True,
181
+ ) -> dict[str, AppState]:
182
+ """
183
+ Resolve several applications at once.
184
+
185
+ Each one costs three systemctl calls and possibly a connection attempt, so
186
+ fifteen applications in sequence is a visible pause before `wasm list`
187
+ prints anything. They are independent, so they run together.
188
+
189
+ Args:
190
+ apps: The applications to resolve.
191
+ service_manager: Used to ask systemd about the units.
192
+ probe: Whether to check that the ports answer.
193
+
194
+ Returns:
195
+ The state of each application, keyed by domain.
196
+ """
197
+ if not apps:
198
+ return {}
199
+
200
+ def one(app: App) -> tuple[str, AppState]:
201
+ return app.domain, resolve_state(app, service_manager, probe=probe)
202
+
203
+ workers = min(8, len(apps))
204
+ with ThreadPoolExecutor(max_workers=workers) as pool:
205
+ return dict(pool.map(one, apps))