fix: config, env, flag setup

This commit is contained in:
ajspig 2026-04-14 12:42:02 -04:00
parent af3463cf4b
commit df092b85fe
5 changed files with 78 additions and 113 deletions

View File

@ -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

View File

@ -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")

View File

@ -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",

View File

@ -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"

View File

@ -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))