botmux 2.9.0 → 2.9.2

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 (330) hide show
  1. package/README.en.md +140 -76
  2. package/README.md +134 -75
  3. package/dist/adapters/backend/pty-backend.d.ts +6 -0
  4. package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/pty-backend.js +10 -0
  6. package/dist/adapters/backend/pty-backend.js.map +1 -1
  7. package/dist/adapters/backend/session-backend-selector.d.ts +11 -0
  8. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -0
  9. package/dist/adapters/backend/session-backend-selector.js +26 -0
  10. package/dist/adapters/backend/session-backend-selector.js.map +1 -0
  11. package/dist/adapters/backend/tmux-backend.d.ts +80 -3
  12. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  13. package/dist/adapters/backend/tmux-backend.js +301 -49
  14. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  15. package/dist/adapters/backend/tmux-pipe-backend.d.ts +100 -0
  16. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -0
  17. package/dist/adapters/backend/tmux-pipe-backend.js +473 -0
  18. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -0
  19. package/dist/adapters/cli/aiden.d.ts.map +1 -1
  20. package/dist/adapters/cli/aiden.js +5 -0
  21. package/dist/adapters/cli/aiden.js.map +1 -1
  22. package/dist/adapters/cli/claude-code.d.ts +40 -1
  23. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  24. package/dist/adapters/cli/claude-code.js +470 -49
  25. package/dist/adapters/cli/claude-code.js.map +1 -1
  26. package/dist/adapters/cli/coco.d.ts.map +1 -1
  27. package/dist/adapters/cli/coco.js +191 -9
  28. package/dist/adapters/cli/coco.js.map +1 -1
  29. package/dist/adapters/cli/codex.d.ts.map +1 -1
  30. package/dist/adapters/cli/codex.js +94 -17
  31. package/dist/adapters/cli/codex.js.map +1 -1
  32. package/dist/adapters/cli/shared-hints.d.ts +2 -2
  33. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  34. package/dist/adapters/cli/shared-hints.js +7 -5
  35. package/dist/adapters/cli/shared-hints.js.map +1 -1
  36. package/dist/adapters/cli/types.d.ts +38 -1
  37. package/dist/adapters/cli/types.d.ts.map +1 -1
  38. package/dist/autostart.d.ts +14 -0
  39. package/dist/autostart.d.ts.map +1 -0
  40. package/dist/autostart.js +357 -0
  41. package/dist/autostart.js.map +1 -0
  42. package/dist/bot-registry.d.ts +29 -3
  43. package/dist/bot-registry.d.ts.map +1 -1
  44. package/dist/bot-registry.js +91 -12
  45. package/dist/bot-registry.js.map +1 -1
  46. package/dist/cli/arg-utils.d.ts +11 -0
  47. package/dist/cli/arg-utils.d.ts.map +1 -0
  48. package/dist/cli/arg-utils.js +25 -0
  49. package/dist/cli/arg-utils.js.map +1 -0
  50. package/dist/cli/create-group-resolver.d.ts +32 -0
  51. package/dist/cli/create-group-resolver.d.ts.map +1 -0
  52. package/dist/cli/create-group-resolver.js +70 -0
  53. package/dist/cli/create-group-resolver.js.map +1 -0
  54. package/dist/cli/quoted-render.d.ts +30 -0
  55. package/dist/cli/quoted-render.d.ts.map +1 -0
  56. package/dist/cli/quoted-render.js +29 -0
  57. package/dist/cli/quoted-render.js.map +1 -0
  58. package/dist/cli.js +916 -272
  59. package/dist/cli.js.map +1 -1
  60. package/dist/config.d.ts +6 -0
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +18 -8
  63. package/dist/config.js.map +1 -1
  64. package/dist/core/command-handler.d.ts +43 -0
  65. package/dist/core/command-handler.d.ts.map +1 -1
  66. package/dist/core/command-handler.js +167 -64
  67. package/dist/core/command-handler.js.map +1 -1
  68. package/dist/core/dashboard-events.d.ts +57 -0
  69. package/dist/core/dashboard-events.d.ts.map +1 -0
  70. package/dist/core/dashboard-events.js +23 -0
  71. package/dist/core/dashboard-events.js.map +1 -0
  72. package/dist/core/dashboard-ipc-server.d.ts +43 -0
  73. package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
  74. package/dist/core/dashboard-ipc-server.js +481 -0
  75. package/dist/core/dashboard-ipc-server.js.map +1 -0
  76. package/dist/core/dashboard-locate.d.ts +20 -0
  77. package/dist/core/dashboard-locate.d.ts.map +1 -0
  78. package/dist/core/dashboard-locate.js +26 -0
  79. package/dist/core/dashboard-locate.js.map +1 -0
  80. package/dist/core/dashboard-rows.d.ts +31 -0
  81. package/dist/core/dashboard-rows.d.ts.map +1 -0
  82. package/dist/core/dashboard-rows.js +65 -0
  83. package/dist/core/dashboard-rows.js.map +1 -0
  84. package/dist/core/inherit-peer.d.ts +14 -0
  85. package/dist/core/inherit-peer.d.ts.map +1 -0
  86. package/dist/core/inherit-peer.js +32 -0
  87. package/dist/core/inherit-peer.js.map +1 -0
  88. package/dist/core/scheduler.d.ts +24 -0
  89. package/dist/core/scheduler.d.ts.map +1 -1
  90. package/dist/core/scheduler.js +93 -2
  91. package/dist/core/scheduler.js.map +1 -1
  92. package/dist/core/session-activity.d.ts +3 -0
  93. package/dist/core/session-activity.d.ts.map +1 -0
  94. package/dist/core/session-activity.js +20 -0
  95. package/dist/core/session-activity.js.map +1 -0
  96. package/dist/core/session-discovery.d.ts +39 -0
  97. package/dist/core/session-discovery.d.ts.map +1 -1
  98. package/dist/core/session-discovery.js +114 -21
  99. package/dist/core/session-discovery.js.map +1 -1
  100. package/dist/core/session-manager.d.ts +72 -0
  101. package/dist/core/session-manager.d.ts.map +1 -1
  102. package/dist/core/session-manager.js +396 -106
  103. package/dist/core/session-manager.js.map +1 -1
  104. package/dist/core/types.d.ts +27 -2
  105. package/dist/core/types.d.ts.map +1 -1
  106. package/dist/core/types.js +14 -3
  107. package/dist/core/types.js.map +1 -1
  108. package/dist/core/worker-pool.d.ts +72 -3
  109. package/dist/core/worker-pool.d.ts.map +1 -1
  110. package/dist/core/worker-pool.js +459 -38
  111. package/dist/core/worker-pool.js.map +1 -1
  112. package/dist/daemon.d.ts.map +1 -1
  113. package/dist/daemon.js +601 -309
  114. package/dist/daemon.js.map +1 -1
  115. package/dist/dashboard/aggregator.d.ts +41 -0
  116. package/dist/dashboard/aggregator.d.ts.map +1 -0
  117. package/dist/dashboard/aggregator.js +125 -0
  118. package/dist/dashboard/aggregator.js.map +1 -0
  119. package/dist/dashboard/auth.d.ts +23 -0
  120. package/dist/dashboard/auth.d.ts.map +1 -0
  121. package/dist/dashboard/auth.js +66 -0
  122. package/dist/dashboard/auth.js.map +1 -0
  123. package/dist/dashboard/operator-selector.d.ts +20 -0
  124. package/dist/dashboard/operator-selector.d.ts.map +1 -0
  125. package/dist/dashboard/operator-selector.js +39 -0
  126. package/dist/dashboard/operator-selector.js.map +1 -0
  127. package/dist/dashboard/registry.d.ts +35 -0
  128. package/dist/dashboard/registry.d.ts.map +1 -0
  129. package/dist/dashboard/registry.js +74 -0
  130. package/dist/dashboard/registry.js.map +1 -0
  131. package/dist/dashboard/web/app.d.ts +2 -0
  132. package/dist/dashboard/web/app.d.ts.map +1 -0
  133. package/dist/dashboard/web/app.js +45 -0
  134. package/dist/dashboard/web/app.js.map +1 -0
  135. package/dist/dashboard/web/bot-defaults.d.ts +2 -0
  136. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -0
  137. package/dist/dashboard/web/bot-defaults.js +201 -0
  138. package/dist/dashboard/web/bot-defaults.js.map +1 -0
  139. package/dist/dashboard/web/groups.d.ts +16 -0
  140. package/dist/dashboard/web/groups.d.ts.map +1 -0
  141. package/dist/dashboard/web/groups.js +584 -0
  142. package/dist/dashboard/web/groups.js.map +1 -0
  143. package/dist/dashboard/web/schedules.d.ts +2 -0
  144. package/dist/dashboard/web/schedules.d.ts.map +1 -0
  145. package/dist/dashboard/web/schedules.js +105 -0
  146. package/dist/dashboard/web/schedules.js.map +1 -0
  147. package/dist/dashboard/web/sessions.d.ts +2 -0
  148. package/dist/dashboard/web/sessions.d.ts.map +1 -0
  149. package/dist/dashboard/web/sessions.js +374 -0
  150. package/dist/dashboard/web/sessions.js.map +1 -0
  151. package/dist/dashboard/web/store.d.ts +23 -0
  152. package/dist/dashboard/web/store.d.ts.map +1 -0
  153. package/dist/dashboard/web/store.js +82 -0
  154. package/dist/dashboard/web/store.js.map +1 -0
  155. package/dist/dashboard-web/app.js +263 -0
  156. package/dist/dashboard-web/index.html +23 -0
  157. package/dist/dashboard-web/style.css +93 -0
  158. package/dist/dashboard.d.ts +2 -0
  159. package/dist/dashboard.d.ts.map +1 -0
  160. package/dist/dashboard.js +639 -0
  161. package/dist/dashboard.js.map +1 -0
  162. package/dist/im/lark/card-builder.d.ts +18 -1
  163. package/dist/im/lark/card-builder.d.ts.map +1 -1
  164. package/dist/im/lark/card-builder.js +70 -9
  165. package/dist/im/lark/card-builder.js.map +1 -1
  166. package/dist/im/lark/card-handler.d.ts.map +1 -1
  167. package/dist/im/lark/card-handler.js +123 -109
  168. package/dist/im/lark/card-handler.js.map +1 -1
  169. package/dist/im/lark/client.d.ts +36 -0
  170. package/dist/im/lark/client.d.ts.map +1 -1
  171. package/dist/im/lark/client.js +119 -13
  172. package/dist/im/lark/client.js.map +1 -1
  173. package/dist/im/lark/event-dispatcher.d.ts +92 -8
  174. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  175. package/dist/im/lark/event-dispatcher.js +410 -89
  176. package/dist/im/lark/event-dispatcher.js.map +1 -1
  177. package/dist/im/lark/forwarded-renderer.d.ts +23 -0
  178. package/dist/im/lark/forwarded-renderer.d.ts.map +1 -0
  179. package/dist/im/lark/forwarded-renderer.js +105 -0
  180. package/dist/im/lark/forwarded-renderer.js.map +1 -0
  181. package/dist/im/lark/md-card.d.ts +73 -0
  182. package/dist/im/lark/md-card.d.ts.map +1 -0
  183. package/dist/im/lark/md-card.js +332 -0
  184. package/dist/im/lark/md-card.js.map +1 -0
  185. package/dist/im/lark/merge-forward.d.ts +32 -0
  186. package/dist/im/lark/merge-forward.d.ts.map +1 -0
  187. package/dist/im/lark/merge-forward.js +110 -0
  188. package/dist/im/lark/merge-forward.js.map +1 -0
  189. package/dist/im/lark/message-parser.d.ts +9 -3
  190. package/dist/im/lark/message-parser.d.ts.map +1 -1
  191. package/dist/im/lark/message-parser.js +48 -13
  192. package/dist/im/lark/message-parser.js.map +1 -1
  193. package/dist/im/lark/quote-hint.d.ts +18 -0
  194. package/dist/im/lark/quote-hint.d.ts.map +1 -0
  195. package/dist/im/lark/quote-hint.js +23 -0
  196. package/dist/im/lark/quote-hint.js.map +1 -0
  197. package/dist/services/bridge-fallback-gate.d.ts +42 -0
  198. package/dist/services/bridge-fallback-gate.d.ts.map +1 -0
  199. package/dist/services/bridge-fallback-gate.js +12 -0
  200. package/dist/services/bridge-fallback-gate.js.map +1 -0
  201. package/dist/services/bridge-rotation-policy.d.ts +139 -0
  202. package/dist/services/bridge-rotation-policy.d.ts.map +1 -0
  203. package/dist/services/bridge-rotation-policy.js +125 -0
  204. package/dist/services/bridge-rotation-policy.js.map +1 -0
  205. package/dist/services/bridge-turn-queue.d.ts +154 -0
  206. package/dist/services/bridge-turn-queue.d.ts.map +1 -0
  207. package/dist/services/bridge-turn-queue.js +316 -0
  208. package/dist/services/bridge-turn-queue.js.map +1 -0
  209. package/dist/services/chat-first-seen-store.d.ts +27 -0
  210. package/dist/services/chat-first-seen-store.d.ts.map +1 -0
  211. package/dist/services/chat-first-seen-store.js +114 -0
  212. package/dist/services/chat-first-seen-store.js.map +1 -0
  213. package/dist/services/claude-transcript.d.ts +268 -0
  214. package/dist/services/claude-transcript.d.ts.map +1 -0
  215. package/dist/services/claude-transcript.js +798 -0
  216. package/dist/services/claude-transcript.js.map +1 -0
  217. package/dist/services/coco-transcript.d.ts +35 -0
  218. package/dist/services/coco-transcript.d.ts.map +1 -0
  219. package/dist/services/coco-transcript.js +192 -0
  220. package/dist/services/coco-transcript.js.map +1 -0
  221. package/dist/services/codex-bridge-queue.d.ts +56 -0
  222. package/dist/services/codex-bridge-queue.d.ts.map +1 -0
  223. package/dist/services/codex-bridge-queue.js +150 -0
  224. package/dist/services/codex-bridge-queue.js.map +1 -0
  225. package/dist/services/codex-transcript.d.ts +84 -0
  226. package/dist/services/codex-transcript.d.ts.map +1 -0
  227. package/dist/services/codex-transcript.js +298 -0
  228. package/dist/services/codex-transcript.js.map +1 -0
  229. package/dist/services/group-creator.d.ts +23 -0
  230. package/dist/services/group-creator.d.ts.map +1 -0
  231. package/dist/services/group-creator.js +75 -0
  232. package/dist/services/group-creator.js.map +1 -0
  233. package/dist/services/groups-store.d.ts +98 -0
  234. package/dist/services/groups-store.d.ts.map +1 -0
  235. package/dist/services/groups-store.js +213 -0
  236. package/dist/services/groups-store.js.map +1 -0
  237. package/dist/services/oncall-store.d.ts +80 -8
  238. package/dist/services/oncall-store.d.ts.map +1 -1
  239. package/dist/services/oncall-store.js +265 -55
  240. package/dist/services/oncall-store.js.map +1 -1
  241. package/dist/services/project-scanner.d.ts +1 -2
  242. package/dist/services/project-scanner.d.ts.map +1 -1
  243. package/dist/services/project-scanner.js +118 -68
  244. package/dist/services/project-scanner.js.map +1 -1
  245. package/dist/services/schedule-store.d.ts +5 -0
  246. package/dist/services/schedule-store.d.ts.map +1 -1
  247. package/dist/services/schedule-store.js +77 -1
  248. package/dist/services/schedule-store.js.map +1 -1
  249. package/dist/services/session-store.d.ts +22 -0
  250. package/dist/services/session-store.d.ts.map +1 -1
  251. package/dist/services/session-store.js +62 -4
  252. package/dist/services/session-store.js.map +1 -1
  253. package/dist/setup/bots-store.d.ts +3 -0
  254. package/dist/setup/bots-store.d.ts.map +1 -0
  255. package/dist/setup/bots-store.js +24 -0
  256. package/dist/setup/bots-store.js.map +1 -0
  257. package/dist/setup/detect-platform.d.ts +14 -0
  258. package/dist/setup/detect-platform.d.ts.map +1 -0
  259. package/dist/setup/detect-platform.js +139 -0
  260. package/dist/setup/detect-platform.js.map +1 -0
  261. package/dist/setup/ensure-fonts.d.ts +13 -0
  262. package/dist/setup/ensure-fonts.d.ts.map +1 -0
  263. package/dist/setup/ensure-fonts.js +225 -0
  264. package/dist/setup/ensure-fonts.js.map +1 -0
  265. package/dist/setup/ensure-tmux.d.ts +60 -0
  266. package/dist/setup/ensure-tmux.d.ts.map +1 -0
  267. package/dist/setup/ensure-tmux.js +236 -0
  268. package/dist/setup/ensure-tmux.js.map +1 -0
  269. package/dist/setup/index.d.ts +9 -0
  270. package/dist/setup/index.d.ts.map +1 -0
  271. package/dist/setup/index.js +46 -0
  272. package/dist/setup/index.js.map +1 -0
  273. package/dist/setup/lark-scopes.json +301 -0
  274. package/dist/setup/register-app.d.ts +52 -0
  275. package/dist/setup/register-app.d.ts.map +1 -0
  276. package/dist/setup/register-app.js +91 -0
  277. package/dist/setup/register-app.js.map +1 -0
  278. package/dist/setup/verify-permissions.d.ts +115 -0
  279. package/dist/setup/verify-permissions.d.ts.map +1 -0
  280. package/dist/setup/verify-permissions.js +207 -0
  281. package/dist/setup/verify-permissions.js.map +1 -0
  282. package/dist/skills/definitions.d.ts +4 -0
  283. package/dist/skills/definitions.d.ts.map +1 -1
  284. package/dist/skills/definitions.js +133 -19
  285. package/dist/skills/definitions.js.map +1 -1
  286. package/dist/skills/installer.d.ts +3 -1
  287. package/dist/skills/installer.d.ts.map +1 -1
  288. package/dist/skills/installer.js +18 -3
  289. package/dist/skills/installer.js.map +1 -1
  290. package/dist/types.d.ts +44 -0
  291. package/dist/types.d.ts.map +1 -1
  292. package/dist/utils/bot-routing.d.ts +6 -0
  293. package/dist/utils/bot-routing.d.ts.map +1 -0
  294. package/dist/utils/bot-routing.js +50 -0
  295. package/dist/utils/bot-routing.js.map +1 -0
  296. package/dist/utils/file-lock.d.ts +2 -0
  297. package/dist/utils/file-lock.d.ts.map +1 -0
  298. package/dist/utils/file-lock.js +114 -0
  299. package/dist/utils/file-lock.js.map +1 -0
  300. package/dist/utils/font-installer.js +1 -1
  301. package/dist/utils/font-installer.js.map +1 -1
  302. package/dist/utils/idle-detector.d.ts +6 -0
  303. package/dist/utils/idle-detector.d.ts.map +1 -1
  304. package/dist/utils/idle-detector.js +25 -4
  305. package/dist/utils/idle-detector.js.map +1 -1
  306. package/dist/utils/logger.d.ts +10 -0
  307. package/dist/utils/logger.d.ts.map +1 -1
  308. package/dist/utils/logger.js +60 -8
  309. package/dist/utils/logger.js.map +1 -1
  310. package/dist/utils/render-dimensions.d.ts +48 -0
  311. package/dist/utils/render-dimensions.d.ts.map +1 -0
  312. package/dist/utils/render-dimensions.js +55 -0
  313. package/dist/utils/render-dimensions.js.map +1 -0
  314. package/dist/utils/screen-analyzer.d.ts.map +1 -1
  315. package/dist/utils/screen-analyzer.js +24 -0
  316. package/dist/utils/screen-analyzer.js.map +1 -1
  317. package/dist/utils/screenshot-renderer.d.ts.map +1 -1
  318. package/dist/utils/screenshot-renderer.js +67 -23
  319. package/dist/utils/screenshot-renderer.js.map +1 -1
  320. package/dist/utils/terminal-renderer.d.ts +16 -0
  321. package/dist/utils/terminal-renderer.d.ts.map +1 -1
  322. package/dist/utils/terminal-renderer.js +40 -23
  323. package/dist/utils/terminal-renderer.js.map +1 -1
  324. package/dist/utils/transient-snapshot.d.ts +28 -0
  325. package/dist/utils/transient-snapshot.d.ts.map +1 -0
  326. package/dist/utils/transient-snapshot.js +96 -0
  327. package/dist/utils/transient-snapshot.js.map +1 -0
  328. package/dist/worker.js +2220 -83
  329. package/dist/worker.js.map +1 -1
  330. package/package.json +12 -5
@@ -1,53 +1,94 @@
1
1
  /**
2
- * Oncall bindings — persist chat_id → default workingDir + owners into the
3
- * bot config JSON file, and keep the in-memory BotConfig in sync so events
4
- * pick up changes without a daemon restart.
2
+ * Oncall bindings — persist chat_id → default workingDir into the bot config
3
+ * JSON file, and keep the in-memory BotConfig in sync so events pick up
4
+ * changes without a daemon restart.
5
+ *
6
+ * Permission model is intentionally simple: anyone in the bot's allowedUsers
7
+ * can bind/unbind/edit (enforced at the call sites — daemon command handler
8
+ * + dashboard token gate). No per-chat owner list.
9
+ *
10
+ * Multi-process safety: 12 daemon processes + 1 dashboard process all share
11
+ * a single `bots.json`. Every write path goes through `withFileLock(path)`
12
+ * so a burst of concurrent auto-binds (each daemon sees a new chat for its
13
+ * own bot at roughly the same time) doesn't lose updates via read-modify-
14
+ * write race. The lock is also re-acquired around the read so the modify
15
+ * step always works against the latest on-disk snapshot.
5
16
  */
6
- import { readFileSync, writeFileSync } from 'node:fs';
17
+ import { promises as fsp } from 'node:fs';
18
+ import { readFileSync } from 'node:fs';
7
19
  import { getBot, getLoadedConfigPath } from '../bot-registry.js';
20
+ import { withFileLock } from '../utils/file-lock.js';
8
21
  import { logger } from '../utils/logger.js';
9
- function loadRawConfig() {
10
- const path = getLoadedConfigPath();
11
- if (!path)
12
- throw new Error('Bot config path unknown — cannot persist oncall bindings');
13
- const raw = JSON.parse(readFileSync(path, 'utf-8'));
22
+ async function readRawConfig(path) {
23
+ const raw = JSON.parse(await fsp.readFile(path, 'utf-8'));
14
24
  if (!Array.isArray(raw))
15
25
  throw new Error(`Config file is not a JSON array: ${path}`);
16
- return { path, raw };
26
+ return raw;
17
27
  }
18
- function writeRawConfig(path, raw) {
19
- writeFileSync(path, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
28
+ async function writeRawConfigAtomic(path, raw) {
29
+ const tmp = path + '.tmp.' + process.pid;
30
+ await fsp.writeFile(tmp, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
31
+ await fsp.rename(tmp, path);
20
32
  }
21
33
  function findEntryIndex(raw, larkAppId) {
22
34
  return raw.findIndex((e) => e?.larkAppId === larkAppId);
23
35
  }
36
+ function requireConfigPath() {
37
+ const p = getLoadedConfigPath();
38
+ if (!p)
39
+ throw new Error('Bot config path unknown — cannot persist oncall bindings');
40
+ return p;
41
+ }
42
+ /**
43
+ * Run a read-modify-write critical section against the bot config file under
44
+ * a cross-process lock. `mutate` runs against the freshest on-disk snapshot
45
+ * and decides what to write back; returning `undefined` means "no write".
46
+ */
47
+ async function rmwBotEntry(larkAppId, mutate) {
48
+ const path = requireConfigPath();
49
+ return withFileLock(path, async () => {
50
+ const raw = await readRawConfig(path);
51
+ const idx = findEntryIndex(raw, larkAppId);
52
+ if (idx < 0)
53
+ return { ok: false, reason: 'bot_not_in_config' };
54
+ const entry = raw[idx];
55
+ const out = mutate(entry, raw);
56
+ if (out && typeof out === 'object' && 'write' in out) {
57
+ const wrap = out;
58
+ if (wrap.write)
59
+ await writeRawConfigAtomic(path, raw);
60
+ return { ok: true, result: wrap.result };
61
+ }
62
+ await writeRawConfigAtomic(path, raw);
63
+ return { ok: true, result: out };
64
+ });
65
+ }
66
+ // ─── Manual binding ───────────────────────────────────────────────────────
24
67
  /**
25
- * Upsert an oncall binding. If the chat is already bound, only existing owners
26
- * can update the workingDir (ownerOpenId must be in owners). First-time bind
27
- * puts the caller into owners automatically.
68
+ * Upsert an oncall binding. Returns whether it was newly created.
28
69
  */
29
- export function bindOncall(larkAppId, chatId, workingDir, ownerOpenId) {
30
- const bot = getBot(larkAppId);
31
- const existingList = bot.config.oncallChats ?? [];
32
- const existing = existingList.find(c => c.chatId === chatId);
33
- if (existing && !existing.owners.includes(ownerOpenId)) {
34
- return { ok: false, reason: 'not_owner' };
70
+ export async function bindOncall(larkAppId, chatId, workingDir) {
71
+ let bot;
72
+ try {
73
+ bot = getBot(larkAppId);
35
74
  }
36
- const next = existing
37
- ? { ...existing, workingDir }
38
- : { chatId, workingDir, owners: [ownerOpenId] };
39
- const { path, raw } = loadRawConfig();
40
- const idx = findEntryIndex(raw, larkAppId);
41
- if (idx < 0)
42
- return { ok: false, reason: 'bot_not_in_config' };
43
- const cur = Array.isArray(raw[idx].oncallChats) ? raw[idx].oncallChats : [];
44
- const curIdx = cur.findIndex((c) => c.chatId === chatId);
45
- if (curIdx >= 0)
46
- cur[curIdx] = next;
47
- else
48
- cur.push(next);
49
- raw[idx].oncallChats = cur;
50
- writeRawConfig(path, raw);
75
+ catch {
76
+ return { ok: false, reason: 'bot_not_registered' };
77
+ }
78
+ const next = { chatId, workingDir };
79
+ const r = await rmwBotEntry(larkAppId, (entry) => {
80
+ const cur = Array.isArray(entry.oncallChats) ? entry.oncallChats : [];
81
+ const curIdx = cur.findIndex((c) => c?.chatId === chatId);
82
+ const created = curIdx < 0;
83
+ if (created)
84
+ cur.push(next);
85
+ else
86
+ cur[curIdx] = next; // wholesale replace strips legacy keys
87
+ entry.oncallChats = cur;
88
+ return { write: true, result: { created } };
89
+ });
90
+ if (!r.ok)
91
+ return { ok: false, reason: r.reason };
51
92
  // Keep in-memory config in sync
52
93
  const inMem = (bot.config.oncallChats ??= []);
53
94
  const memIdx = inMem.findIndex(c => c.chatId === chatId);
@@ -55,30 +96,199 @@ export function bindOncall(larkAppId, chatId, workingDir, ownerOpenId) {
55
96
  inMem[memIdx] = next;
56
97
  else
57
98
  inMem.push(next);
58
- logger.info(`[oncall:${larkAppId}] bind chat=${chatId} dir=${workingDir} owner=${ownerOpenId}`);
59
- return { ok: true, entry: next, created: !existing };
99
+ logger.info(`[oncall:${larkAppId}] bind chat=${chatId} dir=${workingDir}`);
100
+ return { ok: true, entry: next, created: r.result.created };
60
101
  }
61
- export function unbindOncall(larkAppId, chatId, ownerOpenId) {
62
- const bot = getBot(larkAppId);
63
- const existing = bot.config.oncallChats?.find(c => c.chatId === chatId);
64
- if (!existing)
65
- return { ok: false, reason: 'not_bound' };
66
- if (!existing.owners.includes(ownerOpenId))
67
- return { ok: false, reason: 'not_owner' };
68
- const { path, raw } = loadRawConfig();
69
- const idx = findEntryIndex(raw, larkAppId);
70
- if (idx < 0)
71
- return { ok: false, reason: 'bot_not_in_config' };
72
- const cur = Array.isArray(raw[idx].oncallChats) ? raw[idx].oncallChats : [];
73
- raw[idx].oncallChats = cur.filter((c) => c.chatId !== chatId);
74
- writeRawConfig(path, raw);
102
+ /**
103
+ * Unbind oncall for `chatId` and ALWAYS write a tombstone into
104
+ * `defaultOncallAutoboundChats`. The tombstone protects against the case
105
+ * where a user manually fiddled with a chat (bound then unbound, or just
106
+ * unbound) and we then mis-classify it as "new" on the next observation
107
+ * and re-auto-bind. Treating unbind as "default's one shot is spent" is
108
+ * symmetric with auto-bind already adding to the same list.
109
+ *
110
+ * Idempotent: never errors on "not bound". `wasBound` reports whether an
111
+ * existing binding was actually removed so callers can phrase UI text
112
+ * accordingly (the Lark `/oncall unbind` command still wants to say "未绑定"
113
+ * vs "已解绑").
114
+ */
115
+ export async function unbindOncall(larkAppId, chatId) {
116
+ let bot;
117
+ try {
118
+ bot = getBot(larkAppId);
119
+ }
120
+ catch {
121
+ return { ok: false, reason: 'bot_not_registered' };
122
+ }
123
+ const r = await rmwBotEntry(larkAppId, (entry) => {
124
+ const cur = Array.isArray(entry.oncallChats) ? entry.oncallChats : [];
125
+ const wasBound = cur.some(c => c?.chatId === chatId);
126
+ entry.oncallChats = cur.filter((c) => c?.chatId !== chatId);
127
+ const tomb = Array.isArray(entry.defaultOncallAutoboundChats)
128
+ ? entry.defaultOncallAutoboundChats : [];
129
+ if (!tomb.includes(chatId))
130
+ tomb.push(chatId);
131
+ entry.defaultOncallAutoboundChats = tomb;
132
+ return { write: true, result: { wasBound } };
133
+ });
134
+ if (!r.ok)
135
+ return { ok: false, reason: r.reason };
75
136
  if (bot.config.oncallChats) {
76
137
  bot.config.oncallChats = bot.config.oncallChats.filter(c => c.chatId !== chatId);
77
138
  }
78
- logger.info(`[oncall:${larkAppId}] unbind chat=${chatId} by=${ownerOpenId}`);
79
- return { ok: true };
139
+ const inMemTomb = (bot.config.defaultOncallAutoboundChats ??= []);
140
+ if (!inMemTomb.includes(chatId))
141
+ inMemTomb.push(chatId);
142
+ logger.info(`[oncall:${larkAppId}] unbind chat=${chatId} wasBound=${r.result.wasBound} (tombstoned)`);
143
+ return { ok: true, wasBound: r.result.wasBound };
80
144
  }
81
145
  export function getOncallStatus(larkAppId, chatId) {
82
- return getBot(larkAppId).config.oncallChats?.find(c => c.chatId === chatId);
146
+ // Defensive: dashboard callers may probe with an app id whose bot isn't
147
+ // registered yet (boot races, or tests exercising the IPC layer without
148
+ // a full registry). Treat "no such bot" as "no oncall binding" — this
149
+ // is best-effort enrichment, not a critical path.
150
+ let bot;
151
+ try {
152
+ bot = getBot(larkAppId);
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ return bot.config.oncallChats?.find(c => c.chatId === chatId);
158
+ }
159
+ // ─── Per-bot defaultOncall ───────────────────────────────────────────────
160
+ /** Read the current defaultOncall config + autobound list for a bot. Used by
161
+ * the dashboard GET route and by the daemon's auto-bind judge. Sync because
162
+ * it only reads the in-memory snapshot — file-level consistency comes from
163
+ * the daemon never racing with itself on reads. */
164
+ export function getBotDefaultOncall(larkAppId) {
165
+ let bot;
166
+ try {
167
+ bot = getBot(larkAppId);
168
+ }
169
+ catch {
170
+ return { defaultOncall: undefined, autoboundChats: [] };
171
+ }
172
+ return {
173
+ defaultOncall: bot.config.defaultOncall,
174
+ autoboundChats: [...(bot.config.defaultOncallAutoboundChats ?? [])],
175
+ };
176
+ }
177
+ /**
178
+ * Persist a defaultOncall change for the given bot. The dashboard PUT route
179
+ * is the only authorized caller — `since` is server-side authoritative so the
180
+ * frontend can't backdate the cut-off and accidentally include existing chats.
181
+ *
182
+ * `since` is stamped on every enabled save, not just the first transition.
183
+ * This matches the dashboard copy/requirement and prevents a later workingDir
184
+ * edit from reaching chats that were first observed before that edit.
185
+ *
186
+ * When disabled with an empty `workingDir`, the prior workingDir is preserved
187
+ * so the UI can round-trip (toggle off → toggle back on) without forcing the
188
+ * user to retype the path. Disable with a non-empty workingDir overwrites.
189
+ */
190
+ export async function updateBotDefaultOncall(larkAppId, patch) {
191
+ let bot;
192
+ try {
193
+ bot = getBot(larkAppId);
194
+ }
195
+ catch {
196
+ return { ok: false, reason: 'bot_not_registered' };
197
+ }
198
+ let next = null;
199
+ const r = await rmwBotEntry(larkAppId, (entry) => {
200
+ const prior = entry.defaultOncall;
201
+ // Cut-off line: every enabled save re-stamps so a workingDir edit while
202
+ // enabled doesn't reach back to chats observed under the old setting.
203
+ const nextSince = patch.enabled ? Date.now() : (prior?.since ?? 0);
204
+ const trimmed = (patch.workingDir ?? '').trim();
205
+ const resolvedWorkingDir = patch.enabled
206
+ ? trimmed
207
+ // Disabled + empty input → keep the prior path so the toggle is round-
208
+ // trippable. Disabled + explicit non-empty → user is replacing it.
209
+ : (trimmed || prior?.workingDir || '');
210
+ next = {
211
+ enabled: !!patch.enabled,
212
+ workingDir: resolvedWorkingDir,
213
+ since: nextSince,
214
+ };
215
+ entry.defaultOncall = next;
216
+ return { write: true, result: next };
217
+ });
218
+ if (!r.ok)
219
+ return { ok: false, reason: r.reason };
220
+ bot.config.defaultOncall = next;
221
+ logger.info(`[oncall:${larkAppId}] defaultOncall ${next.enabled ? 'enabled' : 'disabled'} ` +
222
+ `(workingDir=${next.workingDir || '∅'}, since=${next.since})`);
223
+ return { ok: true, defaultOncall: next };
224
+ }
225
+ /**
226
+ * Auto-bind a chat as part of the defaultOncall flow. Atomically:
227
+ * 1. RE-CHECK tombstone + existing binding against the freshest on-disk
228
+ * snapshot. The daemon's fast-path tombstone check is informational —
229
+ * if a concurrent `unbindOncall` wrote a tombstone between then and
230
+ * now, the lock-internal view sees it and we skip.
231
+ * 2. Upsert the oncallChats entry (same shape as manual bindOncall).
232
+ * 3. Append chatId to defaultOncallAutoboundChats (idempotent).
233
+ *
234
+ * Returns `skipped: 'tombstoned'` when the lock-internal tombstone check
235
+ * trips, `skipped: 'already_bound'` when another writer (manual bind by
236
+ * the user, or a sibling daemon) bound the chat between the fast-path read
237
+ * and the lock acquisition. Neither is an error.
238
+ */
239
+ export async function autoBindOncallFromDefault(larkAppId, chatId, workingDir) {
240
+ let bot;
241
+ try {
242
+ bot = getBot(larkAppId);
243
+ }
244
+ catch {
245
+ return { ok: false, reason: 'bot_not_registered' };
246
+ }
247
+ const next = { chatId, workingDir };
248
+ const r = await rmwBotEntry(larkAppId, (entry) => {
249
+ // Authoritative re-check #1: tombstone wins. If a concurrent unbind or
250
+ // earlier autoBind wrote one, the user has effectively opted out — never
251
+ // overwrite that decision from the auto-bind path.
252
+ const tomb = Array.isArray(entry.defaultOncallAutoboundChats)
253
+ ? entry.defaultOncallAutoboundChats : [];
254
+ if (tomb.includes(chatId)) {
255
+ return { write: false, result: { kind: 'skipped', reason: 'tombstoned' } };
256
+ }
257
+ // Authoritative re-check #2: existing binding wins. Could be from
258
+ // a sibling daemon, a manual /oncall bind, or a dashboard PUT racing
259
+ // with us. We never overwrite an existing binding with the default —
260
+ // the user's explicit choice (or a sibling's earlier auto-bind to its
261
+ // own default) is authoritative.
262
+ const cur = Array.isArray(entry.oncallChats) ? entry.oncallChats : [];
263
+ if (cur.some(c => c?.chatId === chatId)) {
264
+ return { write: false, result: { kind: 'skipped', reason: 'already_bound' } };
265
+ }
266
+ cur.push(next);
267
+ entry.oncallChats = cur;
268
+ tomb.push(chatId);
269
+ entry.defaultOncallAutoboundChats = tomb;
270
+ return { write: true, result: { kind: 'bound', created: true } };
271
+ });
272
+ if (!r.ok)
273
+ return { ok: false, reason: r.reason };
274
+ if (r.result.kind === 'skipped') {
275
+ return { ok: true, skipped: r.result.reason };
276
+ }
277
+ // Sync in-memory
278
+ const inMem = (bot.config.oncallChats ??= []);
279
+ const memIdx = inMem.findIndex(c => c.chatId === chatId);
280
+ if (memIdx >= 0)
281
+ inMem[memIdx] = next;
282
+ else
283
+ inMem.push(next);
284
+ const inMemAutobound = (bot.config.defaultOncallAutoboundChats ??= []);
285
+ if (!inMemAutobound.includes(chatId))
286
+ inMemAutobound.push(chatId);
287
+ logger.info(`[oncall:${larkAppId}] auto-bind (default) chat=${chatId} dir=${workingDir}`);
288
+ return { ok: true, entry: next, created: r.result.created };
289
+ }
290
+ // Test helper — read raw bots.json synchronously. Not for production use.
291
+ export function _readRawConfigSyncForTesting(path) {
292
+ return JSON.parse(readFileSync(path, 'utf-8'));
83
293
  }
84
294
  //# sourceMappingURL=oncall-store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"oncall-store.js","sourceRoot":"","sources":["../../src/services/oncall-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oncall-store.js","sourceRoot":"","sources":["../../src/services/oncall-store.ts"],"names":[],"mappings":"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"}
@@ -6,11 +6,10 @@ export interface ProjectInfo {
6
6
  }
7
7
  /**
8
8
  * Scan a directory for git repositories and their worktrees.
9
- * Returns a flat list of all projects found.
10
9
  */
11
10
  export declare function scanProjects(baseDir: string, maxDepth?: number): ProjectInfo[];
12
11
  /**
13
- * Scan multiple directories for git repositories, merge and deduplicate results.
12
+ * Scan multiple directories and deduplicate by path.
14
13
  */
15
14
  export declare function scanMultipleProjects(baseDirs: string[], maxDepth?: number): ProjectInfo[];
16
15
  //# sourceMappingURL=project-scanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-scanner.d.ts","sourceRoot":"","sources":["../../src/services/project-scanner.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AA2CD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAU,GAAG,WAAW,EAAE,CA6DjF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,GAAE,MAAU,GAAG,WAAW,EAAE,CAoB5F"}
1
+ {"version":3,"file":"project-scanner.d.ts","sourceRoot":"","sources":["../../src/services/project-scanner.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAmFD;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAU,GAAG,WAAW,EAAE,CA+CjF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,GAAE,MAAU,GAAG,WAAW,EAAE,CAe5F"}
@@ -1,55 +1,124 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { readdirSync, statSync } from 'node:fs';
3
- import { join, basename } from 'node:path';
2
+ import { readdirSync, statSync, existsSync } from 'node:fs';
3
+ import { join, basename, resolve } from 'node:path';
4
4
  import { logger } from '../utils/logger.js';
5
- function getGitBranch(dir) {
5
+ /** A `.git` entry that's a regular file (worktree gitlink) or a directory
6
+ * containing `HEAD`. An empty `.git/` is rejected so the scanner keeps
7
+ * recursing past stray markers like `/root/.git`. */
8
+ function isValidGitMarker(parentDir) {
9
+ const gitPath = join(parentDir, '.git');
10
+ let st;
6
11
  try {
7
- return execSync('git rev-parse --abbrev-ref HEAD', { cwd: dir, timeout: 5000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
12
+ st = statSync(gitPath);
8
13
  }
9
14
  catch {
10
- return 'unknown';
15
+ return false;
11
16
  }
17
+ if (st.isFile())
18
+ return true;
19
+ if (st.isDirectory())
20
+ return existsSync(join(gitPath, 'HEAD'));
21
+ return false;
12
22
  }
13
- function getWorktrees(repoPath) {
23
+ function runGit(args, cwd) {
14
24
  try {
15
- const output = execSync('git worktree list --porcelain', { cwd: repoPath, timeout: 5000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
16
- const worktrees = [];
17
- let currentPath = '';
18
- let currentBranch = '';
19
- for (const line of output.split('\n')) {
20
- if (line.startsWith('worktree ')) {
21
- currentPath = line.slice('worktree '.length);
22
- }
23
- else if (line.startsWith('branch ')) {
24
- currentBranch = line.slice('branch '.length).replace('refs/heads/', '');
25
- }
26
- else if (line === '') {
27
- // End of a worktree entry — skip the main worktree (same as repoPath)
28
- if (currentPath && currentPath !== repoPath) {
29
- worktrees.push({
30
- name: `${basename(repoPath)}/${basename(currentPath)}`,
31
- path: currentPath,
32
- type: 'worktree',
33
- branch: currentBranch || 'unknown',
34
- });
35
- }
36
- currentPath = '';
37
- currentBranch = '';
38
- }
39
- }
40
- return worktrees;
25
+ return execSync(`git ${args}`, {
26
+ cwd, timeout: 5000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
27
+ }).trim();
41
28
  }
42
29
  catch {
43
- return [];
30
+ return null;
31
+ }
32
+ }
33
+ /** `rev-parse --abbrev-ref HEAD` returns the literal string `HEAD` when
34
+ * detached — that's the signal to fall through to tag/SHA. */
35
+ function getGitRef(dir) {
36
+ const branch = runGit('rev-parse --abbrev-ref HEAD', dir);
37
+ if (branch && branch !== 'HEAD')
38
+ return branch;
39
+ const tag = runGit('describe --tags --exact-match HEAD', dir);
40
+ if (tag)
41
+ return tag;
42
+ const sha = runGit('rev-parse --short HEAD', dir);
43
+ return sha || 'unknown';
44
+ }
45
+ function describeDetachedHead(worktreePath, headSha) {
46
+ const tag = runGit('describe --tags --exact-match HEAD', worktreePath);
47
+ if (tag)
48
+ return tag;
49
+ return headSha ? headSha.slice(0, 7) : 'unknown';
50
+ }
51
+ /** Sibling worktrees of one repo share a common-dir — used as the dedup
52
+ * key so the scanner doesn't double-register when main + linked sit
53
+ * side-by-side in the scan root. */
54
+ function getGitCommonDir(dir) {
55
+ const out = runGit('rev-parse --git-common-dir', dir);
56
+ return out ? resolve(dir, out) : dir;
57
+ }
58
+ /** Index 0 of `git worktree list --porcelain` is always the main worktree.
59
+ * All entries share its basename as `name`, so display stays stable
60
+ * regardless of which sibling readdir hits first. */
61
+ function scanRepoFromAnyWorktree(anyWorktreePath) {
62
+ const fallback = [{
63
+ name: basename(anyWorktreePath),
64
+ path: anyWorktreePath,
65
+ type: 'repo',
66
+ branch: getGitRef(anyWorktreePath),
67
+ }];
68
+ const output = runGit('worktree list --porcelain', anyWorktreePath);
69
+ if (output === null)
70
+ return fallback;
71
+ const entries = [];
72
+ let currentPath = '';
73
+ let currentHead = '';
74
+ let currentBranch = '';
75
+ // runGit trims the trailing newline; append a sentinel so the final
76
+ // entry hits the empty-line flush branch below.
77
+ const lines = output.split('\n');
78
+ lines.push('');
79
+ for (const line of lines) {
80
+ if (line.startsWith('worktree ')) {
81
+ currentPath = line.slice('worktree '.length);
82
+ }
83
+ else if (line.startsWith('HEAD ')) {
84
+ currentHead = line.slice('HEAD '.length);
85
+ }
86
+ else if (line.startsWith('branch ')) {
87
+ currentBranch = line.slice('branch '.length).replace('refs/heads/', '');
88
+ }
89
+ else if (line === '') {
90
+ if (currentPath) {
91
+ const ref = currentBranch
92
+ || (currentHead ? describeDetachedHead(currentPath, currentHead) : 'unknown');
93
+ entries.push({ path: currentPath, branch: ref });
94
+ }
95
+ currentPath = '';
96
+ currentHead = '';
97
+ currentBranch = '';
98
+ }
44
99
  }
100
+ if (entries.length === 0)
101
+ return fallback;
102
+ const repoName = basename(entries[0].path);
103
+ return entries.map((wt, i) => ({
104
+ name: repoName,
105
+ path: wt.path,
106
+ type: i === 0 ? 'repo' : 'worktree',
107
+ branch: wt.branch,
108
+ }));
109
+ }
110
+ function compareProjects(a, b) {
111
+ if (a.type !== b.type)
112
+ return a.type === 'repo' ? -1 : 1;
113
+ return a.name.localeCompare(b.name) || a.branch.localeCompare(b.branch);
45
114
  }
46
115
  /**
47
116
  * Scan a directory for git repositories and their worktrees.
48
- * Returns a flat list of all projects found.
49
117
  */
50
118
  export function scanProjects(baseDir, maxDepth = 3) {
51
119
  const projects = [];
52
- const seen = new Set();
120
+ const seenRepos = new Set(); // by git-common-dir
121
+ const seenPaths = new Set(); // by absolute path
53
122
  function walk(dir, depth) {
54
123
  if (depth > maxDepth)
55
124
  return;
@@ -60,28 +129,19 @@ export function scanProjects(baseDir, maxDepth = 3) {
60
129
  catch {
61
130
  return;
62
131
  }
63
- // Check if this directory is a git repo
64
- if (entries.includes('.git')) {
65
- const realPath = dir;
66
- if (!seen.has(realPath)) {
67
- seen.add(realPath);
68
- projects.push({
69
- name: basename(realPath),
70
- path: realPath,
71
- type: 'repo',
72
- branch: getGitBranch(realPath),
73
- });
74
- // Also scan for worktrees
75
- for (const wt of getWorktrees(realPath)) {
76
- if (!seen.has(wt.path)) {
77
- seen.add(wt.path);
78
- projects.push(wt);
79
- }
132
+ if (entries.includes('.git') && isValidGitMarker(dir)) {
133
+ const commonDir = getGitCommonDir(dir);
134
+ if (seenRepos.has(commonDir))
135
+ return;
136
+ seenRepos.add(commonDir);
137
+ for (const p of scanRepoFromAnyWorktree(dir)) {
138
+ if (!seenPaths.has(p.path)) {
139
+ seenPaths.add(p.path);
140
+ projects.push(p);
80
141
  }
81
142
  }
82
- return; // Don't recurse into git repos
143
+ return;
83
144
  }
84
- // Recurse into subdirectories
85
145
  for (const entry of entries) {
86
146
  if (entry.startsWith('.') || entry === 'node_modules' || entry === 'vendor' || entry === 'dist')
87
147
  continue;
@@ -92,22 +152,17 @@ export function scanProjects(baseDir, maxDepth = 3) {
92
152
  }
93
153
  }
94
154
  catch {
95
- // Permission denied or broken symlink
155
+ // permission denied or broken symlink
96
156
  }
97
157
  }
98
158
  }
99
159
  walk(baseDir, 0);
100
- // Sort: repos first, then worktrees, alphabetically within each group
101
- projects.sort((a, b) => {
102
- if (a.type !== b.type)
103
- return a.type === 'repo' ? -1 : 1;
104
- return a.name.localeCompare(b.name);
105
- });
160
+ projects.sort(compareProjects);
106
161
  logger.info(`Scanned ${baseDir}: found ${projects.length} project(s)`);
107
162
  return projects;
108
163
  }
109
164
  /**
110
- * Scan multiple directories for git repositories, merge and deduplicate results.
165
+ * Scan multiple directories and deduplicate by path.
111
166
  */
112
167
  export function scanMultipleProjects(baseDirs, maxDepth = 3) {
113
168
  const seen = new Set();
@@ -120,12 +175,7 @@ export function scanMultipleProjects(baseDirs, maxDepth = 3) {
120
175
  }
121
176
  }
122
177
  }
123
- // Sort: repos first, then worktrees, alphabetically within each group
124
- merged.sort((a, b) => {
125
- if (a.type !== b.type)
126
- return a.type === 'repo' ? -1 : 1;
127
- return a.name.localeCompare(b.name);
128
- });
178
+ merged.sort(compareProjects);
129
179
  return merged;
130
180
  }
131
181
  //# sourceMappingURL=project-scanner.js.map