- commit
- 7fd959d
- parent
- 088343b
- author
- im_wower
- date
- 2026-03-22 01:25:33 +0800 CST
Integrate T-021 runtime bootstrap
13 files changed,
+1139,
-97
1@@ -1,10 +1,10 @@
2 ---
3 task_id: T-021
4 title: launchd 安装脚本与 Runtime Bootstrap
5-status: todo
6+status: review
7 branch: feat/T-021-runtime-bootstrap
8 repo: /Users/george/code/baa-conductor
9-base_ref: main
10+base_ref: main@458d7cf
11 depends_on:
12 - T-011
13 - T-013
14@@ -14,7 +14,7 @@ write_scope:
15 - ops/launchd/**
16 - docs/runtime/**
17 - scripts/runtime/**
18-updated_at: 2026-03-22
19+updated_at: 2026-03-22T01:08:59+0800
20 ---
21
22 # T-021 launchd 安装脚本与 Runtime Bootstrap
23@@ -69,23 +69,47 @@ updated_at: 2026-03-22
24
25 ## files_changed
26
27-- 待填写
28+- `coordination/tasks/T-021-runtime-bootstrap.md`
29+- `docs/runtime/README.md`
30+- `docs/runtime/environment.md`
31+- `docs/runtime/launchd.md`
32+- `docs/runtime/layout.md`
33+- `ops/launchd/so.makefile.baa-conductor.plist`
34+- `ops/launchd/so.makefile.baa-worker-runner.plist`
35+- `ops/launchd/so.makefile.baa-status-api.plist`
36+- `scripts/runtime/bootstrap.sh`
37+- `scripts/runtime/check-launchd.sh`
38+- `scripts/runtime/common.sh`
39+- `scripts/runtime/install-launchd.sh`
40+- `scripts/runtime/reload-launchd.sh`
41
42 ## commands_run
43
44-- 待填写
45+- `npx --yes pnpm install`
46+- `chmod +x scripts/runtime/bootstrap.sh scripts/runtime/install-launchd.sh scripts/runtime/reload-launchd.sh scripts/runtime/check-launchd.sh`
47+- `bash -n scripts/runtime/*.sh`
48+- `plutil -lint ops/launchd/so.makefile.baa-conductor.plist ops/launchd/so.makefile.baa-worker-runner.plist ops/launchd/so.makefile.baa-status-api.plist`
49+- `git diff --check`
50+- `./scripts/runtime/bootstrap.sh --repo-dir "$TMP_REPO"`
51+- `./scripts/runtime/install-launchd.sh --repo-dir "$TMP_REPO" --home-dir "$TMP_HOME" --install-dir "$TMP_INSTALL" --node mini --all-services --shared-token test-token`
52+- `./scripts/runtime/check-launchd.sh --repo-dir "$TMP_REPO" --home-dir "$TMP_HOME" --install-dir "$TMP_INSTALL" --node mini --all-services --shared-token test-token`
53+- `./scripts/runtime/reload-launchd.sh --install-dir "$TMP_INSTALL" --all-services --dry-run`
54
55 ## result
56
57-- 待填写
58+- 已新增 `scripts/runtime/bootstrap.sh`、`install-launchd.sh`、`check-launchd.sh`、`reload-launchd.sh` 与共享 helper,使 runtime 目录初始化、plist 安装副本渲染、静态校验、`launchctl` 重载都有可执行脚本。
59+- 已把 `docs/runtime/**` 改为以脚本驱动流程为准,补充 `--node`、`--repo-dir`、`--shared-token`、`--scope` 等输入约定,并明确默认只安装 `conductor`、其它模板需显式 opt-in。
60+- 已小幅修正 `ops/launchd/*.plist` 说明并补入 `BAA_STATE_DIR`,同时在临时 repo 上完成 bootstrap -> install -> check -> reload(dry-run) 的整链验证。
61
62 ## risks
63
64-- 待填写
65+- 本任务只验证了 plist 渲染、静态检查和 dry-run `launchctl` 命令,没有在本机真实 `bootstrap` 任何 launchd 服务;实际目标机上仍需再做一次权限和 domain 验证。
66+- `worker-runner` 与 `status-api` 模板现在可以被脚本渲染和校验,但是否作为常驻服务启用仍取决于各自宿主进程接线进度;默认流程因此只安装 `conductor`。
67
68 ## next_handoff
69
70-- 待填写
71+- 在真实 `mini` / `mac` 节点上按文档顺序执行 `bootstrap.sh`、`npx --yes pnpm -r build`、`install-launchd.sh`、`check-launchd.sh`,确认安装副本和 runtime 目录都落在预期路径。
72+- 真正准备加载服务时,先运行 `reload-launchd.sh --dry-run` 复核命令;确认无误后再去掉 `--dry-run`。若后续 runner/status 宿主进程落地,可再显式加 `--service worker-runner` 或 `--service status-api`。
73
74 开始时建议直接把 `status` 改为 `in_progress`。
75
+16,
-7
1@@ -1,6 +1,6 @@
2 # runtime
3
4-本目录定义 `mini` 与 `mac` 上的本地 runtime 约定:目录布局、环境变量和 `launchd` 安装方式。
5+本目录定义 `mini` 与 `mac` 上的本地 runtime 约定:目录布局、环境变量,以及 `scripts/runtime/*.sh` 驱动的 `launchd` 安装方式。
6
7 当前仓库已经把 app 级 `build` 从单纯 typecheck 推进到真实 emit。执行 `npx --yes pnpm -r build` 后,`apps/*/dist/index.js` 会生成,`ops/launchd/*.plist` 里的入口路径也因此固定下来。
8
9@@ -9,19 +9,28 @@
10 ## 内容
11
12 - [`layout.md`](./layout.md): `runs/`、`worktrees/`、`logs/`、`tmp/` 与 `state/` 的初始化方式和生命周期
13-- [`environment.md`](./environment.md): `launchd` 下必须显式写入的环境变量
14-- [`launchd.md`](./launchd.md): `mini` 与 `mac` 的安装步骤,以及 `LaunchAgents` / `LaunchDaemons` 的差异
15+- [`environment.md`](./environment.md): `launchd` 下必须显式写入的环境变量,以及安装脚本如何覆盖默认值
16+- [`launchd.md`](./launchd.md): `mini` 与 `mac` 的脚本化安装步骤,以及 `LaunchAgents` / `LaunchDaemons` 的差异
17
18 ## 统一约定
19
20 - `mini` 是首选 leader,默认跑 `so.makefile.baa-conductor` 的 `primary` 配置。
21 - `mac` 使用同一组服务,但 conductor 默认角色是 `standby`。
22 - 两台机器都推荐把仓库放在 `/Users/george/code/baa-conductor`,这样 repo 内的 plist 源模板可以直接复用设计里的绝对路径。
23-- repo 中的 plist 只作为源模板;真正加载的是复制到 `~/Library/LaunchAgents/` 或 `/Library/LaunchDaemons/` 的副本。
24+- repo 中的 plist 只作为源模板;真正加载的是 `scripts/runtime/install-launchd.sh` 复制并改写到 `~/Library/LaunchAgents/` 或 `/Library/LaunchDaemons/` 的副本。
25
26 ## 最短路径
27
28-1. 先按 [`layout.md`](./layout.md) 初始化 runtime 根目录。
29+1. 先按 [`layout.md`](./layout.md) 运行 `./scripts/runtime/bootstrap.sh` 初始化 runtime 根目录。
30 2. 再按 [`environment.md`](./environment.md) 准备共享变量和节点变量,特别是 `BAA_SHARED_TOKEN`。
31-3. 最后按 [`launchd.md`](./launchd.md) 复制、调整并加载 plist。
32-4. 每次准备加载或重载本地服务前,先在 repo 根目录执行一次 `npx --yes pnpm -r build`,确认目标 app 的 `dist/index.js` 已更新。
33+3. 按 [`launchd.md`](./launchd.md) 运行 `install-launchd.sh` 生成安装副本,再用 `check-launchd.sh` / `reload-launchd.sh` 校验与重载。
34+4. 每次准备执行 `check-launchd.sh` 的 dist 校验或真正 `launchctl bootstrap` 前,先在 repo 根目录执行一次 `npx --yes pnpm -r build`,确认目标 app 的 `dist/index.js` 已更新。
35+
36+## 当前脚本集
37+
38+- `scripts/runtime/bootstrap.sh`: 预创建 `state/`、`runs/`、`worktrees/`、`logs/launchd/`、`tmp/`
39+- `scripts/runtime/install-launchd.sh`: 从 `ops/launchd/*.plist` 渲染实际安装副本
40+- `scripts/runtime/check-launchd.sh`: 校验源模板、runtime 目录、构建产物,以及已安装的 plist 副本
41+- `scripts/runtime/reload-launchd.sh`: 执行或 dry-run `launchctl bootout/bootstrap/kickstart`
42+
43+默认安装/检查/重载集合只包含 `conductor`。如果后续要把其它模板也纳入流程,显式加 `--service worker-runner`、`--service status-api`,或直接使用 `--all-services`。
+23,
-1
1@@ -19,10 +19,11 @@
2 | `BAA_WORKTREES_DIR` | `/Users/george/code/baa-conductor/worktrees` | `/Users/george/code/baa-conductor/worktrees` | task worktree 根目录 |
3 | `BAA_LOGS_DIR` | `/Users/george/code/baa-conductor/logs` | `/Users/george/code/baa-conductor/logs` | 服务日志根目录 |
4 | `BAA_TMP_DIR` | `/Users/george/code/baa-conductor/tmp` | `/Users/george/code/baa-conductor/tmp` | 临时目录根 |
5+| `BAA_STATE_DIR` | `/Users/george/code/baa-conductor/state` | `/Users/george/code/baa-conductor/state` | 本地状态镜像目录 |
6 | `BAA_NODE_ID` | `mini-main` | `mac-standby` | 节点实例 ID;应稳定且可用于 lease/日志归因 |
7 | `BAA_SHARED_TOKEN` | 安装时注入 | 安装时注入 | 节点到 control API 的共享认证口令 |
8
9-当前设计已经把 `state/` 纳入本地目录布局,但还没有单独定义 `BAA_STATE_DIR`。在真实 daemon 代码需要它之前,保持 `state/` 固定在 repo 根目录下即可。
10+`scripts/runtime/install-launchd.sh` 会把这些路径写进安装副本;如果 repo 根目录不在 `/Users/george/code/baa-conductor`,直接通过 `--repo-dir` 派生新的路径,不需要再手工改每个 plist。
11
12 ## launchd 辅助变量
13
14@@ -35,6 +36,18 @@
15
16 这些变量属于 `launchd` 安装层面的补充,不代表当前应用代码已经消费了全部字段。模板里统一保留它们,是为了避免后续服务落地时再拆分不同的 plist 版本。
17
18+`install-launchd.sh` 会同时根据 `--home-dir` 重写 `HOME` 和 `PATH`,避免模板里残留固定的 `/Users/george`。
19+
20+## 脚本输入约定
21+
22+`scripts/runtime/install-launchd.sh` / `check-launchd.sh` 目前主要消费这些输入:
23+
24+- `--node mini|mac`:决定 `BAA_CONDUCTOR_HOST`、`BAA_CONDUCTOR_ROLE`、`BAA_NODE_ID`
25+- `--repo-dir PATH`:决定 `WorkingDirectory`、`BAA_*_DIR` 与 `ProgramArguments` 里的 dist 入口
26+- `--home-dir PATH`:决定 `HOME`、`PATH` 与默认 `~/Library/LaunchAgents`
27+- `--shared-token`、`--shared-token-file` 或环境变量 `BAA_SHARED_TOKEN`:写入或校验 `BAA_SHARED_TOKEN`
28+- `--control-api-base`、`--local-api-base`:覆盖默认控制平面地址
29+
30 ## 节点最小差异集
31
32 如果两台机器的 repo 路径一致,`mac` 相对 `mini` 最少只要改这三项:
33@@ -56,3 +69,12 @@ BAA_NODE_ID=mac-standby
34 ```text
35 --host mac --role standby
36 ```
37+
38+脚本化安装时,这些差异不需要手工改 plist;改成:
39+
40+```bash
41+BAA_SHARED_TOKEN='replace-with-real-token'
42+./scripts/runtime/install-launchd.sh --repo-dir /Users/george/code/baa-conductor --node mac
43+```
44+
45+即可把安装副本改成 `mac` 的节点身份。
+102,
-60
1@@ -2,11 +2,19 @@
2
3 ## 服务集合
4
5-推荐长期守护的本地服务:
6+repo 里保留了三个源模板:
7
8 - `so.makefile.baa-conductor`
9 - `so.makefile.baa-worker-runner`
10-- 可选 `so.makefile.baa-status-api`
11+- `so.makefile.baa-status-api`
12+
13+当前脚本化流程默认只安装和重载 `conductor`。如果要把其它模板也纳入流程,显式加:
14+
15+- `--service worker-runner`
16+- `--service status-api`
17+- 或直接用 `--all-services`
18+
19+这样可以先把已接成 CLI/runtime 入口的服务跑通,再按需扩展其他模板。
20
21 repo 内的三个 plist 源模板都默认写成 `mini` 的 canonical 配置:
22
23@@ -21,6 +29,8 @@ repo 内的三个 plist 源模板都默认写成 `mini` 的 canonical 配置:
24 - `logs/launchd/` 已按 [`layout.md`](./layout.md) 创建
25 - 如果 repo 不在 `/Users/george/code/baa-conductor`,同步改 `WorkingDirectory`、`ProgramArguments`、`HOME`、`StandardOutPath`、`StandardErrorPath`
26
27+实际安装时推荐让脚本做这些改写,而不是手工编辑 plist。
28+
29 ## `LaunchAgents` 与 `LaunchDaemons`
30
31 | 目标路径 | 适合场景 | 优点 | 额外要求 |
32@@ -32,104 +42,126 @@ repo 内的三个 plist 源模板都默认写成 `mini` 的 canonical 配置:
33
34 ## mini 安装
35
36-`mini` 可以直接复用 repo 模板的大部分默认值,只需要先在复制后的副本里替换真实 token,并确认 control API 与本地端口设置正确。
37+`mini` 可以直接复用 repo 模板的大部分默认值。推荐顺序如下。
38
39-### 1. 复制模板
40+### 1. 初始化 runtime 根目录
41
42 ```bash
43 REPO_DIR=/Users/george/code/baa-conductor
44-AGENTS_DIR="$HOME/Library/LaunchAgents"
45-
46-install -d "$AGENTS_DIR"
47-cp "$REPO_DIR/ops/launchd/so.makefile.baa-conductor.plist" "$AGENTS_DIR/"
48-cp "$REPO_DIR/ops/launchd/so.makefile.baa-worker-runner.plist" "$AGENTS_DIR/"
49-cp "$REPO_DIR/ops/launchd/so.makefile.baa-status-api.plist" "$AGENTS_DIR/"
50+./scripts/runtime/bootstrap.sh --repo-dir "$REPO_DIR"
51 ```
52
53-### 2. 修改安装副本
54+### 2. 构建 dist 入口
55
56-最少检查这些字段:
57+```bash
58+cd /Users/george/code/baa-conductor
59+npx --yes pnpm -r build
60+```
61
62-- 所有 plist 里的 `BAA_SHARED_TOKEN`
63-- 所有 plist 里的 `BAA_CONTROL_API_BASE`
64-- 所有 plist 里的 `BAA_CONDUCTOR_LOCAL_API`
65+### 3. 渲染安装副本
66
67-如果 `status-api` 暂时不部署,可以删掉或不加载 `so.makefile.baa-status-api.plist`。
68+```bash
69+REPO_DIR=/Users/george/code/baa-conductor
70+export BAA_SHARED_TOKEN='replace-with-real-token'
71
72-### 3. 加载服务
73+./scripts/runtime/install-launchd.sh \
74+ --repo-dir "$REPO_DIR" \
75+ --node mini
76+```
77+
78+如果要同时渲染 runner/status 模板:
79
80 ```bash
81-launchctl bootout "gui/$(id -u)" "$AGENTS_DIR/so.makefile.baa-conductor.plist" 2>/dev/null || true
82-launchctl bootout "gui/$(id -u)" "$AGENTS_DIR/so.makefile.baa-worker-runner.plist" 2>/dev/null || true
83-launchctl bootout "gui/$(id -u)" "$AGENTS_DIR/so.makefile.baa-status-api.plist" 2>/dev/null || true
84+./scripts/runtime/install-launchd.sh \
85+ --repo-dir "$REPO_DIR" \
86+ --node mini \
87+ --all-services
88+```
89
90-launchctl bootstrap "gui/$(id -u)" "$AGENTS_DIR/so.makefile.baa-conductor.plist"
91-launchctl bootstrap "gui/$(id -u)" "$AGENTS_DIR/so.makefile.baa-worker-runner.plist"
92-launchctl bootstrap "gui/$(id -u)" "$AGENTS_DIR/so.makefile.baa-status-api.plist"
93+### 4. 静态校验安装副本
94
95-launchctl kickstart -k "gui/$(id -u)/so.makefile.baa-conductor"
96-launchctl kickstart -k "gui/$(id -u)/so.makefile.baa-worker-runner"
97-launchctl kickstart -k "gui/$(id -u)/so.makefile.baa-status-api"
98-```
99+```bash
100+REPO_DIR=/Users/george/code/baa-conductor
101+AGENTS_DIR="$HOME/Library/LaunchAgents"
102
103-如果 `status-api` 没启用,把对应三行删掉即可。
104+./scripts/runtime/check-launchd.sh \
105+ --repo-dir "$REPO_DIR" \
106+ --node mini \
107+ --install-dir "$AGENTS_DIR"
108+```
109
110-## mac 安装
111+### 5. 预览或执行重载
112
113-`mac` 与 `mini` 用同一组模板,但需要先把节点身份改成 standby。
114+```bash
115+./scripts/runtime/reload-launchd.sh --dry-run
116+```
117
118-### 1. 复制模板
119+确认输出无误后,再去掉 `--dry-run` 真正执行。
120
121-复制命令与 `mini` 相同,只是目标机器改成 `mac` 本机的 `~/Library/LaunchAgents/`。
122+## mac 安装
123
124-### 2. 修改节点差异项
125+`mac` 与 `mini` 用同一组模板,但节点身份切到 standby。
126
127-在三个 plist 的 `EnvironmentVariables` 里统一改:
128+### 1. 初始化 runtime 根目录与构建
129
130-- `BAA_CONDUCTOR_HOST=mac`
131-- `BAA_CONDUCTOR_ROLE=standby`
132-- `BAA_NODE_ID=mac-standby`
133-- `BAA_SHARED_TOKEN=<真实共享口令>`
134+```bash
135+REPO_DIR=/Users/george/code/baa-conductor
136+./scripts/runtime/bootstrap.sh --repo-dir "$REPO_DIR"
137+cd "$REPO_DIR"
138+npx --yes pnpm -r build
139+```
140
141-只在 conductor plist 里额外改:
142+### 2. 渲染 `mac` 安装副本
143
144-- `--host mini` -> `--host mac`
145-- `--role primary` -> `--role standby`
146+```bash
147+REPO_DIR=/Users/george/code/baa-conductor
148+export BAA_SHARED_TOKEN='replace-with-real-token'
149
150-如果 `mac` 的 repo 路径、home 目录或本地端口与 `mini` 不同,也在复制后的副本里同步改绝对路径与 `BAA_CONDUCTOR_LOCAL_API`。
151+./scripts/runtime/install-launchd.sh \
152+ --repo-dir "$REPO_DIR" \
153+ --node mac
154+```
155
156 ### 3. 加载服务
157
158-`launchctl bootstrap` 与 `kickstart` 命令和 `mini` 完全相同,只是执行机器换成 `mac`。
159+静态校验和重载命令与 `mini` 相同,只是把 `--node mini` 换成 `--node mac`。
160
161 ## 切到 `LaunchDaemons`
162
163-如果需要登录前启动,流程改成:
164+如果需要登录前启动,脚本流程改成:
165
166-1. 把编辑好的 plist 复制到 `/Library/LaunchDaemons/`
167-2. 在每个 plist 中增加 `UserName`,例如 `george`
168-3. 设置权限为 `root:wheel` 和 `0644`
169-4. 用 `sudo launchctl bootstrap system ...`
170+1. 用 `bootstrap.sh` 先准备 runtime 根目录
171+2. 用 `install-launchd.sh --scope daemon --username <user>` 渲染到 `/Library/LaunchDaemons/`
172+3. 如有需要,再手工 `sudo chown root:wheel /Library/LaunchDaemons/*.plist`
173+4. 用 `reload-launchd.sh --scope daemon` 或 `launchctl print system/...`
174
175 示例:
176
177 ```bash
178-PLIST_SRC="$HOME/Library/LaunchAgents/so.makefile.baa-conductor.plist"
179-
180-sudo cp "$PLIST_SRC" /Library/LaunchDaemons/
181-sudo chown root:wheel /Library/LaunchDaemons/so.makefile.baa-conductor.plist
182-sudo chmod 644 /Library/LaunchDaemons/so.makefile.baa-conductor.plist
183-
184-sudo launchctl bootout system /Library/LaunchDaemons/so.makefile.baa-conductor.plist 2>/dev/null || true
185-sudo launchctl bootstrap system /Library/LaunchDaemons/so.makefile.baa-conductor.plist
186-sudo launchctl kickstart -k system/so.makefile.baa-conductor
187+REPO_DIR=/Users/george/code/baa-conductor
188+export BAA_SHARED_TOKEN='replace-with-real-token'
189+
190+sudo ./scripts/runtime/install-launchd.sh \
191+ --repo-dir "$REPO_DIR" \
192+ --scope daemon \
193+ --node mini \
194+ --username george
195+
196+sudo ./scripts/runtime/check-launchd.sh \
197+ --repo-dir "$REPO_DIR" \
198+ --scope daemon \
199+ --node mini \
200+ --install-dir /Library/LaunchDaemons \
201+ --username george
202+
203+sudo ./scripts/runtime/reload-launchd.sh --scope daemon --dry-run
204 ```
205
206 使用 `LaunchDaemons` 时尤其要注意:
207
208 - plist 里的 `HOME`、repo 路径和日志路径不能依赖当前登录用户的 shell
209 - runtime 根目录必须对 `UserName` 指定的账号可写
210-- 不要把 repo 源模板直接 `sudo cp` 过去后立刻加载,先改安装副本
211+- 不要把 repo 源模板直接 `sudo cp` 过去后立刻加载,先通过安装脚本生成副本
212
213 ## 校验与排障
214
215@@ -141,6 +173,16 @@ plutil -lint ops/launchd/so.makefile.baa-worker-runner.plist
216 plutil -lint ops/launchd/so.makefile.baa-status-api.plist
217 ```
218
219+脚本化校验:
220+
221+```bash
222+./scripts/runtime/check-launchd.sh --repo-dir /Users/george/code/baa-conductor --node mini
223+./scripts/runtime/check-launchd.sh \
224+ --repo-dir /Users/george/code/baa-conductor \
225+ --node mini \
226+ --install-dir "$HOME/Library/LaunchAgents"
227+```
228+
229 运行时排障常用命令:
230
231 ```bash
232@@ -150,7 +192,7 @@ tail -n 50 /Users/george/code/baa-conductor/logs/launchd/so.makefile.baa-conduct
233 tail -n 50 /Users/george/code/baa-conductor/logs/launchd/so.makefile.baa-worker-runner.err.log
234 ```
235
236-当前仓库已经能为 app 生成基础 `dist/index.js` 产物,因此 launchd 不再依赖“未来某天才会出现的入口文件”。在执行 `launchctl bootstrap` 之前,先在 repo 根目录跑一次:
237+当前仓库已经能为 app 生成基础 `dist/index.js` 产物,因此 launchd 不再依赖“未来某天才会出现的入口文件”。在执行 `check-launchd.sh` 的 dist 校验或真正 `launchctl bootstrap` 之前,先在 repo 根目录跑一次:
238
239 ```bash
240 npx --yes pnpm -r build
241@@ -158,4 +200,4 @@ npx --yes pnpm -r build
242
243 这样可以确保 plist 指向的 `apps/*/dist/index.js` 已刷新到最新代码。
244
245-需要注意的是,第三波后续任务还会继续补各服务的真正运行时接线,所以“入口文件存在”不等于“业务逻辑已经全部落地”。这里解决的是部署路径与构建产物的一致性。
246+需要注意的是,当前脚本解决的是 runtime 目录、plist 渲染、静态检查与 `launchctl` 重载流程;它不替代各服务自己的运行时接线。这里解决的是部署路径与构建产物的一致性,而不是把所有业务进程都变成完整生产守护进程。
+12,
-21
1@@ -26,40 +26,31 @@
2
3 ## 一次性初始化
4
5-在两台机器上都先创建 runtime 根目录:
6+优先直接运行仓库内脚本:
7
8 ```bash
9 REPO_DIR=/Users/george/code/baa-conductor
10-
11-install -d -m 700 \
12- "$REPO_DIR/state" \
13- "$REPO_DIR/runs" \
14- "$REPO_DIR/worktrees" \
15- "$REPO_DIR/logs" \
16- "$REPO_DIR/logs/launchd" \
17- "$REPO_DIR/tmp"
18+./scripts/runtime/bootstrap.sh --repo-dir "$REPO_DIR"
19 ```
20
21-如果服务通过 `LaunchDaemons` 以专门账号运行,再把这些目录的 owner 改成该账号,而不是当前登录用户:
22+如果服务通过 `LaunchDaemons` 以专门账号运行,可以在初始化时直接带 owner:
23
24 ```bash
25-sudo chown -R george:staff \
26- "$REPO_DIR/state" \
27- "$REPO_DIR/runs" \
28- "$REPO_DIR/worktrees" \
29- "$REPO_DIR/logs" \
30- "$REPO_DIR/tmp"
31+REPO_DIR=/Users/george/code/baa-conductor
32+sudo ./scripts/runtime/bootstrap.sh --repo-dir "$REPO_DIR" --owner george:staff
33 ```
34
35+脚本本质上仍然只是执行 `install -d -m 700`,不会加载 `launchd` 服务,也不会修改 repo 之外的配置。
36+
37 ## 目录职责
38
39 | 路径 | 初始化方式 | 后续创建者 | 说明 |
40 | --- | --- | --- | --- |
41-| `state/` | 手工预创建根目录 | conductor | 本地状态镜像,便于恢复与对账 |
42-| `runs/` | 手工预创建根目录 | conductor/worker | 每个 run 在 `runs/<task-id>/<run-id>/` 下落本地元数据与日志 |
43-| `worktrees/` | 手工预创建根目录 | conductor | 每个 task 一个确定性的 worktree,worker 不负责清理 |
44-| `logs/` | 手工预创建根目录 | `launchd` 与服务进程 | `logs/launchd/` 放服务 stdout/stderr,其它运行期日志仍按 run 归档 |
45-| `tmp/` | 手工预创建根目录 | conductor/worker | 临时文件和中间产物,整机停服后才允许清空 |
46+| `state/` | `bootstrap.sh` 预创建根目录 | conductor | 本地状态镜像,便于恢复与对账 |
47+| `runs/` | `bootstrap.sh` 预创建根目录 | conductor/worker | 每个 run 在 `runs/<task-id>/<run-id>/` 下落本地元数据与日志 |
48+| `worktrees/` | `bootstrap.sh` 预创建根目录 | conductor | 每个 task 一个确定性的 worktree,worker 不负责清理 |
49+| `logs/` | `bootstrap.sh` 预创建根目录 | `launchd` 与服务进程 | `logs/launchd/` 放服务 stdout/stderr,其它运行期日志仍按 run 归档 |
50+| `tmp/` | `bootstrap.sh` 预创建根目录 | conductor/worker | 临时文件和中间产物,整机停服后才允许清空 |
51
52 ## `runs/` 的细化约定
53
1@@ -3,6 +3,7 @@
2 <!--
3 Source template kept in the repo.
4 Default values target the mini node at /Users/george/code/baa-conductor.
5+ Use scripts/runtime/install-launchd.sh to render the actual install copy.
6 For mac, change BAA_CONDUCTOR_HOST, BAA_CONDUCTOR_ROLE, BAA_NODE_ID,
7 BAA_SHARED_TOKEN, and keep the CLI args aligned with those values.
8 If this file is installed under /Library/LaunchDaemons, add UserName and keep
9@@ -42,6 +43,8 @@
10 <string>/Users/george/code/baa-conductor/logs</string>
11 <key>BAA_TMP_DIR</key>
12 <string>/Users/george/code/baa-conductor/tmp</string>
13+ <key>BAA_STATE_DIR</key>
14+ <string>/Users/george/code/baa-conductor/state</string>
15 <key>BAA_NODE_ID</key>
16 <string>mini-main</string>
17 <key>BAA_SHARED_TOKEN</key>
1@@ -4,6 +4,7 @@
2 Optional local status API.
3 Keep the same runtime paths as conductor and worker-runner so that service
4 logs and temporary files stay under one repo-owned runtime root.
5+ Use scripts/runtime/install-launchd.sh to render the actual install copy.
6 -->
7 <plist version="1.0">
8 <dict>
9@@ -39,6 +40,8 @@
10 <string>/Users/george/code/baa-conductor/logs</string>
11 <key>BAA_TMP_DIR</key>
12 <string>/Users/george/code/baa-conductor/tmp</string>
13+ <key>BAA_STATE_DIR</key>
14+ <string>/Users/george/code/baa-conductor/state</string>
15 <key>BAA_NODE_ID</key>
16 <string>mini-main</string>
17 <key>BAA_SHARED_TOKEN</key>
1@@ -3,6 +3,7 @@
2 <!--
3 Source template kept in the repo.
4 Defaults target the mini node and share the same runtime tree as conductor.
5+ Use scripts/runtime/install-launchd.sh to render the actual install copy.
6 For mac, change BAA_CONDUCTOR_HOST, BAA_CONDUCTOR_ROLE, BAA_NODE_ID, and
7 BAA_SHARED_TOKEN before copying the file into the launchd install path.
8 -->
9@@ -40,6 +41,8 @@
10 <string>/Users/george/code/baa-conductor/logs</string>
11 <key>BAA_TMP_DIR</key>
12 <string>/Users/george/code/baa-conductor/tmp</string>
13+ <key>BAA_STATE_DIR</key>
14+ <string>/Users/george/code/baa-conductor/state</string>
15 <key>BAA_NODE_ID</key>
16 <string>mini-main</string>
17 <key>BAA_SHARED_TOKEN</key>
+59,
-0
1@@ -0,0 +1,59 @@
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/bootstrap.sh [options]
13+
14+Options:
15+ --repo-dir PATH Runtime root to initialize. Defaults to the current checkout root.
16+ --mode OCTAL Directory mode passed to install -d. Defaults to 700.
17+ --owner USER:GROUP Optional owner to apply to each runtime directory after creation.
18+ --help Show this help text.
19+EOF
20+}
21+
22+repo_dir="${BAA_RUNTIME_REPO_DIR_DEFAULT}"
23+mode="700"
24+owner=""
25+
26+while [[ $# -gt 0 ]]; do
27+ case "$1" in
28+ --repo-dir)
29+ repo_dir="$2"
30+ shift 2
31+ ;;
32+ --mode)
33+ mode="$2"
34+ shift 2
35+ ;;
36+ --owner)
37+ owner="$2"
38+ shift 2
39+ ;;
40+ --help)
41+ usage
42+ exit 0
43+ ;;
44+ *)
45+ die "Unknown option: $1"
46+ ;;
47+ esac
48+done
49+
50+while IFS= read -r directory; do
51+ ensure_directory "$directory" "$mode"
52+
53+ if [[ -n "$owner" ]]; then
54+ chown "$owner" "$directory"
55+ fi
56+
57+ runtime_log "ready: $directory"
58+done < <(resolve_runtime_paths "$repo_dir")
59+
60+runtime_log "runtime bootstrap completed for ${repo_dir}"
+251,
-0
1@@ -0,0 +1,251 @@
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/check-launchd.sh [options]
13+
14+Options:
15+ --node mini|mac Select node defaults. Defaults to mini.
16+ --scope agent|daemon Expected launchd scope for install copies. Defaults to agent.
17+ --service NAME Add one service to the check set. Repeatable.
18+ --all-services Check conductor, worker-runner, and status-api.
19+ --repo-dir PATH Repo root used to derive runtime paths.
20+ --home-dir PATH HOME value expected in installed plist files.
21+ --install-dir PATH Validate installed copies under this directory.
22+ --shared-token TOKEN Expect this exact token in installed copies.
23+ --shared-token-file PATH Read the expected token from a file.
24+ --control-api-base URL Expected BAA_CONTROL_API_BASE.
25+ --local-api-base URL Expected BAA_CONDUCTOR_LOCAL_API.
26+ --username NAME Expected UserName for LaunchDaemons.
27+ --skip-dist-check Skip dist/index.js existence checks.
28+ --check-loaded Also require launchctl print to succeed for each selected service.
29+ --domain TARGET Override launchctl domain target for --check-loaded.
30+ --help Show this help text.
31+
32+Notes:
33+ If no service is specified, only conductor is checked. Use --all-services or
34+ repeat --service to opt into worker-runner/status-api templates.
35+EOF
36+}
37+
38+require_command plutil
39+assert_file /usr/libexec/PlistBuddy
40+
41+node="mini"
42+scope="agent"
43+repo_dir="${BAA_RUNTIME_REPO_DIR_DEFAULT}"
44+home_dir="$(default_home_dir)"
45+install_dir=""
46+shared_token=""
47+shared_token_file=""
48+control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
49+local_api_base="${BAA_RUNTIME_DEFAULT_LOCAL_API}"
50+username="$(default_username)"
51+skip_dist_check="0"
52+check_loaded="0"
53+domain_target=""
54+services=()
55+
56+while [[ $# -gt 0 ]]; do
57+ case "$1" in
58+ --node)
59+ node="$2"
60+ shift 2
61+ ;;
62+ --scope)
63+ scope="$2"
64+ shift 2
65+ ;;
66+ --service)
67+ validate_service "$2"
68+ if ! contains_value "$2" "${services[@]-}"; then
69+ services+=("$2")
70+ fi
71+ shift 2
72+ ;;
73+ --all-services)
74+ while IFS= read -r service; do
75+ if ! contains_value "$service" "${services[@]-}"; then
76+ services+=("$service")
77+ fi
78+ done < <(all_services)
79+ shift
80+ ;;
81+ --repo-dir)
82+ repo_dir="$2"
83+ shift 2
84+ ;;
85+ --home-dir)
86+ home_dir="$2"
87+ shift 2
88+ ;;
89+ --install-dir)
90+ install_dir="$2"
91+ shift 2
92+ ;;
93+ --shared-token)
94+ shared_token="$2"
95+ shift 2
96+ ;;
97+ --shared-token-file)
98+ shared_token_file="$2"
99+ shift 2
100+ ;;
101+ --control-api-base)
102+ control_api_base="$2"
103+ shift 2
104+ ;;
105+ --local-api-base)
106+ local_api_base="$2"
107+ shift 2
108+ ;;
109+ --username)
110+ username="$2"
111+ shift 2
112+ ;;
113+ --skip-dist-check)
114+ skip_dist_check="1"
115+ shift
116+ ;;
117+ --check-loaded)
118+ check_loaded="1"
119+ shift
120+ ;;
121+ --domain)
122+ domain_target="$2"
123+ shift 2
124+ ;;
125+ --help)
126+ usage
127+ exit 0
128+ ;;
129+ *)
130+ die "Unknown option: $1"
131+ ;;
132+ esac
133+done
134+
135+validate_node "$node"
136+validate_scope "$scope"
137+
138+if [[ "${#services[@]}" -eq 0 ]]; then
139+ while IFS= read -r service; do
140+ services+=("$service")
141+ done < <(default_services)
142+fi
143+
144+if [[ -n "$shared_token" || -n "$shared_token_file" ]]; then
145+ shared_token="$(load_shared_token "$shared_token" "$shared_token_file")"
146+fi
147+
148+set -- $(resolve_node_defaults "$node")
149+conductor_host="$1"
150+conductor_role="$2"
151+node_id="$3"
152+launchd_path="$(default_launchd_path "$home_dir")"
153+state_dir="${repo_dir}/state"
154+runs_dir="${repo_dir}/runs"
155+worktrees_dir="${repo_dir}/worktrees"
156+logs_dir="${repo_dir}/logs"
157+logs_launchd_dir="${logs_dir}/launchd"
158+tmp_dir="${repo_dir}/tmp"
159+
160+assert_directory "$state_dir"
161+assert_directory "$runs_dir"
162+assert_directory "$worktrees_dir"
163+assert_directory "$logs_dir"
164+assert_directory "$logs_launchd_dir"
165+assert_directory "$tmp_dir"
166+
167+check_string_equals() {
168+ local name="$1"
169+ local actual="$2"
170+ local expected="$3"
171+
172+ if [[ "$actual" != "$expected" ]]; then
173+ die "${name} mismatch: expected '${expected}', got '${actual}'"
174+ fi
175+}
176+
177+check_installed_plist() {
178+ local service="$1"
179+ local plist_path="$2"
180+ local stdout_path="$3"
181+ local stderr_path="$4"
182+ local dist_entry="$5"
183+ local actual_shared_token=""
184+
185+ assert_file "$plist_path"
186+ plutil -lint "$plist_path" >/dev/null
187+
188+ check_string_equals "${service}:WorkingDirectory" "$(plist_print_value "$plist_path" ":WorkingDirectory")" "$repo_dir"
189+ check_string_equals "${service}:PATH" "$(plist_print_value "$plist_path" ":EnvironmentVariables:PATH")" "$launchd_path"
190+ check_string_equals "${service}:HOME" "$(plist_print_value "$plist_path" ":EnvironmentVariables:HOME")" "$home_dir"
191+ check_string_equals "${service}:BAA_CONDUCTOR_HOST" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_HOST")" "$conductor_host"
192+ check_string_equals "${service}:BAA_CONDUCTOR_ROLE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_ROLE")" "$conductor_role"
193+ check_string_equals "${service}:BAA_CONTROL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" "$control_api_base"
194+ check_string_equals "${service}:BAA_CONDUCTOR_LOCAL_API" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API")" "$local_api_base"
195+ check_string_equals "${service}:BAA_RUNS_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_RUNS_DIR")" "$runs_dir"
196+ check_string_equals "${service}:BAA_WORKTREES_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_WORKTREES_DIR")" "$worktrees_dir"
197+ check_string_equals "${service}:BAA_LOGS_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_LOGS_DIR")" "$logs_dir"
198+ check_string_equals "${service}:BAA_TMP_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_TMP_DIR")" "$tmp_dir"
199+ check_string_equals "${service}:BAA_STATE_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_STATE_DIR")" "$state_dir"
200+ check_string_equals "${service}:BAA_NODE_ID" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_NODE_ID")" "$node_id"
201+ check_string_equals "${service}:stdout" "$(plist_print_value "$plist_path" ":StandardOutPath")" "$stdout_path"
202+ check_string_equals "${service}:stderr" "$(plist_print_value "$plist_path" ":StandardErrorPath")" "$stderr_path"
203+ check_string_equals "${service}:entry" "$(plist_print_value "$plist_path" ":ProgramArguments:2")" "$dist_entry"
204+
205+ actual_shared_token="$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_SHARED_TOKEN")"
206+ if [[ -n "$shared_token" ]]; then
207+ check_string_equals "${service}:BAA_SHARED_TOKEN" "$actual_shared_token" "$shared_token"
208+ elif [[ -z "$actual_shared_token" || "$actual_shared_token" == "replace-me" ]]; then
209+ die "${service}: BAA_SHARED_TOKEN is empty or still replace-me"
210+ fi
211+
212+ if [[ "$service" == "conductor" ]]; then
213+ check_string_equals "${service}:host-arg" "$(plist_print_value "$plist_path" ":ProgramArguments:4")" "$conductor_host"
214+ check_string_equals "${service}:role-arg" "$(plist_print_value "$plist_path" ":ProgramArguments:6")" "$conductor_role"
215+ fi
216+
217+ if [[ "$scope" == "daemon" ]]; then
218+ check_string_equals "${service}:UserName" "$(plist_print_value "$plist_path" ":UserName")" "$username"
219+ fi
220+}
221+
222+for service in "${services[@]}"; do
223+ template_path="$(service_template_path "$repo_dir" "$service")"
224+ dist_entry="${repo_dir}/$(service_dist_entry_relative "$service")"
225+ stdout_path="$(service_stdout_path "$logs_launchd_dir" "$service")"
226+ stderr_path="$(service_stderr_path "$logs_launchd_dir" "$service")"
227+
228+ assert_file "$template_path"
229+ plutil -lint "$template_path" >/dev/null
230+
231+ if [[ "$skip_dist_check" != "1" ]]; then
232+ assert_file "$dist_entry"
233+ fi
234+
235+ if [[ -n "$install_dir" ]]; then
236+ check_installed_plist "$service" "$(service_install_path "$install_dir" "$service")" "$stdout_path" "$stderr_path" "$dist_entry"
237+ fi
238+done
239+
240+if [[ "$check_loaded" == "1" ]]; then
241+ require_command launchctl
242+
243+ if [[ -z "$domain_target" ]]; then
244+ domain_target="$(default_domain_target "$scope")"
245+ fi
246+
247+ for service in "${services[@]}"; do
248+ launchctl print "${domain_target}/$(service_label "$service")" >/dev/null
249+ done
250+fi
251+
252+runtime_log "launchd checks passed"
+294,
-0
1@@ -0,0 +1,294 @@
2+#!/usr/bin/env bash
3+
4+if [[ -n "${BAA_RUNTIME_COMMON_SH_LOADED:-}" ]]; then
5+ return 0
6+fi
7+
8+readonly BAA_RUNTIME_COMMON_SH_LOADED=1
9+readonly BAA_RUNTIME_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
10+readonly BAA_RUNTIME_REPO_DIR_DEFAULT="$(cd -- "${BAA_RUNTIME_SCRIPT_DIR}/../.." && pwd)"
11+readonly BAA_RUNTIME_DEFAULT_CONTROL_API_BASE="https://control-api.makefile.so"
12+readonly BAA_RUNTIME_DEFAULT_LOCAL_API="http://127.0.0.1:4317"
13+readonly BAA_RUNTIME_DEFAULT_LOCALE="en_US.UTF-8"
14+
15+runtime_log() {
16+ printf '[runtime] %s\n' "$*"
17+}
18+
19+runtime_error() {
20+ printf '[runtime] error: %s\n' "$*" >&2
21+}
22+
23+die() {
24+ runtime_error "$*"
25+ exit 1
26+}
27+
28+require_command() {
29+ if ! command -v "$1" >/dev/null 2>&1; then
30+ die "Missing required command: $1"
31+ fi
32+}
33+
34+contains_value() {
35+ local needle="$1"
36+ shift
37+
38+ local value
39+ for value in "$@"; do
40+ if [[ "$value" == "$needle" ]]; then
41+ return 0
42+ fi
43+ done
44+
45+ return 1
46+}
47+
48+validate_service() {
49+ case "$1" in
50+ conductor | worker-runner | status-api) ;;
51+ *)
52+ die "Unsupported service: $1"
53+ ;;
54+ esac
55+}
56+
57+validate_scope() {
58+ case "$1" in
59+ agent | daemon) ;;
60+ *)
61+ die "Unsupported launchd scope: $1"
62+ ;;
63+ esac
64+}
65+
66+validate_node() {
67+ case "$1" in
68+ mini | mac) ;;
69+ *)
70+ die "Unsupported node: $1"
71+ ;;
72+ esac
73+}
74+
75+default_services() {
76+ printf '%s\n' conductor
77+}
78+
79+all_services() {
80+ printf '%s\n' conductor worker-runner status-api
81+}
82+
83+service_label() {
84+ case "$1" in
85+ conductor)
86+ printf '%s\n' "so.makefile.baa-conductor"
87+ ;;
88+ worker-runner)
89+ printf '%s\n' "so.makefile.baa-worker-runner"
90+ ;;
91+ status-api)
92+ printf '%s\n' "so.makefile.baa-status-api"
93+ ;;
94+ esac
95+}
96+
97+service_dist_entry_relative() {
98+ case "$1" in
99+ conductor)
100+ printf '%s\n' "apps/conductor-daemon/dist/index.js"
101+ ;;
102+ worker-runner)
103+ printf '%s\n' "apps/worker-runner/dist/index.js"
104+ ;;
105+ status-api)
106+ printf '%s\n' "apps/status-api/dist/index.js"
107+ ;;
108+ esac
109+}
110+
111+service_template_path() {
112+ local repo_dir="$1"
113+ local service="$2"
114+
115+ printf '%s/ops/launchd/%s.plist\n' "$repo_dir" "$(service_label "$service")"
116+}
117+
118+service_install_path() {
119+ local install_dir="$1"
120+ local service="$2"
121+
122+ printf '%s/%s.plist\n' "$install_dir" "$(service_label "$service")"
123+}
124+
125+service_stdout_path() {
126+ local logs_launchd_dir="$1"
127+ local service="$2"
128+
129+ printf '%s/%s.out.log\n' "$logs_launchd_dir" "$(service_label "$service")"
130+}
131+
132+service_stderr_path() {
133+ local logs_launchd_dir="$1"
134+ local service="$2"
135+
136+ printf '%s/%s.err.log\n' "$logs_launchd_dir" "$(service_label "$service")"
137+}
138+
139+resolve_node_defaults() {
140+ case "$1" in
141+ mini)
142+ printf '%s %s %s\n' "mini" "primary" "mini-main"
143+ ;;
144+ mac)
145+ printf '%s %s %s\n' "mac" "standby" "mac-standby"
146+ ;;
147+ esac
148+}
149+
150+default_home_dir() {
151+ if [[ -n "${HOME:-}" ]]; then
152+ printf '%s\n' "$HOME"
153+ return 0
154+ fi
155+
156+ printf '/Users/%s\n' "$(id -un)"
157+}
158+
159+default_username() {
160+ printf '%s\n' "$(id -un)"
161+}
162+
163+default_launchd_path() {
164+ local home_dir="$1"
165+
166+ printf '%s\n' "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${home_dir}/.local/bin:${home_dir}/bin"
167+}
168+
169+default_install_dir() {
170+ local scope="$1"
171+ local home_dir="$2"
172+
173+ case "$scope" in
174+ agent)
175+ printf '%s/Library/LaunchAgents\n' "$home_dir"
176+ ;;
177+ daemon)
178+ printf '%s\n' "/Library/LaunchDaemons"
179+ ;;
180+ esac
181+}
182+
183+default_domain_target() {
184+ local scope="$1"
185+
186+ case "$scope" in
187+ agent)
188+ printf 'gui/%s\n' "$(id -u)"
189+ ;;
190+ daemon)
191+ printf '%s\n' "system"
192+ ;;
193+ esac
194+}
195+
196+ensure_directory() {
197+ local path="$1"
198+ local mode="$2"
199+
200+ install -d -m "$mode" "$path"
201+}
202+
203+assert_directory() {
204+ if [[ ! -d "$1" ]]; then
205+ die "Missing directory: $1"
206+ fi
207+}
208+
209+assert_file() {
210+ if [[ ! -f "$1" ]]; then
211+ die "Missing file: $1"
212+ fi
213+}
214+
215+escape_plist_value() {
216+ local value="$1"
217+
218+ value=${value//\\/\\\\}
219+ value=${value//\"/\\\"}
220+
221+ printf '%s' "$value"
222+}
223+
224+plist_set_string() {
225+ local plist_path="$1"
226+ local key="$2"
227+ local value
228+
229+ value="$(escape_plist_value "$3")"
230+
231+ if ! /usr/libexec/PlistBuddy -c "Set ${key} \"${value}\"" "$plist_path" >/dev/null 2>&1; then
232+ /usr/libexec/PlistBuddy -c "Add ${key} string \"${value}\"" "$plist_path" >/dev/null
233+ fi
234+}
235+
236+plist_delete_key() {
237+ local plist_path="$1"
238+ local key="$2"
239+
240+ /usr/libexec/PlistBuddy -c "Delete ${key}" "$plist_path" >/dev/null 2>&1 || true
241+}
242+
243+plist_print_value() {
244+ local plist_path="$1"
245+ local key="$2"
246+
247+ /usr/libexec/PlistBuddy -c "Print ${key}" "$plist_path"
248+}
249+
250+print_shell_command() {
251+ printf '+'
252+
253+ local arg
254+ for arg in "$@"; do
255+ printf ' %q' "$arg"
256+ done
257+
258+ printf '\n'
259+}
260+
261+run_or_print() {
262+ local dry_run="$1"
263+ shift
264+
265+ if [[ "$dry_run" == "1" ]]; then
266+ print_shell_command "$@"
267+ return 0
268+ fi
269+
270+ "$@"
271+}
272+
273+resolve_runtime_paths() {
274+ local repo_dir="$1"
275+
276+ printf '%s\n' \
277+ "${repo_dir}/state" \
278+ "${repo_dir}/runs" \
279+ "${repo_dir}/worktrees" \
280+ "${repo_dir}/logs" \
281+ "${repo_dir}/logs/launchd" \
282+ "${repo_dir}/tmp"
283+}
284+
285+load_shared_token() {
286+ local shared_token="$1"
287+ local shared_token_file="$2"
288+
289+ if [[ -n "$shared_token_file" ]]; then
290+ assert_file "$shared_token_file"
291+ shared_token="$(tr -d '\r\n' <"$shared_token_file")"
292+ fi
293+
294+ printf '%s' "$shared_token"
295+}
+204,
-0
1@@ -0,0 +1,204 @@
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-launchd.sh [options]
13+
14+Options:
15+ --node mini|mac Select node defaults. Defaults to mini.
16+ --scope agent|daemon Install under LaunchAgents or LaunchDaemons. Defaults to agent.
17+ --service NAME Add one service to the install set. Repeatable.
18+ --all-services Install conductor, worker-runner, and status-api templates.
19+ --repo-dir PATH Repo root used for WorkingDirectory and runtime paths.
20+ --home-dir PATH HOME value written into plist files.
21+ --install-dir PATH Override launchd install directory.
22+ --shared-token TOKEN Shared token written into the install copy.
23+ --shared-token-file PATH Read the shared token from a file.
24+ --control-api-base URL Override BAA_CONTROL_API_BASE.
25+ --local-api-base URL Override BAA_CONDUCTOR_LOCAL_API.
26+ --username NAME UserName for LaunchDaemons. Defaults to the current user.
27+ --help Show this help text.
28+
29+Notes:
30+ If no service is specified, only conductor is installed. Use --all-services or
31+ repeat --service to opt into worker-runner/status-api templates.
32+EOF
33+}
34+
35+require_command cp
36+require_command plutil
37+assert_file /usr/libexec/PlistBuddy
38+
39+node="mini"
40+scope="agent"
41+repo_dir="${BAA_RUNTIME_REPO_DIR_DEFAULT}"
42+home_dir="$(default_home_dir)"
43+install_dir=""
44+shared_token="${BAA_SHARED_TOKEN:-}"
45+shared_token_file=""
46+control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
47+local_api_base="${BAA_RUNTIME_DEFAULT_LOCAL_API}"
48+username="$(default_username)"
49+services=()
50+
51+while [[ $# -gt 0 ]]; do
52+ case "$1" in
53+ --node)
54+ node="$2"
55+ shift 2
56+ ;;
57+ --scope)
58+ scope="$2"
59+ shift 2
60+ ;;
61+ --service)
62+ validate_service "$2"
63+ if ! contains_value "$2" "${services[@]-}"; then
64+ services+=("$2")
65+ fi
66+ shift 2
67+ ;;
68+ --all-services)
69+ while IFS= read -r service; do
70+ if ! contains_value "$service" "${services[@]-}"; then
71+ services+=("$service")
72+ fi
73+ done < <(all_services)
74+ shift
75+ ;;
76+ --repo-dir)
77+ repo_dir="$2"
78+ shift 2
79+ ;;
80+ --home-dir)
81+ home_dir="$2"
82+ shift 2
83+ ;;
84+ --install-dir)
85+ install_dir="$2"
86+ shift 2
87+ ;;
88+ --shared-token)
89+ shared_token="$2"
90+ shift 2
91+ ;;
92+ --shared-token-file)
93+ shared_token_file="$2"
94+ shift 2
95+ ;;
96+ --control-api-base)
97+ control_api_base="$2"
98+ shift 2
99+ ;;
100+ --local-api-base)
101+ local_api_base="$2"
102+ shift 2
103+ ;;
104+ --username)
105+ username="$2"
106+ shift 2
107+ ;;
108+ --help)
109+ usage
110+ exit 0
111+ ;;
112+ *)
113+ die "Unknown option: $1"
114+ ;;
115+ esac
116+done
117+
118+validate_node "$node"
119+validate_scope "$scope"
120+
121+if [[ "${#services[@]}" -eq 0 ]]; then
122+ while IFS= read -r service; do
123+ services+=("$service")
124+ done < <(default_services)
125+fi
126+
127+shared_token="$(load_shared_token "$shared_token" "$shared_token_file")"
128+if [[ -z "$shared_token" ]]; then
129+ die "A shared token is required. Use --shared-token, --shared-token-file, or BAA_SHARED_TOKEN."
130+fi
131+
132+if [[ -z "$install_dir" ]]; then
133+ install_dir="$(default_install_dir "$scope" "$home_dir")"
134+fi
135+
136+set -- $(resolve_node_defaults "$node")
137+conductor_host="$1"
138+conductor_role="$2"
139+node_id="$3"
140+
141+launchd_path="$(default_launchd_path "$home_dir")"
142+state_dir="${repo_dir}/state"
143+runs_dir="${repo_dir}/runs"
144+worktrees_dir="${repo_dir}/worktrees"
145+logs_dir="${repo_dir}/logs"
146+logs_launchd_dir="${logs_dir}/launchd"
147+tmp_dir="${repo_dir}/tmp"
148+
149+assert_directory "$state_dir"
150+assert_directory "$runs_dir"
151+assert_directory "$worktrees_dir"
152+assert_directory "$logs_dir"
153+assert_directory "$logs_launchd_dir"
154+assert_directory "$tmp_dir"
155+
156+ensure_directory "$install_dir" "755"
157+
158+for service in "${services[@]}"; do
159+ template_path="$(service_template_path "$repo_dir" "$service")"
160+ install_path="$(service_install_path "$install_dir" "$service")"
161+ stdout_path="$(service_stdout_path "$logs_launchd_dir" "$service")"
162+ stderr_path="$(service_stderr_path "$logs_launchd_dir" "$service")"
163+ dist_entry="${repo_dir}/$(service_dist_entry_relative "$service")"
164+
165+ assert_file "$template_path"
166+ cp "$template_path" "$install_path"
167+
168+ plist_set_string "$install_path" ":WorkingDirectory" "$repo_dir"
169+ plist_set_string "$install_path" ":EnvironmentVariables:PATH" "$launchd_path"
170+ plist_set_string "$install_path" ":EnvironmentVariables:HOME" "$home_dir"
171+ plist_set_string "$install_path" ":EnvironmentVariables:LANG" "$BAA_RUNTIME_DEFAULT_LOCALE"
172+ plist_set_string "$install_path" ":EnvironmentVariables:LC_ALL" "$BAA_RUNTIME_DEFAULT_LOCALE"
173+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_HOST" "$conductor_host"
174+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_ROLE" "$conductor_role"
175+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$control_api_base"
176+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API" "$local_api_base"
177+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_RUNS_DIR" "$runs_dir"
178+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_WORKTREES_DIR" "$worktrees_dir"
179+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_LOGS_DIR" "$logs_dir"
180+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_TMP_DIR" "$tmp_dir"
181+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_STATE_DIR" "$state_dir"
182+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_NODE_ID" "$node_id"
183+ plist_set_string "$install_path" ":EnvironmentVariables:BAA_SHARED_TOKEN" "$shared_token"
184+ plist_set_string "$install_path" ":StandardOutPath" "$stdout_path"
185+ plist_set_string "$install_path" ":StandardErrorPath" "$stderr_path"
186+ plist_set_string "$install_path" ":ProgramArguments:2" "$dist_entry"
187+
188+ if [[ "$service" == "conductor" ]]; then
189+ plist_set_string "$install_path" ":ProgramArguments:4" "$conductor_host"
190+ plist_set_string "$install_path" ":ProgramArguments:6" "$conductor_role"
191+ fi
192+
193+ if [[ "$scope" == "daemon" ]]; then
194+ plist_set_string "$install_path" ":UserName" "$username"
195+ else
196+ plist_delete_key "$install_path" ":UserName"
197+ fi
198+
199+ chmod 644 "$install_path"
200+ plutil -lint "$install_path" >/dev/null
201+
202+ runtime_log "installed ${service} template -> ${install_path}"
203+done
204+
205+runtime_log "launchd install copies rendered for ${node} (${scope})"
+137,
-0
1@@ -0,0 +1,137 @@
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/reload-launchd.sh [options]
13+
14+Options:
15+ --scope agent|daemon launchd domain type. Defaults to agent.
16+ --service NAME Add one service to the reload set. Repeatable.
17+ --all-services Reload 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+ --skip-kickstart Skip launchctl kickstart after bootstrap.
22+ --dry-run Print launchctl commands instead of executing them.
23+ --help Show this help text.
24+EOF
25+}
26+
27+require_command launchctl
28+require_command plutil
29+
30+scope="agent"
31+home_dir="$(default_home_dir)"
32+install_dir=""
33+domain_target=""
34+dry_run="0"
35+skip_kickstart="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+ --skip-kickstart)
72+ skip_kickstart="1"
73+ shift
74+ ;;
75+ --dry-run)
76+ dry_run="1"
77+ shift
78+ ;;
79+ --help)
80+ usage
81+ exit 0
82+ ;;
83+ *)
84+ die "Unknown option: $1"
85+ ;;
86+ esac
87+done
88+
89+validate_scope "$scope"
90+
91+if [[ "${#services[@]}" -eq 0 ]]; then
92+ while IFS= read -r service; do
93+ services+=("$service")
94+ done < <(default_services)
95+fi
96+
97+if [[ -z "$install_dir" ]]; then
98+ install_dir="$(default_install_dir "$scope" "$home_dir")"
99+fi
100+
101+if [[ -z "$domain_target" ]]; then
102+ domain_target="$(default_domain_target "$scope")"
103+fi
104+
105+bootout_service() {
106+ local plist_path="$1"
107+
108+ if [[ "$dry_run" == "1" ]]; then
109+ printf '+ launchctl bootout %q %q 2>/dev/null || true\n' "$domain_target" "$plist_path"
110+ return 0
111+ fi
112+
113+ launchctl bootout "$domain_target" "$plist_path" 2>/dev/null || true
114+}
115+
116+for service in "${services[@]}"; do
117+ plist_path="$(service_install_path "$install_dir" "$service")"
118+ assert_file "$plist_path"
119+ plutil -lint "$plist_path" >/dev/null
120+done
121+
122+for service in "${services[@]}"; do
123+ plist_path="$(service_install_path "$install_dir" "$service")"
124+ bootout_service "$plist_path"
125+done
126+
127+for service in "${services[@]}"; do
128+ plist_path="$(service_install_path "$install_dir" "$service")"
129+ run_or_print "$dry_run" launchctl bootstrap "$domain_target" "$plist_path"
130+done
131+
132+if [[ "$skip_kickstart" != "1" ]]; then
133+ for service in "${services[@]}"; do
134+ run_or_print "$dry_run" launchctl kickstart -k "${domain_target}/$(service_label "$service")"
135+ done
136+fi
137+
138+runtime_log "launchd reload completed for ${domain_target}"