sovaria-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apps/cli/README.md +147 -0
- apps/cli/__init__.py +0 -0
- apps/cli/src/__init__.py +3 -0
- apps/cli/src/commands/__init__.py +1 -0
- apps/cli/src/commands/admin.py +160 -0
- apps/cli/src/commands/api.py +48 -0
- apps/cli/src/commands/audit.py +181 -0
- apps/cli/src/commands/auth_product.py +187 -0
- apps/cli/src/commands/billing.py +513 -0
- apps/cli/src/commands/chrono.py +248 -0
- apps/cli/src/commands/context.py +99 -0
- apps/cli/src/commands/data.py +272 -0
- apps/cli/src/commands/db.py +528 -0
- apps/cli/src/commands/deploy.py +1099 -0
- apps/cli/src/commands/governance.py +491 -0
- apps/cli/src/commands/iam.py +436 -0
- apps/cli/src/commands/init_cmd.py +177 -0
- apps/cli/src/commands/keys.py +120 -0
- apps/cli/src/commands/login.py +842 -0
- apps/cli/src/commands/org.py +157 -0
- apps/cli/src/commands/policy.py +317 -0
- apps/cli/src/commands/project.py +130 -0
- apps/cli/src/commands/realtime.py +364 -0
- apps/cli/src/commands/scope.py +42 -0
- apps/cli/src/commands/shell.py +415 -0
- apps/cli/src/commands/sim.py +178 -0
- apps/cli/src/commands/storage.py +405 -0
- apps/cli/src/commands/vector.py +204 -0
- apps/cli/src/main.py +169 -0
- apps/cli/src/utils/__init__.py +1 -0
- apps/cli/src/utils/api_client.py +329 -0
- apps/cli/src/utils/auth_store.py +299 -0
- apps/cli/src/utils/config.py +185 -0
- apps/cli/src/utils/display.py +62 -0
- apps/cli/src/utils/migrations.py +69 -0
- apps/cli/src/utils/project_context.py +124 -0
- apps/cli/tests/test_auth_store_profiles.py +103 -0
- apps/cli/tests/test_cli_security.py +110 -0
- apps/cli/tests/test_cli_smoke_paths.py +279 -0
- apps/cli/tests/test_config_profiles.py +55 -0
- apps/cli/tests/test_main_startup.py +29 -0
- apps/cli/tests/test_project_and_commands.py +196 -0
- sovaria_cli-0.2.0.dist-info/METADATA +170 -0
- sovaria_cli-0.2.0.dist-info/RECORD +46 -0
- sovaria_cli-0.2.0.dist-info/WHEEL +4 -0
- sovaria_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Pulse realtime commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
from urllib.parse import quote, urlencode
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg, print_json
|
|
11
|
+
from apps.cli.src.utils.config import get_api_url, get_scope
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Realtime stream and E2EE operations")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ScopeOpts(TypedDict):
|
|
17
|
+
org_id: str | None
|
|
18
|
+
project_id: str | None
|
|
19
|
+
env_id: str | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _scope_opts(
|
|
23
|
+
org_id: str | None,
|
|
24
|
+
project_id: str | None,
|
|
25
|
+
env_id: str | None,
|
|
26
|
+
) -> ScopeOpts:
|
|
27
|
+
return {
|
|
28
|
+
"org_id": org_id,
|
|
29
|
+
"project_id": project_id,
|
|
30
|
+
"env_id": env_id,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command("subscribe")
|
|
35
|
+
def subscribe(
|
|
36
|
+
channel: str = typer.Argument(...),
|
|
37
|
+
client_id: str = typer.Argument(...),
|
|
38
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
39
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
40
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
41
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Subscribe a client to a channel."""
|
|
44
|
+
try:
|
|
45
|
+
result = call_api(
|
|
46
|
+
method="POST",
|
|
47
|
+
path="/api/v1/rt/stream/subscribe",
|
|
48
|
+
profile=profile,
|
|
49
|
+
body={"channel": channel, "client_id": client_id},
|
|
50
|
+
require_scope=True,
|
|
51
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
52
|
+
)
|
|
53
|
+
except CLIRequestError as exc:
|
|
54
|
+
typer.echo(str(exc), err=True)
|
|
55
|
+
raise typer.Exit(1) from exc
|
|
56
|
+
typer.echo(print_json(result))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("unsubscribe")
|
|
60
|
+
def unsubscribe(
|
|
61
|
+
channel: str = typer.Argument(...),
|
|
62
|
+
client_id: str = typer.Argument(...),
|
|
63
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
64
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
65
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
66
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Unsubscribe a client from a channel."""
|
|
69
|
+
try:
|
|
70
|
+
result = call_api(
|
|
71
|
+
method="POST",
|
|
72
|
+
path="/api/v1/rt/stream/unsubscribe",
|
|
73
|
+
profile=profile,
|
|
74
|
+
body={"channel": channel, "client_id": client_id},
|
|
75
|
+
require_scope=True,
|
|
76
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
77
|
+
)
|
|
78
|
+
except CLIRequestError as exc:
|
|
79
|
+
typer.echo(str(exc), err=True)
|
|
80
|
+
raise typer.Exit(1) from exc
|
|
81
|
+
typer.echo(print_json(result))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("presence")
|
|
85
|
+
def presence(
|
|
86
|
+
channel: str = typer.Argument(...),
|
|
87
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
88
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
89
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
90
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Show online clients for a channel."""
|
|
93
|
+
try:
|
|
94
|
+
result = call_api(
|
|
95
|
+
method="GET",
|
|
96
|
+
path=f"/api/v1/rt/stream/presence/{quote(channel, safe='')}",
|
|
97
|
+
profile=profile,
|
|
98
|
+
require_scope=True,
|
|
99
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
100
|
+
)
|
|
101
|
+
except CLIRequestError as exc:
|
|
102
|
+
typer.echo(str(exc), err=True)
|
|
103
|
+
raise typer.Exit(1) from exc
|
|
104
|
+
typer.echo(print_json(result))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command("channels")
|
|
108
|
+
def channels(
|
|
109
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
110
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
111
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
112
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
113
|
+
) -> None:
|
|
114
|
+
"""List active channels in current scope."""
|
|
115
|
+
try:
|
|
116
|
+
result = call_api(
|
|
117
|
+
method="GET",
|
|
118
|
+
path="/api/v1/rt/stream/channels",
|
|
119
|
+
profile=profile,
|
|
120
|
+
require_scope=True,
|
|
121
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
122
|
+
)
|
|
123
|
+
except CLIRequestError as exc:
|
|
124
|
+
typer.echo(str(exc), err=True)
|
|
125
|
+
raise typer.Exit(1) from exc
|
|
126
|
+
typer.echo(print_json(result))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command("replay")
|
|
130
|
+
def replay(
|
|
131
|
+
channel: str = typer.Argument(...),
|
|
132
|
+
after: int = typer.Option(0, "--after"),
|
|
133
|
+
limit: int = typer.Option(200, "--limit"),
|
|
134
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
135
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
136
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
137
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Replay channel events from a sequence index."""
|
|
140
|
+
try:
|
|
141
|
+
result = call_api(
|
|
142
|
+
method="GET",
|
|
143
|
+
path=f"/api/v1/rt/stream/replay/{quote(channel, safe='')}",
|
|
144
|
+
profile=profile,
|
|
145
|
+
params={"after": str(after), "limit": str(limit)},
|
|
146
|
+
require_scope=True,
|
|
147
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
148
|
+
)
|
|
149
|
+
except CLIRequestError as exc:
|
|
150
|
+
typer.echo(str(exc), err=True)
|
|
151
|
+
raise typer.Exit(1) from exc
|
|
152
|
+
typer.echo(print_json(result))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command("publish")
|
|
156
|
+
def publish(
|
|
157
|
+
channel: str = typer.Argument(...),
|
|
158
|
+
payload: str = typer.Argument(..., help="JSON object payload"),
|
|
159
|
+
clock: str | None = typer.Option(None, "--clock", help="JSON object clock"),
|
|
160
|
+
clock_delta: str | None = typer.Option(None, "--clock-delta", help="JSON object clock delta"),
|
|
161
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
162
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
163
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
164
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Publish a payload on a channel."""
|
|
167
|
+
try:
|
|
168
|
+
parsed_payload = parse_json_arg(payload, label="payload")
|
|
169
|
+
if not isinstance(parsed_payload, dict):
|
|
170
|
+
raise CLIRequestError("payload must be a JSON object")
|
|
171
|
+
parsed_clock = parse_json_arg(clock, label="--clock") if clock else None
|
|
172
|
+
parsed_delta = parse_json_arg(clock_delta, label="--clock-delta") if clock_delta else None
|
|
173
|
+
if parsed_clock is not None and not isinstance(parsed_clock, dict):
|
|
174
|
+
raise CLIRequestError("--clock must be a JSON object")
|
|
175
|
+
if parsed_delta is not None and not isinstance(parsed_delta, dict):
|
|
176
|
+
raise CLIRequestError("--clock-delta must be a JSON object")
|
|
177
|
+
|
|
178
|
+
body = {"payload": parsed_payload}
|
|
179
|
+
if parsed_clock:
|
|
180
|
+
body["clock"] = parsed_clock
|
|
181
|
+
if parsed_delta:
|
|
182
|
+
body["clock_delta"] = parsed_delta
|
|
183
|
+
|
|
184
|
+
result = call_api(
|
|
185
|
+
method="POST",
|
|
186
|
+
path=f"/api/v1/rt/stream/publish/{quote(channel, safe='')}",
|
|
187
|
+
profile=profile,
|
|
188
|
+
body=body,
|
|
189
|
+
require_scope=True,
|
|
190
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
191
|
+
)
|
|
192
|
+
except CLIRequestError as exc:
|
|
193
|
+
typer.echo(str(exc), err=True)
|
|
194
|
+
raise typer.Exit(1) from exc
|
|
195
|
+
|
|
196
|
+
typer.echo(print_json(result))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command("e2ee-bootstrap")
|
|
200
|
+
def e2ee_bootstrap(
|
|
201
|
+
channel: str = typer.Argument(...),
|
|
202
|
+
ttl_seconds: int = typer.Option(86_400, "--ttl"),
|
|
203
|
+
force_new: bool = typer.Option(False, "--force-new"),
|
|
204
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
205
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
206
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
207
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Create or fetch active E2EE key material."""
|
|
210
|
+
try:
|
|
211
|
+
result = call_api(
|
|
212
|
+
method="POST",
|
|
213
|
+
path="/api/v1/rt/e2ee/keys/bootstrap",
|
|
214
|
+
profile=profile,
|
|
215
|
+
body={"channel": channel, "ttl_seconds": ttl_seconds, "force_new": force_new},
|
|
216
|
+
require_scope=True,
|
|
217
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
218
|
+
)
|
|
219
|
+
except CLIRequestError as exc:
|
|
220
|
+
typer.echo(str(exc), err=True)
|
|
221
|
+
raise typer.Exit(1) from exc
|
|
222
|
+
typer.echo(print_json(result))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.command("e2ee-rotate")
|
|
226
|
+
def e2ee_rotate(
|
|
227
|
+
channel: str = typer.Argument(...),
|
|
228
|
+
ttl_seconds: int = typer.Option(86_400, "--ttl"),
|
|
229
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
230
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
231
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
232
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Rotate E2EE key material."""
|
|
235
|
+
try:
|
|
236
|
+
result = call_api(
|
|
237
|
+
method="POST",
|
|
238
|
+
path="/api/v1/rt/e2ee/keys/rotate",
|
|
239
|
+
profile=profile,
|
|
240
|
+
body={"channel": channel, "ttl_seconds": ttl_seconds},
|
|
241
|
+
require_scope=True,
|
|
242
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
243
|
+
)
|
|
244
|
+
except CLIRequestError as exc:
|
|
245
|
+
typer.echo(str(exc), err=True)
|
|
246
|
+
raise typer.Exit(1) from exc
|
|
247
|
+
typer.echo(print_json(result))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.command("e2ee-revoke")
|
|
251
|
+
def e2ee_revoke(
|
|
252
|
+
channel: str = typer.Argument(...),
|
|
253
|
+
key_id: str | None = typer.Option(None, "--key-id"),
|
|
254
|
+
reason: str | None = typer.Option(None, "--reason"),
|
|
255
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
256
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
257
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
258
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Revoke an E2EE key."""
|
|
261
|
+
try:
|
|
262
|
+
result = call_api(
|
|
263
|
+
method="POST",
|
|
264
|
+
path="/api/v1/rt/e2ee/keys/revoke",
|
|
265
|
+
profile=profile,
|
|
266
|
+
body={"channel": channel, "key_id": key_id, "reason": reason},
|
|
267
|
+
require_scope=True,
|
|
268
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
269
|
+
)
|
|
270
|
+
except CLIRequestError as exc:
|
|
271
|
+
typer.echo(str(exc), err=True)
|
|
272
|
+
raise typer.Exit(1) from exc
|
|
273
|
+
typer.echo(print_json(result))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command("e2ee-list")
|
|
277
|
+
def e2ee_list(
|
|
278
|
+
channel: str = typer.Argument(...),
|
|
279
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
280
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
281
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
282
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
283
|
+
) -> None:
|
|
284
|
+
"""List E2EE keys for a channel."""
|
|
285
|
+
try:
|
|
286
|
+
result = call_api(
|
|
287
|
+
method="GET",
|
|
288
|
+
path=f"/api/v1/rt/e2ee/keys/{quote(channel, safe='')}",
|
|
289
|
+
profile=profile,
|
|
290
|
+
require_scope=True,
|
|
291
|
+
**_scope_opts(org_id, project_id, env_id),
|
|
292
|
+
)
|
|
293
|
+
except CLIRequestError as exc:
|
|
294
|
+
typer.echo(str(exc), err=True)
|
|
295
|
+
raise typer.Exit(1) from exc
|
|
296
|
+
typer.echo(print_json(result))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@app.command("ws-url")
|
|
300
|
+
def ws_url(
|
|
301
|
+
channel: str = typer.Argument(...),
|
|
302
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
303
|
+
ticket: str | None = typer.Option(None, "--ticket", help="Use an already issued WS ticket"),
|
|
304
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
305
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
306
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Generate websocket URL for Pulse via gateway."""
|
|
309
|
+
base = get_api_url(profile).rstrip("/")
|
|
310
|
+
if base.startswith("https://"):
|
|
311
|
+
ws_base = "wss://" + base[len("https://") :]
|
|
312
|
+
elif base.startswith("http://"):
|
|
313
|
+
ws_base = "ws://" + base[len("http://") :]
|
|
314
|
+
else:
|
|
315
|
+
ws_base = f"ws://{base}"
|
|
316
|
+
|
|
317
|
+
scope = get_scope(profile)
|
|
318
|
+
resolved_org = (org_id or scope.get("org_id") or "").strip()
|
|
319
|
+
resolved_project = (project_id or scope.get("project_id") or "").strip()
|
|
320
|
+
resolved_env = (env_id or scope.get("env_id") or "").strip()
|
|
321
|
+
|
|
322
|
+
if not resolved_org or not resolved_project:
|
|
323
|
+
typer.echo(
|
|
324
|
+
"Missing scope. Set --org/--project or run: sov scope set <org_id> <project_id>",
|
|
325
|
+
err=True,
|
|
326
|
+
)
|
|
327
|
+
raise typer.Exit(1)
|
|
328
|
+
|
|
329
|
+
resolved_ticket = (ticket or "").strip()
|
|
330
|
+
if not resolved_ticket:
|
|
331
|
+
try:
|
|
332
|
+
result = call_api(
|
|
333
|
+
method="POST",
|
|
334
|
+
path="/api/v1/auth/ws-ticket",
|
|
335
|
+
profile=profile,
|
|
336
|
+
body={
|
|
337
|
+
"channel": channel,
|
|
338
|
+
"org_id": resolved_org,
|
|
339
|
+
"project_id": resolved_project,
|
|
340
|
+
**({"env_id": resolved_env} if resolved_env else {}),
|
|
341
|
+
},
|
|
342
|
+
require_scope=True,
|
|
343
|
+
org_id=resolved_org,
|
|
344
|
+
project_id=resolved_project,
|
|
345
|
+
env_id=resolved_env or None,
|
|
346
|
+
)
|
|
347
|
+
except CLIRequestError as exc:
|
|
348
|
+
typer.echo(str(exc), err=True)
|
|
349
|
+
raise typer.Exit(1) from exc
|
|
350
|
+
resolved_ticket = str(result.get("ticket") or "").strip() if isinstance(result, dict) else ""
|
|
351
|
+
if not resolved_ticket:
|
|
352
|
+
typer.echo("Gateway did not return a websocket ticket.", err=True)
|
|
353
|
+
raise typer.Exit(1)
|
|
354
|
+
|
|
355
|
+
query = {
|
|
356
|
+
"ticket": resolved_ticket,
|
|
357
|
+
"org_id": resolved_org,
|
|
358
|
+
"project_id": resolved_project,
|
|
359
|
+
}
|
|
360
|
+
if resolved_env:
|
|
361
|
+
query["env_id"] = resolved_env
|
|
362
|
+
|
|
363
|
+
safe_channel = quote(channel, safe="")
|
|
364
|
+
typer.echo(f"{ws_base}/api/v1/rt/ws/{safe_channel}?{urlencode(query)}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Scope management commands for org/project/env defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from apps.cli.src.utils.config import clear_scope, get_scope, set_scope
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Manage default org/project/env scope")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("show")
|
|
13
|
+
def show_scope(profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
14
|
+
"""Show currently configured default scope."""
|
|
15
|
+
scope = get_scope(profile)
|
|
16
|
+
typer.echo(f"org_id : {scope.get('org_id') or '-'}")
|
|
17
|
+
typer.echo(f"project_id : {scope.get('project_id') or '-'}")
|
|
18
|
+
typer.echo(f"env_id : {scope.get('env_id') or '-'}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command("set")
|
|
22
|
+
def set_scope_cmd(
|
|
23
|
+
org_id: str = typer.Argument(..., help="Organization UUID"),
|
|
24
|
+
project_id: str = typer.Argument(..., help="Project UUID"),
|
|
25
|
+
env_id: str | None = typer.Option(None, "--env", help="Environment UUID"),
|
|
26
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Persist default scope for future CLI calls."""
|
|
29
|
+
set_scope(
|
|
30
|
+
org_id=org_id,
|
|
31
|
+
project_id=project_id,
|
|
32
|
+
env_id=env_id,
|
|
33
|
+
profile=profile,
|
|
34
|
+
)
|
|
35
|
+
typer.echo("Default scope saved.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("clear")
|
|
39
|
+
def clear_scope_cmd(profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
40
|
+
"""Clear default scope for the active profile."""
|
|
41
|
+
clear_scope(profile)
|
|
42
|
+
typer.echo("Default scope cleared.")
|