@ag-eco/agentplate-cli 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +462 -0
  3. package/agents/ap-co-creation.md +90 -0
  4. package/agents/builder.md +144 -0
  5. package/agents/coordinator.md +377 -0
  6. package/agents/lead.md +435 -0
  7. package/agents/merger.md +164 -0
  8. package/agents/monitor.md +214 -0
  9. package/agents/orchestrator.md +239 -0
  10. package/agents/reviewer.md +140 -0
  11. package/agents/scout.md +125 -0
  12. package/agents/supervisor.md +427 -0
  13. package/package.json +66 -0
  14. package/src/agents/capabilities.test.ts +85 -0
  15. package/src/agents/capabilities.ts +125 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +101 -0
  18. package/src/agents/copilot-hooks-deployer.test.ts +162 -0
  19. package/src/agents/copilot-hooks-deployer.ts +93 -0
  20. package/src/agents/guard-rules.test.ts +372 -0
  21. package/src/agents/guard-rules.ts +97 -0
  22. package/src/agents/headless-mail-injector.test.ts +709 -0
  23. package/src/agents/headless-mail-injector.ts +377 -0
  24. package/src/agents/headless-prompt.test.ts +102 -0
  25. package/src/agents/headless-prompt.ts +68 -0
  26. package/src/agents/hooks-deployer.test.ts +3119 -0
  27. package/src/agents/hooks-deployer.ts +804 -0
  28. package/src/agents/identity.test.ts +604 -0
  29. package/src/agents/identity.ts +384 -0
  30. package/src/agents/lifecycle.test.ts +196 -0
  31. package/src/agents/lifecycle.ts +183 -0
  32. package/src/agents/mail-poll-detect.test.ts +153 -0
  33. package/src/agents/mail-poll-detect.ts +73 -0
  34. package/src/agents/manifest.test.ts +1026 -0
  35. package/src/agents/manifest.ts +376 -0
  36. package/src/agents/overlay.test.ts +1058 -0
  37. package/src/agents/overlay.ts +490 -0
  38. package/src/agents/scope-detect.test.ts +190 -0
  39. package/src/agents/scope-detect.ts +146 -0
  40. package/src/agents/turn-lock.test.ts +181 -0
  41. package/src/agents/turn-lock.ts +235 -0
  42. package/src/agents/turn-runner-dispatch.test.ts +182 -0
  43. package/src/agents/turn-runner-dispatch.ts +105 -0
  44. package/src/agents/turn-runner.test.ts +2312 -0
  45. package/src/agents/turn-runner.ts +1383 -0
  46. package/src/beads/client.test.ts +217 -0
  47. package/src/beads/client.ts +230 -0
  48. package/src/beads/molecules.test.ts +338 -0
  49. package/src/beads/molecules.ts +198 -0
  50. package/src/commands/agents.test.ts +328 -0
  51. package/src/commands/agents.ts +299 -0
  52. package/src/commands/clean.test.ts +797 -0
  53. package/src/commands/clean.ts +791 -0
  54. package/src/commands/completions.test.ts +348 -0
  55. package/src/commands/completions.ts +981 -0
  56. package/src/commands/coordinator.test.ts +2975 -0
  57. package/src/commands/coordinator.ts +1841 -0
  58. package/src/commands/costs.test.ts +1183 -0
  59. package/src/commands/costs.ts +599 -0
  60. package/src/commands/dashboard.test.ts +954 -0
  61. package/src/commands/dashboard.ts +1212 -0
  62. package/src/commands/discover.test.ts +288 -0
  63. package/src/commands/discover.ts +202 -0
  64. package/src/commands/doctor.test.ts +303 -0
  65. package/src/commands/doctor.ts +311 -0
  66. package/src/commands/ecosystem.test.ts +226 -0
  67. package/src/commands/ecosystem.ts +248 -0
  68. package/src/commands/errors.test.ts +654 -0
  69. package/src/commands/errors.ts +197 -0
  70. package/src/commands/feed.test.ts +709 -0
  71. package/src/commands/feed.ts +260 -0
  72. package/src/commands/group.test.ts +475 -0
  73. package/src/commands/group.ts +546 -0
  74. package/src/commands/hooks.test.ts +458 -0
  75. package/src/commands/hooks.ts +263 -0
  76. package/src/commands/init.test.ts +1011 -0
  77. package/src/commands/init.ts +967 -0
  78. package/src/commands/inspect.test.ts +1239 -0
  79. package/src/commands/inspect.ts +648 -0
  80. package/src/commands/log.test.ts +1913 -0
  81. package/src/commands/log.ts +958 -0
  82. package/src/commands/logs.test.ts +801 -0
  83. package/src/commands/logs.ts +483 -0
  84. package/src/commands/mail.test.ts +1501 -0
  85. package/src/commands/mail.ts +848 -0
  86. package/src/commands/merge.test.ts +864 -0
  87. package/src/commands/merge.ts +381 -0
  88. package/src/commands/metrics.test.ts +458 -0
  89. package/src/commands/metrics.ts +129 -0
  90. package/src/commands/monitor.test.ts +191 -0
  91. package/src/commands/monitor.ts +409 -0
  92. package/src/commands/nudge.test.ts +579 -0
  93. package/src/commands/nudge.ts +646 -0
  94. package/src/commands/orchestrator.ts +42 -0
  95. package/src/commands/prime.test.ts +612 -0
  96. package/src/commands/prime.ts +359 -0
  97. package/src/commands/replay.test.ts +757 -0
  98. package/src/commands/replay.ts +231 -0
  99. package/src/commands/run.test.ts +469 -0
  100. package/src/commands/run.ts +353 -0
  101. package/src/commands/serve/agent-actions.test.ts +210 -0
  102. package/src/commands/serve/agent-actions.ts +192 -0
  103. package/src/commands/serve/build.test.ts +202 -0
  104. package/src/commands/serve/build.ts +206 -0
  105. package/src/commands/serve/coordinator-actions.test.ts +339 -0
  106. package/src/commands/serve/coordinator-actions.ts +410 -0
  107. package/src/commands/serve/dev.test.ts +168 -0
  108. package/src/commands/serve/dev.ts +117 -0
  109. package/src/commands/serve/mail-actions.test.ts +312 -0
  110. package/src/commands/serve/mail-actions.ts +167 -0
  111. package/src/commands/serve/rest.test.ts +1680 -0
  112. package/src/commands/serve/rest.ts +1130 -0
  113. package/src/commands/serve/static.ts +51 -0
  114. package/src/commands/serve/ws.test.ts +361 -0
  115. package/src/commands/serve/ws.ts +332 -0
  116. package/src/commands/serve.test.ts +459 -0
  117. package/src/commands/serve.ts +654 -0
  118. package/src/commands/sling.test.ts +1583 -0
  119. package/src/commands/sling.ts +1351 -0
  120. package/src/commands/spec.test.ts +179 -0
  121. package/src/commands/spec.ts +105 -0
  122. package/src/commands/status.test.ts +614 -0
  123. package/src/commands/status.ts +403 -0
  124. package/src/commands/stop.test.ts +964 -0
  125. package/src/commands/stop.ts +319 -0
  126. package/src/commands/supervisor.test.ts +185 -0
  127. package/src/commands/supervisor.ts +537 -0
  128. package/src/commands/trace.test.ts +762 -0
  129. package/src/commands/trace.ts +205 -0
  130. package/src/commands/update.test.ts +466 -0
  131. package/src/commands/update.ts +263 -0
  132. package/src/commands/upgrade.test.ts +48 -0
  133. package/src/commands/upgrade.ts +240 -0
  134. package/src/commands/watch.test.ts +257 -0
  135. package/src/commands/watch.ts +308 -0
  136. package/src/commands/worktree.test.ts +1297 -0
  137. package/src/commands/worktree.ts +451 -0
  138. package/src/config.test.ts +1535 -0
  139. package/src/config.ts +1064 -0
  140. package/src/doctor/agents.test.ts +523 -0
  141. package/src/doctor/agents.ts +399 -0
  142. package/src/doctor/config-check.test.ts +191 -0
  143. package/src/doctor/config-check.ts +183 -0
  144. package/src/doctor/consistency.test.ts +807 -0
  145. package/src/doctor/consistency.ts +347 -0
  146. package/src/doctor/databases.test.ts +350 -0
  147. package/src/doctor/databases.ts +243 -0
  148. package/src/doctor/dependencies.test.ts +296 -0
  149. package/src/doctor/dependencies.ts +272 -0
  150. package/src/doctor/ecosystem.test.ts +308 -0
  151. package/src/doctor/ecosystem.ts +156 -0
  152. package/src/doctor/logs.test.ts +253 -0
  153. package/src/doctor/logs.ts +295 -0
  154. package/src/doctor/merge-queue.test.ts +315 -0
  155. package/src/doctor/merge-queue.ts +167 -0
  156. package/src/doctor/providers.test.ts +409 -0
  157. package/src/doctor/providers.ts +250 -0
  158. package/src/doctor/serve.test.ts +95 -0
  159. package/src/doctor/serve.ts +86 -0
  160. package/src/doctor/structure.test.ts +423 -0
  161. package/src/doctor/structure.ts +285 -0
  162. package/src/doctor/types.ts +43 -0
  163. package/src/doctor/version.test.ts +241 -0
  164. package/src/doctor/version.ts +132 -0
  165. package/src/doctor/watchdog.test.ts +167 -0
  166. package/src/doctor/watchdog.ts +214 -0
  167. package/src/e2e/init-sling-lifecycle.test.ts +283 -0
  168. package/src/errors.test.ts +350 -0
  169. package/src/errors.ts +217 -0
  170. package/src/events/store.test.ts +660 -0
  171. package/src/events/store.ts +369 -0
  172. package/src/events/tailer.test.ts +719 -0
  173. package/src/events/tailer.ts +332 -0
  174. package/src/events/tool-filter.test.ts +330 -0
  175. package/src/events/tool-filter.ts +126 -0
  176. package/src/index.ts +533 -0
  177. package/src/insights/analyzer.test.ts +466 -0
  178. package/src/insights/analyzer.ts +203 -0
  179. package/src/insights/quality-gates.test.ts +141 -0
  180. package/src/insights/quality-gates.ts +156 -0
  181. package/src/json.test.ts +72 -0
  182. package/src/json.ts +53 -0
  183. package/src/loam/client.test.ts +752 -0
  184. package/src/loam/client.ts +664 -0
  185. package/src/logging/color.test.ts +252 -0
  186. package/src/logging/color.ts +105 -0
  187. package/src/logging/format.test.ts +110 -0
  188. package/src/logging/format.ts +255 -0
  189. package/src/logging/logger.test.ts +814 -0
  190. package/src/logging/logger.ts +266 -0
  191. package/src/logging/reporter.test.ts +259 -0
  192. package/src/logging/reporter.ts +110 -0
  193. package/src/logging/sanitizer.test.ts +190 -0
  194. package/src/logging/sanitizer.ts +57 -0
  195. package/src/logging/theme.ts +140 -0
  196. package/src/mail/broadcast.test.ts +204 -0
  197. package/src/mail/broadcast.ts +92 -0
  198. package/src/mail/client.test.ts +774 -0
  199. package/src/mail/client.ts +236 -0
  200. package/src/mail/store.test.ts +898 -0
  201. package/src/mail/store.ts +425 -0
  202. package/src/merge/lock.test.ts +149 -0
  203. package/src/merge/lock.ts +140 -0
  204. package/src/merge/predict.test.ts +387 -0
  205. package/src/merge/predict.ts +249 -0
  206. package/src/merge/queue.test.ts +426 -0
  207. package/src/merge/queue.ts +246 -0
  208. package/src/merge/resolver.test.ts +1993 -0
  209. package/src/merge/resolver.ts +926 -0
  210. package/src/metrics/pricing.test.ts +258 -0
  211. package/src/metrics/pricing.ts +135 -0
  212. package/src/metrics/store.test.ts +978 -0
  213. package/src/metrics/store.ts +501 -0
  214. package/src/metrics/summary.test.ts +398 -0
  215. package/src/metrics/summary.ts +178 -0
  216. package/src/metrics/transcript.test.ts +483 -0
  217. package/src/metrics/transcript.ts +114 -0
  218. package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
  219. package/src/runtimes/aider.test.ts +124 -0
  220. package/src/runtimes/aider.ts +147 -0
  221. package/src/runtimes/amp.test.ts +164 -0
  222. package/src/runtimes/amp.ts +154 -0
  223. package/src/runtimes/claude.test.ts +1474 -0
  224. package/src/runtimes/claude.ts +579 -0
  225. package/src/runtimes/codex.test.ts +805 -0
  226. package/src/runtimes/codex.ts +273 -0
  227. package/src/runtimes/connections.test.ts +214 -0
  228. package/src/runtimes/connections.ts +103 -0
  229. package/src/runtimes/copilot.test.ts +707 -0
  230. package/src/runtimes/copilot.ts +316 -0
  231. package/src/runtimes/cursor.test.ts +497 -0
  232. package/src/runtimes/cursor.ts +205 -0
  233. package/src/runtimes/gemini.test.ts +537 -0
  234. package/src/runtimes/gemini.ts +243 -0
  235. package/src/runtimes/goose.test.ts +133 -0
  236. package/src/runtimes/goose.ts +157 -0
  237. package/src/runtimes/headless-connection.test.ts +264 -0
  238. package/src/runtimes/headless-connection.ts +158 -0
  239. package/src/runtimes/opencode.test.ts +325 -0
  240. package/src/runtimes/opencode.ts +188 -0
  241. package/src/runtimes/pi-guards.test.ts +486 -0
  242. package/src/runtimes/pi-guards.ts +367 -0
  243. package/src/runtimes/pi.test.ts +789 -0
  244. package/src/runtimes/pi.ts +305 -0
  245. package/src/runtimes/registry.test.ts +196 -0
  246. package/src/runtimes/registry.ts +99 -0
  247. package/src/runtimes/sapling.test.ts +1267 -0
  248. package/src/runtimes/sapling.ts +710 -0
  249. package/src/runtimes/types.ts +266 -0
  250. package/src/schema-consistency.test.ts +246 -0
  251. package/src/sessions/compat.test.ts +281 -0
  252. package/src/sessions/compat.ts +105 -0
  253. package/src/sessions/store.test.ts +1748 -0
  254. package/src/sessions/store.ts +858 -0
  255. package/src/test-helpers.test.ts +124 -0
  256. package/src/test-helpers.ts +145 -0
  257. package/src/test-setup.test.ts +31 -0
  258. package/src/test-setup.ts +28 -0
  259. package/src/tools/loam/api.ts +368 -0
  260. package/src/tools/loam/cli.ts +278 -0
  261. package/src/tools/loam/commands/add.ts +52 -0
  262. package/src/tools/loam/commands/archive.ts +214 -0
  263. package/src/tools/loam/commands/audit.ts +276 -0
  264. package/src/tools/loam/commands/compact.ts +1062 -0
  265. package/src/tools/loam/commands/completions.ts +79 -0
  266. package/src/tools/loam/commands/config.ts +381 -0
  267. package/src/tools/loam/commands/delete-domain.ts +121 -0
  268. package/src/tools/loam/commands/delete.ts +316 -0
  269. package/src/tools/loam/commands/diff.ts +200 -0
  270. package/src/tools/loam/commands/doctor.ts +1113 -0
  271. package/src/tools/loam/commands/edit.ts +226 -0
  272. package/src/tools/loam/commands/init.ts +31 -0
  273. package/src/tools/loam/commands/learn.ts +179 -0
  274. package/src/tools/loam/commands/move.ts +323 -0
  275. package/src/tools/loam/commands/onboard.ts +374 -0
  276. package/src/tools/loam/commands/outcome.ts +185 -0
  277. package/src/tools/loam/commands/prime.ts +688 -0
  278. package/src/tools/loam/commands/prune.ts +614 -0
  279. package/src/tools/loam/commands/query.ts +218 -0
  280. package/src/tools/loam/commands/rank.ts +180 -0
  281. package/src/tools/loam/commands/ready.ts +189 -0
  282. package/src/tools/loam/commands/record.ts +1210 -0
  283. package/src/tools/loam/commands/restore.ts +166 -0
  284. package/src/tools/loam/commands/search.ts +327 -0
  285. package/src/tools/loam/commands/setup.ts +887 -0
  286. package/src/tools/loam/commands/status.ts +103 -0
  287. package/src/tools/loam/commands/sync.ts +298 -0
  288. package/src/tools/loam/commands/update.ts +19 -0
  289. package/src/tools/loam/commands/upgrade.ts +93 -0
  290. package/src/tools/loam/commands/validate.ts +190 -0
  291. package/src/tools/loam/index.ts +62 -0
  292. package/src/tools/loam/log.ts +127 -0
  293. package/src/tools/loam/registry/builtins.ts +409 -0
  294. package/src/tools/loam/registry/custom.ts +431 -0
  295. package/src/tools/loam/registry/init.ts +55 -0
  296. package/src/tools/loam/registry/template.ts +40 -0
  297. package/src/tools/loam/registry/type-registry.ts +113 -0
  298. package/src/tools/loam/schemas/config-schema.ts +489 -0
  299. package/src/tools/loam/schemas/config.ts +245 -0
  300. package/src/tools/loam/schemas/index.ts +18 -0
  301. package/src/tools/loam/schemas/record-schema.ts +191 -0
  302. package/src/tools/loam/schemas/record.ts +115 -0
  303. package/src/tools/loam/utils/active-work.ts +205 -0
  304. package/src/tools/loam/utils/anchor-validity.ts +80 -0
  305. package/src/tools/loam/utils/archive.ts +146 -0
  306. package/src/tools/loam/utils/audit.ts +667 -0
  307. package/src/tools/loam/utils/bm25.ts +238 -0
  308. package/src/tools/loam/utils/budget.ts +142 -0
  309. package/src/tools/loam/utils/config.ts +344 -0
  310. package/src/tools/loam/utils/dir-anchors.ts +62 -0
  311. package/src/tools/loam/utils/domain-rules.ts +114 -0
  312. package/src/tools/loam/utils/expertise.ts +393 -0
  313. package/src/tools/loam/utils/format-helpers.ts +96 -0
  314. package/src/tools/loam/utils/format.ts +1234 -0
  315. package/src/tools/loam/utils/git-context.ts +50 -0
  316. package/src/tools/loam/utils/git.ts +183 -0
  317. package/src/tools/loam/utils/hooks.ts +299 -0
  318. package/src/tools/loam/utils/index.ts +52 -0
  319. package/src/tools/loam/utils/json-output.ts +13 -0
  320. package/src/tools/loam/utils/lock.ts +76 -0
  321. package/src/tools/loam/utils/markers.ts +48 -0
  322. package/src/tools/loam/utils/numeric-flags.ts +20 -0
  323. package/src/tools/loam/utils/palette.ts +44 -0
  324. package/src/tools/loam/utils/prime-ranking.ts +135 -0
  325. package/src/tools/loam/utils/recipe-discovery.ts +195 -0
  326. package/src/tools/loam/utils/runtime-flags.ts +28 -0
  327. package/src/tools/loam/utils/scoring.ts +94 -0
  328. package/src/tools/loam/utils/version.ts +116 -0
  329. package/src/tools/sprout/commands/block.ts +64 -0
  330. package/src/tools/sprout/commands/blocked.ts +86 -0
  331. package/src/tools/sprout/commands/close.ts +129 -0
  332. package/src/tools/sprout/commands/completions.ts +198 -0
  333. package/src/tools/sprout/commands/config.ts +238 -0
  334. package/src/tools/sprout/commands/create.ts +164 -0
  335. package/src/tools/sprout/commands/dep.ts +148 -0
  336. package/src/tools/sprout/commands/doctor.ts +979 -0
  337. package/src/tools/sprout/commands/init.ts +83 -0
  338. package/src/tools/sprout/commands/label.ts +178 -0
  339. package/src/tools/sprout/commands/list.ts +210 -0
  340. package/src/tools/sprout/commands/migrate.ts +133 -0
  341. package/src/tools/sprout/commands/onboard.ts +207 -0
  342. package/src/tools/sprout/commands/plan-show.ts +278 -0
  343. package/src/tools/sprout/commands/plan.ts +2526 -0
  344. package/src/tools/sprout/commands/prime.ts +399 -0
  345. package/src/tools/sprout/commands/ready.ts +245 -0
  346. package/src/tools/sprout/commands/search.ts +221 -0
  347. package/src/tools/sprout/commands/show.ts +277 -0
  348. package/src/tools/sprout/commands/stats.ts +146 -0
  349. package/src/tools/sprout/commands/sync.ts +134 -0
  350. package/src/tools/sprout/commands/tpl.ts +364 -0
  351. package/src/tools/sprout/commands/unblock.ts +115 -0
  352. package/src/tools/sprout/commands/update.ts +257 -0
  353. package/src/tools/sprout/commands/upgrade.ts +91 -0
  354. package/src/tools/sprout/config-schema.ts +152 -0
  355. package/src/tools/sprout/config.ts +355 -0
  356. package/src/tools/sprout/filter.ts +107 -0
  357. package/src/tools/sprout/format.ts +43 -0
  358. package/src/tools/sprout/id.ts +22 -0
  359. package/src/tools/sprout/index.ts +204 -0
  360. package/src/tools/sprout/log.ts +76 -0
  361. package/src/tools/sprout/markers.ts +22 -0
  362. package/src/tools/sprout/output.ts +121 -0
  363. package/src/tools/sprout/plan-backref.ts +93 -0
  364. package/src/tools/sprout/plan-context.ts +81 -0
  365. package/src/tools/sprout/plan-domain.ts +139 -0
  366. package/src/tools/sprout/plan-lifecycle.ts +65 -0
  367. package/src/tools/sprout/plan-loam.ts +207 -0
  368. package/src/tools/sprout/plan-schema.ts +209 -0
  369. package/src/tools/sprout/sort.ts +31 -0
  370. package/src/tools/sprout/store.ts +172 -0
  371. package/src/tools/sprout/types.ts +118 -0
  372. package/src/tools/sprout/validation.ts +119 -0
  373. package/src/tools/sprout/version.ts +1 -0
  374. package/src/tools/sprout/yaml.ts +387 -0
  375. package/src/tools/trellis/commands/archive.ts +87 -0
  376. package/src/tools/trellis/commands/completions.ts +610 -0
  377. package/src/tools/trellis/commands/config.ts +382 -0
  378. package/src/tools/trellis/commands/create.ts +252 -0
  379. package/src/tools/trellis/commands/diff.ts +150 -0
  380. package/src/tools/trellis/commands/doctor.ts +771 -0
  381. package/src/tools/trellis/commands/emit.ts +365 -0
  382. package/src/tools/trellis/commands/history.ts +83 -0
  383. package/src/tools/trellis/commands/import.ts +198 -0
  384. package/src/tools/trellis/commands/init.ts +81 -0
  385. package/src/tools/trellis/commands/list.ts +103 -0
  386. package/src/tools/trellis/commands/onboard.ts +156 -0
  387. package/src/tools/trellis/commands/pin.ts +172 -0
  388. package/src/tools/trellis/commands/prime.ts +193 -0
  389. package/src/tools/trellis/commands/render.ts +122 -0
  390. package/src/tools/trellis/commands/schema.ts +353 -0
  391. package/src/tools/trellis/commands/show.ts +115 -0
  392. package/src/tools/trellis/commands/stats.ts +65 -0
  393. package/src/tools/trellis/commands/sync.ts +112 -0
  394. package/src/tools/trellis/commands/tree.ts +123 -0
  395. package/src/tools/trellis/commands/update.ts +330 -0
  396. package/src/tools/trellis/commands/upgrade.ts +95 -0
  397. package/src/tools/trellis/commands/validate.ts +166 -0
  398. package/src/tools/trellis/config-schema.ts +81 -0
  399. package/src/tools/trellis/config.ts +108 -0
  400. package/src/tools/trellis/frontmatter.ts +348 -0
  401. package/src/tools/trellis/id.ts +24 -0
  402. package/src/tools/trellis/index.ts +209 -0
  403. package/src/tools/trellis/markers.ts +28 -0
  404. package/src/tools/trellis/output.ts +84 -0
  405. package/src/tools/trellis/render.ts +212 -0
  406. package/src/tools/trellis/store.ts +144 -0
  407. package/src/tools/trellis/types.ts +82 -0
  408. package/src/tools/trellis/validate.ts +199 -0
  409. package/src/tools/trellis/yaml.ts +309 -0
  410. package/src/tracker/beads.test.ts +454 -0
  411. package/src/tracker/beads.ts +56 -0
  412. package/src/tracker/factory.test.ts +90 -0
  413. package/src/tracker/factory.ts +65 -0
  414. package/src/tracker/sprout.test.ts +461 -0
  415. package/src/tracker/sprout.ts +182 -0
  416. package/src/tracker/types.ts +52 -0
  417. package/src/trellis/client.test.ts +107 -0
  418. package/src/trellis/client.ts +179 -0
  419. package/src/types.ts +970 -0
  420. package/src/utils/bin.test.ts +10 -0
  421. package/src/utils/bin.ts +37 -0
  422. package/src/utils/browser.test.ts +49 -0
  423. package/src/utils/browser.ts +48 -0
  424. package/src/utils/fs.test.ts +119 -0
  425. package/src/utils/fs.ts +62 -0
  426. package/src/utils/pid.test.ts +152 -0
  427. package/src/utils/pid.ts +130 -0
  428. package/src/utils/process-scan.test.ts +53 -0
  429. package/src/utils/process-scan.ts +76 -0
  430. package/src/utils/time.test.ts +43 -0
  431. package/src/utils/time.ts +37 -0
  432. package/src/utils/version.test.ts +33 -0
  433. package/src/utils/version.ts +70 -0
  434. package/src/version.ts +5 -0
  435. package/src/watchdog/daemon.test.ts +3721 -0
  436. package/src/watchdog/daemon.ts +1257 -0
  437. package/src/watchdog/health.test.ts +830 -0
  438. package/src/watchdog/health.ts +434 -0
  439. package/src/watchdog/triage.test.ts +205 -0
  440. package/src/watchdog/triage.ts +205 -0
  441. package/src/worktree/manager.test.ts +720 -0
  442. package/src/worktree/manager.ts +405 -0
  443. package/src/worktree/process.test.ts +172 -0
  444. package/src/worktree/process.ts +131 -0
  445. package/src/worktree/tmux.test.ts +1616 -0
  446. package/src/worktree/tmux.ts +721 -0
  447. package/templates/CLAUDE.md.tmpl +100 -0
  448. package/templates/copilot-hooks.json.tmpl +13 -0
  449. package/templates/hooks.json.tmpl +109 -0
  450. package/templates/overlay.md.tmpl +88 -0
  451. package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
  452. package/ui/dist/chunk-8s31f05k.css +1 -0
  453. package/ui/dist/chunk-vm5rz679.js +300 -0
  454. package/ui/dist/favicon-nzb39vza.svg +4 -0
  455. package/ui/dist/index.html +17 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Action wrappers for the fleet-control REST endpoints (spawn / stop / merge).
3
+ *
4
+ * Thin functions consumed by `src/commands/serve/rest.ts` to drive `ap sling`,
5
+ * `ap stop`, and `ap merge` from the web UI. Each wraps the existing command
6
+ * core (`slingCommand` / `stopCommand` / `mergeCommand` / `predictConflicts`)
7
+ * rather than re-implementing the logic, and exposes DI overrides so tests can
8
+ * inject fakes without spawning real agents or touching git.
9
+ *
10
+ * Mirrors the pattern in ./coordinator-actions.ts: short-lived SQLite handles,
11
+ * typed errors that the REST layer maps to HTTP status, no long-lived state.
12
+ */
13
+
14
+ import { join } from "node:path";
15
+ import { loadConfig } from "../../config.ts";
16
+ import { ValidationError } from "../../errors.ts";
17
+ import { predictConflicts } from "../../merge/predict.ts";
18
+ import type { SessionStore } from "../../sessions/store.ts";
19
+ import { createSessionStore } from "../../sessions/store.ts";
20
+ import type { ConflictPrediction, MergeEntry } from "../../types.ts";
21
+ import { type MergeOptions, mergeCommand } from "../merge.ts";
22
+ import { type SlingOptions, slingCommand } from "../sling.ts";
23
+ import { type StopOptions, stopCommand } from "../stop.ts";
24
+
25
+ export interface AgentActionDeps {
26
+ projectRoot: string;
27
+ /** Inject a SessionStore (closing is the caller's responsibility). */
28
+ _sessionStore?: SessionStore;
29
+ _slingCommand?: (taskId: string, opts: SlingOptions) => Promise<void>;
30
+ _stopCommand?: (agentName: string, opts: StopOptions) => Promise<void>;
31
+ _mergeCommand?: (opts: MergeOptions) => Promise<void>;
32
+ _predictConflicts?: typeof predictConflicts;
33
+ _loadConfig?: typeof loadConfig;
34
+ }
35
+
36
+ // ─── Sling (spawn) ──────────────────────────────────────────────────────────
37
+
38
+ export interface SlingInput {
39
+ taskId: string;
40
+ capability?: string;
41
+ name?: string;
42
+ files?: string;
43
+ spec?: string;
44
+ runtime?: string;
45
+ skipTaskCheck?: boolean;
46
+ }
47
+
48
+ export interface SlingResult {
49
+ agentName: string;
50
+ state: string | null;
51
+ spawned: boolean;
52
+ }
53
+
54
+ /**
55
+ * Spawn a worker agent. Forces headless mode (no tmux) — the web UI is the
56
+ * operator surface. After spawning, reads the freshly-created session row so
57
+ * the caller can correlate. The agent name mirrors slingCommand's derivation
58
+ * (`${capability}-${taskId}` when no explicit name is given).
59
+ */
60
+ export async function slingAgent(deps: AgentActionDeps, input: SlingInput): Promise<SlingResult> {
61
+ const taskId = input.taskId?.trim() ?? "";
62
+ if (taskId.length === 0) {
63
+ throw new ValidationError("Missing or invalid 'taskId' (string)", { field: "taskId" });
64
+ }
65
+ const capability = input.capability ?? "builder";
66
+ const explicitName = input.name?.trim() ?? "";
67
+
68
+ const sling = deps._slingCommand ?? slingCommand;
69
+ const opts: SlingOptions = {
70
+ json: true,
71
+ headless: true,
72
+ capability,
73
+ ...(explicitName.length > 0 ? { name: explicitName } : {}),
74
+ ...(input.files ? { files: input.files } : {}),
75
+ ...(input.spec ? { spec: input.spec } : {}),
76
+ ...(input.runtime ? { runtime: input.runtime } : {}),
77
+ ...(input.skipTaskCheck ? { skipTaskCheck: true } : {}),
78
+ };
79
+ await sling(taskId, opts);
80
+
81
+ const expectedName = explicitName.length > 0 ? explicitName : `${capability}-${taskId}`;
82
+ const store =
83
+ deps._sessionStore ?? createSessionStore(join(deps.projectRoot, ".agentplate", "sessions.db"));
84
+ try {
85
+ const session = store.getByName(expectedName);
86
+ return {
87
+ agentName: expectedName,
88
+ state: session?.state ?? null,
89
+ spawned: session !== null,
90
+ };
91
+ } finally {
92
+ if (deps._sessionStore === undefined) store.close();
93
+ }
94
+ }
95
+
96
+ // ─── Stop ───────────────────────────────────────────────────────────────────
97
+
98
+ export interface StopAgentResult {
99
+ stopped: boolean;
100
+ agentName: string;
101
+ }
102
+
103
+ /**
104
+ * Terminate a running agent. Throws AgentError (→ HTTP 500/409) when the agent
105
+ * is unknown or already completed without `cleanWorktree`.
106
+ */
107
+ export async function stopAgent(
108
+ deps: AgentActionDeps,
109
+ name: string,
110
+ opts?: { cleanWorktree?: boolean },
111
+ ): Promise<StopAgentResult> {
112
+ const agentName = name?.trim() ?? "";
113
+ if (agentName.length === 0) {
114
+ throw new ValidationError("Missing agent name", { field: "name" });
115
+ }
116
+ const stop = deps._stopCommand ?? stopCommand;
117
+ await stop(agentName, { json: true, cleanWorktree: opts?.cleanWorktree ?? false });
118
+ return { stopped: true, agentName };
119
+ }
120
+
121
+ // ─── Merge ──────────────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Resolve the merge target the same way `ap merge` does:
125
+ * `into` flag > session-start branch (.agentplate/session-branch.txt) >
126
+ * config canonicalBranch.
127
+ */
128
+ async function resolveTargetBranch(deps: AgentActionDeps, into?: string): Promise<string> {
129
+ if (into !== undefined && into.trim().length > 0) return into.trim();
130
+ const sessionBranchPath = join(deps.projectRoot, ".agentplate", "session-branch.txt");
131
+ const file = Bun.file(sessionBranchPath);
132
+ if (await file.exists()) {
133
+ const content = (await file.text()).trim();
134
+ if (content.length > 0) return content;
135
+ }
136
+ const loadCfg = deps._loadConfig ?? loadConfig;
137
+ const config = await loadCfg(deps.projectRoot);
138
+ return config.project.canonicalBranch;
139
+ }
140
+
141
+ export type MergePreviewResult = ConflictPrediction & { branch: string; target: string };
142
+
143
+ /**
144
+ * Predict how `ap merge` would resolve `branch` into the target branch.
145
+ * Side-effect-free (runs `git merge-tree --write-tree`).
146
+ */
147
+ export async function mergePreview(
148
+ deps: AgentActionDeps,
149
+ input: { branch: string; into?: string },
150
+ ): Promise<MergePreviewResult> {
151
+ const branch = input.branch?.trim() ?? "";
152
+ if (branch.length === 0) {
153
+ throw new ValidationError("Missing or invalid 'branch' (string)", { field: "branch" });
154
+ }
155
+ const target = await resolveTargetBranch(deps, input.into);
156
+ const predict = deps._predictConflicts ?? predictConflicts;
157
+ const entry: MergeEntry = {
158
+ branchName: branch,
159
+ taskId: "unknown",
160
+ agentName: "unknown",
161
+ filesModified: [],
162
+ enqueuedAt: "",
163
+ status: "pending",
164
+ resolvedTier: null,
165
+ };
166
+ const prediction = await predict(entry, target, deps.projectRoot);
167
+ return { ...prediction, branch, target };
168
+ }
169
+
170
+ /**
171
+ * Trigger an actual merge. Returns an acknowledgement — detailed per-branch
172
+ * results surface through events/mail and the UI refreshes. Throws (→ HTTP
173
+ * error) when the merge fails outright.
174
+ */
175
+ export async function mergeRun(
176
+ deps: AgentActionDeps,
177
+ input: { branch?: string; all?: boolean; into?: string },
178
+ ): Promise<{ triggered: true }> {
179
+ const branch = input.branch?.trim();
180
+ if (input.all !== true && (branch === undefined || branch.length === 0)) {
181
+ throw new ValidationError("Provide 'branch' or set 'all: true'", { field: "branch" });
182
+ }
183
+ const merge = deps._mergeCommand ?? mergeCommand;
184
+ const opts: MergeOptions = {
185
+ json: true,
186
+ ...(input.all === true ? { all: true } : {}),
187
+ ...(branch ? { branch } : {}),
188
+ ...(input.into ? { into: input.into } : {}),
189
+ };
190
+ await merge(opts);
191
+ return { triggered: true };
192
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Tests for ensureUiBuild auto-build helper.
3
+ *
4
+ * Real filesystem (temp dirs + utimesSync) drives the freshness comparison;
5
+ * the runner is always stubbed so we never invoke a real bun install/build.
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+ import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { ensureUiBuild, type RunnerResult } from "./build.ts";
13
+
14
+ interface RunnerCall {
15
+ cmd: string[];
16
+ cwd: string;
17
+ }
18
+
19
+ function makeRunner(results: RunnerResult[]): {
20
+ runner: (cmd: string[], cwd: string) => Promise<RunnerResult>;
21
+ calls: RunnerCall[];
22
+ } {
23
+ const calls: RunnerCall[] = [];
24
+ let i = 0;
25
+ return {
26
+ calls,
27
+ runner: async (cmd, cwd) => {
28
+ calls.push({ cmd, cwd });
29
+ const r = results[i] ?? { exitCode: 0, stderr: "" };
30
+ i += 1;
31
+ return r;
32
+ },
33
+ };
34
+ }
35
+
36
+ function setupUiDir(root: string): { uiDir: string } {
37
+ const uiDir = join(root, "ui");
38
+ mkdirSync(join(uiDir, "src"), { recursive: true });
39
+ writeFileSync(join(uiDir, "src", "main.ts"), 'console.log("hi")');
40
+ writeFileSync(join(uiDir, "index.html"), "<html></html>");
41
+ writeFileSync(join(uiDir, "package.json"), '{"name":"ui"}');
42
+ return { uiDir };
43
+ }
44
+
45
+ describe("ensureUiBuild", () => {
46
+ let tempDir: string;
47
+
48
+ beforeEach(() => {
49
+ tempDir = mkdtempSync(join(tmpdir(), "agentplate-ui-build-"));
50
+ });
51
+
52
+ afterEach(() => {
53
+ rmSync(tempDir, { recursive: true, force: true });
54
+ });
55
+
56
+ test("triggers build when dist/index.html is missing", async () => {
57
+ const { uiDir } = setupUiDir(tempDir);
58
+ // Pretend node_modules already exists so we skip install.
59
+ mkdirSync(join(uiDir, "node_modules"));
60
+
61
+ const { runner, calls } = makeRunner([{ exitCode: 0, stderr: "" }]);
62
+ await ensureUiBuild({ uiDir, _runner: runner, log: () => {} });
63
+
64
+ expect(calls.length).toBe(1);
65
+ expect(calls[0]?.cmd).toEqual(["bun", "run", "build"]);
66
+ expect(calls[0]?.cwd).toBe(uiDir);
67
+ });
68
+
69
+ test("triggers build when source mtime is newer than dist mtime", async () => {
70
+ const { uiDir } = setupUiDir(tempDir);
71
+ mkdirSync(join(uiDir, "node_modules"));
72
+ mkdirSync(join(uiDir, "dist"));
73
+ writeFileSync(join(uiDir, "dist", "index.html"), "<html>old</html>");
74
+
75
+ // Make dist older, src newer (definite mtime separation).
76
+ const past = new Date(Date.now() - 60_000);
77
+ const future = new Date(Date.now() + 60_000);
78
+ utimesSync(join(uiDir, "dist", "index.html"), past, past);
79
+ utimesSync(join(uiDir, "src", "main.ts"), future, future);
80
+
81
+ const { runner, calls } = makeRunner([{ exitCode: 0, stderr: "" }]);
82
+ await ensureUiBuild({ uiDir, _runner: runner, log: () => {} });
83
+
84
+ expect(calls.length).toBe(1);
85
+ expect(calls[0]?.cmd).toEqual(["bun", "run", "build"]);
86
+ });
87
+
88
+ test("skips build when dist is newer than every source file", async () => {
89
+ const { uiDir } = setupUiDir(tempDir);
90
+ mkdirSync(join(uiDir, "node_modules"));
91
+ mkdirSync(join(uiDir, "dist"));
92
+ writeFileSync(join(uiDir, "dist", "index.html"), "<html>fresh</html>");
93
+
94
+ // All sources older than dist.
95
+ const past = new Date(Date.now() - 60_000);
96
+ utimesSync(join(uiDir, "src", "main.ts"), past, past);
97
+ utimesSync(join(uiDir, "index.html"), past, past);
98
+ utimesSync(join(uiDir, "package.json"), past, past);
99
+ // Dist is "now" — already the newest.
100
+
101
+ const { runner, calls } = makeRunner([]);
102
+ await ensureUiBuild({ uiDir, _runner: runner, log: () => {} });
103
+
104
+ expect(calls.length).toBe(0);
105
+ });
106
+
107
+ test("runs install only when node_modules is missing", async () => {
108
+ const { uiDir } = setupUiDir(tempDir);
109
+ // node_modules deliberately missing.
110
+
111
+ const { runner, calls } = makeRunner([
112
+ { exitCode: 0, stderr: "" }, // install
113
+ { exitCode: 0, stderr: "" }, // build
114
+ ]);
115
+ await ensureUiBuild({ uiDir, _runner: runner, log: () => {} });
116
+
117
+ expect(calls.length).toBe(2);
118
+ expect(calls[0]?.cmd).toEqual(["bun", "install"]);
119
+ expect(calls[0]?.cwd).toBe(uiDir);
120
+ expect(calls[1]?.cmd).toEqual(["bun", "run", "build"]);
121
+ });
122
+
123
+ test("does NOT run install when node_modules exists", async () => {
124
+ const { uiDir } = setupUiDir(tempDir);
125
+ mkdirSync(join(uiDir, "node_modules"));
126
+
127
+ const { runner, calls } = makeRunner([{ exitCode: 0, stderr: "" }]);
128
+ await ensureUiBuild({ uiDir, _runner: runner, log: () => {} });
129
+
130
+ expect(calls.length).toBe(1);
131
+ expect(calls[0]?.cmd).toEqual(["bun", "run", "build"]);
132
+ });
133
+
134
+ test("throws on non-zero install exit; error includes stderr", async () => {
135
+ const { uiDir } = setupUiDir(tempDir);
136
+
137
+ const { runner } = makeRunner([{ exitCode: 1, stderr: "lockfile out of date" }]);
138
+
139
+ await expect(ensureUiBuild({ uiDir, _runner: runner, log: () => {} })).rejects.toThrow(
140
+ /lockfile out of date/,
141
+ );
142
+ });
143
+
144
+ test("throws on non-zero build exit; error includes stderr", async () => {
145
+ const { uiDir } = setupUiDir(tempDir);
146
+ mkdirSync(join(uiDir, "node_modules"));
147
+
148
+ const { runner } = makeRunner([{ exitCode: 2, stderr: "type error in main.ts" }]);
149
+
150
+ await expect(ensureUiBuild({ uiDir, _runner: runner, log: () => {} })).rejects.toThrow(
151
+ /type error in main\.ts/,
152
+ );
153
+ });
154
+
155
+ test("logs progress messages on build path", async () => {
156
+ const { uiDir } = setupUiDir(tempDir);
157
+ // node_modules missing → install + build → 3 log lines (install, build, built).
158
+ const messages: string[] = [];
159
+ const { runner } = makeRunner([
160
+ { exitCode: 0, stderr: "" },
161
+ { exitCode: 0, stderr: "" },
162
+ ]);
163
+ await ensureUiBuild({ uiDir, _runner: runner, log: (m) => messages.push(m) });
164
+
165
+ expect(messages).toEqual(["Installing UI dependencies…", "Building UI…", "UI built"]);
166
+ });
167
+
168
+ test("no-ops when ui/src is absent (production-install case)", async () => {
169
+ // No setupUiDir() — tempDir has no ui/ at all, mirroring a fresh `ap init`
170
+ // in a project that doesn't carry a UI workspace (agentplate-916d).
171
+ const uiDir = join(tempDir, "ui");
172
+
173
+ const messages: string[] = [];
174
+ const { runner, calls } = makeRunner([]);
175
+ await ensureUiBuild({ uiDir, _runner: runner, log: (m) => messages.push(m) });
176
+
177
+ // Neither install nor build runs, and no progress messages are emitted.
178
+ expect(calls.length).toBe(0);
179
+ expect(messages.length).toBe(0);
180
+ });
181
+
182
+ test("walks ui/src/ recursively and triggers on nested-file mtime", async () => {
183
+ const { uiDir } = setupUiDir(tempDir);
184
+ mkdirSync(join(uiDir, "node_modules"));
185
+ mkdirSync(join(uiDir, "src", "components"), { recursive: true });
186
+ writeFileSync(join(uiDir, "src", "components", "App.tsx"), "export default null;");
187
+ mkdirSync(join(uiDir, "dist"));
188
+ writeFileSync(join(uiDir, "dist", "index.html"), "<html>old</html>");
189
+
190
+ // dist older than the nested file.
191
+ const past = new Date(Date.now() - 60_000);
192
+ const future = new Date(Date.now() + 60_000);
193
+ utimesSync(join(uiDir, "dist", "index.html"), past, past);
194
+ utimesSync(join(uiDir, "src", "main.ts"), past, past);
195
+ utimesSync(join(uiDir, "src", "components", "App.tsx"), future, future);
196
+
197
+ const { runner, calls } = makeRunner([{ exitCode: 0, stderr: "" }]);
198
+ await ensureUiBuild({ uiDir, _runner: runner, log: () => {} });
199
+
200
+ expect(calls.length).toBe(1);
201
+ });
202
+ });
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Auto-build helper for `ap serve` (production mode).
3
+ *
4
+ * Detects whether ui/dist/index.html is missing or older than any tracked
5
+ * source/config file in the ui/ workspace, runs `bun install` (only if
6
+ * node_modules is missing) followed by `bun run build` when so. Silent when
7
+ * the existing build is up to date.
8
+ *
9
+ * No-ops when the project has no `ui/src` directory — the production-install
10
+ * case where the user's project doesn't carry a UI workspace and is meant to
11
+ * serve the prebuilt assets shipped inside the @ag-eco/agentplate-cli package
12
+ * (see agentplate-916d).
13
+ *
14
+ * The runner and filesystem helpers are injectable so tests can drive the
15
+ * decision logic without invoking real subprocess builds.
16
+ */
17
+
18
+ import {
19
+ existsSync as defaultExistsSync,
20
+ readdirSync as defaultReaddirSync,
21
+ statSync as defaultStatSync,
22
+ } from "node:fs";
23
+ import { join, sep } from "node:path";
24
+
25
+ export interface RunnerResult {
26
+ exitCode: number;
27
+ stderr: string;
28
+ }
29
+
30
+ export interface EnsureUiBuildOptions {
31
+ uiDir: string;
32
+ log?: (msg: string) => void;
33
+ _statSync?: typeof defaultStatSync;
34
+ _existsSync?: typeof defaultExistsSync;
35
+ _readdirSync?: typeof defaultReaddirSync;
36
+ _runner?: (cmd: string[], cwd: string) => Promise<RunnerResult>;
37
+ }
38
+
39
+ /** Files at the workspace root that affect the build output. */
40
+ const ROOT_TRACKED_FILES = [
41
+ "index.html",
42
+ "build.ts",
43
+ "package.json",
44
+ "tsconfig.app.json",
45
+ "tsconfig.json",
46
+ "components.json",
47
+ ];
48
+
49
+ /**
50
+ * Recursively walk a directory, calling visit() on each regular file path.
51
+ * Skips entries that throw on stat (broken symlinks, races during npm
52
+ * install). Uses the injected readdir/stat for testability.
53
+ */
54
+ function walkDir(
55
+ dir: string,
56
+ visit: (path: string) => void,
57
+ exists: typeof defaultExistsSync,
58
+ readdir: typeof defaultReaddirSync,
59
+ stat: typeof defaultStatSync,
60
+ ): void {
61
+ if (!exists(dir)) return;
62
+ let entries: string[];
63
+ try {
64
+ entries = readdir(dir);
65
+ } catch {
66
+ return;
67
+ }
68
+ for (const name of entries) {
69
+ const full = dir + sep + name;
70
+ let st: ReturnType<typeof defaultStatSync>;
71
+ try {
72
+ st = stat(full);
73
+ } catch {
74
+ continue;
75
+ }
76
+ if (st.isDirectory()) {
77
+ walkDir(full, visit, exists, readdir, stat);
78
+ } else if (st.isFile()) {
79
+ visit(full);
80
+ }
81
+ }
82
+ }
83
+
84
+ async function defaultRunner(cmd: string[], cwd: string): Promise<RunnerResult> {
85
+ if (cmd.length === 0) {
86
+ throw new Error("ensureUiBuild runner: empty command");
87
+ }
88
+ const proc = Bun.spawn(cmd as string[], {
89
+ cwd,
90
+ stdout: "pipe",
91
+ stderr: "pipe",
92
+ });
93
+
94
+ const stderrChunks: string[] = [];
95
+ const pipe = async (
96
+ stream: ReadableStream<Uint8Array> | null,
97
+ sink: (line: string) => void,
98
+ capture?: string[],
99
+ ): Promise<void> => {
100
+ if (stream === null) return;
101
+ const decoder = new TextDecoder();
102
+ let buf = "";
103
+ const reader = stream.getReader();
104
+ try {
105
+ while (true) {
106
+ const { done, value } = await reader.read();
107
+ if (done) break;
108
+ const chunk = decoder.decode(value, { stream: true });
109
+ if (capture) capture.push(chunk);
110
+ buf += chunk;
111
+ let idx = buf.indexOf("\n");
112
+ while (idx !== -1) {
113
+ sink(buf.slice(0, idx));
114
+ buf = buf.slice(idx + 1);
115
+ idx = buf.indexOf("\n");
116
+ }
117
+ }
118
+ if (buf.length > 0) sink(buf);
119
+ } finally {
120
+ reader.releaseLock();
121
+ }
122
+ };
123
+
124
+ const writeOut = (line: string): void => {
125
+ process.stderr.write(`[ui-build] ${line}\n`);
126
+ };
127
+
128
+ await Promise.all([pipe(proc.stdout, writeOut), pipe(proc.stderr, writeOut, stderrChunks)]);
129
+
130
+ const exitCode = await proc.exited;
131
+ return { exitCode, stderr: stderrChunks.join("") };
132
+ }
133
+
134
+ /**
135
+ * Ensure that ui/dist/ has a build that is newer than every tracked source
136
+ * file. No-op when the build is current. Throws when a required subprocess
137
+ * (install / build) fails; the thrown Error includes the captured stderr.
138
+ */
139
+ export async function ensureUiBuild(opts: EnsureUiBuildOptions): Promise<void> {
140
+ const exists = opts._existsSync ?? defaultExistsSync;
141
+ const stat = opts._statSync ?? defaultStatSync;
142
+ const readdir = opts._readdirSync ?? defaultReaddirSync;
143
+ const runner = opts._runner ?? defaultRunner;
144
+ const log =
145
+ opts.log ??
146
+ ((msg: string): void => {
147
+ process.stderr.write(`[ui-build] ${msg}\n`);
148
+ });
149
+
150
+ // Production-install case: when the project has no ui/src, there are no
151
+ // sources to build from. ap serve falls back to the prebuilt assets shipped
152
+ // inside the npm package — see resolveUiDistPath() in serve.ts.
153
+ if (!exists(join(opts.uiDir, "src"))) return;
154
+
155
+ const distIndex = join(opts.uiDir, "dist", "index.html");
156
+ let needBuild = false;
157
+ if (!exists(distIndex)) {
158
+ needBuild = true;
159
+ } else {
160
+ let distMtime = 0;
161
+ try {
162
+ distMtime = stat(distIndex).mtimeMs;
163
+ } catch {
164
+ needBuild = true;
165
+ }
166
+
167
+ if (!needBuild) {
168
+ let newest = 0;
169
+ const visit = (path: string): void => {
170
+ try {
171
+ const m = stat(path).mtimeMs;
172
+ if (m > newest) newest = m;
173
+ } catch {
174
+ // File vanished between readdir and stat — ignore.
175
+ }
176
+ };
177
+
178
+ walkDir(join(opts.uiDir, "src"), visit, exists, readdir, stat);
179
+ for (const name of ROOT_TRACKED_FILES) {
180
+ const full = join(opts.uiDir, name);
181
+ if (exists(full)) visit(full);
182
+ }
183
+
184
+ if (newest > distMtime) needBuild = true;
185
+ }
186
+ }
187
+
188
+ if (!needBuild) return;
189
+
190
+ if (!exists(join(opts.uiDir, "node_modules"))) {
191
+ log("Installing UI dependencies…");
192
+ const install = await runner(["bun", "install"], opts.uiDir);
193
+ if (install.exitCode !== 0) {
194
+ throw new Error(
195
+ `UI dependency install failed (exit ${install.exitCode}): ${install.stderr.trim()}`,
196
+ );
197
+ }
198
+ }
199
+
200
+ log("Building UI…");
201
+ const build = await runner(["bun", "run", "build"], opts.uiDir);
202
+ if (build.exitCode !== 0) {
203
+ throw new Error(`UI build failed (exit ${build.exitCode}): ${build.stderr.trim()}`);
204
+ }
205
+ log("UI built");
206
+ }