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/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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bitsreef = bitsreef_cli.main:cli
@@ -0,0 +1 @@
1
+ bitsreef_cli