pragmatiks-cli 0.5.1__py3-none-any.whl → 0.12.5__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.
@@ -6,24 +6,83 @@ import json
6
6
  from pathlib import Path
7
7
  from typing import Annotated
8
8
 
9
+ import httpx
9
10
  import typer
10
11
  import yaml
11
12
  from rich import print
12
13
  from rich.console import Console
13
14
  from rich.markup import escape
15
+ from rich.table import Table
14
16
 
15
17
  from pragma_cli import get_client
16
18
  from pragma_cli.commands.completions import (
17
19
  completion_resource_ids,
18
20
  completion_resource_names,
19
21
  )
20
- from pragma_cli.helpers import parse_resource_id
22
+ from pragma_cli.helpers import OutputFormat, output_data, parse_resource_id
21
23
 
22
24
 
23
25
  console = Console()
24
26
  app = typer.Typer()
25
27
 
26
28
 
29
+ def _format_api_error(error: httpx.HTTPStatusError) -> str:
30
+ """Format an API error response with structured details.
31
+
32
+ Returns:
33
+ Formatted error message with details extracted from JSON response.
34
+ """
35
+ try:
36
+ detail = error.response.json().get("detail", {})
37
+ except (json.JSONDecodeError, ValueError):
38
+ # Fall back to plain text if not JSON
39
+ return error.response.text or str(error)
40
+
41
+ # Handle simple string details
42
+ if isinstance(detail, str):
43
+ return detail
44
+
45
+ # Handle structured error responses
46
+ message = detail.get("message", str(error))
47
+ parts = [message]
48
+
49
+ # DependencyValidationError details
50
+ if missing := detail.get("missing_dependencies"):
51
+ parts.append("\n Missing dependencies:")
52
+ for dep_id in missing:
53
+ parts.append(f" - {dep_id}")
54
+ if not_ready := detail.get("not_ready_dependencies"):
55
+ parts.append("\n Dependencies not ready:")
56
+ for item in not_ready:
57
+ if isinstance(item, dict):
58
+ parts.append(f" - {item['id']} (state: {item['state']})")
59
+ else:
60
+ parts.append(f" - {item}")
61
+
62
+ # FieldReferenceError details
63
+ if field := detail.get("field"):
64
+ ref_parts = [
65
+ detail.get("reference_provider", ""),
66
+ detail.get("reference_resource", ""),
67
+ detail.get("reference_name", ""),
68
+ ]
69
+ ref_id = "/".join(filter(None, ref_parts))
70
+ if ref_id:
71
+ parts.append(f"\n Reference: {ref_id}#{field}")
72
+
73
+ # InvalidLifecycleTransitionError details
74
+ if current_state := detail.get("current_state"):
75
+ target_state = detail.get("target_state", "unknown")
76
+ parts.append(f"\n Current state: {current_state}")
77
+ parts.append(f" Target state: {target_state}")
78
+
79
+ # ResourceInProcessingError details
80
+ if resource_id := detail.get("resource_id"):
81
+ parts.append(f"\n Resource: {resource_id}")
82
+
83
+ return "".join(parts)
84
+
85
+
27
86
  def resolve_file_references(resource: dict, base_dir: Path) -> dict:
28
87
  """Resolve file references in secret resource config.
29
88
 
@@ -55,7 +114,7 @@ def resolve_file_references(resource: dict, base_dir: Path) -> dict:
55
114
  resolved_data = {}
56
115
  for key, value in data.items():
57
116
  if isinstance(value, str) and value.startswith("@"):
58
- file_path = Path(value[1:])
117
+ file_path = Path(value[1:]).expanduser()
59
118
  if not file_path.is_absolute():
60
119
  file_path = base_dir / file_path
61
120
 
@@ -85,32 +144,288 @@ def format_state(state: str) -> str:
85
144
  return escape(f"[{state}]")
86
145
 
87
146
 
147
+ def _print_resource_types_table(types: list[dict]) -> None:
148
+ """Print resource types in a formatted table.
149
+
150
+ Args:
151
+ types: List of resource type dictionaries to display.
152
+ """
153
+ console.print()
154
+ table = Table(show_header=True, header_style="bold")
155
+ table.add_column("Provider")
156
+ table.add_column("Resource")
157
+ table.add_column("Description")
158
+
159
+ for resource_type in types:
160
+ description = resource_type.get("description") or "[dim]—[/dim]"
161
+ table.add_row(
162
+ resource_type["provider"],
163
+ resource_type["resource"],
164
+ description,
165
+ )
166
+
167
+ console.print(table)
168
+ console.print()
169
+
170
+
171
+ @app.command("types")
172
+ def list_resource_types(
173
+ provider: Annotated[str | None, typer.Option("--provider", "-p", help="Filter by provider")] = None,
174
+ output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
175
+ ):
176
+ """List available resource types from deployed providers.
177
+
178
+ Displays resource definitions (types) that have been registered by providers.
179
+ Use this to discover what resources you can create.
180
+
181
+ Examples:
182
+ pragma resources types
183
+ pragma resources types --provider gcp
184
+ pragma resources types -o json
185
+
186
+ Raises:
187
+ typer.Exit: If an error occurs while fetching resource types.
188
+ """
189
+ client = get_client()
190
+ try:
191
+ types = client.list_resource_types(provider=provider)
192
+ except httpx.HTTPStatusError as e:
193
+ console.print(f"[red]Error:[/red] {_format_api_error(e)}")
194
+ raise typer.Exit(1)
195
+
196
+ if not types:
197
+ console.print("[dim]No resource types found.[/dim]")
198
+ return
199
+
200
+ output_data(types, output, table_renderer=_print_resource_types_table)
201
+
202
+
88
203
  @app.command("list")
89
204
  def list_resources(
90
205
  provider: Annotated[str | None, typer.Option("--provider", "-p", help="Filter by provider")] = None,
91
206
  resource: Annotated[str | None, typer.Option("--resource", "-r", help="Filter by resource type")] = None,
92
207
  tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Filter by tags")] = None,
208
+ output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
93
209
  ):
94
- """List resources, optionally filtered by provider, resource type, or tags."""
210
+ """List resources, optionally filtered by provider, resource type, or tags.
211
+
212
+ Examples:
213
+ pragma resources list
214
+ pragma resources list --provider gcp
215
+ pragma resources list -o json
216
+ """
95
217
  client = get_client()
96
- for res in client.list_resources(provider=provider, resource=resource, tags=tags):
97
- print(f"{res['provider']}/{res['resource']}/{res['name']} {format_state(res['lifecycle_state'])}")
218
+ resources = list(client.list_resources(provider=provider, resource=resource, tags=tags))
219
+
220
+ if not resources:
221
+ console.print("[dim]No resources found.[/dim]")
222
+ return
223
+
224
+ output_data(resources, output, table_renderer=_print_resources_table)
225
+
226
+
227
+ def _print_resources_table(resources: list[dict]) -> None:
228
+ """Print resources in a formatted table.
229
+
230
+ Args:
231
+ resources: List of resource dictionaries to display.
232
+ """
233
+ table = Table(show_header=True, header_style="bold")
234
+ table.add_column("Provider")
235
+ table.add_column("Resource")
236
+ table.add_column("Name")
237
+ table.add_column("State")
238
+ table.add_column("Updated")
239
+
240
+ # Track failed resources to show errors after table
241
+ failed_resources: list[tuple[str, str]] = []
242
+
243
+ for res in resources:
244
+ state = _format_state_color(res["lifecycle_state"])
245
+ updated = res.get("updated_at", "[dim]-[/dim]")
246
+ if updated and updated != "[dim]-[/dim]":
247
+ # Truncate to datetime portion if it's a full ISO string
248
+ updated = updated[:19].replace("T", " ") if len(updated) > 19 else updated
249
+
250
+ table.add_row(
251
+ res["provider"],
252
+ res["resource"],
253
+ res["name"],
254
+ state,
255
+ updated,
256
+ )
257
+
258
+ # Track failed resources for error display
259
+ if res.get("lifecycle_state") == "failed" and res.get("error"):
260
+ resource_id = f"{res['provider']}/{res['resource']}/{res['name']}"
261
+ failed_resources.append((resource_id, res["error"]))
262
+
263
+ console.print(table)
264
+
265
+ # Show errors for failed resources below the table
266
+ for resource_id, error in failed_resources:
267
+ console.print(f" [red]{resource_id}:[/red] {escape(error)}")
98
268
 
99
269
 
100
270
  @app.command()
101
271
  def get(
102
272
  resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
103
273
  name: Annotated[str | None, typer.Argument(autocompletion=completion_resource_names)] = None,
274
+ output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
104
275
  ):
105
- """Get resources by provider/resource type, optionally filtered by name."""
276
+ """Get resources by provider/resource type, optionally filtered by name.
277
+
278
+ Examples:
279
+ pragma resources get gcp/secret
280
+ pragma resources get gcp/secret my-secret
281
+ pragma resources get gcp/secret my-secret -o json
282
+ """
106
283
  client = get_client()
107
284
  provider, resource = parse_resource_id(resource_id)
108
285
  if name:
109
286
  res = client.get_resource(provider=provider, resource=resource, name=name)
110
- print(f"{resource_id}/{res['name']} {format_state(res['lifecycle_state'])}")
287
+ output_data([res], output, table_renderer=_print_resources_table)
111
288
  else:
112
- for res in client.list_resources(provider=provider, resource=resource):
113
- print(f"{resource_id}/{res['name']} {format_state(res['lifecycle_state'])}")
289
+ resources = list(client.list_resources(provider=provider, resource=resource))
290
+ if not resources:
291
+ console.print("[dim]No resources found.[/dim]")
292
+ return
293
+ output_data(resources, output, table_renderer=_print_resources_table)
294
+
295
+
296
+ def _format_state_color(state: str) -> str:
297
+ """Format lifecycle state with color markup.
298
+
299
+ Returns:
300
+ State string wrapped in Rich color markup.
301
+ """
302
+ state_colors = {
303
+ "draft": "dim",
304
+ "pending": "yellow",
305
+ "processing": "cyan",
306
+ "ready": "green",
307
+ "failed": "red",
308
+ }
309
+ color = state_colors.get(state.lower(), "white")
310
+ return f"[{color}]{state}[/{color}]"
311
+
312
+
313
+ def _format_config_value(value, *, redact_keys: set[str] | None = None) -> str:
314
+ """Format a config value, redacting sensitive fields.
315
+
316
+ Returns:
317
+ Formatted string representation with sensitive values masked.
318
+ """
319
+ redact_keys = redact_keys or {"credentials", "password", "secret", "token", "key", "data"}
320
+ if isinstance(value, dict):
321
+ # Check if this is a FieldReference
322
+ if "provider" in value and "resource" in value and "name" in value and "field" in value:
323
+ return f"{value['provider']}/{value['resource']}/{value['name']}#{value['field']}"
324
+ # Recursively format nested dicts
325
+ formatted = {}
326
+ for k, v in value.items():
327
+ if k.lower() in redact_keys:
328
+ formatted[k] = "********"
329
+ else:
330
+ formatted[k] = _format_config_value(v, redact_keys=redact_keys)
331
+ return str(formatted)
332
+ elif isinstance(value, list):
333
+ return str([_format_config_value(v, redact_keys=redact_keys) for v in value])
334
+ return str(value)
335
+
336
+
337
+ def _print_resource_details(res: dict) -> None:
338
+ """Print resource details in a formatted table."""
339
+ resource_id = f"{res['provider']}/{res['resource']}/{res['name']}"
340
+
341
+ console.print()
342
+ console.print(f"[bold]Resource:[/bold] {resource_id}")
343
+ console.print()
344
+
345
+ # Main properties table
346
+ table = Table(show_header=True, header_style="bold")
347
+ table.add_column("Property")
348
+ table.add_column("Value")
349
+
350
+ # State with color
351
+ table.add_row("State", _format_state_color(res["lifecycle_state"]))
352
+
353
+ # Error if failed
354
+ if res.get("error"):
355
+ table.add_row("Error", f"[red]{escape(res['error'])}[/red]")
356
+
357
+ # Timestamps
358
+ if res.get("created_at"):
359
+ table.add_row("Created", res["created_at"])
360
+ if res.get("updated_at"):
361
+ table.add_row("Updated", res["updated_at"])
362
+
363
+ console.print(table)
364
+
365
+ # Config section
366
+ config = res.get("config", {})
367
+ if config:
368
+ console.print()
369
+ console.print("[bold]Config:[/bold]")
370
+ for key, value in config.items():
371
+ formatted = _format_config_value(value)
372
+ console.print(f" {key}: {formatted}")
373
+
374
+ # Outputs section
375
+ outputs = res.get("outputs", {})
376
+ if outputs:
377
+ console.print()
378
+ console.print("[bold]Outputs:[/bold]")
379
+ for key, value in outputs.items():
380
+ console.print(f" {key}: {value}")
381
+
382
+ # Dependencies section
383
+ dependencies = res.get("dependencies", [])
384
+ if dependencies:
385
+ console.print()
386
+ console.print("[bold]Dependencies:[/bold]")
387
+ for dep in dependencies:
388
+ dep_id = f"{dep['provider']}/{dep['resource']}/{dep['name']}"
389
+ console.print(f" - {dep_id}")
390
+
391
+ # Tags section
392
+ tags = res.get("tags", [])
393
+ if tags:
394
+ console.print()
395
+ console.print("[bold]Tags:[/bold]")
396
+ console.print(f" {', '.join(tags)}")
397
+
398
+ console.print()
399
+
400
+
401
+ @app.command()
402
+ def describe(
403
+ resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
404
+ name: Annotated[str, typer.Argument(autocompletion=completion_resource_names)],
405
+ output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format")] = OutputFormat.TABLE,
406
+ ):
407
+ """Show detailed information about a resource.
408
+
409
+ Displays the resource's config, outputs, dependencies, and error messages.
410
+
411
+ Examples:
412
+ pragma resources describe gcp/secret my-test-secret
413
+ pragma resources describe postgres/database my-db
414
+ pragma resources describe gcp/secret my-secret -o json
415
+
416
+ Raises:
417
+ typer.Exit: If the resource is not found or an error occurs.
418
+ """
419
+ client = get_client()
420
+ provider, resource = parse_resource_id(resource_id)
421
+
422
+ try:
423
+ res = client.get_resource(provider=provider, resource=resource, name=name)
424
+ except httpx.HTTPStatusError as e:
425
+ console.print(f"[red]Error:[/red] {_format_api_error(e)}")
426
+ raise typer.Exit(1)
427
+
428
+ output_data(res, output, table_renderer=_print_resource_details)
114
429
 
115
430
 
116
431
  @app.command()
@@ -128,6 +443,9 @@ def apply(
128
443
  For pragma/secret resources, file references in config.data values
129
444
  are resolved before submission. Use '@path/to/file' syntax to inline
130
445
  file contents.
446
+
447
+ Raises:
448
+ typer.Exit: If the apply operation fails.
131
449
  """
132
450
  client = get_client()
133
451
  for f in file:
@@ -138,9 +456,14 @@ def apply(
138
456
  resource = resolve_file_references(resource, base_dir)
139
457
  if pending:
140
458
  resource["lifecycle_state"] = "pending"
141
- result = client.apply_resource(resource=resource)
142
- res_id = f"{result['provider']}/{result['resource']}/{result['name']}"
143
- print(f"Applied {res_id} {format_state(result['lifecycle_state'])}")
459
+ res_id = f"{resource.get('provider', '?')}/{resource.get('resource', '?')}/{resource.get('name', '?')}"
460
+ try:
461
+ result = client.apply_resource(resource=resource)
462
+ res_id = f"{result['provider']}/{result['resource']}/{result['name']}"
463
+ print(f"Applied {res_id} {format_state(result['lifecycle_state'])}")
464
+ except httpx.HTTPStatusError as e:
465
+ console.print(f"[red]Error applying {res_id}:[/red] {_format_api_error(e)}")
466
+ raise typer.Exit(1)
144
467
 
145
468
 
146
469
  @app.command()
@@ -148,52 +471,16 @@ def delete(
148
471
  resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
149
472
  name: Annotated[str, typer.Argument(autocompletion=completion_resource_names)],
150
473
  ):
151
- """Delete a resource."""
152
- client = get_client()
153
- provider, resource = parse_resource_id(resource_id)
154
- client.delete_resource(provider=provider, resource=resource, name=name)
155
- print(f"Deleted {resource_id}/{name}")
156
-
157
-
158
- @app.command()
159
- def register(
160
- resource_id: Annotated[str, typer.Argument(help="Resource type in provider/resource format")],
161
- description: Annotated[str | None, typer.Option("--description", "-d", help="Resource type description")] = None,
162
- schema_file: Annotated[typer.FileText | None, typer.Option("--schema", "-s", help="JSON schema file")] = None,
163
- tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Tags for categorization")] = None,
164
- ):
165
- """Register a new resource type.
474
+ """Delete a resource.
166
475
 
167
- Registers a resource type so that resources of this type can be created.
168
- Providers use this to declare what resources they can manage.
169
- """
170
- client = get_client()
171
- provider, resource = parse_resource_id(resource_id)
172
-
173
- schema = None
174
- if schema_file:
175
- schema = json.load(schema_file)
176
-
177
- client.register_resource(
178
- provider=provider,
179
- resource=resource,
180
- schema=schema,
181
- description=description,
182
- tags=tags,
183
- )
184
- print(f"Registered {resource_id}")
185
-
186
-
187
- @app.command()
188
- def unregister(
189
- resource_id: Annotated[str, typer.Argument(autocompletion=completion_resource_ids)],
190
- ):
191
- """Unregister a resource type.
192
-
193
- Removes a resource type registration. Existing resources of this type
194
- will no longer be manageable.
476
+ Raises:
477
+ typer.Exit: If the resource is not found or deletion fails.
195
478
  """
196
479
  client = get_client()
197
480
  provider, resource = parse_resource_id(resource_id)
198
- client.unregister_resource(provider=provider, resource=resource)
199
- print(f"Unregistered {resource_id}")
481
+ try:
482
+ client.delete_resource(provider=provider, resource=resource, name=name)
483
+ print(f"Deleted {resource_id}/{name}")
484
+ except httpx.HTTPStatusError as e:
485
+ console.print(f"[red]Error deleting {resource_id}/{name}:[/red] {_format_api_error(e)}")
486
+ raise typer.Exit(1)
pragma_cli/config.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from pathlib import Path
7
+ from urllib.parse import urlparse
7
8
 
8
9
  import yaml
9
10
  from pydantic import BaseModel
@@ -30,6 +31,24 @@ class ContextConfig(BaseModel):
30
31
  """Configuration for a single CLI context."""
31
32
 
32
33
  api_url: str
34
+ auth_url: str | None = None
35
+
36
+ def get_auth_url(self) -> str:
37
+ """Get the auth URL, deriving from api_url if not explicitly set.
38
+
39
+ Returns:
40
+ Auth URL for Clerk authentication.
41
+ """
42
+ if self.auth_url:
43
+ return self.auth_url
44
+
45
+ # Handle localhost: default to port 3000 for web app
46
+ parsed = urlparse(self.api_url)
47
+ if parsed.hostname in ("localhost", "127.0.0.1"):
48
+ return "http://localhost:3000"
49
+
50
+ # Derive from api_url: api.pragmatiks.io -> app.pragmatiks.io
51
+ return self.api_url.replace("://api.", "://app.")
33
52
 
34
53
 
35
54
  class PragmaConfig(BaseModel):
pragma_cli/helpers.py CHANGED
@@ -1,7 +1,46 @@
1
- """CLI helper functions for parsing resource identifiers."""
1
+ """CLI helper functions for parsing resource identifiers and output formatting."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+ from enum import StrEnum
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import yaml
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+
16
+ class OutputFormat(StrEnum):
17
+ """Output format options for CLI commands."""
18
+
19
+ TABLE = "table"
20
+ JSON = "json"
21
+ YAML = "yaml"
22
+
23
+
24
+ def output_data(
25
+ data: list[dict[str, Any]] | dict[str, Any],
26
+ format: OutputFormat,
27
+ table_renderer: Callable[..., None] | None = None,
28
+ ) -> None:
29
+ """Output data in the specified format.
30
+
31
+ Args:
32
+ data: Data to output (list of dicts or single dict).
33
+ format: Output format (table, json, yaml).
34
+ table_renderer: Function to render table output. Required for TABLE format.
35
+ """
36
+ if format == OutputFormat.TABLE:
37
+ if table_renderer:
38
+ table_renderer(data)
39
+ elif format == OutputFormat.JSON:
40
+ print(json.dumps(data, indent=2, default=str))
41
+ elif format == OutputFormat.YAML:
42
+ print(yaml.dump(data, default_flow_style=False, sort_keys=False))
43
+
5
44
 
6
45
  def parse_resource_id(resource_id: str) -> tuple[str, str]:
7
46
  """Parse resource identifier into provider and resource type.
pragma_cli/main.py CHANGED
@@ -1,21 +1,47 @@
1
1
  """CLI entry point with Typer application setup and command routing."""
2
2
 
3
+ from importlib.metadata import version as get_version
3
4
  from typing import Annotated
4
5
 
5
6
  import typer
6
7
  from pragma_sdk import PragmaClient
7
8
 
8
9
  from pragma_cli import set_client
9
- from pragma_cli.commands import auth, config, ops, provider, resources
10
+ from pragma_cli.commands import auth, config, ops, providers, resources
10
11
  from pragma_cli.config import get_current_context
11
12
 
12
13
 
13
14
  app = typer.Typer()
14
15
 
15
16
 
17
+ def _version_callback(value: bool) -> None:
18
+ """Print version and exit if --version flag is provided.
19
+
20
+ Args:
21
+ value: True if --version flag was provided.
22
+
23
+ Raises:
24
+ typer.Exit: Always exits after displaying version.
25
+ """
26
+ if value:
27
+ package_version = get_version("pragmatiks-cli")
28
+ typer.echo(f"pragma {package_version}")
29
+ raise typer.Exit()
30
+
31
+
16
32
  @app.callback()
17
33
  def main(
18
34
  ctx: typer.Context,
35
+ version: Annotated[
36
+ bool | None,
37
+ typer.Option(
38
+ "--version",
39
+ "-V",
40
+ help="Show version and exit",
41
+ callback=_version_callback,
42
+ is_eager=True,
43
+ ),
44
+ ] = None,
19
45
  context: Annotated[
20
46
  str | None,
21
47
  typer.Option(
@@ -37,14 +63,14 @@ def main(
37
63
  """Pragma CLI - Declarative resource management.
38
64
 
39
65
  Authentication (industry-standard pattern):
40
- - CLI writes credentials: 'pragma login' stores tokens in ~/.config/pragma/credentials
66
+ - CLI writes credentials: 'pragma auth login' stores tokens in ~/.config/pragma/credentials
41
67
  - SDK reads credentials: Automatic token discovery via precedence chain
42
68
 
43
69
  Token Discovery Precedence:
44
70
  1. --token flag (explicit override)
45
71
  2. PRAGMA_AUTH_TOKEN_<CONTEXT> context-specific environment variable
46
72
  3. PRAGMA_AUTH_TOKEN environment variable
47
- 4. ~/.config/pragma/credentials file (from pragma login)
73
+ 4. ~/.config/pragma/credentials file (from pragma auth login)
48
74
  5. No authentication
49
75
  """
50
76
  context_name, context_config = get_current_context(context)
@@ -61,7 +87,7 @@ app.add_typer(resources.app, name="resources")
61
87
  app.add_typer(auth.app, name="auth")
62
88
  app.add_typer(config.app, name="config")
63
89
  app.add_typer(ops.app, name="ops")
64
- app.add_typer(provider.app, name="provider")
90
+ app.add_typer(providers.app, name="providers")
65
91
 
66
92
  if __name__ == "__main__": # pragma: no cover
67
93
  app()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pragmatiks-cli
3
- Version: 0.5.1
3
+ Version: 0.12.5
4
4
  Summary: Command-line interface for Pragmatiks
5
5
  Requires-Dist: typer>=0.15.3
6
6
  Requires-Dist: pragmatiks-sdk>=0.6.0
@@ -10,19 +10,21 @@ Requires-Dist: rich>=13.9.0
10
10
  Requires-Python: >=3.13
11
11
  Description-Content-Type: text/markdown
12
12
 
13
- # Pragmatiks CLI
13
+ <p align="center">
14
+ <img src="assets/wordmark.png" alt="Pragma-OS" width="800">
15
+ </p>
14
16
 
15
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pragmatiks/cli)
17
+ # Pragma CLI
18
+
19
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pragmatiks/pragma-cli)
16
20
  [![PyPI version](https://img.shields.io/pypi/v/pragmatiks-cli.svg)](https://pypi.org/project/pragmatiks-cli/)
17
21
  [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
18
22
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
19
23
  [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
20
24
 
21
- **[Documentation](https://docs.pragmatiks.io/cli/overview)** | **[SDK](https://github.com/pragmatiks/sdk)** | **[Providers](https://github.com/pragmatiks/providers)**
22
-
23
- Command-line interface for managing Pragmatiks resources.
25
+ **[Documentation](https://docs.pragmatiks.io/cli/overview)** | **[SDK](https://github.com/pragmatiks/pragma-sdk)** | **[Providers](https://github.com/pragmatiks/pragma-providers)**
24
26
 
25
- <!-- TODO: Add logo and demo GIF -->
27
+ Command-line interface for managing pragma-os resources.
26
28
 
27
29
  ## Quick Start
28
30
 
@@ -0,0 +1,17 @@
1
+ pragma_cli/__init__.py,sha256=9REbOdKs9CeuOd-rxeFs17gWtou1dUdCogYU8G5Cz6c,682
2
+ pragma_cli/commands/__init__.py,sha256=zltFPaCZgkeTdOH1YWrUEqqBF9Dg6tokgAFcmqP4_n4,24
3
+ pragma_cli/commands/auth.py,sha256=ADpPH8jDbpwhyRzwvG_4PAg2oxVbt2vGx1LrAZNIq-I,9968
4
+ pragma_cli/commands/completions.py,sha256=0SFfq1V0QLjLc4Kr6wcRvQF0UQpB9lZ6V5fU6SVtsbM,3610
5
+ pragma_cli/commands/config.py,sha256=fq4G6DwgTaEZLZIcGdLXzEh4Zrv6M6_ew0IU7WytQk0,2648
6
+ pragma_cli/commands/dead_letter.py,sha256=8Mh_QVZiwkbOA1fYkw1O9BeHgPdqepis6tSJOAY3vhA,6754
7
+ pragma_cli/commands/ops.py,sha256=ztx0Gx2L2mEqJQpbgDHgfOUZ4uaD132NxgKohaPOWv8,361
8
+ pragma_cli/commands/providers.py,sha256=Ab7Oh-WGYvl8CauGiG3e2bFJVFo4kB4NbHXsolsFNoo,29265
9
+ pragma_cli/commands/resources.py,sha256=HpgxD-Rx8IWk1CBVwRcxuhEYd8xe0vqSrsPwfzotiJU,16533
10
+ pragma_cli/config.py,sha256=2r9kcBrh700AWF0H3gMIpYX8uXFSe2Yk2T8tuXzjCaU,2984
11
+ pragma_cli/helpers.py,sha256=lR-s_Q7YNuWcPeluHZO3RrsdpKq8ndwWYLSlzRvY35w,1681
12
+ pragma_cli/main.py,sha256=_S2X3QLfuGcULBfwezp4RBK1_PFkHY_L_8j0hZbT2gk,2676
13
+ pragma_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ pragmatiks_cli-0.12.5.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
15
+ pragmatiks_cli-0.12.5.dist-info/entry_points.txt,sha256=9xeQQlnHxq94dks6mlJ2I9LuMUKmqxuJzyKSZCb9iJM,48
16
+ pragmatiks_cli-0.12.5.dist-info/METADATA,sha256=E8HFQsJjup_TPTswEsrHq8MFuKm8HpPdt5MoRCZWXF0,4466
17
+ pragmatiks_cli-0.12.5.dist-info/RECORD,,