hivepods-cli 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.
- hivepods_cli-0.1.0/PKG-INFO +126 -0
- hivepods_cli-0.1.0/README.md +102 -0
- hivepods_cli-0.1.0/hivepods_cli/__init__.py +3 -0
- hivepods_cli-0.1.0/hivepods_cli/browser_login.py +193 -0
- hivepods_cli-0.1.0/hivepods_cli/client.py +410 -0
- hivepods_cli-0.1.0/hivepods_cli/config.py +137 -0
- hivepods_cli-0.1.0/hivepods_cli/main.py +808 -0
- hivepods_cli-0.1.0/hivepods_cli/terminal.py +314 -0
- hivepods_cli-0.1.0/hivepods_cli.egg-info/PKG-INFO +126 -0
- hivepods_cli-0.1.0/hivepods_cli.egg-info/SOURCES.txt +18 -0
- hivepods_cli-0.1.0/hivepods_cli.egg-info/dependency_links.txt +1 -0
- hivepods_cli-0.1.0/hivepods_cli.egg-info/entry_points.txt +2 -0
- hivepods_cli-0.1.0/hivepods_cli.egg-info/requires.txt +8 -0
- hivepods_cli-0.1.0/hivepods_cli.egg-info/top_level.txt +1 -0
- hivepods_cli-0.1.0/pyproject.toml +41 -0
- hivepods_cli-0.1.0/setup.cfg +4 -0
- hivepods_cli-0.1.0/tests/test_browser_login.py +110 -0
- hivepods_cli-0.1.0/tests/test_client.py +228 -0
- hivepods_cli-0.1.0/tests/test_config.py +101 -0
- hivepods_cli-0.1.0/tests/test_round2.py +364 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hivepods-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command line interface for DockHive / HivePods
|
|
5
|
+
Author: DockHive
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DockHive/hive-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/DockHive/hive-cli/issues
|
|
9
|
+
Keywords: dockhive,hivepods,cli,pods,environments,cloud
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: click>=8.1
|
|
18
|
+
Requires-Dist: requests>=2.31
|
|
19
|
+
Requires-Dist: websockets>=12.0
|
|
20
|
+
Requires-Dist: PyYAML>=6.0
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
24
|
+
|
|
25
|
+
# HivePods
|
|
26
|
+
|
|
27
|
+
Your own cloud dev environment, ready in seconds. Pick an operating system,
|
|
28
|
+
choose how much power you need, and you're in — right from your terminal.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
hivepod create web1 # a quick wizard: name it, pick an OS + size
|
|
32
|
+
hivepod connect web1 # you're in a shell
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pipx install hivepods-cli # recommended — keeps hivepod on its own
|
|
39
|
+
# or
|
|
40
|
+
pip install hivepods-cli
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Get started
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
hivepod login # opens your browser to sign in
|
|
47
|
+
hivepod create # short wizard: name, OS, size
|
|
48
|
+
hivepod connect myenv # open a shell in your environment
|
|
49
|
+
hivepod list # see everything you're running
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`hivepod login` signs you in through the browser. On a machine with no browser,
|
|
53
|
+
use `hivepod login --no-browser` for email and password instead.
|
|
54
|
+
|
|
55
|
+
No servers to manage, no setup. Each environment gets its own address
|
|
56
|
+
(`myenv.dockhive.sh`), so whatever you run inside is reachable.
|
|
57
|
+
|
|
58
|
+
## Everyday commands
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
hivepod create # interactive: OS, size (or custom CPU/RAM/disk), region
|
|
62
|
+
hivepod create api --os ubuntu-24.04 --size small # or one-liner
|
|
63
|
+
|
|
64
|
+
hivepod connect api # interactive shell
|
|
65
|
+
hivepod exec api "npm test" # run a single command
|
|
66
|
+
hivepod logs api -f # follow live logs
|
|
67
|
+
|
|
68
|
+
hivepod stop api # pause (your data stays)
|
|
69
|
+
hivepod start api # resume
|
|
70
|
+
hivepod restart api
|
|
71
|
+
hivepod rm api # delete
|
|
72
|
+
|
|
73
|
+
hivepod list # what you have
|
|
74
|
+
hivepod status api # details + live CPU/RAM
|
|
75
|
+
|
|
76
|
+
hivepod env api PORT=3000 --unset DEBUG # change environment variables
|
|
77
|
+
hivepod apply api # use them (recreates the environment; data/ and projects/ are kept)
|
|
78
|
+
|
|
79
|
+
hivepod backup create api # back up the data/ and projects/ folders
|
|
80
|
+
hivepod backup list
|
|
81
|
+
hivepod backup restore <backup-id> api-copy # a NEW environment from a backup
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Anywhere a command takes a name, you can pass the environment's id instead.
|
|
85
|
+
`start`, `stop`, `restart`, `suspend`, `resume`, `apply`, `rm`, `backup create`
|
|
86
|
+
and `backup restore` wait until the work is done; add `--no-wait` to return
|
|
87
|
+
straight away.
|
|
88
|
+
|
|
89
|
+
`hivepod connect` works on macOS, Linux and Windows (Windows Terminal or the
|
|
90
|
+
Windows 10+ console).
|
|
91
|
+
|
|
92
|
+
## Operating systems
|
|
93
|
+
|
|
94
|
+
Ubuntu, Debian, Fedora, Alpine, Arch, Rocky and more run as fast, lightweight
|
|
95
|
+
environments. **Windows** and **FreeBSD** run as full machines — reach those
|
|
96
|
+
through the built-in web console (or RDP for Windows) instead of a shell.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
hivepod os # everything you can launch
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Sizes
|
|
103
|
+
|
|
104
|
+
Pick a preset, or set your own:
|
|
105
|
+
|
|
106
|
+
| Size | vCPU | RAM | Disk |
|
|
107
|
+
|------|------|-----|------|
|
|
108
|
+
| nano | 0.5 | 512 MB | 5 GB |
|
|
109
|
+
| micro | 1 | 1 GB | 10 GB |
|
|
110
|
+
| small | 2 | 2 GB | 25 GB |
|
|
111
|
+
| medium | 4 | 4 GB | 50 GB |
|
|
112
|
+
| custom | your call | your call | your call |
|
|
113
|
+
|
|
114
|
+
## Settings
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
hivepod config set default_size small # your default when you don't pick one
|
|
118
|
+
hivepod config list
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Help
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
hivepod --help
|
|
125
|
+
hivepod <command> --help
|
|
126
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# HivePods
|
|
2
|
+
|
|
3
|
+
Your own cloud dev environment, ready in seconds. Pick an operating system,
|
|
4
|
+
choose how much power you need, and you're in — right from your terminal.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
hivepod create web1 # a quick wizard: name it, pick an OS + size
|
|
8
|
+
hivepod connect web1 # you're in a shell
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pipx install hivepods-cli # recommended — keeps hivepod on its own
|
|
15
|
+
# or
|
|
16
|
+
pip install hivepods-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Get started
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
hivepod login # opens your browser to sign in
|
|
23
|
+
hivepod create # short wizard: name, OS, size
|
|
24
|
+
hivepod connect myenv # open a shell in your environment
|
|
25
|
+
hivepod list # see everything you're running
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`hivepod login` signs you in through the browser. On a machine with no browser,
|
|
29
|
+
use `hivepod login --no-browser` for email and password instead.
|
|
30
|
+
|
|
31
|
+
No servers to manage, no setup. Each environment gets its own address
|
|
32
|
+
(`myenv.dockhive.sh`), so whatever you run inside is reachable.
|
|
33
|
+
|
|
34
|
+
## Everyday commands
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
hivepod create # interactive: OS, size (or custom CPU/RAM/disk), region
|
|
38
|
+
hivepod create api --os ubuntu-24.04 --size small # or one-liner
|
|
39
|
+
|
|
40
|
+
hivepod connect api # interactive shell
|
|
41
|
+
hivepod exec api "npm test" # run a single command
|
|
42
|
+
hivepod logs api -f # follow live logs
|
|
43
|
+
|
|
44
|
+
hivepod stop api # pause (your data stays)
|
|
45
|
+
hivepod start api # resume
|
|
46
|
+
hivepod restart api
|
|
47
|
+
hivepod rm api # delete
|
|
48
|
+
|
|
49
|
+
hivepod list # what you have
|
|
50
|
+
hivepod status api # details + live CPU/RAM
|
|
51
|
+
|
|
52
|
+
hivepod env api PORT=3000 --unset DEBUG # change environment variables
|
|
53
|
+
hivepod apply api # use them (recreates the environment; data/ and projects/ are kept)
|
|
54
|
+
|
|
55
|
+
hivepod backup create api # back up the data/ and projects/ folders
|
|
56
|
+
hivepod backup list
|
|
57
|
+
hivepod backup restore <backup-id> api-copy # a NEW environment from a backup
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Anywhere a command takes a name, you can pass the environment's id instead.
|
|
61
|
+
`start`, `stop`, `restart`, `suspend`, `resume`, `apply`, `rm`, `backup create`
|
|
62
|
+
and `backup restore` wait until the work is done; add `--no-wait` to return
|
|
63
|
+
straight away.
|
|
64
|
+
|
|
65
|
+
`hivepod connect` works on macOS, Linux and Windows (Windows Terminal or the
|
|
66
|
+
Windows 10+ console).
|
|
67
|
+
|
|
68
|
+
## Operating systems
|
|
69
|
+
|
|
70
|
+
Ubuntu, Debian, Fedora, Alpine, Arch, Rocky and more run as fast, lightweight
|
|
71
|
+
environments. **Windows** and **FreeBSD** run as full machines — reach those
|
|
72
|
+
through the built-in web console (or RDP for Windows) instead of a shell.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
hivepod os # everything you can launch
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Sizes
|
|
79
|
+
|
|
80
|
+
Pick a preset, or set your own:
|
|
81
|
+
|
|
82
|
+
| Size | vCPU | RAM | Disk |
|
|
83
|
+
|------|------|-----|------|
|
|
84
|
+
| nano | 0.5 | 512 MB | 5 GB |
|
|
85
|
+
| micro | 1 | 1 GB | 10 GB |
|
|
86
|
+
| small | 2 | 2 GB | 25 GB |
|
|
87
|
+
| medium | 4 | 4 GB | 50 GB |
|
|
88
|
+
| custom | your call | your call | your call |
|
|
89
|
+
|
|
90
|
+
## Settings
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
hivepod config set default_size small # your default when you don't pick one
|
|
94
|
+
hivepod config list
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Help
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
hivepod --help
|
|
101
|
+
hivepod <command> --help
|
|
102
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Browser sign-in for the CLI.
|
|
2
|
+
|
|
3
|
+
Starts a throwaway web server on 127.0.0.1, opens the DockHive sign-in page in
|
|
4
|
+
the browser, and waits for the page to hand the session back to that local
|
|
5
|
+
address once the user has approved. Falls back cleanly if no browser is around.
|
|
6
|
+
|
|
7
|
+
The page can hand the session back two ways:
|
|
8
|
+
* POST /callback (form or JSON body with state/token/refresh) — preferred, the
|
|
9
|
+
token never appears in a URL or the browser history;
|
|
10
|
+
* GET /callback?state=…&token=… — still accepted; the reply immediately
|
|
11
|
+
replaces the history entry so the token doesn't linger there.
|
|
12
|
+
|
|
13
|
+
Anything else (wrong path, wrong state, wrong Host, stray requests from other
|
|
14
|
+
pages) gets a 4xx and is ignored: we keep waiting for the genuine callback until
|
|
15
|
+
the timeout.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import secrets
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
import urllib.parse
|
|
24
|
+
import webbrowser
|
|
25
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
# How long we'll sit waiting for the browser round-trip before giving up.
|
|
29
|
+
TIMEOUT_SECONDS = 300
|
|
30
|
+
MAX_BODY = 64 * 1024
|
|
31
|
+
|
|
32
|
+
_DONE_PAGE = b"""<!doctype html><meta charset="utf-8">
|
|
33
|
+
<meta name="referrer" content="no-referrer">
|
|
34
|
+
<title>DockHive CLI</title>
|
|
35
|
+
<script>try{history.replaceState(null,'','/done')}catch(e){}</script>
|
|
36
|
+
<style>body{font:16px -apple-system,Segoe UI,sans-serif;background:#0b0c0f;color:#e8e8e8;
|
|
37
|
+
display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}
|
|
38
|
+
div{text-align:center;max-width:22rem;padding:2rem}</style>
|
|
39
|
+
<div><h2>%s</h2><p>%s</p></div>
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _Result:
|
|
44
|
+
token: Optional[str] = None
|
|
45
|
+
refresh: Optional[str] = None
|
|
46
|
+
error: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
50
|
+
# bound by start_login() before the server spins up
|
|
51
|
+
expected_state = ""
|
|
52
|
+
expected_port = 0
|
|
53
|
+
result = _Result()
|
|
54
|
+
done = None # threading.Event set once a genuine callback was handled
|
|
55
|
+
|
|
56
|
+
def log_message(self, *_args): # silence the default stderr logging
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def _reply(self, heading: str, detail: str, code: int = 200):
|
|
60
|
+
body = _DONE_PAGE % (heading.encode(), detail.encode())
|
|
61
|
+
self.send_response(code)
|
|
62
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
63
|
+
self.send_header("Content-Length", str(len(body)))
|
|
64
|
+
self.send_header("Cache-Control", "no-store")
|
|
65
|
+
self.send_header("Referrer-Policy", "no-referrer")
|
|
66
|
+
self.end_headers()
|
|
67
|
+
self.wfile.write(body)
|
|
68
|
+
|
|
69
|
+
def _host_ok(self) -> bool:
|
|
70
|
+
# Blocks DNS-rebinding pages from talking to the loopback server.
|
|
71
|
+
host = (self.headers.get("Host") or "").lower()
|
|
72
|
+
return host in (f"127.0.0.1:{self.expected_port}", f"localhost:{self.expected_port}")
|
|
73
|
+
|
|
74
|
+
def _handle(self, params: dict):
|
|
75
|
+
if not self._host_ok():
|
|
76
|
+
self._reply("Not allowed", "You can close this window.", code=421)
|
|
77
|
+
return
|
|
78
|
+
state = params.get("state") or ""
|
|
79
|
+
if not isinstance(state, str) or not secrets.compare_digest(
|
|
80
|
+
state.encode(), type(self).expected_state.encode()):
|
|
81
|
+
# Not our callback (stale tab, another page probing): ignore it.
|
|
82
|
+
self._reply("Something went wrong", "This sign-in link is not valid. Close this and retry.",
|
|
83
|
+
code=400)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
result = type(self).result
|
|
87
|
+
if params.get("error"):
|
|
88
|
+
result.error = "sign-in was cancelled"
|
|
89
|
+
self._reply("Sign-in cancelled", "You can close this window.")
|
|
90
|
+
type(self).done.set()
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
token = params.get("token") or ""
|
|
94
|
+
if not isinstance(token, str) or not token:
|
|
95
|
+
self._reply("Something went wrong", "No session was returned. Close this and retry.", code=400)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
result.token = token
|
|
99
|
+
refresh = params.get("refresh")
|
|
100
|
+
result.refresh = refresh if isinstance(refresh, str) and refresh else None
|
|
101
|
+
self._reply("You're signed in", "Head back to your terminal — you can close this window.")
|
|
102
|
+
type(self).done.set()
|
|
103
|
+
|
|
104
|
+
def do_GET(self):
|
|
105
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
106
|
+
if parsed.path != "/callback":
|
|
107
|
+
self._reply("Not found", "You can close this window.", code=404)
|
|
108
|
+
return
|
|
109
|
+
params = {k: v[0] for k, v in urllib.parse.parse_qs(parsed.query).items()}
|
|
110
|
+
self._handle(params)
|
|
111
|
+
|
|
112
|
+
def do_POST(self):
|
|
113
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
114
|
+
if parsed.path != "/callback":
|
|
115
|
+
self._reply("Not found", "You can close this window.", code=404)
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
119
|
+
except ValueError:
|
|
120
|
+
length = 0
|
|
121
|
+
if length <= 0 or length > MAX_BODY:
|
|
122
|
+
self._reply("Something went wrong", "Invalid request.", code=400)
|
|
123
|
+
return
|
|
124
|
+
raw = self.rfile.read(length).decode("utf-8", "replace")
|
|
125
|
+
ctype = (self.headers.get("Content-Type") or "").split(";")[0].strip().lower()
|
|
126
|
+
if ctype == "application/json":
|
|
127
|
+
try:
|
|
128
|
+
params = json.loads(raw)
|
|
129
|
+
except ValueError:
|
|
130
|
+
params = None
|
|
131
|
+
if not isinstance(params, dict):
|
|
132
|
+
self._reply("Something went wrong", "Invalid request.", code=400)
|
|
133
|
+
return
|
|
134
|
+
else:
|
|
135
|
+
params = {k: v[0] for k, v in urllib.parse.parse_qs(raw).items()}
|
|
136
|
+
self._handle(params)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def start_login(web_url: str, open_browser: bool = True,
|
|
140
|
+
timeout: float = TIMEOUT_SECONDS) -> _Result:
|
|
141
|
+
"""Run the loopback sign-in dance and return the captured session.
|
|
142
|
+
|
|
143
|
+
Raises RuntimeError on timeout or if the browser reports an error.
|
|
144
|
+
"""
|
|
145
|
+
state = secrets.token_urlsafe(24)
|
|
146
|
+
|
|
147
|
+
server = HTTPServer(("127.0.0.1", 0), _Handler)
|
|
148
|
+
port = server.server_address[1]
|
|
149
|
+
done = threading.Event()
|
|
150
|
+
_Handler.expected_state = state
|
|
151
|
+
_Handler.expected_port = port
|
|
152
|
+
_Handler.result = _Result()
|
|
153
|
+
_Handler.done = done
|
|
154
|
+
|
|
155
|
+
auth_url = (
|
|
156
|
+
web_url.rstrip("/")
|
|
157
|
+
+ "/cli-auth?"
|
|
158
|
+
+ urllib.parse.urlencode({"port": port, "state": state})
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
deadline = time.monotonic() + timeout
|
|
162
|
+
|
|
163
|
+
def _serve():
|
|
164
|
+
# Keep answering until the genuine callback arrives or time runs out;
|
|
165
|
+
# wrong-state / stray requests don't end the wait.
|
|
166
|
+
while not done.is_set():
|
|
167
|
+
remaining = deadline - time.monotonic()
|
|
168
|
+
if remaining <= 0:
|
|
169
|
+
break
|
|
170
|
+
server.timeout = min(remaining, 1.0)
|
|
171
|
+
server.handle_request()
|
|
172
|
+
|
|
173
|
+
worker = threading.Thread(target=_serve, daemon=True)
|
|
174
|
+
worker.start()
|
|
175
|
+
|
|
176
|
+
opened = webbrowser.open(auth_url) if open_browser else False
|
|
177
|
+
if not opened:
|
|
178
|
+
print("Open this URL in your browser to sign in:\n " + auth_url)
|
|
179
|
+
|
|
180
|
+
worker.join(timeout + 5)
|
|
181
|
+
try:
|
|
182
|
+
server.server_close()
|
|
183
|
+
except OSError:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
result = _Handler.result
|
|
187
|
+
if not done.is_set():
|
|
188
|
+
raise RuntimeError("timed out waiting for the browser sign-in")
|
|
189
|
+
if result.error:
|
|
190
|
+
raise RuntimeError(result.error)
|
|
191
|
+
if not result.token:
|
|
192
|
+
raise RuntimeError("no session was returned")
|
|
193
|
+
return result
|