fix: config, env, flag setup
This commit is contained in:
parent
af3463cf4b
commit
df092b85fe
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in New Issue