2c4140a1a2
提供三个文件让 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>
114 lines
4.2 KiB
Bash
114 lines
4.2 KiB
Bash
#!/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"
|