Add install script for clone-first one-click deployment.

Prompt for repo URL and install directory, auto git clone, then run project setup. Keep deploy.py for in-repo updates only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-12 11:52:57 +08:00
parent 3149770887
commit 6897f88afd
9 changed files with 403 additions and 44 deletions
+2
View File
@@ -1,3 +1,5 @@
# 已在项目目录内时:更新依赖与配置(不会克隆代码)
# 首次安装请用: .\scripts\install.ps1
$ErrorActionPreference = "Stop"
Set-Location (Split-Path -Parent $PSScriptRoot)
python scripts/deploy.py
+16 -7
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""LocalNav 一键部署:环境检测、生成 .env、安装依赖。"""
"""LocalNav 项目内部署:环境检测、生成 .env、安装依赖(需已在项目目录内)"""
from __future__ import annotations
@@ -62,11 +62,14 @@ def check_pip(py_exe: str) -> None:
def optional_tools() -> None:
for name in ("git", "pm2"):
if shutil.which(name):
_ok(f"可选工具 {name} 已安装")
else:
_warn(f"未检测到 {name}(可选)")
if shutil.which("git"):
_ok("可选工具 git 已安装")
else:
_warn("未检测到 git(可选,首次安装请用 scripts/install.py")
if shutil.which("pm2"):
_ok("可选工具 pm2 已安装")
else:
_warn("未检测到 pm2(可选)")
def read_env_lines(path: Path) -> list[str]:
@@ -199,7 +202,13 @@ def print_summary() -> None:
def main() -> None:
print(f"LocalNav 一键部署 · {platform.system()} {platform.release()}")
if not (ROOT / "app.py").is_file():
_fail(
"请在 LocalNav 项目目录内运行,或使用 scripts/install.py 自动克隆安装:\n"
" python scripts/install.py"
)
print(f"LocalNav 项目部署 · {platform.system()} {platform.release()}")
print(f"项目目录: {ROOT}")
print()
+2
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# 已在项目目录内时:更新依赖与配置(不会克隆代码)
# 首次安装请用: bash scripts/install.sh
set -euo pipefail
cd "$(dirname "$0")/.."
python3 scripts/deploy.py
+52
View File
@@ -0,0 +1,52 @@
# LocalNav 一键安装(无需预先克隆)
# 用法:在 PowerShell 中执行 .\scripts\install.ps1
$ErrorActionPreference = "Stop"
$DefaultRepo = "https://git.bz121.com/dekun/LocalNav.git"
$DefaultDir = Join-Path (Get-Location) "LocalNav"
function Need-Cmd($name) {
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
Write-Error "[FAIL] 未找到 $name,请先安装"
}
}
Need-Cmd python
Need-Cmd git
$repo = $env:NAV_INSTALL_REPO
if (-not $repo) {
$inputRepo = Read-Host "Git 仓库地址 [$DefaultRepo]"
$repo = if ($inputRepo) { $inputRepo } else { $DefaultRepo }
}
$dest = $env:NAV_INSTALL_DIR
if (-not $dest) {
$inputDir = Read-Host "安装目录 [$DefaultDir]"
$dest = if ($inputDir) { $inputDir } else { $DefaultDir }
}
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$installPy = Join-Path $scriptDir "install.py"
if (Test-Path $installPy) {
python $installPy --repo $repo --dir $dest
exit $LASTEXITCODE
}
$destPath = Resolve-Path -LiteralPath $dest -ErrorAction SilentlyContinue
if ($destPath -and (Test-Path (Join-Path $destPath "app.py"))) {
Write-Host "[OK] 目录已是 LocalNav,执行 git pull"
git -C $destPath pull --ff-only
} elseif (Test-Path $dest) {
Write-Error "[FAIL] 目录已存在且不是 LocalNav 仓库: $dest"
} else {
Write-Host "[OK] 正在克隆 $repo -> $dest"
$parent = Split-Path -Parent $dest
if ($parent -and -not (Test-Path $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
git clone $repo $dest
}
python (Join-Path $dest "scripts\deploy.py")
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""LocalNav 一键安装:无需预先克隆,输入仓库地址后自动克隆并部署。"""
from __future__ import annotations
import argparse
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
DEFAULT_REPO = "https://git.bz121.com/dekun/LocalNav.git"
MIN_PYTHON = (3, 10)
def _ok(msg: str) -> None:
print(f"[OK] {msg}")
def _warn(msg: str) -> None:
print(f"[WARN] {msg}")
def _fail(msg: str) -> None:
print(f"[FAIL] {msg}", file=sys.stderr)
sys.exit(1)
def default_install_dir() -> Path:
if platform.system() == "Windows":
return Path.cwd() / "LocalNav"
return Path("/opt/LocalNav")
def check_python() -> str:
ver = sys.version_info
if ver < MIN_PYTHON:
_fail(f"需要 Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+,当前 {ver.major}.{ver.minor}")
_ok(f"Python {ver.major}.{ver.minor}.{ver.micro}")
return sys.executable
def check_git() -> str:
git = shutil.which("git")
if not git:
_fail("未检测到 git,请先安装:Ubuntu 执行 apt install -y git")
_ok(f"git 可用 ({git})")
return git
def prompt_value(label: str, default: str) -> str:
try:
raw = input(f"{label} [{default}]: ").strip()
except (EOFError, KeyboardInterrupt):
print()
_fail("已取消")
return raw or default
def resolve_paths(repo: str, dest: Path) -> tuple[str, Path]:
dest = dest.expanduser().resolve()
repo = repo.strip()
if not repo:
_fail("仓库地址不能为空")
if dest.name == "" or str(dest).endswith(("/", "\\")):
_fail("安装目录无效")
return repo, dest
def is_localnav_project(path: Path) -> bool:
return (path / "app.py").is_file() and (path / "scripts" / "deploy.py").is_file()
def clone_or_update(git_exe: str, repo: str, dest: Path) -> None:
if dest.exists() and not dest.is_dir():
_fail(f"安装路径已存在且不是目录: {dest}")
if is_localnav_project(dest):
_ok(f"目录已是 LocalNav 项目,执行 git pull: {dest}")
subprocess.run([git_exe, "-C", str(dest), "pull", "--ff-only"], check=True)
return
if dest.exists():
if (dest / ".git").is_dir():
_ok(f"目录为 Git 仓库,执行 git pull: {dest}")
subprocess.run([git_exe, "-C", str(dest), "pull", "--ff-only"], check=True)
if not is_localnav_project(dest):
_fail(f"目录 {dest} 不是 LocalNav 项目(缺少 app.py")
return
_fail(f"目录已存在且不是 Git 仓库: {dest}")
dest.parent.mkdir(parents=True, exist_ok=True)
_ok(f"正在克隆 {repo} -> {dest}")
subprocess.run([git_exe, "clone", repo, str(dest)], check=True)
if not is_localnav_project(dest):
_fail("克隆完成但未找到 app.py,请检查仓库地址是否正确")
def run_project_setup(py_exe: str, project_dir: Path) -> None:
deploy_script = project_dir / "scripts" / "deploy.py"
if not deploy_script.is_file():
_fail(f"未找到部署脚本: {deploy_script}")
_ok(f"开始项目内安装: {project_dir}")
subprocess.run([py_exe, str(deploy_script)], cwd=project_dir, check=True)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="LocalNav 一键安装(自动克隆 + 部署)")
parser.add_argument(
"--repo",
default="",
help=f"Git 仓库地址(默认 {DEFAULT_REPO}",
)
parser.add_argument(
"--dir",
default="",
help="安装目录(Linux 默认 /opt/LocalNavWindows 默认 ./LocalNav",
)
parser.add_argument(
"-y",
"--yes",
action="store_true",
help="使用默认仓库与安装目录,不交互提问",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
print(f"LocalNav 一键安装 · {platform.system()} {platform.release()}")
print("无需预先克隆,将自动拉取代码并完成部署。")
print()
py_exe = check_python()
git_exe = check_git()
default_dest = default_install_dir()
if args.yes:
repo = args.repo.strip() or DEFAULT_REPO
dest = Path(args.dir).expanduser() if args.dir.strip() else default_dest
else:
repo = args.repo.strip() or prompt_value("Git 仓库地址", DEFAULT_REPO)
dest_str = args.dir.strip() or prompt_value("安装目录", str(default_dest))
dest = Path(dest_str)
repo, dest = resolve_paths(repo, dest)
print()
print(f" 仓库: {repo}")
print(f" 目录: {dest}")
print()
clone_or_update(git_exe, repo, dest)
run_project_setup(py_exe, dest)
print()
print(f"安装目录: {dest}")
print(f"进入目录: cd {dest}")
if __name__ == "__main__":
main()
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# LocalNav 一键安装(无需预先克隆)
# 用法:
# curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash
# bash install.sh
set -euo pipefail
DEFAULT_REPO="https://git.bz121.com/dekun/LocalNav.git"
DEFAULT_DIR="/opt/LocalNav"
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "[FAIL] 未找到 $1,请先安装" >&2
exit 1
}
}
need_cmd python3
need_cmd git
REPO="${NAV_INSTALL_REPO:-}"
DEST="${NAV_INSTALL_DIR:-}"
if [ -z "$REPO" ]; then
read -rp "Git 仓库地址 [${DEFAULT_REPO}]: " REPO
REPO="${REPO:-$DEFAULT_REPO}"
fi
if [ -z "$DEST" ]; then
read -rp "安装目录 [${DEFAULT_DIR}]: " DEST
DEST="${DEST:-$DEFAULT_DIR}"
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_PY="${SCRIPT_DIR}/install.py"
if [ -f "$INSTALL_PY" ]; then
exec python3 "$INSTALL_PY" --repo "$REPO" --dir "$DEST"
fi
# 通过 curl 管道执行时,本机没有 install.py,内联克隆 + 部署
if [ -d "$DEST/.git" ] && [ -f "$DEST/app.py" ]; then
echo "[OK] 目录已是 LocalNav,执行 git pull"
git -C "$DEST" pull --ff-only
elif [ -d "$DEST" ]; then
echo "[FAIL] 目录已存在且不是 LocalNav 仓库: $DEST" >&2
exit 1
else
echo "[OK] 正在克隆 $REPO -> $DEST"
mkdir -p "$(dirname "$DEST")"
git clone "$REPO" "$DEST"
fi
exec python3 "$DEST/scripts/deploy.py"