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.
@@ -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,3 @@
1
+ from .main import main
2
+
3
+ __all__ = ["main"]
@@ -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,5 @@
1
+ [console_scripts]
2
+ pa = sandflare_cli.main:main
3
+ pandaagent = sandflare_cli.main:main
4
+ sandflare = sandflare_cli.main:main
5
+ sf = sandflare_cli.main:main
@@ -0,0 +1 @@
1
+ sandflare_cli
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+