borealhost 0.1.0__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.
Potentially problematic release.
This version of borealhost might be problematic. Click here for more details.
- borealhost-0.1.0/.gitignore +36 -0
- borealhost-0.1.0/PKG-INFO +96 -0
- borealhost-0.1.0/README.md +69 -0
- borealhost-0.1.0/borealhost_cli/__init__.py +3 -0
- borealhost-0.1.0/borealhost_cli/commands/__init__.py +1 -0
- borealhost-0.1.0/borealhost_cli/commands/account.py +51 -0
- borealhost-0.1.0/borealhost_cli/commands/alerts.py +46 -0
- borealhost-0.1.0/borealhost_cli/commands/apps.py +43 -0
- borealhost-0.1.0/borealhost_cli/commands/auth.py +91 -0
- borealhost-0.1.0/borealhost_cli/commands/backups.py +35 -0
- borealhost-0.1.0/borealhost_cli/commands/cache.py +32 -0
- borealhost-0.1.0/borealhost_cli/commands/checkout.py +64 -0
- borealhost-0.1.0/borealhost_cli/commands/cloudflare.py +49 -0
- borealhost-0.1.0/borealhost_cli/commands/cron.py +37 -0
- borealhost-0.1.0/borealhost_cli/commands/db.py +64 -0
- borealhost-0.1.0/borealhost_cli/commands/dns.py +28 -0
- borealhost-0.1.0/borealhost_cli/commands/domains.py +138 -0
- borealhost-0.1.0/borealhost_cli/commands/files.py +90 -0
- borealhost-0.1.0/borealhost_cli/commands/firewall.py +37 -0
- borealhost-0.1.0/borealhost_cli/commands/ftp.py +50 -0
- borealhost-0.1.0/borealhost_cli/commands/keys.py +53 -0
- borealhost-0.1.0/borealhost_cli/commands/logs.py +22 -0
- borealhost-0.1.0/borealhost_cli/commands/modules.py +25 -0
- borealhost-0.1.0/borealhost_cli/commands/php.py +25 -0
- borealhost-0.1.0/borealhost_cli/commands/plans.py +30 -0
- borealhost-0.1.0/borealhost_cli/commands/sites.py +79 -0
- borealhost-0.1.0/borealhost_cli/commands/snapshots.py +91 -0
- borealhost-0.1.0/borealhost_cli/commands/ssh.py +45 -0
- borealhost-0.1.0/borealhost_cli/commands/ssl.py +21 -0
- borealhost-0.1.0/borealhost_cli/commands/system.py +32 -0
- borealhost-0.1.0/borealhost_cli/commands/wp.py +70 -0
- borealhost-0.1.0/borealhost_cli/config.py +69 -0
- borealhost-0.1.0/borealhost_cli/context.py +26 -0
- borealhost-0.1.0/borealhost_cli/helpers.py +32 -0
- borealhost-0.1.0/borealhost_cli/main.py +143 -0
- borealhost-0.1.0/borealhost_cli/output.py +111 -0
- borealhost-0.1.0/pyproject.toml +47 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
venv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
db.sqlite3
|
|
8
|
+
|
|
9
|
+
# Node
|
|
10
|
+
node_modules/
|
|
11
|
+
dist/
|
|
12
|
+
bun.lock
|
|
13
|
+
|
|
14
|
+
# Secrets
|
|
15
|
+
.env
|
|
16
|
+
*.key
|
|
17
|
+
*.pem
|
|
18
|
+
secrets/
|
|
19
|
+
.ssh/
|
|
20
|
+
|
|
21
|
+
# Claude Code
|
|
22
|
+
.claude/
|
|
23
|
+
.mcp.json
|
|
24
|
+
|
|
25
|
+
# Deploy artifacts & large files
|
|
26
|
+
*.tar.gz
|
|
27
|
+
*.tar
|
|
28
|
+
*.mo
|
|
29
|
+
|
|
30
|
+
# Backups
|
|
31
|
+
*.bak.*
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
.DS_Store
|
|
35
|
+
*.swp
|
|
36
|
+
*~
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: borealhost
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line tool for BorealHost.ai — agent-native web hosting
|
|
5
|
+
Project-URL: Homepage, https://borealhost.ai
|
|
6
|
+
Project-URL: Documentation, https://borealhost.ai/api/v1/docs/
|
|
7
|
+
Project-URL: Repository, https://github.com/alainsvrd/platform
|
|
8
|
+
Author-email: BorealHost <hello@borealhost.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: borealhost,cli,hosting
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: borealhost-sdk>=0.1.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: typer>=0.12.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# BorealHost CLI (`bh`)
|
|
29
|
+
|
|
30
|
+
Command-line tool for [BorealHost.ai](https://borealhost.ai) — agent-native web hosting.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install borealhost
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Authenticate
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bh auth register # new agent account
|
|
42
|
+
bh auth set-key bh_your_existing_key # existing account
|
|
43
|
+
bh auth whoami
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Keys are persisted to `~/.borealhost/config.toml` (chmod 600).
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Buy hosting
|
|
52
|
+
bh plans list
|
|
53
|
+
bh checkout create bh_site_starter_monthly
|
|
54
|
+
bh checkout update <id> --slug my-site --email you@example.com
|
|
55
|
+
bh checkout complete <id>
|
|
56
|
+
|
|
57
|
+
# Manage a site
|
|
58
|
+
bh sites list
|
|
59
|
+
bh sites status my-site
|
|
60
|
+
bh sites deploy my-site
|
|
61
|
+
bh files ls my-site /public_html
|
|
62
|
+
bh wp plugins list my-site
|
|
63
|
+
bh db query my-site wp_my_site "SELECT * FROM wp_users LIMIT 5"
|
|
64
|
+
|
|
65
|
+
# Snapshots
|
|
66
|
+
bh snapshots create my-site --description "before upgrade"
|
|
67
|
+
bh snapshots list my-site
|
|
68
|
+
|
|
69
|
+
# DNS
|
|
70
|
+
bh dns add my-site --type A --value 1.2.3.4 --subdomain www
|
|
71
|
+
|
|
72
|
+
# Domains
|
|
73
|
+
bh domains search example.com
|
|
74
|
+
bh domains register example.com --period 1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Output formats
|
|
78
|
+
|
|
79
|
+
All commands accept `--json`, `--yaml`, or `--table` (default for TTY):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bh sites list --json | jq '.[] | .slug'
|
|
83
|
+
bh plans list --yaml > plans.yaml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
When stdout is not a TTY, JSON is used automatically.
|
|
87
|
+
|
|
88
|
+
## Environment variables
|
|
89
|
+
|
|
90
|
+
- `BOREALHOST_API_KEY` — API key (overrides config file)
|
|
91
|
+
- `BOREALHOST_BASE_URL` — API base URL (default: `https://borealhost.ai`)
|
|
92
|
+
|
|
93
|
+
## Related
|
|
94
|
+
|
|
95
|
+
- [`borealhost-sdk`](https://pypi.org/project/borealhost-sdk/) — Python SDK
|
|
96
|
+
- [`borealhost-mcp`](https://pypi.org/project/borealhost-mcp/) — MCP server for AI agents
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# BorealHost CLI (`bh`)
|
|
2
|
+
|
|
3
|
+
Command-line tool for [BorealHost.ai](https://borealhost.ai) — agent-native web hosting.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install borealhost
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Authenticate
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bh auth register # new agent account
|
|
15
|
+
bh auth set-key bh_your_existing_key # existing account
|
|
16
|
+
bh auth whoami
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Keys are persisted to `~/.borealhost/config.toml` (chmod 600).
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Buy hosting
|
|
25
|
+
bh plans list
|
|
26
|
+
bh checkout create bh_site_starter_monthly
|
|
27
|
+
bh checkout update <id> --slug my-site --email you@example.com
|
|
28
|
+
bh checkout complete <id>
|
|
29
|
+
|
|
30
|
+
# Manage a site
|
|
31
|
+
bh sites list
|
|
32
|
+
bh sites status my-site
|
|
33
|
+
bh sites deploy my-site
|
|
34
|
+
bh files ls my-site /public_html
|
|
35
|
+
bh wp plugins list my-site
|
|
36
|
+
bh db query my-site wp_my_site "SELECT * FROM wp_users LIMIT 5"
|
|
37
|
+
|
|
38
|
+
# Snapshots
|
|
39
|
+
bh snapshots create my-site --description "before upgrade"
|
|
40
|
+
bh snapshots list my-site
|
|
41
|
+
|
|
42
|
+
# DNS
|
|
43
|
+
bh dns add my-site --type A --value 1.2.3.4 --subdomain www
|
|
44
|
+
|
|
45
|
+
# Domains
|
|
46
|
+
bh domains search example.com
|
|
47
|
+
bh domains register example.com --period 1
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Output formats
|
|
51
|
+
|
|
52
|
+
All commands accept `--json`, `--yaml`, or `--table` (default for TTY):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bh sites list --json | jq '.[] | .slug'
|
|
56
|
+
bh plans list --yaml > plans.yaml
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When stdout is not a TTY, JSON is used automatically.
|
|
60
|
+
|
|
61
|
+
## Environment variables
|
|
62
|
+
|
|
63
|
+
- `BOREALHOST_API_KEY` — API key (overrides config file)
|
|
64
|
+
- `BOREALHOST_BASE_URL` — API base URL (default: `https://borealhost.ai`)
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
|
|
68
|
+
- [`borealhost-sdk`](https://pypi.org/project/borealhost-sdk/) — Python SDK
|
|
69
|
+
- [`borealhost-mcp`](https://pypi.org/project/borealhost-mcp/) — MCP server for AI agents
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command groups — one module per functional area."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Account management: update profile, subscriptions, billing portal."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..helpers import run
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("update")
|
|
13
|
+
def update_cmd(
|
|
14
|
+
ctx: typer.Context,
|
|
15
|
+
email: Optional[str] = typer.Option(None, "--email"),
|
|
16
|
+
language: Optional[str] = typer.Option(None, "--language", help="en or fr."),
|
|
17
|
+
first_name: Optional[str] = typer.Option(None, "--first-name"),
|
|
18
|
+
last_name: Optional[str] = typer.Option(None, "--last-name"),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Update account profile fields."""
|
|
21
|
+
cli = ctx.obj
|
|
22
|
+
run(ctx, cli.client.update_account, email=email, language=language, first_name=first_name, last_name=last_name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("delete")
|
|
26
|
+
def delete_cmd(
|
|
27
|
+
ctx: typer.Context,
|
|
28
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Soft-delete (anonymize) the account. [red]Irreversible.[/red]"""
|
|
31
|
+
if not yes:
|
|
32
|
+
typer.confirm("Really delete this account? This cannot be undone.", abort=True)
|
|
33
|
+
cli = ctx.obj
|
|
34
|
+
run(ctx, cli.client.delete_account)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("subscriptions")
|
|
38
|
+
def subscriptions_cmd(ctx: typer.Context) -> None:
|
|
39
|
+
"""List all subscriptions for the account."""
|
|
40
|
+
cli = ctx.obj
|
|
41
|
+
run(ctx, cli.client.list_subscriptions, title="Subscriptions")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command("billing-portal")
|
|
45
|
+
def billing_portal_cmd(
|
|
46
|
+
ctx: typer.Context,
|
|
47
|
+
flow: Optional[str] = typer.Option(None, "--flow", help="Stripe portal flow type."),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Get the Stripe billing portal URL."""
|
|
50
|
+
cli = ctx.obj
|
|
51
|
+
run(ctx, cli.client.get_billing_portal, flow=flow)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Alert rules: list, create, delete."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..helpers import run
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(no_args_is_help=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command("list")
|
|
11
|
+
def list_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
12
|
+
"""List alert rules for a site."""
|
|
13
|
+
cli = ctx.obj
|
|
14
|
+
run(ctx, cli.client.list_alert_rules, slug)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("create")
|
|
18
|
+
def create_cmd(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
21
|
+
metric: str = typer.Argument(..., help="Metric: cpu, ram, disk, error_5xx, etc."),
|
|
22
|
+
threshold: float = typer.Argument(..., help="Threshold value."),
|
|
23
|
+
operator: str = typer.Option("gt", "--operator", help="gt, lt, gte, lte, eq."),
|
|
24
|
+
severity: str = typer.Option("warning", "--severity", help="info, warning, critical."),
|
|
25
|
+
cooldown_minutes: int = typer.Option(30, "--cooldown", help="Cooldown between alerts (minutes)."),
|
|
26
|
+
notify_email: bool = typer.Option(True, "--email/--no-email"),
|
|
27
|
+
notify_webhook: str = typer.Option("", "--webhook", help="Webhook URL."),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Create a new alert rule."""
|
|
30
|
+
cli = ctx.obj
|
|
31
|
+
run(
|
|
32
|
+
ctx, cli.client.create_alert_rule, slug, metric, threshold,
|
|
33
|
+
operator=operator, severity=severity, cooldown_minutes=cooldown_minutes,
|
|
34
|
+
notify_email=notify_email, notify_webhook=notify_webhook,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("delete")
|
|
39
|
+
def delete_cmd(
|
|
40
|
+
ctx: typer.Context,
|
|
41
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
42
|
+
rule_id: str = typer.Argument(..., help="Alert rule ID."),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Delete an alert rule."""
|
|
45
|
+
cli = ctx.obj
|
|
46
|
+
run(ctx, cli.client.delete_alert_rule, slug, rule_id)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""App installation on VPS sites (Ghost, WordPress, etc)."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..helpers import run
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(no_args_is_help=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command("list")
|
|
11
|
+
def list_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
12
|
+
"""List installed apps on a VPS site."""
|
|
13
|
+
cli = ctx.obj
|
|
14
|
+
run(ctx, cli.client.list_apps, slug)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("install")
|
|
18
|
+
def install_cmd(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
21
|
+
template: str = typer.Argument(..., help="Template name (e.g. 'ghost', 'wordpress')."),
|
|
22
|
+
app_name: str = typer.Argument(..., help="Unique app identifier within the site."),
|
|
23
|
+
db_type: str = typer.Option("none", "--db", help="Database: none, mysql, postgres."),
|
|
24
|
+
domain: str = typer.Option("", "--domain", help="Custom domain for this app."),
|
|
25
|
+
display_name: str = typer.Option("", "--display-name", help="Human-readable name."),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Install an app template on a VPS site."""
|
|
28
|
+
cli = ctx.obj
|
|
29
|
+
run(
|
|
30
|
+
ctx, cli.client.install_app, slug, template, app_name,
|
|
31
|
+
db_type=db_type, domain=domain, display_name=display_name,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("status")
|
|
36
|
+
def status_cmd(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
39
|
+
app_id: str = typer.Argument(..., help="App ID from `apps list`."),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Get install status and logs for an app."""
|
|
42
|
+
cli = ctx.obj
|
|
43
|
+
run(ctx, cli.client.get_app, slug, app_id)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Authentication commands: register, set-key, whoami, request-key, claim-key."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..config import load, save
|
|
8
|
+
from ..helpers import run
|
|
9
|
+
from ..output import emit, success, info
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("register")
|
|
15
|
+
def register_cmd(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
name: str = typer.Option("Agent Key", "--name", help="Human-readable key name."),
|
|
18
|
+
email: Optional[str] = typer.Option(None, "--email", help="Email to link to a human account."),
|
|
19
|
+
save_key: bool = typer.Option(True, "--save/--no-save", help="Persist the new key to the config file."),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Register a new agent account and receive an API key."""
|
|
22
|
+
cli = ctx.obj
|
|
23
|
+
result = cli.client.register(email=email, name=name)
|
|
24
|
+
if result and result.get("api_key"):
|
|
25
|
+
key = result["api_key"]
|
|
26
|
+
cli.client.set_api_key(key)
|
|
27
|
+
if save_key:
|
|
28
|
+
data = load()
|
|
29
|
+
data["api_key"] = key
|
|
30
|
+
save(data)
|
|
31
|
+
success(f"Saved new API key ({key[:10]}...) to ~/.borealhost/config.toml")
|
|
32
|
+
emit(result, cli.output_format)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("set-key")
|
|
36
|
+
def set_key_cmd(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
api_key: str = typer.Argument(..., help="Your BorealHost API key (bh_...)."),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Save an existing API key to ~/.borealhost/config.toml."""
|
|
41
|
+
if not api_key.startswith("bh_"):
|
|
42
|
+
typer.echo("error: keys must start with 'bh_'", err=True)
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
data = load()
|
|
45
|
+
data["api_key"] = api_key
|
|
46
|
+
save(data)
|
|
47
|
+
success(f"Saved API key ({api_key[:10]}...) to ~/.borealhost/config.toml")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command("whoami")
|
|
51
|
+
def whoami_cmd(ctx: typer.Context) -> None:
|
|
52
|
+
"""Show the account associated with the current API key."""
|
|
53
|
+
cli = ctx.obj
|
|
54
|
+
run(ctx, cli.client.whoami, title="Account")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("request-key")
|
|
58
|
+
def request_key_cmd(
|
|
59
|
+
ctx: typer.Context,
|
|
60
|
+
site_slug: str = typer.Argument(..., help="Site slug (you must be running on this container)."),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Request a claim token for challenge-response key provisioning (on-container)."""
|
|
63
|
+
cli = ctx.obj
|
|
64
|
+
run(ctx, cli.client.request_api_key, site_slug)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command("claim-key")
|
|
68
|
+
def claim_key_cmd(
|
|
69
|
+
ctx: typer.Context,
|
|
70
|
+
token: str = typer.Argument(..., help="Claim token from ~/.borealhost/.claim_token on the container."),
|
|
71
|
+
save_key: bool = typer.Option(True, "--save/--no-save", help="Persist the new key to the config file."),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Claim an API key with a claim token from the container."""
|
|
74
|
+
cli = ctx.obj
|
|
75
|
+
result = cli.client.claim_api_key(token)
|
|
76
|
+
if result and result.get("api_key"):
|
|
77
|
+
key = result["api_key"]
|
|
78
|
+
cli.client.set_api_key(key)
|
|
79
|
+
if save_key:
|
|
80
|
+
data = load()
|
|
81
|
+
data["api_key"] = key
|
|
82
|
+
save(data)
|
|
83
|
+
success(f"Saved claimed API key to ~/.borealhost/config.toml")
|
|
84
|
+
emit(result, cli.output_format)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("config-path")
|
|
88
|
+
def config_path_cmd() -> None:
|
|
89
|
+
"""Print the path to the CLI config file."""
|
|
90
|
+
from ..config import CONFIG_PATH
|
|
91
|
+
info(str(CONFIG_PATH))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""VPS backups: list, create, restore."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..helpers import run
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(no_args_is_help=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command("list")
|
|
11
|
+
def list_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
12
|
+
"""List backups for a site."""
|
|
13
|
+
cli = ctx.obj
|
|
14
|
+
run(ctx, cli.client.list_backups, slug)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("create")
|
|
18
|
+
def create_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
19
|
+
"""Create a new backup."""
|
|
20
|
+
cli = ctx.obj
|
|
21
|
+
run(ctx, cli.client.create_backup, slug)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("restore")
|
|
25
|
+
def restore_cmd(
|
|
26
|
+
ctx: typer.Context,
|
|
27
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
28
|
+
backup_id: str = typer.Argument(..., help="Backup ID."),
|
|
29
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Restore a backup. [red]Overwrites current state.[/red]"""
|
|
32
|
+
if not yes:
|
|
33
|
+
typer.confirm(f"Restore '{slug}' from backup {backup_id}?", abort=True)
|
|
34
|
+
cli = ctx.obj
|
|
35
|
+
run(ctx, cli.client.restore_backup, slug, backup_id)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Cache management: status, flush, toggle."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..helpers import run
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(no_args_is_help=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command("status")
|
|
11
|
+
def status_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
12
|
+
"""Show cache status for a site."""
|
|
13
|
+
cli = ctx.obj
|
|
14
|
+
run(ctx, cli.client.get_cache_status, slug)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("flush")
|
|
18
|
+
def flush_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
19
|
+
"""Flush all cache for a site."""
|
|
20
|
+
cli = ctx.obj
|
|
21
|
+
run(ctx, cli.client.flush_cache, slug)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("toggle")
|
|
25
|
+
def toggle_cmd(
|
|
26
|
+
ctx: typer.Context,
|
|
27
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
28
|
+
enable: bool = typer.Argument(..., help="true to enable, false to disable."),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Enable or disable caching for a site."""
|
|
31
|
+
cli = ctx.obj
|
|
32
|
+
run(ctx, cli.client.toggle_cache, slug, enable)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""ACP checkout flow: create, update, complete, status."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..helpers import run
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("create")
|
|
13
|
+
def create_cmd(
|
|
14
|
+
ctx: typer.Context,
|
|
15
|
+
sku: str = typer.Argument(..., help="Plan SKU (e.g. 'bh_site_starter_monthly')."),
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Start a checkout session for a plan SKU."""
|
|
18
|
+
cli = ctx.obj
|
|
19
|
+
run(ctx, cli.client.create_checkout, sku, title="Checkout")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command("update")
|
|
23
|
+
def update_cmd(
|
|
24
|
+
ctx: typer.Context,
|
|
25
|
+
checkout_id: str = typer.Argument(..., help="Checkout session ID."),
|
|
26
|
+
slug: Optional[str] = typer.Option(None, "--slug", help="Requested site slug."),
|
|
27
|
+
email: Optional[str] = typer.Option(None, "--email", help="Buyer email."),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Set buyer email and/or desired site slug on a checkout."""
|
|
30
|
+
cli = ctx.obj
|
|
31
|
+
run(ctx, cli.client.update_checkout, checkout_id, buyer_email=email, requested_slug=slug)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("complete")
|
|
35
|
+
def complete_cmd(
|
|
36
|
+
ctx: typer.Context,
|
|
37
|
+
checkout_id: str = typer.Argument(..., help="Checkout session ID."),
|
|
38
|
+
payment_method: str = typer.Option(
|
|
39
|
+
"stripe_checkout", "--payment-method",
|
|
40
|
+
help="stripe_checkout (hosted URL) or stripe_payment_method.",
|
|
41
|
+
),
|
|
42
|
+
payment_method_id: Optional[str] = typer.Option(
|
|
43
|
+
None, "--payment-method-id", help="Stripe pm_... (for stripe_payment_method)."
|
|
44
|
+
),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Complete checkout and start provisioning."""
|
|
47
|
+
cli = ctx.obj
|
|
48
|
+
run(
|
|
49
|
+
ctx,
|
|
50
|
+
cli.client.complete_checkout,
|
|
51
|
+
checkout_id,
|
|
52
|
+
payment_method=payment_method,
|
|
53
|
+
payment_method_id=payment_method_id,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("status")
|
|
58
|
+
def status_cmd(
|
|
59
|
+
ctx: typer.Context,
|
|
60
|
+
checkout_id: str = typer.Argument(..., help="Checkout session ID."),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Poll the status of a checkout (used after stripe_checkout payment)."""
|
|
63
|
+
cli = ctx.obj
|
|
64
|
+
run(ctx, cli.client.get_checkout, checkout_id, title="Checkout status")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Cloudflare CDN: proxy status, cache purge, analytics."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..helpers import run
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("proxy-status")
|
|
13
|
+
def proxy_status_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
14
|
+
"""Show Cloudflare proxy status for a site."""
|
|
15
|
+
cli = ctx.obj
|
|
16
|
+
run(ctx, cli.client.cf_proxy_status, slug)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("proxy-set")
|
|
20
|
+
def proxy_set_cmd(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
23
|
+
proxied: bool = typer.Argument(..., help="true to proxy through CF, false for DNS-only."),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Toggle Cloudflare proxying on/off for a site."""
|
|
26
|
+
cli = ctx.obj
|
|
27
|
+
run(ctx, cli.client.cf_set_proxy, slug, proxied)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.command("cache-purge")
|
|
31
|
+
def cache_purge_cmd(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
34
|
+
url: Optional[List[str]] = typer.Option(None, "--url", help="Specific URLs to purge (repeat). Default: purge all."),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Purge Cloudflare cache for a site."""
|
|
37
|
+
cli = ctx.obj
|
|
38
|
+
run(ctx, cli.client.cf_cache_purge, slug, urls=list(url) if url else None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("analytics")
|
|
42
|
+
def analytics_cmd(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
45
|
+
minutes: int = typer.Option(1440, "--minutes", help="Time window in minutes."),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Get Cloudflare analytics for a site."""
|
|
48
|
+
cli = ctx.obj
|
|
49
|
+
run(ctx, cli.client.cf_analytics, slug, minutes=minutes)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Cron jobs: list, add, delete."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..helpers import run
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(no_args_is_help=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command("list")
|
|
11
|
+
def list_cmd(ctx: typer.Context, slug: str = typer.Argument(..., help="Site slug.")) -> None:
|
|
12
|
+
"""List cron jobs for a site."""
|
|
13
|
+
cli = ctx.obj
|
|
14
|
+
run(ctx, cli.client.list_cron, slug)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("add")
|
|
18
|
+
def add_cmd(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
21
|
+
schedule: str = typer.Argument(..., help="Cron schedule, e.g. '0 * * * *'."),
|
|
22
|
+
command: str = typer.Argument(..., help="Command to run."),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Add a cron job."""
|
|
25
|
+
cli = ctx.obj
|
|
26
|
+
run(ctx, cli.client.add_cron, slug, schedule, command)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("delete")
|
|
30
|
+
def delete_cmd(
|
|
31
|
+
ctx: typer.Context,
|
|
32
|
+
slug: str = typer.Argument(..., help="Site slug."),
|
|
33
|
+
line_number: int = typer.Argument(..., help="Crontab line number."),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Delete a cron job by line number."""
|
|
36
|
+
cli = ctx.obj
|
|
37
|
+
run(ctx, cli.client.delete_cron, slug, line_number)
|