bitsreef-cli 0.1.0__py3-none-any.whl
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.
- bitsreef_cli/__init__.py +2 -0
- bitsreef_cli/client.py +74 -0
- bitsreef_cli/commands/__init__.py +0 -0
- bitsreef_cli/commands/auth.py +91 -0
- bitsreef_cli/commands/completion.py +29 -0
- bitsreef_cli/commands/cron.py +166 -0
- bitsreef_cli/commands/deploy.py +124 -0
- bitsreef_cli/commands/domains.py +119 -0
- bitsreef_cli/commands/env.py +138 -0
- bitsreef_cli/commands/environments.py +132 -0
- bitsreef_cli/commands/exec_cmd.py +27 -0
- bitsreef_cli/commands/functions.py +186 -0
- bitsreef_cli/commands/link.py +87 -0
- bitsreef_cli/commands/logs.py +44 -0
- bitsreef_cli/commands/metrics.py +50 -0
- bitsreef_cli/commands/projects.py +89 -0
- bitsreef_cli/commands/services.py +149 -0
- bitsreef_cli/commands/templates.py +106 -0
- bitsreef_cli/commands/up.py +195 -0
- bitsreef_cli/commands/volumes.py +99 -0
- bitsreef_cli/commands/webhooks.py +123 -0
- bitsreef_cli/config.py +92 -0
- bitsreef_cli/context.py +175 -0
- bitsreef_cli/deploy_watch.py +87 -0
- bitsreef_cli/log_stream.py +83 -0
- bitsreef_cli/main.py +43 -0
- bitsreef_cli/output.py +25 -0
- bitsreef_cli/shell.py +136 -0
- bitsreef_cli-0.1.0.dist-info/METADATA +143 -0
- bitsreef_cli-0.1.0.dist-info/RECORD +33 -0
- bitsreef_cli-0.1.0.dist-info/WHEEL +5 -0
- bitsreef_cli-0.1.0.dist-info/entry_points.txt +2 -0
- bitsreef_cli-0.1.0.dist-info/top_level.txt +1 -0
bitsreef_cli/shell.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Interactive shell over WebSocket (`bitsreef exec`).
|
|
2
|
+
|
|
3
|
+
Bridges the local terminal to the platform's WebShellConsumer
|
|
4
|
+
(`/ws/services/{id}/shell/?token=<JWT>`) — a Docker exec of `/bin/sh` with a
|
|
5
|
+
TTY. JSON frames:
|
|
6
|
+
server → {"type":"connected"} | {"type":"output","data":..} | {"type":"ping"}
|
|
7
|
+
client → {"type":"input","data":..} | {"type":"pong"} | {"type":"resize",...}
|
|
8
|
+
|
|
9
|
+
POSIX only (needs termios); requires an interactive TTY. Input is decoded as
|
|
10
|
+
UTF-8 to match the web terminal (the consumer re-encodes it UTF-8 before writing
|
|
11
|
+
to the container socket).
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import select
|
|
16
|
+
import sys
|
|
17
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# Close codes the consumer uses → human hints.
|
|
24
|
+
_CLOSE_HINTS = {
|
|
25
|
+
4001: "not authenticated (log in again)",
|
|
26
|
+
4003: "permission denied (viewer role can't open a shell)",
|
|
27
|
+
4004: "no running container for this service (is it deployed and running?)",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def shell_ws_url(api_url: str, service_id) -> str:
|
|
32
|
+
"""Derive wss://host/ws/services/{id}/shell/ from the REST base URL."""
|
|
33
|
+
parts = urlsplit(api_url)
|
|
34
|
+
scheme = "wss" if parts.scheme == "https" else "ws"
|
|
35
|
+
return urlunsplit((scheme, parts.netloc, f"/ws/services/{service_id}/shell/", "", ""))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _term_size() -> tuple[int, int]:
|
|
39
|
+
try:
|
|
40
|
+
size = os.get_terminal_size()
|
|
41
|
+
return size.columns, size.lines
|
|
42
|
+
except OSError:
|
|
43
|
+
return 80, 24
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def exec_shell(api_url: str, token: str | None, service_id) -> int:
|
|
47
|
+
"""Open an interactive shell to the service. Returns a process exit code."""
|
|
48
|
+
try:
|
|
49
|
+
import websocket # from websocket-client
|
|
50
|
+
except ImportError:
|
|
51
|
+
console.print("[red]exec needs 'websocket-client'. Install: pip install websocket-client[/red]")
|
|
52
|
+
return 1
|
|
53
|
+
if not token:
|
|
54
|
+
console.print("[red]Not logged in. Run: bitsreef auth login[/red]")
|
|
55
|
+
return 1
|
|
56
|
+
if not sys.stdin.isatty():
|
|
57
|
+
console.print("[red]exec requires an interactive terminal (a TTY).[/red]")
|
|
58
|
+
return 1
|
|
59
|
+
try:
|
|
60
|
+
import termios
|
|
61
|
+
import tty
|
|
62
|
+
except ImportError:
|
|
63
|
+
console.print("[red]exec is only supported on POSIX terminals.[/red]")
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
url = f"{shell_ws_url(api_url, service_id)}?token={token}"
|
|
67
|
+
try:
|
|
68
|
+
ws = websocket.create_connection(url, timeout=30)
|
|
69
|
+
except Exception as exc: # noqa: BLE001
|
|
70
|
+
console.print(f"[red]Could not open shell: {exc}[/red]")
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
stdin_fd = sys.stdin.fileno()
|
|
74
|
+
old_attr = termios.tcgetattr(stdin_fd)
|
|
75
|
+
connected = False
|
|
76
|
+
last_size = None
|
|
77
|
+
try:
|
|
78
|
+
tty.setraw(stdin_fd)
|
|
79
|
+
while True:
|
|
80
|
+
size = _term_size()
|
|
81
|
+
if size != last_size:
|
|
82
|
+
ws.send(json.dumps({"type": "resize", "cols": size[0], "rows": size[1]}))
|
|
83
|
+
last_size = size
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
readable, _, _ = select.select([stdin_fd, ws.sock], [], [], 0.5)
|
|
87
|
+
except (OSError, select.error):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
if ws.sock in readable:
|
|
91
|
+
try:
|
|
92
|
+
raw = ws.recv()
|
|
93
|
+
except websocket.WebSocketTimeoutException:
|
|
94
|
+
raw = None
|
|
95
|
+
except websocket.WebSocketConnectionClosedException:
|
|
96
|
+
break
|
|
97
|
+
if raw == "":
|
|
98
|
+
break
|
|
99
|
+
if raw:
|
|
100
|
+
msg = _parse(raw)
|
|
101
|
+
kind = msg.get("type")
|
|
102
|
+
if kind == "output":
|
|
103
|
+
os.write(1, msg.get("data", "").encode("utf-8", errors="replace"))
|
|
104
|
+
elif kind == "ping":
|
|
105
|
+
ws.send(json.dumps({"type": "pong"}))
|
|
106
|
+
elif kind == "connected":
|
|
107
|
+
connected = True
|
|
108
|
+
|
|
109
|
+
if stdin_fd in readable:
|
|
110
|
+
data = os.read(stdin_fd, 4096)
|
|
111
|
+
if not data:
|
|
112
|
+
break
|
|
113
|
+
ws.send(json.dumps({"type": "input", "data": data.decode("utf-8", errors="replace")}))
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
pass
|
|
116
|
+
finally:
|
|
117
|
+
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_attr)
|
|
118
|
+
code = getattr(ws, "close_code", None)
|
|
119
|
+
try:
|
|
120
|
+
ws.close()
|
|
121
|
+
except Exception: # noqa: BLE001
|
|
122
|
+
pass
|
|
123
|
+
os.write(1, b"\r\n")
|
|
124
|
+
if not connected:
|
|
125
|
+
hint = _CLOSE_HINTS.get(code)
|
|
126
|
+
console.print(f"[red]Shell closed before it was ready"
|
|
127
|
+
+ (f": {hint}" if hint else ".") + "[/red]")
|
|
128
|
+
return 1
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse(raw):
|
|
133
|
+
try:
|
|
134
|
+
return json.loads(raw)
|
|
135
|
+
except (json.JSONDecodeError, ValueError):
|
|
136
|
+
return {}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bitsreef-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool for BitsReef PaaS platform
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.1
|
|
8
|
+
Requires-Dist: httpx>=0.25
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
Requires-Dist: websocket-client>=1.6
|
|
11
|
+
|
|
12
|
+
# BitsReef CLI
|
|
13
|
+
|
|
14
|
+
`bitsreef` — deploy and manage BitsReef services from the command line.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# One-liner (installs via pipx, or an isolated venv as fallback):
|
|
20
|
+
curl -fsSL https://raw.githubusercontent.com/cygnaragroup/bitsreef/main/cli/install.sh | sh
|
|
21
|
+
|
|
22
|
+
# ...or with pip / pipx directly:
|
|
23
|
+
pipx install bitsreef-cli # recommended — isolated, on PATH
|
|
24
|
+
pip install bitsreef-cli
|
|
25
|
+
|
|
26
|
+
# ...or from a checkout:
|
|
27
|
+
pip install -e cli/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Shell completion
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
eval "$(bitsreef completion bash)" # add to ~/.bashrc
|
|
34
|
+
eval "$(bitsreef completion zsh)" # add to ~/.zshrc
|
|
35
|
+
bitsreef completion fish > ~/.config/fish/completions/bitsreef.fish
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Getting started
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Log in to BitsReef (the CLI talks to the hosted BitsReef API)
|
|
42
|
+
bitsreef auth login
|
|
43
|
+
|
|
44
|
+
# Deploy an image to a project in one shot, streaming to completion
|
|
45
|
+
bitsreef up --project 4 --name web --image nginx:latest --port 80 --follow
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`bitsreef up` creates the service if it doesn't exist (or updates its image if
|
|
49
|
+
it does), triggers a deployment, and with `--follow` streams the build/deploy
|
|
50
|
+
logs and exits non-zero if the deploy fails — so it drops straight into CI.
|
|
51
|
+
|
|
52
|
+
## Linking (stop typing IDs)
|
|
53
|
+
|
|
54
|
+
Bind a directory to a project/service once, then omit IDs everywhere:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bitsreef link --project demo --service web # writes ./.bitsreef.json
|
|
58
|
+
bitsreef logs # → the linked service's logs
|
|
59
|
+
bitsreef up -i nginx:1.27 --follow # redeploy the linked service
|
|
60
|
+
bitsreef deploy status # linked project's deployments
|
|
61
|
+
bitsreef link --show # what's linked here
|
|
62
|
+
bitsreef unlink # remove the link
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The link file (`.bitsreef.json`) is discovered by walking up from the current
|
|
66
|
+
directory, like `.git`. **Names or slugs work in place of numeric IDs**
|
|
67
|
+
everywhere (`bitsreef logs demo web`), and a default org is remembered after the
|
|
68
|
+
first pick so multi-org accounts stop re-prompting.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
| Command | Purpose |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `up` | Create-or-update a service from an image and deploy it (`--follow`, `--json`) |
|
|
75
|
+
| `exec` | Open an interactive shell (`/bin/sh`) in a running container |
|
|
76
|
+
| `link` / `unlink` | Bind this directory to a project/service (`--show`) |
|
|
77
|
+
| `auth` | `login`, `logout`, `whoami` |
|
|
78
|
+
| `projects` | `list`, `info`, `create` |
|
|
79
|
+
| `services` | `list`, `info`, `restart`, `stop`, `scale` |
|
|
80
|
+
| `deploy` | `up` (`--follow`), `rebuild`, `status`, `rollback` |
|
|
81
|
+
| `env` | `list`, `set`, `delete`, `reveal` |
|
|
82
|
+
| `domains` | `list`, `add`, `remove`, `verify` (custom domains) |
|
|
83
|
+
| `volumes` | `list`, `create`, `delete` |
|
|
84
|
+
| `metrics` | Current CPU / memory / network snapshot for a service |
|
|
85
|
+
| `cron` | `list`, `create`, `trigger`, `runs`, `delete` (scheduled jobs) |
|
|
86
|
+
| `webhooks` | `list`, `add`, `update`, `remove` (project event notifications) |
|
|
87
|
+
| `templates` | `list`, `info`, `deploy` (service templates) |
|
|
88
|
+
| `functions` | `list`, `create`, `invoke`, `history`, `update`, `delete` |
|
|
89
|
+
| `logs` | View service logs (`--tail`, `--follow` for a live tail) |
|
|
90
|
+
| `completion` | Print a shell-completion script (`bash`, `zsh`, `fish`) |
|
|
91
|
+
|
|
92
|
+
Run `bitsreef <command> --help` for full options.
|
|
93
|
+
|
|
94
|
+
## CI / scripting / agents
|
|
95
|
+
|
|
96
|
+
Everything works non-interactively and can emit JSON:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Auth without a prompt — a token env var (nothing written to disk):
|
|
100
|
+
export BITSREEF_TOKEN=<access-token>
|
|
101
|
+
|
|
102
|
+
# ...or a piped password / a stored token:
|
|
103
|
+
echo "$PASS" | bitsreef auth login -u ci --password-stdin
|
|
104
|
+
bitsreef auth login --token "$BITSREEF_TOKEN"
|
|
105
|
+
|
|
106
|
+
# Machine-readable output (place --json before the command):
|
|
107
|
+
bitsreef --json services list demo | jq '.[].name'
|
|
108
|
+
bitsreef --json whoami
|
|
109
|
+
bitsreef up -p demo -n web -i img:tag --follow --json # exits non-zero on deploy failure
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`BITSREEF_TOKEN` (env) takes precedence over the config file. `--json` is
|
|
113
|
+
supported on the read commands (`whoami`, `projects/services list|info`,
|
|
114
|
+
`deploy status`, `env list`, `functions list|history`, `logs`) and `up`.
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
Config and JWT tokens live in `~/.bitsreef/config.json` (created `0600`). The
|
|
119
|
+
access token is refreshed automatically on expiry. `BITSREEF_TOKEN` (env)
|
|
120
|
+
overrides the file when set. The API endpoint is built into the CLI — BitsReef
|
|
121
|
+
is a hosted service, so there is no server URL to configure.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cd cli
|
|
127
|
+
pip install -e . pytest
|
|
128
|
+
pytest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Releasing
|
|
132
|
+
|
|
133
|
+
Publishing is automated by `.github/workflows/cli-publish.yml` via PyPI
|
|
134
|
+
[trusted publishing](https://docs.pypi.org/trusted-publishers/) (no API token).
|
|
135
|
+
Bump `version` in `pyproject.toml` and `bitsreef_cli/__init__.py`, then tag:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git tag cli-v0.1.0 && git push origin cli-v0.1.0
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The workflow verifies the tag matches the package version, builds the sdist +
|
|
142
|
+
wheel, `twine check`s them, and uploads to PyPI. It can also be run manually
|
|
143
|
+
from the Actions tab (`workflow_dispatch`).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
bitsreef_cli/__init__.py,sha256=d2rIHTuw6oOKeM_JGJaLc0FNByocLtqyoKUx2A_aUWE,100
|
|
2
|
+
bitsreef_cli/client.py,sha256=lqxnDOEk18f6hdoISRHjfhRfm2gkTOJ7HwktbkdxdYU,2580
|
|
3
|
+
bitsreef_cli/config.py,sha256=tkZXf8DvDJpO5257KG2Q5J6RN6dadT2Zsx2kcKT3yTY,2714
|
|
4
|
+
bitsreef_cli/context.py,sha256=29emi67dm6Gr7tHtISbzB-RUWuSV1oW4Hi76siYIJ4o,6325
|
|
5
|
+
bitsreef_cli/deploy_watch.py,sha256=KSGzT4V8wsPfeLV5nZPHtGytutYqz-3fATHf_eECU0Q,2813
|
|
6
|
+
bitsreef_cli/log_stream.py,sha256=7axF9RfouVa9Q61GZ6KdUw8VBbXVK1TKsLgE5hfWTLE,2811
|
|
7
|
+
bitsreef_cli/main.py,sha256=787tz6szHDhab2VbDDjVG8QNXHLDOph17vBVL9tIn6c,1285
|
|
8
|
+
bitsreef_cli/output.py,sha256=xtKDbHyH9Rif-eodV2LwZwyHjB6Rr0oWQWZxtsVKbBs,645
|
|
9
|
+
bitsreef_cli/shell.py,sha256=IEED9kVHmmtBrc7Zggx0pnaGc7NyfOWK_AaofrSND9A,4675
|
|
10
|
+
bitsreef_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
bitsreef_cli/commands/auth.py,sha256=sssQPWgS-IOfcjhNlcqc572uRLmxjfq0esgTk-MzjnI,2667
|
|
12
|
+
bitsreef_cli/commands/completion.py,sha256=xJksAHO0eP3KhPchaO2IB_4d8-uTiDWK1fBuFIWYjKU,1014
|
|
13
|
+
bitsreef_cli/commands/cron.py,sha256=aXsvIbr4poyB4HtoX8FNMW-BdasJvAqmQs3Z0kQhma8,5843
|
|
14
|
+
bitsreef_cli/commands/deploy.py,sha256=_MRLwd9uHc3A4C0JfUL5-Uv2ZVBUYcKg2JlamCXQg34,4747
|
|
15
|
+
bitsreef_cli/commands/domains.py,sha256=Gvw5gsKY_ekR-pgkG6H_FRvo97jwwdQ9ohpOREpqno8,4285
|
|
16
|
+
bitsreef_cli/commands/env.py,sha256=vPEh3KUOrjoIUSIYZduEbYhMcHyLQqG6BZvboBnbvnc,4599
|
|
17
|
+
bitsreef_cli/commands/environments.py,sha256=1OAhNaiK-hV8-dsxvLyRlmzrjnAZPDUTSIv05-96ChM,4869
|
|
18
|
+
bitsreef_cli/commands/exec_cmd.py,sha256=W8QKp5uFXBP8uAajQYxS4NZf0loSJ5o54qYtNbxs05w,961
|
|
19
|
+
bitsreef_cli/commands/functions.py,sha256=JbDGyG8sfzL6w4ZOpaa93yH5sypqV3r2w1sTuah3Cks,6737
|
|
20
|
+
bitsreef_cli/commands/link.py,sha256=Kk6A-WZcPfc176cmFeKAW27zx-rv7scl01Cx_GBcaag,3470
|
|
21
|
+
bitsreef_cli/commands/logs.py,sha256=6chUbddpPxWq1ZoS9VvwS7-Q3RjtyR8udURMMD8dj1s,1539
|
|
22
|
+
bitsreef_cli/commands/metrics.py,sha256=SafT0PywElDI4S36Pn7iff6S9cAXURYofsxMehmCzHA,1806
|
|
23
|
+
bitsreef_cli/commands/projects.py,sha256=lK5BnfytgFvvnXPE6SHpXVBP-TNEvAlhwE4CG_EUnnQ,3279
|
|
24
|
+
bitsreef_cli/commands/services.py,sha256=FftblzAKZOCtVGVduMvsz1aJ2qWGRCN9wg9_BOvBfnE,6125
|
|
25
|
+
bitsreef_cli/commands/templates.py,sha256=ehvVb8l-pmAFxXpR-gfUaO3ht2hZF1iQCKnfYJnFXrM,3480
|
|
26
|
+
bitsreef_cli/commands/up.py,sha256=gmlRjHPmS9QczphT2huoT18XE9xD0GM3BosucejUL2M,8026
|
|
27
|
+
bitsreef_cli/commands/volumes.py,sha256=DIg9GnoIUqaDYJErLCwvkOij03Qml8AvCt5FdBNleEA,3659
|
|
28
|
+
bitsreef_cli/commands/webhooks.py,sha256=kSj3Ms9EcB_-InRWZGChATHmGphxK_Rcay6h0zJ9lyY,4405
|
|
29
|
+
bitsreef_cli-0.1.0.dist-info/METADATA,sha256=gAjyC8uqShQowGjgHzGHPX6EQ02ZGnZ1gWHw8g1Oj6Q,5164
|
|
30
|
+
bitsreef_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
31
|
+
bitsreef_cli-0.1.0.dist-info/entry_points.txt,sha256=twdWn6DTBFt1Ja86AlULQIZrMtLOx9bIkzb8DuyGajU,51
|
|
32
|
+
bitsreef_cli-0.1.0.dist-info/top_level.txt,sha256=33vyz1VgWvyPRiVCMpq0tOepMtH5JdDrgatSKVE0P4Q,13
|
|
33
|
+
bitsreef_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bitsreef_cli
|