- 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
+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 目标:
+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`
+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+```
+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[@]}"
+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"
+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" "$@"
+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}"
+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
+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}"