aicodeman 1.47.0 → 1.49.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.
- package/README.md +15 -3
- package/dist/cli-notify.d.ts +34 -0
- package/dist/cli-notify.d.ts.map +1 -0
- package/dist/cli-notify.js +100 -0
- package/dist/cli-notify.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/cli-registry/patterns.d.ts +1 -1
- package/dist/config/cli-registry/patterns.d.ts.map +1 -1
- package/dist/config/cli-registry/patterns.js +4 -0
- package/dist/config/cli-registry/patterns.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +7 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +67 -3
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +114 -31
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +51 -21
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/foreign-tmux.d.ts +48 -0
- package/dist/config/foreign-tmux.d.ts.map +1 -0
- package/dist/config/foreign-tmux.js +48 -0
- package/dist/config/foreign-tmux.js.map +1 -0
- package/dist/config/path-probe.d.ts +6 -0
- package/dist/config/path-probe.d.ts.map +1 -1
- package/dist/config/path-probe.js +6 -0
- package/dist/config/path-probe.js.map +1 -1
- package/dist/config-backup.d.ts +1 -1
- package/dist/config-backup.d.ts.map +1 -1
- package/dist/config-backup.js +2 -0
- package/dist/config-backup.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +32 -8
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js +10 -0
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/custom-model-injection-apply.d.ts +21 -5
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +121 -27
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/custom-model-injection.d.ts +6 -0
- package/dist/custom-model-injection.d.ts.map +1 -1
- package/dist/custom-model-injection.js +92 -10
- package/dist/custom-model-injection.js.map +1 -1
- package/dist/discord-bridge.d.ts +338 -0
- package/dist/discord-bridge.d.ts.map +1 -0
- package/dist/discord-bridge.js +1256 -0
- package/dist/discord-bridge.js.map +1 -0
- package/dist/docker-hosts.d.ts +8 -1
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +5 -5
- package/dist/docker-hosts.js.map +1 -1
- package/dist/edit-history-local-source.d.ts +83 -0
- package/dist/edit-history-local-source.d.ts.map +1 -0
- package/dist/edit-history-local-source.js +198 -0
- package/dist/edit-history-local-source.js.map +1 -0
- package/dist/edit-history-parse.d.ts +114 -0
- package/dist/edit-history-parse.d.ts.map +1 -0
- package/dist/edit-history-parse.js +312 -0
- package/dist/edit-history-parse.js.map +1 -0
- package/dist/edit-history-store.d.ts +209 -0
- package/dist/edit-history-store.d.ts.map +1 -0
- package/dist/edit-history-store.js +599 -0
- package/dist/edit-history-store.js.map +1 -0
- package/dist/foreign-tmux-discovery.d.ts +140 -0
- package/dist/foreign-tmux-discovery.d.ts.map +1 -0
- package/dist/foreign-tmux-discovery.js +366 -0
- package/dist/foreign-tmux-discovery.js.map +1 -0
- package/dist/foreign-tmux.d.ts +381 -0
- package/dist/foreign-tmux.d.ts.map +1 -0
- package/dist/foreign-tmux.js +775 -0
- package/dist/foreign-tmux.js.map +1 -0
- package/dist/git-clone.d.ts +6 -1
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +79 -14
- package/dist/git-clone.js.map +1 -1
- package/dist/git-fetch-scheduler.d.ts +2 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -1
- package/dist/git-fetch-scheduler.js +1 -1
- package/dist/git-fetch-scheduler.js.map +1 -1
- package/dist/git-fetch.d.ts +1 -1
- package/dist/git-fetch.d.ts.map +1 -1
- package/dist/git-workspace-status.d.ts +2 -2
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/hooks-config.d.ts +2 -1
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +2 -1
- package/dist/hooks-config.js.map +1 -1
- package/dist/image-watcher.js +4 -4
- package/dist/image-watcher.js.map +1 -1
- package/dist/live-worktree.d.ts +53 -0
- package/dist/live-worktree.d.ts.map +1 -0
- package/dist/live-worktree.js +97 -0
- package/dist/live-worktree.js.map +1 -0
- package/dist/mux-interface.d.ts +22 -2
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/reboot-restore.d.ts +1 -1
- package/dist/reboot-restore.d.ts.map +1 -1
- package/dist/reboot-restore.js +9 -0
- package/dist/reboot-restore.js.map +1 -1
- package/dist/remote-files.d.ts +150 -5
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +505 -7
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-hosts.d.ts +15 -9
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +42 -20
- package/dist/remote-hosts.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +23 -8
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-env-clamp.d.ts +1 -1
- package/dist/session-env-clamp.d.ts.map +1 -1
- package/dist/session-env-clamp.js +4 -3
- package/dist/session-env-clamp.js.map +1 -1
- package/dist/session.d.ts +161 -24
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +337 -37
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +114 -12
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +318 -41
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/discord.d.ts +55 -0
- package/dist/types/discord.d.ts.map +1 -0
- package/dist/types/discord.js +18 -0
- package/dist/types/discord.js.map +1 -0
- package/dist/types/foreign-tmux.d.ts +101 -0
- package/dist/types/foreign-tmux.d.ts.map +1 -0
- package/dist/types/foreign-tmux.js +27 -0
- package/dist/types/foreign-tmux.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +119 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/update.d.ts +4 -1
- package/dist/types/update.d.ts.map +1 -1
- package/dist/web/case-path.d.ts +2 -0
- package/dist/web/case-path.d.ts.map +1 -1
- package/dist/web/case-path.js +3 -3
- package/dist/web/case-path.js.map +1 -1
- package/dist/web/edit-history-sync.d.ts +178 -0
- package/dist/web/edit-history-sync.d.ts.map +1 -0
- package/dist/web/edit-history-sync.js +461 -0
- package/dist/web/edit-history-sync.js.map +1 -0
- package/dist/web/launch-defaults.d.ts +1 -1
- package/dist/web/launch-defaults.js +2 -2
- package/dist/web/launch-defaults.js.map +1 -1
- package/dist/web/linked-cases-writer.d.ts +43 -0
- package/dist/web/linked-cases-writer.d.ts.map +1 -0
- package/dist/web/linked-cases-writer.js +107 -0
- package/dist/web/linked-cases-writer.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +10 -1
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +10 -2
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +13 -0
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/{app.fe8a6b4c.js → app.9ea37070.js} +6 -6
- package/dist/web/public/app.9ea37070.js.br +0 -0
- package/dist/web/public/app.9ea37070.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
- package/dist/web/public/constants.782170e1.js.br +0 -0
- package/dist/web/public/constants.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +643 -0
- package/dist/web/public/edit-history-ui.js.br +0 -0
- package/dist/web/public/edit-history-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/foreign-sessions.js +476 -0
- package/dist/web/public/foreign-sessions.js.br +0 -0
- package/dist/web/public/foreign-sessions.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +5 -3
- package/dist/web/public/git-status-ui.js.br +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.a5e82c95.js +2 -0
- package/dist/web/public/i18n.a5e82c95.js.br +0 -0
- package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +183 -18
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.ff64fafc.js → keyboard-accessory.b941676c.js} +75 -2
- package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
- package/dist/web/public/keyboard-accessory.b941676c.js.gz +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +10 -0
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.67c8eb5b.css +1 -0
- package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
- package/dist/web/public/mobile.67c8eb5b.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.4a56ee63.js → panels-ui.8bcd4473.js} +5 -5
- package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
- package/dist/web/public/panels-ui.8bcd4473.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/{respawn-ui.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
- package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
- package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/security-notices.json +1 -21
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.426fb671.js → session-ui.a7c8c095.js} +30 -30
- package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
- package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
- package/dist/web/public/settings-ui.18f92052.js +106 -0
- package/dist/web/public/settings-ui.18f92052.js.br +0 -0
- package/dist/web/public/settings-ui.18f92052.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.6a718331.css +1 -0
- package/dist/web/public/styles.6a718331.css.br +0 -0
- package/dist/web/public/styles.6a718331.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +2 -2
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.a7883640.js +4 -0
- package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
- package/dist/web/public/terminal-ui.a7883640.js.gz +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -8
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +165 -50
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +5 -1
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
- package/dist/web/routes/config-backup-routes.js +4 -1
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/custom-model-routes.d.ts +7 -2
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +51 -6
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.d.ts +25 -0
- package/dist/web/routes/discord-routes.d.ts.map +1 -0
- package/dist/web/routes/discord-routes.js +206 -0
- package/dist/web/routes/discord-routes.js.map +1 -0
- package/dist/web/routes/edit-history-routes.d.ts +43 -0
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
- package/dist/web/routes/edit-history-routes.js +107 -0
- package/dist/web/routes/edit-history-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +273 -102
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +7 -2
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/mux-routes.d.ts +10 -3
- package/dist/web/routes/mux-routes.d.ts.map +1 -1
- package/dist/web/routes/mux-routes.js +59 -1
- package/dist/web/routes/mux-routes.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +22 -0
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
- package/dist/web/routes/readmymind-routes.js +3 -2
- package/dist/web/routes/readmymind-routes.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +24 -0
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +21 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +338 -55
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +4 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +92 -2
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +111 -10
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/self-update.d.ts +9 -0
- package/dist/web/self-update.d.ts.map +1 -1
- package/dist/web/self-update.js +19 -3
- package/dist/web/self-update.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +15 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +56 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +285 -17
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +13 -0
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +16 -8
- package/dist/web/session-wait-registry.js.map +1 -1
- package/package.json +2 -2
- package/skills/codeman/SKILL.md +20 -4
- package/dist/web/public/app.fe8a6b4c.js.br +0 -0
- package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/i18n.a618ef4e.js +0 -2
- package/dist/web/public/i18n.a618ef4e.js.br +0 -0
- package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +0 -1
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/session-ui.426fb671.js.br +0 -0
- package/dist/web/public/session-ui.426fb671.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +0 -106
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/styles.4f0c67a0.css +0 -1
- package/dist/web/public/styles.4f0c67a0.css.br +0 -0
- package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
- package/dist/web/public/terminal-ui.7e787829.js +0 -4
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
|
@@ -15,6 +15,8 @@ import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.
|
|
|
15
15
|
import { CreateCaseSchema, CloneCaseSchema, ClonePreflightSchema, LinkCaseSchema, CaseOrderSchema, CaseDefaultGroupSchema, RenameCaseSchema, RemoteCaseLinkSchema, RemoteHostSchema, DockerCaseLinkSchema, DockerCaseAdoptSchema, DockerAdoptPreflightSchema, DockerBrowseSchema, DockerHostSchema, DockerExportSchema, DockerImportSchema, DockerQuickCreateSchema, } from '../schemas.js';
|
|
16
16
|
import { exportDockerCase, importDockerBundle, listDockerExports, exportBundleName } from '../../docker-export.js';
|
|
17
17
|
import { cloneRepository, isGitAvailable, isSafeGitRef, parseGitRepositoryUrl, probeGitRemote, } from '../../git-clone.js';
|
|
18
|
+
import { isPathWithin } from '../../git-worktrees.js';
|
|
19
|
+
import { defaultDockerWorkspaces } from './git-status-routes.js';
|
|
18
20
|
import { generateClaudeMd } from '../../templates/claude-md.js';
|
|
19
21
|
import { prepareNewCasePath } from '../case-path.js';
|
|
20
22
|
import { CaseRenameError, isCaseSessionStarting, moveCaseFolder, moveClaudeHistory, renameInOrder, siblingPath, withCaseLock, } from '../../case-rename.js';
|
|
@@ -23,6 +25,7 @@ import { readAgentCaseMarker } from '../../agent-case-marker.js';
|
|
|
23
25
|
import { settingsWriteBlocker, writeHooksConfig } from '../../hooks-config.js';
|
|
24
26
|
import { canAccessOwned, getAuthUser, isAdmin, isWorkingDirAllowed, ownerFor, requireAdmin, resolveCasesDir, SETTINGS_PATH, validatePathWithinBase, parseBody, readJsonConfig, } from '../route-helpers.js';
|
|
25
27
|
import { assertSettingsWritable, mutateSettingsFile, SettingsWriteRefusedError, settingsWriteErrorResponse, } from '../settings-writer.js';
|
|
28
|
+
import { LinkedCasesWriteRefusedError, linkedCasesPath, mutateLinkedCases } from '../linked-cases-writer.js';
|
|
26
29
|
import { isMultiUserMode } from '../../config/multiuser.js';
|
|
27
30
|
import { SseEvent } from '../sse-events.js';
|
|
28
31
|
import { ownerLayoutKey } from '../../tab-layout-persistence.js';
|
|
@@ -31,7 +34,6 @@ import { dataPath, getDataDir } from '../../config/instance.js';
|
|
|
31
34
|
import { checkDockerAvailable, checkDockerImagePresent, checkDockerTmuxAvailable, ensureAgentBaseImage, DEFAULT_AGENT_IMAGE, dockerContainerName, dockerDisplayPath, probeAdoptableContainer, classifyAdoptContainerConflict, listDockerContainers, browseInContainer, dockerAdoptProbeModes, readDockerCases, readDockerHosts, removeDockerContainer, toSessionDocker, writeDockerCases, writeDockerHosts, } from '../../docker-hosts.js';
|
|
32
35
|
import { buildDockerRemoveCommand } from '../../tmux-manager.js';
|
|
33
36
|
import { checkRemoteTmuxAvailable, listRemoteCodemanSessions, readRemoteCases, readRemoteHosts, remoteDisplayPath, writeRemoteCases, writeRemoteHosts, } from '../../remote-hosts.js';
|
|
34
|
-
const LINKED_CASES_FILE = dataPath('linked-cases.json');
|
|
35
37
|
const CODEMAN_CONFIG_DIR = getDataDir();
|
|
36
38
|
const SAFE_CASE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
37
39
|
const DOCKER_EXPORTS_DIR = dataPath('docker-exports');
|
|
@@ -54,6 +56,8 @@ const APP_VERSION = (() => {
|
|
|
54
56
|
* `/api/cases/link` is admin-only for).
|
|
55
57
|
*/
|
|
56
58
|
const LOCAL_CLONE_ADMIN_ONLY = 'Cloning from a local path is admin-only in multi-user mode. Use a repository URL instead.';
|
|
59
|
+
/** Refusal text for a clone at or inside a Docker case workspace (git would run its hooks on the host). */
|
|
60
|
+
const DOCKER_CLONE_REFUSAL = 'Cloning is not available into a folder inside a Docker case workspace: a container can write there, and git would run the new repository’s hooks and filters on this machine. Clone outside the workspace and link it, or clone from inside the container.';
|
|
57
61
|
/**
|
|
58
62
|
* Whether a clone or preflight must run with git's credential helpers cleared:
|
|
59
63
|
* a non-admin in multi-user mode. Every user's git runs as the one server
|
|
@@ -106,10 +110,19 @@ function agentCreatedInfo(marker) {
|
|
|
106
110
|
...(marker.mode ? { mode: marker.mode } : {}),
|
|
107
111
|
};
|
|
108
112
|
}
|
|
109
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Read and parse linked-cases.json, returning empty object on missing/invalid file. For reads only:
|
|
115
|
+
* every write goes through `mutateLinkedCases()`, which re-reads the file inside its own critical
|
|
116
|
+
* section and refuses one that does not parse.
|
|
117
|
+
*/
|
|
110
118
|
async function readLinkedCases() {
|
|
111
|
-
return readJsonConfig(
|
|
119
|
+
return readJsonConfig(linkedCasesPath(), 'linked cases', {});
|
|
112
120
|
}
|
|
121
|
+
/** A conflict thrown from inside a `mutateLinkedCases()` callback: answered 409, nothing written. */
|
|
122
|
+
function linkedConflict(message) {
|
|
123
|
+
return Object.assign(new Error(message), { conflict: true });
|
|
124
|
+
}
|
|
125
|
+
const isLinkedConflict = (err) => err.conflict === true;
|
|
113
126
|
/**
|
|
114
127
|
* Resolve a case name to its directory path, checking linked cases first, then the
|
|
115
128
|
* user's case space (per-user in multi-user mode, the shared CASES_DIR otherwise).
|
|
@@ -418,17 +431,12 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
418
431
|
made.push(join(casePath, 'CLAUDE.md'));
|
|
419
432
|
made.push(join(casePath, '.claude')); // before the write, so a half-written one is undone too
|
|
420
433
|
await writeHooksConfig(casePath);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const fresh = await readLinkedCases();
|
|
428
|
-
if (fresh[name])
|
|
429
|
-
throw Object.assign(new Error(`Case "${name}" was just linked`), { conflict: true });
|
|
430
|
-
fresh[name] = casePath;
|
|
431
|
-
await fs.writeFile(LINKED_CASES_FILE, JSON.stringify(fresh, null, 2));
|
|
434
|
+
// The check above ran before the scaffold; a case linked since then wins and this call is undone.
|
|
435
|
+
await mutateLinkedCases((current) => {
|
|
436
|
+
if (current[name])
|
|
437
|
+
throw linkedConflict(`Case "${name}" was just linked`);
|
|
438
|
+
return { ...current, [name]: casePath };
|
|
439
|
+
});
|
|
432
440
|
ctx.broadcast(SseEvent.CaseCreated, { name, path: casePath });
|
|
433
441
|
return { success: true, data: { case: { name, path: casePath } } };
|
|
434
442
|
}
|
|
@@ -437,10 +445,14 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
437
445
|
// entries inside the empty folder the user picked; never anything else.
|
|
438
446
|
for (const p of made.reverse())
|
|
439
447
|
await fs.rm(p, { recursive: true, force: true }).catch(() => undefined);
|
|
440
|
-
if (err
|
|
448
|
+
if (isLinkedConflict(err)) {
|
|
441
449
|
reply.code(409);
|
|
442
450
|
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, getErrorMessage(err));
|
|
443
451
|
}
|
|
452
|
+
if (err instanceof LinkedCasesWriteRefusedError) {
|
|
453
|
+
reply.code(409);
|
|
454
|
+
return createErrorResponse(ApiErrorCode.CONFLICT, err.message);
|
|
455
|
+
}
|
|
444
456
|
reply.code(500);
|
|
445
457
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
446
458
|
}
|
|
@@ -519,7 +531,8 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
519
531
|
* request died mid-clone still sees the case appear over SSE when git finishes.
|
|
520
532
|
*
|
|
521
533
|
* Deliberately NOT admin-gated in multi-user mode: unlike `/api/cases/link`,
|
|
522
|
-
* this writes only inside the caller's own `resolveCasesDir
|
|
534
|
+
* this writes only inside the caller's own `resolveCasesDir`, EXCEPT with a `path` (a custom folder,
|
|
535
|
+
* registered as a linked case in the shared registry), which is admin-only like Link Existing. Two things would
|
|
523
536
|
* otherwise read through that boundary: a `local`-transport source (refused for
|
|
524
537
|
* non-admins) and the server account's git credential helpers, which every user
|
|
525
538
|
* shares (cleared for non-admins, see `cloneWithoutCredentialHelpers`).
|
|
@@ -531,7 +544,7 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
531
544
|
* user's machine the moment a session starts in the case.
|
|
532
545
|
*/
|
|
533
546
|
app.post('/api/cases/clone', async (req, reply) => {
|
|
534
|
-
const { name, repository, ref, shallow, description } = parseBody(CloneCaseSchema, req.body);
|
|
547
|
+
const { name, repository, ref, shallow, description, path: customPath } = parseBody(CloneCaseSchema, req.body);
|
|
535
548
|
const user = getAuthUser(req);
|
|
536
549
|
const parsed = parseGitRepositoryUrl(repository);
|
|
537
550
|
if (!parsed.cloneable)
|
|
@@ -546,8 +559,52 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
546
559
|
if (!isGitAvailable()) {
|
|
547
560
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'git is not installed on this machine (or not on the server’s PATH).');
|
|
548
561
|
}
|
|
562
|
+
// A custom folder writes outside the cases directory and registers the path in the shared,
|
|
563
|
+
// ownerless linked-cases registry, so it carries the same bar as POST /api/cases/link.
|
|
564
|
+
if (customPath !== undefined) {
|
|
565
|
+
const denied = adminOnly(req, reply);
|
|
566
|
+
if (denied)
|
|
567
|
+
return denied;
|
|
568
|
+
}
|
|
549
569
|
const casesDir = resolveCasesDir(user);
|
|
550
|
-
|
|
570
|
+
let casePath;
|
|
571
|
+
let intoEmptyDir = false;
|
|
572
|
+
if (customPath !== undefined) {
|
|
573
|
+
const prepared = await prepareNewCasePath(customPath, {
|
|
574
|
+
home: homedir(),
|
|
575
|
+
dataDir: getDataDir(),
|
|
576
|
+
casesDirs: [...new Set([casesDir, resolveCasesDir()])],
|
|
577
|
+
casesDirAdvice: 'clone there without a custom folder',
|
|
578
|
+
});
|
|
579
|
+
if (!prepared.ok) {
|
|
580
|
+
reply.code(prepared.code === 'UNREACHABLE'
|
|
581
|
+
? 422
|
|
582
|
+
: prepared.code === 'NOT_FOUND'
|
|
583
|
+
? 404
|
|
584
|
+
: prepared.code === 'EXISTS'
|
|
585
|
+
? 409
|
|
586
|
+
: 400);
|
|
587
|
+
return createErrorResponse(prepared.code === 'UNREACHABLE'
|
|
588
|
+
? ApiErrorCode.OPERATION_FAILED
|
|
589
|
+
: prepared.code === 'NOT_FOUND'
|
|
590
|
+
? ApiErrorCode.NOT_FOUND
|
|
591
|
+
: prepared.code === 'EXISTS'
|
|
592
|
+
? ApiErrorCode.ALREADY_EXISTS
|
|
593
|
+
: ApiErrorCode.INVALID_INPUT, prepared.code === 'EXISTS'
|
|
594
|
+
? `${customPath} already has files in it. The clone target must be empty or not exist yet.`
|
|
595
|
+
: prepared.reason);
|
|
596
|
+
}
|
|
597
|
+
intoEmptyDir = prepared.existedEmpty;
|
|
598
|
+
casePath = prepared.path;
|
|
599
|
+
// The same folder listed as a local case in the cases dir would make deleting it remove files.
|
|
600
|
+
if (existsSync(join(casesDir, name))) {
|
|
601
|
+
reply.code(409);
|
|
602
|
+
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'A case with this name already exists in codeman-cases.');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
casePath = validatePathWithinBase(name, casesDir);
|
|
607
|
+
}
|
|
551
608
|
if (!casePath)
|
|
552
609
|
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path');
|
|
553
610
|
// Reject a duplicate name across EVERY case kind before invoking git, so a
|
|
@@ -556,22 +613,52 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
556
613
|
const linkedCases = await readLinkedCases();
|
|
557
614
|
const dockerCases = await readDockerCases(CODEMAN_CONFIG_DIR);
|
|
558
615
|
const remoteCases = await readRemoteCases(CODEMAN_CONFIG_DIR);
|
|
559
|
-
|
|
616
|
+
const linkedAs = Object.entries(linkedCases).find(([, p]) => p === casePath)?.[0];
|
|
617
|
+
if (linkedAs && customPath !== undefined) {
|
|
618
|
+
reply.code(409);
|
|
619
|
+
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, `That folder is already the case "${linkedAs}"`);
|
|
620
|
+
}
|
|
621
|
+
if (
|
|
622
|
+
// A custom folder was judged by prepareNewCasePath (an existing empty one is fine).
|
|
623
|
+
(customPath === undefined && existsSync(casePath)) ||
|
|
560
624
|
linkedCases[name] ||
|
|
561
625
|
dockerCases.some((item) => item.name === name) ||
|
|
562
626
|
remoteCases.some((item) => item.name === name)) {
|
|
563
627
|
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'Case already exists');
|
|
564
628
|
}
|
|
565
|
-
// git creates the leaf, not necessarily the case space above it
|
|
629
|
+
// git creates the leaf, not necessarily the case space above it (a custom folder's parent
|
|
630
|
+
// already exists: prepareNewCasePath refused a missing one).
|
|
631
|
+
if (customPath === undefined) {
|
|
632
|
+
try {
|
|
633
|
+
mkdirSync(casesDir, { recursive: true });
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// git runs the new repository's hooks and filters on THIS machine once the clone finishes, and a container
|
|
640
|
+
// with the workspace bind-mounted can write both while it fetches. Like git status, fetch and worktrees,
|
|
641
|
+
// never clone at or inside a Docker case workspace. Fails closed when the Docker case list cannot be read.
|
|
566
642
|
try {
|
|
567
|
-
|
|
643
|
+
// Probed first (#516): a case's workspace can sit on a mount that stopped answering, and an unbounded
|
|
644
|
+
// realpath there would hold this request and a threadpool worker. Such a root is judged as spelled.
|
|
645
|
+
const roots = await Promise.all((await defaultDockerWorkspaces()).map(async (root) => (await probePathKind(root, { pastCap: true })) === 'unknown'
|
|
646
|
+
? resolve(root)
|
|
647
|
+
: fs.realpath(root).catch(() => resolve(root))));
|
|
648
|
+
// A custom folder is already symlink-resolved; the default one is judged through its real cases dir.
|
|
649
|
+
const dest = customPath !== undefined ? casePath : join(await fs.realpath(casesDir).catch(() => casesDir), name);
|
|
650
|
+
if (roots.some((root) => isPathWithin(dest, root))) {
|
|
651
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, DOCKER_CLONE_REFUSAL);
|
|
652
|
+
}
|
|
568
653
|
}
|
|
569
|
-
catch
|
|
570
|
-
|
|
654
|
+
catch {
|
|
655
|
+
reply.code(503);
|
|
656
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Could not read the Docker case list, so Codeman cannot tell whether that folder is a Docker case workspace. Try again.');
|
|
571
657
|
}
|
|
572
658
|
const clone = await cloneRepository({
|
|
573
659
|
repository: parsed.repository,
|
|
574
660
|
destination: casePath,
|
|
661
|
+
...(intoEmptyDir ? { intoEmptyDir: true } : {}),
|
|
575
662
|
withoutCredentialHelpers: cloneWithoutCredentialHelpers(req),
|
|
576
663
|
...(ref ? { ref } : {}),
|
|
577
664
|
...(shallow ? { shallow: true } : {}),
|
|
@@ -628,6 +715,24 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
628
715
|
// problem, rather than deleting a tree the user just waited for.
|
|
629
716
|
warnings.push(`Case scaffolding was incomplete: ${getErrorMessage(err)}`);
|
|
630
717
|
}
|
|
718
|
+
if (customPath !== undefined) {
|
|
719
|
+
// The name was checked before git ran, which can take minutes. Checked again where the entry is
|
|
720
|
+
// written, so of two clones that claimed one name only the first to finish links it.
|
|
721
|
+
try {
|
|
722
|
+
await mutateLinkedCases((current) => {
|
|
723
|
+
if (current[name])
|
|
724
|
+
throw linkedConflict(`Case "${name}" was linked while the clone ran`);
|
|
725
|
+
return { ...current, [name]: casePath };
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
// The clone itself succeeded: keep the folder (the user waited for it) and say where it is.
|
|
730
|
+
const conflict = isLinkedConflict(err);
|
|
731
|
+
const refused = err instanceof LinkedCasesWriteRefusedError;
|
|
732
|
+
reply.code(conflict || refused ? 409 : 500);
|
|
733
|
+
return createErrorResponse(conflict ? ApiErrorCode.ALREADY_EXISTS : refused ? ApiErrorCode.CONFLICT : ApiErrorCode.OPERATION_FAILED, `Cloned into ${casePath} but could not register the case (${getErrorMessage(err)}). Use Link Existing to add it.`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
631
736
|
ctx.broadcast(SseEvent.CaseCreated, { name, path: casePath });
|
|
632
737
|
return {
|
|
633
738
|
success: true,
|
|
@@ -1301,24 +1406,21 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1301
1406
|
if (existsSync(casePath)) {
|
|
1302
1407
|
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'A case with this name already exists in codeman-cases.');
|
|
1303
1408
|
}
|
|
1304
|
-
//
|
|
1305
|
-
const linkedCases = await readLinkedCases();
|
|
1306
|
-
// Check if name is already linked
|
|
1307
|
-
if (linkedCases[name]) {
|
|
1308
|
-
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, `Case "${name}" is already linked to ${linkedCases[name]}`);
|
|
1309
|
-
}
|
|
1310
|
-
// Save the linked case
|
|
1311
|
-
linkedCases[name] = expandedPath;
|
|
1409
|
+
// The name check and the write are one step, so two links of one name cannot both succeed.
|
|
1312
1410
|
try {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1411
|
+
await mutateLinkedCases((current) => {
|
|
1412
|
+
if (current[name])
|
|
1413
|
+
throw linkedConflict(`Case "${name}" is already linked to ${current[name]}`);
|
|
1414
|
+
return { ...current, [name]: expandedPath };
|
|
1415
|
+
});
|
|
1318
1416
|
ctx.broadcast(SseEvent.CaseLinked, { name, path: expandedPath });
|
|
1319
1417
|
return { success: true, data: { case: { name, path: expandedPath } } };
|
|
1320
1418
|
}
|
|
1321
1419
|
catch (err) {
|
|
1420
|
+
if (isLinkedConflict(err))
|
|
1421
|
+
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, getErrorMessage(err));
|
|
1422
|
+
if (err instanceof LinkedCasesWriteRefusedError)
|
|
1423
|
+
return createErrorResponse(ApiErrorCode.CONFLICT, err.message);
|
|
1322
1424
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
1323
1425
|
}
|
|
1324
1426
|
});
|
|
@@ -1388,14 +1490,22 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1388
1490
|
// must not unlink one either; skip so control falls through to their local delete.
|
|
1389
1491
|
const linkedCases = await readLinkedCases();
|
|
1390
1492
|
if (linkedCases[name] && (!isMultiUserMode() || isAdmin(req))) {
|
|
1391
|
-
delete linkedCases[name];
|
|
1392
1493
|
try {
|
|
1393
|
-
|
|
1494
|
+
// Removes this one entry from the registry as it is now, keeping any linked since the read above.
|
|
1495
|
+
await mutateLinkedCases((current) => {
|
|
1496
|
+
if (!Object.hasOwn(current, name))
|
|
1497
|
+
return undefined;
|
|
1498
|
+
const next = { ...current };
|
|
1499
|
+
delete next[name];
|
|
1500
|
+
return next;
|
|
1501
|
+
});
|
|
1394
1502
|
ctx.broadcast(SseEvent.CaseDeleted, { name, type: 'unlinked' });
|
|
1395
1503
|
await forgetCaseDefault();
|
|
1396
1504
|
return { success: true, data: { name } };
|
|
1397
1505
|
}
|
|
1398
1506
|
catch (err) {
|
|
1507
|
+
if (err instanceof LinkedCasesWriteRefusedError)
|
|
1508
|
+
return createErrorResponse(ApiErrorCode.CONFLICT, err.message);
|
|
1399
1509
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
1400
1510
|
}
|
|
1401
1511
|
}
|
|
@@ -1564,7 +1674,6 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1564
1674
|
}
|
|
1565
1675
|
if (moveFolder)
|
|
1566
1676
|
await moveCaseFolder(oldPath, newPath);
|
|
1567
|
-
const originalLinks = isLinked ? await readLinkedCases() : undefined;
|
|
1568
1677
|
// The settings.json values this rename replaced, set only once it wrote them. A failure puts back
|
|
1569
1678
|
// these keys alone, so a settings save that landed in between is kept.
|
|
1570
1679
|
let settingsUndo;
|
|
@@ -1585,17 +1694,20 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1585
1694
|
settingsUndo = Object.fromEntries(keys.map((k) => [k, before[k]]));
|
|
1586
1695
|
}
|
|
1587
1696
|
}
|
|
1588
|
-
if (
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1697
|
+
if (isLinked) {
|
|
1698
|
+
// Checked again where the entry is written: the checks above ran before the sessions were
|
|
1699
|
+
// stopped and the folder moved. A failed write leaves the registry as it was.
|
|
1700
|
+
await mutateLinkedCases((current) => {
|
|
1701
|
+
if (current[newName])
|
|
1702
|
+
throw new CaseRenameError('EXISTS', `A case named "${newName}" already exists`);
|
|
1703
|
+
if (current[name] !== oldPath) {
|
|
1704
|
+
throw new CaseRenameError('BUSY', `Case "${name}" was changed while it was being renamed`);
|
|
1705
|
+
}
|
|
1706
|
+
const links = { ...current };
|
|
1707
|
+
delete links[name];
|
|
1708
|
+
links[newName] = newPath;
|
|
1709
|
+
return links;
|
|
1710
|
+
});
|
|
1599
1711
|
}
|
|
1600
1712
|
}
|
|
1601
1713
|
catch (err) {
|
|
@@ -1642,6 +1754,9 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1642
1754
|
: ApiErrorCode.OPERATION_FAILED;
|
|
1643
1755
|
return fail(status, code, err.message, { renameCode: err.code, stoppedSessions: stopped });
|
|
1644
1756
|
}
|
|
1757
|
+
if (err instanceof LinkedCasesWriteRefusedError) {
|
|
1758
|
+
return fail(409, ApiErrorCode.CONFLICT, err.message, { stoppedSessions: stopped });
|
|
1759
|
+
}
|
|
1645
1760
|
return fail(500, ApiErrorCode.OPERATION_FAILED, getErrorMessage(err), { stoppedSessions: stopped });
|
|
1646
1761
|
}
|
|
1647
1762
|
});
|