#!/usr/bin/env bash # install-daemon.sh — 安装 maestrod 为 launchd 用户 Agent # # 功能: # 1. 检测 node 路径 # 2. 构建项目(tsc → dist/) # 3. 生成 ~/Library/LaunchAgents/com.maestro.maestrod.plist(填充绝对路径) # 4. 通过 launchctl 加载服务(开机自启 + 崩溃自拉) # # 用法: # bash scripts/install-daemon.sh # # 重装(更新 plist / 重启服务):重新执行本脚本即可。 set -euo pipefail LABEL="com.maestro.maestrod" PLIST_NAME="${LABEL}.plist" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJ_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" PLIST_TEMPLATE="${SCRIPT_DIR}/${PLIST_NAME}" PLIST_DEST="${HOME}/Library/LaunchAgents/${PLIST_NAME}" DATA_DIR="${HOME}/.maestro" # ── 1. 检测 Node.js ────────────────────────────────────────── echo "==> 检测 Node.js…" NODE_BIN="$(command -v node || true)" if [[ -z "${NODE_BIN}" ]]; then echo "错误:找不到 node,请先安装 Node.js >= 20(推荐 Homebrew:brew install node)" >&2 exit 1 fi NODE_VER="$(node --version 2>&1)" # 主版本号(去掉 "v" 和 ".x.y") NODE_MAJOR="$(node --version 2>&1 | sed 's/^v//' | cut -d. -f1)" if [[ "${NODE_MAJOR}" -lt 20 ]]; then echo "错误:需要 Node.js >= 20,当前 ${NODE_VER}" >&2 exit 1 fi echo " node: ${NODE_BIN} (${NODE_VER})" # node 所在目录,确保 launchd 进程能找到 node 和相关工具 NODE_DIR="$(dirname "${NODE_BIN}")" # 合并常用路径,去重 DAEMON_PATH="${NODE_DIR}:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" # ── 2. 构建项目 ─────────────────────────────────────────────── echo "==> 构建项目(tsc)…" cd "${PROJ_DIR}" npm run build DAEMON_ENTRY="${PROJ_DIR}/dist/daemon/index.js" if [[ ! -f "${DAEMON_ENTRY}" ]]; then echo "错误:构建产物不存在:${DAEMON_ENTRY}" >&2 exit 1 fi echo " 入口文件: ${DAEMON_ENTRY}" # ── 3. 确保数据/日志目录存在 ───────────────────────────────── echo "==> 创建数据目录 ${DATA_DIR}…" mkdir -p "${DATA_DIR}" mkdir -p "${HOME}/Library/LaunchAgents" # ── 4. 生成 plist(用 sed 填充占位符)──────────────────────── echo "==> 生成 plist…" if [[ ! -f "${PLIST_TEMPLATE}" ]]; then echo "错误:找不到 plist 模板:${PLIST_TEMPLATE}" >&2 exit 1 fi # 使用中间临时文件,避免 sed in-place 跨平台差异 TMP_PLIST="$(mktemp /tmp/${LABEL}.XXXXXX.plist)" trap 'rm -f "${TMP_PLIST}"' EXIT sed \ -e "s|__NODE__|${NODE_BIN}|g" \ -e "s|__PROJ_DIR__|${PROJ_DIR}|g" \ -e "s|__HOME__|${HOME}|g" \ -e "s|__PATH__|${DAEMON_PATH}|g" \ "${PLIST_TEMPLATE}" > "${TMP_PLIST}" # 校验生成的 plist plutil -lint "${TMP_PLIST}" || { echo "错误:生成的 plist 格式不合法" >&2; exit 1; } cp "${TMP_PLIST}" "${PLIST_DEST}" echo " 已写入: ${PLIST_DEST}" # ── 5. 加载(或重载)服务 ──────────────────────────────────── echo "==> 加载 launchd 服务…" # 若服务已运行,先卸载再重载 if launchctl list 2>/dev/null | grep -qF "${LABEL}"; then echo " 检测到旧服务,先卸载…" launchctl bootout "gui/${UID}" "${PLIST_DEST}" 2>/dev/null \ || launchctl unload -w "${PLIST_DEST}" 2>/dev/null \ || true # 短暂等待彻底退出 sleep 1 fi # bootstrap 是 macOS 10.11+ 推荐 API;失败时回退到 load -w if ! launchctl bootstrap "gui/${UID}" "${PLIST_DEST}" 2>/dev/null; then launchctl load -w "${PLIST_DEST}" fi echo "" echo "✅ maestrod 安装完成" echo "" echo " 服务标识: ${LABEL}" echo " 日志文件: ${DATA_DIR}/maestrod.log" echo " API 地址: http://127.0.0.1:4517" echo "" echo " 常用命令:" echo " 查看状态: launchctl list ${LABEL}" echo " 实时日志: tail -f ${DATA_DIR}/maestrod.log" echo " 停止服务: launchctl stop ${LABEL}" echo " 启动服务: launchctl start ${LABEL}" echo " 卸载: bash ${SCRIPT_DIR}/uninstall-daemon.sh"