cliyard 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. cliyard-0.3.2/LICENSE +21 -0
  2. cliyard-0.3.2/PKG-INFO +120 -0
  3. cliyard-0.3.2/README.md +92 -0
  4. cliyard-0.3.2/pyproject.toml +40 -0
  5. cliyard-0.3.2/setup.cfg +4 -0
  6. cliyard-0.3.2/src/cliyard/__init__.py +9 -0
  7. cliyard-0.3.2/src/cliyard/cli/__init__.py +1 -0
  8. cliyard-0.3.2/src/cliyard/cli/__main__.py +96 -0
  9. cliyard-0.3.2/src/cliyard/cli/auth_cmd.py +167 -0
  10. cliyard-0.3.2/src/cliyard/cli/gen.py +412 -0
  11. cliyard-0.3.2/src/cliyard/client/__init__.py +4 -0
  12. cliyard-0.3.2/src/cliyard/client/auth.py +523 -0
  13. cliyard-0.3.2/src/cliyard/client/credentials.py +154 -0
  14. cliyard-0.3.2/src/cliyard/client/http.py +113 -0
  15. cliyard-0.3.2/src/cliyard/engine/__init__.py +22 -0
  16. cliyard-0.3.2/src/cliyard/engine/assembler.py +281 -0
  17. cliyard-0.3.2/src/cliyard/engine/binder.py +112 -0
  18. cliyard-0.3.2/src/cliyard/engine/builder.py +595 -0
  19. cliyard-0.3.2/src/cliyard/engine/error_handler.py +24 -0
  20. cliyard-0.3.2/src/cliyard/engine/errors.py +46 -0
  21. cliyard-0.3.2/src/cliyard/engine/hooks.py +28 -0
  22. cliyard-0.3.2/src/cliyard/engine/loader.py +218 -0
  23. cliyard-0.3.2/src/cliyard/engine/template.py +212 -0
  24. cliyard-0.3.2/src/cliyard/output/__init__.py +6 -0
  25. cliyard-0.3.2/src/cliyard/output/formatter.py +187 -0
  26. cliyard-0.3.2/src/cliyard/output/handler.py +96 -0
  27. cliyard-0.3.2/src/cliyard/plugin/__init__.py +195 -0
  28. cliyard-0.3.2/src/cliyard/plugin/discovery.py +107 -0
  29. cliyard-0.3.2/src/cliyard/runtime/__init__.py +25 -0
  30. cliyard-0.3.2/src/cliyard/runtime/auth_commands.py +195 -0
  31. cliyard-0.3.2/src/cliyard/runtime/runner.py +241 -0
  32. cliyard-0.3.2/src/cliyard/schema/__init__.py +34 -0
  33. cliyard-0.3.2/src/cliyard/schema/types.py +237 -0
  34. cliyard-0.3.2/src/cliyard/schema/validator.py +205 -0
  35. cliyard-0.3.2/src/cliyard/validate/__init__.py +17 -0
  36. cliyard-0.3.2/src/cliyard/validate/dependency.py +63 -0
  37. cliyard-0.3.2/src/cliyard/validate/types.py +291 -0
  38. cliyard-0.3.2/src/cliyard.egg-info/PKG-INFO +120 -0
  39. cliyard-0.3.2/src/cliyard.egg-info/SOURCES.txt +46 -0
  40. cliyard-0.3.2/src/cliyard.egg-info/dependency_links.txt +1 -0
  41. cliyard-0.3.2/src/cliyard.egg-info/entry_points.txt +2 -0
  42. cliyard-0.3.2/src/cliyard.egg-info/requires.txt +6 -0
  43. cliyard-0.3.2/src/cliyard.egg-info/top_level.txt +1 -0
  44. cliyard-0.3.2/tests/test_auth.py +517 -0
  45. cliyard-0.3.2/tests/test_integration.py +671 -0
  46. cliyard-0.3.2/tests/test_schema_types.py +74 -0
  47. cliyard-0.3.2/tests/test_validate_dependency.py +146 -0
  48. cliyard-0.3.2/tests/test_validate_types.py +301 -0
cliyard-0.3.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 keta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cliyard-0.3.2/PKG-INFO ADDED
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: cliyard
3
+ Version: 0.3.2
4
+ Summary: YAML-driven CLI framework — turn any REST API into CLI commands
5
+ Author-email: keta <dev@ketaops.cc>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/guolong123/cliyard
8
+ Project-URL: Repository, https://github.com/guolong123/cliyard.git
9
+ Keywords: cli,yaml,rest-api,codegen,click
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Software Development :: Code Generators
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: click>=8.1.0
22
+ Requires-Dist: pyyaml~=6.0
23
+ Requires-Dist: requests~=2.31.0
24
+ Requires-Dist: jinja2~=3.1
25
+ Requires-Dist: rich>=13.7.0
26
+ Requires-Dist: jsonpath-ng~=1.7.0
27
+ Dynamic: license-file
28
+
29
+ # cliyard
30
+
31
+ **CLI + YAML + Yard** — a framework that generates CLI commands from YAML specs. Define your REST API in a few YAML files, and cliyard produces a fully-structured Click CLI with parameters, types, and auto-generated help. Originally inspired by [ketacli](https://gitee.com/xishuhq/ketacli)'s YAML-driven architecture, now generalized for any REST API.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install cliyard
37
+ ```
38
+
39
+ For local development:
40
+
41
+ ```bash
42
+ pip install -e .
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```bash
48
+ # Explore the example spec
49
+ cliyard --spec-dir examples/ketacli-repos/ repos list --help
50
+
51
+ # Usage: repos list [OPTIONS]
52
+ # --page INTEGER Page number
53
+ # --search TEXT Search keyword
54
+ # --help Show this message and exit.
55
+ ```
56
+
57
+ ## How It Works
58
+
59
+ cliyard turns a directory of YAML files into CLI commands. One directory equals one API service. Each YAML file (except `_service.yaml`) becomes a resource group with subcommands for every defined method.
60
+
61
+ ```
62
+ your-api/ ┌─────────────────────────────────┐
63
+ ├── _service.yaml ── service ──► │ cliyard Engine │
64
+ ├── repos.yaml ── resource ──► │ │
65
+ ├── users.yaml ── resource ──► │ Loader ──► Builder ──► Click │
66
+ └── ... │ │ │ │ │
67
+ │ │ Auth Chain │ │
68
+ │ ▼ ▼ ▼ │
69
+ │ YAML Commands CLI run │
70
+ │ parsing + params │
71
+ └─────────────────────────────────┘
72
+ ```
73
+
74
+ **Data flow** for a single invocation:
75
+
76
+ ```
77
+ cliyard --spec-dir examples/ketacli-repos/ repos list --page 1
78
+
79
+ 1. Loader reads _service.yaml + repos.yaml
80
+ 2. Auth chain runs (env → login → inject token)
81
+ 3. Builder generates Click commands from method specs
82
+ 4. Params are typed (int, string, enum, bool) and validated
83
+ 5. (Future) HTTP request is assembled and sent
84
+ 6. (Future) Response is formatted with Rich tables
85
+ ```
86
+
87
+ ## Directory Structure
88
+
89
+ Each service is a directory containing a `_service.yaml` for service-level config and one YAML file per resource:
90
+
91
+ ```
92
+ ketacli-repos/
93
+ ├── _service.yaml # Service config: name, server, auth
94
+ └── repos.yaml # Resource: repos (list, create, get, update, delete)
95
+ ```
96
+
97
+ - `_service.yaml` defines the server URL, authentication chain, and global settings.
98
+ - Resource files (`repos.yaml`, `users.yaml`, etc.) define the methods, parameters, and output format for each API resource.
99
+ - Files prefixed with `_service.` (like `_service.local.yaml`) are reserved for future local overrides and are ignored during loading.
100
+
101
+ ## Comparison with ketacli
102
+
103
+ | Feature | cliyard | ketacli |
104
+ |---------|---------|---------|
105
+ | API scope | Any REST API | KetaDB only |
106
+ | Config format | One directory per API service | Single YAML tree per resource |
107
+ | Auth model | Chain-based (env, login, inject) | Built-in, KetaDB-specific |
108
+ | Auth caching | In-memory TTL token cache | Session-based |
109
+ | Param types | string, int, float, bool, enum | Pre-defined per command |
110
+ | Adding resources | Add a YAML file to the directory | Requires code changes |
111
+ | CLI generation | Dynamic from spec at runtime | Pre-built Click commands |
112
+ | Purpose | General-purpose API-to-CLI framework | KetaDB management tool |
113
+
114
+ ## Creating Your Own Spec
115
+
116
+ See [examples/README.md](examples/README.md) for a step-by-step guide to creating new API specs.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,92 @@
1
+ # cliyard
2
+
3
+ **CLI + YAML + Yard** — a framework that generates CLI commands from YAML specs. Define your REST API in a few YAML files, and cliyard produces a fully-structured Click CLI with parameters, types, and auto-generated help. Originally inspired by [ketacli](https://gitee.com/xishuhq/ketacli)'s YAML-driven architecture, now generalized for any REST API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install cliyard
9
+ ```
10
+
11
+ For local development:
12
+
13
+ ```bash
14
+ pip install -e .
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Explore the example spec
21
+ cliyard --spec-dir examples/ketacli-repos/ repos list --help
22
+
23
+ # Usage: repos list [OPTIONS]
24
+ # --page INTEGER Page number
25
+ # --search TEXT Search keyword
26
+ # --help Show this message and exit.
27
+ ```
28
+
29
+ ## How It Works
30
+
31
+ cliyard turns a directory of YAML files into CLI commands. One directory equals one API service. Each YAML file (except `_service.yaml`) becomes a resource group with subcommands for every defined method.
32
+
33
+ ```
34
+ your-api/ ┌─────────────────────────────────┐
35
+ ├── _service.yaml ── service ──► │ cliyard Engine │
36
+ ├── repos.yaml ── resource ──► │ │
37
+ ├── users.yaml ── resource ──► │ Loader ──► Builder ──► Click │
38
+ └── ... │ │ │ │ │
39
+ │ │ Auth Chain │ │
40
+ │ ▼ ▼ ▼ │
41
+ │ YAML Commands CLI run │
42
+ │ parsing + params │
43
+ └─────────────────────────────────┘
44
+ ```
45
+
46
+ **Data flow** for a single invocation:
47
+
48
+ ```
49
+ cliyard --spec-dir examples/ketacli-repos/ repos list --page 1
50
+
51
+ 1. Loader reads _service.yaml + repos.yaml
52
+ 2. Auth chain runs (env → login → inject token)
53
+ 3. Builder generates Click commands from method specs
54
+ 4. Params are typed (int, string, enum, bool) and validated
55
+ 5. (Future) HTTP request is assembled and sent
56
+ 6. (Future) Response is formatted with Rich tables
57
+ ```
58
+
59
+ ## Directory Structure
60
+
61
+ Each service is a directory containing a `_service.yaml` for service-level config and one YAML file per resource:
62
+
63
+ ```
64
+ ketacli-repos/
65
+ ├── _service.yaml # Service config: name, server, auth
66
+ └── repos.yaml # Resource: repos (list, create, get, update, delete)
67
+ ```
68
+
69
+ - `_service.yaml` defines the server URL, authentication chain, and global settings.
70
+ - Resource files (`repos.yaml`, `users.yaml`, etc.) define the methods, parameters, and output format for each API resource.
71
+ - Files prefixed with `_service.` (like `_service.local.yaml`) are reserved for future local overrides and are ignored during loading.
72
+
73
+ ## Comparison with ketacli
74
+
75
+ | Feature | cliyard | ketacli |
76
+ |---------|---------|---------|
77
+ | API scope | Any REST API | KetaDB only |
78
+ | Config format | One directory per API service | Single YAML tree per resource |
79
+ | Auth model | Chain-based (env, login, inject) | Built-in, KetaDB-specific |
80
+ | Auth caching | In-memory TTL token cache | Session-based |
81
+ | Param types | string, int, float, bool, enum | Pre-defined per command |
82
+ | Adding resources | Add a YAML file to the directory | Requires code changes |
83
+ | CLI generation | Dynamic from spec at runtime | Pre-built Click commands |
84
+ | Purpose | General-purpose API-to-CLI framework | KetaDB management tool |
85
+
86
+ ## Creating Your Own Spec
87
+
88
+ See [examples/README.md](examples/README.md) for a step-by-step guide to creating new API specs.
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "cliyard"
3
+ version = "0.3.2"
4
+ description = "YAML-driven CLI framework — turn any REST API into CLI commands"
5
+ authors = [
6
+ {name = "keta", email = "dev@ketaops.cc"},
7
+ ]
8
+ license = {text = "MIT"}
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ keywords = ["cli", "yaml", "rest-api", "codegen", "click"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Topic :: Software Development :: Code Generators",
20
+ "Topic :: System :: Systems Administration",
21
+ ]
22
+ dependencies = [
23
+ "click>=8.1.0",
24
+ "pyyaml~=6.0",
25
+ "requests~=2.31.0",
26
+ "jinja2~=3.1",
27
+ "rich>=13.7.0",
28
+ "jsonpath-ng~=1.7.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/guolong123/cliyard"
33
+ Repository = "https://github.com/guolong123/cliyard.git"
34
+
35
+ [project.scripts]
36
+ cliyard = "cliyard.cli.__main__:main"
37
+
38
+ [build-system]
39
+ requires = ["setuptools>=65.0.0", "wheel"]
40
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """cliyard — YAML-driven CLI framework for any REST API.
2
+
3
+ Quick start::
4
+
5
+ pip install cliyard
6
+ from cliyard.runtime import create_cli
7
+ cli = create_cli("./my-api-spec/")
8
+ cli()
9
+ """
@@ -0,0 +1 @@
1
+ """CLI entry point and command registration."""
@@ -0,0 +1,96 @@
1
+ """cliyard CLI entry point."""
2
+
3
+ import sys
4
+
5
+ import click
6
+ from click.exceptions import NoArgsIsHelpError, MissingParameter, UsageError
7
+
8
+
9
+ def _intercept_spec_dir() -> None:
10
+ """If ``--spec-dir`` is in sys.argv, extract it and run the spec-based CLI.
11
+
12
+ This must happen *before* Click's command resolution because the
13
+ spec-driven commands (e.g. ``repos list``) don't exist as registered
14
+ Click subcommands — they are built dynamically from YAML specs.
15
+
16
+ Does NOT intercept if the command is ``auth``, ``gen``, ``init``, or ``run``
17
+ — those are native cliyard commands, not spec-driven commands.
18
+ """
19
+ if "--spec-dir" not in sys.argv:
20
+ return
21
+
22
+ # Skip interception for native cliyard commands
23
+ for arg in sys.argv[1:]:
24
+ if not arg.startswith("-") and arg in ("auth", "gen", "init", "run"):
25
+ return
26
+ if arg.startswith("-"):
27
+ continue
28
+ break
29
+
30
+ try:
31
+ idx = sys.argv.index("--spec-dir")
32
+ except ValueError:
33
+ return
34
+
35
+ if idx + 1 >= len(sys.argv):
36
+ click.echo("Error: --spec-dir requires a directory path", err=True)
37
+ sys.exit(1)
38
+
39
+ spec_dir = sys.argv[idx + 1]
40
+
41
+ sys.argv.pop(idx)
42
+ sys.argv.pop(idx)
43
+
44
+ from cliyard.runtime.runner import run_with_spec
45
+
46
+ run_with_spec(spec_dir)
47
+
48
+
49
+ @click.group()
50
+ @click.version_option()
51
+ def cli():
52
+ """cliyard — YAML-driven CLI framework for any REST API.
53
+
54
+ Turn any REST API into CLI commands by writing simple YAML specs.
55
+ """
56
+
57
+
58
+ @cli.command()
59
+ def init():
60
+ """Initialize a new cliyard spec directory."""
61
+ click.echo("Not yet implemented. Stay tuned.")
62
+
63
+
64
+ from cliyard.cli.gen import gen
65
+
66
+ cli.add_command(gen)
67
+
68
+
69
+ @cli.command()
70
+ @click.argument("spec_file")
71
+ @click.argument("resource")
72
+ @click.argument("operation", required=False)
73
+ @click.option("-e", "--extra", type=str, help="Extra args, key=val,key2=val2")
74
+ def run(spec_file, resource, operation, extra):
75
+ """Run a command from a YAML spec file."""
76
+ click.echo(f"Not yet implemented. Would run: {spec_file} {resource} {operation}")
77
+
78
+
79
+ def main():
80
+ """Entry point — intercept --spec-dir before Click, then run CLI."""
81
+ _intercept_spec_dir()
82
+ try:
83
+ cli(standalone_mode=False)
84
+ except MissingParameter as e:
85
+ click.echo(f"Error: {e}", err=True)
86
+ click.echo(f" Usage: cliyard gen --name <name> [--defs-path <path>]")
87
+ except UsageError as e:
88
+ click.echo(f"Error: {e}", err=True)
89
+ except NoArgsIsHelpError as e:
90
+ click.echo(e.format_message())
91
+ except SystemExit:
92
+ pass
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()
@@ -0,0 +1,167 @@
1
+ """cliyard.auth — Manage authentication credentials.
2
+
3
+ Provides ``cliyard auth login``, ``cliyard auth status``, and
4
+ ``cliyard auth logout`` subcommands.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import time
11
+ from pathlib import Path
12
+
13
+ import click
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+
18
+ @click.group(name="auth")
19
+ def auth_group() -> None:
20
+ """Manage authentication credentials.
21
+
22
+ Login to a service, check saved credential status, or clear
23
+ saved credentials.
24
+ """
25
+
26
+
27
+ @auth_group.command(name="login")
28
+ @click.option("--spec-dir", required=True, help="Path to YAML spec directory")
29
+ def auth_login(spec_dir: str) -> None:
30
+ """Initialize auth credentials by running the auth chain.
31
+
32
+ Reads the auth spec from ``_auth.yaml``, executes all auth steps,
33
+ and persists the resulting tokens according to the ``auth.persist``
34
+ configuration.
35
+ """
36
+ from cliyard.client.auth import run_auth_chain
37
+ from cliyard.client.credentials import save_service_credentials
38
+ from cliyard.engine.loader import load_service
39
+
40
+ console = Console()
41
+ spec_path = Path(spec_dir).resolve()
42
+
43
+ try:
44
+ service = load_service(spec_path)
45
+ except Exception as exc:
46
+ console.print(f"[red]Error loading spec: {exc}[/red]")
47
+ return
48
+
49
+ auth_spec = service.get("auth")
50
+ if not auth_spec:
51
+ console.print("[red]No auth config found in _auth.yaml[/red]")
52
+ return
53
+
54
+ # Run the auth chain (no pre-filled — full re-login)
55
+ try:
56
+ from cliyard.client.http import HttpClient
57
+
58
+ server = service.get("server", {})
59
+ base_url = server.get("base_url", "http://localhost:8080")
60
+ client = HttpClient(base_url)
61
+ auth_state = run_auth_chain(auth_spec, http_client=client)
62
+ except Exception as exc:
63
+ console.print(f"[red]Auth failed: {exc}[/red]")
64
+ return
65
+
66
+ # Save based on persist config
67
+ persist = auth_spec.get("persist", {})
68
+ if persist.get("to") == "cliyard-config" or not persist.get("to"):
69
+ service_id = auth_spec.get("id", service.get("name", "default"))
70
+ fields: dict[str, str | int] = {}
71
+
72
+ persist_fields = persist.get("fields", {})
73
+ for field_name, field_config in persist_fields.items():
74
+ ref: str = field_config.get("from", "")
75
+ default = field_config.get("default")
76
+
77
+ if "." in ref:
78
+ step, key = ref.split(".", 1)
79
+ step_value = auth_state.get(step)
80
+ if isinstance(step_value, dict):
81
+ value = step_value.get(key)
82
+ else:
83
+ value = None
84
+ if value is not None:
85
+ fields[field_name] = value
86
+ elif default is not None:
87
+ fields[field_name] = default
88
+ else:
89
+ # Direct reference: field_name = step_name
90
+ value = auth_state.get(ref)
91
+ if value is not None:
92
+ fields[field_name] = value
93
+ elif default is not None:
94
+ fields[field_name] = default
95
+
96
+ # Auto-compute expires_at from ttl if present
97
+ # Look for a "ttl" key in the extracted auth state
98
+ ttl_value: int | None = None
99
+ for step_name, step_value in auth_state.items():
100
+ if isinstance(step_value, dict) and "ttl" in step_value:
101
+ ttl_value = step_value["ttl"]
102
+ break
103
+ if ttl_value:
104
+ fields["expires_at"] = int(time.time()) + int(ttl_value)
105
+
106
+ if fields:
107
+ save_service_credentials(service_id, fields)
108
+ console.print(f"[green]Credentials saved for '{service_id}'[/green]")
109
+ else:
110
+ console.print("[yellow]No fields configured for persistence.[/yellow]")
111
+ console.print(f"[dim]auth_state: {auth_state}[/dim]")
112
+ else:
113
+ console.print(f"[yellow]Persistence target {persist.get('to')!r} not supported yet.[/yellow]")
114
+
115
+
116
+ @auth_group.command(name="status")
117
+ def auth_status() -> None:
118
+ """Show saved authentication status."""
119
+ from cliyard.client.credentials import load_credentials
120
+
121
+ console = Console()
122
+ creds = load_credentials()
123
+ services = creds.get("services", {})
124
+ if not services:
125
+ console.print("[yellow]No credentials saved.[/yellow]")
126
+ return
127
+
128
+ table = Table()
129
+ table.add_column("Service")
130
+ table.add_column("Fields")
131
+ table.add_column("Expires")
132
+
133
+ for svc_id, fields in services.items():
134
+ field_names = [k for k in fields.keys() if k != "expires_at"]
135
+ expires_at = fields.get("expires_at")
136
+ if expires_at:
137
+ remaining = int(expires_at) - int(time.time())
138
+ if remaining > 0:
139
+ hours = remaining // 3600
140
+ minutes = (remaining % 3600) // 60
141
+ expiry = f"{hours}h {minutes}m remaining"
142
+ else:
143
+ expiry = "[red]EXPIRED[/red]"
144
+ else:
145
+ expiry = "never"
146
+ table.add_row(svc_id, ", ".join(field_names), expiry)
147
+
148
+ console.print(table)
149
+
150
+
151
+ @auth_group.command(name="logout")
152
+ @click.option("--service", "-s", help="Service ID to logout (default: all)")
153
+ def auth_logout(service: str | None) -> None:
154
+ """Clear saved credentials."""
155
+ console = Console()
156
+ if service:
157
+ from cliyard.client.credentials import clear_service_credentials
158
+
159
+ clear_service_credentials(service)
160
+ console.print(f"[green]Credentials cleared for '{service}'[/green]")
161
+ else:
162
+ creds_path = os.path.expanduser("~/.cliyard/credentials.yaml")
163
+ if os.path.exists(creds_path):
164
+ os.remove(creds_path)
165
+ console.print("[green]All credentials cleared.[/green]")
166
+ else:
167
+ console.print("[yellow]No credentials to clear.[/yellow]")