merge: maestro/tsk_nqvOyIZkd1Dn [tsk_nqvOyIZkd1Dn]

This commit is contained in:
maestro
2026-06-13 10:26:42 +08:00
3 changed files with 230 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
maestrod launchd User Agent 模板
============================================================
此文件为模板,占位符由 install-daemon.sh 在安装时替换后写入:
~/Library/LaunchAgents/com.maestro.maestrod.plist
占位符说明:
__NODE__ - node 可执行文件的绝对路径(如 /opt/homebrew/bin/node
__PROJ_DIR__ - 项目根目录绝对路径
__HOME__ - 用户家目录绝对路径
__PATH__ - 环境变量 PATH(含 node 所在目录)
管理命令:
安装/重装:bash scripts/install-daemon.sh
卸载: bash scripts/uninstall-daemon.sh
查看状态: launchctl list com.maestro.maestrod
实时日志: tail -f ~/.maestro/maestrod.log
-->
<plist version="1.0">
<dict>
<!-- 服务标识符,必须全局唯一 -->
<key>Label</key>
<string>com.maestro.maestrod</string>
<!-- 启动命令:node <项目>/dist/daemon/index.js -->
<key>ProgramArguments</key>
<array>
<string>__NODE__</string>
<string>__PROJ_DIR__/dist/daemon/index.js</string>
</array>
<!-- 崩溃 / 退出后自动拉起 -->
<key>KeepAlive</key>
<true/>
<!-- 用户登录后立即启动(开机常驻) -->
<key>RunAtLoad</key>
<true/>
<!-- 日志:stdout + stderr 合并写入同一文件 -->
<key>StandardOutPath</key>
<string>__HOME__/.maestro/maestrod.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.maestro/maestrod.log</string>
<!-- 工作目录 -->
<key>WorkingDirectory</key>
<string>__PROJ_DIR__</string>
<!-- 最小运行环境(launchd 默认环境极简,HOME/PATH 必须显式注入) -->
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>__HOME__</string>
<key>PATH</key>
<string>__PATH__</string>
</dict>
<!-- 两次崩溃拉起之间的最短冷却时间(秒),防止崩溃风暴 -->
<key>ThrottleInterval</key>
<integer>10</integer>
<!-- 标记为后台进程 -->
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
+113
View File
@@ -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(推荐 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"
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# uninstall-daemon.sh — 卸载 maestrod launchd 用户 Agent
#
# 功能:
# 1. 停止并卸载 launchd 服务
# 2. 删除 ~/Library/LaunchAgents/com.maestro.maestrod.plist
#
# 注意:数据目录 ~/.maestro(SQLite 数据库、日志)不会被删除,需手动清理。
#
# 用法:
# bash scripts/uninstall-daemon.sh
set -euo pipefail
LABEL="com.maestro.maestrod"
PLIST_DEST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
echo "==> 卸载 maestrod (${LABEL})…"
# ── 1. 停止并卸载服务 ─────────────────────────────────────────
if launchctl list 2>/dev/null | grep -qF "${LABEL}"; then
echo " 服务运行中,正在停止…"
# 优先使用 bootoutmacOS 10.11+ 推荐),失败则回退 unload
if launchctl bootout "gui/${UID}" "${PLIST_DEST}" 2>/dev/null; then
echo " 已通过 bootout 卸载"
elif launchctl unload -w "${PLIST_DEST}" 2>/dev/null; then
echo " 已通过 unload 卸载"
else
echo "警告:无法正常卸载服务,将继续删除 plist 文件" >&2
fi
else
echo " 服务未加载,跳过停止步骤"
fi
# ── 2. 删除 plist ─────────────────────────────────────────────
if [[ -f "${PLIST_DEST}" ]]; then
rm -f "${PLIST_DEST}"
echo " 已删除: ${PLIST_DEST}"
else
echo " plist 不存在,跳过删除"
fi
echo ""
echo "✅ maestrod 已卸载"
echo ""
echo " 注意:数据目录 ${HOME}/.maestro 未被删除(保留数据库和日志)。"
echo " 如需彻底清除:rm -rf ${HOME}/.maestro"