easyrunner-cli 0.0.8.dev97__tar.gz → 0.0.8.dev99__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 (137) hide show
  1. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/PKG-INFO +1 -1
  2. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/pyproject.toml +1 -1
  3. easyrunner_cli-0.0.8.dev99/source/auth/__init__.py +10 -0
  4. easyrunner_cli-0.0.8.dev99/source/auth/auth_sub_command.py +238 -0
  5. easyrunner_cli-0.0.8.dev99/source/auth/github_device_flow.py +247 -0
  6. easyrunner_cli-0.0.8.dev99/source/auth/github_oauth_config.py +24 -0
  7. easyrunner_cli-0.0.8.dev97/source/auth_sub_command.py → easyrunner_cli-0.0.8.dev99/source/link_sub_command.py +81 -74
  8. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/main.py +2 -2
  9. easyrunner_cli-0.0.8.dev97/source/auth/__init__.py +0 -12
  10. easyrunner_cli-0.0.8.dev97/source/auth/auth_sub_command.py +0 -226
  11. easyrunner_cli-0.0.8.dev97/source/auth/github_oauth_config.py +0 -24
  12. easyrunner_cli-0.0.8.dev97/source/auth/github_oauth_flow.py +0 -165
  13. easyrunner_cli-0.0.8.dev97/source/auth/oauth_callback_server.py +0 -109
  14. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/README.md +0 -0
  15. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/__init__.py +0 -0
  16. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/cloud_provider_base.py +0 -0
  17. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/cloud_providers.py +0 -0
  18. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/hetzner_provider.py +0 -0
  19. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/command_executor.py +0 -0
  20. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/command_executor_local.py +0 -0
  21. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/__init__.py +0 -0
  22. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/__init__.py +0 -0
  23. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/archive_commands.py +0 -0
  24. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/caddy_api_curl_commands.py +0 -0
  25. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/caddy_commands.py +0 -0
  26. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/command_base.py +0 -0
  27. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/curl_commands.py +0 -0
  28. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/dir_commands.py +0 -0
  29. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/docker_compose_commands.py +0 -0
  30. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/file_commands.py +0 -0
  31. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/git_commands.py +0 -0
  32. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ip_tables_commands.py +0 -0
  33. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ip_tables_persistent_commands.py +0 -0
  34. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/null_command.py +0 -0
  35. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/os_package_manager_commands.py +0 -0
  36. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/podman_commands.py +0 -0
  37. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ssh_agent_commands.py +0 -0
  38. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ssh_keygen_commands.py +0 -0
  39. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/systemctl_commands.py +0 -0
  40. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/user_commands.py +0 -0
  41. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/utility_commands.py +0 -0
  42. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/runnable_command_string.py +0 -0
  43. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/__init__.py +0 -0
  44. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/archive_commands_ubuntu.py +0 -0
  45. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/caddy_api_curl_commands_ubuntu.py +0 -0
  46. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/caddy_commands_container_ubuntu.py +0 -0
  47. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/curl_commands_ubuntu.py +0 -0
  48. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/dir_commands_ubuntu.py +0 -0
  49. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/docker_compose_commands_ubuntu.py +0 -0
  50. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/file_commands_ubuntu.py +0 -0
  51. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/git_commands_ubuntu.py +0 -0
  52. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ip_tables_commands_ubuntu.py +0 -0
  53. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ip_tables_persistent_commands_ubuntu.py +0 -0
  54. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/os_package_manager_commands_ubuntu.py +0 -0
  55. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/podman_commands_ubuntu.py +0 -0
  56. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ssh_agent_commands_ubuntu.py +0 -0
  57. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ssh_keygen_commands_ubuntu.py +0 -0
  58. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/systemctl_commands_ubuntu.py +0 -0
  59. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/user_commands_ubuntu.py +0 -0
  60. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/utility_commands_ubuntu.py +0 -0
  61. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/format_utils.py +0 -0
  62. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/http_client.py +0 -0
  63. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/known_host_ssh_keys.py +0 -0
  64. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/__init__.py +0 -0
  65. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_firewall_base.py +0 -0
  66. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_resource_api_base.py +0 -0
  67. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_resource_pulumi_base.py +0 -0
  68. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_virtual_machine_base.py +0 -0
  69. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/github/github_api_client.py +0 -0
  70. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/github/github_api_client_dtos.py +0 -0
  71. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/github/github_repo.py +0 -0
  72. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/__init__.py +0 -0
  73. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_firewall.py +0 -0
  74. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_firewall_rule.py +0 -0
  75. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_resource_factory.py +0 -0
  76. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_stack.py +0 -0
  77. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_virtual_machine.py +0 -0
  78. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/__init__.py +0 -0
  79. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/caddy.py +0 -0
  80. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/directory.py +0 -0
  81. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/docker_compose.py +0 -0
  82. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/file.py +0 -0
  83. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/git_repo.py +0 -0
  84. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/host_server_ubuntu.py +0 -0
  85. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/ip_tables.py +0 -0
  86. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/os_package_manager.py +0 -0
  87. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/os_resource_base.py +0 -0
  88. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/podman.py +0 -0
  89. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/podman_network.py +0 -0
  90. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/ssh_agent.py +0 -0
  91. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/systemd_service.py +0 -0
  92. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/user.py +0 -0
  93. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/resource_base.py +0 -0
  94. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/web_security_scanner.py +0 -0
  95. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/ssh.py +0 -0
  96. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/ssh_key.py +0 -0
  97. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/__init__.py +0 -0
  98. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/__init__.py +0 -0
  99. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/app.py +0 -0
  100. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/database_dto_base.py +0 -0
  101. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/server.py +0 -0
  102. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/db_config.py +0 -0
  103. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/db_ctx.py +0 -0
  104. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/easyrunner_store.py +0 -0
  105. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/json_encoder.py +0 -0
  106. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/object_id.py +0 -0
  107. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/uuid7.py +0 -0
  108. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/tool_paths.py +0 -0
  109. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/__init__.py +0 -0
  110. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/caddy/caddy_config.py +0 -0
  111. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/caddy/caddy_site.py +0 -0
  112. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/__init__.py +0 -0
  113. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_network.py +0 -0
  114. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_project.py +0 -0
  115. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_service.py +0 -0
  116. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_volume.py +0 -0
  117. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/cpu_arch_types.py +0 -0
  118. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/dir_info.py +0 -0
  119. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/dto_base.py +0 -0
  120. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/exec_result.py +0 -0
  121. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/file_info.py +0 -0
  122. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/json.py +0 -0
  123. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/jsonobject_to_dataclass.py +0 -0
  124. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/os_type.py +0 -0
  125. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/podman_network_driver.py +0 -0
  126. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/security_scan_result.py +0 -0
  127. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/ssh_key_type.py +0 -0
  128. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/vm_config.py +0 -0
  129. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/__init__.py +0 -0
  130. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/app_sub_command.py +0 -0
  131. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/auth/github_token_manager.py +0 -0
  132. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/infrastructure_deps.py +0 -0
  133. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/license_sub_command.py +0 -0
  134. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/licensing/__init__.py +0 -0
  135. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/licensing/license_manager.py +0 -0
  136. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/servers_sub_command.py +0 -0
  137. {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/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.dev97
3
+ Version: 0.0.8.dev99
4
4
  Summary: EasyRunner CLI.
5
5
  Author: Janaka Abeywardhana
6
6
  Author-email: janaka@easyrunner.xyz
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "easyrunner_cli"
3
- version = "0.0.8.dev97"
3
+ version = "0.0.8.dev99"
4
4
  description = "EasyRunner CLI."
5
5
  authors = ["Janaka Abeywardhana <janaka@easyrunner.xyz>"]
6
6
  license = ""
@@ -0,0 +1,10 @@
1
+ from .github_device_flow import DeviceCodeResponse, GitHubDeviceFlow
2
+ from .github_oauth_config import GitHubOAuthConfig
3
+ from .github_token_manager import GitHubTokenManager
4
+
5
+ __all__ = [
6
+ "DeviceCodeResponse",
7
+ "GitHubDeviceFlow",
8
+ "GitHubOAuthConfig",
9
+ "GitHubTokenManager",
10
+ ]
@@ -0,0 +1,238 @@
1
+ import logging
2
+ from typing import Optional, Self
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from typer import Option
7
+
8
+ from .github_device_flow import GitHubDeviceFlow
9
+ from .github_oauth_config import GitHubOAuthConfig
10
+ from .github_token_manager import GitHubTokenManager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class LinkSubCommand:
16
+ """Link external services to EasyRunner."""
17
+
18
+ typer_app: typer.Typer = typer.Typer(
19
+ name="link",
20
+ no_args_is_help=True,
21
+ rich_markup_mode="rich",
22
+ help="[bold green]Link[/bold green] external services to EasyRunner. Connect GitHub and other service providers.",
23
+ )
24
+
25
+ debug: bool = False
26
+ silent: bool = False
27
+
28
+ _console: Console = Console()
29
+ _print = _console.print
30
+
31
+ # Define progress callback with CLI-specific formatting
32
+ @staticmethod
33
+ def _progress_callback(message: str, end: str) -> None:
34
+ if not LinkSubCommand.silent:
35
+ LinkSubCommand._print(message, end=end)
36
+
37
+ def __init__(self: Self) -> None:
38
+ @self.typer_app.callback(invoke_without_command=True)
39
+ def set_global_options( # type: ignore
40
+ debug: bool = Option(
41
+ False,
42
+ "--debug",
43
+ help="Enables extra debug messages to be output. Independent of --silent.",
44
+ rich_help_panel="Global Options",
45
+ ),
46
+ silent: bool = Option(
47
+ False,
48
+ "--silent",
49
+ help="Suppresses all output messages.",
50
+ rich_help_panel="Global Options",
51
+ ),
52
+ ) -> None:
53
+ LinkSubCommand.debug = debug
54
+ LinkSubCommand.silent = silent
55
+ if debug:
56
+ logger.setLevel(logging.DEBUG)
57
+ elif silent:
58
+ logger.setLevel(logging.ERROR)
59
+
60
+ @typer_app.command(
61
+ name="github",
62
+ help="Link GitHub to EasyRunner. This will allow EasyRunner to manage deploy keys for your repositories.",
63
+ no_args_is_help=False,
64
+ )
65
+ @staticmethod
66
+ def github_link(
67
+ token: Optional[str] = Option(
68
+ None,
69
+ "--token",
70
+ help="Manually provide a GitHub Personal Access Token instead of using OAuth flow."
71
+ ),
72
+ unlink: bool = Option(
73
+ False,
74
+ "--unlink",
75
+ help="Remove GitHub link and stored credentials."
76
+ ),
77
+ status: bool = Option(
78
+ False,
79
+ "--status",
80
+ help="Check current GitHub link status."
81
+ )
82
+ ) -> None:
83
+ """Link GitHub for repository access."""
84
+
85
+ token_manager = GitHubTokenManager()
86
+
87
+ if unlink:
88
+ if token_manager.delete_token():
89
+ LinkSubCommand._progress_callback(
90
+ "[green]✅ Successfully unlinked GitHub[/green]", "\n"
91
+ )
92
+ else:
93
+ LinkSubCommand._progress_callback(
94
+ "[red]❌ Failed to remove GitHub link[/red]", "\n"
95
+ )
96
+ return
97
+
98
+ if status:
99
+ stored_token = token_manager.get_token()
100
+ if stored_token:
101
+ config = GitHubOAuthConfig()
102
+ device_flow = GitHubDeviceFlow(
103
+ client_id=config.client_id,
104
+ scopes=config.scopes,
105
+ progress_callback=LinkSubCommand._progress_callback,
106
+ )
107
+ if device_flow.test_token(stored_token):
108
+ LinkSubCommand._progress_callback(
109
+ "[green]✅ GitHub link active[/green]", "\n"
110
+ )
111
+ else:
112
+ LinkSubCommand._progress_callback(
113
+ "[red]❌ GitHub link invalid (token expired or revoked)[/red]",
114
+ "\n",
115
+ )
116
+ else:
117
+ LinkSubCommand._progress_callback(
118
+ "[yellow]⚠️ GitHub not linked[/yellow]", "\n"
119
+ )
120
+ return
121
+
122
+ if token:
123
+ # Manual token input
124
+ config = GitHubOAuthConfig()
125
+ device_flow = GitHubDeviceFlow(
126
+ client_id=config.client_id,
127
+ scopes=config.scopes,
128
+ progress_callback=LinkSubCommand._progress_callback,
129
+ )
130
+ if device_flow.test_token(token):
131
+ if token_manager.store_token(token):
132
+ LinkSubCommand._progress_callback(
133
+ "[green]✅ GitHub token saved successfully[/green]", "\n"
134
+ )
135
+ else:
136
+ LinkSubCommand._progress_callback(
137
+ "[red]❌ Failed to save token[/red]", "\n"
138
+ )
139
+ else:
140
+ LinkSubCommand._progress_callback(
141
+ "[red]❌ Invalid GitHub token[/red]", "\n"
142
+ )
143
+ return
144
+
145
+ # Device Flow - OAuth for CLIs
146
+ try:
147
+ LinkSubCommand._progress_callback(
148
+ "[blue]🔐 Starting GitHub Device Flow linking...[/blue]", "\n"
149
+ )
150
+
151
+ config = GitHubOAuthConfig()
152
+ device_flow = GitHubDeviceFlow(
153
+ client_id=config.client_id,
154
+ scopes=config.scopes,
155
+ progress_callback=LinkSubCommand._progress_callback,
156
+ )
157
+
158
+ access_token = device_flow.start_device_flow()
159
+
160
+ if access_token:
161
+ if token_manager.store_token(access_token):
162
+ LinkSubCommand._progress_callback(
163
+ "[green]✅ GitHub linked successfully![/green]", "\n"
164
+ )
165
+ LinkSubCommand._progress_callback(
166
+ "🔑 Access token stored securely in keychain", "\n"
167
+ )
168
+ LinkSubCommand._progress_callback(
169
+ "🚀 EasyRunner can now manage deploy keys for your repositories",
170
+ "\n",
171
+ )
172
+ else:
173
+ LinkSubCommand._progress_callback(
174
+ "[red]❌ Link succeeded but failed to store token[/red]",
175
+ "\n",
176
+ )
177
+ else:
178
+ LinkSubCommand._progress_callback(
179
+ "[red]❌ GitHub linking failed[/red]", "\n"
180
+ )
181
+ LinkSubCommand._progress_callback(
182
+ "💡 Try running the command again or use --token to manually provide a token",
183
+ "\n",
184
+ )
185
+
186
+ except KeyboardInterrupt:
187
+ LinkSubCommand._progress_callback(
188
+ "\n[yellow]⚠️ Linking cancelled[/yellow]", "\n"
189
+ )
190
+ except Exception as e:
191
+ LinkSubCommand._progress_callback(
192
+ f"[red]❌ Linking error: {e}[/red]", "\n"
193
+ )
194
+ if LinkSubCommand.debug:
195
+ import traceback
196
+ LinkSubCommand._progress_callback(
197
+ f"[red]{traceback.format_exc()}[/red]", "\n"
198
+ )
199
+
200
+ @typer_app.command(
201
+ name="status",
202
+ help="Show link status for all services.",
203
+ no_args_is_help=False,
204
+ )
205
+ @staticmethod
206
+ def link_status() -> None:
207
+ """Show link status for all services."""
208
+ LinkSubCommand._progress_callback(
209
+ "[bold blue]� Link Status[/bold blue]\n", "\n"
210
+ )
211
+
212
+ # Check GitHub
213
+ token_manager = GitHubTokenManager()
214
+ stored_token = token_manager.get_token()
215
+
216
+ if stored_token:
217
+ config = GitHubOAuthConfig()
218
+ device_flow = GitHubDeviceFlow(
219
+ client_id=config.client_id,
220
+ scopes=config.scopes,
221
+ progress_callback=LinkSubCommand._progress_callback,
222
+ )
223
+ if device_flow.test_token(stored_token):
224
+ LinkSubCommand._progress_callback(
225
+ "GitHub: [green]✅ Linked[/green]", "\n"
226
+ )
227
+ else:
228
+ LinkSubCommand._progress_callback(
229
+ "GitHub: [red]❌ Invalid (token expired or revoked)[/red]", "\n"
230
+ )
231
+ else:
232
+ LinkSubCommand._progress_callback(
233
+ "GitHub: [yellow]⚠️ Not linked[/yellow]", "\n"
234
+ )
235
+
236
+ LinkSubCommand._progress_callback(
237
+ "\n💡 Use 'er link github' to link GitHub", "\n"
238
+ )
@@ -0,0 +1,247 @@
1
+ """GitHub Device Flow authentication for CLI applications.
2
+
3
+ This module implements GitHub's Device Flow OAuth, which is designed for
4
+ CLIs and doesn't require a client secret. Users authorize the app by
5
+ visiting a URL and entering a code displayed in the terminal.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import time
11
+ import webbrowser
12
+ from dataclasses import dataclass
13
+ from typing import Callable, Optional
14
+
15
+ from easyrunner.source.command_executor_local import CommandExecutorLocal
16
+ from easyrunner.source.commands.ubuntu.curl_commands_ubuntu import CurlCommandsUbuntu
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class DeviceCodeResponse:
23
+ """Response from device code request."""
24
+
25
+ device_code: str
26
+ user_code: str
27
+ verification_uri: str
28
+ expires_in: int
29
+ interval: int
30
+
31
+
32
+ class GitHubDeviceFlow:
33
+ """GitHub Device Flow authentication for CLI applications."""
34
+
35
+ DEVICE_CODE_URL = "https://github.com/login/device/code"
36
+ TOKEN_URL = "https://github.com/login/oauth/access_token"
37
+
38
+ def __init__(
39
+ self,
40
+ client_id: str,
41
+ scopes: str = "repo",
42
+ progress_callback: Optional[Callable[[str, str], None]] = None,
43
+ ):
44
+ """Initialize device flow.
45
+
46
+ Args:
47
+ client_id: GitHub OAuth app client ID (public, safe to distribute)
48
+ scopes: OAuth scopes to request (default: "repo")
49
+ progress_callback: Optional callback for progress messages
50
+ """
51
+ self.client_id = client_id
52
+ self.scopes = scopes
53
+ self.executor = CommandExecutorLocal()
54
+ self.curl_commands = CurlCommandsUbuntu()
55
+ self.progress_callback = progress_callback or (lambda msg, end: None)
56
+
57
+ def start_device_flow(self) -> Optional[str]:
58
+ """Start device flow and return access token if successful."""
59
+ try:
60
+ # Step 1: Request device code
61
+ device_response = self._request_device_code()
62
+ if not device_response:
63
+ self.progress_callback(
64
+ "[red]❌ Failed to request device code[/red]", "\n"
65
+ )
66
+ return None
67
+
68
+ # Step 2: Show user instructions
69
+ self._display_user_instructions(device_response)
70
+
71
+ # Step 3: Poll for token
72
+ return self._poll_for_token(device_response)
73
+
74
+ except Exception as e:
75
+ logger.error(f"Device flow failed: {e}")
76
+ self.progress_callback(f"[red]❌ Device flow failed: {e}[/red]", "\n")
77
+ return None
78
+
79
+ def _request_device_code(self) -> Optional[DeviceCodeResponse]:
80
+ """Request device code from GitHub."""
81
+ try:
82
+ request_data = {"client_id": self.client_id, "scope": self.scopes}
83
+
84
+ cmd = self.curl_commands.post(
85
+ url=self.DEVICE_CODE_URL,
86
+ data=json.dumps(request_data),
87
+ headers={
88
+ "Accept": "application/json",
89
+ "Content-Type": "application/json",
90
+ },
91
+ )
92
+
93
+ result = self.executor.execute(cmd)
94
+
95
+ if result.success and result.stdout:
96
+ response_body, _ = self.curl_commands.parse_curl_response_with_status(
97
+ result.stdout
98
+ )
99
+ response_data = json.loads(response_body)
100
+
101
+ # Check for GitHub API errors
102
+ if "error" in response_data:
103
+ error = response_data.get("error")
104
+ error_desc = response_data.get("error_description", "Unknown error")
105
+ logger.error(f"GitHub API error: {error} - {error_desc}")
106
+ self.progress_callback(
107
+ f"[red]❌ GitHub Error: {error_desc}[/red]", "\n"
108
+ )
109
+ if error == "device_flow_disabled":
110
+ self.progress_callback(
111
+ "[yellow]💡 Device Flow must be enabled in GitHub OAuth app settings[/yellow]",
112
+ "\n",
113
+ )
114
+ self.progress_callback(
115
+ "[yellow] Visit: https://github.com/settings/developers[/yellow]",
116
+ "\n",
117
+ )
118
+ return None
119
+
120
+ return DeviceCodeResponse(
121
+ device_code=response_data["device_code"],
122
+ user_code=response_data["user_code"],
123
+ verification_uri=response_data["verification_uri"],
124
+ expires_in=response_data["expires_in"],
125
+ interval=response_data["interval"],
126
+ )
127
+
128
+ logger.error(f"Device code request failed: {result.stderr}")
129
+ return None
130
+
131
+ except Exception as e:
132
+ logger.error(f"Failed to request device code: {e}")
133
+ return None
134
+
135
+ def _display_user_instructions(self, device_response: DeviceCodeResponse) -> None:
136
+ """Display instructions to user."""
137
+ self.progress_callback("\n" + "=" * 60, "\n")
138
+ self.progress_callback("🔐 [bold cyan]GitHub Device Authorization[/bold cyan]", "\n")
139
+ self.progress_callback("=" * 60, "\n\n")
140
+ self.progress_callback(
141
+ f"1. Visit: [bold blue]{device_response.verification_uri}[/bold blue]",
142
+ "\n",
143
+ )
144
+ self.progress_callback(
145
+ f"2. Enter code: [bold yellow]{device_response.user_code}[/bold yellow]",
146
+ "\n\n",
147
+ )
148
+ self.progress_callback("🌐 Opening browser...", "\n")
149
+
150
+ # Auto-open browser
151
+ try:
152
+ webbrowser.open(device_response.verification_uri)
153
+ except Exception as e:
154
+ logger.warning(f"Failed to open browser: {e}")
155
+
156
+ self.progress_callback("\n⏳ Waiting for authorization", "")
157
+
158
+ def _poll_for_token(self, device_response: DeviceCodeResponse) -> Optional[str]:
159
+ """Poll GitHub for access token."""
160
+ interval = device_response.interval
161
+ expires_at = time.time() + device_response.expires_in
162
+ poll_count = 0
163
+
164
+ while time.time() < expires_at:
165
+ time.sleep(interval)
166
+
167
+ # Show progress dots
168
+ poll_count += 1
169
+ if poll_count % 3 == 0:
170
+ self.progress_callback(".", "")
171
+
172
+ token_data = {
173
+ "client_id": self.client_id,
174
+ "device_code": device_response.device_code,
175
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
176
+ }
177
+
178
+ cmd = self.curl_commands.post(
179
+ url=self.TOKEN_URL,
180
+ data=json.dumps(token_data),
181
+ headers={
182
+ "Accept": "application/json",
183
+ "Content-Type": "application/json",
184
+ },
185
+ )
186
+
187
+ result = self.executor.execute(cmd)
188
+
189
+ if result.success and result.stdout:
190
+ response_body, _ = self.curl_commands.parse_curl_response_with_status(
191
+ result.stdout
192
+ )
193
+ response_data = json.loads(response_body)
194
+
195
+ # Success - token received
196
+ if "access_token" in response_data:
197
+ self.progress_callback("\n", "\n")
198
+ return response_data["access_token"]
199
+
200
+ # Handle errors
201
+ error = response_data.get("error")
202
+
203
+ if error == "authorization_pending":
204
+ # Keep polling
205
+ continue
206
+ elif error == "slow_down":
207
+ # Increase interval as requested by GitHub
208
+ interval += 5
209
+ continue
210
+ elif error == "expired_token":
211
+ self.progress_callback(
212
+ "\n[red]❌ Device code expired. Please try again.[/red]", "\n"
213
+ )
214
+ return None
215
+ elif error == "access_denied":
216
+ self.progress_callback(
217
+ "\n[red]❌ Authorization denied by user.[/red]", "\n"
218
+ )
219
+ return None
220
+ else:
221
+ logger.error(f"Unknown error during polling: {error}")
222
+ self.progress_callback(
223
+ f"\n[red]❌ Authorization failed: {error}[/red]", "\n"
224
+ )
225
+ return None
226
+
227
+ self.progress_callback("\n[red]❌ Authorization timed out.[/red]", "\n")
228
+ return None
229
+
230
+ def test_token(self, token: str) -> bool:
231
+ """Test if token is valid by making a test API call."""
232
+ try:
233
+ cmd = self.curl_commands.get(
234
+ url="https://api.github.com/user",
235
+ headers={"Authorization": f"Bearer {token}"},
236
+ )
237
+
238
+ result = self.executor.execute(cmd)
239
+ return (
240
+ result.success
241
+ and result.stdout is not None
242
+ and "login" in result.stdout
243
+ )
244
+
245
+ except Exception as e:
246
+ logger.error(f"Token validation failed: {e}")
247
+ return False
@@ -0,0 +1,24 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class GitHubOAuthConfig:
7
+ """Configuration for GitHub OAuth (Device Flow).
8
+
9
+ Device Flow is designed for CLIs and doesn't require a client secret.
10
+ The client_id is public and safe to distribute in the codebase.
11
+ """
12
+ # Public client ID - safe to distribute
13
+ client_id: str = "Ov23liIBTV75Sjfu4Pay"
14
+ scopes: str = "repo"
15
+
16
+ # URLs for device flow
17
+ device_code_url: str = "https://github.com/login/device/code"
18
+ token_url: str = "https://github.com/login/oauth/access_token"
19
+
20
+ def __post_init__(self):
21
+ """Override client_id from env if present (for testing/development)."""
22
+ env_client_id = os.getenv("ER_GITHUB_OAUTH_CLIENT_ID")
23
+ if env_client_id:
24
+ self.client_id = env_client_id