easyrunner-cli 0.0.8.dev128__tar.gz → 0.0.8.dev129__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 (179) hide show
  1. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/PKG-INFO +1 -1
  2. easyrunner_cli-0.0.8.dev129/easyrunner/tests/test_cli_service_integration.py +348 -0
  3. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/pyproject.toml +1 -1
  4. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/app_sub_command.py +28 -32
  5. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/servers_sub_command.py +85 -72
  6. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/README.md +0 -0
  7. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/authelia/configuration.template.yml +0 -0
  8. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/authelia/configuration.yml +0 -0
  9. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/authelia/local-dns-setup.sh +0 -0
  10. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/authelia/notification.txt +0 -0
  11. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/authelia/users_database.yaml +0 -0
  12. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/caddy/Caddyfile +0 -0
  13. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/caddy/Caddyfile with authcrunch +0 -0
  14. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/caddy/Caddyfile with ldap +0 -0
  15. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/docker-compose/.env.dev +0 -0
  16. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/docker-compose/.env.prod +0 -0
  17. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.server-config/infra/docker-compose/docker-compose-host.yaml +0 -0
  18. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.templates/docker-compose/docker-compose-app.yaml +0 -0
  19. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/.templates/docker-compose/docker-compose-helloworld.yaml +0 -0
  20. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/README.md +0 -0
  21. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/__init__.py +0 -0
  22. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/easyrunner_build.py +0 -0
  23. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/poetry.lock +0 -0
  24. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/pyproject.toml +0 -0
  25. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/pytest.ini +0 -0
  26. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/cloud_providers/__init__.py +0 -0
  27. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/cloud_providers/cloud_provider_base.py +0 -0
  28. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/cloud_providers/cloud_providers.py +0 -0
  29. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/cloud_providers/hetzner_provider.py +0 -0
  30. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/command_executor.py +0 -0
  31. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/command_executor_local.py +0 -0
  32. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/__init__.py +0 -0
  33. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/__init__.py +0 -0
  34. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/archive_commands.py +0 -0
  35. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/caddy_api_curl_commands.py +0 -0
  36. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/caddy_commands.py +0 -0
  37. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/command_base.py +0 -0
  38. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/curl_commands.py +0 -0
  39. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/dir_commands.py +0 -0
  40. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/docker_compose_commands.py +0 -0
  41. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/file_commands.py +0 -0
  42. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/git_commands.py +0 -0
  43. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/ip_tables_commands.py +0 -0
  44. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/ip_tables_persistent_commands.py +0 -0
  45. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/null_command.py +0 -0
  46. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/os_package_manager_commands.py +0 -0
  47. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/podman_commands.py +0 -0
  48. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/ssh_agent_commands.py +0 -0
  49. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/ssh_keygen_commands.py +0 -0
  50. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/systemctl_commands.py +0 -0
  51. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/user_commands.py +0 -0
  52. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/base/utility_commands.py +0 -0
  53. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/runnable_command_string.py +0 -0
  54. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/__init__.py +0 -0
  55. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/archive_commands_ubuntu.py +0 -0
  56. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/caddy_api_curl_commands_ubuntu.py +0 -0
  57. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/caddy_commands_container_ubuntu.py +0 -0
  58. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/curl_commands_ubuntu.py +0 -0
  59. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/dir_commands_ubuntu.py +0 -0
  60. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/docker_compose_commands_ubuntu.py +0 -0
  61. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/file_commands_ubuntu.py +0 -0
  62. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/git_commands_ubuntu.py +0 -0
  63. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/ip_tables_commands_ubuntu.py +0 -0
  64. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/ip_tables_persistent_commands_ubuntu.py +0 -0
  65. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/os_package_manager_commands_ubuntu.py +0 -0
  66. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/podman_commands_ubuntu.py +0 -0
  67. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/ssh_agent_commands_ubuntu.py +0 -0
  68. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/ssh_keygen_commands_ubuntu.py +0 -0
  69. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/systemctl_commands_ubuntu.py +0 -0
  70. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/user_commands_ubuntu.py +0 -0
  71. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/commands/ubuntu/utility_commands_ubuntu.py +0 -0
  72. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/format_utils.py +0 -0
  73. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/http_client.py +0 -0
  74. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/__init__.py +0 -0
  75. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/cloudflare/__init__.py +0 -0
  76. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/cloudflare/cloudflare_api_client.py +0 -0
  77. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/cloudflare/cloudflare_api_config.py +0 -0
  78. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/cloudflare/cloudflare_api_token_manager.py +0 -0
  79. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/github/__init__.py +0 -0
  80. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/github/github_device_flow.py +0 -0
  81. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/github/github_oauth_config.py +0 -0
  82. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/github/github_token_manager.py +0 -0
  83. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/hetzner/__init__.py +0 -0
  84. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/integrations/hetzner/hetzner_api_key_manager.py +0 -0
  85. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/known_host_ssh_keys.py +0 -0
  86. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/__init__.py +0 -0
  87. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/cloud_firewall_base.py +0 -0
  88. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/cloud_resource_api_base.py +0 -0
  89. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/cloud_resource_pulumi_base.py +0 -0
  90. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/cloud_virtual_machine_base.py +0 -0
  91. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/github/github_api_client.py +0 -0
  92. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/github/github_api_client_dtos.py +0 -0
  93. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/github/github_repo.py +0 -0
  94. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/hetzner/__init__.py +0 -0
  95. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_firewall.py +0 -0
  96. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_firewall_rule.py +0 -0
  97. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_resource_factory.py +0 -0
  98. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_stack.py +0 -0
  99. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_virtual_machine.py +0 -0
  100. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/__init__.py +0 -0
  101. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/caddy.py +0 -0
  102. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/directory.py +0 -0
  103. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/docker_compose.py +0 -0
  104. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/file.py +0 -0
  105. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/git_repo.py +0 -0
  106. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/host_server_ubuntu.py +0 -0
  107. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/ip_tables.py +0 -0
  108. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/os_package_manager.py +0 -0
  109. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/os_resource_base.py +0 -0
  110. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/podman.py +0 -0
  111. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/podman_network.py +0 -0
  112. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/ssh_agent.py +0 -0
  113. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/systemd_service.py +0 -0
  114. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/os_resources/user.py +0 -0
  115. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/resource_base.py +0 -0
  116. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/resources/web_security_scanner.py +0 -0
  117. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/services/__init__.py +0 -0
  118. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/services/app_service.py +0 -0
  119. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/services/deployment_service.py +0 -0
  120. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/services/license_service.py +0 -0
  121. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/services/server_service.py +0 -0
  122. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/services/service_result.py +0 -0
  123. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/ssh.py +0 -0
  124. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/ssh_key.py +0 -0
  125. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/__init__.py +0 -0
  126. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/data_models/__init__.py +0 -0
  127. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/data_models/app.py +0 -0
  128. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/data_models/database_dto_base.py +0 -0
  129. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/data_models/server.py +0 -0
  130. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/db_config.py +0 -0
  131. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/db_ctx.py +0 -0
  132. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/easyrunner_store.py +0 -0
  133. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/json_encoder.py +0 -0
  134. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/object_id.py +0 -0
  135. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/secret_store.py +0 -0
  136. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/store/uuid7.py +0 -0
  137. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/tool_paths.py +0 -0
  138. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/__init__.py +0 -0
  139. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/caddy/caddy_config.py +0 -0
  140. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/caddy/caddy_site.py +0 -0
  141. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/compose_project/__init__.py +0 -0
  142. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/compose_project/compose_network.py +0 -0
  143. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/compose_project/compose_project.py +0 -0
  144. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/compose_project/compose_service.py +0 -0
  145. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/compose_project/compose_volume.py +0 -0
  146. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/cpu_arch_types.py +0 -0
  147. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/dir_info.py +0 -0
  148. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/dto_base.py +0 -0
  149. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/exec_result.py +0 -0
  150. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/file_info.py +0 -0
  151. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/json.py +0 -0
  152. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/jsonobject_to_dataclass.py +0 -0
  153. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/os_type.py +0 -0
  154. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/podman_network_driver.py +0 -0
  155. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/security_scan_result.py +0 -0
  156. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/ssh_key_type.py +0 -0
  157. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/source/types/vm_config.py +0 -0
  158. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/tests/__init__.py +0 -0
  159. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/tests/conftest.py +0 -0
  160. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/tests/test_compose_to_quadlet.py +0 -0
  161. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/tests/test_hello_world.py +0 -0
  162. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/tests/test_secret_store.py +0 -0
  163. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/tests/test_services.py +0 -0
  164. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/easyrunner/tests/test_user_resource.py +0 -0
  165. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/__init__.py +0 -0
  166. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/infrastructure_deps.py +0 -0
  167. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/integrations/__init__.py +0 -0
  168. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/integrations/cloudflare/__init__.py +0 -0
  169. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/integrations/github/__init__.py +0 -0
  170. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/integrations/hetzner/__init__.py +0 -0
  171. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/integrations/secret_store/__init__.py +0 -0
  172. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/integrations/secret_store/keyring_secret_store.py +0 -0
  173. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/integrations/secret_store/secret_store.py +0 -0
  174. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/license_sub_command.py +0 -0
  175. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/licensing/__init__.py +0 -0
  176. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/licensing/license_manager.py +0 -0
  177. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/link_sub_command.py +0 -0
  178. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/main.py +0 -0
  179. {easyrunner_cli-0.0.8.dev128 → easyrunner_cli-0.0.8.dev129}/source/ssh_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyrunner_cli
3
- Version: 0.0.8.dev128
3
+ Version: 0.0.8.dev129
4
4
  Summary: EasyRunner CLI.
5
5
  License: Proprietary
6
6
  Author: Janaka Abeywardhana
@@ -0,0 +1,348 @@
1
+ """CLI + Services Integration Tests.
2
+
3
+ Tests that CLI commands properly use the service layer and maintain
4
+ consistent behavior between CLI and service layer interfaces.
5
+
6
+ These tests verify:
7
+ - CLI ServerSubCommand uses ServerService
8
+ - CLI AppSubCommand uses AppService and DeploymentService
9
+ - End-to-end workflows work correctly
10
+ - Error handling is consistent across layers
11
+ """
12
+
13
+ import logging
14
+ from unittest.mock import Mock
15
+
16
+ import pytest
17
+
18
+ from easyrunner.source.services import (
19
+ AppService,
20
+ DeploymentService,
21
+ ServerService,
22
+ ServiceResult,
23
+ )
24
+ from easyrunner.source.store.data_models import App, Server
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @pytest.fixture
30
+ def mock_store() -> Mock:
31
+ """Create a mock EasyRunnerStore."""
32
+ store = Mock()
33
+ store.list_servers.return_value = []
34
+ store.add_server.return_value = None
35
+ store.update_server.return_value = None
36
+ # By default, return None for server lookups (server doesn't exist)
37
+ store.get_server_by_name.return_value = None
38
+ store.get_server_by_hostname_or_ip.return_value = None
39
+ return store
40
+
41
+
42
+ @pytest.fixture
43
+ def mock_secret_store() -> Mock:
44
+ """Create a mock SecretStore."""
45
+ return Mock()
46
+
47
+
48
+ @pytest.fixture
49
+ def server_service(mock_store: Mock) -> ServerService:
50
+ """Create a ServerService with mocked store."""
51
+ return ServerService(store=mock_store)
52
+
53
+
54
+ @pytest.fixture
55
+ def app_service(mock_store: Mock) -> AppService:
56
+ """Create an AppService with mocked store."""
57
+ return AppService(store=mock_store)
58
+
59
+
60
+ @pytest.fixture
61
+ def deployment_service(
62
+ mock_store: Mock, mock_secret_store: Mock
63
+ ) -> DeploymentService:
64
+ """Create a DeploymentService with mocked dependencies."""
65
+ return DeploymentService(store=mock_store, secret_store=mock_secret_store)
66
+
67
+
68
+ class TestServerServiceUsage:
69
+ """Test that ServerService is properly used for server operations."""
70
+
71
+ def test_server_service_add_server_called_from_cli(
72
+ self, server_service: ServerService
73
+ ):
74
+ """Test ServerService.add_server is called correctly."""
75
+ # Arrange
76
+ test_server_name = "test-server"
77
+ test_server_ip = "192.168.1.1"
78
+
79
+ # Act
80
+ result = server_service.add_server(
81
+ name=test_server_name, hostname_or_ip=test_server_ip
82
+ )
83
+
84
+ # Assert
85
+ assert result.success
86
+ assert result.data is not None
87
+ assert result.data.name == test_server_name
88
+ assert result.data.hostname_or_ip == test_server_ip
89
+ assert result.message == f"Server '{test_server_name}' added successfully"
90
+
91
+ def test_server_service_delete_server_called_from_cli(
92
+ self, server_service: ServerService, mock_store: Mock
93
+ ):
94
+ """Test ServerService.delete_server removes server correctly."""
95
+ # Arrange
96
+ test_server = Server(name="test-server", hostname_or_ip="192.168.1.1")
97
+ mock_store.get_server_by_name.return_value = test_server
98
+
99
+ # Act
100
+ result = server_service.delete_server(name=test_server.name)
101
+
102
+ # Assert
103
+ assert result.success
104
+ assert result.message == f"Server '{test_server.name}' deleted successfully"
105
+ mock_store.remove_server.assert_called_once_with(server_id=test_server.id)
106
+
107
+ def test_server_service_respects_uniqueness_constraints(
108
+ self, server_service: ServerService, mock_store: Mock
109
+ ):
110
+ """Test ServerService enforces name/address uniqueness."""
111
+ # Arrange
112
+ existing_server = Server(
113
+ name="existing", hostname_or_ip="192.168.1.1"
114
+ )
115
+ mock_store.get_server_by_name.return_value = existing_server
116
+
117
+ # Act - try to add server with duplicate name
118
+ result = server_service.add_server(
119
+ name="existing", hostname_or_ip="192.168.1.2"
120
+ )
121
+
122
+ # Assert
123
+ assert not result.success
124
+ assert result.error_code == "DUPLICATE_NAME"
125
+
126
+
127
+ class TestAppServiceUsage:
128
+ """Test that AppService is properly used for app operations."""
129
+
130
+ def test_app_service_validate_for_deployment(
131
+ self, app_service: AppService, mock_store: Mock
132
+ ):
133
+ """Test AppService validates app for deployment."""
134
+ # Arrange
135
+ test_server = Server(name="test-server", hostname_or_ip="192.168.1.1")
136
+ test_app = App(
137
+ name="test-app",
138
+ repo_url="https://github.com/user/repo.git",
139
+ custom_domain="app.example.com",
140
+ )
141
+ test_server.apps = [test_app]
142
+ mock_store.get_server_by_name.return_value = test_server
143
+
144
+ # Act
145
+ result = app_service.validate_app_for_deployment(
146
+ server_name="test-server", app_name="test-app"
147
+ )
148
+
149
+ # Assert
150
+ assert result.success
151
+ assert result.data == test_app
152
+ assert (
153
+ result.message
154
+ == "App 'test-app' is ready for deployment"
155
+ )
156
+
157
+ def test_app_service_rejects_missing_custom_domain(
158
+ self, app_service: AppService, mock_store: Mock
159
+ ):
160
+ """Test AppService rejects app without custom domain."""
161
+ # Arrange
162
+ test_server = Server(name="test-server", hostname_or_ip="192.168.1.1")
163
+ test_app = App(
164
+ name="test-app",
165
+ repo_url="https://github.com/user/repo.git",
166
+ custom_domain=None,
167
+ )
168
+ test_server.apps = [test_app]
169
+ mock_store.get_server_by_name.return_value = test_server
170
+
171
+ # Act
172
+ result = app_service.validate_app_for_deployment(
173
+ server_name="test-server", app_name="test-app"
174
+ )
175
+
176
+ # Assert
177
+ assert not result.success
178
+ assert result.error_code == "MISSING_CUSTOM_DOMAIN"
179
+
180
+ def test_app_service_rejects_missing_repo_url(
181
+ self, app_service: AppService, mock_store: Mock
182
+ ):
183
+ """Test AppService rejects app without repo URL."""
184
+ # Arrange
185
+ test_server = Server(name="test-server", hostname_or_ip="192.168.1.1")
186
+ test_app = App(
187
+ name="test-app",
188
+ repo_url="",
189
+ custom_domain="app.example.com",
190
+ )
191
+ test_server.apps = [test_app]
192
+ mock_store.get_server_by_name.return_value = test_server
193
+
194
+ # Act
195
+ result = app_service.validate_app_for_deployment(
196
+ server_name="test-server", app_name="test-app"
197
+ )
198
+
199
+ # Assert
200
+ assert not result.success
201
+ assert result.error_code == "MISSING_REPO_URL"
202
+
203
+
204
+ class TestDeploymentServiceUsage:
205
+ """Test that DeploymentService is properly used for deployments."""
206
+
207
+ def test_deployment_service_validates_app_before_deploy(
208
+ self, deployment_service: DeploymentService, mock_store: Mock
209
+ ):
210
+ """Test DeploymentService validates app before attempting deployment."""
211
+ # Arrange
212
+ mock_store.get_server_by_name.return_value = None
213
+
214
+ # Act
215
+ result = deployment_service.deploy_app(
216
+ server_name="nonexistent",
217
+ app_name="test-app",
218
+ ssh_username="easyrunner",
219
+ ssh_key_path="/path/to/key",
220
+ github_token="dummy_token",
221
+ debug=False,
222
+ silent=False,
223
+ )
224
+
225
+ # Assert
226
+ assert not result.success
227
+ assert "not found" in result.message.lower()
228
+
229
+ def test_deployment_service_returns_structured_error(
230
+ self, deployment_service: DeploymentService, mock_store: Mock
231
+ ):
232
+ """Test DeploymentService returns structured error information."""
233
+ # Arrange
234
+ mock_store.get_server_by_name.return_value = None
235
+
236
+ # Act
237
+ result = deployment_service.deploy_app(
238
+ server_name="nonexistent",
239
+ app_name="test-app",
240
+ ssh_username="easyrunner",
241
+ ssh_key_path="/path/to/key",
242
+ debug=False,
243
+ silent=False,
244
+ )
245
+
246
+ # Assert - verify ServiceResult structure
247
+ assert isinstance(result, ServiceResult)
248
+ assert result.error_code is not None
249
+ assert result.message is not None
250
+
251
+
252
+ class TestCLIServiceConsistency:
253
+ """Test that CLI and service layer behaviors are consistent."""
254
+
255
+ def test_service_result_success_field_matches_cli_expectations(
256
+ self, server_service: ServerService
257
+ ):
258
+ """Test ServiceResult.success matches CLI error handling expectations."""
259
+ # Arrange
260
+ test_server = Server(name="test-server", hostname_or_ip="192.168.1.1")
261
+
262
+ # Act
263
+ result = server_service.add_server(
264
+ name=test_server.name, hostname_or_ip=test_server.hostname_or_ip
265
+ )
266
+
267
+ # Assert - CLI checks 'if result.success:'
268
+ assert isinstance(result.success, bool)
269
+ assert hasattr(result, "message")
270
+ assert hasattr(result, "data")
271
+ assert hasattr(result, "error_code")
272
+
273
+ def test_service_result_contains_cli_required_fields(
274
+ self, server_service: ServerService, mock_store: Mock
275
+ ):
276
+ """Test ServiceResult has all fields CLI needs for error display."""
277
+ # Arrange
278
+ mock_store.get_server_by_name.return_value = Server(
279
+ name="existing", hostname_or_ip="192.168.1.1"
280
+ )
281
+
282
+ # Act
283
+ result = server_service.add_server(
284
+ name="existing", hostname_or_ip="192.168.1.2"
285
+ )
286
+
287
+ # Assert - CLI uses these fields for error messages
288
+ assert not result.success
289
+ assert result.error_code in ["DUPLICATE_NAME", "DUPLICATE_ADDRESS"]
290
+ assert "already exists" in result.message.lower()
291
+ if result.details:
292
+ assert "existing_server" in result.details
293
+
294
+
295
+ class TestEndToEndWorkflows:
296
+ """Test complete workflows from CLI through services to store."""
297
+
298
+ def test_add_server_workflow(
299
+ self, server_service: ServerService, mock_store: Mock
300
+ ):
301
+ """Test complete add server workflow."""
302
+ # Arrange
303
+ server_name = "production"
304
+ server_ip = "192.168.1.100"
305
+
306
+ # Act - add server
307
+ add_result = server_service.add_server(
308
+ name=server_name, hostname_or_ip=server_ip
309
+ )
310
+
311
+ # Assert
312
+ assert add_result.success
313
+ assert add_result.data is not None
314
+ assert add_result.data.name == server_name
315
+ assert add_result.data.hostname_or_ip == server_ip
316
+ mock_store.add_server.assert_called_once()
317
+
318
+ def test_add_and_validate_app_workflow(
319
+ self,
320
+ app_service: AppService,
321
+ mock_store: Mock,
322
+ ):
323
+ """Test complete add and validate app workflow."""
324
+ # Arrange
325
+ test_server = Server(name="prod", hostname_or_ip="192.168.1.1")
326
+ test_app = App(
327
+ name="webapp",
328
+ repo_url="https://github.com/user/webapp.git",
329
+ custom_domain="app.prod.com",
330
+ )
331
+ test_server.apps = [test_app]
332
+ mock_store.get_server_by_name.return_value = test_server
333
+
334
+ # Act - get app
335
+ get_result = app_service.get_app(server_name="prod", app_name="webapp")
336
+
337
+ # Assert - got app
338
+ assert get_result.success
339
+ assert get_result.data == test_app
340
+
341
+ # Act - validate for deployment
342
+ validate_result = app_service.validate_app_for_deployment(
343
+ server_name="prod", app_name="webapp"
344
+ )
345
+
346
+ # Assert - app is valid
347
+ assert validate_result.success
348
+ assert validate_result.data == test_app
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "easyrunner_cli"
3
- version = "0.0.8.dev128"
3
+ version = "0.0.8.dev129"
4
4
  description = "EasyRunner CLI."
5
5
  authors = ["Janaka Abeywardhana <janaka@easyrunner.xyz>"]
6
6
  license = "Proprietary"
@@ -13,6 +13,7 @@ from easyrunner.source.commands.ubuntu.caddy_commands_container_ubuntu import (
13
13
  CaddyCommandsContainerUbuntu,
14
14
  )
15
15
  from easyrunner.source.resources.os_resources import Caddy, HostServerUbuntu
16
+ from easyrunner.source.services import AppService, DeploymentService
16
17
  from easyrunner.source.store.data_models.app import App
17
18
  from easyrunner.source.store.data_models.server import Server
18
19
  from easyrunner.source.store.easyrunner_store import EasyRunnerStore
@@ -97,26 +98,13 @@ class AppSubCommand:
97
98
  ) -> None:
98
99
 
99
100
  try:
101
+ # Get server to validate it exists and get SSH key path
100
102
  server: Server | None = AppSubCommand._store.get_server_by_name(
101
103
  name=server_name
102
104
  )
103
105
  if not server:
104
106
  AppSubCommand._print(f"Server with name '{server_name}' not found.")
105
107
  return
106
- app = next((app for app in server.apps if app.name == name), None)
107
- if not app:
108
- AppSubCommand._print(
109
- f"App with name '{name}' not found on server '{server_name}'."
110
- )
111
- return
112
-
113
- if not app.custom_domain:
114
- AppSubCommand._print(
115
- f"App with name '{name}' must have a custom domain name set because this is used to uniquely identify this app in configuration. The custom domain name must be unique amongst all the apps on each server."
116
- )
117
- return
118
-
119
- hostname_or_ipv4: str = server.hostname_or_ip
120
108
 
121
109
  # Initial deployment info
122
110
  AppSubCommand._print("🚀 [bold blue]Starting deployment...[/bold blue]")
@@ -130,27 +118,26 @@ class AppSubCommand:
130
118
  )
131
119
  return
132
120
 
133
- with Ssh(
134
- hostname_or_ipv4=hostname_or_ipv4,
135
- username=ssh_config.username,
136
- key_filename=build_private_key_path(hostname_or_ipv4),
121
+ # Use DeploymentService to orchestrate deployment
122
+ deployment_service = DeploymentService(
123
+ store=AppSubCommand._store,
124
+ secret_store=KeyringSecretStore(),
125
+ )
126
+ result = deployment_service.deploy_app(
127
+ server_name=server_name,
128
+ app_name=name,
129
+ ssh_username=ssh_config.username,
130
+ ssh_key_path=build_private_key_path(server.hostname_or_ip),
131
+ github_token=github_access_token,
137
132
  debug=AppSubCommand.debug,
138
133
  silent=AppSubCommand.silent,
139
- ) as ssh_client:
140
- executor = CommandExecutor(ssh_client=ssh_client)
141
- host_server_instance = HostServerUbuntu(
142
- easyrunner_username=ssh_config.username,
143
- executor=executor,
144
- debug=AppSubCommand.debug,
145
- silent=AppSubCommand.silent,
146
- progress_callback=AppSubCommand._progress_callback,
147
- )
134
+ )
135
+
136
+ if result.success:
137
+ AppSubCommand._print(f"✅ {result.message}")
138
+ else:
139
+ AppSubCommand._print(f"❌ Deployment failed: {result.message}")
148
140
 
149
- host_server_instance.deploy_app_flow_a(
150
- repo_url=app.repo_url,
151
- custom_app_domain_name=app.custom_domain,
152
- github_access_token=github_access_token,
153
- )
154
141
  except Exception as e:
155
142
  AppSubCommand._print(
156
143
  f"[red]Oops, something went wrong while deploying the app:[/red] {str(e)}"
@@ -286,6 +273,14 @@ class AppSubCommand:
286
273
  help="The HTTPS URL of the application repository. Currently only supports GitHub repositories.",
287
274
  ),
288
275
  ) -> None:
276
+ # Get server
277
+ app_service = AppService(store=AppSubCommand._store)
278
+ server_result = app_service.list_apps(server_name=server_name)
279
+
280
+ if not server_result.success:
281
+ echo(message=f"Server with name '{server_name}' not found.")
282
+ return
283
+
289
284
  server: Server | None = AppSubCommand._store.get_server_by_name(
290
285
  name=server_name
291
286
  )
@@ -293,6 +288,7 @@ class AppSubCommand:
293
288
  echo(message=f"Server with name '{server_name}' not found.")
294
289
  return
295
290
 
291
+ # Check if app already exists
296
292
  if not any(app.name == name for app in server.apps):
297
293
  server.apps.append(
298
294
  App(
@@ -18,6 +18,7 @@ from easyrunner.source.known_host_ssh_keys import KNOWN_HOST_SSH_KEYS
18
18
  from easyrunner.source.resources.cloud_resources.hetzner import HetznerStack
19
19
  from easyrunner.source.resources.os_resources import HostServerUbuntu
20
20
  from easyrunner.source.resources.web_security_scanner import WebSecurityScanner
21
+ from easyrunner.source.services import LicenseService, ServerService
21
22
  from easyrunner.source.ssh import with_ssh_retry
22
23
  from easyrunner.source.store import EasyRunnerStore
23
24
  from easyrunner.source.store.data_models import Server
@@ -82,43 +83,48 @@ class ServerSubCommand:
82
83
 
83
84
  Displays error message to user if limit is exceeded.
84
85
  """
85
- # Get license info
86
- manager = LicenseManager()
87
- license_info = manager.get_license_info()
88
-
89
- if license_info is None:
90
- # This shouldn't happen as main.py checks license on startup
91
- # But handle it gracefully just in case
92
- ServerSubCommand._print(
93
- "[red]Error:[/red] No valid license found. Run [cyan]er license status[/cyan] for details."
94
- )
95
- return False
96
-
97
- # Get current server count
98
- current_servers = ServerSubCommand._store.list_servers()
99
- current_count = len(current_servers)
100
-
101
- # Check if we're at or over the limit
102
- if current_count >= license_info.server_limit:
103
- ServerSubCommand._print()
104
- ServerSubCommand._print(f"[red]✗ Server limit reached[/red]")
105
- ServerSubCommand._print()
106
- ServerSubCommand._print(
107
- f"Your license allows [bold]{license_info.server_limit}[/bold] server(s)."
108
- )
109
- ServerSubCommand._print(
110
- f"You currently have [bold]{current_count}[/bold] server(s) registered."
111
- )
112
- ServerSubCommand._print()
113
- ServerSubCommand._print("To add more servers:")
114
- ServerSubCommand._print(
115
- " Remove an existing server: [cyan]er server remove <name>[/cyan]"
116
- )
117
- ServerSubCommand._print(
118
- " Upgrade your license at [link]https://easyrunner.xyz[/link]"
119
- )
120
- ServerSubCommand._print()
121
- return False
86
+ # Use LicenseService to check server quota
87
+ license_service = LicenseService(store=ServerSubCommand._store)
88
+ limit_result = license_service.can_add_server()
89
+
90
+ # If there's an error with license check, use LicenseManager as fallback
91
+ if not limit_result.success:
92
+ manager = LicenseManager()
93
+ license_info = manager.get_license_info()
94
+
95
+ if license_info is None:
96
+ # This shouldn't happen as main.py checks license on startup
97
+ # But handle it gracefully just in case
98
+ ServerSubCommand._print(
99
+ "[red]Error:[/red] No valid license found. Run [cyan]er license status[/cyan] for details."
100
+ )
101
+ return False
102
+
103
+ # Get current server count
104
+ current_servers = ServerSubCommand._store.list_servers()
105
+ current_count = len(current_servers)
106
+
107
+ # Check if we're at or over the limit
108
+ if current_count >= license_info.server_limit:
109
+ ServerSubCommand._print()
110
+ ServerSubCommand._print("[red]✗ Server limit reached[/red]")
111
+ ServerSubCommand._print()
112
+ ServerSubCommand._print(
113
+ f"Your license allows [bold]{license_info.server_limit}[/bold] server(s)."
114
+ )
115
+ ServerSubCommand._print(
116
+ f"You currently have [bold]{current_count}[/bold] server(s) registered."
117
+ )
118
+ ServerSubCommand._print()
119
+ ServerSubCommand._print("To add more servers:")
120
+ ServerSubCommand._print(
121
+ " • Remove an existing server: [cyan]er server remove <name>[/cyan]"
122
+ )
123
+ ServerSubCommand._print(
124
+ " • Upgrade your license at [link]https://easyrunner.xyz[/link]"
125
+ )
126
+ ServerSubCommand._print()
127
+ return False
122
128
 
123
129
  return True
124
130
 
@@ -189,38 +195,34 @@ class ServerSubCommand:
189
195
  Returns:
190
196
  Server object if successfully added, None if validation failed
191
197
  """
192
- # Check if server name already exists
193
- server_name_exists: Server | None = ServerSubCommand._store.get_server_by_name(
194
- name=name
195
- )
196
- if server_name_exists:
197
- ServerSubCommand._print(
198
- f"⚠️ Server with the name '[bold yellow]{server_name_exists.name}[/bold yellow]' already exists in EasyRunner."
199
- )
198
+ # Use ServerService to add server with validation
199
+ server_service = ServerService(store=ServerSubCommand._store)
200
+ result = server_service.add_server(name=name, hostname_or_ip=hostname_or_ip)
201
+
202
+ if not result.success:
203
+ # Display appropriate error message based on error code
204
+ if result.error_code == "DUPLICATE_NAME":
205
+ existing_server = result.details.get("existing_server") if result.details else name
206
+ ServerSubCommand._print(
207
+ f"⚠️ Server with the name '[bold yellow]{existing_server}[/bold yellow]' already exists in EasyRunner."
208
+ )
209
+ elif result.error_code == "DUPLICATE_ADDRESS":
210
+ existing_server = result.details.get("existing_server") if result.details else hostname_or_ip
211
+ ServerSubCommand._print(
212
+ f"⚠️ Server with the address '[bold yellow]{existing_server}[/bold yellow]' already exists in EasyRunner."
213
+ )
214
+ else:
215
+ ServerSubCommand._print(f"⚠️ {result.message}")
200
216
  return None
201
217
 
202
- # Check if server address already exists
203
- server_address_exists: Server | None = (
204
- ServerSubCommand._store.get_server_by_hostname_or_ip(
205
- hostname_or_ip=hostname_or_ip
206
- )
207
- )
208
- if server_address_exists:
218
+ # Display success message
219
+ server = result.data
220
+ if server:
209
221
  ServerSubCommand._print(
210
- f"⚠️ Server with the address '[bold yellow]{server_address_exists.hostname_or_ip}[/bold yellow]' already exists in EasyRunner."
222
+ f"Server [bold green]{server.name}[/bold green]([italic cyan]{server.hostname_or_ip}[/italic cyan]) added to EasyRunner."
211
223
  )
212
- return None
213
-
214
- # Create and persist the server
215
- server: Server = Server(
216
- name=name,
217
- hostname_or_ip=hostname_or_ip,
218
- )
219
- ServerSubCommand._store.add_server(server=server)
220
- ServerSubCommand._print(
221
- f"✅ Server [bold green]{server.name}[/bold green]([italic cyan]{server.hostname_or_ip}[/italic cyan]) added to EasyRunner."
222
- )
223
- return server
224
+ return server
225
+ return None
224
226
 
225
227
  @staticmethod
226
228
  def _cancel_stack(name: str, cloud_provider_name: CloudProviders) -> None:
@@ -584,20 +586,31 @@ class ServerSubCommand:
584
586
  )
585
587
  @staticmethod
586
588
  def remove_server(name: str = NAME_ARG) -> None:
587
- server = ServerSubCommand._store.get_server_by_name(name=name)
588
- if not server:
589
+ # Use ServerService to get server
590
+ server_service = ServerService(store=ServerSubCommand._store)
591
+ server_result = server_service.get_server(name=name)
592
+
593
+ if not server_result.success:
594
+ echo(message=f"Server {name} not found in EasyRunner.")
595
+ return
596
+
597
+ server = server_result.data
598
+ if server is None:
589
599
  echo(message=f"Server {name} not found in EasyRunner.")
590
600
  return
591
601
 
592
602
  # Remove SSH key files for the server
593
603
  ServerSubCommand._cleanup_ssh_keys(server.hostname_or_ip)
594
604
 
595
- # Remove the server from the store database
596
- ServerSubCommand._store.remove_server(server_id=server.id)
605
+ # Use ServerService to remove the server
606
+ delete_result = server_service.delete_server(name=name)
597
607
 
598
- echo(
599
- message=f"Server {server.name}({server.hostname_or_ip}) removed from EasyRunner."
600
- )
608
+ if delete_result.success:
609
+ echo(
610
+ message=f"Server {server.name}({server.hostname_or_ip}) removed from EasyRunner."
611
+ )
612
+ else:
613
+ echo(message=f"Failed to remove server: {delete_result.message}")
601
614
 
602
615
  @typer_app.command(
603
616
  name="create",