treadstone-cli 0.2.1__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.
- treadstone_cli/__init__.py +0 -0
- treadstone_cli/__main__.py +6 -0
- treadstone_cli/_client.py +77 -0
- treadstone_cli/_output.py +102 -0
- treadstone_cli/api_keys.py +73 -0
- treadstone_cli/auth.py +153 -0
- treadstone_cli/config_cmd.py +157 -0
- treadstone_cli/main.py +104 -0
- treadstone_cli/sandboxes.py +170 -0
- treadstone_cli/templates.py +39 -0
- treadstone_cli-0.2.1.dist-info/METADATA +222 -0
- treadstone_cli-0.2.1.dist-info/RECORD +14 -0
- treadstone_cli-0.2.1.dist-info/WHEEL +4 -0
- treadstone_cli-0.2.1.dist-info/entry_points.txt +2 -0
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""HTTP client factory for CLI commands.
|
|
2
|
+
|
|
3
|
+
Priority: CLI flags > environment variables > config file.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
CONFIG_DIR = Path.home() / ".config" / "treadstone"
|
|
16
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
17
|
+
|
|
18
|
+
_DEFAULT_BASE_URL = "https://api.treadstone-ai.dev"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read_config() -> dict[str, str]:
|
|
22
|
+
if not CONFIG_FILE.exists():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
import tomllib
|
|
26
|
+
except ModuleNotFoundError:
|
|
27
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
28
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
29
|
+
data = tomllib.load(f)
|
|
30
|
+
return data.get("default", {})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_base_url(ctx: click.Context) -> str:
|
|
34
|
+
url = ctx.obj.get("base_url") or _read_config().get("base_url") or _DEFAULT_BASE_URL
|
|
35
|
+
return url.rstrip("/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_api_key(ctx: click.Context) -> str | None:
|
|
39
|
+
return ctx.obj.get("api_key") or _read_config().get("api_key")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def effective_base_url() -> tuple[str, str]:
|
|
43
|
+
"""Return (url, source) without requiring a Click context.
|
|
44
|
+
|
|
45
|
+
Source is one of: 'env', 'file', 'default'.
|
|
46
|
+
Used for displaying configuration in help text.
|
|
47
|
+
"""
|
|
48
|
+
env = os.environ.get("TREADSTONE_BASE_URL")
|
|
49
|
+
if env:
|
|
50
|
+
return env.rstrip("/"), "env"
|
|
51
|
+
cfg = _read_config().get("base_url")
|
|
52
|
+
if cfg:
|
|
53
|
+
return cfg.rstrip("/"), "file"
|
|
54
|
+
return _DEFAULT_BASE_URL, "default"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def effective_api_key() -> str | None:
|
|
58
|
+
"""Return API key without requiring a Click context."""
|
|
59
|
+
return os.environ.get("TREADSTONE_API_KEY") or _read_config().get("api_key")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_client(ctx: click.Context) -> httpx.Client:
|
|
63
|
+
base_url = get_base_url(ctx)
|
|
64
|
+
api_key = get_api_key(ctx)
|
|
65
|
+
headers: dict[str, str] = {}
|
|
66
|
+
if api_key:
|
|
67
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
68
|
+
return httpx.Client(base_url=base_url, headers=headers, timeout=30.0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def require_auth(ctx: click.Context) -> httpx.Client:
|
|
72
|
+
"""Build client and abort if no API key is configured."""
|
|
73
|
+
client = build_client(ctx)
|
|
74
|
+
if "Authorization" not in client.headers:
|
|
75
|
+
click.echo("Error: No API key configured. Set TREADSTONE_API_KEY or use --api-key.", err=True)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
return client
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Output formatting utilities for CLI — table (Rich) or JSON mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import httpx
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def friendly_exception_handler(cli_main: click.Group) -> click.Group:
|
|
18
|
+
"""Wrap a Click group so unhandled exceptions print user-friendly messages instead of tracebacks."""
|
|
19
|
+
original_main = cli_main.main
|
|
20
|
+
|
|
21
|
+
def _patched_main(*args: Any, **kwargs: Any) -> Any:
|
|
22
|
+
try:
|
|
23
|
+
return original_main(*args, standalone_mode=False, **kwargs)
|
|
24
|
+
except click.exceptions.Abort:
|
|
25
|
+
click.echo("\nAborted.", err=True)
|
|
26
|
+
sys.exit(130)
|
|
27
|
+
except click.exceptions.Exit as exc:
|
|
28
|
+
sys.exit(exc.exit_code)
|
|
29
|
+
except click.UsageError as exc:
|
|
30
|
+
exc.show()
|
|
31
|
+
sys.exit(exc.exit_code if exc.exit_code is not None else 2)
|
|
32
|
+
except SystemExit:
|
|
33
|
+
raise
|
|
34
|
+
except KeyboardInterrupt:
|
|
35
|
+
click.echo("\nInterrupted.", err=True)
|
|
36
|
+
sys.exit(130)
|
|
37
|
+
except httpx.ConnectError as exc:
|
|
38
|
+
_print_network_hint(str(exc), "Connection refused")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
except httpx.TimeoutException:
|
|
41
|
+
click.echo("Error: Request timed out. The server may be slow or unreachable.", err=True)
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
except httpx.HTTPStatusError as exc:
|
|
44
|
+
click.echo(f"Error: HTTP {exc.response.status_code} — {exc.response.text[:200]}", err=True)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
except httpx.HTTPError as exc:
|
|
47
|
+
click.echo(f"Error: {type(exc).__name__} — {exc}", err=True)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
click.echo(f"Error: {exc}", err=True)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
cli_main.main = _patched_main # type: ignore[assignment]
|
|
54
|
+
return cli_main
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _print_network_hint(detail: str, summary: str) -> None:
|
|
58
|
+
click.echo(f"Error: {summary}.", err=True)
|
|
59
|
+
click.echo(" Possible causes:", err=True)
|
|
60
|
+
click.echo(" - The Treadstone server is not running", err=True)
|
|
61
|
+
click.echo(" - The --base-url or TREADSTONE_BASE_URL is incorrect", err=True)
|
|
62
|
+
click.echo(" - A firewall or proxy is blocking the connection", err=True)
|
|
63
|
+
click.echo(f" Detail: {detail}", err=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_json_mode(ctx: click.Context) -> bool:
|
|
67
|
+
return ctx.obj.get("json_output", False)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_json(data: Any) -> None:
|
|
71
|
+
click.echo(json.dumps(data, indent=2, default=str))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_table(columns: list[str], rows: list[list[Any]], title: str | None = None) -> None:
|
|
75
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
76
|
+
for col in columns:
|
|
77
|
+
table.add_column(col)
|
|
78
|
+
for row in rows:
|
|
79
|
+
table.add_row(*[str(v) if v is not None else "" for v in row])
|
|
80
|
+
console.print(table)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def print_detail(data: dict[str, Any], title: str | None = None) -> None:
|
|
84
|
+
table = Table(title=title, show_header=False)
|
|
85
|
+
table.add_column("Field", style="bold")
|
|
86
|
+
table.add_column("Value")
|
|
87
|
+
for k, v in data.items():
|
|
88
|
+
table.add_row(k, str(v) if v is not None else "")
|
|
89
|
+
console.print(table)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def handle_error(resp: Any) -> None:
|
|
93
|
+
"""Print error and exit if response is not 2xx."""
|
|
94
|
+
if resp.status_code >= 400:
|
|
95
|
+
try:
|
|
96
|
+
body = resp.json()
|
|
97
|
+
err = body.get("error", body)
|
|
98
|
+
msg = err.get("message", str(err)) if isinstance(err, dict) else str(err)
|
|
99
|
+
except Exception:
|
|
100
|
+
msg = resp.text
|
|
101
|
+
click.echo(f"Error ({resp.status_code}): {msg}", err=True)
|
|
102
|
+
sys.exit(1)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""API key management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from treadstone_cli._client import require_auth
|
|
8
|
+
from treadstone_cli._output import handle_error, is_json_mode, print_json, print_table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def api_keys() -> None:
|
|
13
|
+
"""Manage API keys.
|
|
14
|
+
|
|
15
|
+
API keys provide long-lived authentication tokens for programmatic access.
|
|
16
|
+
|
|
17
|
+
\b
|
|
18
|
+
Examples:
|
|
19
|
+
treadstone api-keys create --name ci-bot
|
|
20
|
+
treadstone api-keys list
|
|
21
|
+
treadstone api-keys delete <key-id>
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@api_keys.command("create")
|
|
26
|
+
@click.option("--name", default="default", help="Key name.")
|
|
27
|
+
@click.option("--expires-in", default=None, type=int, help="Key lifetime in seconds.")
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def create_key(ctx: click.Context, name: str, expires_in: int | None) -> None:
|
|
30
|
+
"""Create a new API key.
|
|
31
|
+
|
|
32
|
+
The full key is shown only once — store it securely.
|
|
33
|
+
"""
|
|
34
|
+
client = require_auth(ctx)
|
|
35
|
+
body: dict = {"name": name}
|
|
36
|
+
if expires_in is not None:
|
|
37
|
+
body["expires_in"] = expires_in
|
|
38
|
+
resp = client.post("/v1/auth/api-keys", json=body)
|
|
39
|
+
handle_error(resp)
|
|
40
|
+
data = resp.json()
|
|
41
|
+
if is_json_mode(ctx):
|
|
42
|
+
print_json(data)
|
|
43
|
+
else:
|
|
44
|
+
click.echo(f"API Key created: {data['key']}")
|
|
45
|
+
click.echo(f" ID: {data['id']} Name: {data['name']}")
|
|
46
|
+
click.echo(" Store this key securely — it won't be shown again.")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@api_keys.command("list")
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def list_keys(ctx: click.Context) -> None:
|
|
52
|
+
"""List all API keys for the current user."""
|
|
53
|
+
client = require_auth(ctx)
|
|
54
|
+
resp = client.get("/v1/auth/api-keys")
|
|
55
|
+
handle_error(resp)
|
|
56
|
+
data = resp.json()
|
|
57
|
+
if is_json_mode(ctx):
|
|
58
|
+
print_json(data)
|
|
59
|
+
else:
|
|
60
|
+
items = data.get("items", [])
|
|
61
|
+
rows = [[k["id"], k["name"], k["key_prefix"], k.get("created_at", ""), k.get("expires_at", "")] for k in items]
|
|
62
|
+
print_table(["ID", "Name", "Key Prefix", "Created", "Expires"], rows, title="API Keys")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@api_keys.command("delete")
|
|
66
|
+
@click.argument("key_id")
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def delete_key(ctx: click.Context, key_id: str) -> None:
|
|
69
|
+
"""Revoke and delete an API key."""
|
|
70
|
+
client = require_auth(ctx)
|
|
71
|
+
resp = client.delete(f"/v1/auth/api-keys/{key_id}")
|
|
72
|
+
handle_error(resp)
|
|
73
|
+
click.echo(f"API key {key_id} deleted.")
|
treadstone_cli/auth.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Auth commands — login, logout, register, whoami, change-password, invite, users, delete-user."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from treadstone_cli._client import build_client, require_auth
|
|
8
|
+
from treadstone_cli._output import handle_error, is_json_mode, print_detail, print_json, print_table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def auth() -> None:
|
|
13
|
+
"""Authentication and user management.
|
|
14
|
+
|
|
15
|
+
Register, log in, manage users, and change passwords.
|
|
16
|
+
|
|
17
|
+
\b
|
|
18
|
+
Quick start:
|
|
19
|
+
treadstone auth register Create a new account
|
|
20
|
+
treadstone auth login Log in (saves session)
|
|
21
|
+
treadstone auth whoami Verify current identity
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@auth.command("login")
|
|
26
|
+
@click.option("--email", required=True, prompt=True, help="Account email.")
|
|
27
|
+
@click.option("--password", required=True, prompt=True, hide_input=True, help="Account password.")
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def login(ctx: click.Context, email: str, password: str) -> None:
|
|
30
|
+
"""Log in with email and password.
|
|
31
|
+
|
|
32
|
+
If --email or --password are not provided, you will be prompted interactively.
|
|
33
|
+
"""
|
|
34
|
+
client = build_client(ctx)
|
|
35
|
+
resp = client.post("/v1/auth/login", json={"email": email, "password": password})
|
|
36
|
+
handle_error(resp)
|
|
37
|
+
data = resp.json()
|
|
38
|
+
if is_json_mode(ctx):
|
|
39
|
+
print_json(data)
|
|
40
|
+
else:
|
|
41
|
+
click.echo("Login successful.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@auth.command("logout")
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def logout(ctx: click.Context) -> None:
|
|
47
|
+
"""Log out (clear session cookie)."""
|
|
48
|
+
client = build_client(ctx)
|
|
49
|
+
resp = client.post("/v1/auth/logout")
|
|
50
|
+
handle_error(resp)
|
|
51
|
+
if is_json_mode(ctx):
|
|
52
|
+
print_json(resp.json())
|
|
53
|
+
else:
|
|
54
|
+
click.echo("Logged out.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@auth.command("register")
|
|
58
|
+
@click.option("--email", required=True, prompt=True, help="Account email.")
|
|
59
|
+
@click.option("--password", required=True, prompt=True, hide_input=True, confirmation_prompt=True, help="Password.")
|
|
60
|
+
@click.option("--invitation-token", default=None, help="Invitation token (required in invitation mode).")
|
|
61
|
+
@click.pass_context
|
|
62
|
+
def register(ctx: click.Context, email: str, password: str, invitation_token: str | None) -> None:
|
|
63
|
+
"""Register a new account.
|
|
64
|
+
|
|
65
|
+
In invitation-only mode, an --invitation-token is required.
|
|
66
|
+
"""
|
|
67
|
+
client = build_client(ctx)
|
|
68
|
+
body: dict = {"email": email, "password": password}
|
|
69
|
+
if invitation_token:
|
|
70
|
+
body["invitation_token"] = invitation_token
|
|
71
|
+
resp = client.post("/v1/auth/register", json=body)
|
|
72
|
+
handle_error(resp)
|
|
73
|
+
data = resp.json()
|
|
74
|
+
if is_json_mode(ctx):
|
|
75
|
+
print_json(data)
|
|
76
|
+
else:
|
|
77
|
+
click.echo(f"Registered: {data['email']} (role: {data['role']})")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@auth.command("whoami")
|
|
81
|
+
@click.pass_context
|
|
82
|
+
def whoami(ctx: click.Context) -> None:
|
|
83
|
+
"""Show current user info."""
|
|
84
|
+
client = require_auth(ctx)
|
|
85
|
+
resp = client.get("/v1/auth/user")
|
|
86
|
+
handle_error(resp)
|
|
87
|
+
data = resp.json()
|
|
88
|
+
if is_json_mode(ctx):
|
|
89
|
+
print_json(data)
|
|
90
|
+
else:
|
|
91
|
+
print_detail(data, title="Current User")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@auth.command("change-password")
|
|
95
|
+
@click.option("--old-password", required=True, prompt=True, hide_input=True, help="Current password.")
|
|
96
|
+
@click.option(
|
|
97
|
+
"--new-password", required=True, prompt=True, hide_input=True, confirmation_prompt=True, help="New password."
|
|
98
|
+
)
|
|
99
|
+
@click.pass_context
|
|
100
|
+
def change_password(ctx: click.Context, old_password: str, new_password: str) -> None:
|
|
101
|
+
"""Change your password."""
|
|
102
|
+
client = require_auth(ctx)
|
|
103
|
+
resp = client.post("/v1/auth/change-password", json={"old_password": old_password, "new_password": new_password})
|
|
104
|
+
handle_error(resp)
|
|
105
|
+
if is_json_mode(ctx):
|
|
106
|
+
print_json(resp.json())
|
|
107
|
+
else:
|
|
108
|
+
click.echo("Password changed.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@auth.command("invite")
|
|
112
|
+
@click.option("--email", required=True, help="Email of the person to invite.")
|
|
113
|
+
@click.option("--role", default="ro", help="Role for the invitee (admin or ro).")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def invite(ctx: click.Context, email: str, role: str) -> None:
|
|
116
|
+
"""Generate an invitation token for a new user (admin only)."""
|
|
117
|
+
client = require_auth(ctx)
|
|
118
|
+
resp = client.post("/v1/auth/invite", json={"email": email, "role": role})
|
|
119
|
+
handle_error(resp)
|
|
120
|
+
data = resp.json()
|
|
121
|
+
if is_json_mode(ctx):
|
|
122
|
+
print_json(data)
|
|
123
|
+
else:
|
|
124
|
+
click.echo(f"Invitation sent to {data['email']}. Token: {data['token']}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@auth.command("users")
|
|
128
|
+
@click.option("--limit", default=100, type=int, help="Max results.")
|
|
129
|
+
@click.option("--offset", default=0, type=int, help="Skip N results.")
|
|
130
|
+
@click.pass_context
|
|
131
|
+
def list_users(ctx: click.Context, limit: int, offset: int) -> None:
|
|
132
|
+
"""List all registered users (admin only)."""
|
|
133
|
+
client = require_auth(ctx)
|
|
134
|
+
resp = client.get("/v1/auth/users", params={"limit": limit, "offset": offset})
|
|
135
|
+
handle_error(resp)
|
|
136
|
+
data = resp.json()
|
|
137
|
+
if is_json_mode(ctx):
|
|
138
|
+
print_json(data)
|
|
139
|
+
else:
|
|
140
|
+
items = data.get("items", [])
|
|
141
|
+
rows = [[u["id"], u["email"], u["role"]] for u in items]
|
|
142
|
+
print_table(["ID", "Email", "Role"], rows, title=f"Users ({data['total']} total)")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@auth.command("delete-user")
|
|
146
|
+
@click.argument("user_id")
|
|
147
|
+
@click.pass_context
|
|
148
|
+
def delete_user(ctx: click.Context, user_id: str) -> None:
|
|
149
|
+
"""Delete a user (admin only)."""
|
|
150
|
+
client = require_auth(ctx)
|
|
151
|
+
resp = client.delete(f"/v1/auth/users/{user_id}")
|
|
152
|
+
handle_error(resp)
|
|
153
|
+
click.echo(f"User {user_id} deleted.")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Local CLI configuration commands.
|
|
2
|
+
|
|
3
|
+
Manages ~/.config/treadstone/config.toml which stores defaults for
|
|
4
|
+
base_url, api_key, and other settings so they don't need to be passed
|
|
5
|
+
as flags or env vars every time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from treadstone_cli._client import CONFIG_DIR, CONFIG_FILE, _read_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _write_config(data: dict[str, dict[str, str]]) -> None:
|
|
16
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
lines: list[str] = []
|
|
18
|
+
for section, kvs in data.items():
|
|
19
|
+
lines.append(f"[{section}]")
|
|
20
|
+
for k, v in kvs.items():
|
|
21
|
+
lines.append(f'{k} = "{v}"')
|
|
22
|
+
lines.append("")
|
|
23
|
+
CONFIG_FILE.write_text("\n".join(lines))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_VALID_KEYS = ("base_url", "api_key")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group()
|
|
30
|
+
def config() -> None:
|
|
31
|
+
"""Manage local CLI configuration.
|
|
32
|
+
|
|
33
|
+
Configuration is stored in ~/.config/treadstone/config.toml and provides
|
|
34
|
+
default values for --base-url and --api-key so they don't need to be
|
|
35
|
+
repeated on every command invocation.
|
|
36
|
+
|
|
37
|
+
\b
|
|
38
|
+
Priority (highest to lowest):
|
|
39
|
+
1. CLI flags (--base-url, --api-key)
|
|
40
|
+
2. Env vars (TREADSTONE_BASE_URL, TREADSTONE_API_KEY)
|
|
41
|
+
3. Config file (~/.config/treadstone/config.toml)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@config.command("set")
|
|
46
|
+
@click.argument("key", type=click.Choice(_VALID_KEYS, case_sensitive=False))
|
|
47
|
+
@click.argument("value")
|
|
48
|
+
def set_value(key: str, value: str) -> None:
|
|
49
|
+
"""Set a configuration value.
|
|
50
|
+
|
|
51
|
+
\b
|
|
52
|
+
Available keys:
|
|
53
|
+
base_url Base URL of the Treadstone server
|
|
54
|
+
api_key Default API key for authentication
|
|
55
|
+
|
|
56
|
+
\b
|
|
57
|
+
Examples:
|
|
58
|
+
treadstone config set base_url https://my-server.example.com
|
|
59
|
+
treadstone config set api_key ts_live_xxxxxxxxxxxx
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
import tomllib
|
|
63
|
+
except ModuleNotFoundError:
|
|
64
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
65
|
+
|
|
66
|
+
raw: dict[str, dict[str, str]] = {}
|
|
67
|
+
if CONFIG_FILE.exists():
|
|
68
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
69
|
+
raw = tomllib.load(f) # type: ignore[assignment]
|
|
70
|
+
|
|
71
|
+
section = raw.setdefault("default", {})
|
|
72
|
+
section[key] = value
|
|
73
|
+
_write_config(raw)
|
|
74
|
+
click.echo(f"Set {key} = {value}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@config.command("get")
|
|
78
|
+
@click.argument("key", required=False, default=None)
|
|
79
|
+
def get_value(key: str | None) -> None:
|
|
80
|
+
"""Get a configuration value (or all values if no key given).
|
|
81
|
+
|
|
82
|
+
\b
|
|
83
|
+
Examples:
|
|
84
|
+
treadstone config get Show all config values
|
|
85
|
+
treadstone config get base_url Show only base_url
|
|
86
|
+
"""
|
|
87
|
+
data = _read_config()
|
|
88
|
+
if not data:
|
|
89
|
+
click.echo("No configuration set. Run 'treadstone config set <key> <value>' to get started.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if key is not None:
|
|
93
|
+
if key not in _VALID_KEYS:
|
|
94
|
+
click.echo(f"Error: Unknown key '{key}'. Valid keys: {', '.join(_VALID_KEYS)}", err=True)
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
val = data.get(key)
|
|
97
|
+
if val is None:
|
|
98
|
+
click.echo(f"{key} is not set.")
|
|
99
|
+
else:
|
|
100
|
+
display = _mask_secret(key, val)
|
|
101
|
+
click.echo(f"{key} = {display}")
|
|
102
|
+
else:
|
|
103
|
+
for k in _VALID_KEYS:
|
|
104
|
+
val = data.get(k)
|
|
105
|
+
if val is not None:
|
|
106
|
+
click.echo(f"{k} = {_mask_secret(k, val)}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@config.command("unset")
|
|
110
|
+
@click.argument("key", type=click.Choice(_VALID_KEYS, case_sensitive=False))
|
|
111
|
+
def unset_value(key: str) -> None:
|
|
112
|
+
"""Remove a configuration value.
|
|
113
|
+
|
|
114
|
+
\b
|
|
115
|
+
Example:
|
|
116
|
+
treadstone config unset api_key
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
import tomllib
|
|
120
|
+
except ModuleNotFoundError:
|
|
121
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
122
|
+
|
|
123
|
+
if not CONFIG_FILE.exists():
|
|
124
|
+
click.echo(f"{key} is not set.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
128
|
+
raw: dict[str, dict[str, str]] = tomllib.load(f) # type: ignore[assignment]
|
|
129
|
+
|
|
130
|
+
section = raw.get("default", {})
|
|
131
|
+
if key not in section:
|
|
132
|
+
click.echo(f"{key} is not set.")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
del section[key]
|
|
136
|
+
_write_config(raw)
|
|
137
|
+
click.echo(f"Unset {key}.")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@config.command("path")
|
|
141
|
+
def show_path() -> None:
|
|
142
|
+
"""Print the path to the configuration file.
|
|
143
|
+
|
|
144
|
+
\b
|
|
145
|
+
Example:
|
|
146
|
+
treadstone config path
|
|
147
|
+
"""
|
|
148
|
+
exists = CONFIG_FILE.exists()
|
|
149
|
+
click.echo(str(CONFIG_FILE))
|
|
150
|
+
if not exists:
|
|
151
|
+
click.echo(" (file does not exist yet)")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _mask_secret(key: str, value: str) -> str:
|
|
155
|
+
if key == "api_key" and len(value) > 8:
|
|
156
|
+
return value[:8] + "..." + value[-4:]
|
|
157
|
+
return value
|
treadstone_cli/main.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Treadstone CLI — agent-native sandbox management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from treadstone_cli._client import build_client, effective_api_key, effective_base_url, get_base_url
|
|
8
|
+
from treadstone_cli._output import friendly_exception_handler, handle_error, is_json_mode, print_json
|
|
9
|
+
|
|
10
|
+
_STATIC_EPILOG = """\b
|
|
11
|
+
Configuration (highest to lowest priority):
|
|
12
|
+
CLI flags --api-key, --base-url
|
|
13
|
+
Env vars TREADSTONE_API_KEY, TREADSTONE_BASE_URL
|
|
14
|
+
Config file ~/.config/treadstone/config.toml
|
|
15
|
+
|
|
16
|
+
Run 'treadstone config --help' to manage the config file.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
treadstone health Check server status
|
|
20
|
+
treadstone sandboxes list List running sandboxes
|
|
21
|
+
treadstone sb create --template default Create a sandbox
|
|
22
|
+
treadstone auth login Log in with email/password
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _TreadstoneGroup(click.Group):
|
|
27
|
+
"""Custom group that appends a live config summary to the help output."""
|
|
28
|
+
|
|
29
|
+
def format_epilog(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
30
|
+
super().format_epilog(ctx, formatter)
|
|
31
|
+
url, source = effective_base_url()
|
|
32
|
+
api_key = effective_api_key()
|
|
33
|
+
api_key_status = "configured" if api_key else "not set"
|
|
34
|
+
with formatter.section("Active configuration"):
|
|
35
|
+
formatter.write_dl(
|
|
36
|
+
[
|
|
37
|
+
("Base URL", f"{url} [{source}]"),
|
|
38
|
+
("API key", api_key_status),
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group(cls=_TreadstoneGroup, epilog=_STATIC_EPILOG)
|
|
44
|
+
@click.option("--json", "json_output", is_flag=True, default=False, help="Output in JSON format.")
|
|
45
|
+
@click.option(
|
|
46
|
+
"--api-key",
|
|
47
|
+
envvar="TREADSTONE_API_KEY",
|
|
48
|
+
default=None,
|
|
49
|
+
help="API key for authentication (env: TREADSTONE_API_KEY).",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--base-url",
|
|
53
|
+
envvar="TREADSTONE_BASE_URL",
|
|
54
|
+
default=None,
|
|
55
|
+
help="Base URL of the Treadstone server (env: TREADSTONE_BASE_URL).",
|
|
56
|
+
)
|
|
57
|
+
@click.version_option(package_name="treadstone-cli")
|
|
58
|
+
@click.pass_context
|
|
59
|
+
def cli(ctx: click.Context, json_output: bool, api_key: str | None, base_url: str | None) -> None:
|
|
60
|
+
"""Treadstone CLI — manage sandboxes, templates, and API keys.
|
|
61
|
+
|
|
62
|
+
An agent-native sandbox service. Run code, build projects, and deploy
|
|
63
|
+
environments via the command line.
|
|
64
|
+
"""
|
|
65
|
+
ctx.ensure_object(dict)
|
|
66
|
+
ctx.obj["json_output"] = json_output
|
|
67
|
+
if api_key:
|
|
68
|
+
ctx.obj["api_key"] = api_key
|
|
69
|
+
if base_url:
|
|
70
|
+
ctx.obj["base_url"] = base_url
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@cli.command()
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def health(ctx: click.Context) -> None:
|
|
76
|
+
"""Check if the Treadstone server is reachable and healthy."""
|
|
77
|
+
client = build_client(ctx)
|
|
78
|
+
base_url = get_base_url(ctx)
|
|
79
|
+
if not is_json_mode(ctx):
|
|
80
|
+
click.echo(f"Connecting to {base_url} ...")
|
|
81
|
+
resp = client.get("/health")
|
|
82
|
+
handle_error(resp)
|
|
83
|
+
data = resp.json()
|
|
84
|
+
if is_json_mode(ctx):
|
|
85
|
+
print_json(data)
|
|
86
|
+
else:
|
|
87
|
+
status = data.get("status", "unknown")
|
|
88
|
+
click.echo(f"Server is {status}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
from treadstone_cli.api_keys import api_keys # noqa: E402
|
|
92
|
+
from treadstone_cli.auth import auth # noqa: E402
|
|
93
|
+
from treadstone_cli.config_cmd import config # noqa: E402
|
|
94
|
+
from treadstone_cli.sandboxes import sandboxes # noqa: E402
|
|
95
|
+
from treadstone_cli.templates import templates # noqa: E402
|
|
96
|
+
|
|
97
|
+
cli.add_command(auth)
|
|
98
|
+
cli.add_command(api_keys, "api-keys")
|
|
99
|
+
cli.add_command(sandboxes)
|
|
100
|
+
cli.add_command(sandboxes, "sb")
|
|
101
|
+
cli.add_command(templates)
|
|
102
|
+
cli.add_command(config)
|
|
103
|
+
|
|
104
|
+
friendly_exception_handler(cli)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Sandbox management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from treadstone_cli._client import require_auth
|
|
8
|
+
from treadstone_cli._output import handle_error, is_json_mode, print_detail, print_json, print_table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def sandboxes() -> None:
|
|
13
|
+
"""Manage sandboxes.
|
|
14
|
+
|
|
15
|
+
Create, list, start, stop, and delete sandboxes. Use 'sb' as a shorthand.
|
|
16
|
+
|
|
17
|
+
\b
|
|
18
|
+
Examples:
|
|
19
|
+
treadstone sandboxes create --template default --name my-box
|
|
20
|
+
treadstone sb list
|
|
21
|
+
treadstone sb get <sandbox-id>
|
|
22
|
+
treadstone sb delete <sandbox-id>
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@sandboxes.command("create")
|
|
27
|
+
@click.option("--template", required=True, help="Sandbox template name.")
|
|
28
|
+
@click.option("--name", default=None, help="Sandbox name (auto-generated if omitted).")
|
|
29
|
+
@click.option("--label", multiple=True, help="Labels in key:val format (repeatable).")
|
|
30
|
+
@click.option("--persist", is_flag=True, default=False, help="Enable persistent storage.")
|
|
31
|
+
@click.option("--storage-size", default="10Gi", help="PVC size when --persist is set.")
|
|
32
|
+
@click.pass_context
|
|
33
|
+
def create(
|
|
34
|
+
ctx: click.Context,
|
|
35
|
+
template: str,
|
|
36
|
+
name: str | None,
|
|
37
|
+
label: tuple[str, ...],
|
|
38
|
+
persist: bool,
|
|
39
|
+
storage_size: str,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Create a new sandbox from a template.
|
|
42
|
+
|
|
43
|
+
\b
|
|
44
|
+
Examples:
|
|
45
|
+
treadstone sb create --template default
|
|
46
|
+
treadstone sb create --template python --name dev-box --label env:dev
|
|
47
|
+
treadstone sb create --template node --persist --storage-size 20Gi
|
|
48
|
+
"""
|
|
49
|
+
client = require_auth(ctx)
|
|
50
|
+
labels = {}
|
|
51
|
+
for lbl in label:
|
|
52
|
+
if ":" not in lbl:
|
|
53
|
+
click.echo(f"Error: Invalid label format '{lbl}'. Use key:val.", err=True)
|
|
54
|
+
raise SystemExit(1)
|
|
55
|
+
k, v = lbl.split(":", 1)
|
|
56
|
+
labels[k] = v
|
|
57
|
+
body: dict = {"template": template, "labels": labels, "persist": persist, "storage_size": storage_size}
|
|
58
|
+
if name:
|
|
59
|
+
body["name"] = name
|
|
60
|
+
resp = client.post("/v1/sandboxes", json=body)
|
|
61
|
+
handle_error(resp)
|
|
62
|
+
data = resp.json()
|
|
63
|
+
if is_json_mode(ctx):
|
|
64
|
+
print_json(data)
|
|
65
|
+
else:
|
|
66
|
+
print_detail(data, title="Sandbox Created")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@sandboxes.command("list")
|
|
70
|
+
@click.option("--label", default=None, help="Filter by label (key:val).")
|
|
71
|
+
@click.option("--limit", default=100, type=int, help="Max results.")
|
|
72
|
+
@click.option("--offset", default=0, type=int, help="Skip N results.")
|
|
73
|
+
@click.pass_context
|
|
74
|
+
def list_sandboxes(ctx: click.Context, label: str | None, limit: int, offset: int) -> None:
|
|
75
|
+
"""List sandboxes with optional filtering.
|
|
76
|
+
|
|
77
|
+
\b
|
|
78
|
+
Examples:
|
|
79
|
+
treadstone sb list
|
|
80
|
+
treadstone sb list --label env:prod --limit 10
|
|
81
|
+
"""
|
|
82
|
+
client = require_auth(ctx)
|
|
83
|
+
params: dict = {"limit": limit, "offset": offset}
|
|
84
|
+
if label:
|
|
85
|
+
params["label"] = label
|
|
86
|
+
resp = client.get("/v1/sandboxes", params=params)
|
|
87
|
+
handle_error(resp)
|
|
88
|
+
data = resp.json()
|
|
89
|
+
if is_json_mode(ctx):
|
|
90
|
+
print_json(data)
|
|
91
|
+
else:
|
|
92
|
+
items = data.get("items", [])
|
|
93
|
+
rows = [[s["id"], s["name"], s["template"], s["status"], s.get("created_at", "")] for s in items]
|
|
94
|
+
print_table(["ID", "Name", "Template", "Status", "Created"], rows, title=f"Sandboxes ({data['total']} total)")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@sandboxes.command("get")
|
|
98
|
+
@click.argument("sandbox_id")
|
|
99
|
+
@click.pass_context
|
|
100
|
+
def get_sandbox(ctx: click.Context, sandbox_id: str) -> None:
|
|
101
|
+
"""Show detailed information about a sandbox."""
|
|
102
|
+
client = require_auth(ctx)
|
|
103
|
+
resp = client.get(f"/v1/sandboxes/{sandbox_id}")
|
|
104
|
+
handle_error(resp)
|
|
105
|
+
data = resp.json()
|
|
106
|
+
if is_json_mode(ctx):
|
|
107
|
+
print_json(data)
|
|
108
|
+
else:
|
|
109
|
+
print_detail(data, title=f"Sandbox {sandbox_id}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@sandboxes.command("delete")
|
|
113
|
+
@click.argument("sandbox_id")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def delete_sandbox(ctx: click.Context, sandbox_id: str) -> None:
|
|
116
|
+
"""Delete a sandbox."""
|
|
117
|
+
client = require_auth(ctx)
|
|
118
|
+
resp = client.delete(f"/v1/sandboxes/{sandbox_id}")
|
|
119
|
+
handle_error(resp)
|
|
120
|
+
click.echo(f"Sandbox {sandbox_id} deleted.")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@sandboxes.command("start")
|
|
124
|
+
@click.argument("sandbox_id")
|
|
125
|
+
@click.pass_context
|
|
126
|
+
def start_sandbox(ctx: click.Context, sandbox_id: str) -> None:
|
|
127
|
+
"""Start a stopped sandbox."""
|
|
128
|
+
client = require_auth(ctx)
|
|
129
|
+
resp = client.post(f"/v1/sandboxes/{sandbox_id}/start")
|
|
130
|
+
handle_error(resp)
|
|
131
|
+
data = resp.json()
|
|
132
|
+
if is_json_mode(ctx):
|
|
133
|
+
print_json(data)
|
|
134
|
+
else:
|
|
135
|
+
click.echo(f"Sandbox {sandbox_id} starting.")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@sandboxes.command("stop")
|
|
139
|
+
@click.argument("sandbox_id")
|
|
140
|
+
@click.pass_context
|
|
141
|
+
def stop_sandbox(ctx: click.Context, sandbox_id: str) -> None:
|
|
142
|
+
"""Stop a running sandbox."""
|
|
143
|
+
client = require_auth(ctx)
|
|
144
|
+
resp = client.post(f"/v1/sandboxes/{sandbox_id}/stop")
|
|
145
|
+
handle_error(resp)
|
|
146
|
+
data = resp.json()
|
|
147
|
+
if is_json_mode(ctx):
|
|
148
|
+
print_json(data)
|
|
149
|
+
else:
|
|
150
|
+
click.echo(f"Sandbox {sandbox_id} stopping.")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@sandboxes.command("token")
|
|
154
|
+
@click.argument("sandbox_id")
|
|
155
|
+
@click.option("--expires-in", default=3600, type=int, help="Token lifetime in seconds.")
|
|
156
|
+
@click.pass_context
|
|
157
|
+
def create_token(ctx: click.Context, sandbox_id: str, expires_in: int) -> None:
|
|
158
|
+
"""Create a short-lived access token for a sandbox.
|
|
159
|
+
|
|
160
|
+
The token can be used to connect to the sandbox directly (e.g. via
|
|
161
|
+
WebSocket) without needing the main API key.
|
|
162
|
+
"""
|
|
163
|
+
client = require_auth(ctx)
|
|
164
|
+
resp = client.post(f"/v1/sandboxes/{sandbox_id}/token", json={"expires_in": expires_in})
|
|
165
|
+
handle_error(resp)
|
|
166
|
+
data = resp.json()
|
|
167
|
+
if is_json_mode(ctx):
|
|
168
|
+
print_json(data)
|
|
169
|
+
else:
|
|
170
|
+
print_detail(data, title="Sandbox Token")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Sandbox template commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from treadstone_cli._client import require_auth
|
|
8
|
+
from treadstone_cli._output import handle_error, is_json_mode, print_json, print_table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def templates() -> None:
|
|
13
|
+
"""Manage sandbox templates.
|
|
14
|
+
|
|
15
|
+
Templates define the runtime environment (image, CPU, memory) for sandboxes.
|
|
16
|
+
|
|
17
|
+
\b
|
|
18
|
+
Examples:
|
|
19
|
+
treadstone templates list
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@templates.command("list")
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def list_templates(ctx: click.Context) -> None:
|
|
26
|
+
"""List all available sandbox templates with resource specs."""
|
|
27
|
+
client = require_auth(ctx)
|
|
28
|
+
resp = client.get("/v1/sandbox-templates")
|
|
29
|
+
handle_error(resp)
|
|
30
|
+
data = resp.json()
|
|
31
|
+
if is_json_mode(ctx):
|
|
32
|
+
print_json(data)
|
|
33
|
+
else:
|
|
34
|
+
items = data.get("items", [])
|
|
35
|
+
rows = [
|
|
36
|
+
[t["name"], t["display_name"], t["resource_spec"]["cpu"], t["resource_spec"]["memory"], t["description"]]
|
|
37
|
+
for t in items
|
|
38
|
+
]
|
|
39
|
+
print_table(["Name", "Display Name", "CPU", "Memory", "Description"], rows, title="Sandbox Templates")
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: treadstone-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: CLI for the Treadstone sandbox service.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: click>=8.1
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: rich>=14.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Treadstone CLI
|
|
12
|
+
|
|
13
|
+
Command-line interface for the Treadstone sandbox service.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### From PyPI
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install treadstone
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Pre-built binary
|
|
24
|
+
|
|
25
|
+
Download the latest release from [GitHub Releases](https://github.com/earayu/treadstone/releases). The binary is self-contained and does not require Python.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# macOS / Linux
|
|
29
|
+
chmod +x treadstone
|
|
30
|
+
sudo mv treadstone /usr/local/bin/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Check that the server is reachable
|
|
37
|
+
treadstone health
|
|
38
|
+
|
|
39
|
+
# Register an account (first time only)
|
|
40
|
+
treadstone auth register
|
|
41
|
+
|
|
42
|
+
# Log in
|
|
43
|
+
treadstone auth login
|
|
44
|
+
|
|
45
|
+
# Create an API key for non-interactive use
|
|
46
|
+
treadstone api-keys create --name my-key
|
|
47
|
+
# Save the key — it is shown only once
|
|
48
|
+
|
|
49
|
+
# Store the key in config so you don't have to pass it every time
|
|
50
|
+
treadstone config set api_key ts_live_xxxxxxxxxxxx
|
|
51
|
+
|
|
52
|
+
# Create a sandbox
|
|
53
|
+
treadstone sandboxes create --template default --name my-sandbox
|
|
54
|
+
|
|
55
|
+
# List sandboxes
|
|
56
|
+
treadstone sb list
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
The CLI reads configuration from three sources, in order of priority:
|
|
62
|
+
|
|
63
|
+
| Priority | Source | Example |
|
|
64
|
+
|----------|--------|---------|
|
|
65
|
+
| 1 (highest) | CLI flags | `--base-url https://... --api-key ts_...` |
|
|
66
|
+
| 2 | Environment variables | `TREADSTONE_BASE_URL`, `TREADSTONE_API_KEY` |
|
|
67
|
+
| 3 (lowest) | Config file | `~/.config/treadstone/config.toml` |
|
|
68
|
+
|
|
69
|
+
### Config file
|
|
70
|
+
|
|
71
|
+
Location: `~/.config/treadstone/config.toml`
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
[default]
|
|
75
|
+
base_url = "https://your-server.example.com"
|
|
76
|
+
api_key = "ts_live_xxxxxxxxxxxx"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can manage this file with the `config` subcommand:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Set a value
|
|
83
|
+
treadstone config set base_url https://your-server.example.com
|
|
84
|
+
treadstone config set api_key ts_live_xxxxxxxxxxxx
|
|
85
|
+
|
|
86
|
+
# View current settings (api_key is partially masked)
|
|
87
|
+
treadstone config get
|
|
88
|
+
|
|
89
|
+
# View a single key
|
|
90
|
+
treadstone config get base_url
|
|
91
|
+
|
|
92
|
+
# Remove a value
|
|
93
|
+
treadstone config unset api_key
|
|
94
|
+
|
|
95
|
+
# Show config file path
|
|
96
|
+
treadstone config path
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Environment variables
|
|
100
|
+
|
|
101
|
+
| Variable | Description | Default |
|
|
102
|
+
|----------|-------------|---------|
|
|
103
|
+
| `TREADSTONE_BASE_URL` | Base URL of the Treadstone server | `https://api.treadstone-ai.dev` |
|
|
104
|
+
| `TREADSTONE_API_KEY` | API key for authentication | (none) |
|
|
105
|
+
|
|
106
|
+
### Global flags
|
|
107
|
+
|
|
108
|
+
| Flag | Description |
|
|
109
|
+
|------|-------------|
|
|
110
|
+
| `--base-url URL` | Override the server URL |
|
|
111
|
+
| `--api-key KEY` | Override the API key |
|
|
112
|
+
| `--json` | Output responses as JSON (useful for scripting) |
|
|
113
|
+
| `--version` | Show CLI version |
|
|
114
|
+
| `--help` | Show help message |
|
|
115
|
+
|
|
116
|
+
## Commands
|
|
117
|
+
|
|
118
|
+
### `health`
|
|
119
|
+
|
|
120
|
+
Check if the server is reachable and healthy.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
treadstone health
|
|
124
|
+
# Server is healthy
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `auth`
|
|
128
|
+
|
|
129
|
+
Authentication and user management.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
treadstone auth register # Create a new account
|
|
133
|
+
treadstone auth login # Log in interactively
|
|
134
|
+
treadstone auth logout # Log out
|
|
135
|
+
treadstone auth whoami # Show current user info
|
|
136
|
+
treadstone auth change-password # Change your password
|
|
137
|
+
treadstone auth invite --email user@example.com # Invite a user (admin)
|
|
138
|
+
treadstone auth users # List all users (admin)
|
|
139
|
+
treadstone auth delete-user <user-id> # Delete a user (admin)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `sandboxes` (alias: `sb`)
|
|
143
|
+
|
|
144
|
+
Create and manage sandboxes.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
treadstone sb create --template default --name my-box
|
|
148
|
+
treadstone sb create --template python --label env:dev --persist
|
|
149
|
+
treadstone sb list
|
|
150
|
+
treadstone sb list --label env:prod --limit 10
|
|
151
|
+
treadstone sb get <sandbox-id>
|
|
152
|
+
treadstone sb start <sandbox-id>
|
|
153
|
+
treadstone sb stop <sandbox-id>
|
|
154
|
+
treadstone sb delete <sandbox-id>
|
|
155
|
+
treadstone sb token <sandbox-id> # Get an access token
|
|
156
|
+
treadstone sb token <sandbox-id> --expires-in 7200 # Custom TTL
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `templates`
|
|
160
|
+
|
|
161
|
+
List available sandbox templates.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
treadstone templates list
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### `api-keys`
|
|
168
|
+
|
|
169
|
+
Manage long-lived API keys.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
treadstone api-keys create --name ci-bot
|
|
173
|
+
treadstone api-keys create --name temp --expires-in 86400 # 24h
|
|
174
|
+
treadstone api-keys list
|
|
175
|
+
treadstone api-keys delete <key-id>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `config`
|
|
179
|
+
|
|
180
|
+
Manage local CLI configuration.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
treadstone config set base_url https://...
|
|
184
|
+
treadstone config set api_key ts_...
|
|
185
|
+
treadstone config get
|
|
186
|
+
treadstone config get base_url
|
|
187
|
+
treadstone config unset api_key
|
|
188
|
+
treadstone config path
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## JSON output
|
|
192
|
+
|
|
193
|
+
Pass `--json` before any command to get machine-readable JSON output:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
treadstone --json sb list
|
|
197
|
+
treadstone --json health
|
|
198
|
+
treadstone --json api-keys list
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Error handling
|
|
202
|
+
|
|
203
|
+
The CLI displays user-friendly error messages instead of stack traces:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
Error: Connection refused.
|
|
207
|
+
Possible causes:
|
|
208
|
+
- The Treadstone server is not running
|
|
209
|
+
- The --base-url or TREADSTONE_BASE_URL is incorrect
|
|
210
|
+
- A firewall or proxy is blocking the connection
|
|
211
|
+
Detail: ...
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Common errors:
|
|
215
|
+
|
|
216
|
+
| Error | Cause | Fix |
|
|
217
|
+
|-------|-------|-----|
|
|
218
|
+
| Connection refused | Server not running or wrong URL | Check `treadstone config get base_url` |
|
|
219
|
+
| Request timed out | Server slow or unreachable | Retry, or check network |
|
|
220
|
+
| No API key configured | Missing authentication | `treadstone config set api_key <key>` |
|
|
221
|
+
| HTTP 401 | Invalid or expired API key | Create a new key with `treadstone api-keys create` |
|
|
222
|
+
| HTTP 404 | Resource not found | Verify the ID is correct |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
treadstone_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
treadstone_cli/__main__.py,sha256=eR-fq48-urz4L6AhswpU2pJf7ZaQ2rtcH6XZF0s3_Fs,148
|
|
3
|
+
treadstone_cli/_client.py,sha256=90xjZNjpWavW0qV2Y-WIbUrDnAnQR8sCt5LYPNi3mAY,2228
|
|
4
|
+
treadstone_cli/_output.py,sha256=qfjz_7kMO6pJFuiftObgvxDGLvmv_uB9cqchk439PCE,3623
|
|
5
|
+
treadstone_cli/api_keys.py,sha256=Otx-n5P3rTX6KxREphkuiGg6EUff_UvT7j3OluTm6y8,2285
|
|
6
|
+
treadstone_cli/auth.py,sha256=cDnVBnBs_LW-MpB_G2ahlLk6nW_LN4JweAjq9xOxv5E,5349
|
|
7
|
+
treadstone_cli/config_cmd.py,sha256=AlGxldd4GxahsbdOTIPSZ0Lq9JXfg3ZVWBqQAYtxbJ0,4421
|
|
8
|
+
treadstone_cli/main.py,sha256=Wj6ILbjRuGLsgURnQhTty1r1y10aCweT64CTbBFkveI,3538
|
|
9
|
+
treadstone_cli/sandboxes.py,sha256=-hSWyfvP-xiymIDCxPdUnYPeaDtIEhnMZu1wWEiPQHk,5529
|
|
10
|
+
treadstone_cli/templates.py,sha256=Q1OMc2rAIo802Pt5eFrgIEEp20mVUwCmeCQQwyySBG0,1109
|
|
11
|
+
treadstone_cli-0.2.1.dist-info/METADATA,sha256=kugHiZgcVi2qGcZShmMxvHP4Ps2Ozzx1w-C1faeEKaI,5491
|
|
12
|
+
treadstone_cli-0.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
+
treadstone_cli-0.2.1.dist-info/entry_points.txt,sha256=AEweiekAwgT6jzNSIVdmYk714t6zO3xMIHv4Hhh2vII,55
|
|
14
|
+
treadstone_cli-0.2.1.dist-info/RECORD,,
|