diff --git a/honcho-cli/README.md b/honcho-cli/README.md index 7b06acb3..19e42445 100644 --- a/honcho-cli/README.md +++ b/honcho-cli/README.md @@ -134,45 +134,42 @@ Errors are structured: Non-interactive onboarding: ```bash -# Pre-seed via flags / env vars; `honcho init` still prompts for anything missing -HONCHO_API_KEY=xxx honcho init --base-url local -``` - -## Context Threading - -Workspace / peer / session come from flags or env vars — not persisted defaults: - -```bash -# Per-command flags -honcho --workspace prod --peer ajspig peer card - -# Or export once per shell -export HONCHO_WORKSPACE_ID=prod -export HONCHO_PEER_ID=ajspig -honcho peer card -honcho peer inspect other_id # positional arg still takes precedence +# Pre-seed via flags / env vars; init still prompts for anything missing +HONCHO_API_KEY=hch-v3-xxx honcho init --base-url https://api.honcho.dev ``` ## Environment Variables -| Variable | Description | -|----------|-------------| -| `HONCHO_BASE_URL` | API base URL pre-fill for `honcho init` only — ignored at runtime | -| `HONCHO_API_KEY` | Admin JWT pre-fill for `honcho init` only — ignored at runtime | -| `HONCHO_WORKSPACE_ID` | Default workspace | -| `HONCHO_PEER_ID` | Default peer | -| `HONCHO_SESSION_ID` | Default session | -| `HONCHO_JSON` | Force JSON output (`1` / `true`) | +All `HONCHO_*` env vars work at runtime — no config file required. -## Global Flags +Precedence (highest first): **flag → env var → config file → default**. -| Flag | Description | -|------|-------------| -| `--json` | Force JSON output | -| `--workspace` / `-w` | Override workspace ID | -| `--peer` / `-p` | Override peer ID | -| `--session` / `-s` | Override session ID | -| `--version` / `-V` | Show version | +| Variable | Flag | Description | +|----------|------|-------------| +| `HONCHO_API_KEY` | `--api-key` (init) | Admin JWT | +| `HONCHO_BASE_URL` | `--base-url` (init) | API URL | +| `HONCHO_WORKSPACE_ID` | `-w` / `--workspace` | Workspace scope | +| `HONCHO_PEER_ID` | `-p` / `--peer` | Peer scope | +| `HONCHO_SESSION_ID` | `-s` / `--session` | Session scope | +| `HONCHO_JSON` | `--json` | Force JSON output (`1` / `true`) | + +```bash +# Per-command flags +honcho -w prod -p user peer card + +# Or export once per shell +export HONCHO_WORKSPACE_ID=prod +export HONCHO_PEER_ID=user +honcho peer card + +# One-off against a different server +HONCHO_BASE_URL=http://localhost:8000 honcho workspace list + +# CI/CD — env vars only, no config file needed +export HONCHO_API_KEY=hch-v3-xxx +export HONCHO_BASE_URL=https://api.honcho.dev +honcho workspace list +``` ## Configuration @@ -182,8 +179,6 @@ top-level keys: `apiKey` and `environmentUrl` (the full Honcho API URL, e.g. top level — `hosts`, `sessions`, `saveMessages`, `sessionStrategy`, etc. — is left untouched. -Example: - ```json { "apiKey": "hch-v3-...", @@ -192,16 +187,8 @@ Example: } ``` -Precedence (highest first): - -- **`apiKey`** and **`base_url`**: read only from `~/.honcho/config.json` at - runtime. No env-var or flag fallback — a missing config file is a hard - error. (`honcho init` still accepts `--api-key` / `HONCHO_API_KEY` and - `--base-url` / `HONCHO_BASE_URL` as one-time pre-fills for the - write-to-file prompts.) This keeps a single, inspectable source of truth - for where you're connecting and with what credentials. -- **`workspace_id` / `peer_id` / `session_id`**: flag (`-w` / `-p` / `-s`) - → env var (`HONCHO_WORKSPACE_ID` etc.). Not persisted to the config file. +`workspace_id` / `peer_id` / `session_id` are per-command only — never +persisted to the config file. ## Development diff --git a/honcho-cli/src/honcho_cli/commands/setup.py b/honcho-cli/src/honcho_cli/commands/setup.py index c357f231..02153bfe 100644 --- a/honcho-cli/src/honcho_cli/commands/setup.py +++ b/honcho-cli/src/honcho_cli/commands/setup.py @@ -7,22 +7,20 @@ from __future__ import annotations import json -import os import typer +from honcho import Honcho from rich.console import Console from rich.panel import Panel -from honcho import Honcho - from honcho_cli import __version__ +from honcho_cli.branding import BANNER, BRAND, ICON_FAIL, ICON_OK, ICON_RUN +from honcho_cli.common import get_resolved_config from honcho_cli.config import ( CONFIG_FILE, DEFAULT_BASE_URL, CLIConfig, ) -from honcho_cli.branding import BANNER, BRAND, ICON_FAIL, ICON_OK, ICON_RUN -from honcho_cli.common import get_resolved_config from honcho_cli.output import print_error, print_result, set_json_mode, use_json _console = Console(stderr=True) @@ -73,11 +71,9 @@ def _test_connection(base_url: str, api_key: str) -> tuple[bool, str]: return False, msg -def _resolve_source(param: str | None, env_val: str, env_name: str, flag_name: str, file_val: str) -> tuple[str, str]: - """Return (value, source_label) for one field, flag/env > file.""" - if param: - return param, f"{env_name} env var" if env_val and env_val == param else f"{flag_name} flag" - return (file_val, str(CONFIG_FILE)) if file_val else ("", "") +def _pick(flag_val: str | None, file_val: str) -> str: + """Return best available value. Flag/env wins over file.""" + return flag_val or file_val or "" # --------------------------------------------------------------------------- # @@ -88,28 +84,22 @@ def init( base_url: str | None = typer.Option(None, "--base-url", envvar="HONCHO_BASE_URL", help="Honcho API URL (e.g. https://api.honcho.dev, http://localhost:8000)"), json_output: bool = typer.Option(False, "--json", help="Force JSON output"), ) -> None: - """Confirm or set ``apiKey`` + Honcho URL in ``~/.honcho/config.json``. + """Set ``apiKey`` + Honcho URL in ``~/.honcho/config.json``. - For each value we either (a) show what we have and ask for a Y/N - confirmation, or (b) prompt for it if missing. Foreign top-level keys - (``hosts``, ``sessions``, …) are preserved. + Each value is shown with its current default — press Enter to keep it or + type a replacement. Foreign top-level keys (``hosts``, ``sessions``, …) + are preserved. Workspace / peer / session scoping is per-command via ``-w`` / ``-p`` / ``-s`` or ``HONCHO_*`` env vars — never persisted. """ - if json_output: set_json_mode(True) file_key, file_url = _read_file_values() - key_val, key_src = _resolve_source( - api_key, os.environ.get("HONCHO_API_KEY", ""), "HONCHO_API_KEY", "--api-key", file_key, - ) - url_val, url_src = _resolve_source( - base_url, os.environ.get("HONCHO_BASE_URL", ""), "HONCHO_BASE_URL", "--base-url", file_url, - ) - url_val = url_val.strip() + key_val = _pick(api_key, file_key) + url_val = _pick(base_url, file_url).strip() if not use_json(): _console.print() @@ -118,11 +108,10 @@ def init( expand=False, subtitle=f"Honcho CLI · v{__version__}", )) _console.print() - if not key_val and not url_val: - _console.print(f"[bold]No existing config at {CONFIG_FILE} — let's create one.[/bold]\n") + _console.print(" [dim]Press Enter to accept the default shown in brackets.[/dim]\n") - final_key = _confirm_or_prompt_api_key(key_val, key_src) - final_url = _confirm_or_prompt_url(url_val, url_src) + final_key = _prompt_api_key(key_val) + final_url = _prompt_url(url_val) # Persist if anything changed or if the value came from env/flag. if final_key != file_key or final_url != file_url: @@ -136,38 +125,30 @@ def init( print_result({"apiKey": _redact(final_key), "baseUrl": final_url}) -def _confirm_or_prompt_api_key(value: str, source: str) -> str: - - - if value: - if not use_json(): - _console.print(f" API key: [dim]{_redact(value)}[/dim] [{BRAND}](from {source})[/{BRAND}]") - if use_json() or typer.confirm(" Use this API key?", default=True): - return value +def _prompt_api_key(value: str) -> str: + """Prompt for API key. Shows redacted default in brackets; Enter keeps it.""" if use_json(): + if value: + return value print_error("MISSING_VALUE", "API key is required", {}) raise typer.Exit(1) - # Never set ``default=`` to the raw key — typer would echo it in brackets. - new = typer.prompt(" API key") - if not new: - print_error("MISSING_VALUE", "API key is required", {}) - raise typer.Exit(1) - return new - - -def _confirm_or_prompt_url(value: str, source: str) -> str: - if value: - if not use_json(): - _console.print(f" Honcho URL: [dim]{value}[/dim] [{BRAND}](from {source})[/{BRAND}]") - if use_json() or typer.confirm(" Use this URL?", default=True): - return value + redacted = _redact(value) + return typer.prompt(f" API key [{redacted}]", default=value, show_default=False) + return typer.prompt(" API key") + + +def _prompt_url(value: str) -> str: + """Prompt for Honcho URL. Shows default in brackets; Enter keeps it.""" if use_json(): + if value: + return value print_error("MISSING_VALUE", "Honcho URL is required", {}) raise typer.Exit(1) - _console.print(" [dim](e.g. https://api.honcho.dev for managed, http://localhost:8000 for local)[/dim]") - return typer.prompt(" Honcho URL", default=DEFAULT_BASE_URL).strip() + + default = value or DEFAULT_BASE_URL + return typer.prompt(" Honcho URL", default=default).strip() def _check_connection(base_url: str, api_key: str) -> None: @@ -264,7 +245,7 @@ def doctor( hint = "" if config.workspace_id else " [dim](pass -w / -p to include workspace, peer, queue checks)[/dim]" _console.print(f"\n [{color}]{passed}/{total}[/{color}] checks passed{hint}\n") - # apiKey must live in config.json → missing file is a hard failure. + # Config file + API connectivity are hard requirements. critical = {"Config file", "API key configured", "API connectivity"} if config.workspace_id: critical.add("Workspace reachable") diff --git a/honcho-cli/src/honcho_cli/config.py b/honcho-cli/src/honcho_cli/config.py index 5ff7d00c..e678c9ab 100644 --- a/honcho-cli/src/honcho_cli/config.py +++ b/honcho-cli/src/honcho_cli/config.py @@ -30,13 +30,10 @@ DEFAULT_BASE_URL = "https://api.honcho.dev" # Env var mapping for runtime overrides. # -# NOTE: ``api_key`` and ``base_url`` are intentionally NOT here. Both must -# live in ``~/.honcho/config.json`` so there's a single, inspectable source -# of truth for where you're connecting and with what credentials. -# (``honcho init`` still accepts ``--api-key`` / ``HONCHO_API_KEY`` and -# ``--base-url`` / ``HONCHO_BASE_URL`` as one-time pre-fills for the -# write-to-file prompts.) +# Resolution order: flag > env var > config file > default. ENV_MAP: dict[str, str] = { + "api_key": "HONCHO_API_KEY", + "base_url": "HONCHO_BASE_URL", "workspace_id": "HONCHO_WORKSPACE_ID", "peer_id": "HONCHO_PEER_ID", "session_id": "HONCHO_SESSION_ID", diff --git a/honcho-cli/tests/test_commands.py b/honcho-cli/tests/test_commands.py index f2ae31ee..2a58065a 100644 --- a/honcho-cli/tests/test_commands.py +++ b/honcho-cli/tests/test_commands.py @@ -56,8 +56,8 @@ class TestInit: cfg.write_text(json.dumps({ "apiKey": "old", "environmentUrl": "http://old.example", - "hosts": {"claude_code": {"peerName": "ajspig"}}, - "sessions": {"/Users/ajspig": "home-chat"}, + "hosts": {"claude_code": {"peerName": "user"}}, + "sessions": {"/Users/user": "home-chat"}, "sessionStrategy": "chat-instance", })) with patch("honcho_cli.commands.setup._test_connection", return_value=(True, "OK")): @@ -69,8 +69,8 @@ class TestInit: on_disk = json.loads(cfg.read_text()) assert on_disk["apiKey"] == "new-key" assert on_disk["environmentUrl"] == "https://api.honcho.dev" - assert on_disk["hosts"] == {"claude_code": {"peerName": "ajspig"}} - assert on_disk["sessions"] == {"/Users/ajspig": "home-chat"} + assert on_disk["hosts"] == {"claude_code": {"peerName": "user"}} + assert on_disk["sessions"] == {"/Users/user": "home-chat"} assert on_disk["sessionStrategy"] == "chat-instance" diff --git a/honcho-cli/tests/test_config.py b/honcho-cli/tests/test_config.py index 67862004..ae0e82aa 100644 --- a/honcho-cli/tests/test_config.py +++ b/honcho-cli/tests/test_config.py @@ -35,14 +35,14 @@ class TestLoad: assert loaded.base_url == "http://localhost:8000" assert loaded.api_key == "k" - def test_api_key_and_base_url_ignore_env(self, cfg_path, monkeypatch): - """Both apiKey and base_url must come from config.json — env vars are ignored at runtime.""" + def test_api_key_and_base_url_from_env(self, cfg_path, monkeypatch): + """HONCHO_API_KEY and HONCHO_BASE_URL override config file at runtime.""" cfg_path.write_text(json.dumps({"environmentUrl": "https://api.honcho.dev"})) monkeypatch.setenv("HONCHO_API_KEY", "env-key") monkeypatch.setenv("HONCHO_BASE_URL", "http://localhost:8000") loaded = CLIConfig.load() - assert loaded.api_key == "" - assert loaded.base_url == "https://api.honcho.dev" + assert loaded.api_key == "env-key" + assert loaded.base_url == "http://localhost:8000" class TestSave: @@ -52,7 +52,7 @@ class TestSave: base_url="http://localhost:8000", api_key="test-key-123", workspace_id="my-ws", # must NOT be persisted - peer_id="ajspig", + peer_id="user", session_id="s1", ).save() assert json.loads(cfg_path.read_text()) == { @@ -66,8 +66,8 @@ class TestSave: "apiKey": "old-key", "environmentUrl": "https://api.honcho.dev", "saveMessages": True, - "sessions": {"/Users/ajspig": "home-chat"}, - "hosts": {"claude_code": {"peerName": "ajspig", "workspace": "agents"}}, + "sessions": {"/Users/user": "home-chat"}, + "hosts": {"claude_code": {"peerName": "user", "workspace": "agents"}}, "sessionStrategy": "chat-instance", } cfg_path.write_text(json.dumps(seed))