meshagent-cli 0.0.17__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/__init__.py +0 -0
- meshagent/cli/agent.py +259 -0
- meshagent/cli/api_keys.py +74 -0
- meshagent/cli/async_typer.py +31 -0
- meshagent/cli/auth.py +28 -0
- meshagent/cli/auth_async.py +115 -0
- meshagent/cli/call.py +127 -0
- meshagent/cli/cli.py +35 -0
- meshagent/cli/cli_mcp.py +113 -0
- meshagent/cli/cli_secrets.py +383 -0
- meshagent/cli/developer.py +76 -0
- meshagent/cli/helper.py +113 -0
- meshagent/cli/messaging.py +192 -0
- meshagent/cli/participant_token.py +33 -0
- meshagent/cli/projects.py +31 -0
- meshagent/cli/services.py +177 -0
- meshagent/cli/sessions.py +19 -0
- meshagent/cli/storage.py +801 -0
- meshagent/cli/version.py +1 -0
- meshagent/cli/webhook.py +89 -0
- meshagent_cli-0.0.17.dist-info/METADATA +23 -0
- meshagent_cli-0.0.17.dist-info/RECORD +25 -0
- meshagent_cli-0.0.17.dist-info/WHEEL +5 -0
- meshagent_cli-0.0.17.dist-info/entry_points.txt +2 -0
- meshagent_cli-0.0.17.dist-info/top_level.txt +1 -0
meshagent/cli/cli_mcp.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
from mcp.client.session import ClientSession
|
|
3
|
+
from mcp.client.sse import sse_client
|
|
4
|
+
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
5
|
+
|
|
6
|
+
from meshagent.mcp import MCPToolkit
|
|
7
|
+
|
|
8
|
+
from meshagent.cli import async_typer
|
|
9
|
+
import typer
|
|
10
|
+
from meshagent.cli.helper import get_client, print_json_table, set_active_project, resolve_project_id
|
|
11
|
+
from rich import print
|
|
12
|
+
from meshagent.api import RoomClient, ParticipantToken, WebSocketClientProtocol, RoomException
|
|
13
|
+
from meshagent.cli.helper import set_active_project, get_active_project, resolve_project_id, resolve_api_key
|
|
14
|
+
from typing import Annotated, Optional
|
|
15
|
+
from meshagent.api.helpers import meshagent_base_url, websocket_room_url
|
|
16
|
+
|
|
17
|
+
from meshagent.api.services import send_webhook
|
|
18
|
+
from meshagent.tools.hosting import RemoteToolkit
|
|
19
|
+
import shlex
|
|
20
|
+
|
|
21
|
+
app = async_typer.AsyncTyper()
|
|
22
|
+
|
|
23
|
+
@app.async_command("sse")
|
|
24
|
+
async def sse(*, project_id: str = None, room: Annotated[str, typer.Option()], api_key_id: Annotated[Optional[str], typer.Option()] = None, name: Annotated[str, typer.Option(..., help="Participant name")] = "cli", role: str = "tool", url: Annotated[str, typer.Option()]):
|
|
25
|
+
account_client = await get_client()
|
|
26
|
+
try:
|
|
27
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
28
|
+
api_key_id = await resolve_api_key(project_id, api_key_id)
|
|
29
|
+
|
|
30
|
+
key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
|
|
31
|
+
|
|
32
|
+
token = ParticipantToken(
|
|
33
|
+
name=name,
|
|
34
|
+
project_id=project_id,
|
|
35
|
+
api_key_id=api_key_id
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
token.add_role_grant(role=role)
|
|
39
|
+
token.add_room_grant(room)
|
|
40
|
+
|
|
41
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
42
|
+
async with RoomClient(protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()), token=token.to_jwt(token=key))) as client:
|
|
43
|
+
|
|
44
|
+
async with sse_client(url) as (read_stream, write_stream):
|
|
45
|
+
|
|
46
|
+
async with ClientSession(read_stream=read_stream, write_stream=write_stream) as session:
|
|
47
|
+
|
|
48
|
+
mcp_tools_response = await session.list_tools()
|
|
49
|
+
|
|
50
|
+
toolkit = MCPToolkit(name=name, session=session, tools=mcp_tools_response.tools)
|
|
51
|
+
|
|
52
|
+
remote_toolkit = RemoteToolkit(name=toolkit.name, tools=toolkit.tools, title=toolkit.title, description=toolkit.description)
|
|
53
|
+
|
|
54
|
+
await remote_toolkit.start(room=client)
|
|
55
|
+
try:
|
|
56
|
+
await client.protocol.wait_for_close()
|
|
57
|
+
except KeyboardInterrupt:
|
|
58
|
+
await remote_toolkit.stop()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
except RoomException as e:
|
|
62
|
+
print(f"[red]{e}[/red]")
|
|
63
|
+
finally:
|
|
64
|
+
await account_client.close()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.async_command("stdio")
|
|
69
|
+
async def stdio(*, project_id: str = None, room: Annotated[str, typer.Option()], api_key_id: Annotated[Optional[str], typer.Option()] = None, name: Annotated[str, typer.Option(..., help="Participant name")] = "cli", role: str = "tool", command: Annotated[str, typer.Option()], args: Annotated[str, typer.Option()]):
|
|
70
|
+
account_client = await get_client()
|
|
71
|
+
try:
|
|
72
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
73
|
+
api_key_id = await resolve_api_key(project_id, api_key_id)
|
|
74
|
+
|
|
75
|
+
key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
|
|
76
|
+
|
|
77
|
+
token = ParticipantToken(
|
|
78
|
+
name=name,
|
|
79
|
+
project_id=project_id,
|
|
80
|
+
api_key_id=api_key_id
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
token.add_role_grant(role=role)
|
|
84
|
+
token.add_room_grant(room)
|
|
85
|
+
|
|
86
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
87
|
+
async with RoomClient(protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()), token=token.to_jwt(token=key))) as client:
|
|
88
|
+
|
|
89
|
+
async with stdio_client(StdioServerParameters(
|
|
90
|
+
command=command, # Executable
|
|
91
|
+
args=shlex.split(args), # Optional command line arguments
|
|
92
|
+
env=None, # Optional environment variables
|
|
93
|
+
)) as (read_stream, write_stream):
|
|
94
|
+
|
|
95
|
+
async with ClientSession(read_stream=read_stream, write_stream=write_stream) as session:
|
|
96
|
+
|
|
97
|
+
mcp_tools_response = await session.list_tools()
|
|
98
|
+
|
|
99
|
+
toolkit = MCPToolkit(name=name, session=session, tools=mcp_tools_response.tools)
|
|
100
|
+
|
|
101
|
+
remote_toolkit = RemoteToolkit(name=toolkit.name, tools=toolkit.tools, title=toolkit.title, description=toolkit.description)
|
|
102
|
+
|
|
103
|
+
await remote_toolkit.start(room=client)
|
|
104
|
+
try:
|
|
105
|
+
await client.protocol.wait_for_close()
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
await remote_toolkit.stop()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
except RoomException as e:
|
|
111
|
+
print(f"[red]{e}[/red]")
|
|
112
|
+
finally:
|
|
113
|
+
await account_client.close()
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Imports
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
from meshagent.cli import async_typer
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print
|
|
7
|
+
from meshagent.cli.helper import get_client, print_json_table, resolve_project_id
|
|
8
|
+
from typing import Annotated, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from meshagent.api.accounts_client import PullSecret, KeysSecret, SecretLike # or wherever you defined them
|
|
11
|
+
|
|
12
|
+
# --------------------------------------------------------------------------
|
|
13
|
+
# App Definition
|
|
14
|
+
# --------------------------------------------------------------------------
|
|
15
|
+
secrets_app = async_typer.AsyncTyper(help="Manage secrets for your project.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# --------------------------------------------------------------------------
|
|
19
|
+
# Utility helpers
|
|
20
|
+
# --------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def _parse_kv_inline(source: str | None) -> Dict[str, str]:
|
|
23
|
+
"""
|
|
24
|
+
Parse a space-separated list of `key=value` tokens into a dict.
|
|
25
|
+
"""
|
|
26
|
+
if source is None:
|
|
27
|
+
return {}
|
|
28
|
+
tokens = source.strip().split()
|
|
29
|
+
kv: Dict[str, str] = {}
|
|
30
|
+
for t in tokens:
|
|
31
|
+
if "=" not in t:
|
|
32
|
+
raise typer.BadParameter(f"Expected key=value, got '{t}'")
|
|
33
|
+
k, v = t.split("=", 1)
|
|
34
|
+
kv[k] = v
|
|
35
|
+
return kv
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --------------------------------------------------------------------------
|
|
39
|
+
# Subcommand group: "keys"
|
|
40
|
+
# e.g.: meshagent secrets keys create --name <NAME> --data ...
|
|
41
|
+
# --------------------------------------------------------------------------
|
|
42
|
+
keys_app = async_typer.AsyncTyper(help="Create or update environment-based key-value secrets.")
|
|
43
|
+
|
|
44
|
+
@keys_app.async_command("create")
|
|
45
|
+
async def create_keys_secret(
|
|
46
|
+
*,
|
|
47
|
+
project_id: Optional[str] = typer.Option(None),
|
|
48
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
49
|
+
data: Annotated[
|
|
50
|
+
str,
|
|
51
|
+
typer.Option(
|
|
52
|
+
"--data",
|
|
53
|
+
help="Format: key=value key2=value (space-separated)",
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Create a new 'keys' secret (opaque env-vars).
|
|
59
|
+
"""
|
|
60
|
+
client = await get_client()
|
|
61
|
+
try:
|
|
62
|
+
project_id = await resolve_project_id(project_id)
|
|
63
|
+
data_dict = _parse_kv_inline(data)
|
|
64
|
+
|
|
65
|
+
secret_obj = KeysSecret(
|
|
66
|
+
id="",
|
|
67
|
+
name=name,
|
|
68
|
+
data=data_dict,
|
|
69
|
+
)
|
|
70
|
+
secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
|
|
71
|
+
print(f"[green]Created keys secret:[/] {secret_id}")
|
|
72
|
+
|
|
73
|
+
finally:
|
|
74
|
+
await client.close()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@keys_app.async_command("update")
|
|
78
|
+
async def update_keys_secret(
|
|
79
|
+
*,
|
|
80
|
+
project_id: Optional[str] = typer.Option(None),
|
|
81
|
+
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
82
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
83
|
+
data: Annotated[
|
|
84
|
+
str,
|
|
85
|
+
typer.Option(
|
|
86
|
+
"--data",
|
|
87
|
+
help="Format: key=value key2=value (space-separated)",
|
|
88
|
+
),
|
|
89
|
+
]
|
|
90
|
+
):
|
|
91
|
+
"""
|
|
92
|
+
Update an existing 'keys' secret (opaque env-vars).
|
|
93
|
+
"""
|
|
94
|
+
client = await get_client()
|
|
95
|
+
try:
|
|
96
|
+
project_id = await resolve_project_id(project_id)
|
|
97
|
+
data_dict = _parse_kv_inline(data)
|
|
98
|
+
|
|
99
|
+
secret_obj = KeysSecret(
|
|
100
|
+
id=secret_id,
|
|
101
|
+
name=name,
|
|
102
|
+
data=data_dict,
|
|
103
|
+
)
|
|
104
|
+
await client.update_secret(project_id=project_id, secret=secret_obj)
|
|
105
|
+
print(f"[green]Keys secret {secret_id} updated.[/]")
|
|
106
|
+
finally:
|
|
107
|
+
await client.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
# Subcommand group: "docker"
|
|
112
|
+
# e.g.: meshagent secrets docker create --name myregistry --server ...
|
|
113
|
+
# --------------------------------------------------------------------------
|
|
114
|
+
docker_app = async_typer.AsyncTyper(help="Create or update a Docker registry pull secret.")
|
|
115
|
+
|
|
116
|
+
@docker_app.async_command("create")
|
|
117
|
+
async def create_docker_secret(
|
|
118
|
+
*,
|
|
119
|
+
project_id: Optional[str] = typer.Option(None),
|
|
120
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
121
|
+
server: Annotated[str, typer.Option(help="Docker registry server, e.g. index.docker.io")],
|
|
122
|
+
username: Annotated[str, typer.Option(help="Registry user name")],
|
|
123
|
+
password: Annotated[str, typer.Option(help="Registry password")],
|
|
124
|
+
email: Annotated[
|
|
125
|
+
str,
|
|
126
|
+
typer.Option("--email", help="User email for Docker config", show_default=False)
|
|
127
|
+
] = "none@example.com"
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Create a new Docker pull secret (generic).
|
|
131
|
+
"""
|
|
132
|
+
client = await get_client()
|
|
133
|
+
try:
|
|
134
|
+
project_id = await resolve_project_id(project_id)
|
|
135
|
+
|
|
136
|
+
secret_obj = PullSecret(
|
|
137
|
+
id="",
|
|
138
|
+
name=name,
|
|
139
|
+
server=server,
|
|
140
|
+
username=username,
|
|
141
|
+
password=password,
|
|
142
|
+
email=email,
|
|
143
|
+
)
|
|
144
|
+
secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
|
|
145
|
+
print(f"[green]Created Docker pull secret:[/] {secret_id}")
|
|
146
|
+
finally:
|
|
147
|
+
await client.close()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@docker_app.async_command("update")
|
|
151
|
+
async def update_docker_secret(
|
|
152
|
+
*,
|
|
153
|
+
project_id: Optional[str] = typer.Option(None),
|
|
154
|
+
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
155
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
156
|
+
server: Annotated[str, typer.Option(help="Docker registry server")],
|
|
157
|
+
username: Annotated[str, typer.Option(help="Registry user name")],
|
|
158
|
+
password: Annotated[str, typer.Option(help="Registry password")],
|
|
159
|
+
email: Annotated[
|
|
160
|
+
str,
|
|
161
|
+
typer.Option("--email", help="User email for Docker config", show_default=False)
|
|
162
|
+
] = "none@example.com"
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
Update an existing Docker pull secret (generic).
|
|
166
|
+
"""
|
|
167
|
+
client = await get_client()
|
|
168
|
+
try:
|
|
169
|
+
project_id = await resolve_project_id(project_id)
|
|
170
|
+
secret_obj = PullSecret(
|
|
171
|
+
id=secret_id,
|
|
172
|
+
name=name,
|
|
173
|
+
server=server,
|
|
174
|
+
username=username,
|
|
175
|
+
password=password,
|
|
176
|
+
email=email,
|
|
177
|
+
)
|
|
178
|
+
await client.update_secret(project_id=project_id, secret=secret_obj)
|
|
179
|
+
print(f"[green]Docker pull secret {secret_id} updated.[/]")
|
|
180
|
+
finally:
|
|
181
|
+
await client.close()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --------------------------------------------------------------------------
|
|
185
|
+
# Subcommand group: "acr"
|
|
186
|
+
# e.g.: meshagent secrets acr create --name <NAME> --server <REG>.azurecr.io ...
|
|
187
|
+
# --------------------------------------------------------------------------
|
|
188
|
+
acr_app = async_typer.AsyncTyper(help="Create or update an Azure Container Registry pull secret.")
|
|
189
|
+
|
|
190
|
+
@acr_app.async_command("create")
|
|
191
|
+
async def create_acr_secret(
|
|
192
|
+
*,
|
|
193
|
+
project_id: Optional[str] = typer.Option(None),
|
|
194
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
195
|
+
server: Annotated[str, typer.Option(help="ACR server, e.g. myregistry.azurecr.io")],
|
|
196
|
+
username: Annotated[str, typer.Option(help="Service principal ID")],
|
|
197
|
+
password: Annotated[str, typer.Option(help="Service principal secret/password")]
|
|
198
|
+
):
|
|
199
|
+
"""
|
|
200
|
+
Create a new ACR pull secret (defaults email to 'none@microsoft.com').
|
|
201
|
+
"""
|
|
202
|
+
client = await get_client()
|
|
203
|
+
try:
|
|
204
|
+
project_id = await resolve_project_id(project_id)
|
|
205
|
+
|
|
206
|
+
secret_obj = PullSecret(
|
|
207
|
+
id="",
|
|
208
|
+
name=name,
|
|
209
|
+
server=server,
|
|
210
|
+
username=username,
|
|
211
|
+
password=password,
|
|
212
|
+
email="none@microsoft.com", # Set a default for ACR usage
|
|
213
|
+
)
|
|
214
|
+
secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
|
|
215
|
+
print(f"[green]Created ACR pull secret:[/] {secret_id}")
|
|
216
|
+
finally:
|
|
217
|
+
await client.close()
|
|
218
|
+
|
|
219
|
+
@acr_app.async_command("update")
|
|
220
|
+
async def update_acr_secret(
|
|
221
|
+
*,
|
|
222
|
+
project_id: Optional[str] = typer.Option(None),
|
|
223
|
+
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
224
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
225
|
+
server: Annotated[str, typer.Option(help="ACR server, e.g. myregistry.azurecr.io")],
|
|
226
|
+
username: Annotated[str, typer.Option(help="Service principal ID")],
|
|
227
|
+
password: Annotated[str, typer.Option(help="Service principal secret/password")]
|
|
228
|
+
):
|
|
229
|
+
"""
|
|
230
|
+
Update an existing ACR pull secret (defaults email to 'none@microsoft.com').
|
|
231
|
+
"""
|
|
232
|
+
client = await get_client()
|
|
233
|
+
try:
|
|
234
|
+
project_id = await resolve_project_id(project_id)
|
|
235
|
+
secret_obj = PullSecret(
|
|
236
|
+
id=secret_id,
|
|
237
|
+
name=name,
|
|
238
|
+
server=server,
|
|
239
|
+
username=username,
|
|
240
|
+
password=password,
|
|
241
|
+
email="none@microsoft.com",
|
|
242
|
+
)
|
|
243
|
+
await client.update_secret(project_id=project_id, secret=secret_obj)
|
|
244
|
+
print(f"[green]ACR pull secret {secret_id} updated.[/]")
|
|
245
|
+
finally:
|
|
246
|
+
await client.close()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# --------------------------------------------------------------------------
|
|
250
|
+
# Subcommand group: "gar"
|
|
251
|
+
# e.g.: meshagent secrets gar create --name <NAME> --server ...
|
|
252
|
+
# (Typically sets email='none@google.com' and username='_json_key')
|
|
253
|
+
# --------------------------------------------------------------------------
|
|
254
|
+
gar_app = async_typer.AsyncTyper(help="Create or update a Google Artifact Registry pull secret.")
|
|
255
|
+
|
|
256
|
+
@gar_app.async_command("create")
|
|
257
|
+
async def create_gar_secret(
|
|
258
|
+
*,
|
|
259
|
+
project_id: Optional[str] = typer.Option(None),
|
|
260
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
261
|
+
server: Annotated[str, typer.Option(help="GAR host, e.g. us-west1-docker.pkg.dev")],
|
|
262
|
+
json_key: Annotated[str, typer.Option(help="Entire GCP service account JSON as string")]
|
|
263
|
+
):
|
|
264
|
+
"""
|
|
265
|
+
Create a new Google Artifact Registry pull secret.
|
|
266
|
+
|
|
267
|
+
By default, sets email='none@google.com', username='_json_key'
|
|
268
|
+
"""
|
|
269
|
+
client = await get_client()
|
|
270
|
+
try:
|
|
271
|
+
project_id = await resolve_project_id(project_id)
|
|
272
|
+
|
|
273
|
+
secret_obj = PullSecret(
|
|
274
|
+
id="",
|
|
275
|
+
name=name,
|
|
276
|
+
server=server,
|
|
277
|
+
username="_json_key",
|
|
278
|
+
password=json_key,
|
|
279
|
+
email="none@google.com",
|
|
280
|
+
)
|
|
281
|
+
secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
|
|
282
|
+
print(f"[green]Created GAR pull secret:[/] {secret_id}")
|
|
283
|
+
finally:
|
|
284
|
+
await client.close()
|
|
285
|
+
|
|
286
|
+
@gar_app.async_command("update")
|
|
287
|
+
async def update_gar_secret(
|
|
288
|
+
*,
|
|
289
|
+
project_id: Optional[str] = typer.Option(None),
|
|
290
|
+
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
291
|
+
name: Annotated[str, typer.Option(help="Secret name")],
|
|
292
|
+
server: Annotated[str, typer.Option(help="GAR host, e.g. us-west1-docker.pkg.dev")],
|
|
293
|
+
json_key: Annotated[str, typer.Option(help="Entire GCP service account JSON as string")]
|
|
294
|
+
):
|
|
295
|
+
"""
|
|
296
|
+
Update an existing Google Artifact Registry pull secret.
|
|
297
|
+
"""
|
|
298
|
+
client = await get_client()
|
|
299
|
+
try:
|
|
300
|
+
project_id = await resolve_project_id(project_id)
|
|
301
|
+
secret_obj = PullSecret(
|
|
302
|
+
id=secret_id,
|
|
303
|
+
name=name,
|
|
304
|
+
server=server,
|
|
305
|
+
username="_json_key",
|
|
306
|
+
password=json_key,
|
|
307
|
+
email="none@google.com",
|
|
308
|
+
)
|
|
309
|
+
await client.update_secret(project_id=project_id, secret=secret_obj)
|
|
310
|
+
print(f"[green]GAR pull secret {secret_id} updated.[/]")
|
|
311
|
+
finally:
|
|
312
|
+
await client.close()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --------------------------------------------------------------------------
|
|
316
|
+
# Additional commands (shared by all secrets): list, delete
|
|
317
|
+
# --------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
@secrets_app.async_command("list")
|
|
320
|
+
async def secret_list(*, project_id: Optional[str] = None):
|
|
321
|
+
"""List all secrets in the project (typed as Docker/ACR/GAR or Keys secrets)."""
|
|
322
|
+
client = await get_client()
|
|
323
|
+
try:
|
|
324
|
+
project_id = await resolve_project_id(project_id)
|
|
325
|
+
|
|
326
|
+
secrets: list[SecretLike] = await client.list_secrets(project_id)
|
|
327
|
+
|
|
328
|
+
# Convert each secret → plain dict for tabular output
|
|
329
|
+
rows = []
|
|
330
|
+
for s in secrets:
|
|
331
|
+
row = {
|
|
332
|
+
"id": s.id,
|
|
333
|
+
"name": s.name,
|
|
334
|
+
"type": s.type,
|
|
335
|
+
}
|
|
336
|
+
# If it's a KeysSecret, row["data_keys"] = ...
|
|
337
|
+
if hasattr(s, "data"):
|
|
338
|
+
# For Docker-ish secrets, 'data' typically has server/username/password
|
|
339
|
+
if isinstance(s, PullSecret):
|
|
340
|
+
row["data_keys"] = "server, username, password"
|
|
341
|
+
else:
|
|
342
|
+
# KeysSecret
|
|
343
|
+
row["data_keys"] = ", ".join(s.data.keys())
|
|
344
|
+
rows.append(row)
|
|
345
|
+
|
|
346
|
+
print_json_table(rows, "id", "type", "name", "data_keys")
|
|
347
|
+
|
|
348
|
+
finally:
|
|
349
|
+
await client.close()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@secrets_app.async_command("delete")
|
|
353
|
+
async def secret_delete(
|
|
354
|
+
*,
|
|
355
|
+
project_id: Optional[str] = None,
|
|
356
|
+
secret_id: Annotated[str, typer.Argument(help="ID of the secret to delete")]
|
|
357
|
+
):
|
|
358
|
+
"""Delete a secret."""
|
|
359
|
+
client = await get_client()
|
|
360
|
+
try:
|
|
361
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
362
|
+
await client.delete_secret(project_id=project_id, secret_id=secret_id)
|
|
363
|
+
print(f"[green]Secret {secret_id} deleted.[/]")
|
|
364
|
+
finally:
|
|
365
|
+
await client.close()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# --------------------------------------------------------------------------
|
|
369
|
+
# Wire up sub-apps
|
|
370
|
+
# --------------------------------------------------------------------------
|
|
371
|
+
secrets_app.add_typer(keys_app, name="keys")
|
|
372
|
+
secrets_app.add_typer(docker_app, name="docker")
|
|
373
|
+
secrets_app.add_typer(acr_app, name="acr")
|
|
374
|
+
secrets_app.add_typer(gar_app, name="gar")
|
|
375
|
+
|
|
376
|
+
app = secrets_app
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# If you want to attach `secrets_app` to your main CLI app, do something like:
|
|
380
|
+
# main_app = async_typer.AsyncTyper()
|
|
381
|
+
# main_app.add_typer(secrets_app, name="secrets")
|
|
382
|
+
# if __name__ == "__main__":
|
|
383
|
+
# main_app()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from meshagent.cli import async_typer
|
|
4
|
+
import typer
|
|
5
|
+
from meshagent.cli.helper import get_client, resolve_project_id, resolve_api_key
|
|
6
|
+
from meshagent.api import RoomClient, ParticipantToken, WebSocketClientProtocol
|
|
7
|
+
from meshagent.api.helpers import meshagent_base_url, websocket_room_url
|
|
8
|
+
from rich import print
|
|
9
|
+
from typing import Annotated, Optional
|
|
10
|
+
|
|
11
|
+
app = async_typer.AsyncTyper()
|
|
12
|
+
|
|
13
|
+
@app.async_command("watch")
|
|
14
|
+
async def watch_logs(
|
|
15
|
+
*,
|
|
16
|
+
project_id: Annotated[Optional[str], typer.Option(..., help="Project ID (if not set, will try to use the active project)")] = None,
|
|
17
|
+
room: Annotated[str, typer.Option(..., help="Name of the room to connect to")],
|
|
18
|
+
api_key_id: Annotated[Optional[str], typer.Option(..., help="API Key ID")] = None,
|
|
19
|
+
name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
|
|
20
|
+
role: Annotated[str, typer.Option(..., help="Role to assign to this participant")] = "user"
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Watch logs from the developer feed in the specified room.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
account_client = await get_client()
|
|
27
|
+
try:
|
|
28
|
+
# Resolve project ID (or fetch from the active project if not provided)
|
|
29
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
30
|
+
api_key_id = await resolve_api_key(project_id, api_key_id)
|
|
31
|
+
|
|
32
|
+
# Decrypt the project's API key
|
|
33
|
+
key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
|
|
34
|
+
|
|
35
|
+
# Build a participant token
|
|
36
|
+
token = ParticipantToken(
|
|
37
|
+
name=name,
|
|
38
|
+
project_id=project_id,
|
|
39
|
+
api_key_id=api_key_id
|
|
40
|
+
)
|
|
41
|
+
token.add_role_grant(role=role)
|
|
42
|
+
token.add_room_grant(room)
|
|
43
|
+
|
|
44
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
45
|
+
async with RoomClient(
|
|
46
|
+
protocol=WebSocketClientProtocol(
|
|
47
|
+
url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
|
|
48
|
+
token=token.to_jwt(token=key)
|
|
49
|
+
)
|
|
50
|
+
) as client:
|
|
51
|
+
# Create a developer client from the room client
|
|
52
|
+
|
|
53
|
+
# Define how to handle the incoming log events
|
|
54
|
+
def handle_log(type: str, data: dict):
|
|
55
|
+
# You can customize this print to suit your needs
|
|
56
|
+
print(f"[magenta]{type}[/magenta]: {json.dumps(data, indent=2)}")
|
|
57
|
+
|
|
58
|
+
# Attach our handler to the "log" event
|
|
59
|
+
client.developer.on("log", handle_log)
|
|
60
|
+
|
|
61
|
+
# Enable watching
|
|
62
|
+
await client.developer.enable()
|
|
63
|
+
print("[bold cyan]watching enabled. Press Ctrl+C to stop.[/bold cyan]")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Block forever, until Ctrl+C
|
|
67
|
+
while True:
|
|
68
|
+
await asyncio.sleep(10)
|
|
69
|
+
except KeyboardInterrupt:
|
|
70
|
+
print("[bold red]Stopping watch...[/bold red]")
|
|
71
|
+
finally:
|
|
72
|
+
# Disable watching before exiting
|
|
73
|
+
await client.developer.disable()
|
|
74
|
+
|
|
75
|
+
finally:
|
|
76
|
+
await account_client.close()
|
meshagent/cli/helper.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from meshagent.cli import async_typer
|
|
2
|
+
import typer
|
|
3
|
+
from meshagent.api.helpers import meshagent_base_url
|
|
4
|
+
from meshagent.api.accounts_client import AccountsClient
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from meshagent.cli import auth_async
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
SETTINGS_FILE = Path.home() / ".meshagent" / "project.json"
|
|
16
|
+
|
|
17
|
+
def _ensure_cache_dir():
|
|
18
|
+
SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
class Settings(BaseModel):
|
|
21
|
+
active_project: Optional[str] = None
|
|
22
|
+
active_api_keys: Optional[dict] = {}
|
|
23
|
+
|
|
24
|
+
def _save_settings(s: Settings):
|
|
25
|
+
_ensure_cache_dir()
|
|
26
|
+
SETTINGS_FILE.write_text(s.model_dump_json())
|
|
27
|
+
|
|
28
|
+
def _load_settings():
|
|
29
|
+
_ensure_cache_dir()
|
|
30
|
+
if SETTINGS_FILE.exists():
|
|
31
|
+
return Settings.model_validate_json(SETTINGS_FILE.read_text())
|
|
32
|
+
|
|
33
|
+
return Settings()
|
|
34
|
+
|
|
35
|
+
async def get_active_project():
|
|
36
|
+
settings = _load_settings()
|
|
37
|
+
if settings == None:
|
|
38
|
+
return None
|
|
39
|
+
return settings.active_project
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def set_active_project(project_id: str | None):
|
|
43
|
+
settings = _load_settings()
|
|
44
|
+
settings.active_project = project_id
|
|
45
|
+
_save_settings(settings)
|
|
46
|
+
|
|
47
|
+
async def get_active_api_key(project_id: str):
|
|
48
|
+
settings = _load_settings()
|
|
49
|
+
if settings == None:
|
|
50
|
+
return None
|
|
51
|
+
return settings.active_api_keys[project_id]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def set_active_api_key(project_id: str, api_key_id: str | None):
|
|
55
|
+
settings = _load_settings()
|
|
56
|
+
settings.active_api_keys[project_id] = api_key_id
|
|
57
|
+
_save_settings(settings)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
app = async_typer.AsyncTyper()
|
|
61
|
+
|
|
62
|
+
async def get_client():
|
|
63
|
+
access_token = await auth_async.get_access_token()
|
|
64
|
+
return AccountsClient(base_url=meshagent_base_url(), token=access_token)
|
|
65
|
+
|
|
66
|
+
def print_json_table(records: list, *cols):
|
|
67
|
+
|
|
68
|
+
if not records:
|
|
69
|
+
raise SystemExit("No rows to print")
|
|
70
|
+
|
|
71
|
+
# 2️⃣ --- build the table -------------------------------------------
|
|
72
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
73
|
+
|
|
74
|
+
if len(cols) > 0:
|
|
75
|
+
# use the keys of the first object as column order
|
|
76
|
+
for col in cols:
|
|
77
|
+
table.add_column(col.title()) # "id" → "Id"
|
|
78
|
+
|
|
79
|
+
for row in records:
|
|
80
|
+
table.add_row(*(str(row.get(col, "")) for col in cols))
|
|
81
|
+
|
|
82
|
+
else:
|
|
83
|
+
# use the keys of the first object as column order
|
|
84
|
+
for col in records[0]:
|
|
85
|
+
table.add_column(col.title()) # "id" → "Id"
|
|
86
|
+
|
|
87
|
+
for row in records:
|
|
88
|
+
table.add_row(*(str(row.get(col, "")) for col in records[0]))
|
|
89
|
+
|
|
90
|
+
# 3️⃣ --- render ------------------------------------------------------
|
|
91
|
+
Console().print(table)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def resolve_project_id(project_id: Optional[str] = None):
|
|
95
|
+
if project_id == None:
|
|
96
|
+
project_id = await get_active_project()
|
|
97
|
+
|
|
98
|
+
if project_id == None:
|
|
99
|
+
print("[red]Project ID not specified, activate a project or pass a project on the command line[/red]")
|
|
100
|
+
raise typer.Exit(code=1)
|
|
101
|
+
|
|
102
|
+
return project_id
|
|
103
|
+
|
|
104
|
+
async def resolve_api_key(project_id: str, api_key_id: Optional[str] = None):
|
|
105
|
+
if api_key_id == None:
|
|
106
|
+
api_key_id = await get_active_api_key(project_id=project_id)
|
|
107
|
+
|
|
108
|
+
if api_key_id == None:
|
|
109
|
+
print("[red]API Key ID not specified, activate an api key or pass an api key id on the command line[/red]")
|
|
110
|
+
raise typer.Exit(code=1)
|
|
111
|
+
|
|
112
|
+
return api_key_id
|
|
113
|
+
|