Files
maestro/scripts/install-daemon.sh
T
wangjia 2c4140a1a2 feat(daemon): launchd plist + install/uninstall 脚本(tsk_nqvOyIZkd1Dn)
提供三个文件让 maestrod 作为 macOS User Agent 开机自启、崩溃自拉:

- scripts/com.maestro.maestrod.plist  — launchd plist 模板
    KeepAlive=true(崩溃自拉)、RunAtLoad=true(登录即启)
    stdout+stderr 统一写入 ~/.maestro/maestrod.log
    ThrottleInterval=10s 防崩溃风暴;ProcessType=Background
    占位符:__NODE__ / __PROJ_DIR__ / __HOME__ / __PATH__

- scripts/install-daemon.sh  — 安装脚本
    1. 检测 node(>= 20)
    2. npm run build 构建项目
    3. sed 替换占位符,生成真实 plist 并用 plutil 校验
    4. launchctl bootstrap gui/$UID(回退 load -w)加载服务

- scripts/uninstall-daemon.sh  — 卸载脚本
    launchctl bootout / unload -w 卸载,删除 plist

shellcheck 通过(两个脚本);现有 77 个单元测试全部通过。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 09:24:04 +08:00

114 lines
4.2 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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(推荐 Homebrewbrew 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"