mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-03 14:05:26 +02:00
51157f91bd
## Problem Admin config was a single responsibility hub with manually duplicated provider metadata. Provider labels, fields, template loading, validation, persistence, and status lived in one place. ## Changes | Before | After | | --- | --- | | Admin config lived in one large `api/admin_config.py` module. | Admin config lives in package modules for manifest, sources, values, validation, persistence, and status. | | Provider admin fields and UI labels were manually duplicated. | Provider admin fields and display names derive from `PROVIDER_CATALOG` with admin-only help overrides. | | `fcc-init` and Admin UI loaded `.env.example` separately. | `fcc-init` and Admin UI use shared `config.env_template` loading. | | Architecture docs pointed to the old admin config module. | Architecture docs describe the package owners and catalog-driven provider manifest. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR refactors admin configuration into a catalog-driven package. The main changes are: - Split the former monolithic `api/admin_config.py` into manifest, source loading, value presentation, validation, persistence, and provider status modules. - Generate provider admin fields and display names from `PROVIDER_CATALOG` with admin-specific help overrides. - Share `.env.example` loading between `fcc-init` and Admin UI defaults through `config.env_template`. - Update admin routes, Admin UI provider labels, architecture docs, version metadata, and contract/API tests for the new module layout. </details> <h3>Confidence Score: 5/5</h3> The refactor appears merge-safe with no code issues identified in the reviewed changes. The package split, catalog-driven provider metadata, shared environment template loading, route updates, and tests/docs changes are cohesive and covered by corresponding contract/API/CLI test updates. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - T-Rex ran manifest validation for catalog provider before and after routes, capturing base and head responses and catalog-alignment checks, and confirmed the validation completed successfully. - T-Rex evaluated the shared-env-template scenarios, observing the before run with no config.env\_template module and the after run with the module present, with patched loader values and all consistency checks passing, and the run exited with code 0. - T-Rex executed the package-admin-workflow validation, verifying the base and after import paths, the load/validate/write workflow produced matching outputs, and the run completed with exit code 0. <a href="https://app.greptile.com/trex/runs/12529845/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=1"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1" height="32"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <sub>Reviews (1): Last reviewed commit: ["Refactor admin config into catalog-drive..."](https://github.com/alishahryar1/free-claude-code/commit/d6239d7953fce75d435b8d6a20536c1aff53aa88) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=40315222)</sub> <!-- /greptile_comment -->
151 lines
4.5 KiB
Python
151 lines
4.5 KiB
Python
"""CLI entry points for the installed package."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import threading
|
|
import time
|
|
import webbrowser
|
|
from pathlib import Path
|
|
|
|
import uvicorn
|
|
|
|
from api.admin_urls import local_admin_url, local_proxy_root_url
|
|
from api.app import GracefulLifespanApp, create_app
|
|
from cli.launchers.common import preflight_proxy
|
|
from cli.process_registry import (
|
|
kill_all_best_effort,
|
|
)
|
|
from config.env_template import load_env_template
|
|
from config.paths import (
|
|
config_dir_path,
|
|
legacy_env_paths,
|
|
managed_env_path,
|
|
)
|
|
from config.settings import Settings, get_settings
|
|
|
|
SERVER_GRACEFUL_SHUTDOWN_SECONDS = 5
|
|
|
|
|
|
def serve() -> None:
|
|
"""Start the FastAPI server (registered as `fcc-server` script)."""
|
|
opened_admin_browser = False
|
|
try:
|
|
try:
|
|
while True:
|
|
_migrate_legacy_env_if_missing()
|
|
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 one uvicorn server instance; return whether admin requested restart."""
|
|
|
|
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
|
|
|
|
app = create_app(lifespan_enabled=False)
|
|
app.state.admin_restart_callback = request_restart
|
|
asgi_app = GracefulLifespanApp(app)
|
|
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
|
|
|
|
|
|
def init() -> None:
|
|
"""Scaffold config at ~/.fcc/.env (registered as `fcc-init`)."""
|
|
config_dir = config_dir_path()
|
|
env_file = managed_env_path()
|
|
|
|
migrated_from = _migrate_legacy_env_if_missing()
|
|
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 _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
|