Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions flocks/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from flocks.cli.commands.import_ import import_app
from flocks.cli.commands.mcp import mcp_app
from flocks.cli.commands.browser import BROWSER_CONTEXT_SETTINGS, browser_command
from flocks.cli.commands.doctor import doctor_command
from flocks.cli.commands.session import session_app
from flocks.cli.commands.skill import skill_app
from flocks.cli.commands.stats import stats_app
Expand All @@ -19,6 +20,7 @@
"mcp_app",
"browser_command",
"BROWSER_CONTEXT_SETTINGS",
"doctor_command",
"export_app",
"import_app",
"stats_app",
Expand Down
130 changes: 130 additions & 0 deletions flocks/cli/commands/doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Source-install repair command for the Flocks CLI."""

from __future__ import annotations

import os
import shlex
import shutil
import subprocess
import sys
from pathlib import Path

import typer
from rich.console import Console

from flocks.cli.install_profile import cn_installer_environment, is_cn_install_language

console = Console()


def doctor_command() -> None:
"""Run the source installer from the Flocks source directory."""
source_root = _find_source_root()
script = _select_source_install_script(source_root)
command = _build_source_install_command(script)
env = _build_source_install_env()

console.print(f"[cyan]Flocks source directory:[/cyan] {source_root}")
console.print(f"[cyan]Source install command:[/cyan] {_format_command(command)}")

try:
subprocess.run(command, cwd=source_root, check=True, env=env)
except FileNotFoundError as error:
console.print(f"[red]Failed to start installer: {error}[/red]")
raise typer.Exit(1) from error
except subprocess.CalledProcessError as error:
raise typer.Exit(error.returncode or 1) from error

console.print("[green]安装正常[/green]")
_print_service_diagnosis()


def _find_source_root(start: Path | None = None) -> Path:
"""Find the repository root that owns the source install scripts."""
current = (start or Path(__file__)).resolve()
candidates = (current, *current.parents)

for candidate in candidates:
if candidate.is_file():
continue
if (candidate / "pyproject.toml").is_file() and (candidate / "scripts" / "install.sh").is_file():
return candidate

raise typer.BadParameter("Could not locate the Flocks source directory.")


def _select_source_install_script(source_root: Path) -> Path:
"""Select the platform-specific source install script."""
suffix = ".ps1" if _is_windows() else ".sh"
script = source_root / "scripts" / f"install{suffix}"

if not script.is_file():
raise typer.BadParameter(f"Source installer not found: {script}")

return script


def _build_source_install_command(script: Path) -> list[str]:
"""Build the subprocess command for the selected installer."""
if script.suffix == ".ps1":
powershell = shutil.which("pwsh") or shutil.which("powershell") or "powershell"
return [
powershell,
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
str(script),
]

return ["bash", str(script)]


def _build_source_install_env() -> dict[str, str] | None:
"""Build installer environment from the persisted install language."""
if not is_cn_install_language():
return None
env = os.environ.copy()
for key, value in cn_installer_environment().items():
if key == "FLOCKS_INSTALL_LANGUAGE":
env[key] = value
continue
env.setdefault(key, value)
return env


def _print_service_diagnosis() -> None:
"""Print a concise post-install service diagnosis."""
try:
from flocks.cli.service_manager import build_status_lines

status_lines = build_status_lines()
except Exception as error:
console.print(f"[yellow]服务状态检查失败:{error}[/yellow]")
console.print("[yellow]服务不正常,请执行 `flocks restart`[/yellow]")
return

for line in status_lines:
console.print(line)

if _service_status_is_healthy(status_lines):
console.print("[green]服务正常[/green]")
else:
console.print("[yellow]服务不正常,请执行 `flocks restart`[/yellow]")


def _service_status_is_healthy(status_lines: list[str]) -> bool:
"""Return whether backend and WebUI both look healthy from status lines."""
backend_running = any("后端运行中" in line for line in status_lines)
webui_running = any("WebUI 运行中" in line for line in status_lines)
return backend_running and webui_running


def _format_command(command: list[str]) -> str:
"""Return a shell-readable representation of the command."""
return " ".join(shlex.quote(part) for part in command)


def _is_windows() -> bool:
"""Return whether the current platform should use PowerShell installers."""
return sys.platform.startswith("win")
4 changes: 4 additions & 0 deletions flocks/cli/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ def update_command(

async def _update(check: bool, yes: bool, force: bool = False, region: str | None = None) -> None:
from flocks.updater import build_updated_frontend, check_update, perform_update, detect_deploy_mode
from flocks.cli.install_profile import is_cn_install_language

if region is None and is_cn_install_language():
region = "cn"

if not yes and not check and region is None:
use_cn_mirror = typer.confirm("\n是否使用中国镜像进行升级?", default=False)
Expand Down
87 changes: 87 additions & 0 deletions flocks/cli/install_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Persisted installer language profile for CLI maintenance commands."""

from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

INSTALL_PROFILE_FILE = "install_profile.json"
INSTALL_PROFILE_LANGUAGE_KEY = "Language"
DEFAULT_INSTALL_LANGUAGE = "en"
CN_INSTALL_LANGUAGE = "zh-CN"


def install_profile_path() -> Path:
"""Return the install profile path under the Flocks config directory."""
config_dir = os.getenv("FLOCKS_CONFIG_DIR")
if config_dir:
return Path(config_dir).expanduser() / INSTALL_PROFILE_FILE

root = os.getenv("FLOCKS_ROOT")
if root:
return Path(root).expanduser() / "config" / INSTALL_PROFILE_FILE

return Path.home() / ".flocks" / "config" / INSTALL_PROFILE_FILE


def normalize_install_language(value: str | None) -> str:
"""Normalize a persisted or environment installer language value."""
language = (value or "").strip()
if _is_cn_language(language):
return CN_INSTALL_LANGUAGE
if language:
return language
return DEFAULT_INSTALL_LANGUAGE


def read_install_language() -> str:
"""Read the persisted installer language, falling back to the environment."""
path = install_profile_path()
try:
if path.is_file():
payload = json.loads(path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
return normalize_install_language(_string_value(payload.get(INSTALL_PROFILE_LANGUAGE_KEY)))
except (OSError, json.JSONDecodeError):
pass

return normalize_install_language(os.getenv("FLOCKS_INSTALL_LANGUAGE"))


def is_cn_install_language(language: str | None = None) -> bool:
"""Return whether *language* or the persisted profile selects China mirrors."""
return _is_cn_language(language or read_install_language())


def cn_installer_environment() -> dict[str, str]:
"""Return environment variables equivalent to the zh source installer wrapper."""
return {
"FLOCKS_INSTALL_LANGUAGE": CN_INSTALL_LANGUAGE,
"FLOCKS_INSTALL_REPO_URL": "https://gitee.com/flocks/flocks.git",
"FLOCKS_RAW_INSTALL_SH_URL": "https://gitee.com/flocks/flocks/raw/main/install_zh.sh",
"FLOCKS_RAW_INSTALL_PS1_URL": "https://gitee.com/flocks/flocks/raw/main/install_zh.ps1",
"FLOCKS_UV_DEFAULT_INDEX": "https://mirrors.aliyun.com/pypi/simple",
"FLOCKS_UV_INSTALL_SH_URL": "https://astral.org.cn/uv/install.sh",
"FLOCKS_UV_INSTALL_SH_FALLBACK_URL": "https://uv.agentsmirror.com/install-cn.sh",
"FLOCKS_UV_INSTALL_SH_SECONDARY_FALLBACK_URL": "https://astral.sh/uv/install.sh",
"FLOCKS_UV_INSTALL_PS1_URL": "https://astral.org.cn/uv/install.ps1",
"FLOCKS_UV_INSTALL_PS1_FALLBACK_URL": "https://uv.agentsmirror.com/install-cn.ps1",
"FLOCKS_UV_INSTALL_PS1_SECONDARY_FALLBACK_URL": "https://astral.sh/uv/install.ps1",
"FLOCKS_NPM_REGISTRY": "https://registry.npmmirror.com/",
"FLOCKS_NVM_INSTALL_SCRIPT_URL": "https://gitee.com/mirrors/nvm/raw/v0.40.3/install.sh",
"PUPPETEER_CHROME_DOWNLOAD_BASE_URL": "https://cdn.npmmirror.com/binaries/chrome-for-testing",
"FLOCKS_NODEJS_MANUAL_DOWNLOAD_URL": "https://nodejs.org/zh-cn/download",
}


def _string_value(value: Any) -> str | None:
if isinstance(value, str):
return value
return None


def _is_cn_language(language: str | None) -> bool:
normalized = (language or "").strip().lower().replace("_", "-")
return normalized.startswith(("zh", "cn"))
2 changes: 2 additions & 0 deletions flocks/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
admin_app,
BROWSER_CONTEXT_SETTINGS,
browser_command,
doctor_command,
export_app,
import_app,
mcp_app,
Expand Down Expand Up @@ -66,6 +67,7 @@
app.add_typer(admin_app, name="admin")

app.command(name="update")(update_command)
app.command(name="doctor")(doctor_command)
app.command(
name="browser",
context_settings=BROWSER_CONTEXT_SETTINGS,
Expand Down
32 changes: 31 additions & 1 deletion scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,35 @@ function Install-FlocksCli {
}
}

function Write-InstallProfile {
$configDir = if ([string]::IsNullOrWhiteSpace($env:FLOCKS_CONFIG_DIR)) {
$flocksRoot = if ([string]::IsNullOrWhiteSpace($env:FLOCKS_ROOT)) {
Join-Path $HOME ".flocks"
}
else {
$env:FLOCKS_ROOT
}
Join-Path $flocksRoot "config"
}
else {
$env:FLOCKS_CONFIG_DIR
}

try {
$language = if (Test-IsZhInstall) { "zh-CN" } else { "en" }
if (-not (Test-Path $configDir)) {
New-Item -ItemType Directory -Path $configDir -Force | Out-Null
}

$profilePath = Join-Path $configDir "install_profile.json"
$profile = [ordered]@{ Language = $language } | ConvertTo-Json
[System.IO.File]::WriteAllText($profilePath, $profile + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
}
catch {
Write-Warning "Failed to write install profile. Continuing with the default installer behavior."
}
}

function Install-Bun {
if (Test-Command "bun") {
return
Expand Down Expand Up @@ -1229,6 +1258,7 @@ function Main {
}

Write-Info (Get-LocalizedText -English "Project directory: $RootDir" -Chinese "项目目录: $RootDir")
Write-InstallProfile
Install-Uv
Ensure-NpmInstalled
Initialize-InstallSources
Expand Down Expand Up @@ -1320,4 +1350,4 @@ function Main {
}
}

Main
Main
22 changes: 21 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,25 @@ ensure_env_var_persisted() {
fi
}

write_install_profile() {
local config_dir language
if [[ -n "${FLOCKS_CONFIG_DIR:-}" ]]; then
config_dir="$FLOCKS_CONFIG_DIR"
else
config_dir="${FLOCKS_ROOT:-$HOME/.flocks}/config"
fi

if is_zh_install; then
language="zh-CN"
else
language="en"
fi

if ! { mkdir -p "$config_dir" && printf '{\n "Language": "%s"\n}\n' "$language" > "$config_dir/install_profile.json"; }; then
warn "Failed to write install profile. Continuing with the default installer behavior."
fi
}

detect_system_browser_path() {
case "$(uname -s)" in
Darwin)
Expand Down Expand Up @@ -1105,6 +1124,7 @@ main() {
else
info "Project directory: $ROOT_DIR"
fi
write_install_profile
install_uv
ensure_npm_installed
select_install_sources
Expand Down Expand Up @@ -1193,4 +1213,4 @@ EOF
# show_path_update_hint
}

main "$@"
main "$@"
Loading