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.
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/PKG-INFO +1 -1
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/pyproject.toml +1 -1
- easyrunner_cli-0.0.8.dev99/source/auth/__init__.py +10 -0
- easyrunner_cli-0.0.8.dev99/source/auth/auth_sub_command.py +238 -0
- easyrunner_cli-0.0.8.dev99/source/auth/github_device_flow.py +247 -0
- easyrunner_cli-0.0.8.dev99/source/auth/github_oauth_config.py +24 -0
- easyrunner_cli-0.0.8.dev97/source/auth_sub_command.py → easyrunner_cli-0.0.8.dev99/source/link_sub_command.py +81 -74
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/main.py +2 -2
- easyrunner_cli-0.0.8.dev97/source/auth/__init__.py +0 -12
- easyrunner_cli-0.0.8.dev97/source/auth/auth_sub_command.py +0 -226
- easyrunner_cli-0.0.8.dev97/source/auth/github_oauth_config.py +0 -24
- easyrunner_cli-0.0.8.dev97/source/auth/github_oauth_flow.py +0 -165
- easyrunner_cli-0.0.8.dev97/source/auth/oauth_callback_server.py +0 -109
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/README.md +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/cloud_provider_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/cloud_providers.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/cloud_providers/hetzner_provider.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/command_executor.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/command_executor_local.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/archive_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/caddy_api_curl_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/caddy_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/command_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/curl_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/dir_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/docker_compose_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/file_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/git_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ip_tables_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ip_tables_persistent_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/null_command.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/os_package_manager_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/podman_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ssh_agent_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/ssh_keygen_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/systemctl_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/user_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/base/utility_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/runnable_command_string.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/archive_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/caddy_api_curl_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/caddy_commands_container_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/curl_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/dir_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/docker_compose_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/file_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/git_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ip_tables_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ip_tables_persistent_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/os_package_manager_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/podman_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ssh_agent_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/ssh_keygen_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/systemctl_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/user_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/commands/ubuntu/utility_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/format_utils.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/http_client.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/known_host_ssh_keys.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_firewall_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_resource_api_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_resource_pulumi_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/cloud_virtual_machine_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/github/github_api_client.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/github/github_api_client_dtos.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/github/github_repo.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_firewall.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_firewall_rule.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_resource_factory.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_stack.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/cloud_resources/hetzner/hetzner_virtual_machine.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/caddy.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/directory.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/docker_compose.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/file.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/git_repo.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/host_server_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/ip_tables.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/os_package_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/os_resource_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/podman.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/podman_network.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/ssh_agent.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/systemd_service.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/os_resources/user.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/resource_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/resources/web_security_scanner.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/ssh.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/ssh_key.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/app.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/database_dto_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/data_models/server.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/db_config.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/db_ctx.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/easyrunner_store.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/json_encoder.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/object_id.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/store/uuid7.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/tool_paths.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/caddy/caddy_config.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/caddy/caddy_site.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_network.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_project.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_service.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/compose_project/compose_volume.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/cpu_arch_types.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/dir_info.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/dto_base.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/exec_result.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/file_info.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/json.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/jsonobject_to_dataclass.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/os_type.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/podman_network_driver.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/security_scan_result.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/ssh_key_type.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/easyrunner/types/vm_config.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/app_sub_command.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/auth/github_token_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/infrastructure_deps.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/license_sub_command.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/licensing/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/licensing/license_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/servers_sub_command.py +0 -0
- {easyrunner_cli-0.0.8.dev97 → easyrunner_cli-0.0.8.dev99}/source/ssh_config.py +0 -0
|
@@ -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
|