feat: implement common_options decorator to support global CLI flags across all commands

This commit is contained in:
beubax
2026-04-21 14:52:37 +05:30
parent 9801774df5
commit 72d08ed934
6 changed files with 54 additions and 13 deletions
+1 -1
View File
@@ -80,4 +80,4 @@ profile:<profile>:<provider>:state
`ClientConfig` supports `env:VAR_NAME` syntax so client credentials can come from environment variables without hardcoding them in the provider JSON.
**CLI (`src/authsome/cli.py`)** is Click-based. All commands support `--profile` to override the active profile.
**CLI (`src/authsome/cli.py`)** is Click-based. All commands support `--json` for machine-readable output and `--profile` to override the active profile.
+4 -3
View File
@@ -47,7 +47,7 @@ $AUTHSOME init
**Goal:** Find the provider and check for existing connections.
```bash
$AUTHSOME list
$AUTHSOME list --json
```
This returns `bundled` and `custom` provider arrays, each with `name`, `auth_type`, and `connections`.
@@ -73,7 +73,7 @@ If the provider supports multiple OAuth2 flows, choose one:
3. **Only one flow** → Use the provider's default.
4. **API key provider** → Flow is already determined (`api_key`).
Use `$AUTHSOME inspect <provider>` to check `oauth.supports_dcr`, `oauth.supports_device_flow`, and the default `flow`.
Use `$AUTHSOME inspect <provider> --json` to check `oauth.supports_dcr`, `oauth.supports_device_flow`, and the default `flow`.
### Step 2.2: Choose a connection name
@@ -107,7 +107,7 @@ $AUTHSOME login openai --api-key "sk-..."
### Step 2.4: Verify
```bash
$AUTHSOME get <provider>
$AUTHSOME get <provider> --json
```
Confirm `status` is `"connected"`.
@@ -156,6 +156,7 @@ TOKEN=$($AUTHSOME get <provider> --field access_token --show-secret)
## Best Practices
- **Always use `--json`** when parsing CLI output programmatically.
- **Prefer `authsome run`** over exporting secrets — it is more secure and ephemeral.
- **Never log or echo secrets** unless the user explicitly asks.
- **Re-use existing connections** — always check before starting a new login.
+46 -7
View File
@@ -44,6 +44,34 @@ class ContextObj:
pass_ctx = click.make_pass_decorator(ContextObj)
def common_options(f):
"""Decorator to add common global options to both group and subcommands."""
@click.option("--json", "json_output", is_flag=True, help="Output in machine-readable JSON format.")
@click.option("--quiet", is_flag=True, help="Suppress non-essential output.")
@click.option("--no-color", is_flag=True, help="Disable ANSI colors.")
@functools.wraps(f)
def wrapper(*args, **kwargs):
json_output = kwargs.pop("json_output", False)
quiet = kwargs.pop("quiet", False)
no_color = kwargs.pop("no_color", False)
ctx = click.get_current_context()
if getattr(ctx, "obj", None) is None:
ctx.obj = ContextObj(json_output, quiet, no_color)
else:
if json_output:
ctx.obj.json_output = True
if quiet:
ctx.obj.quiet = True
if no_color:
ctx.obj.no_color = True
return f(*args, **kwargs)
return wrapper
def format_error_code(exc: Exception) -> int:
"""Map exceptions to standard exit codes per spec §18.3."""
if not isinstance(exc, AuthsomeError):
@@ -86,17 +114,15 @@ def handle_errors(func):
@click.group()
@click.option("--json", "json_output", is_flag=True, help="Output in machine-readable JSON format.")
@click.option("--quiet", is_flag=True, help="Suppress non-essential output.")
@click.option("--no-color", is_flag=True, help="Disable ANSI colors.")
@common_options
@click.pass_context
def cli(ctx: click.Context, json_output: bool, quiet: bool, no_color: bool) -> None:
def cli(ctx: click.Context) -> None:
"""Authsome: Portable local authentication library for AI agents and tools."""
logging.getLogger("authsome").setLevel(logging.WARNING if quiet else logging.INFO)
ctx.obj = ContextObj(json_output, quiet, no_color)
logging.getLogger("authsome").setLevel(logging.WARNING if ctx.obj.quiet else logging.INFO)
@cli.command()
@common_options
@pass_ctx
@handle_errors
def init(ctx_obj: ContextObj) -> None:
@@ -111,6 +137,7 @@ def init(ctx_obj: ContextObj) -> None:
@cli.command(name="list")
@common_options
@pass_ctx
@handle_errors
def list_cmd(ctx_obj: ContextObj) -> None:
@@ -189,6 +216,7 @@ def list_cmd(ctx_obj: ContextObj) -> None:
@click.option("--client-secret", help="Provider Client Secret")
@click.option("--api-key", help="Provider API Key")
@click.option("--force", is_flag=True, help="Force override existing client credentials.")
@common_options
@pass_ctx
@handle_errors
def login(
@@ -237,6 +265,7 @@ def login(
@cli.command()
@click.argument("provider")
@click.option("--connection", default="default", help="Connection name.")
@common_options
@pass_ctx
@handle_errors
def revoke(ctx_obj: ContextObj, provider: str, connection: str) -> None:
@@ -253,6 +282,7 @@ def revoke(ctx_obj: ContextObj, provider: str, connection: str) -> None:
@cli.command()
@click.argument("provider")
@click.option("--connection", default="default", help="Connection name.")
@common_options
@pass_ctx
@handle_errors
def remove(ctx_obj: ContextObj, provider: str, connection: str) -> None:
@@ -271,9 +301,12 @@ def remove(ctx_obj: ContextObj, provider: str, connection: str) -> None:
@click.option("--connection", default="default", help="Connection name.")
@click.option("--field", help="Return only a specific field.")
@click.option("--show-secret", is_flag=True, help="Reveal encrypted secrets.")
@common_options
@pass_ctx
@handle_errors
def get(ctx_obj: ContextObj, provider: str, connection: str, field: str | None, show_secret: bool) -> None:
def get(
ctx_obj: ContextObj, provider: str, connection: str, field: str | None, show_secret: bool
) -> None:
"""Return provider connection metadata by default."""
client = ctx_obj.initialize_client()
record = client.get_connection(provider, connection)
@@ -311,6 +344,7 @@ def get(ctx_obj: ContextObj, provider: str, connection: str, field: str | None,
@cli.command()
@click.argument("provider")
@common_options
@pass_ctx
@handle_errors
def inspect(ctx_obj: ContextObj, provider: str) -> None:
@@ -329,6 +363,7 @@ def inspect(ctx_obj: ContextObj, provider: str) -> None:
@click.argument("provider")
@click.option("--connection", default="default", help="Connection name.")
@click.option("--format", "export_format", type=click.Choice(["env", "shell", "json"]), default="env")
@common_options
@pass_ctx
@handle_errors
def export(ctx_obj: ContextObj, provider: str, connection: str, export_format: str) -> None:
@@ -345,6 +380,7 @@ def export(ctx_obj: ContextObj, provider: str, connection: str, export_format: s
@cli.command(context_settings=dict(ignore_unknown_options=True))
@click.option("--provider", "-p", multiple=True, help="Provider(s) to inject credentials for.")
@click.argument("command", nargs=-1, required=True)
@common_options
@pass_ctx
@handle_errors
def run(ctx_obj: ContextObj, provider: list[str], command: tuple[str]) -> None:
@@ -358,6 +394,7 @@ def run(ctx_obj: ContextObj, provider: list[str], command: tuple[str]) -> None:
@cli.command()
@click.argument("path")
@click.option("--force", is_flag=True, help="Force overwrite if provider exists.")
@common_options
@pass_ctx
@handle_errors
def register(ctx_obj: ContextObj, path: str, force: bool) -> None:
@@ -388,6 +425,7 @@ def register(ctx_obj: ContextObj, path: str, force: bool) -> None:
@cli.command()
@common_options
@pass_ctx
@handle_errors
def whoami(ctx_obj: ContextObj) -> None:
@@ -406,6 +444,7 @@ def whoami(ctx_obj: ContextObj) -> None:
@cli.command()
@common_options
@pass_ctx
@handle_errors
def doctor(ctx_obj: ContextObj) -> None:
+1
View File
@@ -28,6 +28,7 @@ from authsome.crypto.base import CryptoBackend
from authsome.crypto.keyring_crypto import KeyringCryptoBackend
from authsome.crypto.local_file_crypto import LocalFileCryptoBackend
from authsome.errors import (
AuthenticationFailedError,
ConnectionNotFoundError,
CredentialMissingError,
ProfileNotFoundError,
+1 -1
View File
@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
# Valid flow types per auth type
_VALID_FLOWS: dict[AuthType, set[FlowType]] = {
AuthType.OAUTH2: {FlowType.PKCE, FlowType.DEVICE_CODE, FlowType.DCR_PKCE},
AuthType.API_KEY: {FlowType.API_KEY_PROMPT, FlowType.API_KEY_ENV},
AuthType.API_KEY: {FlowType.API_KEY, FlowType.API_KEY},
}
Generated
+1 -1
View File
@@ -13,7 +13,7 @@ wheels = [
[[package]]
name = "authsome"
version = "0.1.4"
version = "0.1.6"
source = { editable = "." }
dependencies = [
{ name = "click" },