Files
alishahryar1--free-claude-code/src/free_claude_code/cli/entrypoints.py
T
wehub-resource-sync 5296d0e97c
CI / Ban suppressions and legacy annotations (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff-check (push) Has been cancelled
CI / ruff-format (push) Has been cancelled
CI / ty (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:44 +08:00

177 lines
5.5 KiB
Python

"""CLI entry points for the installed package."""
import os
import shutil
import sys
import threading
import time
import webbrowser
from collections.abc import Sequence
from pathlib import Path
import uvicorn
from free_claude_code.cli.launchers.common import preflight_proxy
from free_claude_code.cli.process_registry import (
kill_all_best_effort,
)
from free_claude_code.config.env_migrations import (
explicit_env_file_huggingface_warning,
migrate_owned_env_files,
)
from free_claude_code.config.env_template import load_env_template
from free_claude_code.config.paths import (
config_dir_path,
legacy_env_paths,
managed_env_path,
)
from free_claude_code.config.server_urls import local_admin_url, local_proxy_root_url
from free_claude_code.config.settings import Settings, get_settings
from free_claude_code.core.version import package_version
from free_claude_code.runtime.bootstrap import build_asgi_app
SERVER_GRACEFUL_SHUTDOWN_SECONDS = 5
def serve(argv: Sequence[str] | None = None) -> None:
"""Start the FastAPI server (registered as `fcc-server` script)."""
if _print_version_if_requested(argv):
return
opened_admin_browser = False
try:
try:
while True:
_migrate_legacy_env_if_missing()
_migrate_config_env_keys()
settings = get_settings()
if not _run_supervised_server(
settings, open_admin_browser=not opened_admin_browser
):
return
opened_admin_browser = True
get_settings.cache_clear()
except KeyboardInterrupt:
return
finally:
kill_all_best_effort()
def _admin_browser_open_enabled() -> bool:
"""Whether to open /admin when the server becomes reachable (FCC_OPEN_BROWSER)."""
raw = os.environ.get("FCC_OPEN_BROWSER", "true").strip().lower()
return raw not in {"", "0", "false", "no"}
def _schedule_open_admin_browser(settings: Settings) -> None:
"""After /health succeeds, open the admin UI in the default browser (daemon thread)."""
if not _admin_browser_open_enabled():
return
admin_url = local_admin_url(settings)
proxy_root_url = local_proxy_root_url(settings)
def open_when_ready() -> None:
deadline = time.monotonic() + 30.0
while time.monotonic() < deadline:
if preflight_proxy(proxy_root_url) is None:
webbrowser.open(admin_url)
return
time.sleep(0.15)
threading.Thread(
target=open_when_ready, name="fcc-open-admin-browser", daemon=True
).start()
def _run_supervised_server(settings: Settings, *, open_admin_browser: bool) -> bool:
"""Run once; restart only after the old ownership graph fully closes."""
restart_requested = False
server_holder: dict[str, uvicorn.Server] = {}
def request_restart() -> None:
nonlocal restart_requested
restart_requested = True
if server := server_holder.get("server"):
server.should_exit = True
asgi_app = build_asgi_app(settings, restart_callback=request_restart)
config = uvicorn.Config(
asgi_app,
host=settings.host,
port=settings.port,
log_level="debug",
timeout_graceful_shutdown=SERVER_GRACEFUL_SHUTDOWN_SECONDS,
)
server = uvicorn.Server(config)
server_holder["server"] = server
if open_admin_browser:
_schedule_open_admin_browser(settings)
server.run()
return restart_requested and asgi_app.runtime.is_closed
def init(argv: Sequence[str] | None = None) -> None:
"""Scaffold config at ~/.fcc/.env (registered as `fcc-init`)."""
if _print_version_if_requested(argv):
return
config_dir = config_dir_path()
env_file = managed_env_path()
migrated_from = _migrate_legacy_env_if_missing()
_migrate_config_env_keys()
if migrated_from is not None:
print(f"Config migrated from {migrated_from} to {env_file}")
print(
"Edit it to set your API keys and model preferences, then run: fcc-server"
)
return
if env_file.exists():
print(f"Config already exists at {env_file}")
print("Delete it first if you want to reset to defaults.")
return
config_dir.mkdir(parents=True, exist_ok=True)
template = load_env_template()
env_file.write_text(template, encoding="utf-8")
print(f"Config created at {env_file}")
print("Edit it to set your API keys and model preferences, then run: fcc-server")
def _print_version_if_requested(argv: Sequence[str] | None) -> bool:
args = sys.argv[1:] if argv is None else argv
if "--version" not in args:
return False
print(f"free-claude-code {package_version()}")
return True
def _migrate_legacy_env_if_missing() -> Path | None:
"""Copy a legacy user env into the managed config path when absent."""
env_file = managed_env_path()
if env_file.exists():
return None
# TODO: Remove after the ~/.fcc/.env migration has had a release cycle.
for legacy_env in legacy_env_paths():
if not legacy_env.is_file():
continue
env_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(legacy_env, env_file)
return legacy_env
return None
def _migrate_config_env_keys() -> tuple[Path, ...]:
"""Apply dotenv key migrations before Settings loads config."""
migrated = migrate_owned_env_files()
if warning := explicit_env_file_huggingface_warning(os.environ):
print(warning, file=sys.stderr)
return migrated