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.
- hop3_cli-0.4.0b1/PKG-INFO +150 -0
- hop3_cli-0.4.0b1/README.md +130 -0
- hop3_cli-0.4.0b1/pyproject.toml +51 -0
- hop3_cli-0.4.0b1/src/hop3_cli/__init__.py +1 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/__init__.py +47 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/arguments.py +190 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/destructive.py +100 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/flags.py +122 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/help.py +182 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/__init__.py +99 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/auth_cmd.py +52 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/help_text.py +92 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/init_cmd.py +164 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/login_cmd.py +460 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/settings_cmd.py +132 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/ssh_ops.py +204 -0
- hop3_cli-0.4.0b1/src/hop3_cli/commands/local/version_cmd.py +25 -0
- hop3_cli-0.4.0b1/src/hop3_cli/config.py +189 -0
- hop3_cli-0.4.0b1/src/hop3_cli/exceptions.py +6 -0
- hop3_cli-0.4.0b1/src/hop3_cli/exit_codes.py +102 -0
- hop3_cli-0.4.0b1/src/hop3_cli/main.py +226 -0
- hop3_cli-0.4.0b1/src/hop3_cli/rpc/__init__.py +29 -0
- hop3_cli-0.4.0b1/src/hop3_cli/rpc/client.py +314 -0
- hop3_cli-0.4.0b1/src/hop3_cli/rpc/responses.py +179 -0
- hop3_cli-0.4.0b1/src/hop3_cli/rpc/tunnel.py +177 -0
- hop3_cli-0.4.0b1/src/hop3_cli/tokens.py +26 -0
- hop3_cli-0.4.0b1/src/hop3_cli/types.py +12 -0
- hop3_cli-0.4.0b1/src/hop3_cli/ui/__init__.py +30 -0
- hop3_cli-0.4.0b1/src/hop3_cli/ui/console.py +19 -0
- hop3_cli-0.4.0b1/src/hop3_cli/ui/messages.py +40 -0
- hop3_cli-0.4.0b1/src/hop3_cli/ui/prompts.py +90 -0
- 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?")
|