meshagent-cli 0.5.8b5__py3-none-any.whl → 0.5.13__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.
Potentially problematic release.
This version of meshagent-cli might be problematic. Click here for more details.
- meshagent/cli/agent.py +1 -2
- meshagent/cli/call.py +0 -2
- meshagent/cli/chatbot.py +2 -10
- meshagent/cli/cli.py +0 -4
- meshagent/cli/developer.py +1 -7
- meshagent/cli/exec.py +55 -161
- meshagent/cli/helper.py +1 -3
- meshagent/cli/participant_token.py +2 -3
- meshagent/cli/queue.py +2 -4
- meshagent/cli/services.py +5 -27
- meshagent/cli/version.py +1 -1
- {meshagent_cli-0.5.8b5.dist-info → meshagent_cli-0.5.13.dist-info}/METADATA +10 -16
- meshagent_cli-0.5.13.dist-info/RECORD +32 -0
- meshagent/cli/containers.py +0 -849
- meshagent/cli/oauth2.py +0 -75
- meshagent_cli-0.5.8b5.dist-info/RECORD +0 -34
- {meshagent_cli-0.5.8b5.dist-info → meshagent_cli-0.5.13.dist-info}/WHEEL +0 -0
- {meshagent_cli-0.5.8b5.dist-info → meshagent_cli-0.5.13.dist-info}/entry_points.txt +0 -0
- {meshagent_cli-0.5.8b5.dist-info → meshagent_cli-0.5.13.dist-info}/top_level.txt +0 -0
meshagent/cli/containers.py
DELETED
|
@@ -1,849 +0,0 @@
|
|
|
1
|
-
# meshagent/cli/containers.py
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import asyncio
|
|
5
|
-
import io
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
import tarfile
|
|
9
|
-
import time
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
import typer
|
|
13
|
-
from rich import print
|
|
14
|
-
from typing import Annotated, Optional, List, Dict
|
|
15
|
-
|
|
16
|
-
from meshagent.cli import async_typer
|
|
17
|
-
from meshagent.cli.common_options import ProjectIdOption, ApiKeyIdOption, RoomOption
|
|
18
|
-
from meshagent.cli.helper import (
|
|
19
|
-
get_client,
|
|
20
|
-
resolve_project_id,
|
|
21
|
-
resolve_api_key,
|
|
22
|
-
resolve_room,
|
|
23
|
-
)
|
|
24
|
-
from meshagent.api import (
|
|
25
|
-
RoomClient,
|
|
26
|
-
ParticipantToken,
|
|
27
|
-
WebSocketClientProtocol,
|
|
28
|
-
ApiScope,
|
|
29
|
-
)
|
|
30
|
-
from meshagent.api.helpers import meshagent_base_url, websocket_room_url
|
|
31
|
-
from meshagent.api.room_server_client import (
|
|
32
|
-
BuildSource,
|
|
33
|
-
BuildSourceGit,
|
|
34
|
-
BuildSourceContext,
|
|
35
|
-
BuildSourceRoom,
|
|
36
|
-
DockerSecret,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
app = async_typer.AsyncTyper(help="Manage containers and images inside a room")
|
|
40
|
-
|
|
41
|
-
# -------------------------
|
|
42
|
-
# Helpers
|
|
43
|
-
# -------------------------
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _parse_keyvals(items: List[str]) -> Dict[str, str]:
|
|
47
|
-
"""
|
|
48
|
-
Parse ["KEY=VAL", "FOO=BAR"] -> {"KEY":"VAL", "FOO":"BAR"}
|
|
49
|
-
"""
|
|
50
|
-
out: Dict[str, str] = {}
|
|
51
|
-
for s in items or []:
|
|
52
|
-
if "=" not in s:
|
|
53
|
-
raise typer.BadParameter(f"Expected KEY=VALUE, got: {s}")
|
|
54
|
-
k, v = s.split("=", 1)
|
|
55
|
-
out[k] = v
|
|
56
|
-
return out
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _parse_ports(items: List[str]) -> Dict[int, int]:
|
|
60
|
-
"""
|
|
61
|
-
Parse ["8080:3000", "9999:9999"] as CONTAINER:HOST -> {8080:3000, 9999:9999}
|
|
62
|
-
(Matches server's expectation: container_port -> host_port.)
|
|
63
|
-
"""
|
|
64
|
-
out: Dict[int, int] = {}
|
|
65
|
-
for s in items or []:
|
|
66
|
-
if ":" not in s:
|
|
67
|
-
raise typer.BadParameter(f"Expected CONTAINER:HOST, got: {s}")
|
|
68
|
-
c, h = s.split(":", 1)
|
|
69
|
-
try:
|
|
70
|
-
cp, hp = int(c), int(h)
|
|
71
|
-
except ValueError:
|
|
72
|
-
raise typer.BadParameter(f"Ports must be integers, got: {s}")
|
|
73
|
-
out[cp] = hp
|
|
74
|
-
return out
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _parse_creds(items: List[str]) -> List[DockerSecret]:
|
|
78
|
-
"""
|
|
79
|
-
Parse creds given as:
|
|
80
|
-
--cred username,password
|
|
81
|
-
--cred registry,username,password
|
|
82
|
-
"""
|
|
83
|
-
creds: List[DockerSecret] = []
|
|
84
|
-
for s in items or []:
|
|
85
|
-
parts = [p.strip() for p in s.split(",")]
|
|
86
|
-
if len(parts) == 2:
|
|
87
|
-
u, p = parts
|
|
88
|
-
creds.append(DockerSecret(username=u, password=p))
|
|
89
|
-
elif len(parts) == 3:
|
|
90
|
-
r, u, p = parts
|
|
91
|
-
creds.append(DockerSecret(registry=r, username=u, password=p))
|
|
92
|
-
else:
|
|
93
|
-
raise typer.BadParameter(
|
|
94
|
-
f"Invalid --cred format: {s}. Use username,password or registry,username,password"
|
|
95
|
-
)
|
|
96
|
-
return creds
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _tarfilter_strip_macos(ti: tarfile.TarInfo) -> tarfile.TarInfo | None:
|
|
100
|
-
"""
|
|
101
|
-
Filter to make Linux-friendly tarballs:
|
|
102
|
-
- Drop AppleDouble files (._*)
|
|
103
|
-
- Reset uid/gid/uname/gname
|
|
104
|
-
- Clear pax headers
|
|
105
|
-
"""
|
|
106
|
-
base = os.path.basename(ti.name)
|
|
107
|
-
if base.startswith("._"):
|
|
108
|
-
return None
|
|
109
|
-
ti.uid = 0
|
|
110
|
-
ti.gid = 0
|
|
111
|
-
ti.uname = ""
|
|
112
|
-
ti.gname = ""
|
|
113
|
-
ti.pax_headers = {}
|
|
114
|
-
# Preserve mode & type; set a stable-ish mtime
|
|
115
|
-
if ti.mtime is None:
|
|
116
|
-
ti.mtime = int(time.time())
|
|
117
|
-
return ti
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _make_targz_from_dir(path: Path) -> bytes:
|
|
121
|
-
buf = io.BytesIO()
|
|
122
|
-
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
|
123
|
-
tar.add(path, arcname=".", filter=_tarfilter_strip_macos)
|
|
124
|
-
return buf.getvalue()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _make_targz_with_dockerfile_text(text: str) -> bytes:
|
|
128
|
-
b = text.encode("utf-8")
|
|
129
|
-
buf = io.BytesIO()
|
|
130
|
-
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
|
131
|
-
ti = tarfile.TarInfo("Dockerfile")
|
|
132
|
-
ti.size = len(b)
|
|
133
|
-
ti.mtime = int(time.time())
|
|
134
|
-
ti.mode = 0o644
|
|
135
|
-
tar.addfile(ti, io.BytesIO(b))
|
|
136
|
-
return buf.getvalue()
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
async def _drain_stream_plain(stream, *, show_progress: bool = True):
|
|
140
|
-
async def _logs():
|
|
141
|
-
async for line in stream.logs():
|
|
142
|
-
# Server emits plain lines; print as-is
|
|
143
|
-
if line is not None:
|
|
144
|
-
print(line)
|
|
145
|
-
|
|
146
|
-
async def _prog():
|
|
147
|
-
if not show_progress:
|
|
148
|
-
async for _ in stream.progress():
|
|
149
|
-
pass
|
|
150
|
-
return
|
|
151
|
-
async for p in stream.progress():
|
|
152
|
-
if p is None:
|
|
153
|
-
return
|
|
154
|
-
msg = p.message or ""
|
|
155
|
-
# Show progress if we have numbers, else just the message.
|
|
156
|
-
if p.current is not None and p.total:
|
|
157
|
-
print(f"[cyan]{msg} ({p.current}/{p.total})[/cyan]")
|
|
158
|
-
elif msg:
|
|
159
|
-
print(f"[cyan]{msg}[/cyan]")
|
|
160
|
-
|
|
161
|
-
t1 = asyncio.create_task(_logs())
|
|
162
|
-
t2 = asyncio.create_task(_prog())
|
|
163
|
-
try:
|
|
164
|
-
return await stream
|
|
165
|
-
finally:
|
|
166
|
-
await asyncio.gather(t1, t2, return_exceptions=True)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
async def _drain_stream_pretty(stream):
|
|
170
|
-
import asyncio
|
|
171
|
-
import math
|
|
172
|
-
from rich.table import Column
|
|
173
|
-
from rich.live import Live
|
|
174
|
-
from rich.panel import Panel
|
|
175
|
-
from rich.console import Group
|
|
176
|
-
from rich.text import Text
|
|
177
|
-
from rich.progress import (
|
|
178
|
-
Progress,
|
|
179
|
-
TextColumn,
|
|
180
|
-
BarColumn,
|
|
181
|
-
TimeElapsedColumn,
|
|
182
|
-
ProgressColumn,
|
|
183
|
-
SpinnerColumn,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
class MaybeMofN(ProgressColumn):
|
|
187
|
-
def render(self, task):
|
|
188
|
-
import math
|
|
189
|
-
from rich.text import Text
|
|
190
|
-
|
|
191
|
-
def _fmt_bytes(n):
|
|
192
|
-
if n is None:
|
|
193
|
-
return ""
|
|
194
|
-
n = float(n)
|
|
195
|
-
units = ["B", "KiB", "MiB", "GiB", "TiB"]
|
|
196
|
-
i = 0
|
|
197
|
-
while n >= 1024 and i < len(units) - 1:
|
|
198
|
-
n /= 1024
|
|
199
|
-
i += 1
|
|
200
|
-
return f"{n:.1f} {units[i]}"
|
|
201
|
-
|
|
202
|
-
if task.total == 0 or math.isinf(task.total):
|
|
203
|
-
return Text("")
|
|
204
|
-
return Text(f"{_fmt_bytes(task.completed)} / {_fmt_bytes(task.total)}")
|
|
205
|
-
|
|
206
|
-
class MaybeBarColumn(BarColumn):
|
|
207
|
-
def __init__(
|
|
208
|
-
self,
|
|
209
|
-
*,
|
|
210
|
-
bar_width: int | None = 28,
|
|
211
|
-
hide_when_unknown: bool = False,
|
|
212
|
-
column_width: int | None = None,
|
|
213
|
-
**kwargs,
|
|
214
|
-
):
|
|
215
|
-
# bar_width controls the drawn bar size; None = flex
|
|
216
|
-
super().__init__(bar_width=bar_width, **kwargs)
|
|
217
|
-
self.hide_when_unknown = hide_when_unknown
|
|
218
|
-
self.column_width = column_width # fix the table column if set
|
|
219
|
-
|
|
220
|
-
def get_table_column(self) -> Column:
|
|
221
|
-
if self.column_width is None:
|
|
222
|
-
# default behavior (may flex depending on layout)
|
|
223
|
-
return Column(no_wrap=True)
|
|
224
|
-
return Column(
|
|
225
|
-
width=self.column_width,
|
|
226
|
-
min_width=self.column_width,
|
|
227
|
-
max_width=self.column_width,
|
|
228
|
-
no_wrap=True,
|
|
229
|
-
overflow="crop",
|
|
230
|
-
justify="left",
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
def render(self, task):
|
|
234
|
-
if task.total is None or task.total == 0 or math.isinf(task.total):
|
|
235
|
-
return Text("") # hide bar for indeterminate tasks
|
|
236
|
-
return super().render(task)
|
|
237
|
-
|
|
238
|
-
class MaybeETA(ProgressColumn):
|
|
239
|
-
"""Show ETA only if total is known."""
|
|
240
|
-
|
|
241
|
-
_elapsed = TimeElapsedColumn()
|
|
242
|
-
|
|
243
|
-
def render(self, task):
|
|
244
|
-
# You can swap this to a TimeRemainingColumn() if you prefer,
|
|
245
|
-
# but hide when total is unknown.
|
|
246
|
-
if task.total == 0 or math.isinf(task.total):
|
|
247
|
-
return Text("")
|
|
248
|
-
return self._elapsed.render(task) # or TimeRemainingColumn().render(task)
|
|
249
|
-
|
|
250
|
-
progress = Progress(
|
|
251
|
-
SpinnerColumn(),
|
|
252
|
-
TextColumn(
|
|
253
|
-
"[bold]{task.description}",
|
|
254
|
-
table_column=Column(ratio=8, no_wrap=True, overflow="ellipsis"),
|
|
255
|
-
),
|
|
256
|
-
MaybeMofN(table_column=Column(ratio=2, no_wrap=True, overflow="ellipsis")),
|
|
257
|
-
MaybeETA(table_column=Column(ratio=1, no_wrap=True, overflow="ellipsis")),
|
|
258
|
-
MaybeBarColumn(pulse_style="cyan", bar_width=20, hide_when_unknown=True),
|
|
259
|
-
# pulses automatically if total=None
|
|
260
|
-
transient=False, # we’re inside Live; we’ll hide tasks ourselves
|
|
261
|
-
expand=True,
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
logs_tail: list[str] = []
|
|
265
|
-
tasks: dict[str, int] = {} # layer -> task_id
|
|
266
|
-
|
|
267
|
-
def render():
|
|
268
|
-
tail = "\n".join(logs_tail[-12:]) or "waiting…"
|
|
269
|
-
return Group(
|
|
270
|
-
progress,
|
|
271
|
-
Panel(tail, title="logs", border_style="cyan", height=12),
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
async def _logs():
|
|
275
|
-
async for line in stream.logs():
|
|
276
|
-
if line:
|
|
277
|
-
logs_tail.append(line.strip())
|
|
278
|
-
|
|
279
|
-
async def _prog():
|
|
280
|
-
async for p in stream.progress():
|
|
281
|
-
layer = p.layer or "overall"
|
|
282
|
-
if layer not in tasks:
|
|
283
|
-
tasks[layer] = progress.add_task(
|
|
284
|
-
p.message or layer, total=p.total if p.total is not None else 0
|
|
285
|
-
)
|
|
286
|
-
task_id = tasks[layer]
|
|
287
|
-
|
|
288
|
-
updates = {}
|
|
289
|
-
# Keep total=None for pulsing; only set if we get a real number.
|
|
290
|
-
if p.total is not None and not math.isinf(p.total):
|
|
291
|
-
updates["total"] = p.total
|
|
292
|
-
if p.current is not None:
|
|
293
|
-
updates["completed"] = p.current
|
|
294
|
-
if p.message:
|
|
295
|
-
updates["description"] = p.message
|
|
296
|
-
if updates:
|
|
297
|
-
progress.update(task_id, **updates)
|
|
298
|
-
|
|
299
|
-
with Live(render(), refresh_per_second=10) as live:
|
|
300
|
-
|
|
301
|
-
async def _refresh():
|
|
302
|
-
while True:
|
|
303
|
-
live.update(render())
|
|
304
|
-
await asyncio.sleep(0.1)
|
|
305
|
-
|
|
306
|
-
t_logs = asyncio.create_task(_logs())
|
|
307
|
-
t_prog = asyncio.create_task(_prog())
|
|
308
|
-
t_ui = asyncio.create_task(_refresh())
|
|
309
|
-
try:
|
|
310
|
-
result = await stream
|
|
311
|
-
return result
|
|
312
|
-
finally:
|
|
313
|
-
# Hide any still-visible tasks (e.g., indeterminate ones with total=None)
|
|
314
|
-
for tid in list(tasks.values()):
|
|
315
|
-
progress.update(tid, visible=False)
|
|
316
|
-
live.update(render())
|
|
317
|
-
|
|
318
|
-
for t in (t_logs, t_prog):
|
|
319
|
-
await t
|
|
320
|
-
|
|
321
|
-
t_ui.cancel()
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
async def _with_client(
|
|
325
|
-
*,
|
|
326
|
-
project_id: ProjectIdOption,
|
|
327
|
-
room: RoomOption,
|
|
328
|
-
api_key_id: ApiKeyIdOption,
|
|
329
|
-
name: str,
|
|
330
|
-
role: str,
|
|
331
|
-
):
|
|
332
|
-
account_client = await get_client()
|
|
333
|
-
try:
|
|
334
|
-
project_id = await resolve_project_id(project_id=project_id)
|
|
335
|
-
api_key_id = await resolve_api_key(project_id, api_key_id)
|
|
336
|
-
room = resolve_room(room)
|
|
337
|
-
|
|
338
|
-
key = (
|
|
339
|
-
await account_client.decrypt_project_api_key(
|
|
340
|
-
project_id=project_id, id=api_key_id
|
|
341
|
-
)
|
|
342
|
-
)["token"]
|
|
343
|
-
|
|
344
|
-
token = ParticipantToken(
|
|
345
|
-
name=name, project_id=project_id, api_key_id=api_key_id
|
|
346
|
-
)
|
|
347
|
-
token.add_api_grant(ApiScope.agent_default())
|
|
348
|
-
token.add_role_grant(role=role)
|
|
349
|
-
token.add_room_grant(room)
|
|
350
|
-
|
|
351
|
-
print("[bold green]Connecting to room...[/bold green]", flush=True)
|
|
352
|
-
proto = WebSocketClientProtocol(
|
|
353
|
-
url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
|
|
354
|
-
token=token.to_jwt(token=key),
|
|
355
|
-
)
|
|
356
|
-
client_cm = RoomClient(protocol=proto)
|
|
357
|
-
await client_cm.__aenter__()
|
|
358
|
-
return account_client, client_cm
|
|
359
|
-
except Exception:
|
|
360
|
-
await account_client.close()
|
|
361
|
-
raise
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
# -------------------------
|
|
365
|
-
# Top-level: ps / stop / logs / run
|
|
366
|
-
# -------------------------
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
@app.async_command("ps")
|
|
370
|
-
async def list_containers(
|
|
371
|
-
*,
|
|
372
|
-
project_id: ProjectIdOption = None,
|
|
373
|
-
room: RoomOption = None,
|
|
374
|
-
api_key_id: ApiKeyIdOption = None,
|
|
375
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
376
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
377
|
-
output: Annotated[Optional[str], typer.Option(help="json | table")] = "json",
|
|
378
|
-
):
|
|
379
|
-
account_client, client = await _with_client(
|
|
380
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
381
|
-
)
|
|
382
|
-
try:
|
|
383
|
-
containers = await client.containers.list()
|
|
384
|
-
if output == "table":
|
|
385
|
-
from rich.table import Table
|
|
386
|
-
from rich.console import Console
|
|
387
|
-
|
|
388
|
-
table = Table(title="Containers")
|
|
389
|
-
table.add_column("ID", style="cyan")
|
|
390
|
-
table.add_column("Image")
|
|
391
|
-
table.add_column("Status")
|
|
392
|
-
for c in containers:
|
|
393
|
-
table.add_row(c.id, c.image or "", c.status or "")
|
|
394
|
-
Console().print(table)
|
|
395
|
-
else:
|
|
396
|
-
# default json-ish
|
|
397
|
-
print([c.model_dump() for c in containers])
|
|
398
|
-
finally:
|
|
399
|
-
await client.__aexit__(None, None, None)
|
|
400
|
-
await account_client.close()
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
@app.async_command("stop")
|
|
404
|
-
async def stop_container(
|
|
405
|
-
*,
|
|
406
|
-
project_id: ProjectIdOption = None,
|
|
407
|
-
room: RoomOption = None,
|
|
408
|
-
api_key_id: ApiKeyIdOption = None,
|
|
409
|
-
id: Annotated[str, typer.Option(..., help="Container ID")],
|
|
410
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
411
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
412
|
-
):
|
|
413
|
-
account_client, client = await _with_client(
|
|
414
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
415
|
-
)
|
|
416
|
-
try:
|
|
417
|
-
await client.containers.stop(container_id=id)
|
|
418
|
-
print("[green]Stopped[/green]")
|
|
419
|
-
finally:
|
|
420
|
-
await client.__aexit__(None, None, None)
|
|
421
|
-
await account_client.close()
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
@app.async_command("logs")
|
|
425
|
-
async def container_logs(
|
|
426
|
-
*,
|
|
427
|
-
project_id: ProjectIdOption = None,
|
|
428
|
-
room: RoomOption = None,
|
|
429
|
-
api_key_id: ApiKeyIdOption = None,
|
|
430
|
-
id: Annotated[str, typer.Option(..., help="Container ID")],
|
|
431
|
-
follow: Annotated[bool, typer.Option("--follow/--no-follow")] = False,
|
|
432
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
433
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
434
|
-
):
|
|
435
|
-
account_client, client = await _with_client(
|
|
436
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
437
|
-
)
|
|
438
|
-
try:
|
|
439
|
-
stream = client.containers.logs(container_id=id, follow=follow)
|
|
440
|
-
await _drain_stream_plain(stream)
|
|
441
|
-
finally:
|
|
442
|
-
await client.__aexit__(None, None, None)
|
|
443
|
-
await account_client.close()
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
# -------------------------
|
|
447
|
-
# Run (detached) and run-attached
|
|
448
|
-
# -------------------------
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
@app.async_command("run")
|
|
452
|
-
async def run_container(
|
|
453
|
-
*,
|
|
454
|
-
project_id: ProjectIdOption = None,
|
|
455
|
-
room: RoomOption = None,
|
|
456
|
-
api_key_id: ApiKeyIdOption = None,
|
|
457
|
-
image: Annotated[str, typer.Option(..., help="Image to run")],
|
|
458
|
-
command: Annotated[Optional[str], typer.Option(...)] = None,
|
|
459
|
-
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
460
|
-
port: Annotated[
|
|
461
|
-
List[str], typer.Option("--port", "-p", help="CONTAINER:HOST")
|
|
462
|
-
] = [],
|
|
463
|
-
var: Annotated[
|
|
464
|
-
List[str],
|
|
465
|
-
typer.Option("--var", help="Template variable KEY=VALUE (optional)"),
|
|
466
|
-
] = [],
|
|
467
|
-
cred: Annotated[
|
|
468
|
-
List[str],
|
|
469
|
-
typer.Option(
|
|
470
|
-
"--cred",
|
|
471
|
-
help="Docker creds (username,password) or (registry,username,password)",
|
|
472
|
-
),
|
|
473
|
-
] = [],
|
|
474
|
-
mount_path: Annotated[Optional[str], typer.Option()] = None,
|
|
475
|
-
mount_subpath: Annotated[Optional[str], typer.Option()] = None,
|
|
476
|
-
participant_name: Annotated[Optional[str], typer.Option()] = None,
|
|
477
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
478
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
479
|
-
):
|
|
480
|
-
account_client, client = await _with_client(
|
|
481
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
482
|
-
)
|
|
483
|
-
try:
|
|
484
|
-
creds = _parse_creds(cred)
|
|
485
|
-
env_map = _parse_keyvals(env)
|
|
486
|
-
ports_map = _parse_ports(port)
|
|
487
|
-
vars_map = _parse_keyvals(var)
|
|
488
|
-
|
|
489
|
-
stream = client.containers.run(
|
|
490
|
-
image=image,
|
|
491
|
-
command=command,
|
|
492
|
-
env=env_map,
|
|
493
|
-
mount_path=mount_path,
|
|
494
|
-
mount_subpath=mount_subpath,
|
|
495
|
-
role=role,
|
|
496
|
-
participant_name=participant_name,
|
|
497
|
-
ports=ports_map,
|
|
498
|
-
credentials=creds,
|
|
499
|
-
variables=vars_map or None,
|
|
500
|
-
)
|
|
501
|
-
result = await _drain_stream_plain(stream)
|
|
502
|
-
print(result.model_dump() if hasattr(result, "model_dump") else result)
|
|
503
|
-
finally:
|
|
504
|
-
await client.__aexit__(None, None, None)
|
|
505
|
-
await account_client.close()
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
@app.async_command("run-attached")
|
|
509
|
-
async def run_attached(
|
|
510
|
-
*,
|
|
511
|
-
project_id: ProjectIdOption = None,
|
|
512
|
-
room: RoomOption = None,
|
|
513
|
-
api_key_id: ApiKeyIdOption = None,
|
|
514
|
-
image: Annotated[str, typer.Option(..., help="Image to run")],
|
|
515
|
-
command: Annotated[Optional[str], typer.Option(...)] = None,
|
|
516
|
-
tty: Annotated[bool, typer.Option("--tty/--no-tty")] = False,
|
|
517
|
-
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
518
|
-
port: Annotated[
|
|
519
|
-
List[str], typer.Option("--port", "-p", help="CONTAINER:HOST")
|
|
520
|
-
] = [],
|
|
521
|
-
send: Annotated[
|
|
522
|
-
List[str],
|
|
523
|
-
typer.Option(
|
|
524
|
-
"--send",
|
|
525
|
-
help="Optional lines to send to container stdin (each becomes a line)",
|
|
526
|
-
),
|
|
527
|
-
] = [],
|
|
528
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
529
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
530
|
-
):
|
|
531
|
-
account_client, client = await _with_client(
|
|
532
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
533
|
-
)
|
|
534
|
-
try:
|
|
535
|
-
env_map = _parse_keyvals(env)
|
|
536
|
-
ports_map = _parse_ports(port)
|
|
537
|
-
|
|
538
|
-
tty_obj = client.containers.run_attached(
|
|
539
|
-
image=image,
|
|
540
|
-
command=command,
|
|
541
|
-
env=env_map,
|
|
542
|
-
ports=ports_map,
|
|
543
|
-
tty=tty,
|
|
544
|
-
role=role,
|
|
545
|
-
participant_name=name,
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
# Output reader
|
|
549
|
-
async def _read():
|
|
550
|
-
async for b in tty_obj.output():
|
|
551
|
-
if not b:
|
|
552
|
-
continue
|
|
553
|
-
try:
|
|
554
|
-
sys.stdout.buffer.write(b)
|
|
555
|
-
sys.stdout.flush()
|
|
556
|
-
except Exception:
|
|
557
|
-
# fallback printing
|
|
558
|
-
print(b.decode(errors="ignore"), end="")
|
|
559
|
-
|
|
560
|
-
# Optional sender (from --send args)
|
|
561
|
-
async def _preload():
|
|
562
|
-
for line in send:
|
|
563
|
-
await tty_obj.write(line.encode("utf-8") + b"\n")
|
|
564
|
-
|
|
565
|
-
readers = asyncio.gather(_read(), _preload())
|
|
566
|
-
status = await tty_obj.result
|
|
567
|
-
await readers
|
|
568
|
-
if status is not None:
|
|
569
|
-
print(f"\n[green]Exit status:[/green] {status}")
|
|
570
|
-
finally:
|
|
571
|
-
await client.__aexit__(None, None, None)
|
|
572
|
-
await account_client.close()
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
# -------------------------
|
|
576
|
-
# Images sub-commands
|
|
577
|
-
# -------------------------
|
|
578
|
-
|
|
579
|
-
images_app = async_typer.AsyncTyper(help="Image operations")
|
|
580
|
-
app.add_typer(images_app, name="images")
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
@images_app.async_command("list")
|
|
584
|
-
async def images_list(
|
|
585
|
-
*,
|
|
586
|
-
project_id: ProjectIdOption = None,
|
|
587
|
-
room: RoomOption = None,
|
|
588
|
-
api_key_id: ApiKeyIdOption = None,
|
|
589
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
590
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
591
|
-
):
|
|
592
|
-
account_client, client = await _with_client(
|
|
593
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
594
|
-
)
|
|
595
|
-
try:
|
|
596
|
-
imgs = await client.containers.list_images()
|
|
597
|
-
print([i.model_dump() for i in imgs])
|
|
598
|
-
finally:
|
|
599
|
-
await client.__aexit__(None, None, None)
|
|
600
|
-
await account_client.close()
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
@images_app.async_command("delete")
|
|
604
|
-
async def images_delete(
|
|
605
|
-
*,
|
|
606
|
-
project_id: ProjectIdOption = None,
|
|
607
|
-
room: RoomOption = None,
|
|
608
|
-
api_key_id: ApiKeyIdOption = None,
|
|
609
|
-
image: Annotated[str, typer.Option(..., help="Image ref/tag to delete")],
|
|
610
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
611
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
612
|
-
):
|
|
613
|
-
account_client, client = await _with_client(
|
|
614
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
615
|
-
)
|
|
616
|
-
try:
|
|
617
|
-
await client.containers.delete_image(image=image)
|
|
618
|
-
print("[green]Deleted[/green]")
|
|
619
|
-
finally:
|
|
620
|
-
await client.__aexit__(None, None, None)
|
|
621
|
-
await account_client.close()
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
@images_app.async_command("pull")
|
|
625
|
-
async def images_pull(
|
|
626
|
-
*,
|
|
627
|
-
project_id: ProjectIdOption = None,
|
|
628
|
-
room: RoomOption = None,
|
|
629
|
-
api_key_id: ApiKeyIdOption = None,
|
|
630
|
-
tag: Annotated[str, typer.Option(..., help="Image tag/ref to pull")],
|
|
631
|
-
cred: Annotated[
|
|
632
|
-
List[str],
|
|
633
|
-
typer.Option(
|
|
634
|
-
"--cred",
|
|
635
|
-
help="Docker creds (username,password) or (registry,username,password)",
|
|
636
|
-
),
|
|
637
|
-
] = [],
|
|
638
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
639
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
640
|
-
):
|
|
641
|
-
account_client, client = await _with_client(
|
|
642
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
643
|
-
)
|
|
644
|
-
try:
|
|
645
|
-
stream = client.containers.pull_image(tag=tag, credentials=_parse_creds(cred))
|
|
646
|
-
result = await _drain_stream_plain(stream)
|
|
647
|
-
print(result.model_dump() if hasattr(result, "model_dump") else result)
|
|
648
|
-
finally:
|
|
649
|
-
await client.__aexit__(None, None, None)
|
|
650
|
-
await account_client.close()
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
# -------------------------
|
|
654
|
-
# Build sub-commands
|
|
655
|
-
# -------------------------
|
|
656
|
-
|
|
657
|
-
build_app = async_typer.AsyncTyper(help="Build images")
|
|
658
|
-
app.add_typer(build_app, name="build")
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
@build_app.async_command("git")
|
|
662
|
-
async def build_git(
|
|
663
|
-
*,
|
|
664
|
-
project_id: ProjectIdOption = None,
|
|
665
|
-
room: RoomOption = None,
|
|
666
|
-
api_key_id: ApiKeyIdOption = None,
|
|
667
|
-
tag: Annotated[str, typer.Option(..., help="Resulting image tag")],
|
|
668
|
-
url: Annotated[str, typer.Option(..., help="Git URL")],
|
|
669
|
-
ref: Annotated[str, typer.Option(..., help="Git ref/branch/tag")],
|
|
670
|
-
cred: Annotated[
|
|
671
|
-
List[str],
|
|
672
|
-
typer.Option(
|
|
673
|
-
"--cred",
|
|
674
|
-
help="Docker creds (username,password) or (registry,username,password)",
|
|
675
|
-
),
|
|
676
|
-
] = [],
|
|
677
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
678
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
679
|
-
pretty: Annotated[bool, typer.Option(...)] = True,
|
|
680
|
-
):
|
|
681
|
-
account_client, client = await _with_client(
|
|
682
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
683
|
-
)
|
|
684
|
-
try:
|
|
685
|
-
source = BuildSource(git=BuildSourceGit(url=url, ref=ref))
|
|
686
|
-
stream = client.containers.build(
|
|
687
|
-
tag=tag, source=source, credentials=_parse_creds(cred)
|
|
688
|
-
)
|
|
689
|
-
await _drain_stream_pretty(stream) if pretty else await _drain_stream_plain(
|
|
690
|
-
stream
|
|
691
|
-
)
|
|
692
|
-
finally:
|
|
693
|
-
await client.__aexit__(None, None, None)
|
|
694
|
-
await account_client.close()
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
@build_app.async_command("context")
|
|
698
|
-
async def build_context(
|
|
699
|
-
*,
|
|
700
|
-
project_id: ProjectIdOption = None,
|
|
701
|
-
room: RoomOption = None,
|
|
702
|
-
api_key_id: ApiKeyIdOption = None,
|
|
703
|
-
tag: Annotated[str, typer.Option(..., help="Resulting image tag")],
|
|
704
|
-
from_dir: Annotated[
|
|
705
|
-
Optional[str],
|
|
706
|
-
typer.Option(help="Directory to tar.gz as build context"),
|
|
707
|
-
] = None,
|
|
708
|
-
dockerfile: Annotated[
|
|
709
|
-
Optional[str],
|
|
710
|
-
typer.Option(help="Path to a Dockerfile; sends just this file as context"),
|
|
711
|
-
] = None,
|
|
712
|
-
dockerfile_inline: Annotated[
|
|
713
|
-
Optional[str],
|
|
714
|
-
typer.Option(help="Inline Dockerfile text; sends only this as context"),
|
|
715
|
-
] = None,
|
|
716
|
-
tgz: Annotated[
|
|
717
|
-
Optional[str],
|
|
718
|
-
typer.Option(help="Use an existing .tar.gz file as the context"),
|
|
719
|
-
] = None,
|
|
720
|
-
cred: Annotated[
|
|
721
|
-
List[str],
|
|
722
|
-
typer.Option(
|
|
723
|
-
"--cred",
|
|
724
|
-
help="Docker creds (username,password) or (registry,username,password)",
|
|
725
|
-
),
|
|
726
|
-
] = [],
|
|
727
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
728
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
729
|
-
pretty: Annotated[bool, typer.Option(...)] = True,
|
|
730
|
-
):
|
|
731
|
-
# Validate mutually exclusive inputs
|
|
732
|
-
specified = [x for x in [from_dir, dockerfile, dockerfile_inline, tgz] if x]
|
|
733
|
-
if len(specified) != 1:
|
|
734
|
-
raise typer.BadParameter(
|
|
735
|
-
"Specify exactly one of --from-dir, --dockerfile, --dockerfile-inline, or --tgz"
|
|
736
|
-
)
|
|
737
|
-
|
|
738
|
-
# Prepare context bytes
|
|
739
|
-
if from_dir:
|
|
740
|
-
ctx_bytes = _make_targz_from_dir(Path(from_dir).resolve())
|
|
741
|
-
elif dockerfile_inline:
|
|
742
|
-
ctx_bytes = _make_targz_with_dockerfile_text(dockerfile_inline)
|
|
743
|
-
elif dockerfile:
|
|
744
|
-
text = Path(dockerfile).read_text(encoding="utf-8")
|
|
745
|
-
ctx_bytes = _make_targz_with_dockerfile_text(text)
|
|
746
|
-
else:
|
|
747
|
-
ctx_bytes = Path(tgz).read_bytes()
|
|
748
|
-
|
|
749
|
-
account_client, client = await _with_client(
|
|
750
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
751
|
-
)
|
|
752
|
-
try:
|
|
753
|
-
source = BuildSource(context=BuildSourceContext(encoding="gzip"))
|
|
754
|
-
stream = client.containers.build(
|
|
755
|
-
tag=tag,
|
|
756
|
-
source=source,
|
|
757
|
-
context_bytes=ctx_bytes,
|
|
758
|
-
credentials=_parse_creds(cred),
|
|
759
|
-
)
|
|
760
|
-
await _drain_stream_pretty(stream) if pretty else await _drain_stream_plain(
|
|
761
|
-
stream
|
|
762
|
-
)
|
|
763
|
-
finally:
|
|
764
|
-
await client.__aexit__(None, None, None)
|
|
765
|
-
await account_client.close()
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
@build_app.async_command("room")
|
|
769
|
-
async def build_room(
|
|
770
|
-
*,
|
|
771
|
-
project_id: ProjectIdOption = None,
|
|
772
|
-
room: RoomOption = None,
|
|
773
|
-
api_key_id: ApiKeyIdOption = None,
|
|
774
|
-
tag: Annotated[str, typer.Option(..., help="Resulting image tag")],
|
|
775
|
-
path: Annotated[str, typer.Option(..., help="Room path to a .tar.gz context")],
|
|
776
|
-
cred: Annotated[
|
|
777
|
-
List[str],
|
|
778
|
-
typer.Option(
|
|
779
|
-
"--cred",
|
|
780
|
-
help="Docker creds (username,password) or (registry,username,password)",
|
|
781
|
-
),
|
|
782
|
-
] = [],
|
|
783
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
784
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
785
|
-
pretty: Annotated[bool, typer.Option(...)] = True,
|
|
786
|
-
):
|
|
787
|
-
account_client, client = await _with_client(
|
|
788
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
789
|
-
)
|
|
790
|
-
try:
|
|
791
|
-
source = BuildSource(room=BuildSourceRoom(path=path))
|
|
792
|
-
stream = client.containers.build(
|
|
793
|
-
tag=tag, source=source, credentials=_parse_creds(cred)
|
|
794
|
-
)
|
|
795
|
-
await _drain_stream_pretty(stream) if pretty else await _drain_stream_plain(
|
|
796
|
-
stream
|
|
797
|
-
)
|
|
798
|
-
finally:
|
|
799
|
-
await client.__aexit__(None, None, None)
|
|
800
|
-
await account_client.close()
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
# -------------------------
|
|
804
|
-
# Build admin: list/stop
|
|
805
|
-
# -------------------------
|
|
806
|
-
|
|
807
|
-
builds_app = async_typer.AsyncTyper(help="Inspect or manage running builds")
|
|
808
|
-
app.add_typer(builds_app, name="builds")
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
@builds_app.async_command("list")
|
|
812
|
-
async def list_builds(
|
|
813
|
-
*,
|
|
814
|
-
project_id: ProjectIdOption = None,
|
|
815
|
-
room: RoomOption = None,
|
|
816
|
-
api_key_id: ApiKeyIdOption = None,
|
|
817
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
818
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
819
|
-
):
|
|
820
|
-
account_client, client = await _with_client(
|
|
821
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
822
|
-
)
|
|
823
|
-
try:
|
|
824
|
-
builds = await client.containers.list_builds()
|
|
825
|
-
print([b.model_dump() for b in builds])
|
|
826
|
-
finally:
|
|
827
|
-
await client.__aexit__(None, None, None)
|
|
828
|
-
await account_client.close()
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
@builds_app.async_command("stop")
|
|
832
|
-
async def stop_build(
|
|
833
|
-
*,
|
|
834
|
-
project_id: ProjectIdOption = None,
|
|
835
|
-
room: RoomOption = None,
|
|
836
|
-
api_key_id: ApiKeyIdOption = None,
|
|
837
|
-
request_id: Annotated[str, typer.Option(..., help="Build request_id to stop")],
|
|
838
|
-
name: Annotated[str, typer.Option(...)] = "cli",
|
|
839
|
-
role: Annotated[str, typer.Option(...)] = "user",
|
|
840
|
-
):
|
|
841
|
-
account_client, client = await _with_client(
|
|
842
|
-
project_id=project_id, room=room, api_key_id=api_key_id, name=name, role=role
|
|
843
|
-
)
|
|
844
|
-
try:
|
|
845
|
-
await client.containers.stop_build(request_id=request_id)
|
|
846
|
-
print("[green]Stopped[/green]")
|
|
847
|
-
finally:
|
|
848
|
-
await client.__aexit__(None, None, None)
|
|
849
|
-
await account_client.close()
|