meshagent-cli 0.38.2__tar.gz → 0.38.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshagent_cli-0.38.2/meshagent_cli.egg-info → meshagent_cli-0.38.4}/PKG-INFO +15 -14
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/auth.py +52 -6
- meshagent_cli-0.38.4/meshagent/cli/auth_test.py +309 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/cli.py +6 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/cli_test.py +1 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/containers.py +192 -1
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/containers_test.py +161 -1
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/image.py +252 -3
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/image_test.py +389 -0
- meshagent_cli-0.38.4/meshagent/cli/launch.py +98 -0
- meshagent_cli-0.38.4/meshagent/cli/launch_test.py +106 -0
- meshagent_cli-0.38.4/meshagent/cli/projects.py +280 -0
- meshagent_cli-0.38.4/meshagent/cli/projects_test.py +282 -0
- meshagent_cli-0.38.4/meshagent/cli/root_commands.py +339 -0
- meshagent_cli-0.38.4/meshagent/cli/root_commands_test.py +809 -0
- meshagent_cli-0.38.4/meshagent/cli/tool_integrations.py +1413 -0
- meshagent_cli-0.38.4/meshagent/cli/tool_integrations_test.py +881 -0
- meshagent_cli-0.38.4/meshagent/cli/tui/auth_switch.py +277 -0
- meshagent_cli-0.38.4/meshagent/cli/tui/auth_switch_test.py +125 -0
- meshagent_cli-0.38.4/meshagent/cli/tui/project_activate.py +372 -0
- meshagent_cli-0.38.4/meshagent/cli/tui/project_activate_test.py +153 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/tui/setup.py +999 -17
- meshagent_cli-0.38.4/meshagent/cli/tui/setup_test.py +737 -0
- meshagent_cli-0.38.4/meshagent/cli/version.py +1 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4/meshagent_cli.egg-info}/PKG-INFO +15 -14
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent_cli.egg-info/SOURCES.txt +9 -0
- meshagent_cli-0.38.4/meshagent_cli.egg-info/requires.txt +34 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/pyproject.toml +14 -13
- meshagent_cli-0.38.2/meshagent/cli/auth_test.py +0 -109
- meshagent_cli-0.38.2/meshagent/cli/projects.py +0 -119
- meshagent_cli-0.38.2/meshagent/cli/root_commands.py +0 -190
- meshagent_cli-0.38.2/meshagent/cli/root_commands_test.py +0 -185
- meshagent_cli-0.38.2/meshagent/cli/tui/setup_test.py +0 -22
- meshagent_cli-0.38.2/meshagent/cli/version.py +0 -1
- meshagent_cli-0.38.2/meshagent_cli.egg-info/requires.txt +0 -33
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/LICENSE +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/README.md +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/__init__.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/agent.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/agent_cli_options_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/agent_package_cli.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/agent_package_cli_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/api_keys.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/api_keys_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/ask.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/ask_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/async_typer.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/async_typer_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/auth_async.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/auth_async_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/call.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/chatbot.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/cli_mcp.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/cli_secrets.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/codex.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/common_options.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/database.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/database_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/developer.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/developer_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/helper.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/helper_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/helpers.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/host.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/llm.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/llm_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/local_settings.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/local_settings_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/mailbot.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/mailbot_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/mailboxes.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/meeting_transcriber.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/memory.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/memory_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/messaging.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/multi.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/oauth2.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/oauth2_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/oci_archive.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/oci_archive_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/participant_token.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/port.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/port_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/process.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/process_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/queue.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/queue_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/registry.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/registry_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/room.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/room_connect.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/room_connect_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/room_services.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/rooms.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/rooms_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/routes.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/scheduled_tasks.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/scheduled_tasks_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/services.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/services_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/sessions.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/sessions_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/storage.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/storage_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/sync.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/sync_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/task_runner.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/task_runner_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/tui/__init__.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/tui/setup_splash_frames.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/voicebot.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/webhook.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/webserver.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/webserver_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/worker.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent/cli/worker_test.py +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent_cli.egg-info/dependency_links.txt +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent_cli.egg-info/entry_points.txt +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/meshagent_cli.egg-info/top_level.txt +0 -0
- {meshagent_cli-0.38.2 → meshagent_cli-0.38.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-cli
|
|
3
|
-
Version: 0.38.
|
|
3
|
+
Version: 0.38.4
|
|
4
4
|
Summary: CLI for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -19,26 +19,27 @@ Requires-Dist: pyyaml~=6.0.2
|
|
|
19
19
|
Requires-Dist: pydantic-yaml~=1.5
|
|
20
20
|
Requires-Dist: pathspec<2,>=1.0.3
|
|
21
21
|
Requires-Dist: zstandard~=0.25.0
|
|
22
|
-
Requires-Dist: meshagent-llm-proxy==0.38.
|
|
22
|
+
Requires-Dist: meshagent-llm-proxy==0.38.4
|
|
23
23
|
Requires-Dist: rich~=14.3.0
|
|
24
24
|
Requires-Dist: textual<9.0,>=8.2.3
|
|
25
25
|
Requires-Dist: prompt-toolkit~=3.0.52
|
|
26
26
|
Provides-Extra: all
|
|
27
|
-
Requires-Dist: meshagent-agents[all]==0.38.
|
|
28
|
-
Requires-Dist: meshagent-api[all]==0.38.
|
|
29
|
-
Requires-Dist: meshagent-computers==0.38.
|
|
30
|
-
Requires-Dist: meshagent-openai==0.38.
|
|
31
|
-
Requires-Dist: meshagent-anthropic==0.38.
|
|
32
|
-
Requires-Dist: meshagent-codex==0.38.
|
|
33
|
-
Requires-Dist: meshagent-
|
|
34
|
-
Requires-Dist: meshagent-
|
|
27
|
+
Requires-Dist: meshagent-agents[all]==0.38.4; extra == "all"
|
|
28
|
+
Requires-Dist: meshagent-api[all]==0.38.4; extra == "all"
|
|
29
|
+
Requires-Dist: meshagent-computers==0.38.4; extra == "all"
|
|
30
|
+
Requires-Dist: meshagent-openai==0.38.4; extra == "all"
|
|
31
|
+
Requires-Dist: meshagent-anthropic==0.38.4; extra == "all"
|
|
32
|
+
Requires-Dist: meshagent-codex==0.38.4; extra == "all"
|
|
33
|
+
Requires-Dist: meshagent-otel==0.38.4; extra == "all"
|
|
34
|
+
Requires-Dist: meshagent-mcp==0.38.4; extra == "all"
|
|
35
|
+
Requires-Dist: meshagent-tools==0.38.4; extra == "all"
|
|
35
36
|
Requires-Dist: supabase-auth~=2.28.0; extra == "all"
|
|
36
37
|
Requires-Dist: prompt-toolkit~=3.0.52; extra == "all"
|
|
37
38
|
Provides-Extra: mcp-service
|
|
38
|
-
Requires-Dist: meshagent-agents[all]==0.38.
|
|
39
|
-
Requires-Dist: meshagent-api==0.38.
|
|
40
|
-
Requires-Dist: meshagent-mcp==0.38.
|
|
41
|
-
Requires-Dist: meshagent-tools==0.38.
|
|
39
|
+
Requires-Dist: meshagent-agents[all]==0.38.4; extra == "mcp-service"
|
|
40
|
+
Requires-Dist: meshagent-api==0.38.4; extra == "mcp-service"
|
|
41
|
+
Requires-Dist: meshagent-mcp==0.38.4; extra == "mcp-service"
|
|
42
|
+
Requires-Dist: meshagent-tools==0.38.4; extra == "mcp-service"
|
|
42
43
|
Requires-Dist: supabase-auth~=2.28.0; extra == "mcp-service"
|
|
43
44
|
Dynamic: license-file
|
|
44
45
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import typer
|
|
2
3
|
from typing import Annotated
|
|
3
4
|
|
|
@@ -69,27 +70,62 @@ def _format_saved_profile(profile: SavedProfileRecord) -> str:
|
|
|
69
70
|
)
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
def _should_launch_switch_tui(
|
|
74
|
+
*,
|
|
75
|
+
profile: str | None,
|
|
76
|
+
stdin_is_tty: bool,
|
|
77
|
+
stdout_is_tty: bool,
|
|
78
|
+
) -> bool:
|
|
79
|
+
return profile is None and stdin_is_tty and stdout_is_tty
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def _run_auth_switch_tui(
|
|
83
|
+
*,
|
|
84
|
+
saved_profiles: list[SavedProfileRecord],
|
|
85
|
+
):
|
|
86
|
+
from meshagent.cli.tui.auth_switch import run_auth_switch_tui
|
|
87
|
+
|
|
88
|
+
return await run_auth_switch_tui(saved_profiles=saved_profiles)
|
|
89
|
+
|
|
90
|
+
|
|
72
91
|
@app.async_command("switch")
|
|
73
92
|
async def switch(
|
|
74
93
|
profile: Annotated[
|
|
75
94
|
str | None,
|
|
76
95
|
typer.Argument(
|
|
77
|
-
help=
|
|
96
|
+
help=(
|
|
97
|
+
"Saved profile user id or email. If omitted, saved profiles are "
|
|
98
|
+
"listed or an interactive picker is shown in a TTY."
|
|
99
|
+
),
|
|
78
100
|
),
|
|
79
101
|
] = None,
|
|
80
102
|
):
|
|
81
|
-
|
|
103
|
+
selected_profile_selector = profile
|
|
104
|
+
if selected_profile_selector is None:
|
|
82
105
|
saved_profiles = list_saved_profiles()
|
|
83
106
|
if len(saved_profiles) == 0:
|
|
84
107
|
typer.echo("No saved local profiles.")
|
|
85
108
|
return
|
|
86
109
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
if _should_launch_switch_tui(
|
|
111
|
+
profile=selected_profile_selector,
|
|
112
|
+
stdin_is_tty=sys.stdin.isatty(),
|
|
113
|
+
stdout_is_tty=sys.stdout.isatty(),
|
|
114
|
+
):
|
|
115
|
+
result = await _run_auth_switch_tui(saved_profiles=saved_profiles)
|
|
116
|
+
if result.status != "completed" or result.selected_profile is None:
|
|
117
|
+
if result.message is not None:
|
|
118
|
+
typer.echo(result.message)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
selected_profile_selector = result.selected_profile.user_id
|
|
122
|
+
else:
|
|
123
|
+
for saved_profile in saved_profiles:
|
|
124
|
+
typer.echo(_format_saved_profile(saved_profile))
|
|
125
|
+
return
|
|
90
126
|
|
|
91
127
|
try:
|
|
92
|
-
selected_profile = switch_active_profile(
|
|
128
|
+
selected_profile = switch_active_profile(selected_profile_selector)
|
|
93
129
|
except LookupError as exc:
|
|
94
130
|
typer.echo(str(exc))
|
|
95
131
|
raise typer.Exit(code=1) from exc
|
|
@@ -114,3 +150,13 @@ async def whoami():
|
|
|
114
150
|
await client.close()
|
|
115
151
|
|
|
116
152
|
typer.echo(_format_user_identity(profile))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.async_command("token")
|
|
156
|
+
async def token():
|
|
157
|
+
access_token = await auth_async.get_access_token()
|
|
158
|
+
if access_token is None:
|
|
159
|
+
typer.echo("Not logged in", err=True)
|
|
160
|
+
raise typer.Exit(code=1)
|
|
161
|
+
|
|
162
|
+
typer.echo(access_token)
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from meshagent.cli import auth
|
|
6
|
+
from meshagent.cli.local_settings import SavedProfileRecord, StoredUserProfile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _FakeClient:
|
|
10
|
+
def __init__(self, *, profile: dict[str, object]) -> None:
|
|
11
|
+
self._profile = profile
|
|
12
|
+
self.closed = False
|
|
13
|
+
|
|
14
|
+
async def get_user_profile(self, user_id: str) -> dict[str, object]:
|
|
15
|
+
assert user_id == "me"
|
|
16
|
+
return self._profile
|
|
17
|
+
|
|
18
|
+
async def close(self) -> None:
|
|
19
|
+
self.closed = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _FakeTTY:
|
|
23
|
+
def __init__(self, *, is_tty: bool) -> None:
|
|
24
|
+
self._is_tty = is_tty
|
|
25
|
+
|
|
26
|
+
def isatty(self) -> bool:
|
|
27
|
+
return self._is_tty
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_whoami_uses_user_profile(monkeypatch) -> None:
|
|
32
|
+
output: list[str] = []
|
|
33
|
+
client = _FakeClient(
|
|
34
|
+
profile={
|
|
35
|
+
"id": "user-123",
|
|
36
|
+
"first_name": "Jesse",
|
|
37
|
+
"last_name": "Ezell",
|
|
38
|
+
"email": "jesse@example.com",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def _fake_get_access_token() -> str | None:
|
|
43
|
+
return "oauth-token"
|
|
44
|
+
|
|
45
|
+
monkeypatch.setattr(auth.auth_async, "get_access_token", _fake_get_access_token)
|
|
46
|
+
monkeypatch.setattr(
|
|
47
|
+
auth,
|
|
48
|
+
"CustomMeshagentClient",
|
|
49
|
+
lambda *, base_url, token: client,
|
|
50
|
+
)
|
|
51
|
+
monkeypatch.setattr(auth.typer, "echo", output.append)
|
|
52
|
+
|
|
53
|
+
await auth.whoami()
|
|
54
|
+
|
|
55
|
+
assert output == ["Jesse Ezell <jesse@example.com>"]
|
|
56
|
+
assert client.closed is True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_whoami_prints_not_logged_in_without_identity(monkeypatch) -> None:
|
|
61
|
+
output: list[str] = []
|
|
62
|
+
|
|
63
|
+
async def _fake_get_access_token() -> str | None:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
monkeypatch.setattr(auth.auth_async, "get_access_token", _fake_get_access_token)
|
|
67
|
+
monkeypatch.setattr(auth.typer, "echo", output.append)
|
|
68
|
+
|
|
69
|
+
await auth.whoami()
|
|
70
|
+
|
|
71
|
+
assert output == ["Not logged in"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_token_prints_access_token(monkeypatch) -> None:
|
|
76
|
+
output: list[str] = []
|
|
77
|
+
|
|
78
|
+
async def _fake_get_access_token() -> str | None:
|
|
79
|
+
return "oauth-token"
|
|
80
|
+
|
|
81
|
+
monkeypatch.setattr(auth.auth_async, "get_access_token", _fake_get_access_token)
|
|
82
|
+
monkeypatch.setattr(auth.typer, "echo", output.append)
|
|
83
|
+
|
|
84
|
+
await auth.token()
|
|
85
|
+
|
|
86
|
+
assert output == ["oauth-token"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_token_exits_when_not_logged_in(monkeypatch) -> None:
|
|
91
|
+
output: list[tuple[str, bool]] = []
|
|
92
|
+
|
|
93
|
+
async def _fake_get_access_token() -> str | None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _fake_echo(message: str, *, err: bool = False) -> None:
|
|
97
|
+
output.append((message, err))
|
|
98
|
+
|
|
99
|
+
monkeypatch.setattr(auth.auth_async, "get_access_token", _fake_get_access_token)
|
|
100
|
+
monkeypatch.setattr(auth.typer, "echo", _fake_echo)
|
|
101
|
+
|
|
102
|
+
with pytest.raises(auth.typer.Exit) as exc_info:
|
|
103
|
+
await auth.token()
|
|
104
|
+
|
|
105
|
+
assert exc_info.value.exit_code == 1
|
|
106
|
+
assert output == [("Not logged in", True)]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_login_passes_api_url_to_auth_async(monkeypatch) -> None:
|
|
111
|
+
captured: dict[str, str | None] = {}
|
|
112
|
+
|
|
113
|
+
async def _fake_login(*, api_url: str | None = None) -> None:
|
|
114
|
+
captured["api_url"] = api_url
|
|
115
|
+
|
|
116
|
+
async def _fake_get_active_project() -> str | None:
|
|
117
|
+
return "project-123"
|
|
118
|
+
|
|
119
|
+
monkeypatch.setattr(auth.auth_async, "login", _fake_login)
|
|
120
|
+
monkeypatch.setattr(auth, "get_active_project", _fake_get_active_project)
|
|
121
|
+
|
|
122
|
+
await auth.login(api_url="https://override.meshagent.test")
|
|
123
|
+
|
|
124
|
+
assert captured == {"api_url": "https://override.meshagent.test"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_switch_lists_saved_profiles(monkeypatch) -> None:
|
|
129
|
+
output: list[str] = []
|
|
130
|
+
|
|
131
|
+
monkeypatch.setattr(
|
|
132
|
+
auth,
|
|
133
|
+
"list_saved_profiles",
|
|
134
|
+
lambda: [
|
|
135
|
+
SavedProfileRecord(
|
|
136
|
+
user_id="user-123",
|
|
137
|
+
profile=StoredUserProfile(
|
|
138
|
+
id="user-123",
|
|
139
|
+
first_name="Jesse",
|
|
140
|
+
last_name="Ezell",
|
|
141
|
+
email="jesse@example.com",
|
|
142
|
+
),
|
|
143
|
+
api_url="https://api.meshagent.test",
|
|
144
|
+
is_active=True,
|
|
145
|
+
)
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
monkeypatch.setattr(auth.sys, "stdin", _FakeTTY(is_tty=False))
|
|
149
|
+
monkeypatch.setattr(auth.sys, "stdout", _FakeTTY(is_tty=False))
|
|
150
|
+
monkeypatch.setattr(auth.typer, "echo", output.append)
|
|
151
|
+
|
|
152
|
+
await auth.switch()
|
|
153
|
+
|
|
154
|
+
assert output == [
|
|
155
|
+
"* Jesse Ezell [user-123] @ https://api.meshagent.test",
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_should_launch_switch_tui_only_when_selector_missing_in_tty() -> None:
|
|
160
|
+
assert (
|
|
161
|
+
auth._should_launch_switch_tui(
|
|
162
|
+
profile=None,
|
|
163
|
+
stdin_is_tty=True,
|
|
164
|
+
stdout_is_tty=True,
|
|
165
|
+
)
|
|
166
|
+
is True
|
|
167
|
+
)
|
|
168
|
+
assert (
|
|
169
|
+
auth._should_launch_switch_tui(
|
|
170
|
+
profile="user-123",
|
|
171
|
+
stdin_is_tty=True,
|
|
172
|
+
stdout_is_tty=True,
|
|
173
|
+
)
|
|
174
|
+
is False
|
|
175
|
+
)
|
|
176
|
+
assert (
|
|
177
|
+
auth._should_launch_switch_tui(
|
|
178
|
+
profile=None,
|
|
179
|
+
stdin_is_tty=False,
|
|
180
|
+
stdout_is_tty=True,
|
|
181
|
+
)
|
|
182
|
+
is False
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.asyncio
|
|
187
|
+
async def test_switch_launches_tui_when_no_selector_in_tty(monkeypatch) -> None:
|
|
188
|
+
output: list[str] = []
|
|
189
|
+
saved_profiles = [
|
|
190
|
+
SavedProfileRecord(
|
|
191
|
+
user_id="user-123",
|
|
192
|
+
profile=StoredUserProfile(
|
|
193
|
+
id="user-123",
|
|
194
|
+
first_name="Jesse",
|
|
195
|
+
last_name="Ezell",
|
|
196
|
+
email="jesse@example.com",
|
|
197
|
+
),
|
|
198
|
+
api_url="https://api.meshagent.test",
|
|
199
|
+
is_active=True,
|
|
200
|
+
),
|
|
201
|
+
SavedProfileRecord(
|
|
202
|
+
user_id="user-456",
|
|
203
|
+
profile=StoredUserProfile(
|
|
204
|
+
id="user-456",
|
|
205
|
+
first_name="Taylor",
|
|
206
|
+
last_name="Swift",
|
|
207
|
+
email="taylor@example.com",
|
|
208
|
+
),
|
|
209
|
+
api_url="https://api.meshagent.test",
|
|
210
|
+
is_active=False,
|
|
211
|
+
),
|
|
212
|
+
]
|
|
213
|
+
selected_selectors: list[str] = []
|
|
214
|
+
|
|
215
|
+
async def _fake_run_auth_switch_tui(*, saved_profiles):
|
|
216
|
+
return SimpleNamespace(
|
|
217
|
+
status="completed",
|
|
218
|
+
message=None,
|
|
219
|
+
selected_profile=saved_profiles[1],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
monkeypatch.setattr(auth, "list_saved_profiles", lambda: saved_profiles)
|
|
223
|
+
monkeypatch.setattr(auth.sys, "stdin", _FakeTTY(is_tty=True))
|
|
224
|
+
monkeypatch.setattr(auth.sys, "stdout", _FakeTTY(is_tty=True))
|
|
225
|
+
monkeypatch.setattr(
|
|
226
|
+
auth,
|
|
227
|
+
"_run_auth_switch_tui",
|
|
228
|
+
_fake_run_auth_switch_tui,
|
|
229
|
+
)
|
|
230
|
+
monkeypatch.setattr(
|
|
231
|
+
auth,
|
|
232
|
+
"switch_active_profile",
|
|
233
|
+
lambda selector: (
|
|
234
|
+
selected_selectors.append(selector)
|
|
235
|
+
or SavedProfileRecord(
|
|
236
|
+
user_id="user-456",
|
|
237
|
+
profile=StoredUserProfile(
|
|
238
|
+
id="user-456",
|
|
239
|
+
first_name="Taylor",
|
|
240
|
+
last_name="Swift",
|
|
241
|
+
email="taylor@example.com",
|
|
242
|
+
),
|
|
243
|
+
api_url="https://api.meshagent.test",
|
|
244
|
+
is_active=True,
|
|
245
|
+
)
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
monkeypatch.setattr(auth.typer, "echo", output.append)
|
|
249
|
+
|
|
250
|
+
await auth.switch()
|
|
251
|
+
|
|
252
|
+
assert selected_selectors == ["user-456"]
|
|
253
|
+
assert output == [
|
|
254
|
+
"Active profile: * Taylor Swift [user-456] @ https://api.meshagent.test",
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_switch_prints_cancel_message_when_tui_is_canceled(monkeypatch) -> None:
|
|
260
|
+
output: list[str] = []
|
|
261
|
+
switch_attempted = False
|
|
262
|
+
|
|
263
|
+
monkeypatch.setattr(
|
|
264
|
+
auth,
|
|
265
|
+
"list_saved_profiles",
|
|
266
|
+
lambda: [
|
|
267
|
+
SavedProfileRecord(
|
|
268
|
+
user_id="user-123",
|
|
269
|
+
profile=StoredUserProfile(
|
|
270
|
+
id="user-123",
|
|
271
|
+
first_name="Jesse",
|
|
272
|
+
last_name="Ezell",
|
|
273
|
+
email="jesse@example.com",
|
|
274
|
+
),
|
|
275
|
+
api_url="https://api.meshagent.test",
|
|
276
|
+
is_active=True,
|
|
277
|
+
)
|
|
278
|
+
],
|
|
279
|
+
)
|
|
280
|
+
monkeypatch.setattr(auth.sys, "stdin", _FakeTTY(is_tty=True))
|
|
281
|
+
monkeypatch.setattr(auth.sys, "stdout", _FakeTTY(is_tty=True))
|
|
282
|
+
|
|
283
|
+
async def _fake_run_auth_switch_tui(*, saved_profiles):
|
|
284
|
+
del saved_profiles
|
|
285
|
+
return SimpleNamespace(
|
|
286
|
+
status="canceled",
|
|
287
|
+
message="Profile switch canceled.",
|
|
288
|
+
selected_profile=None,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
monkeypatch.setattr(
|
|
292
|
+
auth,
|
|
293
|
+
"_run_auth_switch_tui",
|
|
294
|
+
_fake_run_auth_switch_tui,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _unexpected_switch(selector: str):
|
|
298
|
+
del selector
|
|
299
|
+
nonlocal switch_attempted
|
|
300
|
+
switch_attempted = True
|
|
301
|
+
raise AssertionError("switch_active_profile should not be called")
|
|
302
|
+
|
|
303
|
+
monkeypatch.setattr(auth, "switch_active_profile", _unexpected_switch)
|
|
304
|
+
monkeypatch.setattr(auth.typer, "echo", output.append)
|
|
305
|
+
|
|
306
|
+
await auth.switch()
|
|
307
|
+
|
|
308
|
+
assert switch_attempted is False
|
|
309
|
+
assert output == ["Profile switch canceled."]
|
|
@@ -97,6 +97,12 @@ app.add_lazy_command(
|
|
|
97
97
|
help="Send a one-shot prompt through the LLM router",
|
|
98
98
|
attribute="ask_command",
|
|
99
99
|
)
|
|
100
|
+
app.add_lazy_command(
|
|
101
|
+
name="launch",
|
|
102
|
+
module="meshagent.cli.launch",
|
|
103
|
+
attribute="launch_group",
|
|
104
|
+
help="Launch CLI apps through MeshAgent",
|
|
105
|
+
)
|
|
100
106
|
app.add_lazy_command(
|
|
101
107
|
name="token",
|
|
102
108
|
module="meshagent.cli.participant_token",
|
|
@@ -43,6 +43,7 @@ def test_root_help_hides_legacy_command_namespaces() -> None:
|
|
|
43
43
|
assert result.exit_code == 0
|
|
44
44
|
assert "│ build" in result.output
|
|
45
45
|
assert "│ deploy" in result.output
|
|
46
|
+
assert "│ launch" in result.output
|
|
46
47
|
assert "│ room" in result.output
|
|
47
48
|
assert "│ call" not in result.output
|
|
48
49
|
assert "│ package" not in result.output
|
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
import re
|
|
8
8
|
import tarfile
|
|
9
9
|
import time
|
|
10
|
+
from datetime import datetime
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
import pathlib
|
|
@@ -16,6 +17,8 @@ import aiofiles
|
|
|
16
17
|
import aiofiles.ospath
|
|
17
18
|
import typer
|
|
18
19
|
from rich import print
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.table import Table
|
|
19
22
|
from typing import Annotated, Optional, List, Dict
|
|
20
23
|
|
|
21
24
|
from meshagent.cli import async_typer
|
|
@@ -33,6 +36,7 @@ from meshagent.api import (
|
|
|
33
36
|
)
|
|
34
37
|
from meshagent.api.room_server_client import (
|
|
35
38
|
DockerSecret,
|
|
39
|
+
ImageDescriptor,
|
|
36
40
|
)
|
|
37
41
|
from meshagent.api.specs.service import (
|
|
38
42
|
ContainerMountSpec,
|
|
@@ -163,6 +167,52 @@ def _parse_image_operation_mounts(
|
|
|
163
167
|
return mount_spec
|
|
164
168
|
|
|
165
169
|
|
|
170
|
+
def _format_image_timestamp(value: datetime | None) -> str:
|
|
171
|
+
if value is None:
|
|
172
|
+
return "—"
|
|
173
|
+
return value.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _format_image_bytes(value: int | None) -> str:
|
|
177
|
+
if value is None:
|
|
178
|
+
return "—"
|
|
179
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
180
|
+
size = float(value)
|
|
181
|
+
unit_index = 0
|
|
182
|
+
while size >= 1024 and unit_index < len(units) - 1:
|
|
183
|
+
size /= 1024
|
|
184
|
+
unit_index += 1
|
|
185
|
+
if unit_index == 0:
|
|
186
|
+
return f"{int(size)} {units[unit_index]}"
|
|
187
|
+
return f"{size:.1f} {units[unit_index]}"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _image_reference(image) -> str:
|
|
191
|
+
if image.preferred_ref:
|
|
192
|
+
return image.preferred_ref
|
|
193
|
+
if len(image.references) > 0:
|
|
194
|
+
return image.references[0]
|
|
195
|
+
return image.id
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _descriptor_table(*, title: str, descriptor: ImageDescriptor | None) -> Table:
|
|
199
|
+
table = Table(title=title, show_header=True, header_style="bold magenta")
|
|
200
|
+
table.add_column("Field", style="cyan")
|
|
201
|
+
table.add_column("Value")
|
|
202
|
+
if descriptor is None:
|
|
203
|
+
table.add_row("descriptor", "—")
|
|
204
|
+
return table
|
|
205
|
+
table.add_row("digest", descriptor.digest)
|
|
206
|
+
table.add_row("media_type", descriptor.media_type or "—")
|
|
207
|
+
table.add_row("size", _format_image_bytes(descriptor.size))
|
|
208
|
+
if len(descriptor.annotations) == 0:
|
|
209
|
+
table.add_row("annotations", "—")
|
|
210
|
+
else:
|
|
211
|
+
for key, value in descriptor.annotations.items():
|
|
212
|
+
table.add_row(f"annotation:{key}", value)
|
|
213
|
+
return table
|
|
214
|
+
|
|
215
|
+
|
|
166
216
|
async def _stream_container_job_logs_and_wait_for_exit(
|
|
167
217
|
*,
|
|
168
218
|
client: RoomClient,
|
|
@@ -774,6 +824,10 @@ async def images_list(
|
|
|
774
824
|
*,
|
|
775
825
|
project_id: ProjectIdOption,
|
|
776
826
|
room: RoomOption,
|
|
827
|
+
output: Annotated[
|
|
828
|
+
str,
|
|
829
|
+
typer.Option(help="table | json"),
|
|
830
|
+
] = "table",
|
|
777
831
|
):
|
|
778
832
|
account_client, client = await _with_client(
|
|
779
833
|
project_id=project_id,
|
|
@@ -781,7 +835,144 @@ async def images_list(
|
|
|
781
835
|
)
|
|
782
836
|
try:
|
|
783
837
|
imgs = await client.containers.list_images()
|
|
784
|
-
|
|
838
|
+
if output == "json":
|
|
839
|
+
print([i.model_dump(mode="json") for i in imgs])
|
|
840
|
+
return
|
|
841
|
+
|
|
842
|
+
table = Table(title="Images")
|
|
843
|
+
table.add_column("Reference", style="cyan")
|
|
844
|
+
table.add_column("ID")
|
|
845
|
+
table.add_column("Updated")
|
|
846
|
+
table.add_column("Created")
|
|
847
|
+
table.add_column("Media Type")
|
|
848
|
+
for image in imgs:
|
|
849
|
+
table.add_row(
|
|
850
|
+
_image_reference(image),
|
|
851
|
+
image.id,
|
|
852
|
+
_format_image_timestamp(image.updated_at),
|
|
853
|
+
_format_image_timestamp(image.created_at),
|
|
854
|
+
image.target_media_type or "—",
|
|
855
|
+
)
|
|
856
|
+
Console().print(table)
|
|
857
|
+
finally:
|
|
858
|
+
await client.__aexit__(None, None, None)
|
|
859
|
+
await account_client.close()
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
@images_app.async_command(
|
|
863
|
+
"inspect",
|
|
864
|
+
help="Inspect a container image in a room by image ID.",
|
|
865
|
+
)
|
|
866
|
+
async def images_inspect(
|
|
867
|
+
*,
|
|
868
|
+
project_id: ProjectIdOption,
|
|
869
|
+
room: RoomOption,
|
|
870
|
+
image_id: Annotated[
|
|
871
|
+
str,
|
|
872
|
+
typer.Option(..., "--image-id", help="Image ID from `meshagent images list`"),
|
|
873
|
+
],
|
|
874
|
+
):
|
|
875
|
+
account_client, client = await _with_client(
|
|
876
|
+
project_id=project_id,
|
|
877
|
+
room=room,
|
|
878
|
+
)
|
|
879
|
+
try:
|
|
880
|
+
inspection = await client.containers.inspect_image(image_id=image_id)
|
|
881
|
+
console = Console()
|
|
882
|
+
|
|
883
|
+
summary_table = Table(
|
|
884
|
+
title="Image", show_header=True, header_style="bold magenta"
|
|
885
|
+
)
|
|
886
|
+
summary_table.add_column("Field", style="cyan")
|
|
887
|
+
summary_table.add_column("Value")
|
|
888
|
+
summary_table.add_row("reference", _image_reference(inspection.image))
|
|
889
|
+
summary_table.add_row("id", inspection.image.id)
|
|
890
|
+
summary_table.add_row(
|
|
891
|
+
"references",
|
|
892
|
+
", ".join(inspection.image.references)
|
|
893
|
+
if inspection.image.references
|
|
894
|
+
else "—",
|
|
895
|
+
)
|
|
896
|
+
summary_table.add_row(
|
|
897
|
+
"created_at",
|
|
898
|
+
_format_image_timestamp(inspection.image.created_at),
|
|
899
|
+
)
|
|
900
|
+
summary_table.add_row(
|
|
901
|
+
"updated_at",
|
|
902
|
+
_format_image_timestamp(inspection.image.updated_at),
|
|
903
|
+
)
|
|
904
|
+
summary_table.add_row(
|
|
905
|
+
"target_media_type",
|
|
906
|
+
inspection.image.target_media_type or "—",
|
|
907
|
+
)
|
|
908
|
+
summary_table.add_row(
|
|
909
|
+
"content_size",
|
|
910
|
+
_format_image_bytes(inspection.content_size),
|
|
911
|
+
)
|
|
912
|
+
if len(inspection.image.labels) == 0:
|
|
913
|
+
summary_table.add_row("labels", "—")
|
|
914
|
+
else:
|
|
915
|
+
for key, value in inspection.image.labels.items():
|
|
916
|
+
summary_table.add_row(f"label:{key}", value)
|
|
917
|
+
|
|
918
|
+
manifests_table = Table(
|
|
919
|
+
title="Manifests",
|
|
920
|
+
show_header=True,
|
|
921
|
+
header_style="bold magenta",
|
|
922
|
+
)
|
|
923
|
+
manifests_table.add_column("Digest", style="cyan")
|
|
924
|
+
manifests_table.add_column("Media Type")
|
|
925
|
+
manifests_table.add_column("Size")
|
|
926
|
+
manifests_table.add_column("Platform")
|
|
927
|
+
if len(inspection.manifests) == 0:
|
|
928
|
+
manifests_table.add_row("—", "—", "—", "—")
|
|
929
|
+
else:
|
|
930
|
+
for manifest in inspection.manifests:
|
|
931
|
+
platform = "/".join(
|
|
932
|
+
part
|
|
933
|
+
for part in (
|
|
934
|
+
manifest.platform_os,
|
|
935
|
+
manifest.platform_architecture,
|
|
936
|
+
manifest.platform_variant,
|
|
937
|
+
)
|
|
938
|
+
if part
|
|
939
|
+
)
|
|
940
|
+
manifests_table.add_row(
|
|
941
|
+
manifest.descriptor.digest,
|
|
942
|
+
manifest.descriptor.media_type or "—",
|
|
943
|
+
_format_image_bytes(manifest.descriptor.size),
|
|
944
|
+
platform or "—",
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
layers_table = Table(
|
|
948
|
+
title="Layers",
|
|
949
|
+
show_header=True,
|
|
950
|
+
header_style="bold magenta",
|
|
951
|
+
)
|
|
952
|
+
layers_table.add_column("Digest", style="cyan")
|
|
953
|
+
layers_table.add_column("Media Type")
|
|
954
|
+
layers_table.add_column("Size")
|
|
955
|
+
if len(inspection.layers) == 0:
|
|
956
|
+
layers_table.add_row("—", "—", "—")
|
|
957
|
+
else:
|
|
958
|
+
for layer in inspection.layers:
|
|
959
|
+
layers_table.add_row(
|
|
960
|
+
layer.digest,
|
|
961
|
+
layer.media_type or "—",
|
|
962
|
+
_format_image_bytes(layer.size),
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
console.print(summary_table)
|
|
966
|
+
console.print(_descriptor_table(title="Target", descriptor=inspection.target))
|
|
967
|
+
console.print(
|
|
968
|
+
_descriptor_table(
|
|
969
|
+
title="Selected Manifest",
|
|
970
|
+
descriptor=inspection.selected_manifest,
|
|
971
|
+
)
|
|
972
|
+
)
|
|
973
|
+
console.print(_descriptor_table(title="Config", descriptor=inspection.config))
|
|
974
|
+
console.print(manifests_table)
|
|
975
|
+
console.print(layers_table)
|
|
785
976
|
finally:
|
|
786
977
|
await client.__aexit__(None, None, None)
|
|
787
978
|
await account_client.close()
|