aicodeman 1.43.0 → 1.45.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 +9 -4
- package/dist/case-rename.d.ts +65 -0
- package/dist/case-rename.d.ts.map +1 -0
- package/dist/case-rename.js +198 -0
- package/dist/case-rename.js.map +1 -0
- package/dist/cli-agent.d.ts +63 -4
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +233 -19
- package/dist/cli-agent.js.map +1 -1
- package/dist/config/auth-config.d.ts +16 -1
- package/dist/config/auth-config.d.ts.map +1 -1
- package/dist/config/auth-config.js +16 -1
- package/dist/config/auth-config.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +8 -3
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +2 -2
- package/dist/config/map-limits.d.ts +13 -1
- package/dist/config/map-limits.d.ts.map +1 -1
- package/dist/config/map-limits.js +28 -1
- package/dist/config/map-limits.js.map +1 -1
- package/dist/config-backup.d.ts +279 -0
- package/dist/config-backup.d.ts.map +1 -0
- package/dist/config-backup.js +794 -0
- package/dist/config-backup.js.map +1 -0
- package/dist/cron/cron-service.d.ts +5 -5
- package/dist/cron/cron-service.js +6 -6
- package/dist/cron/cron-service.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +16 -0
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/hooks-config.d.ts +15 -0
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +35 -0
- package/dist/hooks-config.js.map +1 -1
- package/dist/host-sensors.d.ts +115 -0
- package/dist/host-sensors.d.ts.map +1 -0
- package/dist/host-sensors.js +457 -0
- package/dist/host-sensors.js.map +1 -0
- package/dist/intent-store.d.ts +2 -0
- package/dist/intent-store.d.ts.map +1 -1
- package/dist/intent-store.js +9 -0
- package/dist/intent-store.js.map +1 -1
- package/dist/mux-interface.d.ts +21 -4
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.d.ts +193 -0
- package/dist/prompt-cache-keepwarm.d.ts.map +1 -0
- package/dist/prompt-cache-keepwarm.js +359 -0
- package/dist/prompt-cache-keepwarm.js.map +1 -0
- package/dist/remote-hosts.d.ts +7 -0
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +17 -3
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-pane-history.d.ts.map +1 -1
- package/dist/remote-pane-history.js +2 -2
- package/dist/remote-pane-history.js.map +1 -1
- package/dist/respawn-controller.d.ts.map +1 -1
- package/dist/respawn-controller.js +6 -5
- package/dist/respawn-controller.js.map +1 -1
- package/dist/session-auto-ops.d.ts.map +1 -1
- package/dist/session-auto-ops.js +6 -1
- package/dist/session-auto-ops.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts +13 -0
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +20 -3
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-submit-verifier.d.ts +19 -0
- package/dist/session-submit-verifier.d.ts.map +1 -1
- package/dist/session-submit-verifier.js +88 -0
- package/dist/session-submit-verifier.js.map +1 -1
- package/dist/session.d.ts +61 -5
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +125 -7
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +10 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +35 -0
- package/dist/state-store.js.map +1 -1
- package/dist/tab-layout-service.d.ts +33 -0
- package/dist/tab-layout-service.d.ts.map +1 -1
- package/dist/tab-layout-service.js +75 -3
- package/dist/tab-layout-service.js.map +1 -1
- package/dist/tmux-manager.d.ts +57 -11
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +190 -42
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-prefix-table.d.ts +86 -0
- package/dist/tmux-prefix-table.d.ts.map +1 -0
- package/dist/tmux-prefix-table.js +172 -0
- package/dist/tmux-prefix-table.js.map +1 -0
- package/dist/tui/tui-app.d.ts +17 -2
- package/dist/tui/tui-app.d.ts.map +1 -1
- package/dist/tui/tui-app.js +70 -20
- package/dist/tui/tui-app.js.map +1 -1
- package/dist/tui/tui-client.d.ts +93 -5
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +198 -28
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/types/app-state.d.ts +6 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/session.d.ts +40 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/usage-telemetry.d.ts +15 -0
- package/dist/usage-telemetry.d.ts.map +1 -1
- package/dist/usage-telemetry.js +45 -2
- package/dist/usage-telemetry.js.map +1 -1
- package/dist/utils/copilot-plan-usage.d.ts +46 -0
- package/dist/utils/copilot-plan-usage.d.ts.map +1 -0
- package/dist/utils/copilot-plan-usage.js +95 -0
- package/dist/utils/copilot-plan-usage.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/single-line.d.ts +22 -0
- package/dist/utils/single-line.d.ts.map +1 -0
- package/dist/utils/single-line.js +30 -0
- package/dist/utils/single-line.js.map +1 -0
- package/dist/web/agent-inbox-persistence.d.ts +52 -0
- package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
- package/dist/web/agent-inbox-persistence.js +139 -0
- package/dist/web/agent-inbox-persistence.js.map +1 -0
- package/dist/web/agent-inbox.d.ts +140 -0
- package/dist/web/agent-inbox.d.ts.map +1 -0
- package/dist/web/agent-inbox.js +258 -0
- package/dist/web/agent-inbox.js.map +1 -0
- package/dist/web/approval-inbox.d.ts +6 -0
- package/dist/web/approval-inbox.d.ts.map +1 -1
- package/dist/web/approval-inbox.js +2 -2
- package/dist/web/approval-inbox.js.map +1 -1
- package/dist/web/middleware/auth.d.ts +2 -0
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +145 -52
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/plan-usage-latest.d.ts +2 -1
- package/dist/web/plan-usage-latest.d.ts.map +1 -1
- package/dist/web/plan-usage-latest.js +12 -2
- package/dist/web/plan-usage-latest.js.map +1 -1
- package/dist/web/ports/index.d.ts +1 -0
- package/dist/web/ports/index.d.ts.map +1 -1
- package/dist/web/ports/keep-warm-port.d.ts +9 -0
- package/dist/web/ports/keep-warm-port.d.ts.map +1 -0
- package/dist/web/ports/keep-warm-port.js +6 -0
- package/dist/web/ports/keep-warm-port.js.map +1 -0
- package/dist/web/ports/session-port.d.ts +3 -1
- 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.42d0abae.js +43 -0
- package/dist/web/public/app.42d0abae.js.br +0 -0
- package/dist/web/public/app.42d0abae.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.e5e588db.js → constants.0b3cdde9.js} +60 -1
- package/dist/web/public/constants.0b3cdde9.js.br +0 -0
- package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js +1 -0
- package/dist/web/public/header-divider.eb2d2463.js.br +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.1e68220d.js +2 -0
- package/dist/web/public/i18n.1e68220d.js.br +0 -0
- package/dist/web/public/i18n.1e68220d.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/index.html +162 -14
- 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.c283e827.js → keyboard-accessory.4e8b20ac.js} +23 -5
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.0ef9c311.js → mobile-handlers.1c0cbcd8.js} +50 -16
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.12fb18de.css +1 -0
- package/dist/web/public/mobile.12fb18de.css.br +0 -0
- package/dist/web/public/mobile.12fb18de.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.b7bfb9da.js → panels-ui.78c3defb.js} +19 -13
- package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
- package/dist/web/public/panels-ui.78c3defb.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.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.be076c88.js → session-ui.ea6b1994.js} +33 -27
- package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
- package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
- package/dist/web/public/settings-ui.085dd202.js +106 -0
- package/dist/web/public/settings-ui.085dd202.js.br +0 -0
- package/dist/web/public/settings-ui.085dd202.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.3b59b41d.css +1 -0
- package/dist/web/public/styles.3b59b41d.css.br +0 -0
- package/dist/web/public/styles.3b59b41d.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.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +49 -23
- 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.fb2a27f5.js +2 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
- package/dist/web/public/terminal-ui.fb2a27f5.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.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.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 -9
- package/dist/web/route-helpers.d.ts +57 -6
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +72 -4
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/admin-routes.d.ts +2 -1
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +3 -0
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts +2 -2
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +263 -4
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +54 -0
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -0
- package/dist/web/routes/config-backup-routes.js +268 -0
- package/dist/web/routes/config-backup-routes.js.map +1 -0
- package/dist/web/routes/custom-model-routes.d.ts +17 -1
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +118 -13
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +26 -5
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/inbox-routes.d.ts +43 -0
- package/dist/web/routes/inbox-routes.d.ts.map +1 -0
- package/dist/web/routes/inbox-routes.js +148 -0
- package/dist/web/routes/inbox-routes.js.map +1 -0
- 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/ralph-routes.js +2 -2
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +2 -0
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +2 -2
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +110 -61
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts +2 -2
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +8 -4
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts +2 -2
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +30 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/webview-routes.d.ts.map +1 -1
- package/dist/web/routes/webview-routes.js +1 -0
- package/dist/web/routes/webview-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +45 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +79 -2
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +2 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +14 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +176 -7
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +6 -0
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +10 -3
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +48 -4
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +144 -6
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/sse-events.d.ts +9 -3
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +9 -3
- package/dist/web/sse-events.js.map +1 -1
- package/dist/web/webview-proxy.d.ts +8 -3
- package/dist/web/webview-proxy.d.ts.map +1 -1
- package/dist/web/webview-proxy.js +23 -5
- package/dist/web/webview-proxy.js.map +1 -1
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +27 -13
- package/skills/codeman/preamble.sh +15 -5
- package/skills/codeman/reference/endpoints.md +32 -10
- package/skills/codeman/reference/recipes.md +1 -1
- package/skills/codeman/reference/verbs.md +68 -13
- package/dist/web/public/app.ae50029a.js +0 -43
- package/dist/web/public/app.ae50029a.js.br +0 -0
- package/dist/web/public/app.ae50029a.js.gz +0 -0
- package/dist/web/public/constants.e5e588db.js.br +0 -0
- package/dist/web/public/constants.e5e588db.js.gz +0 -0
- package/dist/web/public/i18n.8f291f06.js +0 -2
- package/dist/web/public/i18n.8f291f06.js.br +0 -0
- package/dist/web/public/i18n.8f291f06.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c283e827.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c283e827.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile.13514863.css +0 -1
- package/dist/web/public/mobile.13514863.css.br +0 -0
- package/dist/web/public/mobile.13514863.css.gz +0 -0
- package/dist/web/public/panels-ui.b7bfb9da.js.br +0 -0
- package/dist/web/public/panels-ui.b7bfb9da.js.gz +0 -0
- package/dist/web/public/session-ui.be076c88.js.br +0 -0
- package/dist/web/public/session-ui.be076c88.js.gz +0 -0
- package/dist/web/public/settings-ui.fc4cc50a.js +0 -97
- package/dist/web/public/settings-ui.fc4cc50a.js.br +0 -0
- package/dist/web/public/settings-ui.fc4cc50a.js.gz +0 -0
- package/dist/web/public/styles.dcfa907f.css +0 -1
- package/dist/web/public/styles.dcfa907f.css.br +0 -0
- package/dist/web/public/styles.dcfa907f.css.gz +0 -0
- package/dist/web/public/terminal-ui.ae01a7d1.js +0 -2
- package/dist/web/public/terminal-ui.ae01a7d1.js.br +0 -0
- package/dist/web/public/terminal-ui.ae01a7d1.js.gz +0 -0
|
@@ -8,21 +8,24 @@
|
|
|
8
8
|
import { existsSync, lstatSync, mkdirSync, writeFileSync, readdirSync, readFileSync, createReadStream } from 'node:fs';
|
|
9
9
|
import { exec } from 'node:child_process';
|
|
10
10
|
import fs from 'node:fs/promises';
|
|
11
|
-
import { join, resolve, basename } from 'node:path';
|
|
11
|
+
import { join, resolve, basename, sep } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
|
|
15
|
-
import { CreateCaseSchema, CloneCaseSchema, ClonePreflightSchema, LinkCaseSchema, CaseOrderSchema, RemoteCaseLinkSchema, RemoteHostSchema, DockerCaseLinkSchema, DockerCaseAdoptSchema, DockerAdoptPreflightSchema, DockerBrowseSchema, DockerHostSchema, DockerExportSchema, DockerImportSchema, DockerQuickCreateSchema, } from '../schemas.js';
|
|
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
18
|
import { generateClaudeMd } from '../../templates/claude-md.js';
|
|
19
19
|
import { prepareNewCasePath } from '../case-path.js';
|
|
20
|
-
import {
|
|
20
|
+
import { CaseRenameError, isCaseSessionStarting, moveCaseFolder, moveClaudeHistory, renameInOrder, siblingPath, withCaseLock, } from '../../case-rename.js';
|
|
21
|
+
import { boundedPathExists, describeUnknownPath, probePath, probePathKind } from '../../utils/index.js';
|
|
21
22
|
import { readAgentCaseMarker } from '../../agent-case-marker.js';
|
|
22
23
|
import { settingsWriteBlocker, writeHooksConfig } from '../../hooks-config.js';
|
|
23
24
|
import { canAccessOwned, getAuthUser, isAdmin, isWorkingDirAllowed, ownerFor, requireAdmin, resolveCasesDir, SETTINGS_PATH, validatePathWithinBase, parseBody, readJsonConfig, } from '../route-helpers.js';
|
|
24
25
|
import { isMultiUserMode } from '../../config/multiuser.js';
|
|
25
26
|
import { SseEvent } from '../sse-events.js';
|
|
27
|
+
import { ownerLayoutKey } from '../../tab-layout-persistence.js';
|
|
28
|
+
import { TabLayoutValidationError } from '../../tab-layout.js';
|
|
26
29
|
import { dataPath, getDataDir } from '../../config/instance.js';
|
|
27
30
|
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';
|
|
28
31
|
import { buildDockerRemoveCommand } from '../../tmux-manager.js';
|
|
@@ -152,6 +155,29 @@ async function ensureCaseImage(broadcast, sessionDocker, name) {
|
|
|
152
155
|
.catch((err) => broadcast(SseEvent.DockerImageBuildFailed, { name, image: sessionDocker.image, error: getErrorMessage(err) }));
|
|
153
156
|
return { ok: true, imageBuilding: true };
|
|
154
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Is `name` a case the caller can see (the same rules as GET /api/cases and
|
|
160
|
+
* resolveCasePath): an owned remote or Docker case, a linked case (admin or
|
|
161
|
+
* single-user only), or a directory in the caller's own case space. Bounded:
|
|
162
|
+
* a case folder on a mount that stopped answering still counts as there, like
|
|
163
|
+
* the case list keeps listing it.
|
|
164
|
+
*/
|
|
165
|
+
async function caseVisibleToCaller(name, req) {
|
|
166
|
+
if (!SAFE_CASE_NAME.test(name))
|
|
167
|
+
return false;
|
|
168
|
+
const user = getAuthUser(req);
|
|
169
|
+
if ((await readRemoteCases(CODEMAN_CONFIG_DIR)).some((c) => c.name === name && canAccessOwned(user, c.owner))) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if ((await readDockerCases(CODEMAN_CONFIG_DIR)).some((c) => c.name === name && canAccessOwned(user, c.owner))) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
const linkedCases = await readLinkedCases();
|
|
176
|
+
if (linkedCases[name] && (!isMultiUserMode() || isAdmin(req)))
|
|
177
|
+
return (await probePath(linkedCases[name])) !== 'absent';
|
|
178
|
+
const kind = await probePathKind(join(resolveCasesDir(user), name));
|
|
179
|
+
return kind === 'directory' || kind === 'unknown';
|
|
180
|
+
}
|
|
155
181
|
export function registerCaseRoutes(app, ctx) {
|
|
156
182
|
// ═══════════════════════════════════════════════════════════════
|
|
157
183
|
// Case CRUD (list, create, link, detail, fix-plan)
|
|
@@ -1299,6 +1325,20 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1299
1325
|
app.delete('/api/cases/:name', async (req) => {
|
|
1300
1326
|
const { name } = req.params;
|
|
1301
1327
|
const user = getAuthUser(req);
|
|
1328
|
+
// The caller's default session group for this case goes with it (best-effort:
|
|
1329
|
+
// a stale value is ignored at creation anyway).
|
|
1330
|
+
// An admin deleting another user's remote/Docker case clears that owner's too.
|
|
1331
|
+
const forgetCaseDefault = async (caseOwner) => {
|
|
1332
|
+
const owners = new Set([ownerLayoutKey(ownerFor(req)), ...(caseOwner ? [ownerLayoutKey(caseOwner)] : [])]);
|
|
1333
|
+
for (const owner of owners) {
|
|
1334
|
+
try {
|
|
1335
|
+
await ctx.tabLayouts.forgetCaseDefaultGroup(owner, name);
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
/* never blocks the delete */
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1302
1342
|
if (!validatePathWithinBase(name, resolveCasesDir(user))) {
|
|
1303
1343
|
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
|
|
1304
1344
|
}
|
|
@@ -1306,9 +1346,11 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1306
1346
|
// docker case is skipped so control falls through to the caller's own local delete.
|
|
1307
1347
|
// canAccessOwned is all-true for admins/single-user, so flag-OFF stays byte-identical.
|
|
1308
1348
|
const remoteCases = await readRemoteCases(CODEMAN_CONFIG_DIR);
|
|
1309
|
-
|
|
1349
|
+
const remoteCase = remoteCases.find((item) => item.name === name && canAccessOwned(user, item.owner));
|
|
1350
|
+
if (remoteCase) {
|
|
1310
1351
|
await writeRemoteCases(CODEMAN_CONFIG_DIR, remoteCases.filter((item) => !(item.name === name && canAccessOwned(user, item.owner))));
|
|
1311
1352
|
ctx.broadcast(SseEvent.CaseDeleted, { name, type: 'remote-unlinked' });
|
|
1353
|
+
await forgetCaseDefault(remoteCase.owner);
|
|
1312
1354
|
return { success: true, data: { name } };
|
|
1313
1355
|
}
|
|
1314
1356
|
const dockerCases = await readDockerCases(CODEMAN_CONFIG_DIR);
|
|
@@ -1337,6 +1379,7 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1337
1379
|
.catch(() => { });
|
|
1338
1380
|
}
|
|
1339
1381
|
ctx.broadcast(SseEvent.CaseDeleted, { name, type: 'docker-unlinked' });
|
|
1382
|
+
await forgetCaseDefault(dockerCase.owner);
|
|
1340
1383
|
return { success: true, data: { name } };
|
|
1341
1384
|
}
|
|
1342
1385
|
// Check linked cases first — unlink only, don't delete the actual directory.
|
|
@@ -1348,6 +1391,7 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1348
1391
|
try {
|
|
1349
1392
|
await fs.writeFile(LINKED_CASES_FILE, JSON.stringify(linkedCases, null, 2));
|
|
1350
1393
|
ctx.broadcast(SseEvent.CaseDeleted, { name, type: 'unlinked' });
|
|
1394
|
+
await forgetCaseDefault();
|
|
1351
1395
|
return { success: true, data: { name } };
|
|
1352
1396
|
}
|
|
1353
1397
|
catch (err) {
|
|
@@ -1362,12 +1406,227 @@ export function registerCaseRoutes(app, ctx) {
|
|
|
1362
1406
|
try {
|
|
1363
1407
|
await fs.rm(casePath, { recursive: true, force: true });
|
|
1364
1408
|
ctx.broadcast(SseEvent.CaseDeleted, { name, type: 'deleted' });
|
|
1409
|
+
await forgetCaseDefault();
|
|
1365
1410
|
return { success: true, data: { name } };
|
|
1366
1411
|
}
|
|
1367
1412
|
catch (err) {
|
|
1368
1413
|
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
1369
1414
|
}
|
|
1370
1415
|
});
|
|
1416
|
+
// ========== Default session group ==========
|
|
1417
|
+
/**
|
|
1418
|
+
* The tab group new sessions of this case land in when the create request names
|
|
1419
|
+
* no group and no parent places them. Scoped to the caller's own tab layout: the
|
|
1420
|
+
* options are their groups, and a stored id that is no longer one of them reads
|
|
1421
|
+
* as Ungrouped (null); the read writes nothing (the next creation for the case
|
|
1422
|
+
* forgets it). Not admin-gated in multi-user mode: the
|
|
1423
|
+
* value is per owner (like the layout it points into), never server-wide.
|
|
1424
|
+
*/
|
|
1425
|
+
app.get('/api/cases/:name/default-group', async (req, reply) => {
|
|
1426
|
+
const { name } = req.params;
|
|
1427
|
+
if (!(await caseVisibleToCaller(name, req))) {
|
|
1428
|
+
return reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, `Case "${name}" not found`));
|
|
1429
|
+
}
|
|
1430
|
+
const view = await ctx.tabLayouts.caseDefaultGroup(ownerLayoutKey(ownerFor(req)), name);
|
|
1431
|
+
return { success: true, data: { caseName: name, ...view } };
|
|
1432
|
+
});
|
|
1433
|
+
app.put('/api/cases/:name/default-group', async (req, reply) => {
|
|
1434
|
+
const { name } = req.params;
|
|
1435
|
+
const { groupId } = parseBody(CaseDefaultGroupSchema, req.body, 'Invalid default group');
|
|
1436
|
+
if (!(await caseVisibleToCaller(name, req))) {
|
|
1437
|
+
return reply.code(404).send(createErrorResponse(ApiErrorCode.NOT_FOUND, `Case "${name}" not found`));
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
const view = await ctx.tabLayouts.setCaseDefaultGroup(ownerLayoutKey(ownerFor(req)), name, groupId);
|
|
1441
|
+
return { success: true, data: { caseName: name, ...view } };
|
|
1442
|
+
}
|
|
1443
|
+
catch (err) {
|
|
1444
|
+
if (err instanceof TabLayoutValidationError) {
|
|
1445
|
+
return reply.code(400).send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'That tab group does not exist'));
|
|
1446
|
+
}
|
|
1447
|
+
throw err;
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
// ========== Rename Case ==========
|
|
1451
|
+
/**
|
|
1452
|
+
* Rename a case: its name and, with it, its folder.
|
|
1453
|
+
*
|
|
1454
|
+
* - A case under the cases directory IS its folder name, so it always moves.
|
|
1455
|
+
* - A linked case has an alias that is only a key in linked-cases.json; the real folder moves too only
|
|
1456
|
+
* with `renameFolder: true` (it can be shared with other tools).
|
|
1457
|
+
* - Remote and Docker cases are not renamable here: their folder lives on another host or in a container
|
|
1458
|
+
* whose name derives from the case name.
|
|
1459
|
+
*
|
|
1460
|
+
* ⚠️ Moving a folder that sessions have open is the dangerous part (see src/case-rename.ts): live
|
|
1461
|
+
* sessions in it are listed and refused (409 `requiresStop`) unless `stopSessions` is set, in which
|
|
1462
|
+
* case they are ended first. The folder is held by an in-process lock for the whole operation, so a
|
|
1463
|
+
* session cannot start in it half-moved.
|
|
1464
|
+
*/
|
|
1465
|
+
app.post('/api/cases/:name/rename', async (req, reply) => {
|
|
1466
|
+
const { name } = req.params;
|
|
1467
|
+
const user = getAuthUser(req);
|
|
1468
|
+
const body = parseBody(RenameCaseSchema, req.body);
|
|
1469
|
+
const newName = body.newName;
|
|
1470
|
+
const fail = (status, code, message, extra = {}) => {
|
|
1471
|
+
reply.code(status);
|
|
1472
|
+
return { ...createErrorResponse(code, message), ...extra };
|
|
1473
|
+
};
|
|
1474
|
+
if (!SAFE_CASE_NAME.test(name) || !validatePathWithinBase(name, resolveCasesDir(user))) {
|
|
1475
|
+
return fail(400, ApiErrorCode.INVALID_INPUT, 'Invalid case name');
|
|
1476
|
+
}
|
|
1477
|
+
if (newName === name)
|
|
1478
|
+
return fail(400, ApiErrorCode.INVALID_INPUT, 'The new name is the same as the old one');
|
|
1479
|
+
// Remote and Docker cases first, so a same-named local case cannot be confused for them.
|
|
1480
|
+
const remoteCases = await readRemoteCases(CODEMAN_CONFIG_DIR);
|
|
1481
|
+
const dockerCases = await readDockerCases(CODEMAN_CONFIG_DIR);
|
|
1482
|
+
if (remoteCases.some((c) => c.name === name && canAccessOwned(user, c.owner)) ||
|
|
1483
|
+
dockerCases.some((c) => c.name === name && canAccessOwned(user, c.owner))) {
|
|
1484
|
+
return fail(400, ApiErrorCode.INVALID_INPUT, 'Remote and Docker cases cannot be renamed here');
|
|
1485
|
+
}
|
|
1486
|
+
const casesBase = resolveCasesDir(user);
|
|
1487
|
+
const linkedCases = await readLinkedCases();
|
|
1488
|
+
const isLinked = Boolean(linkedCases[name]) && (!isMultiUserMode() || isAdmin(req));
|
|
1489
|
+
const oldPath = isLinked ? linkedCases[name] : join(casesBase, name);
|
|
1490
|
+
// The new name must be free in every namespace the list merges.
|
|
1491
|
+
const localTaken = existsSync(join(casesBase, newName));
|
|
1492
|
+
const taken = localTaken ||
|
|
1493
|
+
Boolean(linkedCases[newName]) ||
|
|
1494
|
+
remoteCases.some((c) => c.name === newName) ||
|
|
1495
|
+
dockerCases.some((c) => c.name === newName);
|
|
1496
|
+
if (taken)
|
|
1497
|
+
return fail(409, ApiErrorCode.ALREADY_EXISTS, `A case named "${newName}" already exists`);
|
|
1498
|
+
const state = await probePath(oldPath);
|
|
1499
|
+
if (state === 'absent')
|
|
1500
|
+
return fail(404, ApiErrorCode.NOT_FOUND, `Case "${name}" not found`);
|
|
1501
|
+
// An alias-only rename never touches the folder, so an unreachable one does not block it.
|
|
1502
|
+
if (state === 'unknown' && (!isLinked || body.renameFolder === true)) {
|
|
1503
|
+
return fail(503, ApiErrorCode.OPERATION_FAILED, describeUnknownPath('Case folder', oldPath, { pastCap: true }));
|
|
1504
|
+
}
|
|
1505
|
+
const moveFolder = !isLinked || body.renameFolder === true;
|
|
1506
|
+
const newPath = isLinked ? (moveFolder ? siblingPath(oldPath, newName) : oldPath) : join(casesBase, newName);
|
|
1507
|
+
if (moveFolder && newPath !== oldPath) {
|
|
1508
|
+
// probePath follows symlinks, so a dangling one reads as absent; moveCaseFolder would still refuse it
|
|
1509
|
+
// (it checks with lstat), but only after the sessions below were stopped. The probe answered for this
|
|
1510
|
+
// very path, so the lstat after it is not a first touch of an unreachable mount.
|
|
1511
|
+
const target = await probePath(newPath, { pastCap: true });
|
|
1512
|
+
const danglingLink = target === 'absent' &&
|
|
1513
|
+
(await fs.lstat(newPath).then(() => true, () => false));
|
|
1514
|
+
if (target !== 'absent' || danglingLink) {
|
|
1515
|
+
return fail(409, ApiErrorCode.ALREADY_EXISTS, `A folder named ${newPath} already exists`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
// Sessions with the folder open. Only a move disturbs them; an alias-only rename leaves them running.
|
|
1519
|
+
// Remote sessions carry the remote path as their workingDir, so they are not this folder's.
|
|
1520
|
+
const heldRoot = resolve(oldPath);
|
|
1521
|
+
const open = moveFolder
|
|
1522
|
+
? Array.from(ctx.sessions.values()).filter((s) => {
|
|
1523
|
+
if (s.remote)
|
|
1524
|
+
return false;
|
|
1525
|
+
const dir = resolve(s.workingDir);
|
|
1526
|
+
return dir === heldRoot || dir.startsWith(heldRoot + sep);
|
|
1527
|
+
})
|
|
1528
|
+
: [];
|
|
1529
|
+
if (open.some((s) => !canAccessOwned(user, s.owner))) {
|
|
1530
|
+
return fail(403, ApiErrorCode.FORBIDDEN, "Another user's session has this case open");
|
|
1531
|
+
}
|
|
1532
|
+
// A session being created in the folder is not in ctx.sessions yet, so it could be neither listed nor
|
|
1533
|
+
// stopped: it would start in the old path after the move. Checked in the same tick that takes the lock
|
|
1534
|
+
// below (nothing is awaited in between), and the Session constructor refuses the folder from then on.
|
|
1535
|
+
if (moveFolder && isCaseSessionStarting(oldPath)) {
|
|
1536
|
+
return fail(409, ApiErrorCode.CONFLICT, 'A session is starting in this case. Try again in a moment.', {
|
|
1537
|
+
renameCode: 'BUSY',
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
if (open.length > 0 && !body.stopSessions) {
|
|
1541
|
+
return fail(409, ApiErrorCode.CONFLICT, `${open.length} live session${open.length === 1 ? ' has' : 's have'} this folder open. Stop them to rename it.`, { requiresStop: true, sessions: open.map((s) => ({ id: s.id, name: s.name })) });
|
|
1542
|
+
}
|
|
1543
|
+
// Sessions really ended so far: an error after `stopSessions` still says how many are gone.
|
|
1544
|
+
let stopped = 0;
|
|
1545
|
+
try {
|
|
1546
|
+
await withCaseLock(oldPath, newPath, async () => {
|
|
1547
|
+
for (const s of open) {
|
|
1548
|
+
await ctx.cleanupSession(s.id, true, 'case_rename');
|
|
1549
|
+
stopped++;
|
|
1550
|
+
}
|
|
1551
|
+
if (moveFolder)
|
|
1552
|
+
await moveCaseFolder(oldPath, newPath);
|
|
1553
|
+
const originalLinks = isLinked ? await readLinkedCases() : undefined;
|
|
1554
|
+
// The settings.json content before this rename wrote it, set only once a write is attempted (an
|
|
1555
|
+
// absent file reads as {}, which must not be written back as a file that never existed).
|
|
1556
|
+
let originalSettings;
|
|
1557
|
+
try {
|
|
1558
|
+
// settings.json is server-wide and caseOrder/lastUsedCase name the admin's cases, so only the
|
|
1559
|
+
// admin's rename touches it (a non-admin's own rename must not rewrite someone else's list).
|
|
1560
|
+
// Written before linked-cases.json so a failure here leaves the registry untouched.
|
|
1561
|
+
if (isAdmin(req)) {
|
|
1562
|
+
const existing = await readJsonConfig(SETTINGS_PATH, 'settings.json', {});
|
|
1563
|
+
const order = renameInOrder(existing.caseOrder, name, newName);
|
|
1564
|
+
const last = existing.lastUsedCase === name ? newName : undefined;
|
|
1565
|
+
if (order || last) {
|
|
1566
|
+
originalSettings = existing;
|
|
1567
|
+
await fs.writeFile(SETTINGS_PATH, JSON.stringify({ ...existing, ...(order ? { caseOrder: order } : {}), ...(last ? { lastUsedCase: last } : {}) }, null, 2));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (originalLinks) {
|
|
1571
|
+
const links = { ...originalLinks };
|
|
1572
|
+
delete links[name];
|
|
1573
|
+
links[newName] = newPath;
|
|
1574
|
+
try {
|
|
1575
|
+
await fs.writeFile(LINKED_CASES_FILE, JSON.stringify(links, null, 2));
|
|
1576
|
+
}
|
|
1577
|
+
catch (err) {
|
|
1578
|
+
await fs.writeFile(LINKED_CASES_FILE, JSON.stringify(originalLinks, null, 2)).catch(() => { });
|
|
1579
|
+
throw err;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
catch (err) {
|
|
1584
|
+
// The folder moved but the registries did not follow: put settings.json and the folder back
|
|
1585
|
+
// rather than leave a case whose name and folder disagree.
|
|
1586
|
+
if (originalSettings) {
|
|
1587
|
+
await fs.writeFile(SETTINGS_PATH, JSON.stringify(originalSettings, null, 2)).catch(() => { });
|
|
1588
|
+
}
|
|
1589
|
+
if (moveFolder) {
|
|
1590
|
+
const movedBack = await fs.rename(newPath, oldPath).then(() => true, () => false);
|
|
1591
|
+
if (!movedBack) {
|
|
1592
|
+
throw new Error(`${getErrorMessage(err)}. The folder could not be moved back and is now at ${newPath}.`);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
throw err;
|
|
1596
|
+
}
|
|
1597
|
+
if (moveFolder)
|
|
1598
|
+
await moveClaudeHistory(oldPath, newPath);
|
|
1599
|
+
// The owner's default session group is keyed by case name: carry it over, best effort.
|
|
1600
|
+
const layoutOwner = ownerLayoutKey(ownerFor(req));
|
|
1601
|
+
try {
|
|
1602
|
+
const { groupId } = await ctx.tabLayouts.caseDefaultGroup(layoutOwner, name);
|
|
1603
|
+
if (groupId)
|
|
1604
|
+
await ctx.tabLayouts.setCaseDefaultGroup(layoutOwner, newName, groupId);
|
|
1605
|
+
}
|
|
1606
|
+
catch {
|
|
1607
|
+
/* the group is a convenience; the rename already happened */
|
|
1608
|
+
}
|
|
1609
|
+
await ctx.tabLayouts.forgetCaseDefaultGroup(layoutOwner, name).catch(() => undefined);
|
|
1610
|
+
});
|
|
1611
|
+
ctx.broadcast(SseEvent.CaseRenamed, { oldName: name, newName, oldPath, newPath, folderMoved: moveFolder });
|
|
1612
|
+
return {
|
|
1613
|
+
success: true,
|
|
1614
|
+
data: { oldName: name, newName, oldPath, newPath, folderMoved: moveFolder, stoppedSessions: open.length },
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
catch (err) {
|
|
1618
|
+
if (err instanceof CaseRenameError) {
|
|
1619
|
+
const status = err.code === 'BUSY' ? 409 : err.code === 'EXISTS' ? 409 : err.code === 'LOCKED' ? 423 : 400;
|
|
1620
|
+
const code = err.code === 'EXISTS'
|
|
1621
|
+
? ApiErrorCode.ALREADY_EXISTS
|
|
1622
|
+
: err.code === 'BUSY'
|
|
1623
|
+
? ApiErrorCode.CONFLICT
|
|
1624
|
+
: ApiErrorCode.OPERATION_FAILED;
|
|
1625
|
+
return fail(status, code, err.message, { renameCode: err.code, stoppedSessions: stopped });
|
|
1626
|
+
}
|
|
1627
|
+
return fail(500, ApiErrorCode.OPERATION_FAILED, getErrorMessage(err), { stoppedSessions: stopped });
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1371
1630
|
// ========== Reorder Cases ==========
|
|
1372
1631
|
app.put('/api/cases/order', async (req, reply) => {
|
|
1373
1632
|
// `caseOrder` is ONE list in the server-wide settings.json, read by every user's
|