fix(xiaohe): completion 缺失 _xiaohe + 首次运行无认证时引导设置 provider
- completion_install: completions_installed() 原只检查 progs[0](myagents), 已存在则短路,导致 xiaohe 等后加 prog 的 completion 从未安装。 改为遍历全部 prog,任一缺失即触发重新安装。 - first_run.py: xiaohe_main() 启动时检测是否有任何可用认证(env var、 credentials、settings.json、已存 keys),无则弹交互式 provider 选择菜单。 - install.sh.in: 移除末尾 xiaohe init + scripts/init.sh wizard,安装脚本 只装软件不初始化配置。
This commit is contained in:
@@ -132,16 +132,18 @@ def completions_installed(
|
||||
*,
|
||||
install_root: Path | None = None,
|
||||
) -> bool:
|
||||
"""Check whether the zsh completion for the first prog is current."""
|
||||
if not progs:
|
||||
return True
|
||||
prog_name, _ = progs[0]
|
||||
for target in completion_targets(prog_name, install_root=install_root):
|
||||
if target.shell == "zsh" and target.installed:
|
||||
marker = f"_{prog_name.upper()}_COMPLETE"
|
||||
if marker in target.path.read_text(encoding="utf-8"):
|
||||
return True
|
||||
return False
|
||||
"""Check whether the zsh completion for every prog is current."""
|
||||
for prog_name, _ in progs:
|
||||
ok = False
|
||||
for target in completion_targets(prog_name, install_root=install_root):
|
||||
if target.shell == "zsh" and target.installed:
|
||||
marker = f"_{prog_name.upper()}_COMPLETE"
|
||||
if marker in target.path.read_text(encoding="utf-8"):
|
||||
ok = True
|
||||
break
|
||||
if not ok:
|
||||
return False
|
||||
return bool(progs)
|
||||
|
||||
|
||||
def ensure_completions_installed(
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"""First-run detection: guide user through provider setup when nothing is configured.
|
||||
|
||||
Called from ``xiaohe_main()`` on every invocation. When no auth is detected
|
||||
(no env vars, no stored keys, no login credentials), prints a menu and
|
||||
walks the user through selecting a provider and entering an API key.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
|
||||
from myagents.backends import (
|
||||
PROVIDERS,
|
||||
THIRD_PARTY_PROVIDER_IDS,
|
||||
Provider,
|
||||
)
|
||||
from myagents.claude_settings import (
|
||||
apply_provider,
|
||||
get_active_provider,
|
||||
load_settings,
|
||||
save_settings,
|
||||
)
|
||||
from myagents.secrets import has_key, set_key
|
||||
|
||||
stderr_console = Console(stderr=True)
|
||||
|
||||
|
||||
def _has_claude_login() -> bool:
|
||||
"""Check for Claude Code OAuth login credentials file."""
|
||||
creds = Path.home() / ".claude" / ".credentials.json"
|
||||
return creds.is_file()
|
||||
|
||||
|
||||
def is_any_auth_configured() -> bool:
|
||||
"""Return True when at least one usable auth method is available."""
|
||||
# 1. Official Claude: env var or login credentials file
|
||||
if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN"):
|
||||
return True
|
||||
if _has_claude_login():
|
||||
return True
|
||||
|
||||
# 2. Third-party provider active in Claude Code settings
|
||||
if get_active_provider() is not None:
|
||||
return True
|
||||
|
||||
# 3. ANTHROPIC_API_KEY set in ~/.claude/settings.json env block
|
||||
env = load_settings().get("env", {})
|
||||
if isinstance(env, dict) and env.get("ANTHROPIC_API_KEY"):
|
||||
return True
|
||||
|
||||
# 4. Stored third-party keys (even if not currently active)
|
||||
for pid in THIRD_PARTY_PROVIDER_IDS:
|
||||
if has_key(PROVIDERS[pid].key_name):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _secure_prompt_key(provider: Provider) -> str:
|
||||
"""Prompt for an API key with hidden input."""
|
||||
try:
|
||||
return click.prompt(
|
||||
f"Enter {provider.display_name} API key",
|
||||
hide_input=True,
|
||||
err=True,
|
||||
)
|
||||
except click.UsageError:
|
||||
try:
|
||||
with open("/dev/tty", encoding="utf-8") as tty:
|
||||
return tty.readline().rstrip("\n")
|
||||
except OSError as exc:
|
||||
raise click.ClickException(
|
||||
f"Cannot read API key interactively: {exc}"
|
||||
) from exc
|
||||
|
||||
|
||||
def _persist_claude_api_key(key: str) -> None:
|
||||
"""Write ANTHROPIC_API_KEY to ~/.claude/settings.json env block."""
|
||||
settings = load_settings()
|
||||
env: dict = {k: v for k, v in settings.get("env", {}).items()
|
||||
if isinstance(v, str)}
|
||||
env["ANTHROPIC_API_KEY"] = key
|
||||
settings["env"] = env
|
||||
save_settings(settings)
|
||||
|
||||
|
||||
def _run_setup_wizard() -> None:
|
||||
"""Interactive guided setup: choose provider and enter API key."""
|
||||
providers = [
|
||||
PROVIDERS["claude"],
|
||||
PROVIDERS["deepseek"],
|
||||
PROVIDERS["kimi"],
|
||||
PROVIDERS["kimi-code"],
|
||||
]
|
||||
|
||||
stderr_console.print(
|
||||
"\n[bold yellow]No model provider configured yet.[/bold yellow]"
|
||||
)
|
||||
stderr_console.print(
|
||||
"[dim]Claude Code needs authentication. Pick one:[/dim]\n"
|
||||
)
|
||||
for i, p in enumerate(providers, 1):
|
||||
stderr_console.print(f" {i}) {p.display_name}")
|
||||
stderr_console.print(
|
||||
" [dim]Or press Enter to skip (set up later with:"
|
||||
" xiaohe switch provider <name>)[/dim]"
|
||||
)
|
||||
|
||||
try:
|
||||
choice = click.prompt(
|
||||
"Choose", default="", show_default=False, err=True
|
||||
)
|
||||
except (click.Abort, EOFError):
|
||||
stderr_console.print("[dim]Skipped. Run 'xiaohe switch provider' later.[/dim]")
|
||||
return
|
||||
|
||||
choice = choice.strip()
|
||||
if not choice:
|
||||
stderr_console.print("[dim]Skipped. Run 'xiaohe switch provider' later.[/dim]")
|
||||
return
|
||||
|
||||
try:
|
||||
idx = int(choice) - 1
|
||||
if idx < 0 or idx >= len(providers):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
stderr_console.print(f"[red]Invalid choice: {choice}[/red]")
|
||||
return
|
||||
|
||||
provider = providers[idx]
|
||||
stderr_console.print(f"\n[bold]{provider.display_name}[/bold] selected.")
|
||||
|
||||
if provider.id == "claude":
|
||||
key = _secure_prompt_key(provider)
|
||||
if not key:
|
||||
stderr_console.print("[yellow]No key entered — skipped.[/yellow]")
|
||||
return
|
||||
_persist_claude_api_key(key)
|
||||
stderr_console.print(
|
||||
f"[green]{provider.display_name} API key saved to"
|
||||
f" ~/.claude/settings.json.[/green]"
|
||||
)
|
||||
else:
|
||||
key = _secure_prompt_key(provider)
|
||||
if not key:
|
||||
stderr_console.print("[yellow]No key entered — skipped.[/yellow]")
|
||||
return
|
||||
set_key(provider.key_name, key)
|
||||
settings = apply_provider(provider, key=key)
|
||||
save_settings(settings)
|
||||
stderr_console.print(
|
||||
f"[green]Switched provider to {provider.display_name}.[/green]"
|
||||
)
|
||||
|
||||
stderr_console.print()
|
||||
|
||||
|
||||
def ensure_auth_configured() -> None:
|
||||
"""Check auth state on ``xiaohe`` startup; run wizard if unconfigured."""
|
||||
if is_any_auth_configured():
|
||||
return
|
||||
try:
|
||||
_run_setup_wizard()
|
||||
except Exception:
|
||||
# Never block xiaohe launch on a setup error.
|
||||
stderr_console.print(
|
||||
"[yellow]Setup interrupted."
|
||||
" Run 'xiaohe switch provider' later to configure.[/yellow]"
|
||||
)
|
||||
@@ -110,6 +110,8 @@ def xiaohe_main() -> None:
|
||||
from myagents.commands.completion_install import (
|
||||
ensure_completions_installed,
|
||||
)
|
||||
from myagents.commands.first_run import ensure_auth_configured
|
||||
|
||||
ensure_completions_installed(_progs())
|
||||
ensure_auth_configured()
|
||||
build_xiaohe_cli()()
|
||||
|
||||
Reference in New Issue
Block a user