#!/bin/bash # # DooTask 一键安装 / 升级脚本 # # 用法(在目标目录执行): # curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash # # 脚本会根据「当前目录」自动判断该做什么,无需额外参数: # - 空目录 : 全新安装(克隆代码到当前目录 + ./cmd install) # - 已克隆但未安装 : 继续安装(./cmd install) # - 已安装 : 检查更新,确认后用「线上最新 cmd」执行升级 # - 非空且不是 DooTask : 拒绝操作并提示(绝不在此克隆或重置) # # 升级一次到位的关键:升级时从「线上 raw」取最新 cmd 到临时文件执行, # 既不依赖用户机器上那份可能过时的 cmd,也不写本地 .git(规避属主/权限问题), # 真正的 git pull / 依赖 / 迁移 / 重启全部交给这份最新 cmd,避免「升两次」。 # # 输出语言:仅当 locale 明确是中文 UTF-8 时显示中文,否则一律英文。 # set -u # ---------- 配置 ---------- BRANCH="pro" # 全新安装默认分支(升级时跟随当前分支) REPO_GITHUB="https://github.com/kuaifan/dootask.git" REPO_GITEE="https://gitee.com/aipaw/dootask.git" # raw 基址:升级时取版本号与最新 cmd 用。后期可把 RAW_PRIMARY 换成官网映射的域名。 RAW_PRIMARY="https://raw.githubusercontent.com/kuaifan/dootask" # https://// RAW_FALLBACK="https://cdn.jsdelivr.net/gh/kuaifan/dootask" # https://@/ # ---------- 语言判定 ---------- # 默认英文;仅当 locale 明确是「中文 UTF-8」时才用中文(中文非 UTF-8 如 GBK 也用英文以免乱码)。 DT_LANG="en" __loc="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" case "$__loc" in zh_*|zh-*|zh) case "$__loc" in *[Uu][Tt][Ff]*) DT_LANG="zh" ;; # 明确 UTF-8 → 中文 *.*) DT_LANG="en" ;; # 其他编码(如 .GBK)→ 英文,避免乱码 *) DT_LANG="zh" ;; # 无编码后缀(裸 zh_CN)→ 现代默认 UTF-8 esac ;; esac unset __loc # ---------- 文案 ---------- # 调用处只写中文(动态值用 (*) 占位,顺序对应后续参数,与前端 $L 风格一致)。 # 中文环境直接用原文;英文环境在此集中查表翻译,未登记的中文原样返回。 msg() { local tpl="$1"; shift local out="$tpl" if [ "$DT_LANG" != "zh" ]; then case "$tpl" in "成功") out="OK" ;; "警告") out="WARN" ;; "错误") out="ERROR" ;; "未知") out="unknown" ;; "git 未安装,请先安装后重试") out="git is not installed. Please install it and retry." ;; "curl 未安装,请先安装后重试") out="curl is not installed. Please install it and retry." ;; "Docker 未安装,请先安装后重试") out="Docker is not installed. Please install it and retry." ;; "docker-compose(或 docker compose 插件)未安装,请先安装后重试") out="docker-compose (or the docker compose plugin) is not installed. Please install it and retry." ;; "当前目录为空,开始全新安装 DooTask ...") out="Current directory is empty. Starting a fresh DooTask installation..." ;; "克隆代码(GitHub)...") out="Cloning source (GitHub)..." ;; "GitHub 克隆失败,尝试 Gitee 镜像 ...") out="GitHub clone failed, trying the Gitee mirror..." ;; "代码克隆失败,请检查网络后重试") out="Failed to clone the source. Please check your network and retry." ;; "代码克隆完成") out="Source cloned." ;; "执行安装 ...") out="Running installation..." ;; "DooTask 安装完成") out="DooTask installation complete." ;; "检测到已克隆但尚未安装,执行安装 ...") out="Repository found but not yet installed. Running installation..." ;; "检测到已安装的 DooTask,正在检查更新 ...") out="Existing DooTask installation detected. Checking for updates..." ;; "无法获取远程版本信息(分支 (*)),请检查网络后重试") out="Unable to fetch remote version info (branch (*)). Please check your network and retry." ;; "当前已是最新版本(v(*))") out="Already up to date (v(*))." ;; "发现新版本:当前 v(*) → 最新 v(*)(分支 (*))") out="New version available: current v(*) -> latest v(*) (branch (*))." ;; "是否立即升级?") out="Upgrade now?" ;; "已取消升级") out="Upgrade cancelled." ;; "获取最新 cmd 失败,请检查网络后重试") out="Failed to fetch the latest cmd. Please check your network and retry." ;; "开始升级 ...") out="Starting upgrade..." ;; "DooTask 升级完成") out="DooTask upgrade complete." ;; "当前目录非空,且不是 DooTask 项目目录。") out="Current directory is not empty and is not a DooTask project." ;; "请在「空目录」中执行全新安装,或进入「已安装的 DooTask 目录」执行升级。") out="Run this in an empty directory for a fresh install, or inside an existing DooTask directory to upgrade." ;; esac fi # 动态值:依次把 (*) 替换为参数 local a for a in "$@"; do out="${out/(\*)/$a}" done printf '%s' "$out" } # ---------- 输出 ---------- if [ -t 1 ]; then Red="\033[31m"; Green="\033[32m"; Yellow="\033[33m"; Blue="\033[36m"; Font="\033[0m" else Red=""; Green=""; Yellow=""; Blue=""; Font="" fi info() { echo -e "${Blue}==>${Font} $1"; } success() { echo -e "${Green}[$(msg 成功)]${Font} $1"; } warning() { echo -e "${Yellow}[$(msg 警告)]${Font} $1"; } error() { echo -e "${Red}[$(msg 错误)]${Font} $1" >&2; } die() { error "$1"; exit 1; } # ---------- 交互输入 ---------- # curl | bash 时 stdin 被管道占用,交互一律从 /dev/tty 读,否则 read 会读到 EOF has_tty() { [ -e /dev/tty ]; } confirm() { # $1=提示语,默认 Y;无终端时返回失败(不擅自执行需确认的操作) local prompt="$1" ans has_tty || return 1 read -r -p "$prompt [Y/n] " ans < /dev/tty [[ -z "$ans" || "$ans" =~ ^[Yy]([Ee][Ss])?$ ]] } # ---------- 提权执行 ---------- # install / update 需要 root;统一用 bash 执行脚本(规避 /tmp noexec), # 交互(含 cmd 内部的 read 与 sudo 密码)接到 /dev/tty。 # git clone 不走这里,用当前用户执行,避免代码属主变成 root。 run_cmd() { local script="$1"; shift local stdin_src="/dev/stdin" has_tty && stdin_src="/dev/tty" if [ "$(id -u)" -eq 0 ]; then bash "$script" "$@" < "$stdin_src" else sudo bash "$script" "$@" < "$stdin_src" fi } # ---------- 前置检查 ---------- precheck() { command -v git >/dev/null 2>&1 || die "$(msg 'git 未安装,请先安装后重试')" command -v curl >/dev/null 2>&1 || die "$(msg 'curl 未安装,请先安装后重试')" command -v docker >/dev/null 2>&1 || die "$(msg 'Docker 未安装,请先安装后重试')" if ! docker compose version >/dev/null 2>&1 && ! docker-compose version >/dev/null 2>&1; then die "$(msg 'docker-compose(或 docker compose 插件)未安装,请先安装后重试')" fi } # ---------- 工具 ---------- # 从 raw 取「指定分支的文件」到 stdout:主源失败时回退 jsdelivr 镜像 fetch_raw() { # $1=branch, $2=path curl -fsSL "${RAW_PRIMARY}/$1/$2" 2>/dev/null \ || curl -fsSL "${RAW_FALLBACK}@$1/$2" 2>/dev/null } # 从 stdin 读取 package.json 内容并提取版本号 read_pkg_version() { grep -m1 '"version"' | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' } is_dootask_project() { # 只读 .git,不写;当前用户读取一般文件不受属主影响 if [ -d .git ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then local url; url="$(git config --get remote.origin.url 2>/dev/null || true)" [[ "$url" == *dootask* ]] && return 0 fi [ -f cmd ] && [ -f docker-compose.yml ] && return 0 return 1 } is_installed() { [ -f vendor/autoload.php ]; } # 可忽略的系统垃圾文件(判断空目录时忽略;全新安装前会清除以便 git clone .) _IGNORABLE=".DS_Store .localized .Spotlight-V100 .fseventsd .TemporaryItems .Trashes .DocumentRevisions-V100 .VolumeIcon.icns .AppleDouble .AppleDB .AppleDesktop Thumbs.db ehthumbs.db desktop.ini .directory" # 是否「可忽略的系统文件」(含 macOS AppleDouble 的 ._xxx) _is_ignorable() { case " $_IGNORABLE " in *" $1 "*) return 0 ;; esac case "$1" in ._*) return 0 ;; esac return 1 } # 当前目录是否「实质为空」:只剩可忽略的系统垃圾文件也算空 dir_empty() { local f while IFS= read -r f; do _is_ignorable "${f##*/}" || return 1 done < <(find . -maxdepth 1 -mindepth 1 2>/dev/null) return 0 } # 清除可忽略的系统垃圾文件(仅白名单),确保 git clone . 不被这些文件挡住 clean_ignorable() { local f while IFS= read -r f; do _is_ignorable "${f##*/}" && rm -rf "$f" done < <(find . -maxdepth 1 -mindepth 1 2>/dev/null) } current_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "$BRANCH"; } # ---------- 动作:全新安装 ---------- do_fresh_install() { info "$(msg '当前目录为空,开始全新安装 DooTask ...')" clean_ignorable # 清掉 .DS_Store 等系统垃圾,确保 git clone . 不被挡住 info "$(msg '克隆代码(GitHub)...')" if ! git clone --depth=1 --branch "$BRANCH" "$REPO_GITHUB" . 2>/dev/null; then warning "$(msg 'GitHub 克隆失败,尝试 Gitee 镜像 ...')" rm -rf .git # 仅清理 clone 残留;若工作树仍有残留,下一步 clone 会报错而非误删 git clone --depth=1 --branch "$BRANCH" "$REPO_GITEE" . \ || die "$(msg '代码克隆失败,请检查网络后重试')" fi success "$(msg '代码克隆完成')" info "$(msg '执行安装 ...')" run_cmd ./cmd install success "$(msg 'DooTask 安装完成')" } # ---------- 动作:续装 ---------- do_install() { info "$(msg '检测到已克隆但尚未安装,执行安装 ...')" run_cmd ./cmd install success "$(msg 'DooTask 安装完成')" } # ---------- 动作:升级 ---------- do_upgrade() { info "$(msg '检测到已安装的 DooTask,正在检查更新 ...')" local branch; branch="$(current_branch)" local local_ver remote_ver local_ver="$( [ -f package.json ] && read_pkg_version < package.json )" remote_ver="$(fetch_raw "$branch" package.json | read_pkg_version)" [ -z "$local_ver" ] && local_ver="$(msg 未知)" # 取不到远程版本(网络/分支异常)→ 报错,避免误判 [ -z "$remote_ver" ] && die "$(msg '无法获取远程版本信息(分支 (*)),请检查网络后重试' "$branch")" if [ "$local_ver" = "$remote_ver" ]; then success "$(msg '当前已是最新版本(v(*))' "$local_ver")" exit 0 fi echo info "$(msg '发现新版本:当前 v(*) → 最新 v(*)(分支 (*))' "$local_ver" "$remote_ver" "$branch")" if ! confirm "$(msg '是否立即升级?')"; then warning "$(msg '已取消升级')" exit 0 fi # 从 raw 取「线上最新 cmd」到临时文件执行:不碰本地 .git、不依赖磁盘旧 cmd, # 真正的 git pull / 装依赖 / 迁移 / 重启由这份最新 cmd 完成,一次到位。 local tmp; tmp="$(mktemp)" if ! fetch_raw "$branch" cmd > "$tmp" || [ ! -s "$tmp" ]; then rm -f "$tmp"; die "$(msg '获取最新 cmd 失败,请检查网络后重试')" fi info "$(msg '开始升级 ...')" run_cmd "$tmp" update rm -f "$tmp" success "$(msg 'DooTask 升级完成')" } # ---------- 主流程 ---------- main() { precheck if is_dootask_project; then if is_installed; then do_upgrade else do_install fi elif dir_empty; then do_fresh_install else error "$(msg '当前目录非空,且不是 DooTask 项目目录。')" error "$(msg '请在「空目录」中执行全新安装,或进入「已安装的 DooTask 目录」执行升级。')" exit 1 fi } main "$@"