Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/kscli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,34 @@
import os
from pathlib import Path

from kscli.config import get_current_environment

_CREDENTIALS_DIR = Path(
os.environ.get("KSCLI_CREDENTIALS_PATH", "/tmp/kscli")
)


def _credentials_path() -> Path:
"""Resolve per-environment credentials file path."""
env = get_current_environment()
return _CREDENTIALS_DIR / f".credentials_{env}"
"""Resolve credentials file path."""
return _CREDENTIALS_DIR / ".credentials"


def save_api_key(api_key: str) -> None:
"""Store API key to per-environment credentials file with restricted permissions."""
"""Store API key to credentials file with restricted permissions."""
path = _credentials_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"api_key": api_key}))
path.chmod(0o600)


def load_credentials() -> dict[str, str]:
"""Load credentials for the current environment."""
"""Load credentials."""
path = _credentials_path()
if not path.exists():
env = get_current_environment()
raise SystemExit(
f"Not authenticated for '{env}' environment. Run: kscli login --api-key <key>"
"Not authenticated. Run: kscli login --api-key <key>"
)
return json.loads(path.read_text())


def clear_credentials() -> None:
"""Remove credentials file for the current environment."""
"""Remove credentials file."""
_credentials_path().unlink(missing_ok=True)
33 changes: 26 additions & 7 deletions src/kscli/commands/auth.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Authentication commands: login, logout, whoami."""

import certifi
import click
import ksapi

from kscli.auth import clear_credentials, save_api_key
from kscli.client import get_api_client, handle_client_errors
from kscli.config import get_current_environment
from kscli.config import get_base_url, write_config
from kscli.output import print_result


Expand All @@ -17,19 +18,37 @@
hide_input=True,
help="User-scoped API key (sk-user-...).",
)
def login(api_key: str) -> None:
@click.option(
"--url",
default=None,
help="API base URL. Defaults to the staging instance.",
)
@click.pass_context
def login(ctx: click.Context, api_key: str, url: str | None) -> None:
"""Authenticate with a user-scoped API key."""
target = get_base_url(url or ctx.obj.get("base_url"))
verify_ssl = target.startswith("https")

config = ksapi.Configuration(host=target)
config.verify_ssl = verify_ssl
if verify_ssl:
config.ssl_ca_cert = certifi.where()
client = ksapi.ApiClient(config)
client.default_headers["authorization"] = f"Bearer {api_key}"

with handle_client_errors():
ksapi.UsersApi(client).get_me()

save_api_key(api_key)
env = get_current_environment()
click.echo(f"Logged in successfully ({env}).")
write_config({"base_url": target, "verify_ssl": verify_ssl})
click.echo(f"Logged in successfully ({target}).")


@click.command("logout")
def logout() -> None:
"""Remove stored credentials for the current environment."""
env = get_current_environment()
"""Remove stored credentials."""
clear_credentials()
click.echo(f"Logged out ({env}).")
click.echo("Logged out.")


@click.command("whoami")
Expand Down
47 changes: 2 additions & 45 deletions src/kscli/commands/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Settings commands: environment, show."""
"""Settings commands: show."""

import click

Expand All @@ -7,72 +7,29 @@
get_config_path,
get_default_format,
get_tls_config,
load_config,
write_config,
)
from kscli.output import print_result

_ENV_PRESETS: dict[str, dict[str, object]] = {
"local": {
"environment": "local",
"base_url": "http://localhost:18000",
"verify_ssl": False,
},
"staging": {
"environment": "staging",
"base_url": "https://api-staging.knowledgestack.ai",
"verify_ssl": True,
},
"prod": {
"environment": "prod",
"base_url": "https://api.knowledgestack.ai",
"verify_ssl": True,
},
}


@click.group("settings")
def settings():
"""Manage CLI configuration."""


@settings.command("environment")
@click.argument("env_name", type=click.Choice(["local", "staging", "prod"]))
@click.option(
"--url",
default=None,
help="Override default API base URL for the selected environment",
)
def environment(env_name: str, url: str | None) -> None:
"""Set the environment (local, prod) and associated config."""
preset = _ENV_PRESETS[env_name].copy()
if url:
preset["base_url"] = url
write_config(preset)
click.echo(f"Environment set to '{env_name}'.")
if "base_url" in preset:
click.echo(f" base_url = {preset['base_url']}")
click.echo(f" verify_ssl = {preset['verify_ssl']}")


@settings.command("show")
@click.pass_context
def show(ctx: click.Context) -> None:
"""Print current resolved configuration (env + config file + defaults)."""
"""Print current resolved configuration."""
base_url = get_base_url(None)
verify_ssl, ca_bundle = get_tls_config()
format_ = get_default_format()
path = get_config_path()

file_config = load_config()
environment_label = file_config.get("environment", "(not set)")

result = {
"config_file": str(path),
"base_url": base_url,
"verify_ssl": verify_ssl,
"ca_bundle": ca_bundle or "(default)",
"format": format_,
"environment": environment_label,
}
print_result(ctx, result)
7 changes: 1 addition & 6 deletions src/kscli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Any

_DEFAULT_CONFIG_PATH = Path.home() / ".config" / "kscli" / "config.json"
_DEFAULT_BASE_URL = "http://localhost:8000"
_DEFAULT_BASE_URL = "https://api-staging.knowledgestack.ai"
_DEFAULT_FORMAT = "table"


Expand All @@ -26,11 +26,6 @@ def load_config() -> dict[str, Any]:
return {}


def get_current_environment() -> str:
"""Resolve current environment from config. Defaults to 'local'."""
return load_config().get("environment", "local")


def get_base_url(override: str | None = None) -> str:
if override:
return override
Expand Down
33 changes: 0 additions & 33 deletions tests/e2e/test_cli_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,3 @@ def test_settings_show(self, cli_authenticated: dict[str, str]) -> None:
assert "base_url" in data
assert "format" in data

def test_settings_environment_local(self, cli_authenticated: dict[str, str]) -> None:
"""Settings environment local sets the local preset."""
result = run_kscli_ok(
["settings", "environment", "local"],
env=cli_authenticated,
format_json=False,
)
assert "local" in result.stdout

def test_settings_environment_prod(self, cli_authenticated: dict[str, str]) -> None:
"""Settings environment prod sets the prod preset."""
result = run_kscli_ok(
["settings", "environment", "prod"],
env=cli_authenticated,
format_json=False,
)
assert "prod" in result.stdout

def test_settings_environment_resets_to_local(
self, cli_authenticated: dict[str, str]
) -> None:
"""Settings environment local restores the local preset."""
run_kscli_ok(
["settings", "environment", "local"],
env=cli_authenticated,
format_json=False,
)
result = run_kscli_ok(
["settings", "environment", "local"],
env=cli_authenticated,
format_json=False,
)
assert "localhost:18000" in result.stdout
Loading