sandflare-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.
- sandflare_cli-0.1.0/PKG-INFO +69 -0
- sandflare_cli-0.1.0/README.md +56 -0
- sandflare_cli-0.1.0/pyproject.toml +28 -0
- sandflare_cli-0.1.0/sandflare_cli/__init__.py +3 -0
- sandflare_cli-0.1.0/sandflare_cli/main.py +386 -0
- sandflare_cli-0.1.0/sandflare_cli.egg-info/PKG-INFO +69 -0
- sandflare_cli-0.1.0/sandflare_cli.egg-info/SOURCES.txt +9 -0
- sandflare_cli-0.1.0/sandflare_cli.egg-info/dependency_links.txt +1 -0
- sandflare_cli-0.1.0/sandflare_cli.egg-info/entry_points.txt +5 -0
- sandflare_cli-0.1.0/sandflare_cli.egg-info/top_level.txt +1 -0
- sandflare_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandflare-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sandflare CLI — manage sandboxes and databases
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: sandflare,sandbox,microvm,ai,firecracker
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Sandflare CLI
|
|
15
|
+
|
|
16
|
+
Zero-dependency Python CLI for managing Sandflare sandboxes and databases.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install sandflare-cli
|
|
22
|
+
# or from source:
|
|
23
|
+
pip install -e sdk/cli/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Save your API key once
|
|
30
|
+
sandflare config set-key pa_live_your_key_here
|
|
31
|
+
|
|
32
|
+
# Sandboxes
|
|
33
|
+
sandflare sandbox create --template python
|
|
34
|
+
sandflare sandbox list
|
|
35
|
+
sandflare sandbox exec sb-1 python3 -c "print('hello')"
|
|
36
|
+
sandflare sandbox shell sb-1
|
|
37
|
+
sandflare sandbox delete sb-1
|
|
38
|
+
|
|
39
|
+
# Databases
|
|
40
|
+
sandflare db create
|
|
41
|
+
sandflare db list
|
|
42
|
+
sandflare db connect psql-1
|
|
43
|
+
sandflare db delete psql-1
|
|
44
|
+
|
|
45
|
+
# API Keys
|
|
46
|
+
sandflare keys list
|
|
47
|
+
sandflare keys create --label "CI/CD"
|
|
48
|
+
sandflare keys revoke <key-id>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Environment Variables
|
|
52
|
+
|
|
53
|
+
| Variable | Description |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `SANDFLARE_API_KEY` | API key (preferred) |
|
|
56
|
+
| `PANDAAGENT_API_KEY` | Legacy API key env alias |
|
|
57
|
+
| `SANDFLARE_API_URL` | API base URL (default: https://api.sandflare.io) |
|
|
58
|
+
|
|
59
|
+
## Aliases
|
|
60
|
+
|
|
61
|
+
Preferred commands:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
sandflare sandbox create --template node
|
|
65
|
+
sf sb ls
|
|
66
|
+
sf db ls
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Legacy `pandaagent` / `pa` aliases can remain during migration.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Sandflare CLI
|
|
2
|
+
|
|
3
|
+
Zero-dependency Python CLI for managing Sandflare sandboxes and databases.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sandflare-cli
|
|
9
|
+
# or from source:
|
|
10
|
+
pip install -e sdk/cli/
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Save your API key once
|
|
17
|
+
sandflare config set-key pa_live_your_key_here
|
|
18
|
+
|
|
19
|
+
# Sandboxes
|
|
20
|
+
sandflare sandbox create --template python
|
|
21
|
+
sandflare sandbox list
|
|
22
|
+
sandflare sandbox exec sb-1 python3 -c "print('hello')"
|
|
23
|
+
sandflare sandbox shell sb-1
|
|
24
|
+
sandflare sandbox delete sb-1
|
|
25
|
+
|
|
26
|
+
# Databases
|
|
27
|
+
sandflare db create
|
|
28
|
+
sandflare db list
|
|
29
|
+
sandflare db connect psql-1
|
|
30
|
+
sandflare db delete psql-1
|
|
31
|
+
|
|
32
|
+
# API Keys
|
|
33
|
+
sandflare keys list
|
|
34
|
+
sandflare keys create --label "CI/CD"
|
|
35
|
+
sandflare keys revoke <key-id>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Environment Variables
|
|
39
|
+
|
|
40
|
+
| Variable | Description |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `SANDFLARE_API_KEY` | API key (preferred) |
|
|
43
|
+
| `PANDAAGENT_API_KEY` | Legacy API key env alias |
|
|
44
|
+
| `SANDFLARE_API_URL` | API base URL (default: https://api.sandflare.io) |
|
|
45
|
+
|
|
46
|
+
## Aliases
|
|
47
|
+
|
|
48
|
+
Preferred commands:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sandflare sandbox create --template node
|
|
52
|
+
sf sb ls
|
|
53
|
+
sf db ls
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Legacy `pandaagent` / `pa` aliases can remain during migration.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sandflare-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Sandflare CLI — manage sandboxes and databases"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["sandflare", "sandbox", "microvm", "ai", "firecracker"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
sandflare = "sandflare_cli.main:main"
|
|
22
|
+
sf = "sandflare_cli.main:main"
|
|
23
|
+
pandaagent = "sandflare_cli.main:main"
|
|
24
|
+
pa = "sandflare_cli.main:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["sandflare_cli*"]
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sandflare CLI — manage sandboxes and databases from your terminal.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
PRIMARY_CONFIG_FILE = Path.home() / ".sandflare" / "config.json"
|
|
15
|
+
LEGACY_CONFIG_FILE = Path.home() / ".pandaagent" / "config.json"
|
|
16
|
+
DEFAULT_BASE = "https://api.sandflare.io"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_config() -> dict:
|
|
20
|
+
for path in (PRIMARY_CONFIG_FILE, LEGACY_CONFIG_FILE):
|
|
21
|
+
if path.exists():
|
|
22
|
+
try:
|
|
23
|
+
return json.loads(path.read_text())
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def save_config(cfg: dict):
|
|
30
|
+
PRIMARY_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
PRIMARY_CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_api_key() -> str:
|
|
35
|
+
return (
|
|
36
|
+
os.environ.get("SANDFLARE_API_KEY")
|
|
37
|
+
or os.environ.get("PANDAAGENT_API_KEY")
|
|
38
|
+
or os.environ.get("AGENTBOX_API_KEY")
|
|
39
|
+
or load_config().get("api_key")
|
|
40
|
+
or ""
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_base_url() -> str:
|
|
45
|
+
return (
|
|
46
|
+
os.environ.get("SANDFLARE_API_URL")
|
|
47
|
+
or load_config().get("base_url")
|
|
48
|
+
or DEFAULT_BASE
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _headers() -> dict:
|
|
53
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
54
|
+
key = get_api_key()
|
|
55
|
+
if key:
|
|
56
|
+
headers["X-API-Key"] = key
|
|
57
|
+
return headers
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def api(method: str, path: str, body: dict | None = None):
|
|
61
|
+
url = get_base_url().rstrip("/") + path
|
|
62
|
+
data = json.dumps(body).encode() if body else None
|
|
63
|
+
req = urllib.request.Request(url, data=data, headers=_headers(), method=method)
|
|
64
|
+
try:
|
|
65
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
66
|
+
return json.loads(resp.read())
|
|
67
|
+
except urllib.error.HTTPError as e:
|
|
68
|
+
try:
|
|
69
|
+
err = json.loads(e.read())
|
|
70
|
+
msg = err.get("error") or err.get("message") or str(e)
|
|
71
|
+
except Exception:
|
|
72
|
+
msg = str(e)
|
|
73
|
+
die(f"API error ({e.code}): {msg}")
|
|
74
|
+
except urllib.error.URLError as e:
|
|
75
|
+
die(f"Connection error: {e.reason}\nIs the server reachable at {get_base_url()}?")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def die(msg: str):
|
|
79
|
+
print(f"\033[31m✗\033[0m {msg}", file=sys.stderr)
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def ok(msg: str):
|
|
84
|
+
print(f"\033[32m✓\033[0m {msg}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def info(msg: str):
|
|
88
|
+
print(f"\033[90m {msg}\033[0m")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _col(s: str, w: int, color: str = "") -> str:
|
|
92
|
+
s = str(s or "")
|
|
93
|
+
truncated = s[: w - 1] + "…" if len(s) > w else s.ljust(w)
|
|
94
|
+
return f"\033[{color}m{truncated}\033[0m" if color else truncated
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _status_color(status: str) -> str:
|
|
98
|
+
mapping = {"running": "32", "ready": "32", "starting": "33", "stopped": "31", "error": "31"}
|
|
99
|
+
return mapping.get(status.lower(), "37")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def print_table(headers: list[str], rows: list[list], widths: list[int]):
|
|
103
|
+
header_line = " ".join(_col(h, w, "1") for h, w in zip(headers, widths))
|
|
104
|
+
print(f"\033[90m{header_line}\033[0m")
|
|
105
|
+
print("\033[90m" + "─" * sum(w + 2 for w in widths) + "\033[0m")
|
|
106
|
+
for row in rows:
|
|
107
|
+
parts = []
|
|
108
|
+
for index, (cell, width) in enumerate(zip(row, widths)):
|
|
109
|
+
cell_str = str(cell or "")
|
|
110
|
+
if headers[index].lower() == "status":
|
|
111
|
+
parts.append(_col(cell_str, width, _status_color(cell_str)))
|
|
112
|
+
else:
|
|
113
|
+
parts.append(_col(cell_str, width))
|
|
114
|
+
print(" ".join(parts))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def sandbox_create(args):
|
|
118
|
+
body = {}
|
|
119
|
+
if args.template:
|
|
120
|
+
body["template"] = args.template
|
|
121
|
+
if args.name:
|
|
122
|
+
body["name"] = args.name
|
|
123
|
+
result = api("POST", "/sandboxes", body)
|
|
124
|
+
ok(f"Sandbox created: \033[1m{result['name']}\033[0m")
|
|
125
|
+
info(f"Agent URL : {result.get('agent_url', 'starting...')}")
|
|
126
|
+
info(f"Template : {result.get('template', 'base')}")
|
|
127
|
+
if result.get("ip"):
|
|
128
|
+
info(f"IP : {result['ip']}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def sandbox_list(_args):
|
|
132
|
+
result = api("GET", "/sandboxes")
|
|
133
|
+
items = result if isinstance(result, list) else result.get("sandboxes", [])
|
|
134
|
+
if not items:
|
|
135
|
+
info("No sandboxes found.")
|
|
136
|
+
return
|
|
137
|
+
print_table(
|
|
138
|
+
["NAME", "STATUS", "TEMPLATE", "IP", "CREATED"],
|
|
139
|
+
[
|
|
140
|
+
[
|
|
141
|
+
item.get("name"),
|
|
142
|
+
item.get("status"),
|
|
143
|
+
item.get("template", "base"),
|
|
144
|
+
item.get("ip", ""),
|
|
145
|
+
item.get("created_at", "")[:10] if item.get("created_at") else "",
|
|
146
|
+
]
|
|
147
|
+
for item in items
|
|
148
|
+
],
|
|
149
|
+
[18, 10, 14, 16, 12],
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def sandbox_exec(args):
|
|
154
|
+
result = api("POST", f"/sandboxes/{args.name}/exec", {"cmd": " ".join(args.cmd)})
|
|
155
|
+
stdout = result.get("stdout", "")
|
|
156
|
+
stderr = result.get("stderr", "")
|
|
157
|
+
if stdout:
|
|
158
|
+
print(stdout, end="" if stdout.endswith("\n") else "\n")
|
|
159
|
+
if stderr:
|
|
160
|
+
print(stderr, end="" if stderr.endswith("\n") else "\n", file=sys.stderr)
|
|
161
|
+
exit_code = result.get("exit_code", 0)
|
|
162
|
+
if exit_code != 0:
|
|
163
|
+
sys.exit(exit_code)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def sandbox_delete(args):
|
|
167
|
+
api("DELETE", f"/sandboxes/{args.name}")
|
|
168
|
+
ok(f"Sandbox \033[1m{args.name}\033[0m deleted.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def sandbox_shell(args):
|
|
172
|
+
result = api("GET", f"/sandboxes/{args.name}")
|
|
173
|
+
ip = result.get("ip")
|
|
174
|
+
if not ip:
|
|
175
|
+
die(f"Sandbox {args.name} has no IP yet (status: {result.get('status')}). Try again shortly.")
|
|
176
|
+
print(f"\033[90mConnecting to {args.name} ({ip})…\033[0m")
|
|
177
|
+
os.execvp("ssh", ["ssh", "-o", "StrictHostKeyChecking=no", f"root@{ip}"])
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def db_create(args):
|
|
181
|
+
body = {}
|
|
182
|
+
if args.name:
|
|
183
|
+
body["name"] = args.name
|
|
184
|
+
if args.ttl:
|
|
185
|
+
body["ttl_hours"] = int(args.ttl)
|
|
186
|
+
result = api("POST", "/instances", body)
|
|
187
|
+
ok(f"Database created: \033[1m{result['name']}\033[0m")
|
|
188
|
+
info(f"Host : {result.get('ip', 'starting...')}")
|
|
189
|
+
info(f"Port : {result.get('pg_port', 5432)}")
|
|
190
|
+
info(f"User : {result.get('username', 'postgres')}")
|
|
191
|
+
info(f"Pass : {result.get('password', '(see dashboard)')}")
|
|
192
|
+
if result.get("ip") and result.get("pg_port"):
|
|
193
|
+
pg = (
|
|
194
|
+
f"postgresql://{result.get('username', 'postgres')}:{result.get('password', '')}@"
|
|
195
|
+
f"{result['ip']}:{result['pg_port']}/postgres"
|
|
196
|
+
)
|
|
197
|
+
info(f"DSN : {pg}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def db_list(_args):
|
|
201
|
+
result = api("GET", "/instances")
|
|
202
|
+
items = result if isinstance(result, list) else result.get("instances", [])
|
|
203
|
+
if not items:
|
|
204
|
+
info("No databases found.")
|
|
205
|
+
return
|
|
206
|
+
print_table(
|
|
207
|
+
["NAME", "STATUS", "IP", "PORT", "USER", "CREATED"],
|
|
208
|
+
[
|
|
209
|
+
[
|
|
210
|
+
item.get("name"),
|
|
211
|
+
item.get("status"),
|
|
212
|
+
item.get("ip", ""),
|
|
213
|
+
item.get("pg_port", ""),
|
|
214
|
+
item.get("username", ""),
|
|
215
|
+
item.get("created_at", "")[:10] if item.get("created_at") else "",
|
|
216
|
+
]
|
|
217
|
+
for item in items
|
|
218
|
+
],
|
|
219
|
+
[18, 10, 16, 6, 12, 12],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def db_connect(args):
|
|
224
|
+
result = api("GET", f"/instances/{args.name}")
|
|
225
|
+
ip = result.get("ip")
|
|
226
|
+
port = result.get("pg_port", 5432)
|
|
227
|
+
user = result.get("username", "postgres")
|
|
228
|
+
password = result.get("password", "")
|
|
229
|
+
if not ip:
|
|
230
|
+
die(f"Database {args.name} has no IP yet (status: {result.get('status')}). Try again shortly.")
|
|
231
|
+
env = {**os.environ, "PGPASSWORD": password}
|
|
232
|
+
print(f"\033[90mConnecting psql → {args.name} ({ip}:{port})…\033[0m")
|
|
233
|
+
os.execvpe("psql", ["psql", "-h", ip, "-p", str(port), "-U", user, "-d", "postgres"], env)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def db_delete(args):
|
|
237
|
+
api("DELETE", f"/instances/{args.name}")
|
|
238
|
+
ok(f"Database \033[1m{args.name}\033[0m deleted.")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def keys_list(_args):
|
|
242
|
+
result = api("GET", "/api-keys")
|
|
243
|
+
items = result if isinstance(result, list) else result.get("keys", [])
|
|
244
|
+
if not items:
|
|
245
|
+
info("No API keys found.")
|
|
246
|
+
return
|
|
247
|
+
print_table(
|
|
248
|
+
["ID", "PREFIX", "LABEL", "CREATED"],
|
|
249
|
+
[
|
|
250
|
+
[
|
|
251
|
+
item.get("id", ""),
|
|
252
|
+
item.get("key_prefix", ""),
|
|
253
|
+
item.get("label", ""),
|
|
254
|
+
item.get("created_at", "")[:10] if item.get("created_at") else "",
|
|
255
|
+
]
|
|
256
|
+
for item in items
|
|
257
|
+
],
|
|
258
|
+
[36, 20, 24, 12],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def keys_create(args):
|
|
263
|
+
body = {}
|
|
264
|
+
if args.label:
|
|
265
|
+
body["label"] = args.label
|
|
266
|
+
result = api("POST", "/api-keys", body)
|
|
267
|
+
ok("API key created:")
|
|
268
|
+
print(f"\n \033[1;33m{result['key']}\033[0m\n")
|
|
269
|
+
print(" \033[90mStore this securely — it won't be shown again.\033[0m")
|
|
270
|
+
print(f" Key ID: {result.get('id', '')}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def keys_revoke(args):
|
|
274
|
+
api("DELETE", f"/api-keys/{args.id}")
|
|
275
|
+
ok(f"API key \033[1m{args.id}\033[0m revoked.")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def config_set_key(args):
|
|
279
|
+
cfg = load_config()
|
|
280
|
+
cfg["api_key"] = args.api_key
|
|
281
|
+
save_config(cfg)
|
|
282
|
+
ok(f"API key saved to {PRIMARY_CONFIG_FILE}")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def config_show(_args):
|
|
286
|
+
cfg = load_config()
|
|
287
|
+
key = cfg.get("api_key", "")
|
|
288
|
+
masked = key[:12] + "…" if len(key) > 12 else key
|
|
289
|
+
print(f" base_url : {cfg.get('base_url', DEFAULT_BASE)}")
|
|
290
|
+
print(f" api_key : {masked or '(not set)'}")
|
|
291
|
+
cfg_path = PRIMARY_CONFIG_FILE if PRIMARY_CONFIG_FILE.exists() else LEGACY_CONFIG_FILE
|
|
292
|
+
print(f" config : {cfg_path}")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def main():
|
|
296
|
+
prog_name = Path(sys.argv[0]).stem or "sandflare"
|
|
297
|
+
parser = argparse.ArgumentParser(
|
|
298
|
+
prog=prog_name,
|
|
299
|
+
description="Sandflare CLI — manage sandboxes and databases",
|
|
300
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
301
|
+
)
|
|
302
|
+
sub = parser.add_subparsers(dest="resource", metavar="<resource>")
|
|
303
|
+
|
|
304
|
+
sandbox_parser = sub.add_parser("sandbox", aliases=["sb"], help="Manage sandboxes")
|
|
305
|
+
sandbox_sub = sandbox_parser.add_subparsers(dest="action", metavar="<action>")
|
|
306
|
+
|
|
307
|
+
sandbox_create_parser = sandbox_sub.add_parser("create", help="Create a sandbox")
|
|
308
|
+
sandbox_create_parser.add_argument("--template", "-t", help="Template name (python, node, fullstack…)")
|
|
309
|
+
sandbox_create_parser.add_argument("--name", "-n", help="Custom name")
|
|
310
|
+
sandbox_create_parser.set_defaults(func=sandbox_create)
|
|
311
|
+
|
|
312
|
+
sandbox_list_parser = sandbox_sub.add_parser("list", aliases=["ls"], help="List sandboxes")
|
|
313
|
+
sandbox_list_parser.set_defaults(func=sandbox_list)
|
|
314
|
+
|
|
315
|
+
sandbox_exec_parser = sandbox_sub.add_parser("exec", help="Run a command in a sandbox")
|
|
316
|
+
sandbox_exec_parser.add_argument("name", help="Sandbox name")
|
|
317
|
+
sandbox_exec_parser.add_argument("cmd", nargs=argparse.REMAINDER, help="Command to run")
|
|
318
|
+
sandbox_exec_parser.set_defaults(func=sandbox_exec)
|
|
319
|
+
|
|
320
|
+
sandbox_delete_parser = sandbox_sub.add_parser("delete", aliases=["rm", "del"], help="Delete a sandbox")
|
|
321
|
+
sandbox_delete_parser.add_argument("name", help="Sandbox name")
|
|
322
|
+
sandbox_delete_parser.set_defaults(func=sandbox_delete)
|
|
323
|
+
|
|
324
|
+
sandbox_shell_parser = sandbox_sub.add_parser("shell", aliases=["ssh"], help="Open a shell in a sandbox")
|
|
325
|
+
sandbox_shell_parser.add_argument("name", help="Sandbox name")
|
|
326
|
+
sandbox_shell_parser.set_defaults(func=sandbox_shell)
|
|
327
|
+
|
|
328
|
+
db_parser = sub.add_parser("db", help="Manage databases")
|
|
329
|
+
db_sub = db_parser.add_subparsers(dest="action", metavar="<action>")
|
|
330
|
+
|
|
331
|
+
db_create_parser = db_sub.add_parser("create", help="Create a PostgreSQL database")
|
|
332
|
+
db_create_parser.add_argument("--name", "-n", help="Custom name")
|
|
333
|
+
db_create_parser.add_argument("--ttl", help="TTL in hours (default 24)")
|
|
334
|
+
db_create_parser.set_defaults(func=db_create)
|
|
335
|
+
|
|
336
|
+
db_list_parser = db_sub.add_parser("list", aliases=["ls"], help="List databases")
|
|
337
|
+
db_list_parser.set_defaults(func=db_list)
|
|
338
|
+
|
|
339
|
+
db_connect_parser = db_sub.add_parser("connect", aliases=["psql"], help="Connect via psql")
|
|
340
|
+
db_connect_parser.add_argument("name", help="Database name")
|
|
341
|
+
db_connect_parser.set_defaults(func=db_connect)
|
|
342
|
+
|
|
343
|
+
db_delete_parser = db_sub.add_parser("delete", aliases=["rm", "del"], help="Delete a database")
|
|
344
|
+
db_delete_parser.add_argument("name", help="Database name")
|
|
345
|
+
db_delete_parser.set_defaults(func=db_delete)
|
|
346
|
+
|
|
347
|
+
keys_parser = sub.add_parser("keys", help="Manage API keys")
|
|
348
|
+
keys_sub = keys_parser.add_subparsers(dest="action", metavar="<action>")
|
|
349
|
+
|
|
350
|
+
keys_list_parser = keys_sub.add_parser("list", aliases=["ls"], help="List API keys")
|
|
351
|
+
keys_list_parser.set_defaults(func=keys_list)
|
|
352
|
+
|
|
353
|
+
keys_create_parser = keys_sub.add_parser("create", help="Create an API key")
|
|
354
|
+
keys_create_parser.add_argument("--label", "-l", help="Key label/description")
|
|
355
|
+
keys_create_parser.set_defaults(func=keys_create)
|
|
356
|
+
|
|
357
|
+
keys_revoke_parser = keys_sub.add_parser("revoke", aliases=["delete", "rm"], help="Revoke an API key")
|
|
358
|
+
keys_revoke_parser.add_argument("id", help="Key ID to revoke")
|
|
359
|
+
keys_revoke_parser.set_defaults(func=keys_revoke)
|
|
360
|
+
|
|
361
|
+
config_parser = sub.add_parser("config", help="CLI configuration")
|
|
362
|
+
config_sub = config_parser.add_subparsers(dest="action", metavar="<action>")
|
|
363
|
+
|
|
364
|
+
config_key_parser = config_sub.add_parser("set-key", help="Save API key to config")
|
|
365
|
+
config_key_parser.add_argument("api_key", help="Your pa_live_... API key")
|
|
366
|
+
config_key_parser.set_defaults(func=config_set_key)
|
|
367
|
+
|
|
368
|
+
config_show_parser = config_sub.add_parser("show", help="Show current config")
|
|
369
|
+
config_show_parser.set_defaults(func=config_show)
|
|
370
|
+
|
|
371
|
+
args = parser.parse_args()
|
|
372
|
+
if not hasattr(args, "func"):
|
|
373
|
+
for subparser in [sandbox_parser, db_parser, keys_parser, config_parser]:
|
|
374
|
+
if args.resource in (
|
|
375
|
+
getattr(subparser, "prog", "").split()[-1:] + list(subparser._option_string_actions.get("aliases", []))
|
|
376
|
+
):
|
|
377
|
+
subparser.print_help()
|
|
378
|
+
sys.exit(0)
|
|
379
|
+
parser.print_help()
|
|
380
|
+
sys.exit(0)
|
|
381
|
+
|
|
382
|
+
args.func(args)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
if __name__ == "__main__":
|
|
386
|
+
main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandflare-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sandflare CLI — manage sandboxes and databases
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: sandflare,sandbox,microvm,ai,firecracker
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Sandflare CLI
|
|
15
|
+
|
|
16
|
+
Zero-dependency Python CLI for managing Sandflare sandboxes and databases.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install sandflare-cli
|
|
22
|
+
# or from source:
|
|
23
|
+
pip install -e sdk/cli/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Save your API key once
|
|
30
|
+
sandflare config set-key pa_live_your_key_here
|
|
31
|
+
|
|
32
|
+
# Sandboxes
|
|
33
|
+
sandflare sandbox create --template python
|
|
34
|
+
sandflare sandbox list
|
|
35
|
+
sandflare sandbox exec sb-1 python3 -c "print('hello')"
|
|
36
|
+
sandflare sandbox shell sb-1
|
|
37
|
+
sandflare sandbox delete sb-1
|
|
38
|
+
|
|
39
|
+
# Databases
|
|
40
|
+
sandflare db create
|
|
41
|
+
sandflare db list
|
|
42
|
+
sandflare db connect psql-1
|
|
43
|
+
sandflare db delete psql-1
|
|
44
|
+
|
|
45
|
+
# API Keys
|
|
46
|
+
sandflare keys list
|
|
47
|
+
sandflare keys create --label "CI/CD"
|
|
48
|
+
sandflare keys revoke <key-id>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Environment Variables
|
|
52
|
+
|
|
53
|
+
| Variable | Description |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `SANDFLARE_API_KEY` | API key (preferred) |
|
|
56
|
+
| `PANDAAGENT_API_KEY` | Legacy API key env alias |
|
|
57
|
+
| `SANDFLARE_API_URL` | API base URL (default: https://api.sandflare.io) |
|
|
58
|
+
|
|
59
|
+
## Aliases
|
|
60
|
+
|
|
61
|
+
Preferred commands:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
sandflare sandbox create --template node
|
|
65
|
+
sf sb ls
|
|
66
|
+
sf db ls
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Legacy `pandaagent` / `pa` aliases can remain during migration.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
sandflare_cli/__init__.py
|
|
4
|
+
sandflare_cli/main.py
|
|
5
|
+
sandflare_cli.egg-info/PKG-INFO
|
|
6
|
+
sandflare_cli.egg-info/SOURCES.txt
|
|
7
|
+
sandflare_cli.egg-info/dependency_links.txt
|
|
8
|
+
sandflare_cli.egg-info/entry_points.txt
|
|
9
|
+
sandflare_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sandflare_cli
|