@co0ontty/wand 4.106.0 → 4.106.1
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 +2 -2
- package/dist/ai-text-runner.js +24 -14
- package/dist/build-info.d.ts +2 -0
- package/dist/build-info.js +18 -0
- package/dist/build-info.json +3 -3
- package/dist/cli.js +8 -2
- package/dist/daemon-maintenance-state.js +1 -1
- package/dist/daemon-maintenance-targets.d.ts +1 -0
- package/dist/daemon-maintenance-targets.js +25 -9
- package/dist/daemon-maintenance.d.ts +3 -0
- package/dist/daemon-maintenance.js +60 -13
- package/dist/employee-im-types.d.ts +1 -1
- package/dist/employee-input-scheduling.d.ts +4 -0
- package/dist/employee-knowledge-cli.js +5 -5
- package/dist/employee-knowledge.d.ts +3 -1
- package/dist/employee-knowledge.js +24 -8
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +15 -2
- package/dist/git-worktree-batch.d.ts +26 -0
- package/dist/git-worktree-batch.js +139 -0
- package/dist/global-knowledge.d.ts +12 -0
- package/dist/global-knowledge.js +124 -0
- package/dist/iteration-log.d.ts +13 -5
- package/dist/iteration-log.js +120 -59
- package/dist/knowledge-types.d.ts +12 -1
- package/dist/knowledge-types.js +4 -0
- package/dist/middleware/api-scope.js +1 -0
- package/dist/model-groups.js +6 -3
- package/dist/models/parsers/opencode.js +31 -1
- package/dist/models.js +15 -14
- package/dist/npm-update-utils.d.ts +5 -2
- package/dist/npm-update-utils.js +34 -4
- package/dist/opencode-cli.d.ts +6 -0
- package/dist/opencode-cli.js +51 -0
- package/dist/opencode-system-fallback.d.ts +1 -0
- package/dist/opencode-system-fallback.js +16 -4
- package/dist/pi-native-event-bridge.js +55 -3
- package/dist/pi-native-input.d.ts +22 -0
- package/dist/pi-native-input.js +77 -0
- package/dist/pi-tmux-host.d.ts +14 -5
- package/dist/pi-tmux-host.js +68 -17
- package/dist/process-manager.js +17 -3
- package/dist/pty-tmux-backend.d.ts +27 -2
- package/dist/pty-tmux-backend.js +471 -70
- package/dist/pty-tmux-log.d.ts +1 -1
- package/dist/pty-tmux-log.js +23 -2
- package/dist/retention-protection.d.ts +1 -1
- package/dist/retention-protection.js +3 -2
- package/dist/retention.d.ts +5 -0
- package/dist/retention.js +11 -0
- package/dist/server-knowledge-routes.js +2 -2
- package/dist/server-session-input-routes.js +38 -19
- package/dist/server-session-lifecycle-routes.js +14 -1
- package/dist/server-session-worktree-routes.js +105 -12
- package/dist/server-task-routes.js +41 -12
- package/dist/server-workspace-routes.js +77 -9
- package/dist/server.js +67 -35
- package/dist/session-registry.d.ts +17 -3
- package/dist/session-registry.js +66 -13
- package/dist/session-service.js +1 -1
- package/dist/storage/internal.d.ts +4 -3
- package/dist/storage/internal.js +3 -3
- package/dist/storage/knowledge-repository.d.ts +5 -0
- package/dist/storage/knowledge-repository.js +75 -16
- package/dist/storage/mappers.d.ts +1 -0
- package/dist/storage/mappers.js +6 -1
- package/dist/storage/migrations.js +60 -0
- package/dist/storage/session-input-repository.d.ts +19 -0
- package/dist/storage/session-input-repository.js +35 -0
- package/dist/storage/task-workflow-repository.d.ts +29 -0
- package/dist/storage/task-workflow-repository.js +110 -0
- package/dist/storage/tasks-repository.d.ts +6 -1
- package/dist/storage/tasks-repository.js +94 -34
- package/dist/storage/workspaces-repository.d.ts +7 -0
- package/dist/storage/workspaces-repository.js +28 -2
- package/dist/storage-cleanup.d.ts +7 -2
- package/dist/storage-cleanup.js +16 -0
- package/dist/storage-schema.js +2 -0
- package/dist/storage.d.ts +5 -1
- package/dist/storage.js +12 -0
- package/dist/structured-exec-host.d.ts +4 -0
- package/dist/structured-exec-pump.d.ts +1 -0
- package/dist/structured-exec-pump.js +18 -11
- package/dist/structured-opencode-adapter.d.ts +2 -1
- package/dist/structured-opencode-adapter.js +32 -8
- package/dist/structured-pi-adapter.js +13 -2
- package/dist/structured-runner.d.ts +6 -0
- package/dist/structured-session-manager-replay.d.ts +2 -0
- package/dist/structured-session-manager-replay.js +27 -0
- package/dist/structured-session-manager.d.ts +32 -21
- package/dist/structured-session-manager.js +395 -118
- package/dist/structured-tmux-host.d.ts +23 -2
- package/dist/structured-tmux-host.js +143 -20
- package/dist/task-actions.js +5 -3
- package/dist/task-archive-policy.js +2 -1
- package/dist/task-comment-dispatch.d.ts +2 -1
- package/dist/task-comment-dispatch.js +19 -6
- package/dist/task-execution-queue.d.ts +2 -0
- package/dist/task-execution-queue.js +10 -10
- package/dist/task-hierarchy.js +1 -1
- package/dist/task-read-model.d.ts +1 -1
- package/dist/task-read-model.js +2 -1
- package/dist/task-types.d.ts +34 -3
- package/dist/task-types.js +21 -5
- package/dist/task-workflow-cli.d.ts +2 -0
- package/dist/task-workflow-cli.js +29 -0
- package/dist/task-workflow-runner.d.ts +4 -0
- package/dist/task-workflow-runner.js +36 -0
- package/dist/task-workflow.d.ts +13 -0
- package/dist/task-workflow.js +48 -0
- package/dist/terminal-daemon-server.js +12 -4
- package/dist/tmux-policy.d.ts +9 -0
- package/dist/tmux-policy.js +46 -0
- package/dist/tmux-terminal-host.js +5 -2
- package/dist/tui/commands.js +2 -1
- package/dist/types.d.ts +5 -0
- package/dist/wand-task-sync.js +5 -0
- package/dist/web-ui/content/ai-teams.js +9 -9
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/{52574891ef08e29b → 014fa5c6f973badf}/host.js +3 -3
- package/dist/web-ui/content/modules/0818b1d31a11d006/employee-profile.js +1 -0
- package/dist/web-ui/content/modules/{31157ceb6d23b809 → 0a58d9ec273078e4}/chunk.js +3 -3
- package/dist/web-ui/content/modules/{bf3ee58e0da44822 → 0a979dcd165cd0cc}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5f298a8f43dd0589 → 0c143928e8f96abc}/chunk.js +1 -1
- package/dist/web-ui/content/modules/0e9e42a319f6b8fc/workspace-window.js +1 -0
- package/dist/web-ui/content/modules/{b713cad51ca4c512 → 0f0d1769b732c0bd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4970319a83432ec7 → 1447886e6a1e74a4}/chunk.js +1 -1
- package/dist/web-ui/content/modules/151c734cbca31da5/host.js +1 -0
- package/dist/web-ui/content/modules/{e5174201f9d918fb → 16e4ab9c5263fcfd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/17e752fb53952753/host.js +1 -0
- package/dist/web-ui/content/modules/{913187c92a98a1ac → 20ed1454fc09fa6f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/255a3baf59d2b734/home.js +6 -0
- package/dist/web-ui/content/modules/{2f801e53dda62770 → 296dd81886c42b45}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{8891ca8165435ca1 → 2a01f4e525c0725e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/2ab4409c82b44fba/markdown.js +1 -0
- package/dist/web-ui/content/modules/2c955eced11610c2/chunk.js +1 -0
- package/dist/web-ui/content/modules/2d7ee4f9e2c0aca4/panel.js +1 -0
- package/dist/web-ui/content/modules/{3f8830f8ad28decc → 33ae550e4a295c85}/chunk.js +1 -1
- package/dist/web-ui/content/modules/35f4aa4443696bd0/chunk.js +1 -0
- package/dist/web-ui/content/modules/388b37ffcce9d4ff/host.js +1 -0
- package/dist/web-ui/content/modules/39279604dcdac7c7/runtime.js +338 -0
- package/dist/web-ui/content/modules/{57c357d321db5df9 → 3a3cdff2a662b7d3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{d49f106c4d106057 → 3ab646be55514243}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{16dab1297efd71a7 → 3ae88a5c5e1949d2}/chunk.js +1 -1
- package/dist/web-ui/content/modules/3e3b0e71d6ce80d8/chunk.js +1 -0
- package/dist/web-ui/content/modules/{ec33e17c00c1eb24 → 434aaaaf9604dc34}/chunk.js +1 -1
- package/dist/web-ui/content/modules/4385cd48771e1d4f/host.js +1 -0
- package/dist/web-ui/content/modules/{face3dbf117d63e3 → 4638129d60f549a0}/chunk.js +1 -1
- package/dist/web-ui/content/modules/47407209931b7011/host.js +13 -0
- package/dist/web-ui/content/modules/4829dd712d8b366b/chunk.js +1 -0
- package/dist/web-ui/content/modules/488d5b747bf374c8/chunk.js +1 -0
- package/dist/web-ui/content/modules/{ed7405f064944215 → 4ab085c195f7d76a}/sha256.js +1 -1
- package/dist/web-ui/content/modules/{f27a5d3c6c02a71c → 4c485bd44740fec7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{771439a41e737ee2 → 4ca0494308ab2bb3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/4ec571218779ba3e/composer-surface-adapter.js +1 -0
- package/dist/web-ui/content/modules/{5933ce8965aa4004 → 5023bf0294c2e7cb}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e63582a3bce48f7d → 5456e3417600bf15}/chunk.js +1 -1
- package/dist/web-ui/content/modules/55c588b0f5b41a52/host-bridge.js +1 -0
- package/dist/web-ui/content/modules/{9d941d6ff39861b8 → 5c3c8237ba7d8460}/chunk.js +1 -1
- package/dist/web-ui/content/modules/624b858da9834757/host.js +1 -0
- package/dist/web-ui/content/modules/6290be936320d4b1/host.js +1 -0
- package/dist/web-ui/content/modules/66be408793048cc3/progress.js +1 -0
- package/dist/web-ui/content/modules/67a11176a563317b/chunk.js +1 -0
- package/dist/web-ui/content/modules/{e060508ecd50fa7e → 68ef6ec7d26d78cd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{41445c6589dba325 → 6ec3e41df31927d9}/host.js +2 -2
- package/dist/web-ui/content/modules/{97d7f267959fd385 → 6f10f369445bbcbe}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{d47d75a78ae598e3 → 7295d9ff92d5e1e7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f383d892fef73483 → 72cd613de1a5f69b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/73aae1daafeea7e7/workspaces-panel.js +4 -0
- package/dist/web-ui/content/modules/{c2f09c8a472f5d83 → 73d2689f13b9a3e9}/dialog-modal.js +1 -1
- package/dist/web-ui/content/modules/{2f97ddba3842adaa → 75cf0c048e9b92ab}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4ba1e8c74bed7ef2 → 75dabda3b141296e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f3cff7792ba39df8 → 76c10b58d5f92d02}/login-visual.js +1 -1
- package/dist/web-ui/content/modules/{b63e075f222a1266 → 783583a12f5010f8}/library-bridge.js +1 -1
- package/dist/web-ui/content/modules/7e13a8a4a981f2f1/group-editor.js +1 -0
- package/dist/web-ui/content/modules/{0d76e582a7a71b2f → 7e3034fea34c6dae}/chunk.js +1 -1
- package/dist/web-ui/content/modules/80bf3291f2577a68/host.js +1 -0
- package/dist/web-ui/content/modules/{cba6f13c0b2aec1c → 82c3e08f34a6b828}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{dedd4a19bb70e327 → 83d1f4cf82c65f19}/page.js +3 -3
- package/dist/web-ui/content/modules/844f97958f25240c/shell-file-panel-drawer.js +1 -0
- package/dist/web-ui/content/modules/{62e0023451f428cf → 899cdc467044d418}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5083a5dbb5b155b7 → 8c7ca1fe72741c07}/chunk.js +1 -1
- package/dist/web-ui/content/modules/8dae4ca2261525e4/task-board-host.js +27 -0
- package/dist/web-ui/content/modules/92195c14c74929da/list-filter-panel.js +1 -0
- package/dist/web-ui/content/modules/{a61393bfa976d8d5 → 93793696af76bcbf}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{db521a246e14e5e4 → 97cd6af3da5622b8}/chunk.js +4 -4
- package/dist/web-ui/content/modules/{8de8368570ab089d → 9d3179258249ec77}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{579d486763d47c0d → a234c484078c0568}/chunk.js +1 -1
- package/dist/web-ui/content/modules/a4d215448a089a7d/host.js +1 -0
- package/dist/web-ui/content/modules/{c0a446100d969f3e → ae003f89901c5687}/chunk.js +1 -1
- package/dist/web-ui/content/modules/ae712bdd45b9917e/employee-card.js +1 -0
- package/dist/web-ui/content/modules/{a2caed0139df4b21 → b2a15293f7965d09}/chunk.js +1 -1
- package/dist/web-ui/content/modules/b4ee65e9998f0088/host.js +61 -0
- package/dist/web-ui/content/modules/{bd3daf04a8bf4422 → b666137d17f83be9}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{831b6c7b43e800be → b66beb6c0185256b}/host.js +1 -1
- package/dist/web-ui/content/modules/{816eb9b3a000fb2c → bb03b17e877657de}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{1eab7410d5586038 → bc343afc991db1a1}/chunk.js +1 -1
- package/dist/web-ui/content/modules/bca45976ff8fe2ac/host.js +69 -0
- package/dist/web-ui/content/modules/{d434edbb2f4672e5 → c2715dbfc9eaf510}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{d6fffe2c2a3a738d → c30c220281f35d74}/toast-region.js +1 -1
- package/dist/web-ui/content/modules/c4cc46fa9b4adddc/dropdown-surface.js +1 -0
- package/dist/web-ui/content/modules/c6dd19ee3d3ffd59/brand-identity.js +1 -0
- package/dist/web-ui/content/modules/c7ea6a114e6ff8ae/host.js +1 -0
- package/dist/web-ui/content/modules/{a59734be368daa7e → c916d82c8acef9c8}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{182929e188c1ab3a → cceaedda619a737a}/host.js +1 -1
- package/dist/web-ui/content/modules/cf67f52b076b9d2d/panel-host.js +1 -0
- package/dist/web-ui/content/modules/{9e163d54ff8fd1aa → d26c86558d422313}/chunk.js +1 -1
- package/dist/web-ui/content/modules/d2b8c6c6f3706032/queue-view-adapter.js +1 -0
- package/dist/web-ui/content/modules/d52ebe2ef12a9fa8/chunk.js +3 -0
- package/dist/web-ui/content/modules/{3fe11a75284ad8ae → d552b3bff2151eb6}/host.js +2 -2
- package/dist/web-ui/content/modules/d5d6b4511ac76a6e/chunk.js +3 -0
- package/dist/web-ui/content/modules/{bd7dc4bb46731ee7 → df42e151d625463b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{2a791cd2e14aa1e0 → e0a04f5ae56c68aa}/notice-view.js +1 -1
- package/dist/web-ui/content/modules/{438807b8bf112cd0 → e28e607c95d6f63a}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{50da446c1377c220 → e3880df97ea62d0c}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{87c7f50d9455d55e → e66196f1ece3e3ad}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{9b12c2301f3a18f3 → eb76307444ac9d71}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{c33935155289c22d → ebff7ca22cee1468}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4861eca9a8a87ad0 → ed1c712c569960d6}/chunk.js +1 -1
- package/dist/web-ui/content/modules/ee8b9c2d4bec63d4/chunk.js +1 -0
- package/dist/web-ui/content/modules/f458a194ce4208e2/chunk.js +1 -0
- package/dist/web-ui/content/modules/{11af5d14b30e57b6 → f938e826420cfe1b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/fa7b9bf6cc84bfc1/chat-render.js +81 -0
- package/dist/web-ui/content/modules/{ea8fe4b9999fd2aa → fd1fb1834f5f06df}/chunk.js +1 -1
- package/dist/web-ui/content/modules/fd3084e562a3dbd6/host.js +1 -0
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +416 -413
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/page-route.js +2 -2
- package/dist/web-ui/react/workspaces/types.d.ts +3 -0
- package/dist/web-ui/thinking-efforts.d.ts +2 -0
- package/dist/web-ui/thinking-efforts.js +8 -0
- package/package.json +1 -2
- package/scripts/stage-render-binaries.js +0 -1
- package/skills/wand-knowledge/SKILL.md +5 -3
- package/dist/structured-bounded-keys.d.ts +0 -19
- package/dist/structured-bounded-keys.js +0 -47
- package/dist/web-ui/content/modules/00b168a19664439e/dropdown-surface.js +0 -1
- package/dist/web-ui/content/modules/059c5fb7f128d340/task-board-host.js +0 -27
- package/dist/web-ui/content/modules/07494f5a623e4471/chat-render.js +0 -76
- package/dist/web-ui/content/modules/0a743ad3b4db6874/chunk.js +0 -3
- package/dist/web-ui/content/modules/1858670c0f84fec5/employee-profile.js +0 -1
- package/dist/web-ui/content/modules/20c47dc0442f5a04/host.js +0 -1
- package/dist/web-ui/content/modules/226241b999d12008/host.js +0 -1
- package/dist/web-ui/content/modules/23c4b8d223ec64c5/host.js +0 -61
- package/dist/web-ui/content/modules/2882c97388ff4f7b/employee-card.js +0 -1
- package/dist/web-ui/content/modules/2c90c3be260e6cce/progress.js +0 -1
- package/dist/web-ui/content/modules/2d7be9935394f397/workspace-window.js +0 -1
- package/dist/web-ui/content/modules/389a5112332ec23a/host.js +0 -1
- package/dist/web-ui/content/modules/3ea13e8b3c2deaa4/panel-host.js +0 -1
- package/dist/web-ui/content/modules/42e5c34002fcffb7/chunk.js +0 -1
- package/dist/web-ui/content/modules/4a5d669c90f545cb/host.js +0 -1
- package/dist/web-ui/content/modules/5705d25468274534/host.js +0 -1
- package/dist/web-ui/content/modules/5961ab2947c86ca1/shell-file-panel-drawer.js +0 -1
- package/dist/web-ui/content/modules/5b6e1369df7dea69/chunk.js +0 -1
- package/dist/web-ui/content/modules/6787bdcf56f743a5/host.js +0 -1
- package/dist/web-ui/content/modules/67c39b0398186a9f/host.js +0 -1
- package/dist/web-ui/content/modules/6b16f35c0627bafb/chunk.js +0 -1
- package/dist/web-ui/content/modules/6dd4f3e2f619f7a0/chunk.js +0 -1
- package/dist/web-ui/content/modules/70a2e13e0ccc8549/panel.js +0 -1
- package/dist/web-ui/content/modules/710db590e9707f73/runtime.js +0 -338
- package/dist/web-ui/content/modules/7249950f8771dbeb/workspaces-panel.js +0 -4
- package/dist/web-ui/content/modules/87dcd516ffd9f688/composer-surface-adapter.js +0 -1
- package/dist/web-ui/content/modules/8b7cd7bffdfd69e7/brand-identity.js +0 -1
- package/dist/web-ui/content/modules/8bf9917f6c3429f6/host.js +0 -1
- package/dist/web-ui/content/modules/8e612d4881fa4118/home.js +0 -6
- package/dist/web-ui/content/modules/8e6178262ea63dec/markdown.js +0 -1
- package/dist/web-ui/content/modules/96f2ae970f8c576b/host.js +0 -1
- package/dist/web-ui/content/modules/ba89cd97627e6682/queue-view-adapter.js +0 -1
- package/dist/web-ui/content/modules/c351eb11e0bdec2c/sidebar-peek.js +0 -1
- package/dist/web-ui/content/modules/ca7ca5902a35d26e/chunk.js +0 -1
- package/dist/web-ui/content/modules/d5d28ec375826ef2/chunk.js +0 -3
- package/dist/web-ui/content/modules/d6b16dedb9011c33/chunk.js +0 -1
- package/dist/web-ui/content/modules/d8a7011760f61719/group-editor.js +0 -1
- package/dist/web-ui/content/modules/d9e2b65e9679ad39/host.js +0 -75
- package/dist/web-ui/content/modules/eac72f69716dba05/chunk.js +0 -1
- package/dist/web-ui/content/modules/ebc53dedc3945514/host.js +0 -1
- package/dist/web-ui/content/modules/f43da7050badf54c/host.js +0 -1
- package/dist/web-ui/content/modules/fb2356eced24c4cc/host-bridge.js +0 -1
package/dist/pty-tmux-backend.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { chmodSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, openSync, closeSync, readSync, statSync, renameSync, rmSync } from "node:fs";
|
|
3
|
+
import { chmodSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, openSync, closeSync, readSync, statSync, renameSync, rmSync, lstatSync, existsSync } from "node:fs";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
import { ptyTmuxLogWriter } from "./pty-tmux-log.js";
|
|
@@ -13,22 +13,107 @@ s.removeAllListeners('data');const r=JSON.parse(b.slice(0,n));const c=spawn(r.fi
|
|
|
13
13
|
c.on('spawn',()=>s.end(JSON.stringify({pid:c.pid})+'\\n'));c.on('error',()=>process.exit(127));
|
|
14
14
|
c.on('exit',(code,signal)=>process.exit(code===null?128:code));});s.on('error',()=>process.exit(125));`;
|
|
15
15
|
function quote(value) { return `'${value.replaceAll("'", "'\\''")}'`; }
|
|
16
|
-
|
|
16
|
+
function directoryLatestMtime(directory) {
|
|
17
|
+
let latest = lstatSync(directory).mtimeMs;
|
|
18
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
19
|
+
const child = path.join(directory, entry.name);
|
|
20
|
+
const stat = lstatSync(child);
|
|
21
|
+
if (stat.isSymbolicLink())
|
|
22
|
+
continue;
|
|
23
|
+
if (stat.isDirectory())
|
|
24
|
+
latest = Math.max(latest, directoryLatestMtime(child));
|
|
25
|
+
else
|
|
26
|
+
latest = Math.max(latest, stat.mtimeMs);
|
|
27
|
+
}
|
|
28
|
+
return latest;
|
|
29
|
+
}
|
|
30
|
+
function directoryBytes(directory) {
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
33
|
+
const child = path.join(directory, entry.name);
|
|
34
|
+
const stat = lstatSync(child);
|
|
35
|
+
if (stat.isSymbolicLink())
|
|
36
|
+
continue;
|
|
37
|
+
if (stat.isDirectory())
|
|
38
|
+
total += directoryBytes(child);
|
|
39
|
+
else
|
|
40
|
+
total += stat.size;
|
|
41
|
+
}
|
|
42
|
+
return total;
|
|
43
|
+
}
|
|
44
|
+
function streamSocketPath(recordName) {
|
|
45
|
+
const hash = createHash("sha256").update(recordName).digest("hex").slice(0, 16);
|
|
46
|
+
const uid = process.getuid?.() ?? 0;
|
|
47
|
+
return path.join("/tmp", `wand-pts-${uid}-${hash}.sock`);
|
|
48
|
+
}
|
|
49
|
+
/** The default tmux server owns panes; attach transports are disposable, not execution owners. */
|
|
17
50
|
export class PtyTmuxBackend {
|
|
18
51
|
directory;
|
|
19
|
-
|
|
52
|
+
legacySocket;
|
|
20
53
|
constructor(configPath) {
|
|
21
54
|
this.directory = path.join(path.dirname(configPath), "pty-tmux");
|
|
22
55
|
mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
23
56
|
const hash = createHash("sha256").update(path.resolve(configPath)).digest("hex").slice(0, 20);
|
|
24
|
-
this.
|
|
57
|
+
this.legacySocket = `/tmp/wand-pty-${process.getuid?.() ?? 0}-${hash}.sock`;
|
|
25
58
|
}
|
|
26
|
-
|
|
27
|
-
|
|
59
|
+
connection(record) {
|
|
60
|
+
if (record.socket === "default")
|
|
61
|
+
return ["-L", "default"];
|
|
62
|
+
if (record.socket !== undefined)
|
|
63
|
+
throw new Error("Unknown tmux terminal owner socket");
|
|
64
|
+
return ["-S", this.legacySocket];
|
|
65
|
+
}
|
|
66
|
+
tmux(record, ...args) {
|
|
67
|
+
return execFileSync("tmux", [...this.connection(record), ...args], {
|
|
28
68
|
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000,
|
|
29
|
-
env: { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG },
|
|
69
|
+
env: { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG, TMUX_TMPDIR: process.env.TMUX_TMPDIR },
|
|
30
70
|
}).trim();
|
|
31
71
|
}
|
|
72
|
+
pane(record) { return record.pane ?? `=${record.name}:0.0`; }
|
|
73
|
+
/**
|
|
74
|
+
* 只认稳定身份:会话名 + 我们创建的 pane id + 写入的 owner 标记。
|
|
75
|
+
* `pane_pid` 在新建后可能还没就绪,不能作为归属证据;需要时从实时结果取。
|
|
76
|
+
*/
|
|
77
|
+
assertOwner(record) {
|
|
78
|
+
if (record.socket !== "default")
|
|
79
|
+
return record.panePid ?? null; // 旧私有 server 没有 owner 标记。
|
|
80
|
+
let fields = [];
|
|
81
|
+
try {
|
|
82
|
+
fields = this.tmux(record, "list-panes", "-t", `=${record.name}`, "-F", "#{pane_id}\t#{session_name}\t#{@wand-pty-owner}\t#{pane_pid}").split("\n")
|
|
83
|
+
.find(entry => entry.startsWith(`${record.pane}\t`))?.split("\t") ?? [];
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (this.isTmuxMissing(error))
|
|
87
|
+
return record.panePid ?? null;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (fields.length > 0 && fields[2] === record.sessionId) {
|
|
91
|
+
if (fields[0] !== record.pane || fields[1] !== record.name) {
|
|
92
|
+
throw new Error("tmux terminal ownership changed; refusing to control another pane");
|
|
93
|
+
}
|
|
94
|
+
const pid = Number(fields[3]);
|
|
95
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
96
|
+
}
|
|
97
|
+
// A different owner tag is conclusive; pane missing or not published is non-fatal check.
|
|
98
|
+
if (fields.length > 0 && fields[2]) {
|
|
99
|
+
throw new Error("tmux terminal ownership changed; refusing to control another pane");
|
|
100
|
+
}
|
|
101
|
+
return record.panePid ?? null;
|
|
102
|
+
}
|
|
103
|
+
isTmuxMissing(error) {
|
|
104
|
+
if (!error || typeof error !== "object")
|
|
105
|
+
return false;
|
|
106
|
+
const err = error;
|
|
107
|
+
const text = `${typeof err.stderr === "string" ? err.stderr : ""} ${err.message ?? ""}`.toLowerCase();
|
|
108
|
+
return text.includes("no server running") ||
|
|
109
|
+
text.includes("error connecting to") ||
|
|
110
|
+
text.includes("failed to connect") ||
|
|
111
|
+
text.includes("can't find session") ||
|
|
112
|
+
text.includes("session not found") ||
|
|
113
|
+
text.includes("can't find pane") ||
|
|
114
|
+
text.includes("can't find window") ||
|
|
115
|
+
text.includes("no such file or directory");
|
|
116
|
+
}
|
|
32
117
|
forget(sessionId) {
|
|
33
118
|
for (const file of readdirSync(this.directory).filter(file => file.endsWith(".json"))) {
|
|
34
119
|
const record = JSON.parse(readFileSync(path.join(this.directory, file), "utf8"));
|
|
@@ -36,14 +121,120 @@ export class PtyTmuxBackend {
|
|
|
36
121
|
if (record.exitCode === undefined)
|
|
37
122
|
throw new Error("tmux termination is not acknowledged");
|
|
38
123
|
try {
|
|
39
|
-
this.
|
|
124
|
+
this.assertOwner(record);
|
|
125
|
+
this.tmux(record, "kill-session", "-t", `=${record.name}`);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!this.isTmuxMissing(error))
|
|
129
|
+
throw error;
|
|
40
130
|
}
|
|
41
|
-
catch { /* Already destroyed or naturally exited. */ }
|
|
42
131
|
unlinkSync(path.join(this.directory, file));
|
|
43
132
|
rmSync(path.join(this.directory, `${record.name}.output`), { recursive: true, force: true });
|
|
44
133
|
}
|
|
45
134
|
}
|
|
46
135
|
}
|
|
136
|
+
collectGarbage(options) {
|
|
137
|
+
return PtyTmuxBackend.collectGarbage(path.dirname(this.directory), options);
|
|
138
|
+
}
|
|
139
|
+
static collectGarbage(configDir, options) {
|
|
140
|
+
const report = { removedRecords: 0, freedBytes: 0, removedSockets: 0 };
|
|
141
|
+
const now = options.now ?? Date.now();
|
|
142
|
+
const directory = path.join(configDir, "pty-tmux");
|
|
143
|
+
let entries = [];
|
|
144
|
+
try {
|
|
145
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
146
|
+
}
|
|
147
|
+
catch { /* directory not created */ }
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
150
|
+
continue;
|
|
151
|
+
const jsonPath = path.join(directory, entry.name);
|
|
152
|
+
let record;
|
|
153
|
+
try {
|
|
154
|
+
record = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (options.activeSessionIds.has(record.sessionId))
|
|
160
|
+
continue;
|
|
161
|
+
if (record.exitCode === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
const outputDir = path.join(directory, `${record.name}.output`);
|
|
164
|
+
let latestMtime = statSync(jsonPath).mtimeMs;
|
|
165
|
+
let bytes = statSync(jsonPath).size;
|
|
166
|
+
if (existsSync(outputDir)) {
|
|
167
|
+
try {
|
|
168
|
+
latestMtime = Math.max(latestMtime, directoryLatestMtime(outputDir));
|
|
169
|
+
bytes += directoryBytes(outputDir);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (now - latestMtime < options.retentionMs)
|
|
176
|
+
continue;
|
|
177
|
+
try {
|
|
178
|
+
unlinkSync(jsonPath);
|
|
179
|
+
if (existsSync(outputDir))
|
|
180
|
+
rmSync(outputDir, { recursive: true, force: true });
|
|
181
|
+
report.removedRecords += 1;
|
|
182
|
+
report.freedBytes += bytes;
|
|
183
|
+
}
|
|
184
|
+
catch { /* concurrent removal or permission error */ }
|
|
185
|
+
}
|
|
186
|
+
const uid = process.getuid?.() ?? 0;
|
|
187
|
+
const prefix = `wand-pty-${uid}-`;
|
|
188
|
+
const streamPrefix = `wand-pts-${uid}-`;
|
|
189
|
+
let tmpEntries = [];
|
|
190
|
+
try {
|
|
191
|
+
tmpEntries = readdirSync("/tmp");
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
tmpEntries = [];
|
|
195
|
+
}
|
|
196
|
+
for (const name of tmpEntries) {
|
|
197
|
+
if ((!name.startsWith(prefix) && !name.startsWith(streamPrefix)) || !name.endsWith(".sock"))
|
|
198
|
+
continue;
|
|
199
|
+
const socketPath = path.join("/tmp", name);
|
|
200
|
+
try {
|
|
201
|
+
const stat = lstatSync(socketPath);
|
|
202
|
+
if (!stat.isSocket())
|
|
203
|
+
continue;
|
|
204
|
+
if (name.startsWith(streamPrefix)) {
|
|
205
|
+
const probe = net.connect(socketPath);
|
|
206
|
+
probe.on("error", () => {
|
|
207
|
+
try {
|
|
208
|
+
unlinkSync(socketPath);
|
|
209
|
+
report.removedSockets += 1;
|
|
210
|
+
}
|
|
211
|
+
catch { /* ignore */ }
|
|
212
|
+
});
|
|
213
|
+
probe.on("connect", () => { probe.destroy(); });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
execFileSync("tmux", ["-S", socketPath, "-f", "/dev/null", "ls"], {
|
|
218
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 1000,
|
|
219
|
+
env: { PATH: process.env.PATH, HOME: process.env.HOME, LANG: process.env.LANG },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (probeError) {
|
|
223
|
+
const err = probeError;
|
|
224
|
+
const text = `${typeof err.stderr === "string" ? err.stderr : ""} ${err.message ?? ""}`.toLowerCase();
|
|
225
|
+
if (text.includes("no server running") || text.includes("error connecting to") || text.includes("failed to connect")) {
|
|
226
|
+
try {
|
|
227
|
+
unlinkSync(socketPath);
|
|
228
|
+
report.removedSockets += 1;
|
|
229
|
+
}
|
|
230
|
+
catch { /* ignore */ }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch { /* socket vanished during iteration */ }
|
|
235
|
+
}
|
|
236
|
+
return report;
|
|
237
|
+
}
|
|
47
238
|
recover(exclude = new Set()) {
|
|
48
239
|
return readdirSync(this.directory).filter(file => file.endsWith(".json"))
|
|
49
240
|
.map(file => JSON.parse(readFileSync(path.join(this.directory, file), "utf8")))
|
|
@@ -61,7 +252,7 @@ export class PtyTmuxBackend {
|
|
|
61
252
|
if (process.platform === "win32")
|
|
62
253
|
throw new Error("tmux PTY requires a POSIX host");
|
|
63
254
|
execFileSync("tmux", ["-V"], { stdio: "ignore" });
|
|
64
|
-
const name = `
|
|
255
|
+
const name = `wand-pty-${request.sessionId.replace(/[^a-zA-Z0-9-]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
65
256
|
const ipc = `/tmp/wand-pty-launch-${randomUUID()}.sock`;
|
|
66
257
|
let accept;
|
|
67
258
|
let reject;
|
|
@@ -94,15 +285,29 @@ export class PtyTmuxBackend {
|
|
|
94
285
|
});
|
|
95
286
|
await new Promise((resolve, fail) => { server.once("error", fail); server.listen(ipc, () => { chmodSync(ipc, 0o600); resolve(); }); });
|
|
96
287
|
const timer = setTimeout(() => reject(new Error("tmux launch acknowledgement missing; input was not retried")), 10000);
|
|
97
|
-
const record = { sessionId: request.sessionId, name, cols: request.cols, rows: request.rows,
|
|
288
|
+
const record = { sessionId: request.sessionId, name, socket: "default", cols: request.cols, rows: request.rows,
|
|
98
289
|
launchMarkerToken: request.launchMarkerToken, pid: 0 };
|
|
99
290
|
// Publish ownership before launch, never persist the command, environment or prompt.
|
|
100
291
|
this.save(record);
|
|
101
292
|
try {
|
|
102
|
-
this.tmux("new-session", "-d", "-s", name, "-x", String(request.cols), "-y", String(request.rows), `${quote(process.execPath)} -e ${quote(bootstrap)} ${quote(ipc)}
|
|
293
|
+
record.pane = this.tmux(record, "new-session", "-d", "-P", "-F", "#{pane_id}", "-s", name, "-x", String(request.cols), "-y", String(request.rows), `${quote(process.execPath)} -e ${quote(bootstrap)} ${quote(ipc)}`);
|
|
294
|
+
if (!/^%\d+$/.test(record.pane))
|
|
295
|
+
throw new Error("tmux terminal pane identity is unavailable");
|
|
296
|
+
record.panePid = Number(this.tmux(record, "display-message", "-p", "-t", record.pane, "#{pane_pid}"));
|
|
297
|
+
if (!Number.isSafeInteger(record.panePid) || record.panePid <= 0)
|
|
298
|
+
throw new Error("Invalid tmux terminal pane PID");
|
|
299
|
+
this.tmux(record, "set-option", "-t", name, "@wand-pty-owner", record.sessionId);
|
|
300
|
+
this.tmux(record, "set-window-option", "-t", record.pane, "remain-on-exit", "on");
|
|
301
|
+
this.tmux(record, "set-option", "-t", name, "status", "off");
|
|
302
|
+
this.save(record);
|
|
103
303
|
const log = path.join(this.directory, `${name}.output`);
|
|
104
304
|
mkdirSync(log, { mode: 0o700 });
|
|
105
|
-
|
|
305
|
+
const sockPath = streamSocketPath(name);
|
|
306
|
+
try {
|
|
307
|
+
unlinkSync(sockPath);
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
this.tmux(record, "pipe-pane", "-O", "-t", record.pane, `exec ${quote(process.execPath)} -e ${quote(ptyTmuxLogWriter)} ${quote(log)} ${quote(sockPath)}`);
|
|
106
311
|
ready();
|
|
107
312
|
record.pid = await delivered;
|
|
108
313
|
this.save(record);
|
|
@@ -147,8 +352,14 @@ export class PtyTmuxBackend {
|
|
|
147
352
|
let segment = 0;
|
|
148
353
|
let preservedMarker = "";
|
|
149
354
|
const log = path.join(this.directory, `${record.name}.output`);
|
|
150
|
-
const drain = () => {
|
|
151
|
-
|
|
355
|
+
const drain = (targetSegment, targetSize) => {
|
|
356
|
+
let segments = [];
|
|
357
|
+
try {
|
|
358
|
+
segments = readdirSync(log).filter(file => /^\d+\.chunk$/.test(file)).map(file => Number(file.split(".")[0])).sort((a, b) => a - b);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
152
363
|
if (!segments.length)
|
|
153
364
|
return false;
|
|
154
365
|
if (segment < segments[0]) {
|
|
@@ -165,37 +376,60 @@ export class PtyTmuxBackend {
|
|
|
165
376
|
throw error;
|
|
166
377
|
}
|
|
167
378
|
}
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
379
|
+
const maxSegment = typeof targetSegment === "number" ? Math.min(targetSegment, segments[segments.length - 1]) : segments[segments.length - 1];
|
|
380
|
+
while (segment <= maxSegment) {
|
|
381
|
+
const file = path.join(log, `${segment}.chunk`);
|
|
382
|
+
let size = 0;
|
|
383
|
+
try {
|
|
384
|
+
size = statSync(file).size;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
const limitSize = (typeof targetSegment === "number" && segment === targetSegment && typeof targetSize === "number")
|
|
390
|
+
? Math.min(size, targetSize)
|
|
391
|
+
: size;
|
|
392
|
+
if (offset >= limitSize) {
|
|
393
|
+
if (segment < maxSegment) {
|
|
394
|
+
segment++;
|
|
395
|
+
offset = 0;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (preservedMarker) {
|
|
399
|
+
for (const listener of data)
|
|
400
|
+
listener(preservedMarker);
|
|
401
|
+
preservedMarker = "";
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
const fd = openSync(file, "r");
|
|
406
|
+
try {
|
|
407
|
+
const buffer = Buffer.alloc(Math.min(limitSize - offset, 256 * 1024));
|
|
408
|
+
const count = readSync(fd, buffer, 0, buffer.length, offset);
|
|
409
|
+
offset += count;
|
|
410
|
+
const chunk = decoder.write(buffer.subarray(0, count));
|
|
411
|
+
if (chunk)
|
|
412
|
+
for (const listener of data)
|
|
413
|
+
listener(chunk);
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
closeSync(fd);
|
|
417
|
+
}
|
|
418
|
+
if (offset >= limitSize) {
|
|
419
|
+
if (segment < maxSegment) {
|
|
420
|
+
segment++;
|
|
421
|
+
offset = 0;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
180
425
|
}
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
const fd = openSync(file, "r");
|
|
184
|
-
try {
|
|
185
|
-
const buffer = Buffer.alloc(Math.min(size - offset, 256 * 1024));
|
|
186
|
-
const count = readSync(fd, buffer, 0, buffer.length, offset);
|
|
187
|
-
offset += count;
|
|
188
|
-
const chunk = decoder.write(buffer.subarray(0, count));
|
|
189
|
-
if (chunk)
|
|
190
|
-
for (const listener of data)
|
|
191
|
-
listener(chunk);
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
closeSync(fd);
|
|
195
426
|
}
|
|
196
|
-
return
|
|
427
|
+
return false;
|
|
428
|
+
};
|
|
429
|
+
const inspect = () => {
|
|
430
|
+
this.assertOwner(record);
|
|
431
|
+
return this.tmux(record, "display-message", "-p", "-t", this.pane(record), "#{pane_dead}:#{pane_dead_status}:#{pane_pipe}");
|
|
197
432
|
};
|
|
198
|
-
const inspect = () => this.tmux("display-message", "-p", "-t", `${record.name}:0.0`, "#{pane_dead}:#{pane_dead_status}:#{pane_pipe}");
|
|
199
433
|
const finish = (exitCode) => {
|
|
200
434
|
if (exited)
|
|
201
435
|
return;
|
|
@@ -203,46 +437,157 @@ export class PtyTmuxBackend {
|
|
|
203
437
|
record.exitCode = exitCode;
|
|
204
438
|
this.save(record);
|
|
205
439
|
clearInterval(timer);
|
|
440
|
+
if (streamSocket) {
|
|
441
|
+
try {
|
|
442
|
+
streamSocket.destroy();
|
|
443
|
+
}
|
|
444
|
+
catch { }
|
|
445
|
+
streamSocket = null;
|
|
446
|
+
}
|
|
206
447
|
transport?.kill();
|
|
207
448
|
for (const listener of exits)
|
|
208
449
|
listener(exited);
|
|
209
450
|
};
|
|
210
451
|
const connect = () => {
|
|
211
452
|
ensureNodePtyHelperExecutable();
|
|
212
|
-
transport = pty.spawn("tmux", [
|
|
453
|
+
transport = pty.spawn("tmux", [...this.connection(record), "attach-session", "-t", `=${record.name}`], {
|
|
213
454
|
name: "xterm-256color", cols: record.cols, rows: record.rows,
|
|
214
|
-
env: { PATH: process.env.PATH, HOME: process.env.HOME, TERM: "xterm-256color" },
|
|
455
|
+
env: { PATH: process.env.PATH, HOME: process.env.HOME, TERM: "xterm-256color", TMUX_TMPDIR: process.env.TMUX_TMPDIR },
|
|
215
456
|
});
|
|
216
457
|
transport.onData(() => { }); // Raw pipe-pane bytes, not screen repaint, are the PTY stream.
|
|
217
458
|
transport.onExit(() => { transport = null; });
|
|
218
459
|
};
|
|
460
|
+
const sockPath = streamSocketPath(record.name);
|
|
461
|
+
let streamSocket = null;
|
|
462
|
+
let handshaked = false;
|
|
463
|
+
let handshakeBuffer = "";
|
|
464
|
+
let connectAttempts = 0;
|
|
465
|
+
const checkExitAndFinish = () => {
|
|
466
|
+
if (exited)
|
|
467
|
+
return;
|
|
468
|
+
try {
|
|
469
|
+
const state = inspect();
|
|
470
|
+
if (state.startsWith("1:")) {
|
|
471
|
+
finish(Number(state.split(":")[1]) || 0);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
finish(0);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
if (this.isTmuxMissing(error))
|
|
479
|
+
finish(137);
|
|
480
|
+
else
|
|
481
|
+
finish(0);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const tryConnectStream = () => {
|
|
485
|
+
if (stopping || exited)
|
|
486
|
+
return;
|
|
487
|
+
const client = net.connect(sockPath);
|
|
488
|
+
streamSocket = client;
|
|
489
|
+
client.on("data", (chunk) => {
|
|
490
|
+
if (stopping || exited)
|
|
491
|
+
return;
|
|
492
|
+
if (!handshaked) {
|
|
493
|
+
handshakeBuffer += chunk.toString("utf8");
|
|
494
|
+
const newline = handshakeBuffer.indexOf("\n");
|
|
495
|
+
if (newline < 0)
|
|
496
|
+
return;
|
|
497
|
+
const line = handshakeBuffer.slice(0, newline);
|
|
498
|
+
const rest = handshakeBuffer.slice(newline + 1);
|
|
499
|
+
handshaked = true;
|
|
500
|
+
let targetSeg;
|
|
501
|
+
let targetSz;
|
|
502
|
+
try {
|
|
503
|
+
const meta = JSON.parse(line);
|
|
504
|
+
if (typeof meta.segment === "number")
|
|
505
|
+
targetSeg = meta.segment;
|
|
506
|
+
if (typeof meta.size === "number")
|
|
507
|
+
targetSz = meta.size;
|
|
508
|
+
}
|
|
509
|
+
catch { }
|
|
510
|
+
drain(targetSeg, targetSz);
|
|
511
|
+
if (rest.length > 0) {
|
|
512
|
+
for (const listener of data)
|
|
513
|
+
listener(rest);
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const text = decoder.write(chunk);
|
|
518
|
+
if (text) {
|
|
519
|
+
for (const listener of data)
|
|
520
|
+
listener(text);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
client.on("end", () => {
|
|
524
|
+
if (stopping || exited)
|
|
525
|
+
return;
|
|
526
|
+
drain();
|
|
527
|
+
checkExitAndFinish();
|
|
528
|
+
});
|
|
529
|
+
client.on("error", () => {
|
|
530
|
+
if (streamSocket === client)
|
|
531
|
+
streamSocket = null;
|
|
532
|
+
if (stopping || exited || handshaked)
|
|
533
|
+
return;
|
|
534
|
+
if (connectAttempts++ < 30) {
|
|
535
|
+
setTimeout(tryConnectStream, 40).unref();
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
drain();
|
|
539
|
+
if (record.exitCode !== undefined) {
|
|
540
|
+
finish(record.exitCode);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
checkExitAndFinish();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
client.on("close", () => {
|
|
548
|
+
if (streamSocket === client)
|
|
549
|
+
streamSocket = null;
|
|
550
|
+
});
|
|
551
|
+
};
|
|
552
|
+
tryConnectStream();
|
|
219
553
|
const timer = setInterval(() => {
|
|
554
|
+
if (stopping || exited || observationFailed) {
|
|
555
|
+
clearInterval(timer);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
220
558
|
try {
|
|
221
559
|
const state = record.exitCode === undefined ? inspect() : null;
|
|
222
|
-
if (state
|
|
560
|
+
if (state && !state.startsWith("0:") && !state.startsWith("1:")) {
|
|
223
561
|
reportFailure();
|
|
224
|
-
|
|
562
|
+
clearInterval(timer);
|
|
225
563
|
return;
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
564
|
+
}
|
|
565
|
+
if (state?.startsWith("0:") && state.split(":")[2] !== "1") {
|
|
566
|
+
reportFailure();
|
|
567
|
+
clearInterval(timer);
|
|
230
568
|
return;
|
|
231
569
|
}
|
|
232
570
|
if (state?.startsWith("1:")) {
|
|
233
|
-
|
|
234
|
-
|
|
571
|
+
drain();
|
|
572
|
+
finish(Number(state.split(":")[1]) || 0);
|
|
573
|
+
clearInterval(timer);
|
|
574
|
+
return;
|
|
235
575
|
}
|
|
236
|
-
|
|
576
|
+
if (!transport && !stopping && record.exitCode === undefined) {
|
|
237
577
|
connect();
|
|
578
|
+
}
|
|
238
579
|
}
|
|
239
580
|
catch (error) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
581
|
+
if (this.isTmuxMissing(error)) {
|
|
582
|
+
clearInterval(timer);
|
|
583
|
+
finish(137);
|
|
584
|
+
}
|
|
585
|
+
else if (error.code !== "ENOENT") {
|
|
243
586
|
reportFailure();
|
|
587
|
+
clearInterval(timer);
|
|
588
|
+
}
|
|
244
589
|
}
|
|
245
|
-
},
|
|
590
|
+
}, 2000);
|
|
246
591
|
timer.unref();
|
|
247
592
|
if (record.exitCode === undefined)
|
|
248
593
|
try {
|
|
@@ -252,31 +597,87 @@ export class PtyTmuxBackend {
|
|
|
252
597
|
catch { /* Keep unknown published ownership visible and block new launches. */ }
|
|
253
598
|
return {
|
|
254
599
|
pid: record.pid,
|
|
600
|
+
detach() {
|
|
601
|
+
stopping = true;
|
|
602
|
+
clearInterval(timer);
|
|
603
|
+
if (streamSocket) {
|
|
604
|
+
try {
|
|
605
|
+
streamSocket.destroy();
|
|
606
|
+
}
|
|
607
|
+
catch { }
|
|
608
|
+
streamSocket = null;
|
|
609
|
+
}
|
|
610
|
+
transport?.kill();
|
|
611
|
+
transport = null;
|
|
612
|
+
data.clear();
|
|
613
|
+
exits.clear();
|
|
614
|
+
},
|
|
255
615
|
write(chunk) { if (!transport || stopping || observationFailed)
|
|
256
616
|
throw new Error("tmux terminal transport unavailable"); transport.write(chunk); },
|
|
257
|
-
resize: (cols, rows) => {
|
|
617
|
+
resize: (cols, rows) => {
|
|
618
|
+
this.assertOwner(record);
|
|
619
|
+
this.tmux(record, "resize-window", "-t", this.pane(record), "-x", String(cols), "-y", String(rows));
|
|
620
|
+
transport?.resize(cols, rows);
|
|
621
|
+
},
|
|
258
622
|
kill: () => {
|
|
259
623
|
if (exited)
|
|
260
624
|
return;
|
|
625
|
+
stopping = true;
|
|
626
|
+
clearInterval(timer);
|
|
627
|
+
if (streamSocket) {
|
|
628
|
+
try {
|
|
629
|
+
streamSocket.destroy();
|
|
630
|
+
}
|
|
631
|
+
catch { }
|
|
632
|
+
streamSocket = null;
|
|
633
|
+
}
|
|
261
634
|
// Successful kill-session is the acknowledgement that the pane, not merely its client, was destroyed.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
635
|
+
let panePid = null;
|
|
636
|
+
try {
|
|
637
|
+
panePid = this.assertOwner(record);
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
if (!this.isTmuxMissing(error))
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
643
|
+
const killTree = (rootPid) => {
|
|
644
|
+
try {
|
|
645
|
+
const tree = execFileSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" }).trim().split("\n")
|
|
646
|
+
.map(line => line.trim().split(/\s+/).map(Number));
|
|
647
|
+
const descendants = (pid) => tree.filter(row => row[1] === pid)
|
|
648
|
+
.flatMap(row => [...descendants(row[0]), row[0]]);
|
|
649
|
+
for (const pid of descendants(rootPid)) {
|
|
650
|
+
try {
|
|
651
|
+
process.kill(pid, "SIGKILL");
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
if (error.code !== "ESRCH")
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch { /* ps or process traversal unavailable */ }
|
|
660
|
+
};
|
|
661
|
+
if (panePid !== null) {
|
|
662
|
+
killTree(panePid);
|
|
663
|
+
}
|
|
664
|
+
else if (record.socket === undefined && record.pid > 0) {
|
|
665
|
+
killTree(record.pid);
|
|
270
666
|
try {
|
|
271
|
-
process.kill(pid, "SIGKILL");
|
|
667
|
+
process.kill(record.pid, "SIGKILL");
|
|
272
668
|
}
|
|
273
669
|
catch (error) {
|
|
274
670
|
if (error.code !== "ESRCH")
|
|
275
671
|
throw error;
|
|
276
672
|
}
|
|
277
673
|
}
|
|
278
|
-
|
|
279
|
-
|
|
674
|
+
try {
|
|
675
|
+
this.tmux(record, "kill-session", "-t", `=${record.name}`);
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (!this.isTmuxMissing(error))
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
280
681
|
finish(137);
|
|
281
682
|
},
|
|
282
683
|
onData(listener) { data.add(listener); return { dispose() { data.delete(listener); } }; },
|