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.
@@ -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,3 @@
1
+ """HivePods CLI — manage DockHive pods from your terminal."""
2
+
3
+ __version__ = "0.1.0"
@@ -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