genalpha_test_cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,756 @@
1
+ """Auto-generated CLI for genalpha_test_cli. Generated by genalphacli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ import typer
9
+
10
+ from genalpha_test_cli.client import ApiError, api_call
11
+
12
+ app = typer.Typer(name="genalpha_test_cli", help="CLI for genalpha_test_cli")
13
+
14
+
15
+ def _output(data: Any, pretty: bool = False) -> None:
16
+ """Format and print API response."""
17
+ if pretty:
18
+ from rich import print_json
19
+
20
+ print_json(data=data)
21
+ else:
22
+ typer.echo(json.dumps(data, indent=2))
23
+
24
+
25
+
26
+ @app.command(name="health")
27
+ def health(
28
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
29
+ ) -> None:
30
+ """GET /health"""
31
+ try:
32
+ result = api_call("GET", f"/health")
33
+ _output(result, pretty)
34
+ except ApiError as e:
35
+ typer.echo(f"Error: {e.message}", err=True)
36
+ raise typer.Exit(1)
37
+
38
+
39
+ @app.command(name="login")
40
+ def login(
41
+ body_val: str = typer.Option(..., "--body", help="body"),
42
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
43
+ ) -> None:
44
+ """Login and get a token."""
45
+ try:
46
+ try:
47
+ data = json.loads(body_val)
48
+ except json.JSONDecodeError as e:
49
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
50
+ raise typer.Exit(1)
51
+ result = api_call("POST", f"/auth/login", json_data=data)
52
+ _output(result, pretty)
53
+ except ApiError as e:
54
+ typer.echo(f"Error: {e.message}", err=True)
55
+ raise typer.Exit(1)
56
+
57
+
58
+ @app.command(name="get-me")
59
+ def get_me(
60
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
61
+ ) -> None:
62
+ """Get current user from token."""
63
+ try:
64
+ result = api_call("GET", f"/auth/me")
65
+ _output(result, pretty)
66
+ except ApiError as e:
67
+ typer.echo(f"Error: {e.message}", err=True)
68
+ raise typer.Exit(1)
69
+
70
+
71
+ @app.command(name="list-users")
72
+ def list_users(
73
+ limit: int | None = typer.Option(None, "--limit", help="limit"),
74
+ offset: int | None = typer.Option(None, "--offset", help="offset"),
75
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
76
+ ) -> None:
77
+ """List all users."""
78
+ try:
79
+ params: dict[str, Any] = {}
80
+ if limit is not None:
81
+ params["limit"] = limit
82
+ if offset is not None:
83
+ params["offset"] = offset
84
+ result = api_call("GET", f"/api/v1/users", params=params)
85
+ _output(result, pretty)
86
+ except ApiError as e:
87
+ typer.echo(f"Error: {e.message}", err=True)
88
+ raise typer.Exit(1)
89
+
90
+
91
+ @app.command(name="get-user")
92
+ def get_user(
93
+ user_id: str = typer.Argument(help="user_id"),
94
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
95
+ ) -> None:
96
+ """Get a user by ID."""
97
+ try:
98
+ result = api_call("GET", f"/api/v1/users/{user_id}")
99
+ _output(result, pretty)
100
+ except ApiError as e:
101
+ typer.echo(f"Error: {e.message}", err=True)
102
+ raise typer.Exit(1)
103
+
104
+
105
+ @app.command(name="create-user")
106
+ def create_user(
107
+ body_val: str = typer.Option(..., "--body", help="body"),
108
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
109
+ ) -> None:
110
+ """Create a new user."""
111
+ try:
112
+ try:
113
+ data = json.loads(body_val)
114
+ except json.JSONDecodeError as e:
115
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
116
+ raise typer.Exit(1)
117
+ result = api_call("POST", f"/api/v1/users", json_data=data)
118
+ _output(result, pretty)
119
+ except ApiError as e:
120
+ typer.echo(f"Error: {e.message}", err=True)
121
+ raise typer.Exit(1)
122
+
123
+
124
+ @app.command(name="delete-user")
125
+ def delete_user(
126
+ user_id: str = typer.Argument(help="user_id"),
127
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
128
+ ) -> None:
129
+ """Delete a user."""
130
+ try:
131
+ result = api_call("DELETE", f"/api/v1/users/{user_id}")
132
+ _output(result, pretty)
133
+ except ApiError as e:
134
+ typer.echo(f"Error: {e.message}", err=True)
135
+ raise typer.Exit(1)
136
+
137
+
138
+ @app.command(name="list-projects")
139
+ def list_projects(
140
+ limit: int | None = typer.Option(None, "--limit", help="limit"),
141
+ offset: int | None = typer.Option(None, "--offset", help="offset"),
142
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
143
+ ) -> None:
144
+ """List all projects."""
145
+ try:
146
+ params: dict[str, Any] = {}
147
+ if limit is not None:
148
+ params["limit"] = limit
149
+ if offset is not None:
150
+ params["offset"] = offset
151
+ result = api_call("GET", f"/api/v1/projects", params=params)
152
+ _output(result, pretty)
153
+ except ApiError as e:
154
+ typer.echo(f"Error: {e.message}", err=True)
155
+ raise typer.Exit(1)
156
+
157
+
158
+ @app.command(name="get-project")
159
+ def get_project(
160
+ project_id: str = typer.Argument(help="project_id"),
161
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
162
+ ) -> None:
163
+ """Get a project by ID."""
164
+ try:
165
+ result = api_call("GET", f"/api/v1/projects/{project_id}")
166
+ _output(result, pretty)
167
+ except ApiError as e:
168
+ typer.echo(f"Error: {e.message}", err=True)
169
+ raise typer.Exit(1)
170
+
171
+
172
+ @app.command(name="create-project")
173
+ def create_project(
174
+ body_val: str = typer.Option(..., "--body", help="body"),
175
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
176
+ ) -> None:
177
+ """Create a new project."""
178
+ try:
179
+ try:
180
+ data = json.loads(body_val)
181
+ except json.JSONDecodeError as e:
182
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
183
+ raise typer.Exit(1)
184
+ result = api_call("POST", f"/api/v1/projects", json_data=data)
185
+ _output(result, pretty)
186
+ except ApiError as e:
187
+ typer.echo(f"Error: {e.message}", err=True)
188
+ raise typer.Exit(1)
189
+
190
+
191
+ @app.command(name="get-root")
192
+ def get_root(
193
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
194
+ ) -> None:
195
+ """Root endpoint."""
196
+ try:
197
+ result = api_call("GET", f"/")
198
+ _output(result, pretty)
199
+ except ApiError as e:
200
+ typer.echo(f"Error: {e.message}", err=True)
201
+ raise typer.Exit(1)
202
+
203
+
204
+ @app.command(name="list-users")
205
+ def list_users(
206
+ limit: int | None = typer.Option(None, "--limit", help="limit"),
207
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
208
+ ) -> None:
209
+ """GET /users/"""
210
+ try:
211
+ params: dict[str, Any] = {}
212
+ if limit is not None:
213
+ params["limit"] = limit
214
+ result = api_call("GET", f"/users/", params=params)
215
+ _output(result, pretty)
216
+ except ApiError as e:
217
+ typer.echo(f"Error: {e.message}", err=True)
218
+ raise typer.Exit(1)
219
+
220
+
221
+ @app.command(name="get-user")
222
+ def get_user(
223
+ user_id: int = typer.Argument(help="user_id"),
224
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
225
+ ) -> None:
226
+ """GET /users/{user_id}"""
227
+ try:
228
+ result = api_call("GET", f"/users/{user_id}")
229
+ _output(result, pretty)
230
+ except ApiError as e:
231
+ typer.echo(f"Error: {e.message}", err=True)
232
+ raise typer.Exit(1)
233
+
234
+
235
+ @app.command(name="create-user")
236
+ def create_user(
237
+ name: str = typer.Option(..., "--name", help="name"),
238
+ email: str = typer.Option(..., "--email", help="email"),
239
+ raw_body: str | None = typer.Option(None, "--body", help="Raw JSON body (overrides flags)"),
240
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
241
+ ) -> None:
242
+ """POST /users/"""
243
+ try:
244
+ if raw_body:
245
+ try:
246
+ data = json.loads(raw_body)
247
+ except json.JSONDecodeError as e:
248
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
249
+ raise typer.Exit(1)
250
+ else:
251
+ data = { "name": name, "email": email, }
252
+ result = api_call("POST", f"/users/", json_data=data)
253
+ _output(result, pretty)
254
+ except ApiError as e:
255
+ typer.echo(f"Error: {e.message}", err=True)
256
+ raise typer.Exit(1)
257
+
258
+
259
+ @app.command(name="delete-user")
260
+ def delete_user(
261
+ user_id: int = typer.Argument(help="user_id"),
262
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
263
+ ) -> None:
264
+ """DELETE /users/{user_id}"""
265
+ try:
266
+ result = api_call("DELETE", f"/users/{user_id}")
267
+ _output(result, pretty)
268
+ except ApiError as e:
269
+ typer.echo(f"Error: {e.message}", err=True)
270
+ raise typer.Exit(1)
271
+
272
+
273
+ @app.command(name="request-magic-link")
274
+ def request_magic_link(
275
+ body_val: str = typer.Option(..., "--body", help="body"),
276
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
277
+ ) -> None:
278
+ """Send a magic link to the user's email. Always returns 200 regardless of whether the email exists (prevents enumeration). Uses Resend for email delivery. Falls back to console logging if no API key."""
279
+ try:
280
+ try:
281
+ data = json.loads(body_val)
282
+ except json.JSONDecodeError as e:
283
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
284
+ raise typer.Exit(1)
285
+ result = api_call("POST", f"/auth/magic-link", json_data=data)
286
+ _output(result, pretty)
287
+ except ApiError as e:
288
+ typer.echo(f"Error: {e.message}", err=True)
289
+ raise typer.Exit(1)
290
+
291
+
292
+ @app.command(name="verify-magic-link")
293
+ def verify_magic_link(
294
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
295
+ ) -> None:
296
+ """Verify a magic link token, create/find user, set session cookie."""
297
+ try:
298
+ result = api_call("GET", f"/auth/verify")
299
+ _output(result, pretty)
300
+ except ApiError as e:
301
+ typer.echo(f"Error: {e.message}", err=True)
302
+ raise typer.Exit(1)
303
+
304
+
305
+ @app.command(name="get-session")
306
+ def get_session(
307
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
308
+ ) -> None:
309
+ """Get the current user's session info including workspace."""
310
+ try:
311
+ result = api_call("GET", f"/auth/session")
312
+ _output(result, pretty)
313
+ except ApiError as e:
314
+ typer.echo(f"Error: {e.message}", err=True)
315
+ raise typer.Exit(1)
316
+
317
+
318
+ @app.command(name="logout")
319
+ def logout(
320
+ session_id: str | None = typer.Option(None, "--session-id", help="session_id"),
321
+ raw_body: str | None = typer.Option(None, "--body", help="Raw JSON body (overrides flags)"),
322
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
323
+ ) -> None:
324
+ """Log out by deleting the session."""
325
+ try:
326
+ if raw_body:
327
+ try:
328
+ data = json.loads(raw_body)
329
+ except json.JSONDecodeError as e:
330
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
331
+ raise typer.Exit(1)
332
+ else:
333
+ data = { "session_id": session_id, }
334
+ result = api_call("POST", f"/auth/logout", json_data=data)
335
+ _output(result, pretty)
336
+ except ApiError as e:
337
+ typer.echo(f"Error: {e.message}", err=True)
338
+ raise typer.Exit(1)
339
+
340
+
341
+ @app.command(name="list-all-services")
342
+ def list_all_services(
343
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
344
+ ) -> None:
345
+ """List all services across all projects for the current workspace."""
346
+ try:
347
+ result = api_call("GET", f"/services")
348
+ _output(result, pretty)
349
+ except ApiError as e:
350
+ typer.echo(f"Error: {e.message}", err=True)
351
+ raise typer.Exit(1)
352
+
353
+
354
+ @app.command(name="list-services-by-project")
355
+ def list_services_by_project(
356
+ project_id: str = typer.Argument(help="project_id"),
357
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
358
+ ) -> None:
359
+ """List all services for a project (excludes route_graph for performance)."""
360
+ try:
361
+ result = api_call("GET", f"/services/by-project/{project_id}")
362
+ _output(result, pretty)
363
+ except ApiError as e:
364
+ typer.echo(f"Error: {e.message}", err=True)
365
+ raise typer.Exit(1)
366
+
367
+
368
+ @app.command(name="get-service")
369
+ def get_service(
370
+ service_id: str = typer.Argument(help="service_id"),
371
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
372
+ ) -> None:
373
+ """Get a service with ownership check."""
374
+ try:
375
+ result = api_call("GET", f"/services/{service_id}")
376
+ _output(result, pretty)
377
+ except ApiError as e:
378
+ typer.echo(f"Error: {e.message}", err=True)
379
+ raise typer.Exit(1)
380
+
381
+
382
+ @app.command(name="delete-service")
383
+ def delete_service(
384
+ service_id: str = typer.Argument(help="service_id"),
385
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
386
+ ) -> None:
387
+ """Delete a service with ownership check."""
388
+ try:
389
+ result = api_call("DELETE", f"/services/{service_id}")
390
+ _output(result, pretty)
391
+ except ApiError as e:
392
+ typer.echo(f"Error: {e.message}", err=True)
393
+ raise typer.Exit(1)
394
+
395
+
396
+ @app.command(name="download-service")
397
+ def download_service(
398
+ service_id: str = typer.Argument(help="service_id"),
399
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
400
+ ) -> None:
401
+ """Download the generated zip for a service (redirects to artifact)."""
402
+ try:
403
+ result = api_call("GET", f"/services/{service_id}/download")
404
+ _output(result, pretty)
405
+ except ApiError as e:
406
+ typer.echo(f"Error: {e.message}", err=True)
407
+ raise typer.Exit(1)
408
+
409
+
410
+ @app.command(name="start-generate")
411
+ def start_generate(
412
+ body_val: str = typer.Option(..., "--body", help="body"),
413
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
414
+ ) -> None:
415
+ """Start generation workflow for a parsed service."""
416
+ try:
417
+ try:
418
+ data = json.loads(body_val)
419
+ except json.JSONDecodeError as e:
420
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
421
+ raise typer.Exit(1)
422
+ result = api_call("POST", f"/generate", json_data=data)
423
+ _output(result, pretty)
424
+ except ApiError as e:
425
+ typer.echo(f"Error: {e.message}", err=True)
426
+ raise typer.Exit(1)
427
+
428
+
429
+ @app.command(name="oauth-callback")
430
+ def oauth_callback(
431
+ code: str | None = typer.Option(None, "--code", help="code"),
432
+ state: str | None = typer.Option(None, "--state", help="state"),
433
+ error: str | None = typer.Option(None, "--error", help="error"),
434
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
435
+ ) -> None:
436
+ """Public OAuth callback — no auth required. Context is in the encrypted state."""
437
+ try:
438
+ params: dict[str, Any] = {}
439
+ if code is not None:
440
+ params["code"] = code
441
+ if state is not None:
442
+ params["state"] = state
443
+ if error is not None:
444
+ params["error"] = error
445
+ result = api_call("GET", f"/oauth/callback", params=params)
446
+ _output(result, pretty)
447
+ except ApiError as e:
448
+ typer.echo(f"Error: {e.message}", err=True)
449
+ raise typer.Exit(1)
450
+
451
+
452
+ @app.command(name="upload-artifact")
453
+ def upload_artifact(
454
+ service_id: str = typer.Argument(help="service_id"),
455
+ file: str | None = typer.Option(None, "--file", help="file"),
456
+ artifact_type: str | None = typer.Option(None, "--artifact-type", help="artifact_type"),
457
+ raw_body: str | None = typer.Option(None, "--body", help="Raw JSON body (overrides flags)"),
458
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
459
+ ) -> None:
460
+ """Upload a generated artifact (ZIP) for a service. Called by the worker."""
461
+ try:
462
+ if raw_body:
463
+ try:
464
+ data = json.loads(raw_body)
465
+ except json.JSONDecodeError as e:
466
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
467
+ raise typer.Exit(1)
468
+ else:
469
+ data = { "file": file, "artifact_type": artifact_type, }
470
+ result = api_call("POST", f"/services/{service_id}/artifacts", json_data=data)
471
+ _output(result, pretty)
472
+ except ApiError as e:
473
+ typer.echo(f"Error: {e.message}", err=True)
474
+ raise typer.Exit(1)
475
+
476
+
477
+ @app.command(name="download-artifact")
478
+ def download_artifact(
479
+ artifact_id: str = typer.Argument(help="artifact_id"),
480
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
481
+ ) -> None:
482
+ """Download a generated artifact ZIP."""
483
+ try:
484
+ result = api_call("GET", f"/artifacts/{artifact_id}/download")
485
+ _output(result, pretty)
486
+ except ApiError as e:
487
+ typer.echo(f"Error: {e.message}", err=True)
488
+ raise typer.Exit(1)
489
+
490
+
491
+ @app.command(name="start-parse")
492
+ def start_parse(
493
+ body_val: str = typer.Option(..., "--body", help="body"),
494
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
495
+ ) -> None:
496
+ """Validate GitHub URL, check limits, create service record, start parse workflow."""
497
+ try:
498
+ try:
499
+ data = json.loads(body_val)
500
+ except json.JSONDecodeError as e:
501
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
502
+ raise typer.Exit(1)
503
+ result = api_call("POST", f"/parse", json_data=data)
504
+ _output(result, pretty)
505
+ except ApiError as e:
506
+ typer.echo(f"Error: {e.message}", err=True)
507
+ raise typer.Exit(1)
508
+
509
+
510
+ @app.command(name="update-service-status")
511
+ def update_service_status(
512
+ service_id: str = typer.Argument(help="service_id"),
513
+ body_val: str = typer.Option(..., "--body", help="body"),
514
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
515
+ ) -> None:
516
+ """Update service status — called by Temporal worker activities."""
517
+ try:
518
+ try:
519
+ data = json.loads(body_val)
520
+ except json.JSONDecodeError as e:
521
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
522
+ raise typer.Exit(1)
523
+ result = api_call("POST", f"/services/{service_id}/status", json_data=data)
524
+ _output(result, pretty)
525
+ except ApiError as e:
526
+ typer.echo(f"Error: {e.message}", err=True)
527
+ raise typer.Exit(1)
528
+
529
+
530
+ @app.command(name="list-projects")
531
+ def list_projects(
532
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
533
+ ) -> None:
534
+ """List all projects in the current workspace."""
535
+ try:
536
+ result = api_call("GET", f"/projects")
537
+ _output(result, pretty)
538
+ except ApiError as e:
539
+ typer.echo(f"Error: {e.message}", err=True)
540
+ raise typer.Exit(1)
541
+
542
+
543
+ @app.command(name="create-project")
544
+ def create_project(
545
+ body_val: str = typer.Option(..., "--body", help="body"),
546
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
547
+ ) -> None:
548
+ """Create a new project in the current workspace."""
549
+ try:
550
+ try:
551
+ data = json.loads(body_val)
552
+ except json.JSONDecodeError as e:
553
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
554
+ raise typer.Exit(1)
555
+ result = api_call("POST", f"/projects", json_data=data)
556
+ _output(result, pretty)
557
+ except ApiError as e:
558
+ typer.echo(f"Error: {e.message}", err=True)
559
+ raise typer.Exit(1)
560
+
561
+
562
+ @app.command(name="update-project")
563
+ def update_project(
564
+ project_id: str = typer.Argument(help="project_id"),
565
+ body_val: str = typer.Option(..., "--body", help="body"),
566
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
567
+ ) -> None:
568
+ """Update a project's name or description."""
569
+ try:
570
+ try:
571
+ data = json.loads(body_val)
572
+ except json.JSONDecodeError as e:
573
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
574
+ raise typer.Exit(1)
575
+ result = api_call("PATCH", f"/projects/{project_id}", json_data=data)
576
+ _output(result, pretty)
577
+ except ApiError as e:
578
+ typer.echo(f"Error: {e.message}", err=True)
579
+ raise typer.Exit(1)
580
+
581
+
582
+ @app.command(name="delete-project")
583
+ def delete_project(
584
+ project_id: str = typer.Argument(help="project_id"),
585
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
586
+ ) -> None:
587
+ """Delete a project (cascade deletes services)."""
588
+ try:
589
+ result = api_call("DELETE", f"/projects/{project_id}")
590
+ _output(result, pretty)
591
+ except ApiError as e:
592
+ typer.echo(f"Error: {e.message}", err=True)
593
+ raise typer.Exit(1)
594
+
595
+
596
+ @app.command(name="list-apps")
597
+ def list_apps(
598
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
599
+ ) -> None:
600
+ """GET /integrations/apps"""
601
+ try:
602
+ result = api_call("GET", f"/integrations/apps")
603
+ _output(result, pretty)
604
+ except ApiError as e:
605
+ typer.echo(f"Error: {e.message}", err=True)
606
+ raise typer.Exit(1)
607
+
608
+
609
+ @app.command(name="get-app")
610
+ def get_app(
611
+ identifier: str = typer.Argument(help="identifier"),
612
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
613
+ ) -> None:
614
+ """GET /integrations/apps/{identifier}"""
615
+ try:
616
+ result = api_call("GET", f"/integrations/apps/{identifier}")
617
+ _output(result, pretty)
618
+ except ApiError as e:
619
+ typer.echo(f"Error: {e.message}", err=True)
620
+ raise typer.Exit(1)
621
+
622
+
623
+ @app.command(name="list-integrations")
624
+ def list_integrations(
625
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
626
+ ) -> None:
627
+ """GET /integrations"""
628
+ try:
629
+ result = api_call("GET", f"/integrations")
630
+ _output(result, pretty)
631
+ except ApiError as e:
632
+ typer.echo(f"Error: {e.message}", err=True)
633
+ raise typer.Exit(1)
634
+
635
+
636
+ @app.command(name="get-integration")
637
+ def get_integration(
638
+ identifier: str = typer.Argument(help="identifier"),
639
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
640
+ ) -> None:
641
+ """GET /integrations/{identifier}"""
642
+ try:
643
+ result = api_call("GET", f"/integrations/{identifier}")
644
+ _output(result, pretty)
645
+ except ApiError as e:
646
+ typer.echo(f"Error: {e.message}", err=True)
647
+ raise typer.Exit(1)
648
+
649
+
650
+ @app.command(name="install-app")
651
+ def install_app(
652
+ app_name: str = typer.Argument(help="app_name"),
653
+ body_val: str = typer.Option(..., "--body", help="body"),
654
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
655
+ ) -> None:
656
+ """Build OAuth authorize URL. TPS is stateless — just plugs state into URL."""
657
+ try:
658
+ try:
659
+ data = json.loads(body_val)
660
+ except json.JSONDecodeError as e:
661
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
662
+ raise typer.Exit(1)
663
+ result = api_call("POST", f"/integrations/{app_name}/install", json_data=data)
664
+ _output(result, pretty)
665
+ except ApiError as e:
666
+ typer.echo(f"Error: {e.message}", err=True)
667
+ raise typer.Exit(1)
668
+
669
+
670
+ @app.command(name="connect-app")
671
+ def connect_app(
672
+ app_name: str = typer.Argument(help="app_name"),
673
+ body_val: str = typer.Option(..., "--body", help="body"),
674
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
675
+ ) -> None:
676
+ """Connect a credential-based app. Validates fields, stores encrypted."""
677
+ try:
678
+ try:
679
+ data = json.loads(body_val)
680
+ except json.JSONDecodeError as e:
681
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
682
+ raise typer.Exit(1)
683
+ result = api_call("POST", f"/integrations/{app_name}/connect", json_data=data)
684
+ _output(result, pretty)
685
+ except ApiError as e:
686
+ typer.echo(f"Error: {e.message}", err=True)
687
+ raise typer.Exit(1)
688
+
689
+
690
+ @app.command(name="remove-integration")
691
+ def remove_integration(
692
+ integration_id: str = typer.Argument(help="integration_id"),
693
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
694
+ ) -> None:
695
+ """DELETE /integrations/{integration_id}"""
696
+ try:
697
+ result = api_call("DELETE", f"/integrations/{integration_id}")
698
+ _output(result, pretty)
699
+ except ApiError as e:
700
+ typer.echo(f"Error: {e.message}", err=True)
701
+ raise typer.Exit(1)
702
+
703
+
704
+ @app.command(name="list-apps")
705
+ def list_apps(
706
+ category: str | None = typer.Option(None, "--category", help="category"),
707
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
708
+ ) -> None:
709
+ """GET /apps — list all active apps, optionally filtered by category."""
710
+ try:
711
+ params: dict[str, Any] = {}
712
+ if category is not None:
713
+ params["category"] = category
714
+ result = api_call("GET", f"/apps", params=params)
715
+ _output(result, pretty)
716
+ except ApiError as e:
717
+ typer.echo(f"Error: {e.message}", err=True)
718
+ raise typer.Exit(1)
719
+
720
+
721
+ @app.command(name="get-app")
722
+ def get_app(
723
+ identifier: str = typer.Argument(help="identifier"),
724
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
725
+ ) -> None:
726
+ """GET /apps/{identifier} — polymorphic lookup by app_name or app_code."""
727
+ try:
728
+ result = api_call("GET", f"/apps/{identifier}")
729
+ _output(result, pretty)
730
+ except ApiError as e:
731
+ typer.echo(f"Error: {e.message}", err=True)
732
+ raise typer.Exit(1)
733
+
734
+
735
+ @app.command(name="exchange-oauth-code")
736
+ def exchange_oauth_code(
737
+ app_name: str = typer.Argument(help="app_name"),
738
+ body_val: str = typer.Option(..., "--body", help="body"),
739
+ pretty: bool = typer.Option(False, "--pretty", help="Rich-formatted output"),
740
+ ) -> None:
741
+ """Exchange OAuth code for token. No state validation — Core already did that."""
742
+ try:
743
+ try:
744
+ data = json.loads(body_val)
745
+ except json.JSONDecodeError as e:
746
+ typer.echo(f"Error: Invalid JSON in --body: {e}", err=True)
747
+ raise typer.Exit(1)
748
+ result = api_call("POST", f"/integrations/{app_name}/exchange", json_data=data)
749
+ _output(result, pretty)
750
+ except ApiError as e:
751
+ typer.echo(f"Error: {e.message}", err=True)
752
+ raise typer.Exit(1)
753
+
754
+
755
+ if __name__ == "__main__":
756
+ app()