hop3-cli 0.4.0b1__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 (32) hide show
  1. hop3_cli-0.4.0b1/PKG-INFO +150 -0
  2. hop3_cli-0.4.0b1/README.md +130 -0
  3. hop3_cli-0.4.0b1/pyproject.toml +51 -0
  4. hop3_cli-0.4.0b1/src/hop3_cli/__init__.py +1 -0
  5. hop3_cli-0.4.0b1/src/hop3_cli/commands/__init__.py +47 -0
  6. hop3_cli-0.4.0b1/src/hop3_cli/commands/arguments.py +190 -0
  7. hop3_cli-0.4.0b1/src/hop3_cli/commands/destructive.py +100 -0
  8. hop3_cli-0.4.0b1/src/hop3_cli/commands/flags.py +122 -0
  9. hop3_cli-0.4.0b1/src/hop3_cli/commands/help.py +182 -0
  10. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/__init__.py +99 -0
  11. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/auth_cmd.py +52 -0
  12. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/help_text.py +92 -0
  13. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/init_cmd.py +164 -0
  14. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/login_cmd.py +460 -0
  15. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/settings_cmd.py +132 -0
  16. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/ssh_ops.py +204 -0
  17. hop3_cli-0.4.0b1/src/hop3_cli/commands/local/version_cmd.py +25 -0
  18. hop3_cli-0.4.0b1/src/hop3_cli/config.py +189 -0
  19. hop3_cli-0.4.0b1/src/hop3_cli/exceptions.py +6 -0
  20. hop3_cli-0.4.0b1/src/hop3_cli/exit_codes.py +102 -0
  21. hop3_cli-0.4.0b1/src/hop3_cli/main.py +226 -0
  22. hop3_cli-0.4.0b1/src/hop3_cli/rpc/__init__.py +29 -0
  23. hop3_cli-0.4.0b1/src/hop3_cli/rpc/client.py +314 -0
  24. hop3_cli-0.4.0b1/src/hop3_cli/rpc/responses.py +179 -0
  25. hop3_cli-0.4.0b1/src/hop3_cli/rpc/tunnel.py +177 -0
  26. hop3_cli-0.4.0b1/src/hop3_cli/tokens.py +26 -0
  27. hop3_cli-0.4.0b1/src/hop3_cli/types.py +12 -0
  28. hop3_cli-0.4.0b1/src/hop3_cli/ui/__init__.py +30 -0
  29. hop3_cli-0.4.0b1/src/hop3_cli/ui/console.py +19 -0
  30. hop3_cli-0.4.0b1/src/hop3_cli/ui/messages.py +40 -0
  31. hop3_cli-0.4.0b1/src/hop3_cli/ui/prompts.py +90 -0
  32. hop3_cli-0.4.0b1/src/hop3_cli/ui/rich_printer.py +247 -0
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.3
2
+ Name: hop3-cli
3
+ Version: 0.4.0b1
4
+ Summary:
5
+ Author: Stefane Fermigier
6
+ Author-email: Stefane Fermigier <sf@abilian.com>
7
+ License: MIT
8
+ Requires-Dist: toml>=0.10.2,<1.0.0
9
+ Requires-Dist: jsonrpcclient>=4.0.3,<5.0.0
10
+ Requires-Dist: requests>=2.32.3,<3.0.0
11
+ Requires-Dist: tabulate>=0.9.0,<1.0.0
12
+ Requires-Dist: rich>=13.0.0
13
+ Requires-Dist: sshtunnel>=0.4.0
14
+ Requires-Dist: paramiko>=2.11.0,<3.0.0
15
+ Requires-Dist: loguru>=0.7.3
16
+ Requires-Dist: pathspec>=0.12.1
17
+ Requires-Dist: platformdirs>=4.3.8
18
+ Requires-Python: >=3.10, <4.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # hop3-cli
22
+
23
+ Command-line interface for interacting with Hop3 servers.
24
+
25
+ ## Overview
26
+
27
+ hop3-cli is a thin client that communicates with hop3-server via JSON-RPC over HTTP or SSH tunneling. It provides a familiar Heroku-like CLI experience for deploying and managing applications.
28
+
29
+ ## Features
30
+
31
+ - **Application management** - Deploy, start, stop, restart, and scale applications
32
+ - **Environment variables** - Securely manage app configuration
33
+ - **Log streaming** - View real-time application logs
34
+ - **Addon management** - Provision and manage backing services (PostgreSQL, Redis, MySQL)
35
+ - **SSH tunneling** - Secure communication with remote servers
36
+ - **Multiple output formats** - Human-readable, JSON, or quiet mode
37
+
38
+ ## Installation
39
+
40
+ ### From PyPI (end users)
41
+
42
+ ```bash
43
+ pip install hop3-cli
44
+ ```
45
+
46
+ ### For development
47
+
48
+ ```bash
49
+ # From workspace root
50
+ cd packages/hop3-cli
51
+ uv pip install -e ".[dev]"
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ```bash
57
+ # Configure server connection
58
+ export HOP3_SERVER_URL="https://hop3.example.com"
59
+ # Or use SSH tunneling
60
+ export HOP3_SERVER="user@hop3.example.com"
61
+
62
+ # Authenticate
63
+ hop3 auth login
64
+
65
+ # Deploy an application
66
+ cd my-app
67
+ hop3 deploy
68
+
69
+ # View logs
70
+ hop3 logs my-app
71
+
72
+ # List applications
73
+ hop3 apps
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ Configuration can be set via environment variables or config file (`~/.config/hop3/config.toml`).
79
+
80
+ | Variable | Description | Default |
81
+ |----------|-------------|---------|
82
+ | `HOP3_SERVER_URL` | Server URL (HTTP mode) | - |
83
+ | `HOP3_SERVER` | Server hostname (SSH mode) | - |
84
+ | `HOP3_AUTH_TOKEN` | Authentication token | - |
85
+ | `HOP3_CONFIG_DIR` | Config directory | `~/.config/hop3` |
86
+
87
+ ## Architecture
88
+
89
+ ```
90
+ hop3-cli/
91
+ ├── src/hop3_cli/
92
+ │ ├── main.py # Entry point, argument parsing
93
+ │ ├── config.py # Configuration management
94
+ │ ├── tunnel.py # SSH tunnel management
95
+ │ ├── rpc/
96
+ │ │ └── client.py # JSON-RPC client
97
+ │ ├── commands/
98
+ │ │ ├── local.py # Local commands (init, config)
99
+ │ │ └── help.py # Help system
100
+ │ └── ui/
101
+ │ ├── console.py # Output formatting
102
+ │ └── prompts.py # Interactive prompts
103
+ └── tests/
104
+ ```
105
+
106
+ **Communication flow:**
107
+ ```
108
+ User → CLI → [SSH Tunnel] → HTTP → hop3-server → JSON-RPC response → CLI → User
109
+ ```
110
+
111
+ ## Development
112
+
113
+ ### Running tests
114
+
115
+ ```bash
116
+ # From package directory
117
+ uv run pytest tests/ -v
118
+
119
+ # With coverage
120
+ uv run pytest tests/ --cov=hop3_cli
121
+ ```
122
+
123
+ ### Code quality
124
+
125
+ ```bash
126
+ # Lint
127
+ uv run ruff check src/
128
+
129
+ # Format
130
+ uv run ruff format src/
131
+
132
+ # Type check
133
+ uv run pyright src/
134
+ ```
135
+
136
+ ## Documentation
137
+
138
+ - **User Guide**: [Main documentation](../../docs/src/guide.md)
139
+ - **CLI Reference**: [Command reference](../../docs/src/cli-reference.md)
140
+ - **System Architecture**: [Architecture overview](../../docs/src/dev/architecture.md)
141
+ - **Package Internals**: [Deep-dive documentation](./docs/internals.md)
142
+
143
+ ## Related Packages
144
+
145
+ - [hop3-server](../hop3-server/) - The server that hop3-cli communicates with
146
+ - [hop3-tui](../hop3-tui/) - Alternative terminal UI interface
147
+
148
+ ## License
149
+
150
+ Apache-2.0 - Copyright (c) 2024-2025, Abilian SAS
@@ -0,0 +1,130 @@
1
+ # hop3-cli
2
+
3
+ Command-line interface for interacting with Hop3 servers.
4
+
5
+ ## Overview
6
+
7
+ hop3-cli is a thin client that communicates with hop3-server via JSON-RPC over HTTP or SSH tunneling. It provides a familiar Heroku-like CLI experience for deploying and managing applications.
8
+
9
+ ## Features
10
+
11
+ - **Application management** - Deploy, start, stop, restart, and scale applications
12
+ - **Environment variables** - Securely manage app configuration
13
+ - **Log streaming** - View real-time application logs
14
+ - **Addon management** - Provision and manage backing services (PostgreSQL, Redis, MySQL)
15
+ - **SSH tunneling** - Secure communication with remote servers
16
+ - **Multiple output formats** - Human-readable, JSON, or quiet mode
17
+
18
+ ## Installation
19
+
20
+ ### From PyPI (end users)
21
+
22
+ ```bash
23
+ pip install hop3-cli
24
+ ```
25
+
26
+ ### For development
27
+
28
+ ```bash
29
+ # From workspace root
30
+ cd packages/hop3-cli
31
+ uv pip install -e ".[dev]"
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ # Configure server connection
38
+ export HOP3_SERVER_URL="https://hop3.example.com"
39
+ # Or use SSH tunneling
40
+ export HOP3_SERVER="user@hop3.example.com"
41
+
42
+ # Authenticate
43
+ hop3 auth login
44
+
45
+ # Deploy an application
46
+ cd my-app
47
+ hop3 deploy
48
+
49
+ # View logs
50
+ hop3 logs my-app
51
+
52
+ # List applications
53
+ hop3 apps
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ Configuration can be set via environment variables or config file (`~/.config/hop3/config.toml`).
59
+
60
+ | Variable | Description | Default |
61
+ |----------|-------------|---------|
62
+ | `HOP3_SERVER_URL` | Server URL (HTTP mode) | - |
63
+ | `HOP3_SERVER` | Server hostname (SSH mode) | - |
64
+ | `HOP3_AUTH_TOKEN` | Authentication token | - |
65
+ | `HOP3_CONFIG_DIR` | Config directory | `~/.config/hop3` |
66
+
67
+ ## Architecture
68
+
69
+ ```
70
+ hop3-cli/
71
+ ├── src/hop3_cli/
72
+ │ ├── main.py # Entry point, argument parsing
73
+ │ ├── config.py # Configuration management
74
+ │ ├── tunnel.py # SSH tunnel management
75
+ │ ├── rpc/
76
+ │ │ └── client.py # JSON-RPC client
77
+ │ ├── commands/
78
+ │ │ ├── local.py # Local commands (init, config)
79
+ │ │ └── help.py # Help system
80
+ │ └── ui/
81
+ │ ├── console.py # Output formatting
82
+ │ └── prompts.py # Interactive prompts
83
+ └── tests/
84
+ ```
85
+
86
+ **Communication flow:**
87
+ ```
88
+ User → CLI → [SSH Tunnel] → HTTP → hop3-server → JSON-RPC response → CLI → User
89
+ ```
90
+
91
+ ## Development
92
+
93
+ ### Running tests
94
+
95
+ ```bash
96
+ # From package directory
97
+ uv run pytest tests/ -v
98
+
99
+ # With coverage
100
+ uv run pytest tests/ --cov=hop3_cli
101
+ ```
102
+
103
+ ### Code quality
104
+
105
+ ```bash
106
+ # Lint
107
+ uv run ruff check src/
108
+
109
+ # Format
110
+ uv run ruff format src/
111
+
112
+ # Type check
113
+ uv run pyright src/
114
+ ```
115
+
116
+ ## Documentation
117
+
118
+ - **User Guide**: [Main documentation](../../docs/src/guide.md)
119
+ - **CLI Reference**: [Command reference](../../docs/src/cli-reference.md)
120
+ - **System Architecture**: [Architecture overview](../../docs/src/dev/architecture.md)
121
+ - **Package Internals**: [Deep-dive documentation](./docs/internals.md)
122
+
123
+ ## Related Packages
124
+
125
+ - [hop3-server](../hop3-server/) - The server that hop3-cli communicates with
126
+ - [hop3-tui](../hop3-tui/) - Alternative terminal UI interface
127
+
128
+ ## License
129
+
130
+ Apache-2.0 - Copyright (c) 2024-2025, Abilian SAS
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "hop3-cli"
3
+ version = "0.4.0.beta1"
4
+ authors = [
5
+ {name = "Stefane Fermigier", email = "sf@abilian.com"},
6
+ ]
7
+ license = {text = "MIT"}
8
+ requires-python = "<4.0,>=3.10"
9
+ dependencies = [
10
+ # Configuration
11
+ "toml<1.0.0,>=0.10.2",
12
+ # Communication
13
+ "jsonrpcclient<5.0.0,>=4.0.3",
14
+ "requests<3.0.0,>=2.32.3",
15
+ # Presentation
16
+ "tabulate<1.0.0,>=0.9.0",
17
+ "rich>=13.0.0", # Rich formatting, tables, progress bars, colors
18
+ # SSH tunneling - pin paramiko to 2.x for DSS key compatibility with sshtunnel
19
+ "sshtunnel>=0.4.0",
20
+ "paramiko>=2.11.0,<3.0.0",
21
+ "loguru>=0.7.3",
22
+ "pathspec>=0.12.1",
23
+ "platformdirs>=4.3.8",
24
+ ]
25
+ description = ""
26
+ readme = "README.md"
27
+
28
+ [project.scripts]
29
+ hop = "hop3_cli.main:main"
30
+ hop3 = "hop3_cli.main:main"
31
+
32
+ # ----------------------------------------
33
+
34
+ [build-system]
35
+ requires = ["uv-build>=0.8.4,<0.9.0"]
36
+ build-backend = "uv_build"
37
+
38
+
39
+ [tool.pytest.ini_options]
40
+ filterwarnings = [
41
+ # paramiko uses TripleDES which cryptography has deprecated
42
+ "ignore:TripleDES has been moved:cryptography.utils.CryptographyDeprecationWarning",
43
+ ]
44
+
45
+ # Was:
46
+ #[build-system]
47
+ #requires = ["pdm-backend"]
48
+ #build-backend = "pdm.backend"
49
+ #
50
+ #[tool.pdm.build]
51
+ #includes = ["src/hop3_cli"]
@@ -0,0 +1 @@
1
+ # Copyright (c) 2023-2025, Abilian SAS
@@ -0,0 +1,47 @@
1
+ # Copyright (c) 2025, Abilian SAS
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Command processing for the Hop3 CLI.
6
+
7
+ This package handles command parsing and local command execution:
8
+ - local: Commands handled locally without server RPC
9
+ - help: Help flag handling and help output injection
10
+ - destructive: Confirmation prompts for destructive commands
11
+ - flags: CLI flag parsing (--json, --quiet, -y, etc.)
12
+ - arguments: Argument generation (e.g., deploy archive)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from .arguments import generate_archive, get_extra_args, pack_repository
18
+ from .destructive import confirm_destructive_action, is_destructive_command
19
+ from .flags import CliFlags, parse_flags
20
+ from .help import (
21
+ handle_help_flags,
22
+ inject_local_commands_into_help,
23
+ is_help_command,
24
+ )
25
+ from .local import (
26
+ LOCAL_COMMANDS,
27
+ LOCAL_COMMANDS_INFO,
28
+ handle_local_command,
29
+ is_local_command,
30
+ )
31
+
32
+ __all__ = [
33
+ "LOCAL_COMMANDS",
34
+ "LOCAL_COMMANDS_INFO",
35
+ "CliFlags",
36
+ "confirm_destructive_action",
37
+ "generate_archive",
38
+ "get_extra_args",
39
+ "handle_help_flags",
40
+ "handle_local_command",
41
+ "inject_local_commands_into_help",
42
+ "is_destructive_command",
43
+ "is_help_command",
44
+ "is_local_command",
45
+ "pack_repository",
46
+ "parse_flags",
47
+ ]
@@ -0,0 +1,190 @@
1
+ # Copyright (c) 2025, Abilian SAS
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Argument generation for CLI commands."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import io
11
+ import tarfile
12
+ from pathlib import Path
13
+
14
+ import pathspec
15
+
16
+ from hop3_cli.types import JsonDict
17
+
18
+ __all__ = ["generate_archive", "get_extra_args", "pack_repository"]
19
+
20
+
21
+ def get_extra_args(args: list[str], verbosity: int = 1) -> JsonDict:
22
+ """Generate a dictionary of extra arguments for RPC commands.
23
+
24
+ Args:
25
+ args: Command-line arguments
26
+ verbosity: Verbosity level (0=quiet, 1=normal, 2=verbose, 3=debug)
27
+
28
+ Returns:
29
+ Dictionary with extra arguments. Verbosity is always included as it's
30
+ used by the server to set the logging context for all commands.
31
+ """
32
+ # Always include verbosity - server extracts it and uses it as context
33
+ extra_args: JsonDict = {"verbosity": verbosity}
34
+
35
+ if not args:
36
+ return extra_args
37
+
38
+ command = args[0]
39
+
40
+ match command:
41
+ case "deploy":
42
+ # Parse deploy-specific flags
43
+ # args[0]="deploy", args[1]=app_name, remaining args may include --env and directory
44
+ env_vars, remaining_args = _parse_deploy_args(args[1:])
45
+
46
+ # Directory is the last non-flag argument (if any)
47
+ directory = Path(remaining_args[-1]) if len(remaining_args) > 1 else Path()
48
+ extra_args["repository"] = pack_repository(directory)
49
+
50
+ # Include env vars if any were specified
51
+ if env_vars:
52
+ extra_args["env_vars"] = env_vars
53
+
54
+ return extra_args
55
+
56
+
57
+ def _parse_deploy_args(args: list[str]) -> tuple[dict[str, str], list[str]]:
58
+ """Parse deploy command arguments, extracting --env flags.
59
+
60
+ Args:
61
+ args: Arguments after 'deploy' command (app_name, --env flags, directory)
62
+
63
+ Returns:
64
+ Tuple of (env_vars dict, remaining args without --env flags)
65
+
66
+ Example:
67
+ >>> _parse_deploy_args(['myapp', '--env', 'FOO=bar', '--env', 'BAZ=qux', '.'])
68
+ ({'FOO': 'bar', 'BAZ': 'qux'}, ['myapp', '.'])
69
+ """
70
+ env_vars: dict[str, str] = {}
71
+ remaining: list[str] = []
72
+ i = 0
73
+
74
+ while i < len(args):
75
+ arg = args[i]
76
+
77
+ if arg in {"--env", "-e"}:
78
+ # Next argument should be KEY=VALUE
79
+ if i + 1 < len(args):
80
+ env_spec = args[i + 1]
81
+ if "=" in env_spec:
82
+ key, _, value = env_spec.partition("=")
83
+ env_vars[key] = value
84
+ i += 2
85
+ else:
86
+ i += 1 # Skip malformed --env without value
87
+ elif arg.startswith("--env="):
88
+ # Handle --env=KEY=VALUE format
89
+ env_spec = arg[6:] # Remove --env=
90
+ if "=" in env_spec:
91
+ key, _, value = env_spec.partition("=")
92
+ env_vars[key] = value
93
+ i += 1
94
+ else:
95
+ remaining.append(arg)
96
+ i += 1
97
+
98
+ return env_vars, remaining
99
+
100
+
101
+ def pack_repository(directory: Path = Path()) -> str:
102
+ """Pack a directory into a base64-encoded tar.gz archive.
103
+
104
+ Args:
105
+ directory: Directory to pack (defaults to current directory)
106
+
107
+ Returns:
108
+ Base64-encoded tar.gz archive
109
+ """
110
+ tar_gz = generate_archive(directory)
111
+ return base64.b64encode(tar_gz).decode("ascii")
112
+
113
+
114
+ def generate_archive(source_dir: Path) -> bytes:
115
+ """
116
+ Creates an in-memory tar.gz archive of a source directory as a bytes object,
117
+ excluding files and directories specified in a .gitignore file.
118
+
119
+ Args:
120
+ source_dir: The path to the directory to archive.
121
+
122
+ Returns:
123
+ The content of the .tar.gz archive as a bytes object.
124
+
125
+ Raises:
126
+ ValueError: If the source_dir is not a valid directory.
127
+ FileNotFoundError: If the source_dir does not exist.
128
+ """
129
+ source_dir = Path(source_dir).resolve()
130
+
131
+ if not source_dir.exists():
132
+ msg = f"Source directory not found: {source_dir}"
133
+ raise FileNotFoundError(msg)
134
+ if not source_dir.is_dir():
135
+ msg = f"Source path is not a directory: {source_dir}"
136
+ raise ValueError(msg)
137
+
138
+ # --- 1. Load .gitignore rules ---
139
+ spec = get_ignored_spec(source_dir)
140
+
141
+ # --- 2. Walk the directory and gather files to include ---
142
+ files_to_add = get_files_to_add(source_dir, spec)
143
+
144
+ # --- 3. Create the tar.gz archive in memory ---
145
+ fileobj = io.BytesIO()
146
+
147
+ # The 'w:gz' mode creates a gzip-compressed tar file.
148
+ # We pass our BytesIO object as the file to write to.
149
+ with tarfile.open(fileobj=fileobj, mode="w:gz") as tar:
150
+ for file_path in files_to_add:
151
+ relative_path = file_path.relative_to(source_dir)
152
+ arcname = Path() / relative_path
153
+ tar.add(file_path, arcname=str(arcname))
154
+
155
+ return fileobj.getvalue()
156
+
157
+
158
+ def get_ignored_spec(source_dir: Path) -> pathspec.PathSpec | None:
159
+ """Load .gitignore rules from a directory."""
160
+ gitignore_path = source_dir / ".gitignore"
161
+ spec: pathspec.PathSpec | None = None
162
+ if gitignore_path.is_file():
163
+ with gitignore_path.open(encoding="utf-8") as f:
164
+ spec = pathspec.PathSpec.from_lines("gitignore", f)
165
+ return spec
166
+
167
+
168
+ def get_files_to_add(source_dir: Path, spec: pathspec.PathSpec | None) -> list[Path]:
169
+ """Get list of files to add to archive, excluding gitignored files."""
170
+ files_to_add: list[Path] = []
171
+ for file_path in source_dir.rglob("*"):
172
+ relative_path = file_path.relative_to(source_dir)
173
+ relative_str = str(relative_path)
174
+
175
+ # Always exclude .git directory (not deployment material)
176
+ if relative_str.startswith(".git") and (
177
+ relative_str == ".git" or relative_str.startswith(".git/")
178
+ ):
179
+ continue
180
+
181
+ # Let pathspec determine if the file should be ignored
182
+ if spec and spec.match_file(relative_str):
183
+ continue
184
+
185
+ # We only add files to the tar, not directories
186
+ if not file_path.is_file():
187
+ continue
188
+
189
+ files_to_add.append(file_path)
190
+ return files_to_add
@@ -0,0 +1,100 @@
1
+ # Copyright (c) 2025, Abilian SAS
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Destructive command handling and confirmation prompts."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from hop3_cli.ui.prompts import confirm, show_destructive_warning, type_to_confirm
12
+
13
+ if TYPE_CHECKING:
14
+ from hop3_cli.ui.rich_printer import RichPrinter
15
+
16
+
17
+ def is_destructive_command(cli_args: list[str]) -> bool:
18
+ """Check if the command is destructive (requires confirmation).
19
+
20
+ Args:
21
+ cli_args: Command-line arguments
22
+
23
+ Returns:
24
+ True if command is destructive, False otherwise
25
+ """
26
+ if not cli_args:
27
+ return False
28
+
29
+ command = cli_args[0]
30
+
31
+ # List of destructive commands that require confirmation
32
+ destructive_commands = {
33
+ "app:destroy",
34
+ "destroy", # Alias for app:destroy
35
+ "backup:delete",
36
+ "services:destroy",
37
+ }
38
+
39
+ return command in destructive_commands
40
+
41
+
42
+ def confirm_destructive_action(cli_args: list[str], printer: RichPrinter) -> bool:
43
+ """Prompt user to confirm a destructive action.
44
+
45
+ Args:
46
+ cli_args: Command-line arguments
47
+ printer: Printer for output (for JSON mode detection)
48
+
49
+ Returns:
50
+ True if user confirmed, False if cancelled
51
+ """
52
+ if printer.json_output:
53
+ # In JSON mode, auto-confirm (user should use -y flag)
54
+ return True
55
+
56
+ command = cli_args[0]
57
+ args = cli_args[1:]
58
+
59
+ # app:destroy or destroy command - requires type-to-confirm
60
+ if command in {"app:destroy", "destroy"}:
61
+ if not args:
62
+ # No app name provided, let server handle error
63
+ return True
64
+
65
+ app_name = args[0]
66
+ show_destructive_warning(
67
+ "destroy",
68
+ f"app '{app_name}'",
69
+ "All files, data, and configuration will be permanently deleted.",
70
+ )
71
+ return type_to_confirm(f"Type '{app_name}' to confirm:", app_name)
72
+
73
+ # backup:delete command
74
+ if command == "backup:delete":
75
+ if not args:
76
+ return True
77
+
78
+ backup_id = args[0]
79
+ show_destructive_warning(
80
+ "delete",
81
+ f"backup '{backup_id}'",
82
+ "This backup cannot be recovered once deleted.",
83
+ )
84
+ return confirm("Are you sure you want to delete this backup?")
85
+
86
+ # services:destroy command
87
+ if command == "services:destroy":
88
+ if not args:
89
+ return True
90
+
91
+ addon_name = args[0]
92
+ show_destructive_warning(
93
+ "destroy",
94
+ f"service '{addon_name}'",
95
+ "All data in this service will be permanently deleted.",
96
+ )
97
+ return type_to_confirm(f"Type '{addon_name}' to confirm:", addon_name)
98
+
99
+ # Unknown destructive command (shouldn't happen)
100
+ return confirm("This action cannot be undone. Continue?")