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>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user