powerbase-cli 0.1.0__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.
- powerbase_cli/__init__.py +5 -0
- powerbase_cli/__main__.py +5 -0
- powerbase_cli/api.py +348 -0
- powerbase_cli/certs/powerbase-test-ca.pem +21 -0
- powerbase_cli/cli.py +7 -0
- powerbase_cli/commands/__init__.py +3 -0
- powerbase_cli/commands/agent.py +192 -0
- powerbase_cli/commands/auth.py +169 -0
- powerbase_cli/commands/branch.py +86 -0
- powerbase_cli/commands/config_cmd.py +73 -0
- powerbase_cli/commands/context.py +84 -0
- powerbase_cli/commands/database.py +129 -0
- powerbase_cli/commands/instance.py +81 -0
- powerbase_cli/commands/org.py +18 -0
- powerbase_cli/commands/parser.py +114 -0
- powerbase_cli/commands/publish.py +44 -0
- powerbase_cli/commands/sandbox.py +90 -0
- powerbase_cli/commands/shared.py +152 -0
- powerbase_cli/commands/sql.py +32 -0
- powerbase_cli/config.py +239 -0
- powerbase_cli/session.py +141 -0
- powerbase_cli/transport.py +130 -0
- powerbase_cli-0.1.0.dist-info/METADATA +318 -0
- powerbase_cli-0.1.0.dist-info/RECORD +27 -0
- powerbase_cli-0.1.0.dist-info/WHEEL +5 -0
- powerbase_cli-0.1.0.dist-info/entry_points.txt +2 -0
- powerbase_cli-0.1.0.dist-info/top_level.txt +1 -0
powerbase_cli/api.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Iterable
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
from .transport import PowerbaseTransport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _response_data(response: dict[str, Any]) -> Any:
|
|
13
|
+
if response.get("success") is False:
|
|
14
|
+
raise RuntimeError(response.get("error") or response.get("message") or "Request failed")
|
|
15
|
+
return response.get("data", response)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PowerbaseApi:
|
|
19
|
+
def __init__(self, transport: PowerbaseTransport) -> None:
|
|
20
|
+
self.transport = transport
|
|
21
|
+
|
|
22
|
+
def start_cli_login(self) -> dict[str, Any]:
|
|
23
|
+
return _response_data(
|
|
24
|
+
self.transport.invoke("cli-auth/start", method="POST", body={}, requires_auth=False)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def poll_cli_login(self, login_id: str) -> dict[str, Any]:
|
|
28
|
+
return _response_data(self.transport.invoke("cli-auth/poll", method="POST", body={"login_id": login_id}, requires_auth=False))
|
|
29
|
+
|
|
30
|
+
def list_orgs(self) -> Any:
|
|
31
|
+
return _response_data(self.transport.invoke("organizations", method="GET"))
|
|
32
|
+
|
|
33
|
+
def list_databases(self) -> Any:
|
|
34
|
+
return _response_data(self.transport.invoke("databases", method="GET"))
|
|
35
|
+
|
|
36
|
+
def get_database(self, database_id: str) -> Any:
|
|
37
|
+
return _response_data(self.transport.invoke(f"databases/{database_id}", method="GET"))
|
|
38
|
+
|
|
39
|
+
def create_database(
|
|
40
|
+
self,
|
|
41
|
+
name: str,
|
|
42
|
+
dsn: str,
|
|
43
|
+
description: str | None = None,
|
|
44
|
+
db_type: str | None = None,
|
|
45
|
+
) -> Any:
|
|
46
|
+
body: dict[str, Any] = {"name": name, "dsn": dsn}
|
|
47
|
+
if description:
|
|
48
|
+
body["description"] = description
|
|
49
|
+
if db_type:
|
|
50
|
+
body["db_type"] = db_type
|
|
51
|
+
return _response_data(self.transport.invoke("databases", method="POST", body=body))
|
|
52
|
+
|
|
53
|
+
def update_database(
|
|
54
|
+
self,
|
|
55
|
+
database_id: str,
|
|
56
|
+
name: str | None = None,
|
|
57
|
+
dsn: str | None = None,
|
|
58
|
+
description: str | None = None,
|
|
59
|
+
db_type: str | None = None,
|
|
60
|
+
) -> Any:
|
|
61
|
+
body: dict[str, Any] = {}
|
|
62
|
+
if name is not None:
|
|
63
|
+
body["name"] = name
|
|
64
|
+
if dsn is not None:
|
|
65
|
+
body["dsn"] = dsn
|
|
66
|
+
if description is not None:
|
|
67
|
+
body["description"] = description
|
|
68
|
+
if db_type is not None:
|
|
69
|
+
body["db_type"] = db_type
|
|
70
|
+
return _response_data(self.transport.invoke(f"databases/{database_id}", method="PUT", body=body))
|
|
71
|
+
|
|
72
|
+
def delete_database(self, database_id: str) -> Any:
|
|
73
|
+
return _response_data(self.transport.invoke(f"databases/{database_id}", method="DELETE"))
|
|
74
|
+
|
|
75
|
+
def list_instances(self) -> Any:
|
|
76
|
+
return _response_data(self.transport.invoke("instances", method="GET"))
|
|
77
|
+
|
|
78
|
+
def get_instance(self, instance_id: str) -> Any:
|
|
79
|
+
return _response_data(self.transport.invoke(f"instances/{instance_id}", method="GET"))
|
|
80
|
+
|
|
81
|
+
def create_instance(self, name: str | None, database_id: str | None, organization_id: str | None) -> Any:
|
|
82
|
+
body: dict[str, Any] = {}
|
|
83
|
+
if name:
|
|
84
|
+
body["name"] = name
|
|
85
|
+
if database_id:
|
|
86
|
+
body["database_id"] = database_id
|
|
87
|
+
if organization_id:
|
|
88
|
+
body["organization_id"] = organization_id
|
|
89
|
+
return _response_data(self.transport.invoke("instances", method="POST", body=body))
|
|
90
|
+
|
|
91
|
+
def delete_instance(self, instance_id: str) -> Any:
|
|
92
|
+
return _response_data(self.transport.invoke(f"instances/{instance_id}", method="DELETE"))
|
|
93
|
+
|
|
94
|
+
def list_branches(self, instance_id: str) -> Any:
|
|
95
|
+
return _response_data(self.transport.invoke(f"instances/{instance_id}/branches", method="GET"))
|
|
96
|
+
|
|
97
|
+
def create_branch(self, instance_id: str, branch_name: str, source_branch_slug: str | None) -> Any:
|
|
98
|
+
body = {
|
|
99
|
+
"branch_name": branch_name,
|
|
100
|
+
"source_branch_slug": source_branch_slug or "main",
|
|
101
|
+
}
|
|
102
|
+
return _response_data(self.transport.invoke(f"instances/{instance_id}/branches", method="POST", body=body))
|
|
103
|
+
|
|
104
|
+
def delete_branch(self, instance_id: str, branch_slug: str) -> Any:
|
|
105
|
+
return _response_data(self.transport.invoke(f"instances/{instance_id}/branches/{branch_slug}", method="DELETE"))
|
|
106
|
+
|
|
107
|
+
def switch_branch(self, instance_id: str, branch_name: str) -> Any:
|
|
108
|
+
return self.transport.invoke(
|
|
109
|
+
"sandbox-control/switch-branch",
|
|
110
|
+
method="POST",
|
|
111
|
+
body={"instance_id": instance_id, "branch_name": branch_name},
|
|
112
|
+
instance_id=instance_id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def run_sql(self, instance_id: str, sql: str, branch: str | None) -> Any:
|
|
116
|
+
return self.transport.invoke(
|
|
117
|
+
"sql-execute",
|
|
118
|
+
method="POST",
|
|
119
|
+
body={"instance_id": instance_id, "sql": sql, "branch": branch or "main"},
|
|
120
|
+
instance_id=instance_id,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def publish_diff(self, instance_id: str, target: str = "prod") -> Any:
|
|
124
|
+
return self.transport.invoke(
|
|
125
|
+
f"sandbox-control/promote/diff?target={target}",
|
|
126
|
+
method="POST",
|
|
127
|
+
body={"instance_id": instance_id},
|
|
128
|
+
instance_id=instance_id,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def publish_run(self, instance_id: str, sql: str, target: str = "prod") -> Iterable[dict[str, Any]]:
|
|
132
|
+
resp = self.transport.invoke(
|
|
133
|
+
f"sandbox-control/promote/execute?target={target}&stream=1",
|
|
134
|
+
method="POST",
|
|
135
|
+
body={"instance_id": instance_id, "sql": sql},
|
|
136
|
+
headers={"Accept": "text/event-stream"},
|
|
137
|
+
instance_id=instance_id,
|
|
138
|
+
stream=True,
|
|
139
|
+
)
|
|
140
|
+
buffer = ""
|
|
141
|
+
while True:
|
|
142
|
+
chunk = resp.readline()
|
|
143
|
+
if not chunk:
|
|
144
|
+
break
|
|
145
|
+
line = chunk.decode("utf-8")
|
|
146
|
+
if line == "\n":
|
|
147
|
+
buffer = ""
|
|
148
|
+
continue
|
|
149
|
+
if line.startswith("data:"):
|
|
150
|
+
payload = line[5:].strip()
|
|
151
|
+
if payload:
|
|
152
|
+
yield json.loads(payload)
|
|
153
|
+
|
|
154
|
+
def sandbox_files_list(self, instance_id: str, path: str = "/", include_hidden: bool = False) -> Any:
|
|
155
|
+
params = {
|
|
156
|
+
"path": path,
|
|
157
|
+
"instance_id": instance_id,
|
|
158
|
+
}
|
|
159
|
+
if include_hidden:
|
|
160
|
+
params["includeHidden"] = "true"
|
|
161
|
+
suffix = urlencode(params)
|
|
162
|
+
return self.transport.invoke(f"sandbox-files/list?{suffix}", instance_id=instance_id)
|
|
163
|
+
|
|
164
|
+
def sandbox_files_tree(self, instance_id: str, root: str = "/", max_depth: int = 3) -> Any:
|
|
165
|
+
suffix = urlencode({"root": root, "maxDepth": max_depth, "instance_id": instance_id})
|
|
166
|
+
return self.transport.invoke(
|
|
167
|
+
f"sandbox-files/tree?{suffix}",
|
|
168
|
+
instance_id=instance_id,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def sandbox_files_read(self, instance_id: str, path: str) -> Any:
|
|
172
|
+
suffix = urlencode({"path": path, "instance_id": instance_id})
|
|
173
|
+
return self.transport.invoke(
|
|
174
|
+
f"sandbox-files/content?{suffix}",
|
|
175
|
+
instance_id=instance_id,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def sandbox_files_create_file(self, instance_id: str, path: str, content: str) -> Any:
|
|
179
|
+
return self.transport.invoke(
|
|
180
|
+
"sandbox-files/create",
|
|
181
|
+
method="POST",
|
|
182
|
+
body={"type": "file", "path": path, "content": content, "instance_id": instance_id},
|
|
183
|
+
instance_id=instance_id,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def sandbox_files_create_folder(self, instance_id: str, path: str) -> Any:
|
|
187
|
+
return self.transport.invoke(
|
|
188
|
+
"sandbox-files/create",
|
|
189
|
+
method="POST",
|
|
190
|
+
body={"type": "folder", "path": path, "instance_id": instance_id},
|
|
191
|
+
instance_id=instance_id,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def sandbox_files_upload(self, instance_id: str, source_path: str, target_path: str) -> Any:
|
|
195
|
+
file_bytes = Path(source_path).read_bytes()
|
|
196
|
+
encoded = base64.b64encode(file_bytes).decode("ascii")
|
|
197
|
+
return self.transport.invoke(
|
|
198
|
+
"sandbox-files/upload",
|
|
199
|
+
method="POST",
|
|
200
|
+
body={
|
|
201
|
+
"fileName": Path(source_path).name,
|
|
202
|
+
"fileContent": encoded,
|
|
203
|
+
"path": target_path,
|
|
204
|
+
"instance_id": instance_id,
|
|
205
|
+
},
|
|
206
|
+
instance_id=instance_id,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def sandbox_files_delete(self, instance_id: str, path: str) -> Any:
|
|
210
|
+
return self.transport.invoke(
|
|
211
|
+
"sandbox-files/delete",
|
|
212
|
+
method="POST",
|
|
213
|
+
body={"path": path, "instance_id": instance_id},
|
|
214
|
+
instance_id=instance_id,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def agent_providers(self, instance_id: str | None) -> Any:
|
|
218
|
+
return self.transport.invoke(
|
|
219
|
+
"sandbox-agent/providers",
|
|
220
|
+
instance_id=instance_id,
|
|
221
|
+
requires_auth=bool(instance_id),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def agent_status(self, instance_id: str, provider: str) -> Any:
|
|
225
|
+
return self.transport.invoke(
|
|
226
|
+
f"sandbox-agent/status?provider={provider}",
|
|
227
|
+
instance_id=instance_id,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def agent_login_url(self, instance_id: str, provider: str) -> Any:
|
|
231
|
+
return self.transport.invoke(
|
|
232
|
+
f"sandbox-agent/login-url?provider={provider}",
|
|
233
|
+
method="POST",
|
|
234
|
+
body={},
|
|
235
|
+
instance_id=instance_id,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def agent_opencode_config_get(self, instance_id: str) -> Any:
|
|
239
|
+
return self.transport.invoke(
|
|
240
|
+
"sandbox-agent/opencode-config",
|
|
241
|
+
method="GET",
|
|
242
|
+
instance_id=instance_id,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def agent_opencode_config_set(
|
|
246
|
+
self,
|
|
247
|
+
instance_id: str,
|
|
248
|
+
provider: str,
|
|
249
|
+
api_key: str,
|
|
250
|
+
models: list[dict[str, Any]] | None = None,
|
|
251
|
+
) -> Any:
|
|
252
|
+
body: dict[str, Any] = {"provider": provider, "apiKey": api_key}
|
|
253
|
+
if models:
|
|
254
|
+
body["models"] = models
|
|
255
|
+
return self.transport.invoke(
|
|
256
|
+
"sandbox-agent/opencode-config",
|
|
257
|
+
method="POST",
|
|
258
|
+
body=body,
|
|
259
|
+
instance_id=instance_id,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def agent_opencode_config_delete(self, instance_id: str, provider: str) -> Any:
|
|
263
|
+
suffix = urlencode({"provider": provider})
|
|
264
|
+
return self.transport.invoke(
|
|
265
|
+
f"sandbox-agent/opencode-config?{suffix}",
|
|
266
|
+
method="DELETE",
|
|
267
|
+
instance_id=instance_id,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def agent_custom_provider_create(
|
|
271
|
+
self,
|
|
272
|
+
instance_id: str,
|
|
273
|
+
provider_id: str,
|
|
274
|
+
name: str | None,
|
|
275
|
+
base_url: str,
|
|
276
|
+
api_key: str,
|
|
277
|
+
models: list[dict[str, Any]],
|
|
278
|
+
) -> Any:
|
|
279
|
+
body: dict[str, Any] = {
|
|
280
|
+
"id": provider_id,
|
|
281
|
+
"baseURL": base_url,
|
|
282
|
+
"apiKey": api_key,
|
|
283
|
+
"models": models,
|
|
284
|
+
}
|
|
285
|
+
if name:
|
|
286
|
+
body["name"] = name
|
|
287
|
+
return self.transport.invoke(
|
|
288
|
+
"sandbox-agent/opencode-custom-provider",
|
|
289
|
+
method="POST",
|
|
290
|
+
body=body,
|
|
291
|
+
instance_id=instance_id,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def agent_custom_provider_delete(self, instance_id: str, provider_id: str) -> Any:
|
|
295
|
+
suffix = urlencode({"id": provider_id})
|
|
296
|
+
return self.transport.invoke(
|
|
297
|
+
f"sandbox-agent/opencode-custom-provider?{suffix}",
|
|
298
|
+
method="DELETE",
|
|
299
|
+
instance_id=instance_id,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def agent_opencode_json_set(self, instance_id: str, payload: dict[str, Any]) -> Any:
|
|
303
|
+
return self.transport.invoke(
|
|
304
|
+
"sandbox-agent/opencode-json",
|
|
305
|
+
method="POST",
|
|
306
|
+
body=payload,
|
|
307
|
+
instance_id=instance_id,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def agent_chat(
|
|
311
|
+
self,
|
|
312
|
+
instance_id: str,
|
|
313
|
+
message: str,
|
|
314
|
+
*,
|
|
315
|
+
session_id: str | None = None,
|
|
316
|
+
provider: str | None = None,
|
|
317
|
+
model: str | None = None,
|
|
318
|
+
first_user_message: str | None = None,
|
|
319
|
+
agent: str | None = None,
|
|
320
|
+
) -> Iterable[dict[str, Any]]:
|
|
321
|
+
body: dict[str, Any] = {"message": message}
|
|
322
|
+
if session_id:
|
|
323
|
+
body["session_id"] = session_id
|
|
324
|
+
if provider:
|
|
325
|
+
body["provider"] = provider
|
|
326
|
+
if model:
|
|
327
|
+
body["model"] = model
|
|
328
|
+
if first_user_message:
|
|
329
|
+
body["first_user_message"] = first_user_message
|
|
330
|
+
if agent:
|
|
331
|
+
body["agent"] = agent
|
|
332
|
+
resp = self.transport.invoke(
|
|
333
|
+
"sandbox-agent/chat",
|
|
334
|
+
method="POST",
|
|
335
|
+
body=body,
|
|
336
|
+
headers={"Accept": "text/event-stream"},
|
|
337
|
+
instance_id=instance_id,
|
|
338
|
+
stream=True,
|
|
339
|
+
)
|
|
340
|
+
while True:
|
|
341
|
+
chunk = resp.readline()
|
|
342
|
+
if not chunk:
|
|
343
|
+
break
|
|
344
|
+
line = chunk.decode("utf-8")
|
|
345
|
+
if line.startswith("data:"):
|
|
346
|
+
payload = line[5:].strip()
|
|
347
|
+
if payload:
|
|
348
|
+
yield json.loads(payload)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDhDCCAmygAwIBAgIUWu1JfeyC2qw56+7PUB5QY9w1MG8wDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwHjEcMBoGA1UEAwwTNi4xMi4yMzUuMTY1Lm5pcC5pbzAeFw0yNjA0MDcwNTUx
|
|
4
|
+
MjZaFw0yNzA0MDcwNTUxMjZaMB4xHDAaBgNVBAMMEzYuMTIuMjM1LjE2NS5uaXAu
|
|
5
|
+
aW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVaa9eQzG9t/WNcoP8
|
|
6
|
+
vJDX41oELoz/LOVzmr9zuFK3jQnIrVjFW8MdBHq1pOBLyHjKtnhKPo1n0br4/iPP
|
|
7
|
+
/TiJNwAuybN6iiSxWSsGpzFv7gIyJivdr65khzeLNda6HyBFxsRsVZAKKe8tueB4
|
|
8
|
+
FQM0YWxHkfAMw5bigDumZq3wzLUuvB7EtscVtl0dV0tcqovLq1Lg9/eBDSqfWuxv
|
|
9
|
+
hW9iROv23ryJETE/8Wh/2Gd5UV0E8CEX8As30+5nJ2jGF6O8abDRipvqc67+5v7h
|
|
10
|
+
dEXT2xIg4Fh8DAhlbgzM6Sj6I70pWiro+Q+N8GWvpJeJYGZu7J97eEOw+u1C2pCV
|
|
11
|
+
sbiBAgMBAAGjgbkwgbYwHQYDVR0OBBYEFODuAm1rmrl2GyZBcClH9HVE9kGMMB8G
|
|
12
|
+
A1UdIwQYMBaAFODuAm1rmrl2GyZBcClH9HVE9kGMMA8GA1UdEwEB/wQFMAMBAf8w
|
|
13
|
+
YwYDVR0RBFwwWoITNi4xMi4yMzUuMTY1Lm5pcC5pb4IbY29uc29sZS42LjEyLjIz
|
|
14
|
+
NS4xNjUubmlwLmlvghUqLjYuMTIuMjM1LjE2NS5uaXAuaW+CCWxvY2FsaG9zdIcE
|
|
15
|
+
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAmvCeJ4Xnx8rXmU+oxiDiDZRfUK204Ta7
|
|
16
|
+
hztXu9LvxheW99p2R1AP2F7VUJOTq31HY9/r2p3qCEyz8F4nc/GPunHfF3y8mMiB
|
|
17
|
+
beFDdwrw8NWTRLGlOJynjsOXtkRkFa03DTKa4x9roZo10S+imDk1/DywWlv7UoAK
|
|
18
|
+
9OsUB1zb8c6m7uZXrOTAZOzvO2fBrdymXnQ4tMvl54iyVT8X1rft5NUywfBzCtzB
|
|
19
|
+
g/OxLM2zJg3kz9kFztRuf1e07Tz6cqSy2CzClvhxaYJSfnlT2b73SceKy3kHn7Sh
|
|
20
|
+
VXA8yjqcTjk6wkMlHueIWnjnjYkRSpFjw3/ODNXbGJjxh+86l/ekPQ==
|
|
21
|
+
-----END CERTIFICATE-----
|
powerbase_cli/cli.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .commands.agent import handle_agent_chat
|
|
2
|
+
from .commands.auth import handle_auth_login
|
|
3
|
+
from .commands.parser import build_parser, main
|
|
4
|
+
from .commands.shared import build_api, resolve_config
|
|
5
|
+
|
|
6
|
+
__all__ = ["build_parser", "main", "build_api", "resolve_config", "handle_auth_login", "handle_agent_chat"]
|
|
7
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from .shared import build_api, load_json_document, load_text, parse_json_specs, render_output, require_instance
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def handle_agent_providers(args: argparse.Namespace) -> int:
|
|
10
|
+
_, _, context, _, api = build_api(args)
|
|
11
|
+
render_output(args, api.agent_providers(args.instance_id or context.instance_id))
|
|
12
|
+
return 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_agent_status(args: argparse.Namespace) -> int:
|
|
16
|
+
_, _, context, _, api = build_api(args)
|
|
17
|
+
render_output(args, api.agent_status(require_instance(args.instance_id, context), args.provider))
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handle_agent_login_url(args: argparse.Namespace) -> int:
|
|
22
|
+
_, _, context, _, api = build_api(args)
|
|
23
|
+
render_output(args, api.agent_login_url(require_instance(args.instance_id, context), args.provider))
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_agent_opencode_config_get(args: argparse.Namespace) -> int:
|
|
28
|
+
_, _, context, _, api = build_api(args)
|
|
29
|
+
render_output(args, api.agent_opencode_config_get(require_instance(args.instance_id, context)))
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_agent_opencode_config_set(args: argparse.Namespace) -> int:
|
|
34
|
+
_, _, context, _, api = build_api(args)
|
|
35
|
+
instance_id = require_instance(args.instance_id, context)
|
|
36
|
+
models = parse_json_specs(args.model_limit, label="--model-limit entry")
|
|
37
|
+
render_output(
|
|
38
|
+
args,
|
|
39
|
+
api.agent_opencode_config_set(instance_id, args.provider, args.api_key, models or None),
|
|
40
|
+
)
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def handle_agent_opencode_config_delete(args: argparse.Namespace) -> int:
|
|
45
|
+
_, _, context, _, api = build_api(args)
|
|
46
|
+
render_output(
|
|
47
|
+
args,
|
|
48
|
+
api.agent_opencode_config_delete(require_instance(args.instance_id, context), args.provider),
|
|
49
|
+
)
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def handle_agent_opencode_json_set(args: argparse.Namespace) -> int:
|
|
54
|
+
_, _, context, _, api = build_api(args)
|
|
55
|
+
payload = load_json_document(args.file)
|
|
56
|
+
render_output(
|
|
57
|
+
args,
|
|
58
|
+
api.agent_opencode_json_set(require_instance(args.instance_id, context), payload),
|
|
59
|
+
)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def handle_agent_chat(args: argparse.Namespace) -> int:
|
|
64
|
+
_, _, context, _, api = build_api(args)
|
|
65
|
+
instance_id = require_instance(args.instance_id, context)
|
|
66
|
+
message = load_text(
|
|
67
|
+
args.message,
|
|
68
|
+
args.message_file,
|
|
69
|
+
required_message="A chat message is required. Pass --message or --message-file.",
|
|
70
|
+
)
|
|
71
|
+
first_user_message = None
|
|
72
|
+
if args.first_user_message or args.first_user_message_file:
|
|
73
|
+
first_user_message = load_text(
|
|
74
|
+
args.first_user_message,
|
|
75
|
+
args.first_user_message_file,
|
|
76
|
+
required_message="first_user_message input is missing.",
|
|
77
|
+
)
|
|
78
|
+
events = api.agent_chat(
|
|
79
|
+
instance_id,
|
|
80
|
+
message,
|
|
81
|
+
session_id=args.session_id,
|
|
82
|
+
provider=args.provider,
|
|
83
|
+
model=args.model,
|
|
84
|
+
first_user_message=first_user_message,
|
|
85
|
+
agent=args.agent_name,
|
|
86
|
+
)
|
|
87
|
+
if args.stream_jsonl:
|
|
88
|
+
for event in events:
|
|
89
|
+
print(json.dumps(event, ensure_ascii=False))
|
|
90
|
+
return 0
|
|
91
|
+
render_output(args, {"events": list(events)})
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def register_agent_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
96
|
+
agent = subparsers.add_parser("agent", help="Agent provider commands.", description="Inspect sandbox agent providers.")
|
|
97
|
+
agent_sub = agent.add_subparsers(dest="agent_command")
|
|
98
|
+
p = agent_sub.add_parser("providers", help="List providers.", description="List available sandbox agent providers.")
|
|
99
|
+
p.add_argument("--instance-id", help="Optional instance ID.")
|
|
100
|
+
p.set_defaults(handler=handle_agent_providers)
|
|
101
|
+
p = agent_sub.add_parser("status", help="Show provider status.", description="Show login status for one agent provider.")
|
|
102
|
+
p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
|
|
103
|
+
p.add_argument("--provider", default="cursor", help="Provider ID. Example: cursor")
|
|
104
|
+
p.set_defaults(handler=handle_agent_status)
|
|
105
|
+
p = agent_sub.add_parser(
|
|
106
|
+
"login-url",
|
|
107
|
+
help="Get a provider login URL.",
|
|
108
|
+
description="Get a browser login URL for one agent provider.",
|
|
109
|
+
)
|
|
110
|
+
p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
|
|
111
|
+
p.add_argument("--provider", default="cursor", help="Provider ID. Example: cursor")
|
|
112
|
+
p.set_defaults(handler=handle_agent_login_url)
|
|
113
|
+
|
|
114
|
+
p = agent_sub.add_parser(
|
|
115
|
+
"chat",
|
|
116
|
+
help="Send a prompt to the sandbox agent.",
|
|
117
|
+
description=(
|
|
118
|
+
"Send one prompt to the sandbox agent running in an instance. Use this when you want the sandbox "
|
|
119
|
+
"itself to inspect or modify the project. The automatic preview build follows the sandbox's current "
|
|
120
|
+
"branch, so run `powerbase branch switch` first when you want a feature-branch preview. By default "
|
|
121
|
+
"the command collects all streamed events and prints them as JSON. Use --stream-jsonl to print one "
|
|
122
|
+
"JSON event per line as events arrive."
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
|
|
126
|
+
p.add_argument("--provider", default="cursor", help="Agent provider ID, such as cursor, opencode, or kiro.")
|
|
127
|
+
p.add_argument("--session-id", help="Optional provider session ID to resume an existing conversation.")
|
|
128
|
+
p.add_argument("--model", help="Optional model ID for providers that support explicit model selection.")
|
|
129
|
+
p.add_argument("--agent", dest="agent_name", help="Optional Kiro agent name.")
|
|
130
|
+
p.add_argument("--message", help="Prompt text. Prefix with @ to load from a file.")
|
|
131
|
+
p.add_argument("--message-file", help="Path to a file containing the prompt text.")
|
|
132
|
+
p.add_argument(
|
|
133
|
+
"--first-user-message",
|
|
134
|
+
help="Optional first user message for language alignment during auto-fix. Prefix with @ to load from a file.",
|
|
135
|
+
)
|
|
136
|
+
p.add_argument("--first-user-message-file", help="Path to a file containing the first user message.")
|
|
137
|
+
p.add_argument("--stream-jsonl", action="store_true", help="Print one JSON event per line while the chat runs.")
|
|
138
|
+
p.set_defaults(handler=handle_agent_chat)
|
|
139
|
+
|
|
140
|
+
opencode_config = agent_sub.add_parser(
|
|
141
|
+
"opencode-config",
|
|
142
|
+
help="Manage opencode API-key config.",
|
|
143
|
+
description="Read or modify the sandbox opencode auth/config files for one instance.",
|
|
144
|
+
)
|
|
145
|
+
opencode_config_sub = opencode_config.add_subparsers(dest="agent_opencode_config_command")
|
|
146
|
+
p = opencode_config_sub.add_parser(
|
|
147
|
+
"get",
|
|
148
|
+
help="Show opencode config.",
|
|
149
|
+
description="Show configured providers, custom providers, and model limit overrides for one instance.",
|
|
150
|
+
)
|
|
151
|
+
p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
|
|
152
|
+
p.set_defaults(handler=handle_agent_opencode_config_get)
|
|
153
|
+
p = opencode_config_sub.add_parser(
|
|
154
|
+
"set",
|
|
155
|
+
help="Save one provider API key.",
|
|
156
|
+
description=(
|
|
157
|
+
"Save or update one opencode provider API key for an instance. Repeat --model-limit with JSON objects like "
|
|
158
|
+
'\'{"id":"claude-sonnet-4","limit":{"context":200000,"output":16000}}\'.'
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
|
|
162
|
+
p.add_argument("--provider", required=True, help="Provider ID to configure, such as anthropic or openai.")
|
|
163
|
+
p.add_argument("--api-key", required=True, help="API key to store in the sandbox.")
|
|
164
|
+
p.add_argument(
|
|
165
|
+
"--model-limit",
|
|
166
|
+
action="append",
|
|
167
|
+
help="Optional JSON object describing one model limit override.",
|
|
168
|
+
)
|
|
169
|
+
p.set_defaults(handler=handle_agent_opencode_config_set)
|
|
170
|
+
p = opencode_config_sub.add_parser(
|
|
171
|
+
"delete",
|
|
172
|
+
help="Delete one provider API key.",
|
|
173
|
+
description="Remove one opencode provider API key from the sandbox auth file.",
|
|
174
|
+
)
|
|
175
|
+
p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
|
|
176
|
+
p.add_argument("--provider", required=True, help="Provider ID to remove.")
|
|
177
|
+
p.set_defaults(handler=handle_agent_opencode_config_delete)
|
|
178
|
+
|
|
179
|
+
opencode_json = agent_sub.add_parser(
|
|
180
|
+
"opencode-json",
|
|
181
|
+
help="Replace the sandbox opencode.json file.",
|
|
182
|
+
description="Replace the sandbox opencode.json file with the contents of one local JSON document.",
|
|
183
|
+
)
|
|
184
|
+
opencode_json_sub = opencode_json.add_subparsers(dest="agent_opencode_json_command")
|
|
185
|
+
p = opencode_json_sub.add_parser(
|
|
186
|
+
"set",
|
|
187
|
+
help="Replace opencode.json.",
|
|
188
|
+
description="Load one local JSON file and replace the sandbox opencode.json file with it.",
|
|
189
|
+
)
|
|
190
|
+
p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
|
|
191
|
+
p.add_argument("--file", required=True, help="Path to a local JSON file.")
|
|
192
|
+
p.set_defaults(handler=handle_agent_opencode_json_set)
|