agor-live 0.16.4 → 0.17.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 (435) hide show
  1. package/dist/cli/commands/admin/sync-unix.d.ts +1 -48
  2. package/dist/cli/commands/admin/sync-unix.js +702 -390
  3. package/dist/cli/commands/daemon/start.d.ts +1 -0
  4. package/dist/cli/commands/daemon/start.js +42 -4
  5. package/dist/cli/commands/version.d.ts +19 -0
  6. package/dist/cli/commands/version.js +63 -0
  7. package/dist/cli/lib/check-migrations.d.ts +36 -0
  8. package/dist/cli/lib/check-migrations.js +24 -0
  9. package/dist/cli/lib/check-migrations.test.d.ts +2 -0
  10. package/dist/cli/lib/check-migrations.test.js +17295 -0
  11. package/dist/cli/lib/daemon-manager.test.js +6 -6
  12. package/dist/core/{agentic-tool-Cs4nK-CC.d.cts → agentic-tool-BulhIUGb.d.cts} +4 -2
  13. package/dist/core/{agentic-tool-B6RT-ZX5.d.ts → agentic-tool-cYqsU5i5.d.ts} +4 -2
  14. package/dist/core/api/index.cjs +9 -0
  15. package/dist/core/api/index.d.cts +13 -13
  16. package/dist/core/api/index.d.ts +13 -13
  17. package/dist/core/api/index.js +9 -0
  18. package/dist/core/{artifact-DaHQPZVX.d.cts → artifact-B2AqjUMb.d.cts} +23 -3
  19. package/dist/core/{artifact-CIQzxjNP.d.ts → artifact-bMxYKV3b.d.ts} +23 -3
  20. package/dist/core/{board-DG--dAS_.d.ts → board-D3YJ0_ih.d.ts} +10 -3
  21. package/dist/core/{board-DogjFoWy.d.cts → board-D7SnPqp-.d.cts} +10 -3
  22. package/dist/core/{board-comment-9ORrSlA1.d.ts → board-comment-3N-jSgsk.d.ts} +2 -2
  23. package/dist/core/{board-comment-WzJC3SuF.d.cts → board-comment-DIdOJrSF.d.cts} +2 -2
  24. package/dist/core/claude/index.cjs +191 -32
  25. package/dist/core/claude/index.d.cts +4 -4
  26. package/dist/core/claude/index.d.ts +4 -4
  27. package/dist/core/claude/index.js +213 -40
  28. package/dist/core/client/index.cjs +1397 -0
  29. package/dist/core/client/index.d.cts +24 -0
  30. package/dist/core/client/index.d.ts +24 -0
  31. package/dist/core/client/index.js +1279 -0
  32. package/dist/core/{client-XpghdMQL.d.cts → client-B5PyIvNc.d.cts} +883 -131
  33. package/dist/core/{client-NFCS0H8T.d.ts → client-C1CsRi19.d.ts} +883 -131
  34. package/dist/core/config/browser.cjs +249 -2
  35. package/dist/core/config/browser.d.cts +138 -9
  36. package/dist/core/config/browser.d.ts +138 -9
  37. package/dist/core/config/browser.js +229 -1
  38. package/dist/core/config/index.cjs +950 -118
  39. package/dist/core/config/index.d.cts +285 -67
  40. package/dist/core/config/index.d.ts +285 -67
  41. package/dist/core/config/index.js +951 -123
  42. package/dist/core/{config-manager-BbMvB3Lz.d.cts → config-manager-BqXXvlih.d.ts} +34 -2
  43. package/dist/core/{config-manager-etFWO6Wo.d.ts → config-manager-C8zzUrMs.d.cts} +34 -2
  44. package/dist/core/{config-services-C848cfbD.d.ts → config-services-4zjcxc8Z.d.ts} +5 -5
  45. package/dist/core/{config-services-CDhfaNpd.d.cts → config-services-CXe0OHq7.d.cts} +5 -5
  46. package/dist/core/{context-ByxGjp5l.d.cts → context-BpkCELpO.d.cts} +1 -1
  47. package/dist/core/{context-ByxGjp5l.d.ts → context-BpkCELpO.d.ts} +1 -1
  48. package/dist/core/db/index.cjs +993 -294
  49. package/dist/core/db/index.d.cts +1076 -166
  50. package/dist/core/db/index.d.ts +1076 -166
  51. package/dist/core/db/index.js +998 -295
  52. package/dist/core/db/session-guard.d.cts +9 -9
  53. package/dist/core/db/session-guard.d.ts +9 -9
  54. package/dist/core/drizzle/postgres/0024_add_serialized_sessions.sql +24 -0
  55. package/dist/core/drizzle/postgres/0025_add_session_env_selections.sql +15 -0
  56. package/dist/core/drizzle/postgres/0026_env_command_variants.sql +41 -0
  57. package/dist/core/drizzle/postgres/0027_mcp_oauth_token_refresh.sql +72 -0
  58. package/dist/core/drizzle/postgres/meta/_journal.json +28 -0
  59. package/dist/core/drizzle/sqlite/0035_add_serialized_sessions.sql +22 -0
  60. package/dist/core/drizzle/sqlite/0036_add_session_env_selections.sql +18 -0
  61. package/dist/core/drizzle/sqlite/0037_env_command_variants.sql +38 -0
  62. package/dist/core/drizzle/sqlite/0038_mcp_oauth_token_refresh.sql +109 -0
  63. package/dist/core/drizzle/sqlite/meta/_journal.json +28 -0
  64. package/dist/core/environment/render-snapshot.cjs +172 -0
  65. package/dist/core/environment/render-snapshot.d.cts +76 -0
  66. package/dist/core/environment/render-snapshot.d.ts +76 -0
  67. package/dist/core/environment/render-snapshot.js +135 -0
  68. package/dist/core/environment/variable-resolver.cjs +6 -2
  69. package/dist/core/environment/variable-resolver.d.cts +3 -3
  70. package/dist/core/environment/variable-resolver.d.ts +3 -3
  71. package/dist/core/environment/variable-resolver.js +6 -2
  72. package/dist/core/feathers/index.cjs +6 -0
  73. package/dist/core/feathers/index.d.cts +1 -1
  74. package/dist/core/feathers/index.d.ts +1 -1
  75. package/dist/core/feathers/index.js +11 -1
  76. package/dist/core/{feathers-C8PkF35p.d.ts → feathers-BK9CCjVG.d.ts} +4 -4
  77. package/dist/core/{feathers--R3ml98e.d.cts → feathers-DnypeNDM.d.cts} +4 -4
  78. package/dist/core/gateway/index.d.cts +6 -6
  79. package/dist/core/gateway/index.d.ts +6 -6
  80. package/dist/core/{gateway-BYCTTJVJ.d.ts → gateway-B_4X-v07.d.ts} +5 -5
  81. package/dist/core/{gateway-D5me_jjo.d.cts → gateway-BnOlAelY.d.cts} +5 -5
  82. package/dist/core/git/index.cjs +294 -123
  83. package/dist/core/git/index.d.cts +89 -10
  84. package/dist/core/git/index.d.ts +89 -10
  85. package/dist/core/git/index.js +291 -124
  86. package/dist/core/{id-2oR2NdLp.d.cts → id-CoIbY_uc.d.cts} +43 -8
  87. package/dist/core/{id-2oR2NdLp.d.ts → id-CoIbY_uc.d.ts} +43 -8
  88. package/dist/core/index.cjs +2778 -791
  89. package/dist/core/index.d.cts +27 -26
  90. package/dist/core/index.d.ts +27 -26
  91. package/dist/core/index.js +2705 -767
  92. package/dist/core/lib/feathers-validation.d.cts +1 -1
  93. package/dist/core/lib/feathers-validation.d.ts +1 -1
  94. package/dist/core/mcp/index.cjs +150 -13
  95. package/dist/core/mcp/index.d.cts +2 -2
  96. package/dist/core/mcp/index.d.ts +2 -2
  97. package/dist/core/mcp/index.js +152 -13
  98. package/dist/core/{mcp-DUrvGUDS.d.cts → mcp-DC7GLAg2.d.cts} +10 -2
  99. package/dist/core/{mcp-D7eTnVUO.d.ts → mcp-z5v19H-r.d.ts} +10 -2
  100. package/dist/core/{message-C4Bb-L6c.d.ts → message-CHUUP6OS.d.ts} +2 -2
  101. package/dist/core/{message-BbDSJvyl.d.cts → message-DinITA7a.d.cts} +2 -2
  102. package/dist/core/models/browser.cjs +351 -0
  103. package/dist/core/models/browser.d.cts +208 -0
  104. package/dist/core/models/browser.d.ts +208 -0
  105. package/dist/core/models/browser.js +313 -0
  106. package/dist/core/models/gemini-shared.cjs +126 -0
  107. package/dist/core/models/gemini-shared.d.cts +39 -0
  108. package/dist/core/models/gemini-shared.d.ts +39 -0
  109. package/dist/core/models/gemini-shared.js +98 -0
  110. package/dist/core/models/index.cjs +68 -4
  111. package/dist/core/models/index.d.cts +80 -239
  112. package/dist/core/models/index.d.ts +80 -239
  113. package/dist/core/models/index.js +65 -3
  114. package/dist/core/package.json +35 -0
  115. package/dist/core/permissions/index.d.cts +2 -2
  116. package/dist/core/permissions/index.d.ts +2 -2
  117. package/dist/core/{repo-DaP4omZL.d.ts → repo-BcPbf8Ck.d.ts} +178 -9
  118. package/dist/core/{repo-zg1xnWQQ.d.cts → repo-CVyROjO4.d.cts} +178 -9
  119. package/dist/core/seed/index.cjs +912 -327
  120. package/dist/core/seed/index.d.cts +1 -1
  121. package/dist/core/seed/index.d.ts +1 -1
  122. package/dist/core/seed/index.js +921 -321
  123. package/dist/core/{session-C7mvs-rD.d.cts → session-CAfhv1qL.d.ts} +35 -15
  124. package/dist/core/{session-elEYFVev.d.ts → session-MNU4BQv4.d.cts} +35 -15
  125. package/dist/core/{session-guard-DOQgVFL6.d.cts → session-guard-C0LkDEN7.d.ts} +9 -4
  126. package/dist/core/{session-guard-D7hUa4D2.d.ts → session-guard-kvAx_8Fb.d.cts} +9 -4
  127. package/dist/core/{task-DJMxZTv4.d.cts → task-BLPFCORT.d.ts} +17 -3
  128. package/dist/core/{task-C8SPRSHg.d.ts → task-wht1RJEL.d.cts} +17 -3
  129. package/dist/core/templates/handlebars-helpers.cjs +3 -0
  130. package/dist/core/templates/handlebars-helpers.d.cts +5 -1
  131. package/dist/core/templates/handlebars-helpers.d.ts +5 -1
  132. package/dist/core/templates/handlebars-helpers.js +3 -0
  133. package/dist/core/templates/session-context.d.cts +6 -6
  134. package/dist/core/templates/session-context.d.ts +6 -6
  135. package/dist/core/templates/session-context.js +1 -1
  136. package/dist/core/tools/mcp/jwt-auth.d.cts +2 -2
  137. package/dist/core/tools/mcp/jwt-auth.d.ts +2 -2
  138. package/dist/core/tools/mcp/oauth-mcp-transport.d.cts +20 -4
  139. package/dist/core/tools/mcp/oauth-mcp-transport.d.ts +20 -4
  140. package/dist/core/tools/mcp/oauth-refresh.cjs +2902 -0
  141. package/dist/core/tools/mcp/oauth-refresh.d.cts +129 -0
  142. package/dist/core/tools/mcp/oauth-refresh.d.ts +129 -0
  143. package/dist/core/tools/mcp/oauth-refresh.js +2897 -0
  144. package/dist/core/types/index.cjs +45 -5
  145. package/dist/core/types/index.d.cts +17 -17
  146. package/dist/core/types/index.d.ts +17 -17
  147. package/dist/core/types/index.js +39 -5
  148. package/dist/core/{types-CvXKxTNP.d.ts → types-BsvM6vfZ.d.cts} +282 -4
  149. package/dist/core/{types-BQRGoDkg.d.cts → types-DuWSldjW.d.ts} +282 -4
  150. package/dist/core/unix/index.cjs +1448 -257
  151. package/dist/core/unix/index.d.cts +442 -36
  152. package/dist/core/unix/index.d.ts +442 -36
  153. package/dist/core/unix/index.js +1429 -247
  154. package/dist/core/{user-C9UDwwtA.d.ts → user-BfVWBmXA.d.ts} +46 -9
  155. package/dist/core/{user-wScngdUE.d.cts → user-DkNdeFoz.d.cts} +46 -9
  156. package/dist/core/utils/board-placement.d.cts +3 -3
  157. package/dist/core/utils/board-placement.d.ts +3 -3
  158. package/dist/core/utils/host-ip.cjs +51 -0
  159. package/dist/core/utils/host-ip.d.cts +27 -0
  160. package/dist/core/utils/host-ip.d.ts +27 -0
  161. package/dist/core/utils/host-ip.js +25 -0
  162. package/dist/core/utils/permission-mode-mapper.d.cts +4 -4
  163. package/dist/core/utils/permission-mode-mapper.d.ts +4 -4
  164. package/dist/core/utils/url.cjs +4 -3
  165. package/dist/core/utils/url.d.cts +1 -1
  166. package/dist/core/utils/url.d.ts +1 -1
  167. package/dist/core/utils/url.js +5 -4
  168. package/dist/core/yaml/index.cjs +49 -0
  169. package/dist/core/yaml/index.d.cts +19 -0
  170. package/dist/core/yaml/index.d.ts +19 -0
  171. package/dist/core/yaml/index.js +12 -0
  172. package/dist/daemon/auth/session-token-strategy.js +5 -5
  173. package/dist/daemon/declarations.d.ts +10 -3
  174. package/dist/daemon/index.js +5259 -2322
  175. package/dist/daemon/main.js +5259 -2322
  176. package/dist/daemon/mcp/resolve-ids.d.ts +1 -1
  177. package/dist/daemon/mcp/server.js +379 -138
  178. package/dist/daemon/mcp/tokens.d.ts +51 -60
  179. package/dist/daemon/mcp/tokens.js +93 -45
  180. package/dist/daemon/mcp/tools/analytics.js +49 -21
  181. package/dist/daemon/mcp/tools/artifacts.js +202 -13
  182. package/dist/daemon/mcp/tools/boards.js +51 -11
  183. package/dist/daemon/mcp/tools/card-types.js +42 -10
  184. package/dist/daemon/mcp/tools/cards.js +42 -10
  185. package/dist/daemon/mcp/tools/environment.js +42 -10
  186. package/dist/daemon/mcp/tools/mcp-servers.js +44 -14
  187. package/dist/daemon/mcp/tools/messages.js +57 -9
  188. package/dist/daemon/mcp/tools/repos.js +42 -10
  189. package/dist/daemon/mcp/tools/search.js +42 -10
  190. package/dist/daemon/mcp/tools/sessions.js +69 -30
  191. package/dist/daemon/mcp/tools/tasks.js +42 -10
  192. package/dist/daemon/mcp/tools/users.js +42 -10
  193. package/dist/daemon/mcp/tools/worktrees.js +46 -23
  194. package/dist/daemon/oauth-cache.d.ts +15 -20
  195. package/dist/daemon/oauth-cache.js +21 -94
  196. package/dist/daemon/register-hooks.d.ts +59 -1
  197. package/dist/daemon/register-hooks.js +496 -186
  198. package/dist/daemon/register-routes.d.ts +16 -1
  199. package/dist/daemon/register-routes.js +919 -201
  200. package/dist/daemon/register-services.d.ts +2 -0
  201. package/dist/daemon/register-services.js +2097 -479
  202. package/dist/daemon/services/artifacts.d.ts +76 -2
  203. package/dist/daemon/services/artifacts.js +276 -3
  204. package/dist/daemon/services/boards.js +1 -0
  205. package/dist/daemon/services/context.js +1 -0
  206. package/dist/daemon/services/file.js +1 -0
  207. package/dist/daemon/services/gateway.js +7 -16
  208. package/dist/daemon/services/github-app-setup.d.ts +70 -7
  209. package/dist/daemon/services/github-app-setup.js +161 -13
  210. package/dist/daemon/services/github-install-state.d.ts +52 -0
  211. package/dist/daemon/services/github-install-state.js +58 -0
  212. package/dist/daemon/services/leaderboard.d.ts +18 -4
  213. package/dist/daemon/services/leaderboard.js +104 -26
  214. package/dist/daemon/services/mcp-servers.d.ts +7 -2
  215. package/dist/daemon/services/messages.js +10 -3
  216. package/dist/daemon/services/oauth-disconnect.d.ts +5 -1
  217. package/dist/daemon/services/oauth-disconnect.js +10 -13
  218. package/dist/daemon/services/repos.d.ts +33 -2
  219. package/dist/daemon/services/repos.js +130 -33
  220. package/dist/daemon/services/scheduler.d.ts +58 -8
  221. package/dist/daemon/services/scheduler.js +136 -14
  222. package/dist/daemon/services/session-env-selections.d.ts +56 -0
  223. package/dist/daemon/services/session-env-selections.js +39 -0
  224. package/dist/daemon/services/sessions.d.ts +39 -2
  225. package/dist/daemon/services/sessions.js +180 -30
  226. package/dist/daemon/services/tasks.js +1 -1
  227. package/dist/daemon/services/terminals.js +62 -7
  228. package/dist/daemon/services/users.d.ts +7 -3
  229. package/dist/daemon/services/users.js +66 -15
  230. package/dist/daemon/services/worktree-owners.js +15 -7
  231. package/dist/daemon/services/worktrees.d.ts +46 -0
  232. package/dist/daemon/services/worktrees.js +342 -38
  233. package/dist/daemon/setup/build-info.d.ts +37 -0
  234. package/dist/daemon/setup/build-info.js +51 -0
  235. package/dist/daemon/setup/cors.d.ts +59 -22
  236. package/dist/daemon/setup/cors.js +69 -33
  237. package/dist/daemon/setup/database.js +15 -17
  238. package/dist/daemon/setup/index.d.ts +2 -1
  239. package/dist/daemon/setup/index.js +309 -66
  240. package/dist/daemon/setup/security-headers.d.ts +48 -0
  241. package/dist/daemon/setup/security-headers.js +35 -0
  242. package/dist/daemon/setup/service-tiers.d.ts +1 -1
  243. package/dist/daemon/setup/socketio.d.ts +93 -2
  244. package/dist/daemon/setup/socketio.js +179 -13
  245. package/dist/daemon/startup.js +153 -14
  246. package/dist/daemon/utils/auth-rate-limit-key.d.ts +21 -0
  247. package/dist/daemon/utils/auth-rate-limit-key.js +12 -0
  248. package/dist/daemon/utils/authorization.d.ts +14 -1
  249. package/dist/daemon/utils/authorization.js +17 -1
  250. package/dist/daemon/utils/html.d.ts +18 -0
  251. package/dist/daemon/utils/html.js +8 -0
  252. package/dist/daemon/utils/inject-created-by.d.ts +31 -0
  253. package/dist/daemon/utils/inject-created-by.js +28 -0
  254. package/dist/daemon/utils/session-state-hooks.d.ts +55 -0
  255. package/dist/daemon/utils/session-state-hooks.js +188 -0
  256. package/dist/daemon/utils/session-state.d.ts +45 -0
  257. package/dist/daemon/utils/session-state.js +100 -0
  258. package/dist/daemon/utils/spawn-executor.d.ts +9 -4
  259. package/dist/daemon/utils/spawn-executor.js +15 -7
  260. package/dist/daemon/utils/upload.d.ts +36 -1
  261. package/dist/daemon/utils/upload.js +89 -6
  262. package/dist/daemon/utils/worktree-authorization.d.ts +210 -3
  263. package/dist/daemon/utils/worktree-authorization.js +161 -27
  264. package/dist/executor/cli.js +10 -2
  265. package/dist/executor/commands/git.d.ts.map +1 -1
  266. package/dist/executor/commands/git.js +121 -56
  267. package/dist/executor/commands/unix.d.ts +7 -0
  268. package/dist/executor/commands/unix.d.ts.map +1 -1
  269. package/dist/executor/commands/unix.js +52 -8
  270. package/dist/executor/handlers/sdk/base-executor.d.ts.map +1 -1
  271. package/dist/executor/handlers/sdk/base-executor.js +3 -0
  272. package/dist/executor/index.d.ts.map +1 -1
  273. package/dist/executor/index.js +3 -0
  274. package/dist/executor/payload-types.d.ts +16 -14
  275. package/dist/executor/payload-types.d.ts.map +1 -1
  276. package/dist/executor/payload-types.js +2 -0
  277. package/dist/executor/sdk-handlers/claude/model-utils.d.ts +22 -0
  278. package/dist/executor/sdk-handlers/claude/model-utils.d.ts.map +1 -0
  279. package/dist/executor/sdk-handlers/claude/model-utils.js +28 -0
  280. package/dist/executor/sdk-handlers/claude/normalizer.d.ts.map +1 -1
  281. package/dist/executor/sdk-handlers/claude/normalizer.js +2 -1
  282. package/dist/executor/sdk-handlers/claude/query-builder.d.ts.map +1 -1
  283. package/dist/executor/sdk-handlers/claude/query-builder.js +20 -20
  284. package/dist/executor/sdk-handlers/codex/prompt-service.d.ts.map +1 -1
  285. package/dist/executor/sdk-handlers/codex/prompt-service.js +7 -3
  286. package/dist/executor/sdk-handlers/copilot/prompt-service.d.ts.map +1 -1
  287. package/dist/executor/sdk-handlers/copilot/prompt-service.js +4 -1
  288. package/dist/executor/sdk-handlers/gemini/prompt-service.d.ts.map +1 -1
  289. package/dist/executor/sdk-handlers/gemini/prompt-service.js +4 -3
  290. package/dist/executor/sdk-handlers/opencode/opencode-tool.d.ts.map +1 -1
  291. package/dist/executor/sdk-handlers/opencode/opencode-tool.js +2 -1
  292. package/dist/ui/assets/CodeEditor.inner-bDJMowpz.js +2 -0
  293. package/dist/ui/assets/CodeEditor.inner-bDJMowpz.js.gz +0 -0
  294. package/dist/ui/assets/{_basePickBy-CQZh_v13.js → _basePickBy-Bp06Otx8.js} +1 -1
  295. package/dist/ui/assets/_basePickBy-Bp06Otx8.js.gz +0 -0
  296. package/dist/ui/assets/{_baseUniq-Cgf5LtCM.js → _baseUniq-BfcctmWO.js} +1 -1
  297. package/dist/ui/assets/_baseUniq-BfcctmWO.js.gz +0 -0
  298. package/dist/ui/assets/{arc-DnTl2a4f.js → arc-CNqJvXfe.js} +1 -1
  299. package/dist/ui/assets/arc-CNqJvXfe.js.gz +0 -0
  300. package/dist/ui/assets/{architectureDiagram-VXUJARFQ-DZHKQxRL.js → architectureDiagram-VXUJARFQ-Lda-Lg4v.js} +1 -1
  301. package/dist/ui/assets/architectureDiagram-VXUJARFQ-Lda-Lg4v.js.gz +0 -0
  302. package/dist/ui/assets/{base-80a1f760-pDLzaKJY.js → base-80a1f760-If1fxi5k.js} +1 -1
  303. package/dist/ui/assets/{blockDiagram-VD42YOAC-BAOUMLja.js → blockDiagram-VD42YOAC-CziS0xWd.js} +1 -1
  304. package/dist/ui/assets/blockDiagram-VD42YOAC-CziS0xWd.js.gz +0 -0
  305. package/dist/ui/assets/{c4Diagram-YG6GDRKO-C0xcWHxz.js → c4Diagram-YG6GDRKO-Cu_7Cpwg.js} +1 -1
  306. package/dist/ui/assets/c4Diagram-YG6GDRKO-Cu_7Cpwg.js.gz +0 -0
  307. package/dist/ui/assets/channel-CTywVsey.js +1 -0
  308. package/dist/ui/assets/{chunk-4BX2VUAB-DCYNUaJZ.js → chunk-4BX2VUAB-CL7awCKO.js} +1 -1
  309. package/dist/ui/assets/{chunk-55IACEB6-DU3b1X7p.js → chunk-55IACEB6-CdEpihRe.js} +1 -1
  310. package/dist/ui/assets/{chunk-B4BG7PRW-BJlNsfTo.js → chunk-B4BG7PRW-CJZNfnap.js} +1 -1
  311. package/dist/ui/assets/chunk-B4BG7PRW-CJZNfnap.js.gz +0 -0
  312. package/dist/ui/assets/{chunk-DI55MBZ5-CKQY-IiF.js → chunk-DI55MBZ5-CVxoCWqP.js} +1 -1
  313. package/dist/ui/assets/chunk-DI55MBZ5-CVxoCWqP.js.gz +0 -0
  314. package/dist/ui/assets/{chunk-FMBD7UC4-DmjQ-Dzu.js → chunk-FMBD7UC4-DP03W4uO.js} +1 -1
  315. package/dist/ui/assets/{chunk-QN33PNHL-Bps50N51.js → chunk-QN33PNHL-BAZubWUQ.js} +1 -1
  316. package/dist/ui/assets/{chunk-QZHKN3VN-CNDKr6Xh.js → chunk-QZHKN3VN-BzRHvECc.js} +1 -1
  317. package/dist/ui/assets/{chunk-TZMSLE5B-R2Vz5IHZ.js → chunk-TZMSLE5B-iEWip6lm.js} +1 -1
  318. package/dist/ui/assets/chunk-TZMSLE5B-iEWip6lm.js.gz +0 -0
  319. package/dist/ui/assets/classDiagram-2ON5EDUG-DZGEyZn1.js +1 -0
  320. package/dist/ui/assets/classDiagram-v2-WZHVMYZB-DZGEyZn1.js +1 -0
  321. package/dist/ui/assets/clone-DTzUqr7D.js +1 -0
  322. package/dist/ui/assets/{consoleHook-59e792cb-MfGBgEJx.js → consoleHook-59e792cb-CVUkgtJ5.js} +1 -1
  323. package/dist/ui/assets/consoleHook-59e792cb-CVUkgtJ5.js.gz +0 -0
  324. package/dist/ui/assets/{cose-bilkent-S5V4N54A-Bkbv_h_w.js → cose-bilkent-S5V4N54A-CaNMYr_q.js} +1 -1
  325. package/dist/ui/assets/cose-bilkent-S5V4N54A-CaNMYr_q.js.gz +0 -0
  326. package/dist/ui/assets/{dagre-6UL2VRFP-DUoxEnB9.js → dagre-6UL2VRFP-PgKTEyMg.js} +1 -1
  327. package/dist/ui/assets/dagre-6UL2VRFP-PgKTEyMg.js.gz +0 -0
  328. package/dist/ui/assets/{diagram-PSM6KHXK-BUdRu7ma.js → diagram-PSM6KHXK-Bbakw1vV.js} +1 -1
  329. package/dist/ui/assets/diagram-PSM6KHXK-Bbakw1vV.js.gz +0 -0
  330. package/dist/ui/assets/{diagram-QEK2KX5R-DEoZ3bpF.js → diagram-QEK2KX5R-DORE6MDc.js} +1 -1
  331. package/dist/ui/assets/diagram-QEK2KX5R-DORE6MDc.js.gz +0 -0
  332. package/dist/ui/assets/{diagram-S2PKOQOG-CxM9wSuL.js → diagram-S2PKOQOG-CWMSw88L.js} +1 -1
  333. package/dist/ui/assets/diagram-S2PKOQOG-CWMSw88L.js.gz +0 -0
  334. package/dist/ui/assets/{erDiagram-Q2GNP2WA-D5VuZpBE.js → erDiagram-Q2GNP2WA-ClUb9rMg.js} +1 -1
  335. package/dist/ui/assets/erDiagram-Q2GNP2WA-ClUb9rMg.js.gz +0 -0
  336. package/dist/ui/assets/{flowDiagram-NV44I4VS-uuzmVpS4.js → flowDiagram-NV44I4VS-NGGzalyO.js} +1 -1
  337. package/dist/ui/assets/flowDiagram-NV44I4VS-NGGzalyO.js.gz +0 -0
  338. package/dist/ui/assets/ganttDiagram-LVOFAZNH-BD2QM47F.js +267 -0
  339. package/dist/ui/assets/ganttDiagram-LVOFAZNH-BD2QM47F.js.gz +0 -0
  340. package/dist/ui/assets/{gitGraphDiagram-NY62KEGX-DJDE3D90.js → gitGraphDiagram-NY62KEGX-IuwPjdlq.js} +2 -2
  341. package/dist/ui/assets/gitGraphDiagram-NY62KEGX-IuwPjdlq.js.gz +0 -0
  342. package/dist/ui/assets/{graph-BmWYHUOg.js → graph-mTd64UTw.js} +1 -1
  343. package/dist/ui/assets/graph-mTd64UTw.js.gz +0 -0
  344. package/dist/ui/assets/{index-599aeaf7-C76CG5K3.js → index-599aeaf7-C31VkrEQ.js} +1 -1
  345. package/dist/ui/assets/index-599aeaf7-C31VkrEQ.js.gz +0 -0
  346. package/dist/ui/assets/{index-CMrDOI5G.js → index-BmF5q3WZ.js} +435 -346
  347. package/dist/ui/assets/index-BmF5q3WZ.js.gz +0 -0
  348. package/dist/ui/assets/{index-CuMJFr7K.js → index-DlTdTyWM.js} +3 -3
  349. package/dist/ui/assets/index-DlTdTyWM.js.gz +0 -0
  350. package/dist/ui/assets/{index-fQo-B6L6.js → index-DlWtzMCo.js} +1 -1
  351. package/dist/ui/assets/index-DlWtzMCo.js.gz +0 -0
  352. package/dist/ui/assets/index-cfuYAmaV.js +4 -0
  353. package/dist/ui/assets/index-cfuYAmaV.js.gz +0 -0
  354. package/dist/ui/assets/{infoDiagram-ER5ION4S--dZLWvYP.js → infoDiagram-ER5ION4S-DPQFLihF.js} +1 -1
  355. package/dist/ui/assets/{journeyDiagram-XKPGCS4Q-C-Mzd3Ky.js → journeyDiagram-XKPGCS4Q-DTcUc9R_.js} +1 -1
  356. package/dist/ui/assets/journeyDiagram-XKPGCS4Q-DTcUc9R_.js.gz +0 -0
  357. package/dist/ui/assets/{kanban-definition-3W4ZIXB7-DoDzQCSc.js → kanban-definition-3W4ZIXB7-BS9ZD8g6.js} +5 -5
  358. package/dist/ui/assets/kanban-definition-3W4ZIXB7-BS9ZD8g6.js.gz +0 -0
  359. package/dist/ui/assets/{layout-BmSvKYXZ.js → layout-BjEUAE2D.js} +1 -1
  360. package/dist/ui/assets/layout-BjEUAE2D.js.gz +0 -0
  361. package/dist/ui/assets/linear-CLcpJ4o5.js +1 -0
  362. package/dist/ui/assets/linear-CLcpJ4o5.js.gz +0 -0
  363. package/dist/ui/assets/{mermaid.core-Suk7MkfT.js → mermaid.core-Br1XtbEr.js} +49 -49
  364. package/dist/ui/assets/mermaid.core-Br1XtbEr.js.gz +0 -0
  365. package/dist/ui/assets/{mindmap-definition-VGOIOE7T-Bj9HZQA9.js → mindmap-definition-VGOIOE7T-0S9U22YV.js} +3 -3
  366. package/dist/ui/assets/mindmap-definition-VGOIOE7T-0S9U22YV.js.gz +0 -0
  367. package/dist/ui/assets/{pieDiagram-ADFJNKIX-BIcJ5ee8.js → pieDiagram-ADFJNKIX-4yBZb57e.js} +2 -2
  368. package/dist/ui/assets/pieDiagram-ADFJNKIX-4yBZb57e.js.gz +0 -0
  369. package/dist/ui/assets/{quadrantDiagram-AYHSOK5B-CJm5QQ8N.js → quadrantDiagram-AYHSOK5B-BGdI-z0e.js} +2 -2
  370. package/dist/ui/assets/quadrantDiagram-AYHSOK5B-BGdI-z0e.js.gz +0 -0
  371. package/dist/ui/assets/{requirementDiagram-UZGBJVZJ-Bg6r943V.js → requirementDiagram-UZGBJVZJ-CfZa19uA.js} +1 -1
  372. package/dist/ui/assets/requirementDiagram-UZGBJVZJ-CfZa19uA.js.gz +0 -0
  373. package/dist/ui/assets/{sankeyDiagram-TZEHDZUN-C2XnZZZp.js → sankeyDiagram-TZEHDZUN-uFBRhhPF.js} +1 -1
  374. package/dist/ui/assets/sankeyDiagram-TZEHDZUN-uFBRhhPF.js.gz +0 -0
  375. package/dist/ui/assets/{sequenceDiagram-WL72ISMW-DOg-bBN1.js → sequenceDiagram-WL72ISMW-BdGgfhvi.js} +1 -1
  376. package/dist/ui/assets/sequenceDiagram-WL72ISMW-BdGgfhvi.js.gz +0 -0
  377. package/dist/ui/assets/{stateDiagram-FKZM4ZOC-DgZgPM-o.js → stateDiagram-FKZM4ZOC-DKkzcGoH.js} +1 -1
  378. package/dist/ui/assets/stateDiagram-FKZM4ZOC-DKkzcGoH.js.gz +0 -0
  379. package/dist/ui/assets/stateDiagram-v2-4FDKWEC3-DL2_q_1s.js +1 -0
  380. package/dist/ui/assets/{timeline-definition-IT6M3QCI-CmkWt6nL.js → timeline-definition-IT6M3QCI-BDzprIh8.js} +1 -1
  381. package/dist/ui/assets/timeline-definition-IT6M3QCI-BDzprIh8.js.gz +0 -0
  382. package/dist/ui/assets/{treemap-KMMF4GRG-CV8wwXTM.js → treemap-KMMF4GRG-BjxPgjXQ.js} +1 -1
  383. package/dist/ui/assets/treemap-KMMF4GRG-BjxPgjXQ.js.gz +0 -0
  384. package/dist/ui/assets/{xychartDiagram-PRI3JC2R-CvuPvlyn.js → xychartDiagram-PRI3JC2R-DVGB7LfP.js} +2 -2
  385. package/dist/ui/assets/xychartDiagram-PRI3JC2R-DVGB7LfP.js.gz +0 -0
  386. package/dist/ui/index.html +1 -1
  387. package/package.json +21 -20
  388. package/dist/ui/assets/_basePickBy-CQZh_v13.js.gz +0 -0
  389. package/dist/ui/assets/_baseUniq-Cgf5LtCM.js.gz +0 -0
  390. package/dist/ui/assets/arc-DnTl2a4f.js.gz +0 -0
  391. package/dist/ui/assets/architectureDiagram-VXUJARFQ-DZHKQxRL.js.gz +0 -0
  392. package/dist/ui/assets/blockDiagram-VD42YOAC-BAOUMLja.js.gz +0 -0
  393. package/dist/ui/assets/c4Diagram-YG6GDRKO-C0xcWHxz.js.gz +0 -0
  394. package/dist/ui/assets/channel-DpHH4gqH.js +0 -1
  395. package/dist/ui/assets/chunk-B4BG7PRW-BJlNsfTo.js.gz +0 -0
  396. package/dist/ui/assets/chunk-DI55MBZ5-CKQY-IiF.js.gz +0 -0
  397. package/dist/ui/assets/chunk-TZMSLE5B-R2Vz5IHZ.js.gz +0 -0
  398. package/dist/ui/assets/classDiagram-2ON5EDUG-CbDCU4XU.js +0 -1
  399. package/dist/ui/assets/classDiagram-v2-WZHVMYZB-CbDCU4XU.js +0 -1
  400. package/dist/ui/assets/clone-DSMLFNzW.js +0 -1
  401. package/dist/ui/assets/consoleHook-59e792cb-MfGBgEJx.js.gz +0 -0
  402. package/dist/ui/assets/cose-bilkent-S5V4N54A-Bkbv_h_w.js.gz +0 -0
  403. package/dist/ui/assets/dagre-6UL2VRFP-DUoxEnB9.js.gz +0 -0
  404. package/dist/ui/assets/diagram-PSM6KHXK-BUdRu7ma.js.gz +0 -0
  405. package/dist/ui/assets/diagram-QEK2KX5R-DEoZ3bpF.js.gz +0 -0
  406. package/dist/ui/assets/diagram-S2PKOQOG-CxM9wSuL.js.gz +0 -0
  407. package/dist/ui/assets/erDiagram-Q2GNP2WA-D5VuZpBE.js.gz +0 -0
  408. package/dist/ui/assets/flowDiagram-NV44I4VS-uuzmVpS4.js.gz +0 -0
  409. package/dist/ui/assets/ganttDiagram-LVOFAZNH-7spsgOUT.js +0 -267
  410. package/dist/ui/assets/ganttDiagram-LVOFAZNH-7spsgOUT.js.gz +0 -0
  411. package/dist/ui/assets/gitGraphDiagram-NY62KEGX-DJDE3D90.js.gz +0 -0
  412. package/dist/ui/assets/graph-BmWYHUOg.js.gz +0 -0
  413. package/dist/ui/assets/index-599aeaf7-C76CG5K3.js.gz +0 -0
  414. package/dist/ui/assets/index-B0lOKf7g.js +0 -4
  415. package/dist/ui/assets/index-B0lOKf7g.js.gz +0 -0
  416. package/dist/ui/assets/index-CMrDOI5G.js.gz +0 -0
  417. package/dist/ui/assets/index-CuMJFr7K.js.gz +0 -0
  418. package/dist/ui/assets/index-fQo-B6L6.js.gz +0 -0
  419. package/dist/ui/assets/journeyDiagram-XKPGCS4Q-C-Mzd3Ky.js.gz +0 -0
  420. package/dist/ui/assets/kanban-definition-3W4ZIXB7-DoDzQCSc.js.gz +0 -0
  421. package/dist/ui/assets/layout-BmSvKYXZ.js.gz +0 -0
  422. package/dist/ui/assets/linear-BnPF1K20.js +0 -1
  423. package/dist/ui/assets/linear-BnPF1K20.js.gz +0 -0
  424. package/dist/ui/assets/mermaid.core-Suk7MkfT.js.gz +0 -0
  425. package/dist/ui/assets/mindmap-definition-VGOIOE7T-Bj9HZQA9.js.gz +0 -0
  426. package/dist/ui/assets/pieDiagram-ADFJNKIX-BIcJ5ee8.js.gz +0 -0
  427. package/dist/ui/assets/quadrantDiagram-AYHSOK5B-CJm5QQ8N.js.gz +0 -0
  428. package/dist/ui/assets/requirementDiagram-UZGBJVZJ-Bg6r943V.js.gz +0 -0
  429. package/dist/ui/assets/sankeyDiagram-TZEHDZUN-C2XnZZZp.js.gz +0 -0
  430. package/dist/ui/assets/sequenceDiagram-WL72ISMW-DOg-bBN1.js.gz +0 -0
  431. package/dist/ui/assets/stateDiagram-FKZM4ZOC-DgZgPM-o.js.gz +0 -0
  432. package/dist/ui/assets/stateDiagram-v2-4FDKWEC3-C_0TAQGU.js +0 -1
  433. package/dist/ui/assets/timeline-definition-IT6M3QCI-CmkWt6nL.js.gz +0 -0
  434. package/dist/ui/assets/treemap-KMMF4GRG-CV8wwXTM.js.gz +0 -0
  435. package/dist/ui/assets/xychartDiagram-PRI3JC2R-CvuPvlyn.js.gz +0 -0
@@ -1,6 +1,6 @@
1
1
  // src/commands/admin/sync-unix.ts
2
2
  import { execSync } from "child_process";
3
- import { existsSync } from "fs";
3
+ import { existsSync, readlinkSync } from "fs";
4
4
  import { homedir } from "os";
5
5
  import { join } from "path";
6
6
  import { loadConfig } from "@agor/core/config";
@@ -15,14 +15,29 @@ import {
15
15
  worktreeOwners,
16
16
  worktrees
17
17
  } from "@agor/core/db";
18
+ import { restoreWorktreeFilesystem } from "@agor/core/git";
18
19
  import {
19
20
  AGOR_USERS_GROUP,
21
+ CommandError,
22
+ createAdminExecutor,
20
23
  generateRepoGroupName,
21
24
  generateWorktreeGroupName,
25
+ getGroupMembers,
26
+ getUserGroups,
27
+ getUserWorktreesDir,
28
+ getWorktreeDirectoryAction,
22
29
  getWorktreePermissionMode,
30
+ getWorktreeSymlinkPath,
31
+ groupExists,
32
+ isUserInGroup,
33
+ listAgorUsers,
34
+ listRepoGroups,
35
+ listWorktreeGroups,
23
36
  REPO_GIT_PERMISSION_MODE,
37
+ SymlinkCommands,
24
38
  UnixGroupCommands,
25
- UnixUserCommands
39
+ UnixUserCommands,
40
+ unixUserExists
26
41
  } from "@agor/core/unix";
27
42
  import { Command, Flags } from "@oclif/core";
28
43
  import chalk from "chalk";
@@ -32,7 +47,8 @@ var SyncUnix = class _SyncUnix extends Command {
32
47
  "<%= config.bin %> <%= command.id %> # Full sync (creates users, groups, sets permissions)",
33
48
  "<%= config.bin %> <%= command.id %> --dry-run # Preview what would be done",
34
49
  "<%= config.bin %> <%= command.id %> --cleanup # Full sync + remove stale users/groups",
35
- "<%= config.bin %> <%= command.id %> --verbose # Show detailed output"
50
+ "<%= config.bin %> <%= command.id %> --verbose # Show detailed output",
51
+ "<%= config.bin %> <%= command.id %> --worktree-id <uuid> --dry-run # Preview sync for a single worktree"
36
52
  ];
37
53
  static flags = {
38
54
  "dry-run": Flags.boolean({
@@ -57,193 +73,79 @@ var SyncUnix = class _SyncUnix extends Command {
57
73
  "cleanup-users": Flags.boolean({
58
74
  description: "Delete stale agor_* users not in database (keeps home directories)",
59
75
  default: false
76
+ }),
77
+ "worktree-id": Flags.string({
78
+ char: "w",
79
+ description: "Sync a single worktree and its parent repo (skips unrelated user/membership/symlink phases)"
60
80
  })
61
81
  };
62
- /**
63
- * Check if a Unix user exists on the system
64
- */
65
- userExists(username) {
66
- try {
67
- execSync(UnixUserCommands.userExists(username), { stdio: "ignore" });
68
- return true;
69
- } catch {
70
- return false;
71
- }
72
- }
73
- /**
74
- * Get groups a Unix user belongs to
75
- */
76
- getUserGroups(username) {
77
- try {
78
- const output = execSync(UnixUserCommands.getUserGroups(username), {
79
- encoding: "utf-8",
80
- stdio: ["pipe", "pipe", "ignore"]
81
- });
82
- return output.trim().split(/\s+/).filter(Boolean);
83
- } catch {
84
- return [];
85
- }
86
- }
87
- /**
88
- * Check if a Unix group exists
89
- */
90
- groupExists(groupName) {
91
- try {
92
- execSync(UnixGroupCommands.groupExists(groupName), { stdio: "ignore" });
93
- return true;
94
- } catch {
95
- return false;
96
- }
97
- }
98
- /**
99
- * Check if a Unix user is in a group
100
- */
101
- isUserInGroup(username, groupName) {
102
- try {
103
- execSync(UnixGroupCommands.isUserInGroup(username, groupName), { stdio: "ignore" });
104
- return true;
105
- } catch {
106
- return false;
107
- }
108
- }
109
- /**
110
- * Create a Unix user (assumes running as root via sudo)
111
- */
112
- createUser(username, dryRun) {
113
- const cmd = UnixUserCommands.createUser(username);
114
- if (dryRun) {
115
- this.log(chalk.gray(` [dry-run] Would run: ${cmd}`));
116
- return true;
117
- }
118
- try {
119
- execSync(cmd, { stdio: "inherit" });
120
- return true;
121
- } catch {
122
- return false;
123
- }
124
- }
125
- /**
126
- * Add user to a group (assumes running as root via sudo)
127
- */
128
- addUserToGroup(username, groupName, dryRun) {
129
- const cmd = UnixGroupCommands.addUserToGroup(username, groupName);
130
- if (dryRun) {
131
- this.log(chalk.gray(` [dry-run] Would run: ${cmd}`));
132
- return true;
133
- }
134
- try {
135
- execSync(cmd, { stdio: "inherit" });
136
- return true;
137
- } catch {
138
- return false;
139
- }
140
- }
141
- /**
142
- * Create a Unix group (assumes running as root via sudo)
143
- */
144
- createGroup(groupName, dryRun) {
145
- const cmd = UnixGroupCommands.createGroup(groupName);
146
- if (dryRun) {
147
- this.log(chalk.gray(` [dry-run] Would run: ${cmd}`));
148
- return true;
149
- }
150
- try {
151
- execSync(cmd, { stdio: "inherit" });
152
- return true;
153
- } catch {
154
- return false;
155
- }
156
- }
157
- /**
158
- * Delete a Unix user (keeps home directory)
159
- */
160
- deleteUser(username, dryRun) {
161
- const cmd = UnixUserCommands.deleteUser(username);
162
- if (dryRun) {
163
- this.log(chalk.gray(` [dry-run] Would run: ${cmd}`));
164
- return true;
165
- }
166
- try {
167
- execSync(cmd, { stdio: "inherit" });
168
- return true;
169
- } catch {
170
- return false;
171
- }
172
- }
173
- /**
174
- * Delete a Unix group
175
- */
176
- deleteGroup(groupName, dryRun) {
177
- const cmd = UnixGroupCommands.deleteGroup(groupName);
178
- if (dryRun) {
179
- this.log(chalk.gray(` [dry-run] Would run: ${cmd}`));
180
- return true;
181
- }
182
- try {
183
- execSync(cmd, { stdio: "inherit" });
184
- return true;
185
- } catch {
186
- return false;
187
- }
188
- }
189
- /**
190
- * List all agor_* users on the system (auto-generated format: agor_<8-hex>)
191
- */
192
- listAgorUsers() {
193
- try {
194
- const output = execSync("getent passwd | grep '^agor_' | cut -d: -f1", {
195
- encoding: "utf-8",
196
- stdio: ["pipe", "pipe", "ignore"]
197
- });
198
- return output.trim().split("\n").filter((u) => u && /^agor_[0-9a-f]{8}$/.test(u));
199
- } catch {
200
- return [];
201
- }
202
- }
203
- /**
204
- * List all agor_wt_* groups on the system
205
- */
206
- listWorktreeGroups() {
207
- try {
208
- const output = execSync("getent group | grep '^agor_wt_' | cut -d: -f1", {
209
- encoding: "utf-8",
210
- stdio: ["pipe", "pipe", "ignore"]
211
- });
212
- return output.trim().split("\n").filter((g) => g && /^agor_wt_[0-9a-f]{8}$/.test(g));
213
- } catch {
214
- return [];
215
- }
216
- }
217
- /**
218
- * List all agor_rp_* (repo) groups on the system
219
- */
220
- listRepoGroups() {
221
- try {
222
- const output = execSync("getent group | grep '^agor_rp_' | cut -d: -f1", {
223
- encoding: "utf-8",
224
- stdio: ["pipe", "pipe", "ignore"]
225
- });
226
- return output.trim().split("\n").filter((g) => g && /^agor_rp_[0-9a-f]{8}$/.test(g));
227
- } catch {
228
- return [];
229
- }
230
- }
231
82
  async run() {
232
83
  const { flags } = await this.parse(_SyncUnix);
233
84
  const dryRun = flags["dry-run"];
234
85
  const verbose = flags.verbose;
235
86
  const cleanupGroups = flags.cleanup || flags["cleanup-groups"];
236
87
  const cleanupUsers = flags.cleanup || flags["cleanup-users"];
88
+ const targetWorktreeId = flags["worktree-id"];
89
+ if (targetWorktreeId) {
90
+ this.log(chalk.cyan(`\u{1F3AF} Targeting single worktree: ${targetWorktreeId}
91
+ `));
92
+ }
237
93
  if (dryRun) {
238
94
  this.log(chalk.yellow("\u{1F50D} Dry run mode - no changes will be made\n"));
239
95
  }
96
+ const executor = createAdminExecutor({ "dry-run": dryRun, verbose });
97
+ const logCmdError = (err, fallbackCmd) => {
98
+ if (err instanceof CommandError) {
99
+ const cmd = err.command || fallbackCmd;
100
+ const stderr = err.result.stderr.trim();
101
+ if (cmd) this.log(chalk.red(` \u21B3 ${cmd}`));
102
+ if (stderr) {
103
+ for (const line of stderr.split("\n").slice(0, 10)) {
104
+ this.log(chalk.red(` ${line}`));
105
+ }
106
+ }
107
+ this.log(chalk.red(` (exit ${err.result.exitCode})`));
108
+ } else {
109
+ const msg = err instanceof Error ? err.message : String(err);
110
+ if (fallbackCmd) this.log(chalk.red(` \u21B3 ${fallbackCmd}`));
111
+ this.log(chalk.red(` ${msg}`));
112
+ }
113
+ };
114
+ const execCmd = async (cmd) => {
115
+ try {
116
+ await executor.exec(cmd);
117
+ return true;
118
+ } catch (err) {
119
+ logCmdError(err, cmd);
120
+ return false;
121
+ }
122
+ };
123
+ const execAllCmds = async (cmds) => {
124
+ try {
125
+ await executor.execAll(cmds);
126
+ return true;
127
+ } catch (err) {
128
+ logCmdError(err);
129
+ return false;
130
+ }
131
+ };
240
132
  let groupsCreated = 0;
241
133
  let groupsDeleted = 0;
242
134
  let usersDeleted = 0;
243
135
  let cleanupErrors = 0;
244
136
  let worktreesSynced = 0;
137
+ let worktreesBackfilled = 0;
138
+ let worktreeDirsCreated = 0;
139
+ let worktreesRestored = 0;
140
+ let groupsCleaned = 0;
141
+ let statusFixed = 0;
142
+ let worktreesSkipped = 0;
245
143
  let reposBackfilled = 0;
246
144
  let reposPermSynced = 0;
145
+ let membershipsRemoved = 0;
146
+ let daemonAclsApplied = 0;
147
+ let symlinksCreated = 0;
148
+ let symlinksCleaned = 0;
247
149
  let syncErrors = 0;
248
150
  try {
249
151
  let databaseUrl = process.env.DATABASE_URL;
@@ -302,11 +204,25 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
302
204
  );
303
205
  }
304
206
  let daemonMembershipsAdded = 0;
207
+ let targetRepoId;
208
+ if (targetWorktreeId) {
209
+ const targetWts = await select(db).from(worktrees).where(eq(worktrees.worktree_id, targetWorktreeId)).all();
210
+ if (targetWts.length === 0) {
211
+ this.log(chalk.red(`\u2717 Worktree ${targetWorktreeId} not found in database
212
+ `));
213
+ process.exit(1);
214
+ }
215
+ targetRepoId = targetWts[0].repo_id;
216
+ this.log(
217
+ chalk.cyan(` Parent repo: ${targetRepoId.substring(0, 8)} (also scoped to this repo)
218
+ `)
219
+ );
220
+ }
305
221
  this.log(chalk.cyan(`Checking ${AGOR_USERS_GROUP} group...
306
222
  `));
307
- if (!this.groupExists(AGOR_USERS_GROUP)) {
223
+ if (!groupExists(AGOR_USERS_GROUP)) {
308
224
  this.log(chalk.yellow(` \u2192 Creating ${AGOR_USERS_GROUP} group...`));
309
- if (this.createGroup(AGOR_USERS_GROUP, dryRun)) {
225
+ if (await execCmd(UnixGroupCommands.createGroup(AGOR_USERS_GROUP))) {
310
226
  groupsCreated++;
311
227
  this.log(chalk.green(` \u2713 Created ${AGOR_USERS_GROUP} group
312
228
  `));
@@ -321,136 +237,153 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
321
237
  const allUsers = await select(db).from(users).all();
322
238
  const validUsers = allUsers.filter((u) => u.unix_username);
323
239
  const results = [];
324
- if (validUsers.length === 0) {
325
- this.log(chalk.yellow("No users with unix_username found in database"));
326
- this.log(chalk.gray("\nTo set a unix_username for a user:"));
327
- this.log(chalk.gray(" agor user update <email> --unix-username <username>\n"));
328
- } else {
329
- this.log(chalk.cyan(`Found ${validUsers.length} user(s) with unix_username
330
- `));
331
- const userIds = validUsers.map((u) => u.user_id);
332
- const allOwnerships = await db.select().from(worktreeOwners).innerJoin(worktrees, eq(worktreeOwners.worktree_id, worktrees.worktree_id)).where(inArray(worktreeOwners.user_id, userIds));
333
- const ownershipsByUser = /* @__PURE__ */ new Map();
334
- for (const row of allOwnerships) {
335
- const userId = row.worktree_owners.user_id;
336
- const ownership = {
337
- worktree_id: row.worktrees.worktree_id,
338
- name: row.worktrees.name,
339
- unix_group: row.worktrees.unix_group,
340
- repo_id: row.worktrees.repo_id
341
- };
342
- const existing = ownershipsByUser.get(userId) || [];
343
- existing.push(ownership);
344
- ownershipsByUser.set(userId, existing);
345
- }
346
- let allRepos = await select(db).from(repos).all();
240
+ {
241
+ const reposInScope = targetRepoId ? await select(db).from(repos).where(eq(repos.repo_id, targetRepoId)).all() : await select(db).from(repos).all();
347
242
  this.log(chalk.cyan.bold("\n\u2501\u2501\u2501 Sync Repos \u2501\u2501\u2501\n"));
348
- const reposWithoutGroup = allRepos.filter(
349
- (r) => r.unix_group === null
350
- );
351
- if (reposWithoutGroup.length === 0) {
352
- this.log(chalk.green(" \u2713 All repos have unix_group set\n"));
243
+ if (reposInScope.length === 0) {
244
+ this.log(chalk.yellow(" No repos in scope\n"));
353
245
  } else {
246
+ this.log(chalk.cyan(`Processing ${reposInScope.length} repo(s)
247
+ `));
248
+ }
249
+ for (const repo of reposInScope) {
250
+ const rawRepo = repo;
251
+ const expectedGroup = rawRepo.unix_group || generateRepoGroupName(rawRepo.repo_id);
252
+ const dbNeedsBackfill = rawRepo.unix_group === null;
253
+ const groupMissingOnSystem = !groupExists(expectedGroup);
254
+ const repoPath = rawRepo.data?.local_path;
255
+ const pathUsable = repoPath ? existsSync(repoPath) : false;
256
+ this.log(chalk.bold(`\u{1F4C1} ${rawRepo.slug}`));
257
+ this.log(chalk.gray(` repo_id: ${rawRepo.repo_id.substring(0, 8)}`));
354
258
  this.log(
355
- chalk.cyan(
356
- `Found ${reposWithoutGroup.length} repo(s) without unix_group (of ${allRepos.length} total)
357
- `
358
- )
259
+ chalk.gray(` unix_group: ${expectedGroup}${dbNeedsBackfill ? " (to backfill)" : ""}`)
359
260
  );
360
- for (const repo of reposWithoutGroup) {
361
- const rawRepo = repo;
362
- const repoGroup = generateRepoGroupName(rawRepo.repo_id);
363
- this.log(chalk.bold(`\u{1F4C1} ${rawRepo.slug}`));
364
- this.log(chalk.gray(` repo_id: ${rawRepo.repo_id.substring(0, 8)}`));
365
- this.log(chalk.gray(` generated group: ${repoGroup}`));
366
- const groupExistsOnSystem = this.groupExists(repoGroup);
367
- if (groupExistsOnSystem) {
368
- this.log(chalk.green(` \u2713 Unix group already exists`));
261
+ if (repoPath) {
262
+ this.log(chalk.gray(` repo path: ${repoPath}${pathUsable ? "" : " (missing)"}`));
263
+ } else {
264
+ this.log(chalk.gray(` repo path: <none in data.local_path>`));
265
+ }
266
+ let hadError = false;
267
+ if (groupMissingOnSystem) {
268
+ this.log(chalk.yellow(` \u2192 Creating Unix group ${expectedGroup}...`));
269
+ if (await execCmd(UnixGroupCommands.createGroup(expectedGroup))) {
270
+ groupsCreated++;
271
+ this.log(chalk.green(` \u2713 Created Unix group ${expectedGroup}`));
369
272
  } else {
370
- this.log(chalk.yellow(` \u2192 Creating Unix group ${repoGroup}...`));
371
- if (this.createGroup(repoGroup, dryRun)) {
372
- groupsCreated++;
373
- this.log(chalk.green(` \u2713 Created Unix group ${repoGroup}`));
273
+ syncErrors++;
274
+ hadError = true;
275
+ this.log(chalk.red(` \u2717 Failed to create Unix group ${expectedGroup}`));
276
+ }
277
+ } else if (verbose) {
278
+ this.log(chalk.gray(` \u2713 Unix group exists`));
279
+ }
280
+ if (!hadError && daemonUser) {
281
+ const daemonInGroup = dryRun ? false : isUserInGroup(daemonUser, expectedGroup);
282
+ if (!daemonInGroup) {
283
+ this.log(
284
+ chalk.yellow(` \u2192 Adding daemon user ${daemonUser} to ${expectedGroup}...`)
285
+ );
286
+ if (await execCmd(UnixGroupCommands.addUserToGroup(daemonUser, expectedGroup))) {
287
+ daemonMembershipsAdded++;
288
+ this.log(chalk.green(` \u2713 Added daemon user to ${expectedGroup}`));
374
289
  } else {
375
290
  syncErrors++;
376
- this.log(chalk.red(` \u2717 Failed to create Unix group ${repoGroup}`));
377
- this.log("");
378
- continue;
379
- }
380
- }
381
- if (daemonUser) {
382
- const daemonInGroup = dryRun ? false : this.isUserInGroup(daemonUser, repoGroup);
383
- if (!daemonInGroup) {
384
- this.log(chalk.yellow(` \u2192 Adding daemon user ${daemonUser} to ${repoGroup}...`));
385
- if (this.addUserToGroup(daemonUser, repoGroup, dryRun)) {
386
- daemonMembershipsAdded++;
387
- this.log(chalk.green(` \u2713 Added daemon user to ${repoGroup}`));
388
- } else {
389
- this.log(chalk.red(` \u2717 Failed to add daemon user to ${repoGroup}`));
390
- }
391
- } else if (verbose) {
392
- this.log(chalk.gray(` \u2713 Daemon user already in ${repoGroup}`));
291
+ this.log(chalk.red(` \u2717 Failed to add daemon user to ${expectedGroup}`));
393
292
  }
293
+ } else if (verbose) {
294
+ this.log(chalk.gray(` \u2713 Daemon user already in ${expectedGroup}`));
394
295
  }
296
+ }
297
+ if (!hadError && dbNeedsBackfill) {
395
298
  if (dryRun) {
396
299
  this.log(
397
300
  chalk.gray(
398
- ` [dry-run] Would update database: SET unix_group = '${repoGroup}' WHERE repo_id = '${rawRepo.repo_id}'`
301
+ ` [dry-run] Would update database: SET unix_group = '${expectedGroup}' WHERE repo_id = '${rawRepo.repo_id}'`
399
302
  )
400
303
  );
304
+ reposBackfilled++;
401
305
  } else {
402
306
  try {
403
- await update(db, repos).set({ unix_group: repoGroup }).where(eq(repos.repo_id, rawRepo.repo_id)).run();
404
- this.log(chalk.green(` \u2713 Updated database with unix_group`));
307
+ await update(db, repos).set({ unix_group: expectedGroup }).where(eq(repos.repo_id, rawRepo.repo_id)).run();
308
+ reposBackfilled++;
309
+ this.log(chalk.green(` \u2713 Backfilled unix_group in database`));
405
310
  } catch (error) {
406
311
  syncErrors++;
312
+ hadError = true;
407
313
  this.log(chalk.red(` \u2717 Failed to update database: ${error}`));
408
- this.log("");
409
- continue;
410
314
  }
411
315
  }
412
- const repoPath = rawRepo.data?.local_path;
413
- if (repoPath) {
316
+ }
317
+ if (!hadError) {
318
+ if (!repoPath) {
319
+ this.log(chalk.yellow(` \u26A0 No local_path in repo data, skipping permissions`));
320
+ } else if (!pathUsable) {
321
+ if (verbose) {
322
+ this.log(chalk.gray(` \u2298 Repo path missing on disk, skipping permissions`));
323
+ }
324
+ } else {
414
325
  const gitPath = `${repoPath}/.git`;
415
- this.log(chalk.gray(` .git path: ${gitPath}`));
416
- if (dryRun) {
417
- this.log(chalk.gray(` [dry-run] Would run: chgrp -R ${repoGroup} "${gitPath}"`));
326
+ const rootCmds = UnixGroupCommands.setDirectoryGroupShallow(
327
+ repoPath,
328
+ expectedGroup,
329
+ REPO_GIT_PERMISSION_MODE
330
+ );
331
+ const cmds = existsSync(gitPath) ? [
332
+ ...rootCmds,
333
+ ...UnixGroupCommands.setDirectoryGroup(
334
+ gitPath,
335
+ expectedGroup,
336
+ REPO_GIT_PERMISSION_MODE
337
+ )
338
+ ] : rootCmds;
339
+ if (await execAllCmds(cmds)) {
340
+ reposPermSynced++;
418
341
  this.log(
419
- chalk.gray(
420
- ` [dry-run] Would run: chmod -R ${REPO_GIT_PERMISSION_MODE} "${gitPath}"`
421
- )
342
+ chalk.green(` \u2713 Applied repo permissions (${REPO_GIT_PERMISSION_MODE})`)
422
343
  );
423
- } else {
424
- try {
425
- for (const cmd of UnixGroupCommands.setDirectoryGroup(
426
- gitPath,
427
- repoGroup,
428
- REPO_GIT_PERMISSION_MODE
429
- )) {
430
- execSync(cmd, { stdio: "pipe" });
431
- }
432
- this.log(
433
- chalk.green(` \u2713 Applied .git permissions (${REPO_GIT_PERMISSION_MODE})`)
434
- );
435
- } catch (error) {
436
- syncErrors++;
437
- this.log(chalk.red(` \u2717 Failed to set .git permissions: ${error}`));
344
+ if (!existsSync(gitPath) && verbose) {
345
+ this.log(chalk.gray(` \u2298 .git path missing on disk, root traversal only`));
438
346
  }
347
+ } else {
348
+ syncErrors++;
349
+ this.log(chalk.red(` \u2717 Failed to set repo permissions`));
439
350
  }
440
- } else {
441
- this.log(chalk.yellow(` \u26A0 No local_path found, skipping .git permissions`));
442
351
  }
443
- reposBackfilled++;
444
- this.log("");
445
- }
446
- this.log(chalk.bold("Repo Backfill Summary:"));
447
- this.log(` Repos backfilled: ${reposBackfilled}${dryRun ? " (dry-run)" : ""}`);
448
- if (syncErrors > 0) {
449
- this.log(chalk.red(` Errors: ${syncErrors}`));
450
352
  }
451
353
  this.log("");
452
- allRepos = await select(db).from(repos).all();
453
354
  }
355
+ if (reposInScope.length > 0) {
356
+ this.log(chalk.bold("Sync Repos Summary:"));
357
+ this.log(` DB backfilled: ${reposBackfilled}${dryRun ? " (dry-run)" : ""}`);
358
+ this.log(` Permissions synced:${reposPermSynced}${dryRun ? " (dry-run)" : ""}`);
359
+ this.log("");
360
+ }
361
+ }
362
+ if (targetWorktreeId) {
363
+ this.log(chalk.gray(" \u2298 Skipping user sync phase (--worktree-id mode)\n"));
364
+ } else if (validUsers.length === 0) {
365
+ this.log(chalk.yellow("No users with unix_username found in database"));
366
+ this.log(chalk.gray("\nTo set a unix_username for a user:"));
367
+ this.log(chalk.gray(" agor user update <email> --unix-username <username>\n"));
368
+ } else {
369
+ this.log(chalk.cyan(`Found ${validUsers.length} user(s) with unix_username
370
+ `));
371
+ const userIds = validUsers.map((u) => u.user_id);
372
+ const allOwnerships = await db.select().from(worktreeOwners).innerJoin(worktrees, eq(worktreeOwners.worktree_id, worktrees.worktree_id)).where(inArray(worktreeOwners.user_id, userIds));
373
+ const ownershipsByUser = /* @__PURE__ */ new Map();
374
+ for (const row of allOwnerships) {
375
+ const userId = row.worktree_owners.user_id;
376
+ const ownership = {
377
+ worktree_id: row.worktrees.worktree_id,
378
+ name: row.worktrees.name,
379
+ unix_group: row.worktrees.unix_group,
380
+ repo_id: row.worktrees.repo_id
381
+ };
382
+ const existing = ownershipsByUser.get(userId) || [];
383
+ existing.push(ownership);
384
+ ownershipsByUser.set(userId, existing);
385
+ }
386
+ const allRepos = await select(db).from(repos).all();
454
387
  const repoGroupMap = /* @__PURE__ */ new Map();
455
388
  for (const repo of allRepos) {
456
389
  const r = repo;
@@ -472,13 +405,13 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
472
405
  this.log(chalk.bold(`\u{1F4CB} ${user.email}`));
473
406
  this.log(chalk.gray(` unix_username: ${user.unix_username}`));
474
407
  this.log(chalk.gray(` user_id: ${user.user_id.substring(0, 8)}`));
475
- result.unixUserExists = this.userExists(user.unix_username);
408
+ result.unixUserExists = unixUserExists(user.unix_username);
476
409
  if (result.unixUserExists) {
477
410
  this.log(chalk.green(` \u2713 Unix user exists`));
478
411
  } else {
479
412
  this.log(chalk.red(` \u2717 Unix user does not exist`));
480
413
  this.log(chalk.yellow(` \u2192 Creating Unix user...`));
481
- if (this.createUser(user.unix_username, dryRun)) {
414
+ if (await execCmd(UnixUserCommands.createUser(user.unix_username))) {
482
415
  result.unixUserCreated = true;
483
416
  result.unixUserExists = true;
484
417
  this.log(chalk.green(` \u2713 Unix user created`));
@@ -488,13 +421,15 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
488
421
  }
489
422
  }
490
423
  if (result.unixUserExists || dryRun) {
491
- result.groups.actual = result.unixUserExists ? this.getUserGroups(user.unix_username) : [];
424
+ result.groups.actual = result.unixUserExists ? getUserGroups(user.unix_username) : [];
492
425
  if (verbose && result.groups.actual.length > 0) {
493
426
  this.log(chalk.gray(` Current groups: ${result.groups.actual.join(", ")}`));
494
427
  }
495
428
  if (!result.groups.actual.includes(AGOR_USERS_GROUP)) {
496
429
  this.log(chalk.yellow(` \u2192 Adding to ${AGOR_USERS_GROUP}...`));
497
- if (this.addUserToGroup(user.unix_username, AGOR_USERS_GROUP, dryRun)) {
430
+ if (await execCmd(
431
+ UnixGroupCommands.addUserToGroup(user.unix_username, AGOR_USERS_GROUP)
432
+ )) {
498
433
  result.groups.added.push(AGOR_USERS_GROUP);
499
434
  this.log(chalk.green(` \u2713 Added to ${AGOR_USERS_GROUP}`));
500
435
  } else {
@@ -510,7 +445,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
510
445
  const expectedGroup = wt.unix_group || generateWorktreeGroupName(wt.worktree_id);
511
446
  result.groups.expected.push(expectedGroup);
512
447
  const isInGroup = result.groups.actual.includes(expectedGroup);
513
- const groupExistsOnSystem = this.groupExists(expectedGroup);
448
+ const groupExistsOnSystem = groupExists(expectedGroup);
514
449
  if (verbose) {
515
450
  this.log(
516
451
  chalk.gray(
@@ -521,7 +456,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
521
456
  let groupReady = groupExistsOnSystem;
522
457
  if (!groupExistsOnSystem) {
523
458
  this.log(chalk.yellow(` \u2192 Creating group ${expectedGroup}...`));
524
- if (this.createGroup(expectedGroup, dryRun)) {
459
+ if (await execCmd(UnixGroupCommands.createGroup(expectedGroup))) {
525
460
  groupsCreated++;
526
461
  groupReady = true;
527
462
  this.log(chalk.green(` \u2713 Created group ${expectedGroup}`));
@@ -532,7 +467,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
532
467
  }
533
468
  if (groupReady && !isInGroup) {
534
469
  this.log(chalk.yellow(` \u2192 Adding to group ${expectedGroup}...`));
535
- if (this.addUserToGroup(user.unix_username, expectedGroup, dryRun)) {
470
+ if (await execCmd(UnixGroupCommands.addUserToGroup(user.unix_username, expectedGroup))) {
536
471
  result.groups.added.push(expectedGroup);
537
472
  this.log(chalk.green(` \u2713 Added to ${expectedGroup}`));
538
473
  } else {
@@ -541,12 +476,12 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
541
476
  }
542
477
  }
543
478
  if (groupReady && daemonUser) {
544
- const daemonInWtGroup = dryRun ? false : this.isUserInGroup(daemonUser, expectedGroup);
479
+ const daemonInWtGroup = dryRun ? false : isUserInGroup(daemonUser, expectedGroup);
545
480
  if (!daemonInWtGroup) {
546
481
  this.log(
547
482
  chalk.yellow(` \u2192 Adding daemon user ${daemonUser} to ${expectedGroup}...`)
548
483
  );
549
- if (this.addUserToGroup(daemonUser, expectedGroup, dryRun)) {
484
+ if (await execCmd(UnixGroupCommands.addUserToGroup(daemonUser, expectedGroup))) {
550
485
  daemonMembershipsAdded++;
551
486
  this.log(chalk.green(` \u2713 Added daemon user to ${expectedGroup}`));
552
487
  } else {
@@ -564,7 +499,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
564
499
  const repoGroup = repoGroupMap.get(wt.repo_id) || generateRepoGroupName(wt.repo_id);
565
500
  result.groups.expected.push(repoGroup);
566
501
  const isInRepoGroup = result.groups.actual.includes(repoGroup);
567
- const repoGroupExistsOnSystem = this.groupExists(repoGroup);
502
+ const repoGroupExistsOnSystem = groupExists(repoGroup);
568
503
  if (verbose) {
569
504
  this.log(
570
505
  chalk.gray(
@@ -575,7 +510,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
575
510
  let repoGroupReady = repoGroupExistsOnSystem;
576
511
  if (!repoGroupExistsOnSystem) {
577
512
  this.log(chalk.yellow(` \u2192 Creating repo group ${repoGroup}...`));
578
- if (this.createGroup(repoGroup, dryRun)) {
513
+ if (await execCmd(UnixGroupCommands.createGroup(repoGroup))) {
579
514
  groupsCreated++;
580
515
  repoGroupReady = true;
581
516
  this.log(chalk.green(` \u2713 Created repo group ${repoGroup}`));
@@ -586,7 +521,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
586
521
  }
587
522
  if (repoGroupReady && !isInRepoGroup) {
588
523
  this.log(chalk.yellow(` \u2192 Adding to repo group ${repoGroup}...`));
589
- if (this.addUserToGroup(user.unix_username, repoGroup, dryRun)) {
524
+ if (await execCmd(UnixGroupCommands.addUserToGroup(user.unix_username, repoGroup))) {
590
525
  result.groups.added.push(repoGroup);
591
526
  this.log(chalk.green(` \u2713 Added to ${repoGroup}`));
592
527
  } else {
@@ -595,12 +530,12 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
595
530
  }
596
531
  }
597
532
  if (repoGroupReady && daemonUser) {
598
- const daemonInRpGroup = dryRun ? false : this.isUserInGroup(daemonUser, repoGroup);
533
+ const daemonInRpGroup = dryRun ? false : isUserInGroup(daemonUser, repoGroup);
599
534
  if (!daemonInRpGroup) {
600
535
  this.log(
601
536
  chalk.yellow(` \u2192 Adding daemon user ${daemonUser} to ${repoGroup}...`)
602
537
  );
603
- if (this.addUserToGroup(daemonUser, repoGroup, dryRun)) {
538
+ if (await execCmd(UnixGroupCommands.addUserToGroup(daemonUser, repoGroup))) {
604
539
  daemonMembershipsAdded++;
605
540
  this.log(chalk.green(` \u2713 Added daemon user to ${repoGroup}`));
606
541
  } else {
@@ -616,11 +551,118 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
616
551
  this.log("");
617
552
  }
618
553
  }
554
+ this.log(chalk.cyan.bold("\n\u2501\u2501\u2501 Sync Worktree Groups \u2501\u2501\u2501\n"));
555
+ const allWorktreesForBackfill = targetWorktreeId ? await select(db).from(worktrees).where(eq(worktrees.worktree_id, targetWorktreeId)).all() : await select(db).from(worktrees).all();
556
+ const worktreesForGroupSync = allWorktreesForBackfill.filter(
557
+ (wt) => !(wt.archived && wt.filesystem_status === "deleted")
558
+ );
559
+ if (worktreesForGroupSync.length === 0) {
560
+ this.log(chalk.yellow(" No active worktrees in scope\n"));
561
+ } else {
562
+ this.log(chalk.cyan(`Processing ${worktreesForGroupSync.length} worktree(s)
563
+ `));
564
+ for (const wt of worktreesForGroupSync) {
565
+ const rawWt = wt;
566
+ const expectedGroup = rawWt.unix_group || generateWorktreeGroupName(rawWt.worktree_id);
567
+ const dbNeedsBackfill = rawWt.unix_group === null;
568
+ const groupMissingOnSystem = !groupExists(expectedGroup);
569
+ if (!dbNeedsBackfill && !groupMissingOnSystem && !verbose) {
570
+ if (daemonUser && !isUserInGroup(daemonUser, expectedGroup)) {
571
+ this.log(chalk.bold(`\u{1F4C1} ${rawWt.name}`));
572
+ this.log(
573
+ chalk.yellow(` \u2192 Adding daemon user ${daemonUser} to ${expectedGroup}...`)
574
+ );
575
+ if (await execCmd(UnixGroupCommands.addUserToGroup(daemonUser, expectedGroup))) {
576
+ daemonMembershipsAdded++;
577
+ this.log(chalk.green(` \u2713 Added daemon user to ${expectedGroup}
578
+ `));
579
+ } else {
580
+ syncErrors++;
581
+ this.log(chalk.red(` \u2717 Failed to add daemon user to ${expectedGroup}
582
+ `));
583
+ }
584
+ }
585
+ continue;
586
+ }
587
+ this.log(chalk.bold(`\u{1F4C1} ${rawWt.name}`));
588
+ this.log(chalk.gray(` worktree_id: ${rawWt.worktree_id.substring(0, 8)}`));
589
+ this.log(
590
+ chalk.gray(` unix_group: ${expectedGroup}${dbNeedsBackfill ? " (to backfill)" : ""}`)
591
+ );
592
+ let hadError = false;
593
+ if (groupMissingOnSystem) {
594
+ this.log(chalk.yellow(` \u2192 Creating Unix group ${expectedGroup}...`));
595
+ if (await execCmd(UnixGroupCommands.createGroup(expectedGroup))) {
596
+ groupsCreated++;
597
+ this.log(chalk.green(` \u2713 Created Unix group ${expectedGroup}`));
598
+ } else {
599
+ syncErrors++;
600
+ hadError = true;
601
+ this.log(chalk.red(` \u2717 Failed to create Unix group ${expectedGroup}`));
602
+ }
603
+ } else if (verbose) {
604
+ this.log(chalk.gray(` \u2713 Unix group exists`));
605
+ }
606
+ if (!hadError && daemonUser) {
607
+ const daemonInGroup = dryRun ? false : isUserInGroup(daemonUser, expectedGroup);
608
+ if (!daemonInGroup) {
609
+ this.log(
610
+ chalk.yellow(` \u2192 Adding daemon user ${daemonUser} to ${expectedGroup}...`)
611
+ );
612
+ if (await execCmd(UnixGroupCommands.addUserToGroup(daemonUser, expectedGroup))) {
613
+ daemonMembershipsAdded++;
614
+ this.log(chalk.green(` \u2713 Added daemon user to ${expectedGroup}`));
615
+ } else {
616
+ syncErrors++;
617
+ this.log(chalk.red(` \u2717 Failed to add daemon user to ${expectedGroup}`));
618
+ }
619
+ } else if (verbose) {
620
+ this.log(chalk.gray(` \u2713 Daemon user already in ${expectedGroup}`));
621
+ }
622
+ }
623
+ if (!hadError && dbNeedsBackfill) {
624
+ if (dryRun) {
625
+ this.log(
626
+ chalk.gray(
627
+ ` [dry-run] Would update database: SET unix_group = '${expectedGroup}' WHERE worktree_id = '${rawWt.worktree_id}'`
628
+ )
629
+ );
630
+ worktreesBackfilled++;
631
+ } else {
632
+ try {
633
+ await update(db, worktrees).set({ unix_group: expectedGroup }).where(eq(worktrees.worktree_id, rawWt.worktree_id)).run();
634
+ worktreesBackfilled++;
635
+ this.log(chalk.green(` \u2713 Backfilled unix_group in database`));
636
+ } catch (error) {
637
+ syncErrors++;
638
+ this.log(chalk.red(` \u2717 Failed to update database: ${error}`));
639
+ }
640
+ }
641
+ }
642
+ this.log("");
643
+ }
644
+ if (worktreesBackfilled > 0 || groupsCreated > 0 || daemonMembershipsAdded > 0) {
645
+ this.log(chalk.bold("Sync Worktree Groups Summary:"));
646
+ this.log(` DB backfilled: ${worktreesBackfilled}${dryRun ? " (dry-run)" : ""}`);
647
+ this.log("");
648
+ }
649
+ }
619
650
  this.log(chalk.cyan.bold("\n\u2501\u2501\u2501 Sync Worktree Permissions \u2501\u2501\u2501\n"));
620
- const allWorktreesForSync = await select(db).from(worktrees).all();
651
+ const allWorktreesForSync = targetWorktreeId ? await select(db).from(worktrees).where(eq(worktrees.worktree_id, targetWorktreeId)).all() : await select(db).from(worktrees).all();
621
652
  const worktreesWithGroup = allWorktreesForSync.filter(
622
653
  (wt) => wt.unix_group !== null
623
654
  );
655
+ const allReposForWtSync = await select(db).from(repos).all();
656
+ const repoPathMap = /* @__PURE__ */ new Map();
657
+ for (const repo of allReposForWtSync) {
658
+ const r = repo;
659
+ if (r.data?.local_path) {
660
+ repoPathMap.set(r.repo_id, {
661
+ localPath: r.data.local_path,
662
+ defaultBranch: r.data.default_branch || "main"
663
+ });
664
+ }
665
+ }
624
666
  if (worktreesWithGroup.length === 0) {
625
667
  this.log(chalk.yellow("No worktrees with unix_group found\n"));
626
668
  } else {
@@ -630,141 +672,398 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
630
672
  const rawWorktree = wt;
631
673
  const worktreePath = rawWorktree.data?.path;
632
674
  if (!worktreePath) {
633
- this.log(chalk.yellow(`\u{1F4C1} ${rawWorktree.name}`));
675
+ if (verbose) {
676
+ this.log(chalk.gray(` \u26A0 ${rawWorktree.name}: no path in data, skipping`));
677
+ }
678
+ worktreesSkipped++;
679
+ continue;
680
+ }
681
+ const dirExists = existsSync(worktreePath);
682
+ const action = getWorktreeDirectoryAction(
683
+ dirExists,
684
+ rawWorktree.archived,
685
+ rawWorktree.filesystem_status
686
+ );
687
+ if (action === "cleanup") {
688
+ const wtGroup = rawWorktree.unix_group;
689
+ if (groupExists(wtGroup)) {
690
+ this.log(
691
+ chalk.yellow(
692
+ ` \u{1F9F9} ${rawWorktree.name}: archived+deleted, removing group ${wtGroup}...`
693
+ )
694
+ );
695
+ if (await execCmd(UnixGroupCommands.deleteGroup(wtGroup))) {
696
+ groupsCleaned++;
697
+ this.log(chalk.green(` \u2713 Deleted group ${wtGroup}`));
698
+ } else {
699
+ syncErrors++;
700
+ this.log(chalk.red(` \u2717 Failed to delete group ${wtGroup}`));
701
+ }
702
+ } else if (verbose) {
703
+ this.log(
704
+ chalk.gray(
705
+ ` \u2298 ${rawWorktree.name}: archived+deleted, group ${wtGroup} already gone`
706
+ )
707
+ );
708
+ }
709
+ continue;
710
+ }
711
+ if (action === "skip") {
712
+ if (verbose) {
713
+ const reason = rawWorktree.filesystem_status === "creating" ? "still creating" : rawWorktree.archived && !dirExists ? `archived (${rawWorktree.filesystem_status || "unknown"}), dir missing` : "unknown";
714
+ this.log(chalk.gray(` \u2298 ${rawWorktree.name}: ${reason}, skipping`));
715
+ }
716
+ worktreesSkipped++;
717
+ continue;
718
+ }
719
+ if (action === "restore") {
720
+ const repoInfo = repoPathMap.get(rawWorktree.repo_id);
721
+ if (!repoInfo) {
722
+ if (verbose) {
723
+ this.log(
724
+ chalk.gray(
725
+ ` \u2298 ${rawWorktree.name}: failed, no repo path found, skipping restore`
726
+ )
727
+ );
728
+ }
729
+ worktreesSkipped++;
730
+ continue;
731
+ }
732
+ const baseRef = rawWorktree.data?.base_ref || repoInfo.defaultBranch;
733
+ this.log(chalk.bold(`\u{1F527} ${rawWorktree.name}`));
634
734
  this.log(chalk.gray(` worktree_id: ${rawWorktree.worktree_id.substring(0, 8)}`));
635
- this.log(chalk.gray(` unix_group: ${rawWorktree.unix_group}`));
636
- this.log(chalk.red(` \u26A0 No path found in worktree data, skipping
637
- `));
735
+ this.log(chalk.gray(` status: failed \u2192 attempting restore`));
736
+ this.log(chalk.gray(` ref: ${rawWorktree.ref}, base: ${baseRef}`));
737
+ this.log(chalk.gray(` path: ${worktreePath}`));
738
+ if (dryRun) {
739
+ this.log(
740
+ chalk.gray(
741
+ ` [dry-run] Would attempt restoreWorktreeFilesystem() for ${rawWorktree.ref} at ${worktreePath}`
742
+ )
743
+ );
744
+ worktreesRestored++;
745
+ this.log("");
746
+ continue;
747
+ }
748
+ this.log(chalk.yellow(` \u2192 Restoring worktree filesystem...`));
749
+ const result = await restoreWorktreeFilesystem(
750
+ repoInfo.localPath,
751
+ worktreePath,
752
+ rawWorktree.ref,
753
+ baseRef
754
+ );
755
+ if (result.success) {
756
+ await update(db, worktrees).set({ filesystem_status: "ready" }).where(eq(worktrees.worktree_id, rawWorktree.worktree_id)).run();
757
+ worktreesRestored++;
758
+ this.log(chalk.green(` \u2713 Restored worktree (${result.strategy}), status \u2192 ready`));
759
+ } else {
760
+ syncErrors++;
761
+ this.log(chalk.red(` \u2717 Failed to restore worktree: ${result.error}`));
762
+ }
763
+ this.log("");
638
764
  continue;
639
765
  }
640
766
  this.log(chalk.bold(`\u{1F4C1} ${rawWorktree.name}`));
641
767
  this.log(chalk.gray(` worktree_id: ${rawWorktree.worktree_id.substring(0, 8)}`));
642
768
  this.log(chalk.gray(` unix_group: ${rawWorktree.unix_group}`));
643
769
  this.log(chalk.gray(` path: ${worktreePath}`));
770
+ if (rawWorktree.archived) {
771
+ this.log(
772
+ chalk.gray(` archived: yes (fs: ${rawWorktree.filesystem_status || "preserved"})`)
773
+ );
774
+ }
775
+ if (action === "create") {
776
+ const repoInfo = repoPathMap.get(rawWorktree.repo_id);
777
+ if (repoInfo) {
778
+ const baseRef = rawWorktree.data?.base_ref || repoInfo.defaultBranch;
779
+ this.log(
780
+ chalk.yellow(
781
+ ` \u2192 Directory missing, creating git worktree (branch: ${rawWorktree.ref}, base: ${baseRef})...`
782
+ )
783
+ );
784
+ if (dryRun) {
785
+ worktreeDirsCreated++;
786
+ this.log(
787
+ chalk.gray(
788
+ ` [dry-run] Would run restoreWorktreeFilesystem() for ${rawWorktree.ref} at ${worktreePath}`
789
+ )
790
+ );
791
+ } else {
792
+ const result = await restoreWorktreeFilesystem(
793
+ repoInfo.localPath,
794
+ worktreePath,
795
+ rawWorktree.ref,
796
+ baseRef
797
+ );
798
+ if (result.success) {
799
+ worktreeDirsCreated++;
800
+ this.log(chalk.green(` \u2713 Created git worktree (${result.strategy})`));
801
+ } else {
802
+ this.log(
803
+ chalk.yellow(
804
+ ` \u26A0 git worktree add failed (${result.error}), falling back to mkdir -p`
805
+ )
806
+ );
807
+ if (await execCmd(`sudo -n mkdir -p "${worktreePath}"`)) {
808
+ worktreeDirsCreated++;
809
+ this.log(chalk.green(` \u2713 Created directory (mkdir fallback)`));
810
+ } else {
811
+ syncErrors++;
812
+ this.log(chalk.red(` \u2717 Failed to create directory`));
813
+ this.log("");
814
+ continue;
815
+ }
816
+ }
817
+ }
818
+ } else {
819
+ this.log(
820
+ chalk.yellow(` \u2192 Directory missing, creating (no repo path for git worktree)...`)
821
+ );
822
+ if (await execCmd(`sudo -n mkdir -p "${worktreePath}"`)) {
823
+ worktreeDirsCreated++;
824
+ this.log(chalk.green(` \u2713 Created directory`));
825
+ } else {
826
+ syncErrors++;
827
+ this.log(chalk.red(` \u2717 Failed to create directory`));
828
+ this.log("");
829
+ continue;
830
+ }
831
+ }
832
+ }
833
+ if (action === "sync" && !rawWorktree.archived && (rawWorktree.filesystem_status === "deleted" || rawWorktree.filesystem_status === "preserved")) {
834
+ const gitFilePath = join(worktreePath, ".git");
835
+ if (existsSync(gitFilePath)) {
836
+ const oldStatus = rawWorktree.filesystem_status;
837
+ this.log(chalk.yellow(` \u2192 Fixing filesystem_status: ${oldStatus} \u2192 ready`));
838
+ if (!dryRun) {
839
+ try {
840
+ await update(db, worktrees).set({ filesystem_status: "ready" }).where(eq(worktrees.worktree_id, rawWorktree.worktree_id)).run();
841
+ this.log(
842
+ chalk.green(
843
+ ` \u2713 Fixed filesystem_status: ${oldStatus} \u2192 ready for ${rawWorktree.name}`
844
+ )
845
+ );
846
+ } catch (error) {
847
+ syncErrors++;
848
+ this.log(chalk.red(` \u2717 Failed to fix filesystem_status: ${error}`));
849
+ }
850
+ } else {
851
+ this.log(
852
+ chalk.gray(
853
+ ` [dry-run] Would fix filesystem_status: ${oldStatus} \u2192 ready for ${rawWorktree.name}`
854
+ )
855
+ );
856
+ }
857
+ statusFixed++;
858
+ }
859
+ }
644
860
  const othersAccess = rawWorktree.others_fs_access || "read";
645
861
  const permissionMode = getWorktreePermissionMode(othersAccess);
646
862
  this.log(chalk.gray(` others_fs_access: ${othersAccess} \u2192 mode: ${permissionMode}`));
647
- if (dryRun) {
648
- this.log(
649
- chalk.gray(
650
- ` [dry-run] Would run: chgrp -R ${rawWorktree.unix_group} "${worktreePath}"`
651
- )
652
- );
653
- this.log(
654
- chalk.gray(` [dry-run] Would run: chmod -R ${permissionMode} "${worktreePath}"`)
655
- );
656
- this.log("");
863
+ const permCmds = UnixGroupCommands.setDirectoryGroup(
864
+ worktreePath,
865
+ rawWorktree.unix_group,
866
+ permissionMode
867
+ );
868
+ if (await execAllCmds(permCmds)) {
869
+ worktreesSynced++;
870
+ this.log(chalk.green(` \u2713 Applied permissions (${permissionMode})`));
657
871
  } else {
658
- try {
659
- for (const cmd of UnixGroupCommands.setDirectoryGroup(
660
- worktreePath,
661
- rawWorktree.unix_group,
662
- permissionMode
663
- )) {
664
- execSync(cmd, { stdio: "pipe" });
872
+ syncErrors++;
873
+ this.log(chalk.red(` \u2717 Failed to set permissions`));
874
+ }
875
+ if (daemonUser && (dirExists || action === "create")) {
876
+ const aclCmds = UnixGroupCommands.setUserAcl(worktreePath, daemonUser);
877
+ if (await execAllCmds(aclCmds)) {
878
+ daemonAclsApplied++;
879
+ if (verbose) {
880
+ this.log(chalk.green(` \u2713 Applied daemon ACL for ${daemonUser}`));
665
881
  }
666
- worktreesSynced++;
667
- this.log(chalk.green(` \u2713 Applied permissions (${permissionMode})
668
- `));
669
- } catch (error) {
882
+ } else {
670
883
  syncErrors++;
671
- this.log(chalk.red(` \u2717 Failed: ${error}
672
- `));
884
+ this.log(chalk.red(` \u2717 Failed to set daemon ACL`));
673
885
  }
674
886
  }
887
+ this.log("");
675
888
  }
676
889
  this.log(chalk.bold("Worktree Sync Summary:"));
677
890
  this.log(` Worktrees synced: ${worktreesSynced}${dryRun ? " (dry-run)" : ""}`);
891
+ this.log(` Directories created: ${worktreeDirsCreated}${dryRun ? " (dry-run)" : ""}`);
892
+ this.log(` Worktrees restored: ${worktreesRestored}${dryRun ? " (dry-run)" : ""}`);
893
+ this.log(` Groups cleaned: ${groupsCleaned}${dryRun ? " (dry-run)" : ""}`);
894
+ this.log(` Status fixed: ${statusFixed}${dryRun ? " (dry-run)" : ""}`);
895
+ this.log(` Daemon ACLs applied: ${daemonAclsApplied}${dryRun ? " (dry-run)" : ""}`);
896
+ this.log(` Skipped: ${worktreesSkipped}`);
678
897
  if (syncErrors > 0) {
679
898
  this.log(chalk.red(` Errors: ${syncErrors}`));
680
899
  }
681
900
  this.log("");
682
901
  }
683
- this.log(chalk.cyan.bold("\n\u2501\u2501\u2501 Sync Repo Permissions \u2501\u2501\u2501\n"));
684
- const allReposForSync = await select(db).from(repos).all();
685
- const reposWithGroup = allReposForSync.filter(
686
- (r) => r.unix_group !== null
687
- );
688
- if (reposWithGroup.length === 0) {
689
- this.log(chalk.yellow("No repos with unix_group found\n"));
902
+ if (targetWorktreeId) {
903
+ this.log(chalk.gray(" \u2298 Skipping membership pruning phase (--worktree-id mode)\n"));
690
904
  } else {
691
- this.log(chalk.cyan(`Found ${reposWithGroup.length} repo(s) with unix_group
692
- `));
693
- for (const repo of reposWithGroup) {
694
- const rawRepo = repo;
695
- const repoPath = rawRepo.data?.local_path;
696
- if (!repoPath) {
697
- this.log(chalk.yellow(`\u{1F4C1} ${rawRepo.slug}`));
698
- this.log(chalk.gray(` repo_id: ${rawRepo.repo_id.substring(0, 8)}`));
699
- this.log(chalk.gray(` unix_group: ${rawRepo.unix_group}`));
700
- this.log(chalk.red(` \u26A0 No local_path found in repo data, skipping
701
- `));
702
- continue;
905
+ this.log(chalk.cyan.bold("\n\u2501\u2501\u2501 Prune Stale Group Memberships \u2501\u2501\u2501\n"));
906
+ {
907
+ const allWtForPrune = await select(db).from(worktrees).all();
908
+ const allOwnerRows = await select(db).from(worktreeOwners).all();
909
+ const wtGroupMap = /* @__PURE__ */ new Map();
910
+ for (const wt of allWtForPrune) {
911
+ const raw = wt;
912
+ if (raw.unix_group) {
913
+ wtGroupMap.set(raw.worktree_id, raw.unix_group);
914
+ }
703
915
  }
704
- const gitPath = `${repoPath}/.git`;
705
- this.log(chalk.bold(`\u{1F4C1} ${rawRepo.slug}`));
706
- this.log(chalk.gray(` repo_id: ${rawRepo.repo_id.substring(0, 8)}`));
707
- this.log(chalk.gray(` unix_group: ${rawRepo.unix_group}`));
708
- this.log(chalk.gray(` .git path: ${gitPath}`));
709
- this.log(chalk.gray(` mode: ${REPO_GIT_PERMISSION_MODE} (setgid, owner+group rwx)`));
710
- if (daemonUser) {
711
- const daemonInThisRepoGroup = dryRun ? false : this.isUserInGroup(daemonUser, rawRepo.unix_group);
712
- if (!daemonInThisRepoGroup) {
713
- this.log(
714
- chalk.yellow(` \u2192 Adding daemon user ${daemonUser} to ${rawRepo.unix_group}...`)
715
- );
716
- if (this.addUserToGroup(daemonUser, rawRepo.unix_group, dryRun)) {
717
- daemonMembershipsAdded++;
718
- this.log(chalk.green(` \u2713 Added daemon user to ${rawRepo.unix_group}`));
916
+ const groupToOwnerIds = /* @__PURE__ */ new Map();
917
+ for (const row of allOwnerRows) {
918
+ const raw = row;
919
+ const group = wtGroupMap.get(raw.worktree_id);
920
+ if (group) {
921
+ const owners = groupToOwnerIds.get(group) || /* @__PURE__ */ new Set();
922
+ owners.add(raw.user_id);
923
+ groupToOwnerIds.set(group, owners);
924
+ }
925
+ }
926
+ const allUsersForPrune = await select(db).from(users).all();
927
+ const userIdToUnixName = /* @__PURE__ */ new Map();
928
+ const unixNameToUserId = /* @__PURE__ */ new Map();
929
+ for (const u of allUsersForPrune) {
930
+ if (u.unix_username) {
931
+ userIdToUnixName.set(u.user_id, u.unix_username);
932
+ unixNameToUserId.set(u.unix_username, u.user_id);
933
+ }
934
+ }
935
+ let pruneChecked = 0;
936
+ for (const [, group] of wtGroupMap.entries()) {
937
+ if (!groupExists(group)) continue;
938
+ pruneChecked++;
939
+ const ownerIds = groupToOwnerIds.get(group) || /* @__PURE__ */ new Set();
940
+ const expectedUsernames = /* @__PURE__ */ new Set();
941
+ for (const ownerId of ownerIds) {
942
+ const uname = userIdToUnixName.get(ownerId);
943
+ if (uname) expectedUsernames.add(uname);
944
+ }
945
+ if (daemonUser) expectedUsernames.add(daemonUser);
946
+ const actualMembers = getGroupMembers(group);
947
+ for (const member of actualMembers) {
948
+ if (expectedUsernames.has(member)) continue;
949
+ if (daemonUser && member === daemonUser) continue;
950
+ if (!unixNameToUserId.has(member)) continue;
951
+ this.log(chalk.yellow(` \u2192 Removing ${member} from ${group} (no longer owner)`));
952
+ if (await execCmd(UnixGroupCommands.removeUserFromGroup(member, group))) {
953
+ membershipsRemoved++;
954
+ this.log(chalk.green(` \u2713 Removed ${member} from ${group}`));
719
955
  } else {
720
- this.log(chalk.red(` \u2717 Failed to add daemon user to ${rawRepo.unix_group}`));
956
+ syncErrors++;
957
+ this.log(chalk.red(` \u2717 Failed to remove ${member} from ${group}`));
721
958
  }
722
- } else if (verbose) {
723
- this.log(chalk.gray(` \u2713 Daemon user already in ${rawRepo.unix_group}`));
724
959
  }
725
960
  }
726
- if (dryRun) {
961
+ if (membershipsRemoved === 0) {
727
962
  this.log(
728
- chalk.gray(` [dry-run] Would run: chgrp -R ${rawRepo.unix_group} "${gitPath}"`)
729
- );
730
- this.log(
731
- chalk.gray(
732
- ` [dry-run] Would run: chmod -R ${REPO_GIT_PERMISSION_MODE} "${gitPath}"`
733
- )
963
+ chalk.green(` \u2713 No stale memberships found (checked ${pruneChecked} groups)
964
+ `)
734
965
  );
735
- this.log("");
736
966
  } else {
967
+ this.log("");
968
+ this.log(chalk.bold("Membership Pruning Summary:"));
969
+ this.log(` Memberships removed: ${membershipsRemoved}${dryRun ? " (dry-run)" : ""}`);
970
+ this.log("");
971
+ }
972
+ }
973
+ }
974
+ if (targetWorktreeId) {
975
+ this.log(chalk.gray(" \u2298 Skipping symlink sync phase (--worktree-id mode)\n"));
976
+ } else if (validUsers.length > 0) {
977
+ this.log(chalk.cyan.bold("\n\u2501\u2501\u2501 Sync User Symlinks \u2501\u2501\u2501\n"));
978
+ const allWtForSymlinks = await select(db).from(worktrees).all();
979
+ const allOwnershipsForSymlinks = await select(db).from(worktreeOwners).all();
980
+ const wtInfoMap = /* @__PURE__ */ new Map();
981
+ for (const wt of allWtForSymlinks) {
982
+ const raw = wt;
983
+ wtInfoMap.set(raw.worktree_id, {
984
+ name: raw.name,
985
+ path: raw.data?.path,
986
+ archived: raw.archived,
987
+ filesystem_status: raw.filesystem_status
988
+ });
989
+ }
990
+ const userToWorktrees = /* @__PURE__ */ new Map();
991
+ for (const row of allOwnershipsForSymlinks) {
992
+ const raw = row;
993
+ const existing = userToWorktrees.get(raw.user_id) || [];
994
+ existing.push(raw.worktree_id);
995
+ userToWorktrees.set(raw.user_id, existing);
996
+ }
997
+ for (const user of validUsers) {
998
+ const worktreesDir = getUserWorktreesDir(user.unix_username);
999
+ if (verbose) {
1000
+ this.log(chalk.gray(` ${user.unix_username}: checking symlinks...`));
1001
+ }
1002
+ if (!existsSync(worktreesDir)) {
1003
+ const setupCmds = UnixUserCommands.setupWorktreesDir(user.unix_username);
1004
+ if (!await execAllCmds(setupCmds)) {
1005
+ if (verbose) {
1006
+ this.log(chalk.gray(` \u26A0 Could not create ${worktreesDir}`));
1007
+ }
1008
+ continue;
1009
+ }
1010
+ }
1011
+ if (existsSync(worktreesDir)) {
1012
+ await execCmd(SymlinkCommands.removeBrokenSymlinks(worktreesDir));
1013
+ symlinksCleaned++;
1014
+ }
1015
+ const ownedWtIds = userToWorktrees.get(user.user_id) || [];
1016
+ for (const wtId of ownedWtIds) {
1017
+ const wtInfo = wtInfoMap.get(wtId);
1018
+ if (!wtInfo?.path) continue;
1019
+ if (wtInfo.archived && wtInfo.filesystem_status === "deleted") continue;
1020
+ if (!existsSync(wtInfo.path)) continue;
1021
+ const symlinkPath = getWorktreeSymlinkPath(user.unix_username, wtInfo.name);
1022
+ let needsCreate = true;
737
1023
  try {
738
- for (const cmd of UnixGroupCommands.setDirectoryGroup(
739
- gitPath,
740
- rawRepo.unix_group,
741
- REPO_GIT_PERMISSION_MODE
742
- )) {
743
- execSync(cmd, { stdio: "pipe" });
1024
+ const currentTarget = readlinkSync(symlinkPath);
1025
+ if (currentTarget === wtInfo.path) {
1026
+ needsCreate = false;
1027
+ }
1028
+ } catch {
1029
+ }
1030
+ if (!needsCreate) continue;
1031
+ const symlinkCmds = SymlinkCommands.createSymlinkWithOwnership(
1032
+ wtInfo.path,
1033
+ symlinkPath,
1034
+ user.unix_username
1035
+ ).map((cmd) => `sudo -n ${cmd}`);
1036
+ if (await execAllCmds(symlinkCmds)) {
1037
+ symlinksCreated++;
1038
+ if (verbose) {
1039
+ this.log(
1040
+ chalk.green(` \u2713 ${user.unix_username}: ${wtInfo.name} \u2192 ${wtInfo.path}`)
1041
+ );
1042
+ }
1043
+ } else {
1044
+ if (verbose) {
1045
+ this.log(chalk.red(` \u2717 Failed to create symlink for ${wtInfo.name}`));
744
1046
  }
745
- reposPermSynced++;
746
- this.log(
747
- chalk.green(` \u2713 Applied .git permissions (${REPO_GIT_PERMISSION_MODE})
748
- `)
749
- );
750
- } catch (error) {
751
1047
  syncErrors++;
752
- this.log(chalk.red(` \u2717 Failed: ${error}
753
- `));
754
1048
  }
755
1049
  }
756
1050
  }
757
- this.log(chalk.bold("Repo Permission Sync Summary:"));
758
- this.log(` Repos synced: ${reposPermSynced}${dryRun ? " (dry-run)" : ""}`);
759
- if (syncErrors > 0) {
760
- this.log(chalk.red(` Errors: ${syncErrors}`));
1051
+ if (symlinksCreated > 0 || symlinksCleaned > 0) {
1052
+ this.log("");
1053
+ this.log(chalk.bold("Symlink Sync Summary:"));
1054
+ this.log(` Symlinks created: ${symlinksCreated}${dryRun ? " (dry-run)" : ""}`);
1055
+ this.log(` Users cleaned: ${symlinksCleaned}${dryRun ? " (dry-run)" : ""}`);
1056
+ this.log("");
1057
+ } else {
1058
+ this.log(chalk.green(" \u2713 All symlinks up to date\n"));
761
1059
  }
762
- this.log("");
763
1060
  }
764
- if (cleanupGroups || cleanupUsers) {
1061
+ if (targetWorktreeId && (cleanupGroups || cleanupUsers)) {
1062
+ this.log(chalk.gray(" \u2298 Skipping cleanup phase (--worktree-id mode)\n"));
1063
+ } else if (cleanupGroups || cleanupUsers) {
765
1064
  this.log(chalk.cyan.bold("\u2501\u2501\u2501 Cleanup \u2501\u2501\u2501\n"));
766
1065
  }
767
- if (cleanupGroups) {
1066
+ if (cleanupGroups && !targetWorktreeId) {
768
1067
  this.log(chalk.cyan("Checking for stale worktree groups...\n"));
769
1068
  const allWorktrees = await select(db).from(worktrees).all();
770
1069
  const expectedGroups = new Set(
@@ -772,7 +1071,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
772
1071
  (wt) => wt.unix_group || generateWorktreeGroupName(wt.worktree_id)
773
1072
  )
774
1073
  );
775
- const systemGroups = this.listWorktreeGroups();
1074
+ const systemGroups = listWorktreeGroups();
776
1075
  if (verbose) {
777
1076
  this.log(chalk.gray(` Found ${systemGroups.length} agor_wt_* group(s) on system`));
778
1077
  this.log(chalk.gray(` Expected ${expectedGroups.size} group(s) from database`));
@@ -785,7 +1084,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
785
1084
  `));
786
1085
  for (const groupName of staleGroups) {
787
1086
  this.log(chalk.yellow(` \u2192 Deleting group ${groupName}...`));
788
- if (this.deleteGroup(groupName, dryRun)) {
1087
+ if (await execCmd(UnixGroupCommands.deleteGroup(groupName))) {
789
1088
  groupsDeleted++;
790
1089
  this.log(chalk.green(` \u2713 Deleted ${groupName}`));
791
1090
  } else {
@@ -802,7 +1101,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
802
1101
  (r) => r.unix_group || generateRepoGroupName(r.repo_id)
803
1102
  )
804
1103
  );
805
- const systemRepoGroups = this.listRepoGroups();
1104
+ const systemRepoGroups = listRepoGroups();
806
1105
  if (verbose) {
807
1106
  this.log(chalk.gray(` Found ${systemRepoGroups.length} agor_rp_* group(s) on system`));
808
1107
  this.log(chalk.gray(` Expected ${expectedRepoGroups.size} group(s) from database`));
@@ -817,7 +1116,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
817
1116
  );
818
1117
  for (const groupName of staleRepoGroups) {
819
1118
  this.log(chalk.yellow(` \u2192 Deleting group ${groupName}...`));
820
- if (this.deleteGroup(groupName, dryRun)) {
1119
+ if (await execCmd(UnixGroupCommands.deleteGroup(groupName))) {
821
1120
  groupsDeleted++;
822
1121
  this.log(chalk.green(` \u2713 Deleted ${groupName}`));
823
1122
  } else {
@@ -828,12 +1127,12 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
828
1127
  this.log("");
829
1128
  }
830
1129
  }
831
- if (cleanupUsers) {
1130
+ if (cleanupUsers && !targetWorktreeId) {
832
1131
  this.log(chalk.cyan("Checking for stale Agor users...\n"));
833
1132
  const expectedUsers = new Set(
834
1133
  validUsers.map((u) => u.unix_username).filter((u) => /^agor_[0-9a-f]{8}$/.test(u))
835
1134
  );
836
- const systemUsers = this.listAgorUsers();
1135
+ const systemUsers = listAgorUsers();
837
1136
  if (verbose) {
838
1137
  this.log(chalk.gray(` Found ${systemUsers.length} agor_* user(s) on system`));
839
1138
  this.log(chalk.gray(` Expected ${expectedUsers.size} user(s) from database`));
@@ -847,7 +1146,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
847
1146
  this.log(chalk.gray(" Note: Home directories will be kept\n"));
848
1147
  for (const username of staleUsers) {
849
1148
  this.log(chalk.yellow(` \u2192 Deleting user ${username}...`));
850
- if (this.deleteUser(username, dryRun)) {
1149
+ if (await execCmd(UnixUserCommands.deleteUser(username))) {
851
1150
  usersDeleted++;
852
1151
  this.log(chalk.green(` \u2713 Deleted ${username}`));
853
1152
  } else {
@@ -869,15 +1168,28 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
869
1168
  this.log(` Users created: ${usersCreated}${dryRunSuffix}`);
870
1169
  this.log(` Groups created: ${groupsCreated}${dryRunSuffix}`);
871
1170
  this.log(` Memberships added: ${groupsAdded}${dryRunSuffix}`);
1171
+ this.log(` Memberships removed: ${membershipsRemoved}${dryRunSuffix}`);
872
1172
  if (daemonUser) {
873
1173
  this.log(` Daemon memberships: ${daemonMembershipsAdded}${dryRunSuffix}`);
874
1174
  }
875
1175
  this.log("");
876
1176
  this.log(chalk.bold("Filesystem Sync:"));
1177
+ this.log(` WT groups backfilled: ${worktreesBackfilled}${dryRunSuffix}`);
877
1178
  this.log(` Worktrees synced: ${worktreesSynced}${dryRunSuffix}`);
1179
+ this.log(` Dirs created: ${worktreeDirsCreated}${dryRunSuffix}`);
1180
+ this.log(` Worktrees restored:${worktreesRestored}${dryRunSuffix}`);
1181
+ this.log(` Groups cleaned: ${groupsCleaned}${dryRunSuffix}`);
1182
+ this.log(` Status fixed: ${statusFixed}${dryRunSuffix}`);
1183
+ this.log(` Skipped: ${worktreesSkipped}`);
1184
+ this.log(` Daemon ACLs: ${daemonAclsApplied}${dryRunSuffix}`);
878
1185
  this.log(` Repos backfilled: ${reposBackfilled}${dryRunSuffix}`);
879
1186
  this.log(` Repo perms synced: ${reposPermSynced}${dryRunSuffix}`);
1187
+ this.log("");
1188
+ this.log(chalk.bold("Symlinks:"));
1189
+ this.log(` Created: ${symlinksCreated}${dryRunSuffix}`);
1190
+ this.log(` Users cleaned: ${symlinksCleaned}${dryRunSuffix}`);
880
1191
  if (syncErrors > 0) {
1192
+ this.log("");
881
1193
  this.log(chalk.red(` Sync errors: ${syncErrors}`));
882
1194
  }
883
1195
  if (cleanupGroups || cleanupUsers) {
@@ -894,7 +1206,7 @@ Hint: Running as root via sudo. Expected database at ~${sudoUser}/.agor/agor.db`
894
1206
  this.log("");
895
1207
  this.log(chalk.red(`Errors: ${totalErrors}`));
896
1208
  }
897
- const hasChanges = usersCreated > 0 || groupsAdded > 0 || groupsCreated > 0 || daemonMembershipsAdded > 0 || usersDeleted > 0 || groupsDeleted > 0 || worktreesSynced > 0 || reposBackfilled > 0 || reposPermSynced > 0;
1209
+ const hasChanges = usersCreated > 0 || groupsAdded > 0 || groupsCreated > 0 || daemonMembershipsAdded > 0 || membershipsRemoved > 0 || usersDeleted > 0 || groupsDeleted > 0 || worktreesSynced > 0 || worktreesBackfilled > 0 || worktreeDirsCreated > 0 || worktreesRestored > 0 || groupsCleaned > 0 || statusFixed > 0 || daemonAclsApplied > 0 || reposBackfilled > 0 || reposPermSynced > 0 || symlinksCreated > 0 || symlinksCleaned > 0;
898
1210
  if (dryRun && hasChanges) {
899
1211
  this.log(chalk.yellow("\nRun without --dry-run to apply changes"));
900
1212
  }