baa-conductor

git clone 

commit
a6e9636
parent
5279601
author
im_wower
date
2026-03-22 15:29:39 +0800 CST
Add mini runtime management and Firefox launcher scripts
9 files changed,  +821, -6
M docs/firefox/README.md
+31, -0
 1@@ -4,8 +4,39 @@
 2 
 3 - [`../../plugins/baa-firefox/`](../../plugins/baa-firefox/)
 4 
 5+当前推荐启动脚本:
 6+
 7+- [`../../scripts/firefox/open-firefox-with-plugin.sh`](../../scripts/firefox/open-firefox-with-plugin.sh)
 8+
 9 这里保留协议和控制面约定文档;具体实现以 `plugins/baa-firefox/` 为准。
10 
11+## 快速启动
12+
13+使用你当前指定的已登录 Firefox profile:
14+
15+```bash
16+./scripts/firefox/open-firefox-with-plugin.sh
17+```
18+
19+默认参数是:
20+
21+- profile: `/Users/george/Library/Application Support/Firefox/Profiles/biog272e.default-release`
22+- extension source: `/Users/george/code/baa-conductor/plugins/baa-firefox`
23+- firefox binary: `/Applications/Firefox.app/Contents/MacOS/firefox`
24+
25+如果要改 profile:
26+
27+```bash
28+./scripts/firefox/open-firefox-with-plugin.sh \
29+  --profile "/Users/george/Library/Application Support/Firefox/Profiles/biog272e.default-release"
30+```
31+
32+说明:
33+
34+- 这是 `web-ext run` 的临时加载模式
35+- 脚本进程退出后,扩展会被卸载
36+- 下次新对话直接运行这个脚本即可恢复同一套 profile + 插件环境
37+
38 本文档定义 `baa-firefox` 与 `baa-conductor` control API 之间的最小协议。
39 
40 目标:
M docs/runtime/README.md
+18, -5
 1@@ -17,9 +17,22 @@
 2 
 3 ## 最短路径
 4 
 5-1. `./scripts/runtime/bootstrap.sh`
 6-2. `npx --yes pnpm -r build`
 7-3. `./scripts/runtime/install-launchd.sh --node mini`
 8-4. `./scripts/runtime/check-launchd.sh --node mini`
 9-5. `./scripts/runtime/reload-launchd.sh`
10+1. `./scripts/runtime/install-mini.sh`
11+2. `./scripts/runtime/status-launchd.sh`
12+3. `./scripts/runtime/stop-launchd.sh`
13+4. `./scripts/runtime/start-launchd.sh`
14+5. `./scripts/runtime/restart-launchd.sh`
15 6. `./scripts/runtime/check-node.sh --node mini`
16+
17+## 当前推荐入口
18+
19+- 安装并切到当前正式仓库路径:
20+  - `./scripts/runtime/install-mini.sh`
21+- 查看当前 launchd / HTTP 状态:
22+  - `./scripts/runtime/status-launchd.sh`
23+- 停止:
24+  - `./scripts/runtime/stop-launchd.sh`
25+- 启动:
26+  - `./scripts/runtime/start-launchd.sh`
27+- 重启:
28+  - `./scripts/runtime/restart-launchd.sh`
M docs/runtime/launchd.md
+66, -1
 1@@ -1,6 +1,62 @@
 2 # launchd
 3 
 4-当前只记录 `mini` 的 launchd 安装路径。
 5+当前只记录 `mini` 单节点的 launchd 管理方式。
 6+
 7+## 当前目标状态
 8+
 9+- `conductor` 与 `status-api` 由 `launchd` 托管
10+- 工作目录固定到 `/Users/george/code/baa-conductor`
11+- 通过仓库内脚本统一安装、启动、停止、重启与验证
12+
13+## 一键安装到当前仓库路径
14+
15+```bash
16+./scripts/runtime/install-mini.sh
17+```
18+
19+这个脚本会顺序执行:
20+
21+1. 初始化 runtime 目录
22+2. 构建仓库
23+3. 渲染并安装 `conductor` / `status-api` 的 LaunchAgents
24+4. 重启 launchd 服务
25+5. 跑静态检查和节点检查
26+
27+默认会把共享 token 收口到:
28+
29+- `~/.config/baa-conductor/shared-token.txt`
30+
31+如果这个文件不存在,脚本会尝试从:
32+
33+- `~/.config/baa-conductor/control-api-worker.secrets.env`
34+
35+里提取 `BAA_SHARED_TOKEN` 并生成它。
36+
37+## 日常管理
38+
39+查看状态:
40+
41+```bash
42+./scripts/runtime/status-launchd.sh
43+```
44+
45+停止:
46+
47+```bash
48+./scripts/runtime/stop-launchd.sh
49+```
50+
51+启动:
52+
53+```bash
54+./scripts/runtime/start-launchd.sh
55+```
56+
57+重启:
58+
59+```bash
60+./scripts/runtime/restart-launchd.sh
61+```
62 
63 ## 1. 构建
64 
65@@ -71,3 +127,12 @@ npx --yes pnpm -r build
66   --expected-rolez leader \
67   --check-loaded
68 ```
69+
70+## 7. 当前验证口径
71+
72+最小验证就是这两条:
73+
74+```bash
75+./scripts/runtime/status-launchd.sh
76+./scripts/runtime/check-node.sh --node mini --check-loaded --expected-rolez leader
77+```
A scripts/firefox/open-firefox-with-plugin.sh
+118, -0
  1@@ -0,0 +1,118 @@
  2+#!/usr/bin/env bash
  3+set -euo pipefail
  4+
  5+usage() {
  6+  cat <<'EOF'
  7+Usage:
  8+  scripts/firefox/open-firefox-with-plugin.sh [options]
  9+
 10+Options:
 11+  --repo-dir PATH      Repo root. Defaults to /Users/george/code/baa-conductor.
 12+  --profile PATH       Firefox profile path.
 13+  --firefox-bin PATH   Firefox binary path.
 14+  --source-dir PATH    Extension source dir. Defaults to <repo>/plugins/baa-firefox.
 15+  --start-url URL      Optional start URL passed to web-ext.
 16+  --devtools           Open browser devtools.
 17+  --verbose            Enable verbose web-ext logging.
 18+  --help               Show this help text.
 19+
 20+Notes:
 21+  This uses web-ext temporary loading. Keep this script process alive while
 22+  you want the extension to remain installed in Firefox.
 23+EOF
 24+}
 25+
 26+require_command() {
 27+  if ! command -v "$1" >/dev/null 2>&1; then
 28+    printf '[firefox] error: missing command: %s\n' "$1" >&2
 29+    exit 1
 30+  fi
 31+}
 32+
 33+repo_dir="/Users/george/code/baa-conductor"
 34+profile_path="${HOME}/Library/Application Support/Firefox/Profiles/biog272e.default-release"
 35+firefox_bin="/Applications/Firefox.app/Contents/MacOS/firefox"
 36+source_dir=""
 37+start_url=""
 38+devtools="0"
 39+verbose="0"
 40+
 41+while [[ $# -gt 0 ]]; do
 42+  case "$1" in
 43+    --repo-dir)
 44+      repo_dir="$2"
 45+      shift 2
 46+      ;;
 47+    --profile)
 48+      profile_path="$2"
 49+      shift 2
 50+      ;;
 51+    --firefox-bin)
 52+      firefox_bin="$2"
 53+      shift 2
 54+      ;;
 55+    --source-dir)
 56+      source_dir="$2"
 57+      shift 2
 58+      ;;
 59+    --start-url)
 60+      start_url="$2"
 61+      shift 2
 62+      ;;
 63+    --devtools)
 64+      devtools="1"
 65+      shift
 66+      ;;
 67+    --verbose)
 68+      verbose="1"
 69+      shift
 70+      ;;
 71+    --help)
 72+      usage
 73+      exit 0
 74+      ;;
 75+    *)
 76+      printf '[firefox] error: unknown option: %s\n' "$1" >&2
 77+      exit 1
 78+      ;;
 79+  esac
 80+done
 81+
 82+require_command npx
 83+
 84+if [[ -z "$source_dir" ]]; then
 85+  source_dir="${repo_dir}/plugins/baa-firefox"
 86+fi
 87+
 88+[[ -d "$source_dir" ]] || { printf '[firefox] error: missing source dir: %s\n' "$source_dir" >&2; exit 1; }
 89+[[ -d "$profile_path" ]] || { printf '[firefox] error: missing profile dir: %s\n' "$profile_path" >&2; exit 1; }
 90+[[ -x "$firefox_bin" ]] || { printf '[firefox] error: missing firefox binary: %s\n' "$firefox_bin" >&2; exit 1; }
 91+
 92+cmd=(
 93+  npx --yes web-ext run
 94+  --source-dir "$source_dir"
 95+  --firefox "$firefox_bin"
 96+  --target firefox-desktop
 97+  --firefox-profile "$profile_path"
 98+  --keep-profile-changes
 99+  --pref=network.proxy.allow_hijacking_localhost=false
100+  --pref=network.proxy.no_proxies_on=localhost,127.0.0.1
101+)
102+
103+if [[ -n "$start_url" ]]; then
104+  cmd+=(--start-url "$start_url")
105+fi
106+
107+if [[ "$devtools" == "1" ]]; then
108+  cmd+=(--devtools)
109+fi
110+
111+if [[ "$verbose" == "1" ]]; then
112+  cmd+=(--verbose)
113+fi
114+
115+printf '[firefox] source dir: %s\n' "$source_dir"
116+printf '[firefox] profile: %s\n' "$profile_path"
117+printf '[firefox] binary: %s\n' "$firefox_bin"
118+
119+exec "${cmd[@]}"
A scripts/runtime/install-mini.sh
+201, -0
  1@@ -0,0 +1,201 @@
  2+#!/usr/bin/env bash
  3+set -euo pipefail
  4+
  5+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
  6+# shellcheck source=./common.sh
  7+source "${SCRIPT_DIR}/common.sh"
  8+
  9+usage() {
 10+  cat <<'EOF'
 11+Usage:
 12+  scripts/runtime/install-mini.sh [options]
 13+
 14+Options:
 15+  --repo-dir PATH           Repo root. Defaults to the current checkout.
 16+  --home-dir PATH           HOME used for LaunchAgents and defaults.
 17+  --install-dir PATH        Override launchd install directory.
 18+  --shared-token-file PATH  Preferred shared token file. Defaults to ~/.config/baa-conductor/shared-token.txt
 19+  --secrets-env PATH        Fallback env file used to extract BAA_SHARED_TOKEN.
 20+  --skip-build              Skip pnpm build.
 21+  --skip-restart            Skip launchd restart.
 22+  --skip-check              Skip check-launchd/check-node verification.
 23+  --help                    Show this help text.
 24+
 25+Notes:
 26+  This is the single-node mini convenience wrapper. It:
 27+  1. bootstraps runtime directories
 28+  2. builds the repo
 29+  3. installs conductor + status-api LaunchAgents
 30+  4. restarts them
 31+  5. verifies the node
 32+EOF
 33+}
 34+
 35+require_command awk
 36+require_command chmod
 37+require_command curl
 38+require_command mkdir
 39+require_command npx
 40+
 41+repo_dir="${BAA_RUNTIME_REPO_DIR_DEFAULT}"
 42+home_dir="$(default_home_dir)"
 43+install_dir=""
 44+shared_token_file="${HOME}/.config/baa-conductor/shared-token.txt"
 45+secrets_env="${HOME}/.config/baa-conductor/control-api-worker.secrets.env"
 46+skip_build="0"
 47+skip_restart="0"
 48+skip_check="0"
 49+status_api_base="http://100.71.210.78:4318"
 50+
 51+while [[ $# -gt 0 ]]; do
 52+  case "$1" in
 53+    --repo-dir)
 54+      repo_dir="$2"
 55+      shift 2
 56+      ;;
 57+    --home-dir)
 58+      home_dir="$2"
 59+      shift 2
 60+      ;;
 61+    --install-dir)
 62+      install_dir="$2"
 63+      shift 2
 64+      ;;
 65+    --shared-token-file)
 66+      shared_token_file="$2"
 67+      shift 2
 68+      ;;
 69+    --secrets-env)
 70+      secrets_env="$2"
 71+      shift 2
 72+      ;;
 73+    --skip-build)
 74+      skip_build="1"
 75+      shift
 76+      ;;
 77+    --skip-restart)
 78+      skip_restart="1"
 79+      shift
 80+      ;;
 81+    --skip-check)
 82+      skip_check="1"
 83+      shift
 84+      ;;
 85+    --help)
 86+      usage
 87+      exit 0
 88+      ;;
 89+    *)
 90+      die "Unknown option: $1"
 91+      ;;
 92+  esac
 93+done
 94+
 95+if [[ -z "$install_dir" ]]; then
 96+  install_dir="$(default_install_dir agent "$home_dir")"
 97+fi
 98+
 99+prepare_shared_token_file() {
100+  local target_path="$1"
101+  local env_file="$2"
102+  local token=""
103+
104+  if [[ -f "$target_path" ]]; then
105+    token="$(tr -d '\r\n' <"$target_path")"
106+  fi
107+
108+  if [[ -z "$token" && -f "$env_file" ]]; then
109+    token="$(awk -F= '/^BAA_SHARED_TOKEN=/{sub(/^[^=]*=/, ""); print; exit}' "$env_file")"
110+  fi
111+
112+  if [[ -z "$token" ]]; then
113+    die "Could not resolve BAA_SHARED_TOKEN. Provide ${target_path} or ${env_file}."
114+  fi
115+
116+  ensure_directory "$(dirname "$target_path")" "700"
117+  printf '%s\n' "$token" >"$target_path"
118+  chmod 600 "$target_path"
119+}
120+
121+prepare_shared_token_file "$shared_token_file" "$secrets_env"
122+
123+wait_for_http() {
124+  local name="$1"
125+  local url="$2"
126+  local attempts="${3:-30}"
127+  local delay="${4:-1}"
128+  local index
129+
130+  for ((index = 1; index <= attempts; index += 1)); do
131+    if curl -fsS "$url" >/dev/null 2>&1; then
132+      runtime_log "${name} is ready: ${url}"
133+      return 0
134+    fi
135+
136+    sleep "$delay"
137+  done
138+
139+  die "${name} did not become ready in time: ${url}"
140+}
141+
142+run_or_print 0 "${SCRIPT_DIR}/bootstrap.sh" --repo-dir "$repo_dir"
143+
144+if [[ "$skip_build" != "1" ]]; then
145+  (
146+    cd "$repo_dir"
147+    npx --yes pnpm -r build
148+  )
149+fi
150+
151+run_or_print 0 "${SCRIPT_DIR}/install-launchd.sh" \
152+  --repo-dir "$repo_dir" \
153+  --node mini \
154+  --service conductor \
155+  --service status-api \
156+  --install-dir "$install_dir" \
157+  --shared-token-file "$shared_token_file" \
158+  --control-api-base "https://control-api.makefile.so" \
159+  --local-api-base "http://100.71.210.78:4317" \
160+  --local-api-allowed-hosts "100.71.210.78" \
161+  --status-api-host "100.71.210.78"
162+
163+if [[ "$skip_restart" != "1" ]]; then
164+  run_or_print 0 "${SCRIPT_DIR}/restart-launchd.sh" \
165+    --install-dir "$install_dir" \
166+    --service conductor \
167+    --service status-api
168+fi
169+
170+if [[ "$skip_check" != "1" ]]; then
171+  wait_for_http "conductor" "http://100.71.210.78:4317/healthz"
172+  wait_for_http "status-api" "${status_api_base}/healthz"
173+
174+  run_or_print 0 "${SCRIPT_DIR}/check-launchd.sh" \
175+    --repo-dir "$repo_dir" \
176+    --node mini \
177+    --service conductor \
178+    --service status-api \
179+    --install-dir "$install_dir" \
180+    --shared-token-file "$shared_token_file" \
181+    --control-api-base "https://control-api.makefile.so" \
182+    --local-api-base "http://100.71.210.78:4317" \
183+    --local-api-allowed-hosts "100.71.210.78" \
184+    --status-api-host "100.71.210.78" \
185+    --check-loaded
186+
187+  run_or_print 0 "${SCRIPT_DIR}/check-node.sh" \
188+    --repo-dir "$repo_dir" \
189+    --node mini \
190+    --service conductor \
191+    --service status-api \
192+    --install-dir "$install_dir" \
193+    --shared-token-file "$shared_token_file" \
194+    --local-api-base "http://100.71.210.78:4317" \
195+    --local-api-allowed-hosts "100.71.210.78" \
196+    --status-api-base "${status_api_base}" \
197+    --status-api-host "100.71.210.78" \
198+    --expected-rolez leader \
199+    --check-loaded
200+fi
201+
202+runtime_log "mini runtime install completed"
A scripts/runtime/restart-launchd.sh
+6, -0
1@@ -0,0 +1,6 @@
2+#!/usr/bin/env bash
3+set -euo pipefail
4+
5+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
6+
7+exec "${SCRIPT_DIR}/reload-launchd.sh" "$@"
A scripts/runtime/start-launchd.sh
+119, -0
  1@@ -0,0 +1,119 @@
  2+#!/usr/bin/env bash
  3+set -euo pipefail
  4+
  5+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
  6+# shellcheck source=./common.sh
  7+source "${SCRIPT_DIR}/common.sh"
  8+
  9+usage() {
 10+  cat <<'EOF'
 11+Usage:
 12+  scripts/runtime/start-launchd.sh [options]
 13+
 14+Options:
 15+  --scope agent|daemon   launchd domain type. Defaults to agent.
 16+  --service NAME         Add one service to the start set. Repeatable.
 17+  --all-services         Start conductor, worker-runner, and status-api.
 18+  --home-dir PATH        Used only to derive the default LaunchAgents path.
 19+  --install-dir PATH     Override launchd install directory.
 20+  --domain TARGET        Override launchctl domain target. Defaults to gui/<uid> or system.
 21+  --dry-run              Print launchctl commands instead of executing them.
 22+  --help                 Show this help text.
 23+
 24+Notes:
 25+  If no service is specified, conductor + status-api are started.
 26+EOF
 27+}
 28+
 29+require_command launchctl
 30+require_command plutil
 31+
 32+scope="agent"
 33+home_dir="$(default_home_dir)"
 34+install_dir=""
 35+domain_target=""
 36+dry_run="0"
 37+services=()
 38+
 39+while [[ $# -gt 0 ]]; do
 40+  case "$1" in
 41+    --scope)
 42+      scope="$2"
 43+      shift 2
 44+      ;;
 45+    --service)
 46+      validate_service "$2"
 47+      if ! contains_value "$2" "${services[@]-}"; then
 48+        services+=("$2")
 49+      fi
 50+      shift 2
 51+      ;;
 52+    --all-services)
 53+      while IFS= read -r service; do
 54+        if ! contains_value "$service" "${services[@]-}"; then
 55+          services+=("$service")
 56+        fi
 57+      done < <(all_services)
 58+      shift
 59+      ;;
 60+    --home-dir)
 61+      home_dir="$2"
 62+      shift 2
 63+      ;;
 64+    --install-dir)
 65+      install_dir="$2"
 66+      shift 2
 67+      ;;
 68+    --domain)
 69+      domain_target="$2"
 70+      shift 2
 71+      ;;
 72+    --dry-run)
 73+      dry_run="1"
 74+      shift
 75+      ;;
 76+    --help)
 77+      usage
 78+      exit 0
 79+      ;;
 80+    *)
 81+      die "Unknown option: $1"
 82+      ;;
 83+  esac
 84+done
 85+
 86+validate_scope "$scope"
 87+
 88+if [[ "${#services[@]}" -eq 0 ]]; then
 89+  while IFS= read -r service; do
 90+    services+=("$service")
 91+  done < <(default_node_verification_services)
 92+fi
 93+
 94+if [[ -z "$install_dir" ]]; then
 95+  install_dir="$(default_install_dir "$scope" "$home_dir")"
 96+fi
 97+
 98+if [[ -z "$domain_target" ]]; then
 99+  domain_target="$(default_domain_target "$scope")"
100+fi
101+
102+service_is_loaded() {
103+  launchctl print "${domain_target}/$(service_label "$1")" >/dev/null 2>&1
104+}
105+
106+for service in "${services[@]}"; do
107+  plist_path="$(service_install_path "$install_dir" "$service")"
108+  assert_file "$plist_path"
109+  plutil -lint "$plist_path" >/dev/null
110+
111+  if service_is_loaded "$service"; then
112+    runtime_log "${service} already loaded"
113+    continue
114+  fi
115+
116+  run_or_print "$dry_run" launchctl bootstrap "$domain_target" "$plist_path"
117+  run_or_print "$dry_run" launchctl kickstart -k "${domain_target}/$(service_label "$service")"
118+done
119+
120+runtime_log "launchd start completed for ${domain_target}"
A scripts/runtime/status-launchd.sh
+146, -0
  1@@ -0,0 +1,146 @@
  2+#!/usr/bin/env bash
  3+set -euo pipefail
  4+
  5+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
  6+# shellcheck source=./common.sh
  7+source "${SCRIPT_DIR}/common.sh"
  8+
  9+usage() {
 10+  cat <<'EOF'
 11+Usage:
 12+  scripts/runtime/status-launchd.sh [options]
 13+
 14+Options:
 15+  --scope agent|daemon   launchd domain type. Defaults to agent.
 16+  --service NAME         Add one service to the status set. Repeatable.
 17+  --all-services         Show conductor, worker-runner, and status-api.
 18+  --home-dir PATH        Used only to derive the default LaunchAgents path.
 19+  --install-dir PATH     Override launchd install directory.
 20+  --domain TARGET        Override launchctl domain target. Defaults to gui/<uid> or system.
 21+  --local-api-base URL   Conductor local API base. Defaults to http://100.71.210.78:4317
 22+  --status-api-base URL  Status API base. Defaults to http://100.71.210.78:4318
 23+  --skip-http            Skip HTTP probes.
 24+  --help                 Show this help text.
 25+
 26+Notes:
 27+  If no service is specified, conductor + status-api are shown.
 28+EOF
 29+}
 30+
 31+require_command curl
 32+require_command launchctl
 33+require_command plutil
 34+
 35+scope="agent"
 36+home_dir="$(default_home_dir)"
 37+install_dir=""
 38+domain_target=""
 39+local_api_base="http://100.71.210.78:4317"
 40+status_api_base="http://100.71.210.78:4318"
 41+skip_http="0"
 42+services=()
 43+
 44+while [[ $# -gt 0 ]]; do
 45+  case "$1" in
 46+    --scope)
 47+      scope="$2"
 48+      shift 2
 49+      ;;
 50+    --service)
 51+      validate_service "$2"
 52+      if ! contains_value "$2" "${services[@]-}"; then
 53+        services+=("$2")
 54+      fi
 55+      shift 2
 56+      ;;
 57+    --all-services)
 58+      while IFS= read -r service; do
 59+        if ! contains_value "$service" "${services[@]-}"; then
 60+          services+=("$service")
 61+        fi
 62+      done < <(all_services)
 63+      shift
 64+      ;;
 65+    --home-dir)
 66+      home_dir="$2"
 67+      shift 2
 68+      ;;
 69+    --install-dir)
 70+      install_dir="$2"
 71+      shift 2
 72+      ;;
 73+    --domain)
 74+      domain_target="$2"
 75+      shift 2
 76+      ;;
 77+    --local-api-base)
 78+      local_api_base="$2"
 79+      shift 2
 80+      ;;
 81+    --status-api-base)
 82+      status_api_base="$2"
 83+      shift 2
 84+      ;;
 85+    --skip-http)
 86+      skip_http="1"
 87+      shift
 88+      ;;
 89+    --help)
 90+      usage
 91+      exit 0
 92+      ;;
 93+    *)
 94+      die "Unknown option: $1"
 95+      ;;
 96+  esac
 97+done
 98+
 99+validate_scope "$scope"
100+
101+if [[ "${#services[@]}" -eq 0 ]]; then
102+  while IFS= read -r service; do
103+    services+=("$service")
104+  done < <(default_node_verification_services)
105+fi
106+
107+if [[ -z "$install_dir" ]]; then
108+  install_dir="$(default_install_dir "$scope" "$home_dir")"
109+fi
110+
111+if [[ -z "$domain_target" ]]; then
112+  domain_target="$(default_domain_target "$scope")"
113+fi
114+
115+for service in "${services[@]}"; do
116+  label="$(service_label "$service")"
117+  plist_path="$(service_install_path "$install_dir" "$service")"
118+
119+  printf '=== %s ===\n' "$service"
120+  printf 'label: %s\n' "$label"
121+  printf 'plist: %s\n' "$plist_path"
122+
123+  if [[ -f "$plist_path" ]]; then
124+    printf 'plist_lint: ok\n'
125+  else
126+    printf 'plist_lint: missing\n'
127+  fi
128+
129+  if launchctl print "${domain_target}/${label}" >/dev/null 2>&1; then
130+    printf 'launchd: loaded\n'
131+  else
132+    printf 'launchd: not_loaded\n'
133+  fi
134+
135+  printf '\n'
136+done
137+
138+if [[ "$skip_http" != "1" ]]; then
139+  printf '=== http ===\n'
140+  printf 'conductor healthz: '
141+  curl -fsS "${local_api_base%/}/healthz" || true
142+  printf '\nrolez: '
143+  curl -fsS "${local_api_base%/}/rolez" || true
144+  printf '\nstatus-api /v1/status: '
145+  curl -fsS "${status_api_base%/}/v1/status" || true
146+  printf '\n'
147+fi
A scripts/runtime/stop-launchd.sh
+116, -0
  1@@ -0,0 +1,116 @@
  2+#!/usr/bin/env bash
  3+set -euo pipefail
  4+
  5+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
  6+# shellcheck source=./common.sh
  7+source "${SCRIPT_DIR}/common.sh"
  8+
  9+usage() {
 10+  cat <<'EOF'
 11+Usage:
 12+  scripts/runtime/stop-launchd.sh [options]
 13+
 14+Options:
 15+  --scope agent|daemon   launchd domain type. Defaults to agent.
 16+  --service NAME         Add one service to the stop set. Repeatable.
 17+  --all-services         Stop conductor, worker-runner, and status-api.
 18+  --home-dir PATH        Used only to derive the default LaunchAgents path.
 19+  --install-dir PATH     Override launchd install directory.
 20+  --domain TARGET        Override launchctl domain target. Defaults to gui/<uid> or system.
 21+  --dry-run              Print launchctl commands instead of executing them.
 22+  --help                 Show this help text.
 23+
 24+Notes:
 25+  If no service is specified, conductor + status-api are stopped.
 26+EOF
 27+}
 28+
 29+require_command launchctl
 30+
 31+scope="agent"
 32+home_dir="$(default_home_dir)"
 33+install_dir=""
 34+domain_target=""
 35+dry_run="0"
 36+services=()
 37+
 38+while [[ $# -gt 0 ]]; do
 39+  case "$1" in
 40+    --scope)
 41+      scope="$2"
 42+      shift 2
 43+      ;;
 44+    --service)
 45+      validate_service "$2"
 46+      if ! contains_value "$2" "${services[@]-}"; then
 47+        services+=("$2")
 48+      fi
 49+      shift 2
 50+      ;;
 51+    --all-services)
 52+      while IFS= read -r service; do
 53+        if ! contains_value "$service" "${services[@]-}"; then
 54+          services+=("$service")
 55+        fi
 56+      done < <(all_services)
 57+      shift
 58+      ;;
 59+    --home-dir)
 60+      home_dir="$2"
 61+      shift 2
 62+      ;;
 63+    --install-dir)
 64+      install_dir="$2"
 65+      shift 2
 66+      ;;
 67+    --domain)
 68+      domain_target="$2"
 69+      shift 2
 70+      ;;
 71+    --dry-run)
 72+      dry_run="1"
 73+      shift
 74+      ;;
 75+    --help)
 76+      usage
 77+      exit 0
 78+      ;;
 79+    *)
 80+      die "Unknown option: $1"
 81+      ;;
 82+  esac
 83+done
 84+
 85+validate_scope "$scope"
 86+
 87+if [[ "${#services[@]}" -eq 0 ]]; then
 88+  while IFS= read -r service; do
 89+    services+=("$service")
 90+  done < <(default_node_verification_services)
 91+fi
 92+
 93+if [[ -z "$install_dir" ]]; then
 94+  install_dir="$(default_install_dir "$scope" "$home_dir")"
 95+fi
 96+
 97+if [[ -z "$domain_target" ]]; then
 98+  domain_target="$(default_domain_target "$scope")"
 99+fi
100+
101+for service in "${services[@]}"; do
102+  plist_path="$(service_install_path "$install_dir" "$service")"
103+  if [[ ! -f "$plist_path" ]]; then
104+    runtime_log "skip ${service}: missing ${plist_path}"
105+    continue
106+  fi
107+
108+  if [[ "$dry_run" == "1" ]]; then
109+    printf '+ launchctl bootout %q %q 2>/dev/null || true\n' "$domain_target" "$plist_path"
110+    continue
111+  fi
112+
113+  launchctl bootout "$domain_target" "$plist_path" 2>/dev/null || true
114+  runtime_log "stopped ${service}"
115+done
116+
117+runtime_log "launchd stop completed for ${domain_target}"