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
|
@@ -21,13 +21,13 @@ import { getOfficePreviewPdfPath, getPreviewPdfDownloadName } from '../../docume
|
|
|
21
21
|
import { sanitizeAttachmentHistoryItem } from '../../session-attachment-history.js';
|
|
22
22
|
import { isBlockedAttachmentPath, isUnderTree, loadAttachmentGuardConfig } from '../../config/attachment-guard.js';
|
|
23
23
|
import { isMultiUserMode, userSpacePath } from '../../config/multiuser.js';
|
|
24
|
-
import { CASES_DIR, canAccessOwned, findSessionOrFail, getAuthUser, parseBody, validateSessionFilePath, validateSessionFilePathLexical, } from '../route-helpers.js';
|
|
24
|
+
import { CASES_DIR, canAccessOwned, findSessionOrFail, getAuthUser, parseBody, resolveCasesDir, validateSessionFilePath, validateSessionFilePathLexical, } from '../route-helpers.js';
|
|
25
25
|
import { RemoteFileAccessError, remoteCreateReadStream, remoteProbePaths, remoteReadFile, } from '../../remote-files.js';
|
|
26
26
|
import { downloadTooLargeMessage, exceedsDownloadLimit } from '../../config/buffer-limits.js';
|
|
27
27
|
import { parseByteRange } from '../http-range.js';
|
|
28
28
|
import { isSensitivePath } from '../sensitive-path.js';
|
|
29
29
|
import { SseEvent } from '../sse-events.js';
|
|
30
|
-
import { FilesystemBrowseQuerySchema, FilesystemPreviewQuerySchema, FileWriteSchema } from '../schemas.js';
|
|
30
|
+
import { FilesystemBrowseQuerySchema, FilesystemMkdirSchema, FilesystemPreviewQuerySchema, FileWriteSchema, } from '../schemas.js';
|
|
31
31
|
import { MAX_EDITABLE_BYTES, applyEol, detectEol, isDeniedEditRelativePath, isEditableFileName, } from '../../config/file-editing.js';
|
|
32
32
|
/**
|
|
33
33
|
* Upper bound on an XLSX the browser preview will fetch (`?preview=true`).
|
|
@@ -523,6 +523,85 @@ export function isPathWithinRoot(root, candidate) {
|
|
|
523
523
|
const rel = relative(root, candidate);
|
|
524
524
|
return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
|
|
525
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Folder entries one Files panel listing may read, summed over the folders it opens. The budget of
|
|
528
|
+
* the walk caps what a listing RETURNS, but taking turns means opening every folder found so far,
|
|
529
|
+
* so this is what caps the reading: past it, folders not opened yet stay unread and are reported as
|
|
530
|
+
* unfinished. It also bounds what the walk holds, since a folder keeps only entries it has read. A
|
|
531
|
+
* folder is read whole, so the one that crosses this line overshoots by its own size.
|
|
532
|
+
*/
|
|
533
|
+
const MAX_WALK_ENTRIES_READ = 250_000;
|
|
534
|
+
/**
|
|
535
|
+
* First turn of a folder: reads it and sets `cursor.entries` to the part the listing could use,
|
|
536
|
+
* which is the entries it lists, sorted, up to what is left of the budget. Leaves `entries` unset
|
|
537
|
+
* for a folder it could not read, and sets `unanswered` when that was a probe that established
|
|
538
|
+
* nothing. Answers the number of entries read, for the walk's read ceiling.
|
|
539
|
+
*/
|
|
540
|
+
async function readWalkFolder(cursor, budgetLeft, isListed) {
|
|
541
|
+
// The walk's root is the session's own folder. Below it any folder can be a mount that stopped
|
|
542
|
+
// answering, and a bulk scan keeps the stall cap (#516): an unbounded readdir there would hold a
|
|
543
|
+
// threadpool worker for good. `unknown` is never `absent`, so only a folder the filesystem says
|
|
544
|
+
// is gone drops out quietly: one that did not answer goes on the walk's unfinished list.
|
|
545
|
+
if (cursor.depth > 1) {
|
|
546
|
+
const kind = await probePathKind(cursor.dirPath);
|
|
547
|
+
cursor.unanswered = kind === 'unknown';
|
|
548
|
+
if (kind !== 'directory')
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
let read;
|
|
552
|
+
try {
|
|
553
|
+
read = await fs.readdir(cursor.dirPath, { withFileTypes: true });
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// Can't read directory (permission denied, etc.).
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
const listed = read.filter(isListed).sort((a, b) => {
|
|
560
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
561
|
+
return -1;
|
|
562
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
563
|
+
return 1;
|
|
564
|
+
return a.name.localeCompare(b.name);
|
|
565
|
+
});
|
|
566
|
+
cursor.more = listed.length > budgetLeft;
|
|
567
|
+
cursor.entries = cursor.more ? listed.slice(0, budgetLeft) : listed;
|
|
568
|
+
return read.length;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Where a folder's turn ends, given a `share` of the budget. Folders sort first, and each one found
|
|
572
|
+
* here waits for a later round, by which time a folder that spent the rest of its share on its own
|
|
573
|
+
* files has left nothing to read it with. So those files count as one more folder: they take an
|
|
574
|
+
* equal part of what the subfolders left and the remainder stays in the budget. A turn that found
|
|
575
|
+
* no subfolder has nothing to hold back for, and a turn always covers at least one entry.
|
|
576
|
+
*/
|
|
577
|
+
function walkTurnEnd(entries, start, share) {
|
|
578
|
+
const end = Math.min(start + share, entries.length);
|
|
579
|
+
let firstFile = start;
|
|
580
|
+
while (firstFile < end && entries[firstFile].isDirectory())
|
|
581
|
+
firstFile++;
|
|
582
|
+
return firstFile + Math.floor((end - firstFile) / (firstFile - start + 1));
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Whether a `root=live` listing may start at a session's `liveWorktree` (#765). `findLiveWorktree()`
|
|
586
|
+
* judged that path lexically when the pane moved, and a symlink can take its place, or a parent's,
|
|
587
|
+
* without moving the pane. The probe and `readdir` both follow one, and the walk's `path`s only look
|
|
588
|
+
* relative to `workingDir`, so this is the request-time half of the rule, the same one
|
|
589
|
+
* `validateSessionFilePath()` applies to a file: resolved, the worktree must lie strictly inside the
|
|
590
|
+
* resolved `workingDir`. Only the CHECK uses the resolved paths: the walk keeps the session's spelling,
|
|
591
|
+
* or a workspace reached through a symlink would stop producing paths relative to `workingDir`.
|
|
592
|
+
* Probed first (#516); both realpaths then walk the components that probe has just answered for.
|
|
593
|
+
*/
|
|
594
|
+
async function canWalkLiveWorktree(workingDir, worktreePath) {
|
|
595
|
+
if ((await probePathKind(worktreePath, { pastCap: true })) !== 'directory')
|
|
596
|
+
return false;
|
|
597
|
+
try {
|
|
598
|
+
const [realRoot, realWorktree] = await Promise.all([fs.realpath(workingDir), fs.realpath(worktreePath)]);
|
|
599
|
+
return realWorktree !== realRoot && isPathWithinRoot(realRoot, realWorktree);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
526
605
|
function findMatchingPickerRoot(roots, candidate) {
|
|
527
606
|
return roots
|
|
528
607
|
.filter((root) => isPathWithinRoot(root.path, candidate))
|
|
@@ -1113,6 +1192,69 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1113
1192
|
},
|
|
1114
1193
|
};
|
|
1115
1194
|
});
|
|
1195
|
+
// "New folder" in the path picker: creates ONE folder directly inside a folder the picker may browse.
|
|
1196
|
+
app.post('/api/filesystem/mkdir', async (req, reply) => {
|
|
1197
|
+
const { path: parentPath, name, sessionId, showHidden } = parseBody(FilesystemMkdirSchema, req.body);
|
|
1198
|
+
// Probed first (#516): the picker's resolver runs realpathSync on this path, which would freeze the
|
|
1199
|
+
// whole server on a mount that stopped answering.
|
|
1200
|
+
if ((await probePathKind(parentPath, { pastCap: true })) === 'unknown') {
|
|
1201
|
+
reply.code(422);
|
|
1202
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, describeUnknownPath('The folder', parentPath, { pastCap: true }));
|
|
1203
|
+
}
|
|
1204
|
+
const { candidatePath, resolvedPath, blockedTrees } = await resolveFilesystemPickerPath(ctx, req, parentPath, sessionId, wantsHiddenPickerEntries(showHidden));
|
|
1205
|
+
const target = join(resolvedPath, name);
|
|
1206
|
+
if (isBlockedPickerPath(resolvedPath, blockedTrees, true) || isBlockedPickerPath(target, blockedTrees, true)) {
|
|
1207
|
+
reply.code(403);
|
|
1208
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Folders cannot be created here');
|
|
1209
|
+
}
|
|
1210
|
+
// The cases directories (shared, and the caller's own in multi-user mode) are what GET /api/cases lists as
|
|
1211
|
+
// local cases: a folder made there is a case with no scaffold, which Link then refuses by name and which
|
|
1212
|
+
// deleting would take the linked folder with. A case there is made through Add Case > Create New with no
|
|
1213
|
+
// custom folder. The refusal has to read right from every picker that offers New folder: a Create New or
|
|
1214
|
+
// Clone Repo user is already on that tab, with the custom folder ticked, and this is where Browse opens
|
|
1215
|
+
// while the field is empty.
|
|
1216
|
+
const casesDirs = await Promise.all([...new Set([CASES_DIR, resolveCasesDir(getAuthUser(req))])].map((dir) => fs.realpath(dir).catch(() => resolve(dir))));
|
|
1217
|
+
if (casesDirs.includes(resolvedPath)) {
|
|
1218
|
+
reply.code(400);
|
|
1219
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'This is a cases folder: every folder in it is a case. Pick a folder outside it, or make the case with Add Case > Create New or Clone Repo (no custom folder).');
|
|
1220
|
+
}
|
|
1221
|
+
// Like browse, a file is not a place to create in.
|
|
1222
|
+
try {
|
|
1223
|
+
if (!(await fs.stat(resolvedPath)).isDirectory())
|
|
1224
|
+
throw new Error('not a directory');
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
reply.code(400);
|
|
1228
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'The folder to create in must be a directory');
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
// Not recursive: the parent was just resolved inside an allowed root, and an existing folder must be a 409.
|
|
1232
|
+
await fs.mkdir(target);
|
|
1233
|
+
}
|
|
1234
|
+
catch (err) {
|
|
1235
|
+
const code = err.code;
|
|
1236
|
+
if (code === 'EEXIST') {
|
|
1237
|
+
reply.code(409);
|
|
1238
|
+
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, `"${name}" already exists in this folder`);
|
|
1239
|
+
}
|
|
1240
|
+
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') {
|
|
1241
|
+
reply.code(403);
|
|
1242
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'This folder is not writable');
|
|
1243
|
+
}
|
|
1244
|
+
if (code === 'ENOENT') {
|
|
1245
|
+
reply.code(404);
|
|
1246
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, `Folder not found: ${candidatePath}`);
|
|
1247
|
+
}
|
|
1248
|
+
if (code === 'ENAMETOOLONG') {
|
|
1249
|
+
reply.code(400);
|
|
1250
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Folder name is too long');
|
|
1251
|
+
}
|
|
1252
|
+
// Never the errno text: it names the resolved, symlink-followed path, not the one the picker shows.
|
|
1253
|
+
reply.code(500);
|
|
1254
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Could not create the folder${code ? ` (${code})` : ''}`);
|
|
1255
|
+
}
|
|
1256
|
+
return { success: true, data: { path: join(candidatePath, name) } };
|
|
1257
|
+
});
|
|
1116
1258
|
// Inline preview for files selected through the root-confined filesystem picker.
|
|
1117
1259
|
app.get('/api/filesystem/preview', { compress: false }, async (req, reply) => {
|
|
1118
1260
|
const { path: requestedPath, sessionId, showHidden } = parseBody(FilesystemPreviewQuerySchema, req.query);
|
|
@@ -1156,11 +1298,20 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1156
1298
|
// File tree listing
|
|
1157
1299
|
app.get('/api/sessions/:id/files', async (req) => {
|
|
1158
1300
|
const { id } = req.params;
|
|
1159
|
-
const { depth, showHidden, q } = req.query;
|
|
1301
|
+
const { depth, showHidden, q, root } = req.query;
|
|
1160
1302
|
const session = findSessionOrFail(ctx, id, req);
|
|
1161
1303
|
const maxDepth = Math.min(parseInt(depth || '5', 10), 10);
|
|
1162
1304
|
const includeHidden = showHidden === 'true';
|
|
1163
1305
|
const workingDir = session.workingDir;
|
|
1306
|
+
// `root=live` starts the walk at the linked worktree the pane is in (#765), once that folder
|
|
1307
|
+
// proves to be inside `workingDir` right now, and every `path` below stays relative to
|
|
1308
|
+
// `workingDir`, so file-content, file-raw and the edit route need nothing new and judge these
|
|
1309
|
+
// paths against the same root as any other. Only the search predicate reads a path relative
|
|
1310
|
+
// to the listing, since that is the tree the person is looking at.
|
|
1311
|
+
const liveWorktree = session.liveWorktree;
|
|
1312
|
+
const walkRoot = root === 'live' && liveWorktree && (await canWalkLiveWorktree(workingDir, liveWorktree.path))
|
|
1313
|
+
? liveWorktree.path
|
|
1314
|
+
: workingDir;
|
|
1164
1315
|
// null for an empty/whitespace query, which is what keeps the default
|
|
1165
1316
|
// tree response byte-identical when no search is requested.
|
|
1166
1317
|
const matcher = compileFileQuery(q ?? '');
|
|
@@ -1185,6 +1336,8 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1185
1336
|
let totalDirectories = 0;
|
|
1186
1337
|
let truncated = false;
|
|
1187
1338
|
const maxFiles = 5000;
|
|
1339
|
+
let entriesRead = 0;
|
|
1340
|
+
const isListed = (entry) => (includeHidden || !entry.name.startsWith('.')) && !(entry.isDirectory() && excludeDirs.has(entry.name));
|
|
1188
1341
|
// ===== Search mode =====
|
|
1189
1342
|
// A query turns this endpoint into a FLAT match list rather than a nested
|
|
1190
1343
|
// tree. It recurses past non-matching directories on purpose — the whole
|
|
@@ -1194,71 +1347,84 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1194
1347
|
if (matcher) {
|
|
1195
1348
|
const matches = [];
|
|
1196
1349
|
const maxMatches = 1000;
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if (a.isDirectory() && !b.isDirectory())
|
|
1212
|
-
return -1;
|
|
1213
|
-
if (!a.isDirectory() && b.isDirectory())
|
|
1214
|
-
return 1;
|
|
1215
|
-
return a.name.localeCompare(b.name);
|
|
1216
|
-
});
|
|
1217
|
-
for (const entry of entries) {
|
|
1218
|
-
if (totalFiles + totalDirectories > maxFiles || matches.length >= maxMatches) {
|
|
1219
|
-
truncated = true;
|
|
1350
|
+
const queue = [{ dirPath: walkRoot, depth: 1, index: 0 }];
|
|
1351
|
+
// Folders the read ceiling kept shut or that did not answer. What was already read still gets its turns.
|
|
1352
|
+
const unread = [];
|
|
1353
|
+
while (queue.length > 0 && totalFiles + totalDirectories < maxFiles && matches.length < maxMatches) {
|
|
1354
|
+
// Per-directory round-robin (breadth-first): each queued directory gets an equal share of what
|
|
1355
|
+
// is left before any of them gets more, so results arrive shallow-first. Fair per directory,
|
|
1356
|
+
// not per subtree: a folder of thousands of small subfolders queues thousands of directories
|
|
1357
|
+
// and can still take most of the budget one level down. Reading a folder costs the same
|
|
1358
|
+
// whatever its share, so this never bounds the reading: MAX_WALK_ENTRIES_READ does.
|
|
1359
|
+
const directoriesInRound = queue.length;
|
|
1360
|
+
const nodesPerDirectory = Math.max(1, Math.floor((maxFiles - totalFiles - totalDirectories) / directoriesInRound));
|
|
1361
|
+
for (let directoryIndex = 0; directoryIndex < directoriesInRound; directoryIndex++) {
|
|
1362
|
+
// Spent part way through a round: the rest stay queued unread rather than sorted for nothing.
|
|
1363
|
+
if (totalFiles + totalDirectories >= maxFiles || matches.length >= maxMatches)
|
|
1220
1364
|
break;
|
|
1221
|
-
|
|
1222
|
-
if (!
|
|
1223
|
-
|
|
1224
|
-
if (
|
|
1365
|
+
const cursor = queue.shift();
|
|
1366
|
+
if (!cursor)
|
|
1367
|
+
break;
|
|
1368
|
+
if (cursor.depth > maxDepth) {
|
|
1369
|
+
truncated = true;
|
|
1225
1370
|
continue;
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
matches.push({ name: entry.name, path: relativePath, type: 'directory' });
|
|
1371
|
+
}
|
|
1372
|
+
if (!cursor.entries) {
|
|
1373
|
+
if (entriesRead >= MAX_WALK_ENTRIES_READ) {
|
|
1374
|
+
unread.push(cursor);
|
|
1375
|
+
continue;
|
|
1232
1376
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1377
|
+
entriesRead += await readWalkFolder(cursor, maxFiles - totalFiles - totalDirectories, isListed);
|
|
1378
|
+
if (cursor.unanswered)
|
|
1379
|
+
unread.push(cursor);
|
|
1380
|
+
if (!cursor.entries)
|
|
1381
|
+
continue;
|
|
1235
1382
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1383
|
+
const endIndex = walkTurnEnd(cursor.entries, cursor.index, nodesPerDirectory);
|
|
1384
|
+
while (cursor.index < endIndex && totalFiles + totalDirectories < maxFiles && matches.length < maxMatches) {
|
|
1385
|
+
const entry = cursor.entries[cursor.index++];
|
|
1386
|
+
const fullPath = join(cursor.dirPath, entry.name);
|
|
1387
|
+
const relativePath = relative(workingDir, fullPath);
|
|
1388
|
+
const listedPath = relative(walkRoot, fullPath);
|
|
1389
|
+
if (entry.isDirectory()) {
|
|
1390
|
+
totalDirectories++;
|
|
1391
|
+
if (matcher(entry.name, listedPath)) {
|
|
1392
|
+
matches.push({ name: entry.name, path: relativePath, type: 'directory' });
|
|
1242
1393
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1394
|
+
// Queued, not entered: it waits for a later round with every other directory found.
|
|
1395
|
+
queue.push({ dirPath: fullPath, depth: cursor.depth + 1, index: 0 });
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
totalFiles++;
|
|
1399
|
+
if (matcher(entry.name, listedPath)) {
|
|
1400
|
+
let size;
|
|
1401
|
+
try {
|
|
1402
|
+
size = (await fs.stat(fullPath)).size;
|
|
1403
|
+
}
|
|
1404
|
+
catch {
|
|
1405
|
+
// Skip size if we can't stat the match.
|
|
1406
|
+
}
|
|
1407
|
+
matches.push({
|
|
1408
|
+
name: entry.name,
|
|
1409
|
+
path: relativePath,
|
|
1410
|
+
type: 'file',
|
|
1411
|
+
size,
|
|
1412
|
+
extension: entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() : undefined,
|
|
1413
|
+
});
|
|
1245
1414
|
}
|
|
1246
|
-
matches.push({
|
|
1247
|
-
name: entry.name,
|
|
1248
|
-
path: relativePath,
|
|
1249
|
-
type: 'file',
|
|
1250
|
-
size,
|
|
1251
|
-
extension: entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() : undefined,
|
|
1252
|
-
});
|
|
1253
1415
|
}
|
|
1254
1416
|
}
|
|
1417
|
+
if (cursor.index < cursor.entries.length || cursor.more)
|
|
1418
|
+
queue.push(cursor);
|
|
1255
1419
|
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1420
|
+
}
|
|
1421
|
+
if (queue.length > 0 || unread.length > 0)
|
|
1422
|
+
truncated = true;
|
|
1258
1423
|
return {
|
|
1259
1424
|
success: true,
|
|
1260
1425
|
data: {
|
|
1261
|
-
root:
|
|
1426
|
+
root: walkRoot,
|
|
1427
|
+
...(liveWorktree && { worktree: liveWorktree }),
|
|
1262
1428
|
tree: [],
|
|
1263
1429
|
matches,
|
|
1264
1430
|
totalFiles,
|
|
@@ -1270,44 +1436,50 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1270
1436
|
},
|
|
1271
1437
|
};
|
|
1272
1438
|
}
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
//
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
// Skip hidden files unless requested
|
|
1295
|
-
if (!includeHidden && entry.name.startsWith('.'))
|
|
1439
|
+
const tree = [];
|
|
1440
|
+
const queue = [{ dirPath: walkRoot, depth: 1, nodes: tree, index: 0 }];
|
|
1441
|
+
const unread = [];
|
|
1442
|
+
while (queue.length > 0 && totalFiles + totalDirectories < maxFiles) {
|
|
1443
|
+
// The same per-directory round-robin as the search walk above, with the same limits.
|
|
1444
|
+
const directoriesInRound = queue.length;
|
|
1445
|
+
const nodesPerDirectory = Math.max(1, Math.floor((maxFiles - totalFiles - totalDirectories) / directoriesInRound));
|
|
1446
|
+
for (let directoryIndex = 0; directoryIndex < directoriesInRound; directoryIndex++) {
|
|
1447
|
+
// Before the shift, so a cursor this skips stays queued and is flagged below.
|
|
1448
|
+
if (totalFiles + totalDirectories >= maxFiles)
|
|
1449
|
+
break;
|
|
1450
|
+
const cursor = queue.shift();
|
|
1451
|
+
if (!cursor)
|
|
1452
|
+
break;
|
|
1453
|
+
if (cursor.depth > maxDepth) {
|
|
1454
|
+
truncated = true;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (!cursor.entries) {
|
|
1458
|
+
if (entriesRead >= MAX_WALK_ENTRIES_READ) {
|
|
1459
|
+
unread.push(cursor);
|
|
1296
1460
|
continue;
|
|
1297
|
-
|
|
1298
|
-
|
|
1461
|
+
}
|
|
1462
|
+
entriesRead += await readWalkFolder(cursor, maxFiles - totalFiles - totalDirectories, isListed);
|
|
1463
|
+
if (cursor.unanswered)
|
|
1464
|
+
unread.push(cursor);
|
|
1465
|
+
if (!cursor.entries)
|
|
1299
1466
|
continue;
|
|
1300
|
-
|
|
1467
|
+
}
|
|
1468
|
+
const endIndex = walkTurnEnd(cursor.entries, cursor.index, nodesPerDirectory);
|
|
1469
|
+
while (cursor.index < endIndex && totalFiles + totalDirectories < maxFiles) {
|
|
1470
|
+
const entry = cursor.entries[cursor.index++];
|
|
1471
|
+
const fullPath = join(cursor.dirPath, entry.name);
|
|
1301
1472
|
const relativePath = fullPath.slice(workingDir.length + 1);
|
|
1302
1473
|
if (entry.isDirectory()) {
|
|
1303
1474
|
totalDirectories++;
|
|
1304
|
-
const
|
|
1305
|
-
nodes.push({
|
|
1475
|
+
const node = {
|
|
1306
1476
|
name: entry.name,
|
|
1307
1477
|
path: relativePath,
|
|
1308
1478
|
type: 'directory',
|
|
1309
|
-
children,
|
|
1310
|
-
}
|
|
1479
|
+
children: [],
|
|
1480
|
+
};
|
|
1481
|
+
cursor.nodes.push(node);
|
|
1482
|
+
queue.push({ dirPath: fullPath, depth: cursor.depth + 1, node, nodes: node.children, index: 0 });
|
|
1311
1483
|
}
|
|
1312
1484
|
else {
|
|
1313
1485
|
totalFiles++;
|
|
@@ -1318,29 +1490,28 @@ export function registerFileRoutes(app, ctx) {
|
|
|
1318
1490
|
size = stat.size;
|
|
1319
1491
|
}
|
|
1320
1492
|
catch {
|
|
1321
|
-
// Skip if can't stat
|
|
1493
|
+
// Skip if can't stat.
|
|
1322
1494
|
}
|
|
1323
|
-
nodes.push({
|
|
1324
|
-
name: entry.name,
|
|
1325
|
-
path: relativePath,
|
|
1326
|
-
type: 'file',
|
|
1327
|
-
size,
|
|
1328
|
-
extension: ext,
|
|
1329
|
-
});
|
|
1495
|
+
cursor.nodes.push({ name: entry.name, path: relativePath, type: 'file', size, extension: ext });
|
|
1330
1496
|
}
|
|
1331
1497
|
}
|
|
1332
|
-
|
|
1498
|
+
if (cursor.index < cursor.entries.length || cursor.more)
|
|
1499
|
+
queue.push(cursor);
|
|
1333
1500
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1501
|
+
}
|
|
1502
|
+
// Every directory still queued is unread or half read, and an empty `children` would read as an
|
|
1503
|
+
// empty folder. The root has no node to flag: `truncated` says the same for it.
|
|
1504
|
+
for (const cursor of [...queue, ...unread]) {
|
|
1505
|
+
if (cursor.node)
|
|
1506
|
+
cursor.node.incomplete = true;
|
|
1507
|
+
}
|
|
1508
|
+
if (queue.length > 0 || unread.length > 0)
|
|
1509
|
+
truncated = true;
|
|
1340
1510
|
return {
|
|
1341
1511
|
success: true,
|
|
1342
1512
|
data: {
|
|
1343
|
-
root:
|
|
1513
|
+
root: walkRoot,
|
|
1514
|
+
...(liveWorktree && { worktree: liveWorktree }),
|
|
1344
1515
|
tree,
|
|
1345
1516
|
totalFiles,
|
|
1346
1517
|
totalDirectories,
|