botmux 2.9.1 → 2.9.3
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.
- package/README.en.md +140 -76
- package/README.md +134 -75
- package/dist/adapters/backend/pty-backend.d.ts +6 -0
- package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
- package/dist/adapters/backend/pty-backend.js +10 -0
- package/dist/adapters/backend/pty-backend.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +11 -0
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -0
- package/dist/adapters/backend/session-backend-selector.js +26 -0
- package/dist/adapters/backend/session-backend-selector.js.map +1 -0
- package/dist/adapters/backend/tmux-backend.d.ts +80 -3
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +301 -49
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +100 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -0
- package/dist/adapters/backend/tmux-pipe-backend.js +473 -0
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -0
- package/dist/adapters/cli/aiden.d.ts.map +1 -1
- package/dist/adapters/cli/aiden.js +5 -0
- package/dist/adapters/cli/aiden.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts +40 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +470 -49
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +191 -9
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +94 -17
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +2 -2
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +7 -5
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +38 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/autostart.d.ts +14 -0
- package/dist/autostart.d.ts.map +1 -0
- package/dist/autostart.js +357 -0
- package/dist/autostart.js.map +1 -0
- package/dist/bot-registry.d.ts +29 -3
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +91 -12
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/arg-utils.d.ts +11 -0
- package/dist/cli/arg-utils.d.ts.map +1 -0
- package/dist/cli/arg-utils.js +25 -0
- package/dist/cli/arg-utils.js.map +1 -0
- package/dist/cli/create-group-resolver.d.ts +32 -0
- package/dist/cli/create-group-resolver.d.ts.map +1 -0
- package/dist/cli/create-group-resolver.js +70 -0
- package/dist/cli/create-group-resolver.js.map +1 -0
- package/dist/cli/quoted-render.d.ts +30 -0
- package/dist/cli/quoted-render.d.ts.map +1 -0
- package/dist/cli/quoted-render.js +29 -0
- package/dist/cli/quoted-render.js.map +1 -0
- package/dist/cli.js +916 -272
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -8
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts +43 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +167 -64
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-events.d.ts +57 -0
- package/dist/core/dashboard-events.d.ts.map +1 -0
- package/dist/core/dashboard-events.js +23 -0
- package/dist/core/dashboard-events.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts +43 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
- package/dist/core/dashboard-ipc-server.js +481 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -0
- package/dist/core/dashboard-locate.d.ts +20 -0
- package/dist/core/dashboard-locate.d.ts.map +1 -0
- package/dist/core/dashboard-locate.js +26 -0
- package/dist/core/dashboard-locate.js.map +1 -0
- package/dist/core/dashboard-rows.d.ts +31 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -0
- package/dist/core/dashboard-rows.js +65 -0
- package/dist/core/dashboard-rows.js.map +1 -0
- package/dist/core/inherit-peer.d.ts +14 -0
- package/dist/core/inherit-peer.d.ts.map +1 -0
- package/dist/core/inherit-peer.js +32 -0
- package/dist/core/inherit-peer.js.map +1 -0
- package/dist/core/scheduler.d.ts +24 -0
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +93 -2
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-activity.d.ts +3 -0
- package/dist/core/session-activity.d.ts.map +1 -0
- package/dist/core/session-activity.js +20 -0
- package/dist/core/session-activity.js.map +1 -0
- package/dist/core/session-discovery.d.ts +39 -0
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +114 -21
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +72 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +396 -106
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +27 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +14 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +72 -3
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +459 -38
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +645 -314
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +41 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -0
- package/dist/dashboard/aggregator.js +125 -0
- package/dist/dashboard/aggregator.js.map +1 -0
- package/dist/dashboard/auth.d.ts +23 -0
- package/dist/dashboard/auth.d.ts.map +1 -0
- package/dist/dashboard/auth.js +66 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/operator-selector.d.ts +20 -0
- package/dist/dashboard/operator-selector.d.ts.map +1 -0
- package/dist/dashboard/operator-selector.js +39 -0
- package/dist/dashboard/operator-selector.js.map +1 -0
- package/dist/dashboard/registry.d.ts +35 -0
- package/dist/dashboard/registry.d.ts.map +1 -0
- package/dist/dashboard/registry.js +74 -0
- package/dist/dashboard/registry.js.map +1 -0
- package/dist/dashboard/web/app.d.ts +2 -0
- package/dist/dashboard/web/app.d.ts.map +1 -0
- package/dist/dashboard/web/app.js +45 -0
- package/dist/dashboard/web/app.js.map +1 -0
- package/dist/dashboard/web/bot-defaults.d.ts +2 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -0
- package/dist/dashboard/web/bot-defaults.js +201 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -0
- package/dist/dashboard/web/groups.d.ts +16 -0
- package/dist/dashboard/web/groups.d.ts.map +1 -0
- package/dist/dashboard/web/groups.js +584 -0
- package/dist/dashboard/web/groups.js.map +1 -0
- package/dist/dashboard/web/schedules.d.ts +2 -0
- package/dist/dashboard/web/schedules.d.ts.map +1 -0
- package/dist/dashboard/web/schedules.js +105 -0
- package/dist/dashboard/web/schedules.js.map +1 -0
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -0
- package/dist/dashboard/web/sessions.js +374 -0
- package/dist/dashboard/web/sessions.js.map +1 -0
- package/dist/dashboard/web/store.d.ts +23 -0
- package/dist/dashboard/web/store.d.ts.map +1 -0
- package/dist/dashboard/web/store.js +82 -0
- package/dist/dashboard/web/store.js.map +1 -0
- package/dist/dashboard-web/app.js +263 -0
- package/dist/dashboard-web/index.html +23 -0
- package/dist/dashboard-web/style.css +93 -0
- package/dist/dashboard.d.ts +2 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +639 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +18 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +70 -9
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +123 -109
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +35 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +114 -11
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +88 -6
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +398 -62
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/forwarded-renderer.d.ts +23 -0
- package/dist/im/lark/forwarded-renderer.d.ts.map +1 -0
- package/dist/im/lark/forwarded-renderer.js +105 -0
- package/dist/im/lark/forwarded-renderer.js.map +1 -0
- package/dist/im/lark/md-card.d.ts +73 -0
- package/dist/im/lark/md-card.d.ts.map +1 -0
- package/dist/im/lark/md-card.js +332 -0
- package/dist/im/lark/md-card.js.map +1 -0
- package/dist/im/lark/merge-forward.d.ts +32 -0
- package/dist/im/lark/merge-forward.d.ts.map +1 -0
- package/dist/im/lark/merge-forward.js +110 -0
- package/dist/im/lark/merge-forward.js.map +1 -0
- package/dist/im/lark/message-parser.d.ts +9 -3
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +48 -13
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/quote-hint.d.ts +18 -0
- package/dist/im/lark/quote-hint.d.ts.map +1 -0
- package/dist/im/lark/quote-hint.js +23 -0
- package/dist/im/lark/quote-hint.js.map +1 -0
- package/dist/services/bridge-fallback-gate.d.ts +42 -0
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -0
- package/dist/services/bridge-fallback-gate.js +12 -0
- package/dist/services/bridge-fallback-gate.js.map +1 -0
- package/dist/services/bridge-rotation-policy.d.ts +139 -0
- package/dist/services/bridge-rotation-policy.d.ts.map +1 -0
- package/dist/services/bridge-rotation-policy.js +125 -0
- package/dist/services/bridge-rotation-policy.js.map +1 -0
- package/dist/services/bridge-turn-queue.d.ts +154 -0
- package/dist/services/bridge-turn-queue.d.ts.map +1 -0
- package/dist/services/bridge-turn-queue.js +316 -0
- package/dist/services/bridge-turn-queue.js.map +1 -0
- package/dist/services/chat-first-seen-store.d.ts +27 -0
- package/dist/services/chat-first-seen-store.d.ts.map +1 -0
- package/dist/services/chat-first-seen-store.js +114 -0
- package/dist/services/chat-first-seen-store.js.map +1 -0
- package/dist/services/claude-transcript.d.ts +268 -0
- package/dist/services/claude-transcript.d.ts.map +1 -0
- package/dist/services/claude-transcript.js +798 -0
- package/dist/services/claude-transcript.js.map +1 -0
- package/dist/services/coco-transcript.d.ts +35 -0
- package/dist/services/coco-transcript.d.ts.map +1 -0
- package/dist/services/coco-transcript.js +192 -0
- package/dist/services/coco-transcript.js.map +1 -0
- package/dist/services/codex-bridge-queue.d.ts +56 -0
- package/dist/services/codex-bridge-queue.d.ts.map +1 -0
- package/dist/services/codex-bridge-queue.js +150 -0
- package/dist/services/codex-bridge-queue.js.map +1 -0
- package/dist/services/codex-transcript.d.ts +84 -0
- package/dist/services/codex-transcript.d.ts.map +1 -0
- package/dist/services/codex-transcript.js +298 -0
- package/dist/services/codex-transcript.js.map +1 -0
- package/dist/services/group-creator.d.ts +23 -0
- package/dist/services/group-creator.d.ts.map +1 -0
- package/dist/services/group-creator.js +75 -0
- package/dist/services/group-creator.js.map +1 -0
- package/dist/services/groups-store.d.ts +98 -0
- package/dist/services/groups-store.d.ts.map +1 -0
- package/dist/services/groups-store.js +213 -0
- package/dist/services/groups-store.js.map +1 -0
- package/dist/services/oncall-store.d.ts +80 -8
- package/dist/services/oncall-store.d.ts.map +1 -1
- package/dist/services/oncall-store.js +265 -55
- package/dist/services/oncall-store.js.map +1 -1
- package/dist/services/project-scanner.d.ts +1 -2
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +118 -68
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/schedule-store.d.ts +5 -0
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +77 -1
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/services/session-store.d.ts +22 -0
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +62 -4
- package/dist/services/session-store.js.map +1 -1
- package/dist/setup/bots-store.d.ts +3 -0
- package/dist/setup/bots-store.d.ts.map +1 -0
- package/dist/setup/bots-store.js +24 -0
- package/dist/setup/bots-store.js.map +1 -0
- package/dist/setup/detect-platform.d.ts +14 -0
- package/dist/setup/detect-platform.d.ts.map +1 -0
- package/dist/setup/detect-platform.js +139 -0
- package/dist/setup/detect-platform.js.map +1 -0
- package/dist/setup/ensure-fonts.d.ts +13 -0
- package/dist/setup/ensure-fonts.d.ts.map +1 -0
- package/dist/setup/ensure-fonts.js +225 -0
- package/dist/setup/ensure-fonts.js.map +1 -0
- package/dist/setup/ensure-tmux.d.ts +60 -0
- package/dist/setup/ensure-tmux.d.ts.map +1 -0
- package/dist/setup/ensure-tmux.js +236 -0
- package/dist/setup/ensure-tmux.js.map +1 -0
- package/dist/setup/index.d.ts +9 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +46 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/lark-scopes.json +301 -0
- package/dist/setup/register-app.d.ts +52 -0
- package/dist/setup/register-app.d.ts.map +1 -0
- package/dist/setup/register-app.js +91 -0
- package/dist/setup/register-app.js.map +1 -0
- package/dist/setup/verify-permissions.d.ts +115 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -0
- package/dist/setup/verify-permissions.js +207 -0
- package/dist/setup/verify-permissions.js.map +1 -0
- package/dist/skills/definitions.d.ts +4 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +133 -19
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/installer.d.ts +3 -1
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +18 -3
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bot-routing.d.ts +6 -0
- package/dist/utils/bot-routing.d.ts.map +1 -0
- package/dist/utils/bot-routing.js +50 -0
- package/dist/utils/bot-routing.js.map +1 -0
- package/dist/utils/file-lock.d.ts +2 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +114 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/font-installer.js +1 -1
- package/dist/utils/font-installer.js.map +1 -1
- package/dist/utils/idle-detector.d.ts +6 -0
- package/dist/utils/idle-detector.d.ts.map +1 -1
- package/dist/utils/idle-detector.js +25 -4
- package/dist/utils/idle-detector.js.map +1 -1
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +60 -8
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/render-dimensions.d.ts +48 -0
- package/dist/utils/render-dimensions.d.ts.map +1 -0
- package/dist/utils/render-dimensions.js +55 -0
- package/dist/utils/render-dimensions.js.map +1 -0
- package/dist/utils/screen-analyzer.d.ts.map +1 -1
- package/dist/utils/screen-analyzer.js +24 -0
- package/dist/utils/screen-analyzer.js.map +1 -1
- package/dist/utils/screenshot-renderer.d.ts.map +1 -1
- package/dist/utils/screenshot-renderer.js +67 -23
- package/dist/utils/screenshot-renderer.js.map +1 -1
- package/dist/utils/terminal-renderer.d.ts +16 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +40 -23
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/utils/transient-snapshot.d.ts +28 -0
- package/dist/utils/transient-snapshot.d.ts.map +1 -0
- package/dist/utils/transient-snapshot.js +96 -0
- package/dist/utils/transient-snapshot.js.map +1 -0
- package/dist/worker.js +2248 -83
- package/dist/worker.js.map +1 -1
- package/package.json +12 -5
|
@@ -1,53 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Oncall bindings — persist chat_id → default workingDir
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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
|
|
10
|
-
const
|
|
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
|
|
26
|
+
return raw;
|
|
17
27
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
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.
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cur
|
|
49
|
-
|
|
50
|
-
|
|
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}
|
|
59
|
-
return { ok: true, entry: next, created:
|
|
99
|
+
logger.info(`[oncall:${larkAppId}] bind chat=${chatId} dir=${workingDir}`);
|
|
100
|
+
return { ok: true, entry: next, created: r.result.created };
|
|
60
101
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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":"AAAA
|
|
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
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
st = statSync(gitPath);
|
|
8
13
|
}
|
|
9
14
|
catch {
|
|
10
|
-
return
|
|
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
|
|
23
|
+
function runGit(args, cwd) {
|
|
14
24
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
path
|
|
71
|
-
|
|
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;
|
|
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
|
-
//
|
|
155
|
+
// permission denied or broken symlink
|
|
96
156
|
}
|
|
97
157
|
}
|
|
98
158
|
}
|
|
99
159
|
walk(baseDir, 0);
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
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
|