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,
|
install_root: Path | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check whether the zsh completion for the first prog is current."""
|
"""Check whether the zsh completion for every prog is current."""
|
||||||
if not progs:
|
for prog_name, _ in progs:
|
||||||
return True
|
ok = False
|
||||||
prog_name, _ = progs[0]
|
|
||||||
for target in completion_targets(prog_name, install_root=install_root):
|
for target in completion_targets(prog_name, install_root=install_root):
|
||||||
if target.shell == "zsh" and target.installed:
|
if target.shell == "zsh" and target.installed:
|
||||||
marker = f"_{prog_name.upper()}_COMPLETE"
|
marker = f"_{prog_name.upper()}_COMPLETE"
|
||||||
if marker in target.path.read_text(encoding="utf-8"):
|
if marker in target.path.read_text(encoding="utf-8"):
|
||||||
return True
|
ok = True
|
||||||
|
break
|
||||||
|
if not ok:
|
||||||
return False
|
return False
|
||||||
|
return bool(progs)
|
||||||
|
|
||||||
|
|
||||||
def ensure_completions_installed(
|
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 (
|
from myagents.commands.completion_install import (
|
||||||
ensure_completions_installed,
|
ensure_completions_installed,
|
||||||
)
|
)
|
||||||
|
from myagents.commands.first_run import ensure_auth_configured
|
||||||
|
|
||||||
ensure_completions_installed(_progs())
|
ensure_completions_installed(_progs())
|
||||||
|
ensure_auth_configured()
|
||||||
build_xiaohe_cli()()
|
build_xiaohe_cli()()
|
||||||
|
|||||||
Reference in New Issue
Block a user