zidane 7.2.5 → 8.0.4
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/LICENSE +21 -0
- package/README.md +34 -6
- package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
- package/dist/acp-DQDhi2dj.js.map +1 -0
- package/dist/acp-cli.d.ts +1 -1
- package/dist/acp-cli.js +9 -66
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +10 -590
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +3 -3
- package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
- package/dist/agent-DSLNmBp0.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -2
- package/dist/aliasing-lVFbm81n.js +217 -0
- package/dist/aliasing-lVFbm81n.js.map +1 -0
- package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
- package/dist/anthropic-CD00LhUP.js.map +1 -0
- package/dist/artifacts-CV2hUKnL.js.map +1 -1
- package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
- package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
- package/dist/call-tool-dHlbGOIY.js +70 -0
- package/dist/call-tool-dHlbGOIY.js.map +1 -0
- package/dist/chat/pure.d.ts +372 -6
- package/dist/chat/pure.d.ts.map +1 -1
- package/dist/chat/pure.js +35 -244
- package/dist/chat/pure.js.map +1 -1
- package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
- package/dist/chat-JPJJ8uu5.js.map +1 -0
- package/dist/chat.d.ts +4 -6
- package/dist/chat.js +23 -12
- package/dist/cli-shared-C-YCaglL.js +159 -0
- package/dist/cli-shared-C-YCaglL.js.map +1 -0
- package/dist/client-core-BQSyDALT.d.ts +2115 -0
- package/dist/client-core-BQSyDALT.d.ts.map +1 -0
- package/dist/codemode-worker.d.ts +1 -0
- package/dist/codemode-worker.js +155 -0
- package/dist/codemode-worker.js.map +1 -0
- package/dist/codemode.d.ts +352 -0
- package/dist/codemode.d.ts.map +1 -0
- package/dist/codemode.js +5 -0
- package/dist/connection-Dr-Js4yZ.d.ts +28 -0
- package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
- package/dist/connection-loop-gdh22LTI.js +112 -0
- package/dist/connection-loop-gdh22LTI.js.map +1 -0
- package/dist/context-segments-BHKlagL6.js +1016 -0
- package/dist/context-segments-BHKlagL6.js.map +1 -0
- package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
- package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +19 -8
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +1 -440
- package/dist/contexts/docker.d.ts +36 -7
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +1 -456
- package/dist/contexts/e2b.d.ts +68 -13
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -382
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +5 -0
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
- package/dist/contexts-d7UolYot.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/cred-keys-GpBg71cP.js +105 -0
- package/dist/cred-keys-GpBg71cP.js.map +1 -0
- package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
- package/dist/credentials-JttEuRON.js.map +1 -0
- package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
- package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
- package/dist/daemon-cli.d.ts +1 -1
- package/dist/daemon-cli.js +3 -2
- package/dist/daemon-cli.js.map +1 -1
- package/dist/daytona-DG3omipZ.js +513 -0
- package/dist/daytona-DG3omipZ.js.map +1 -0
- package/dist/defaults-Cx3zvDOV.js +23 -0
- package/dist/defaults-Cx3zvDOV.js.map +1 -0
- package/dist/dispatcher-BMlu2BmZ.js +4941 -0
- package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
- package/dist/docker-BZhMMJUp.js +727 -0
- package/dist/docker-BZhMMJUp.js.map +1 -0
- package/dist/e2b-CEVOdRib.js +480 -0
- package/dist/e2b-CEVOdRib.js.map +1 -0
- package/dist/edit-utils-EGosADZq.js +234 -0
- package/dist/edit-utils-EGosADZq.js.map +1 -0
- package/dist/environment-8wsKE2Ln.js +214 -0
- package/dist/environment-8wsKE2Ln.js.map +1 -0
- package/dist/eval-BNcvle_J.js +1595 -0
- package/dist/eval-BNcvle_J.js.map +1 -0
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1489
- package/dist/executor-Cx5-8yqi.js +621 -0
- package/dist/executor-Cx5-8yqi.js.map +1 -0
- package/dist/extension-authoring-DyGGmq9V.js +385 -0
- package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +3 -3
- package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
- package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
- package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
- package/dist/format-C2NQEVUH.js.map +1 -0
- package/dist/format-bytes-C7tfc3Zm.js +55 -0
- package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
- package/dist/headless-C_rNUuYC.js +960 -0
- package/dist/headless-C_rNUuYC.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -413
- package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
- package/dist/history-search-CcYzF8-0.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -1
- package/dist/history-search-worker.js +1 -1
- package/dist/host-D7NrI03V.js +378 -0
- package/dist/host-D7NrI03V.js.map +1 -0
- package/dist/host-signals-BcP5QKUD.js +118 -0
- package/dist/host-signals-BcP5QKUD.js.map +1 -0
- package/dist/identity-CwfO8K-t.js +26 -0
- package/dist/identity-CwfO8K-t.js.map +1 -0
- package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
- package/dist/index-BGe1fQT7.d.ts.map +1 -0
- package/dist/index-BVQsIKGn.d.ts +448 -0
- package/dist/index-BVQsIKGn.d.ts.map +1 -0
- package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
- package/dist/index-CHABJd-L.d.ts.map +1 -0
- package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
- package/dist/index-D6a7qXpg.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -23
- package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
- package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
- package/dist/ipc/remote.d.ts +313 -0
- package/dist/ipc/remote.d.ts.map +1 -0
- package/dist/ipc/remote.js +4 -0
- package/dist/ipc.d.ts +6 -964
- package/dist/ipc.js +403 -4
- package/dist/ipc.js.map +1 -0
- package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
- package/dist/isolation-D9L650W5.d.ts.map +1 -0
- package/dist/json-rpc--HXQti1E.js +350 -0
- package/dist/json-rpc--HXQti1E.js.map +1 -0
- package/dist/links-j8rED1sR.js +603 -0
- package/dist/links-j8rED1sR.js.map +1 -0
- package/dist/local-terminal-D8QThwo_.js +252 -0
- package/dist/local-terminal-D8QThwo_.js.map +1 -0
- package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
- package/dist/logger-Gtqlayx8.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -1
- package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
- package/dist/mcp-_wecH9Qe.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/media-sniff-UKijEaiQ.js.map +1 -1
- package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
- package/dist/messages-BqiwcD2Q.js.map +1 -0
- package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
- package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
- package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
- package/dist/openai-compat-CFrpH7uq.js.map +1 -0
- package/dist/orchestration-BCGmB8Ps.js +511 -0
- package/dist/orchestration-BCGmB8Ps.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +13 -7
- package/dist/output/terminal.js.map +1 -1
- package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
- package/dist/plugins-DS7mahzZ.js.map +1 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -2
- package/dist/prelude-Cxo4t74B.js +517 -0
- package/dist/prelude-Cxo4t74B.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -2
- package/dist/project-root-B8zADr4o.js +167 -0
- package/dist/project-root-B8zADr4o.js.map +1 -0
- package/dist/project-user-paths-BSgtrRxl.js +58 -0
- package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
- package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
- package/dist/prompts-DYcvqShf.d.ts.map +1 -0
- package/dist/protocol-DnhqqSmE.js +13 -0
- package/dist/protocol-DnhqqSmE.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +1 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +3 -2
- package/dist/providers/cerebras.js.map +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -2
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +15 -12
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +3 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +3 -3
- package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
- package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/registry-D6ShBWJU.js +5234 -0
- package/dist/registry-D6ShBWJU.js.map +1 -0
- package/dist/remote-hosts-BtA_9ZGp.js +97 -0
- package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
- package/dist/restate.d.ts +2 -3
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +631 -572
- package/dist/restate.js.map +1 -1
- package/dist/run-summary-CklLiZ4y.js +288 -0
- package/dist/run-summary-CklLiZ4y.js.map +1 -0
- package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
- package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
- package/dist/session/sqlite.d.ts +1 -2
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -616
- package/dist/session-factory-BUGY5P40.js +1576 -0
- package/dist/session-factory-BUGY5P40.js.map +1 -0
- package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
- package/dist/session-xh9JXu6i.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/settings-keys-Ckf29NeM.js +677 -0
- package/dist/settings-keys-Ckf29NeM.js.map +1 -0
- package/dist/shell-path-Dqaoow2J.js +412 -0
- package/dist/shell-path-Dqaoow2J.js.map +1 -0
- package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
- package/dist/skills-DKqZJm4X.js.map +1 -0
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -2
- package/dist/source-mPExaUm8.js +381 -0
- package/dist/source-mPExaUm8.js.map +1 -0
- package/dist/sqlite-DV1DQzN2.js +698 -0
- package/dist/sqlite-DV1DQzN2.js.map +1 -0
- package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
- package/dist/src-BRMYRJib.js.map +1 -0
- package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
- package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
- package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
- package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
- package/dist/timeout-DEaqslLO.d.ts.map +1 -1
- package/dist/todo-edits-Dn251YUP.js +1790 -0
- package/dist/todo-edits-Dn251YUP.js.map +1 -0
- package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
- package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
- package/dist/tool-state-Dku6YqAB.js +989 -0
- package/dist/tool-state-Dku6YqAB.js.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -2
- package/dist/tools/prompts.d.ts +1 -1
- package/dist/tools/prompts.js +7 -7
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -2
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools-_DnT3AGM.js +578 -0
- package/dist/tools-_DnT3AGM.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -2
- package/dist/tui.d.ts +534 -517
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +34933 -27243
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-B8sokwi5.js +333 -0
- package/dist/turn-operations-B8sokwi5.js.map +1 -0
- package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
- package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
- package/dist/types-C7ZFOztE.d.ts +132 -0
- package/dist/types-C7ZFOztE.d.ts.map +1 -0
- package/dist/types-CH5XpIHw.js +114 -0
- package/dist/types-CH5XpIHw.js.map +1 -0
- package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
- package/dist/types-Cu0ZGDu9.js.map +1 -0
- package/dist/types-IqegbJEE.d.ts +624 -0
- package/dist/types-IqegbJEE.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/dist/url-D8HCl65b.js +1259 -0
- package/dist/url-D8HCl65b.js.map +1 -0
- package/dist/wasm-BVT-j5cl.js +33 -0
- package/dist/wasm-BVT-j5cl.js.map +1 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
- package/dist/ws-CIHMvlSh.js +952 -0
- package/dist/ws-CIHMvlSh.js.map +1 -0
- package/dist/ws-client-N7NxzvQp.js +230 -0
- package/dist/ws-client-N7NxzvQp.js.map +1 -0
- package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
- package/dist/xai-CvJTeLrI.js.map +1 -0
- package/dist/xdg-zweed0-t.js +117 -0
- package/dist/xdg-zweed0-t.js.map +1 -0
- package/docs/ACP.md +52 -0
- package/docs/ARCHITECTURE.md +103 -6
- package/docs/ARTIFACTS.md +8 -4
- package/docs/CHAT.md +165 -14
- package/docs/EXECUTION_CONTEXT.md +8 -0
- package/docs/EXTENSIONS.md +123 -2
- package/docs/HEADLESS.md +2 -0
- package/docs/IPC.md +775 -64
- package/docs/MACHINES.md +456 -0
- package/docs/PANELS.md +3 -0
- package/docs/README.md +4 -0
- package/docs/RESTATE.md +3 -2
- package/docs/RUN_IN_BACKGROUND.md +10 -2
- package/docs/SERVER.md +1085 -0
- package/docs/SKILL.md +6 -1
- package/docs/TERMINAL.md +75 -0
- package/docs/TUI.md +65 -14
- package/package.json +58 -26
- package/scripts/eval.ts +4 -0
- package/dist/acp-D-tVyUjw.js.map +0 -1
- package/dist/agent-D0zZnGbt.js.map +0 -1
- package/dist/anthropic-C2aole0G.js.map +0 -1
- package/dist/approvals-DKaJZYlm.js.map +0 -1
- package/dist/atomic-write-B7zr1ddF.js.map +0 -1
- package/dist/auth-BG2rVKSt.js.map +0 -1
- package/dist/completion-core-BRo86R_L.js.map +0 -1
- package/dist/contexts/daytona.js.map +0 -1
- package/dist/contexts/docker.js.map +0 -1
- package/dist/contexts/e2b.js.map +0 -1
- package/dist/contexts-CRE9pw07.js.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/format-BF745pLL.js.map +0 -1
- package/dist/glob-DluQFSw1.js +0 -270
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/headless.js.map +0 -1
- package/dist/history-search-C4bk1Wz_.js.map +0 -1
- package/dist/index-BgN1T3lk.d.ts.map +0 -1
- package/dist/index-C37_ppWv.d.ts.map +0 -1
- package/dist/index-C6NEd5y_.d.ts.map +0 -1
- package/dist/ipc-BKVIIOqH.js +0 -331
- package/dist/ipc-BKVIIOqH.js.map +0 -1
- package/dist/ipc.d.ts.map +0 -1
- package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
- package/dist/json-rpc-rSWMUh9b.js +0 -192
- package/dist/json-rpc-rSWMUh9b.js.map +0 -1
- package/dist/logger--C1VQjwJ.js.map +0 -1
- package/dist/mcp-CgNd-f3F.js.map +0 -1
- package/dist/messages-2jcOA_5R.js.map +0 -1
- package/dist/openai-compat-Dy56weK8.js.map +0 -1
- package/dist/output-cap-C9zukcCj.js +0 -100
- package/dist/output-cap-C9zukcCj.js.map +0 -1
- package/dist/panels-B6gRk4bf.js +0 -789
- package/dist/panels-B6gRk4bf.js.map +0 -1
- package/dist/plugins-gbI7SBA6.js.map +0 -1
- package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
- package/dist/run-summary-BU_NgwUT.js +0 -269
- package/dist/run-summary-BU_NgwUT.js.map +0 -1
- package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
- package/dist/session/sqlite.js.map +0 -1
- package/dist/session-BqRP1hS7.js.map +0 -1
- package/dist/shell-path-DfdXrGg9.js +0 -1549
- package/dist/shell-path-DfdXrGg9.js.map +0 -1
- package/dist/skills-BC3dodWp.js.map +0 -1
- package/dist/src-CkNQKFKr.js.map +0 -1
- package/dist/todo-edits-UOwgxGKu.js +0 -528
- package/dist/todo-edits-UOwgxGKu.js.map +0 -1
- package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
- package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
- package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
- package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
- package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
- package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
- package/dist/turn-operations-DrbJlpq9.js.map +0 -1
- package/dist/types-CWgENVLY.js.map +0 -1
- package/dist/xai-CazjStOK.js.map +0 -1
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
import { a as loginShell, c as DEFAULT_MAX_OUTPUT_BUFFER, f as outputExceededMessage, i as localTerminalProcess, l as appendCapped, o as ptyChildProcess, r as hasLocalPty, s as spawnLocalPty, t as TERMINAL_ENV } from "./local-terminal-D8QThwo_.js";
|
|
1
2
|
import { t as lazyAsync } from "./lazy-DLOurOC_.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
-
import { accessSync, constants, createWriteStream, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { accessSync, constants, createWriteStream, openSync, realpathSync, statSync } from "node:fs";
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import { homedir } from "node:os";
|
|
7
6
|
import { mkdir, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
//#region src/contexts/types.ts
|
|
9
|
+
/**
|
|
10
|
+
* Effective {@link DetachedTasksCapability} of a context, with the
|
|
11
|
+
* backward-compatible inference for contexts that predate the field.
|
|
12
|
+
*/
|
|
13
|
+
function resolveDetachedTasksCapability(context) {
|
|
14
|
+
return context.capabilities.detachedTasks ?? (context.execBackground ? "process-lifetime" : "none");
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
8
17
|
//#region src/contexts/isolation.ts
|
|
9
18
|
/**
|
|
10
19
|
* Only `/usr/bin/sandbox-exec` is trusted — never a `PATH` lookup — so a
|
|
@@ -78,7 +87,7 @@ function isolationAvailability(platform = process.platform, env = process.env) {
|
|
|
78
87
|
executable: bwrap
|
|
79
88
|
} : {
|
|
80
89
|
ok: false,
|
|
81
|
-
reason: "bwrap (bubblewrap) is not installed
|
|
90
|
+
reason: "bwrap (bubblewrap) is not installed. Install it with your package manager (e.g. `apt install bubblewrap`)"
|
|
82
91
|
};
|
|
83
92
|
}
|
|
84
93
|
/** Expand a leading `~` / `~/` to `home`. Other paths returned unchanged. */
|
|
@@ -488,7 +497,7 @@ function unavailableResult(reason) {
|
|
|
488
497
|
};
|
|
489
498
|
}
|
|
490
499
|
//#endregion
|
|
491
|
-
//#region src/contexts/process.ts
|
|
500
|
+
//#region src/contexts/process-state.ts
|
|
492
501
|
/**
|
|
493
502
|
* Lazily load `node:child_process`'s `spawn` (memoized after first use).
|
|
494
503
|
*
|
|
@@ -512,18 +521,15 @@ const loadSpawn = lazyAsync(async () => (await import("node:child_process")).spa
|
|
|
512
521
|
*/
|
|
513
522
|
const SUPPORTS_PROCESS_GROUPS = process.platform !== "win32";
|
|
514
523
|
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* marker is appended to stderr.
|
|
519
|
-
*/
|
|
520
|
-
/**
|
|
521
|
-
* How long `destroy()` waits for a SIGTERM'd background task to settle
|
|
522
|
-
* before escalating to SIGKILL and abandoning the wait. Matches the MCP
|
|
523
|
-
* connection's default `closeTimeout` so `agent.destroy()` stays bounded
|
|
524
|
-
* by a single grace period regardless of which leg is slowest.
|
|
524
|
+
* Background tasks on a PTY see a terminal where they used to see pipes:
|
|
525
|
+
* keep pagers and git's credential prompt from waiting on input nobody
|
|
526
|
+
* types. The command's own env still wins.
|
|
525
527
|
*/
|
|
526
|
-
const
|
|
528
|
+
const TERMINAL_TASK_ENV = {
|
|
529
|
+
PAGER: "cat",
|
|
530
|
+
GIT_PAGER: "cat",
|
|
531
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
532
|
+
};
|
|
527
533
|
/**
|
|
528
534
|
* How long an `exec` kill (timeout / abort / output-overflow) waits after
|
|
529
535
|
* SIGTERM before escalating the process group to SIGKILL. A child that
|
|
@@ -559,7 +565,7 @@ const EXIT_PIPE_DRAIN_GRACE_MS = 1500;
|
|
|
559
565
|
*/
|
|
560
566
|
const TASK_ID_RE = /^[a-z][\w-]*$/i;
|
|
561
567
|
function assertSafeTaskId(taskId) {
|
|
562
|
-
if (!TASK_ID_RE.test(taskId)) throw new Error(`Invalid task id "${taskId}"
|
|
568
|
+
if (!TASK_ID_RE.test(taskId)) throw new Error(`Invalid task id "${taskId}": it must match ${TASK_ID_RE}.`);
|
|
563
569
|
}
|
|
564
570
|
/**
|
|
565
571
|
* Format `date` as `YYYYMMDD-HHMMSS-mmm` in UTC.
|
|
@@ -580,12 +586,11 @@ function pickEnv(env, keys) {
|
|
|
580
586
|
for (const key of keys) if (env[key] !== void 0) out[key] = env[key];
|
|
581
587
|
return out;
|
|
582
588
|
}
|
|
583
|
-
function
|
|
584
|
-
let counter = 0;
|
|
589
|
+
function createProcessState(config) {
|
|
585
590
|
const handles = /* @__PURE__ */ new Map();
|
|
586
591
|
const defaultCwd = config?.cwd ?? process.cwd();
|
|
587
592
|
const defaultEnv = config?.env;
|
|
588
|
-
const destroyGraceMs = config?.destroyGraceMs ??
|
|
593
|
+
const destroyGraceMs = config?.destroyGraceMs ?? 5e3;
|
|
589
594
|
const baseEnv = config?.inheritEnv === false ? pickEnv(process.env, [
|
|
590
595
|
"PATH",
|
|
591
596
|
"HOME",
|
|
@@ -600,68 +605,6 @@ function createProcessContext(config) {
|
|
|
600
605
|
const isolation = config?.isolation;
|
|
601
606
|
const isolationState = isolation ? isolationAvailability() : void 0;
|
|
602
607
|
/**
|
|
603
|
-
* `{ file, args }` for `/bin/sh -c command`, wrapped in the sandbox when
|
|
604
|
-
* isolation is on. Callers check `isolationState.ok` first (the three
|
|
605
|
-
* shell entry points return `unavailableResult` / throw before spawning).
|
|
606
|
-
*/
|
|
607
|
-
function shellCommand(command) {
|
|
608
|
-
if (isolation && isolationState?.ok) return wrapCommand(isolationState.backend, isolationState.executable, isolation, command);
|
|
609
|
-
return {
|
|
610
|
-
file: "/bin/sh",
|
|
611
|
-
args: ["-c", command]
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
/** Failure result when isolation was requested but cannot be enforced; `undefined` = proceed. */
|
|
615
|
-
function isolationBlocker() {
|
|
616
|
-
return isolationState && !isolationState.ok ? unavailableResult(isolationState.reason) : void 0;
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Resolve `path` against the handle cwd and, when a `workspaceRoot` is
|
|
620
|
-
* configured, reject anything that escapes it. Uses `realpath` on the
|
|
621
|
-
* nearest existing ancestor so symlink escapes (and `..`) are caught for
|
|
622
|
-
* both existing and not-yet-created targets.
|
|
623
|
-
*/
|
|
624
|
-
async function resolveContained(handle, path) {
|
|
625
|
-
const full = resolve(handle.cwd, path);
|
|
626
|
-
if (workspaceRoot === void 0) return full;
|
|
627
|
-
const canonical = await canonicalizeForCheck(full);
|
|
628
|
-
const canonicalRoot = await realpath(workspaceRoot).catch(() => workspaceRoot);
|
|
629
|
-
if (!isWithinRoot(canonicalRoot, canonical)) throw new Error(`Path escapes the workspace root: ${path} (resolved outside ${canonicalRoot})`);
|
|
630
|
-
return full;
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Canonicalize the deepest existing ancestor of `full` to defeat symlink
|
|
634
|
-
* escapes; the non-existent tail (for writes) is appended back verbatim.
|
|
635
|
-
*/
|
|
636
|
-
async function canonicalizeForCheck(full) {
|
|
637
|
-
let existing = full;
|
|
638
|
-
const tail = [];
|
|
639
|
-
while (true) try {
|
|
640
|
-
existing = await realpath(existing);
|
|
641
|
-
break;
|
|
642
|
-
} catch {
|
|
643
|
-
const parent = dirname(existing);
|
|
644
|
-
if (parent === existing) {
|
|
645
|
-
existing = parent;
|
|
646
|
-
break;
|
|
647
|
-
}
|
|
648
|
-
tail.unshift(relative(parent, existing));
|
|
649
|
-
existing = parent;
|
|
650
|
-
}
|
|
651
|
-
return tail.length ? resolve(existing, ...tail) : existing;
|
|
652
|
-
}
|
|
653
|
-
/**
|
|
654
|
-
* In-process counterpart of the sandbox's write policy: `writeFile` (and
|
|
655
|
-
* so the `write_file` / `edit` tools) may only target `isolation.writableRoots`.
|
|
656
|
-
* Reads are deliberately unrestricted — same boundary the kernel enforces
|
|
657
|
-
* for shell commands, so the two never disagree about where writes land.
|
|
658
|
-
*/
|
|
659
|
-
async function assertWritable(full, path) {
|
|
660
|
-
if (!isolation) return;
|
|
661
|
-
const canonical = await canonicalizeForCheck(full);
|
|
662
|
-
if (!isolation.writableRoots.some((root) => isWithinRoot(root, canonical))) throw new Error(`[sandbox] write denied: ${path} is outside the writable roots (${isolation.writableRoots.join(", ")}). Ask the user to add it under Settings → "Sandbox writable paths" or turn off "Sandbox shell commands".`);
|
|
663
|
-
}
|
|
664
|
-
/**
|
|
665
608
|
* Per-context background-task registry. Entries live for the context's
|
|
666
609
|
* lifetime — even after the child exits — so the model can read output
|
|
667
610
|
* of completed tasks until `destroy()` tears everything down. Kept as
|
|
@@ -669,7 +612,6 @@ function createProcessContext(config) {
|
|
|
669
612
|
* abstraction. The state and the operations on it live inline.
|
|
670
613
|
*/
|
|
671
614
|
const tasks = /* @__PURE__ */ new Map();
|
|
672
|
-
let taskCounter = 0;
|
|
673
615
|
/**
|
|
674
616
|
* Per-context UTC timestamp segment baked into every background task's
|
|
675
617
|
* log filename. Same `taskCounter` value across two contexts (e.g. a
|
|
@@ -711,7 +653,25 @@ function createProcessContext(config) {
|
|
|
711
653
|
*/
|
|
712
654
|
/** Live `execStream` children → owning handle id. See `exitHandler`. */
|
|
713
655
|
const streamChildren = /* @__PURE__ */ new Map();
|
|
714
|
-
|
|
656
|
+
const s = {
|
|
657
|
+
config,
|
|
658
|
+
counter: 0,
|
|
659
|
+
handles,
|
|
660
|
+
defaultCwd,
|
|
661
|
+
defaultEnv,
|
|
662
|
+
destroyGraceMs,
|
|
663
|
+
baseEnv,
|
|
664
|
+
workspaceRoot,
|
|
665
|
+
isolation,
|
|
666
|
+
isolationState,
|
|
667
|
+
tasks,
|
|
668
|
+
taskCounter: 0,
|
|
669
|
+
contextTimestamp,
|
|
670
|
+
streamChildren,
|
|
671
|
+
exitHandlerRegistered: false,
|
|
672
|
+
exitHandler: () => {},
|
|
673
|
+
registerExitHandler: () => {}
|
|
674
|
+
};
|
|
715
675
|
const exitHandler = () => {
|
|
716
676
|
for (const task of tasks.values()) {
|
|
717
677
|
if (task.status !== "running") continue;
|
|
@@ -725,156 +685,679 @@ function createProcessContext(config) {
|
|
|
725
685
|
for (const child of streamChildren.keys()) killProcessGroup(child, "SIGTERM");
|
|
726
686
|
};
|
|
727
687
|
const registerExitHandler = () => {
|
|
728
|
-
if (!exitHandlerRegistered) {
|
|
688
|
+
if (!s.exitHandlerRegistered) {
|
|
729
689
|
process.on("exit", exitHandler);
|
|
730
|
-
exitHandlerRegistered = true;
|
|
690
|
+
s.exitHandlerRegistered = true;
|
|
731
691
|
}
|
|
732
692
|
};
|
|
693
|
+
s.exitHandler = exitHandler;
|
|
694
|
+
s.registerExitHandler = registerExitHandler;
|
|
695
|
+
return s;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Send `signal` to the child's whole process group. Falls back to a
|
|
699
|
+
* single-process kill on Windows (no POSIX process groups). Shared
|
|
700
|
+
* across the foreground `exec` path, the background spawn / kill
|
|
701
|
+
* paths, and the shutdown-time orphan reaper — keeping one definition
|
|
702
|
+
* so the kill semantics can't drift between them.
|
|
703
|
+
*/
|
|
704
|
+
function killProcessGroup(child, signal) {
|
|
705
|
+
const pid = child?.pid;
|
|
706
|
+
if (pid === void 0) return;
|
|
707
|
+
try {
|
|
708
|
+
if (SUPPORTS_PROCESS_GROUPS) process.kill(-pid, signal);
|
|
709
|
+
else process.kill(pid, signal);
|
|
710
|
+
} catch {}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Project a `TaskState` to the `TaskEntry` shape `listBackground` and
|
|
714
|
+
* `reassignBackgroundTasks` return. Single helper keeps the shape
|
|
715
|
+
* consistent across both call sites (and a future addition of fields
|
|
716
|
+
* to `TaskEntry` only needs to land here).
|
|
717
|
+
*/
|
|
718
|
+
function stateToTaskEntry(state) {
|
|
733
719
|
return {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
720
|
+
taskId: state.taskId,
|
|
721
|
+
pid: state.pid,
|
|
722
|
+
command: state.command,
|
|
723
|
+
cwd: state.cwd,
|
|
724
|
+
startedAt: state.startedAt,
|
|
725
|
+
...state.endedAt !== void 0 ? { endedAt: state.endedAt } : {},
|
|
726
|
+
outputPath: state.outputPath,
|
|
727
|
+
status: state.status,
|
|
728
|
+
...state.exitCode !== void 0 ? { exitCode: state.exitCode } : {},
|
|
729
|
+
...state.signal ? { signal: state.signal } : {},
|
|
730
|
+
bytesWritten: state.bytesWritten
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Project a settled `TaskState` to the `TaskExitInfo` shape `settle()`
|
|
735
|
+
* fires from `onExit` and `killBackground` returns on the
|
|
736
|
+
* cached-exit path. Pre-condition: `state.status !== 'running'`
|
|
737
|
+
* (callers gate on this).
|
|
738
|
+
*/
|
|
739
|
+
function stateToTaskExitInfo(state) {
|
|
740
|
+
return {
|
|
741
|
+
taskId: state.taskId,
|
|
742
|
+
status: state.status,
|
|
743
|
+
exitCode: state.exitCode ?? 0,
|
|
744
|
+
...state.signal ? { signal: state.signal } : {},
|
|
745
|
+
outputPath: state.outputPath,
|
|
746
|
+
durationMs: (state.endedAt ?? Date.now()) - state.startedAt,
|
|
747
|
+
command: state.command
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
//#endregion
|
|
751
|
+
//#region src/contexts/process-background.ts
|
|
752
|
+
const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
753
|
+
/**
|
|
754
|
+
* The log's fd: a new file (exclusive), never through a symlink planted at
|
|
755
|
+
* its path. A name collision keeps the old append behavior on the existing
|
|
756
|
+
* regular file (still not following a link), as the comment at the call says.
|
|
757
|
+
*/
|
|
758
|
+
function openTaskLog(path) {
|
|
759
|
+
const write = constants.O_WRONLY | constants.O_APPEND | NO_FOLLOW;
|
|
760
|
+
try {
|
|
761
|
+
return openSync(path, write | constants.O_CREAT | constants.O_EXCL, 384);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
if (err.code !== "EEXIST") throw err;
|
|
764
|
+
return openSync(path, write);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function processBackgroundMethods(s, sh) {
|
|
768
|
+
const { baseEnv, defaultEnv, tasks, contextTimestamp, registerExitHandler } = s;
|
|
769
|
+
const { shellCommand, isolationBlocker, spawnTerminalTask } = sh;
|
|
770
|
+
return { async execBackground(handle, command, options) {
|
|
771
|
+
const cwd = options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
|
|
772
|
+
const spawnChild = await loadSpawn();
|
|
773
|
+
const blocked = isolationBlocker();
|
|
774
|
+
if (blocked) throw new Error(blocked.stderr);
|
|
775
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
776
|
+
const taskId = `bash_${++s.taskCounter}`;
|
|
777
|
+
assertSafeTaskId(taskId);
|
|
778
|
+
const outputPath = resolve(options.outputDir, `${taskId}.${contextTimestamp}.log`);
|
|
779
|
+
registerExitHandler();
|
|
780
|
+
const outputStream = createWriteStream(outputPath, { fd: openTaskLog(outputPath) });
|
|
781
|
+
outputStream.on("error", (err) => {
|
|
782
|
+
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} log stream error: ${err.message}\n`);
|
|
783
|
+
});
|
|
784
|
+
const shell = shellCommand(command);
|
|
785
|
+
const tty = options.terminal && hasLocalPty() ? options.terminal : void 0;
|
|
786
|
+
const child = tty ? spawnTerminalTask([shell.file, ...shell.args], cwd, {
|
|
787
|
+
...TERMINAL_TASK_ENV,
|
|
788
|
+
...options.env
|
|
789
|
+
}, tty) : spawnChild(shell.file, shell.args, {
|
|
790
|
+
cwd,
|
|
791
|
+
env: {
|
|
792
|
+
...baseEnv,
|
|
793
|
+
...defaultEnv,
|
|
794
|
+
...options.env
|
|
795
|
+
},
|
|
796
|
+
stdio: [
|
|
797
|
+
"ignore",
|
|
798
|
+
"pipe",
|
|
799
|
+
"pipe"
|
|
800
|
+
],
|
|
801
|
+
detached: SUPPORTS_PROCESS_GROUPS
|
|
802
|
+
});
|
|
803
|
+
const state = {
|
|
804
|
+
taskId,
|
|
805
|
+
handleId: handle.id,
|
|
806
|
+
pid: child.pid ?? -1,
|
|
807
|
+
command,
|
|
808
|
+
cwd,
|
|
809
|
+
startedAt: Date.now(),
|
|
810
|
+
outputPath,
|
|
811
|
+
outputStream,
|
|
812
|
+
child,
|
|
813
|
+
status: "running",
|
|
814
|
+
bytesWritten: 0,
|
|
815
|
+
settled: false,
|
|
816
|
+
onExit: options.onExit,
|
|
817
|
+
onStall: options.onStall,
|
|
818
|
+
exitWatchers: []
|
|
819
|
+
};
|
|
820
|
+
tasks.set(taskId, state);
|
|
821
|
+
const maxOutputBytes = typeof options.maxOutputBytes === "number" && options.maxOutputBytes > 0 ? options.maxOutputBytes : void 0;
|
|
822
|
+
let droppedBytes = 0;
|
|
823
|
+
let capExhausted = false;
|
|
824
|
+
const stallTimeoutMs = typeof options.stallTimeoutMs === "number" && options.stallTimeoutMs > 0 ? options.stallTimeoutMs : void 0;
|
|
825
|
+
let stallTimer;
|
|
826
|
+
let lastOutputAt = Date.now();
|
|
827
|
+
const armStallTimer = () => {
|
|
828
|
+
if (!stallTimeoutMs || !state.onStall) return;
|
|
829
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
830
|
+
stallTimer = setTimeout(() => {
|
|
831
|
+
stallTimer = void 0;
|
|
832
|
+
if (state.settled) return;
|
|
833
|
+
try {
|
|
834
|
+
state.onStall?.({
|
|
835
|
+
taskId,
|
|
836
|
+
command,
|
|
837
|
+
outputPath,
|
|
838
|
+
stalledForMs: Date.now() - lastOutputAt,
|
|
839
|
+
bytesWritten: state.bytesWritten
|
|
840
|
+
});
|
|
841
|
+
} catch (err) {
|
|
842
|
+
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} onStall threw: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
843
|
+
}
|
|
844
|
+
}, stallTimeoutMs);
|
|
845
|
+
stallTimer.unref?.();
|
|
846
|
+
};
|
|
847
|
+
const clearStallTimer = () => {
|
|
848
|
+
if (stallTimer) {
|
|
849
|
+
clearTimeout(stallTimer);
|
|
850
|
+
stallTimer = void 0;
|
|
753
851
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
852
|
+
};
|
|
853
|
+
armStallTimer();
|
|
854
|
+
const appendChunk = (chunk) => {
|
|
855
|
+
state.bytesWritten += chunk.length;
|
|
856
|
+
lastOutputAt = Date.now();
|
|
857
|
+
armStallTimer();
|
|
858
|
+
if (maxOutputBytes !== void 0) {
|
|
859
|
+
const onDisk = state.bytesWritten - droppedBytes - chunk.length;
|
|
860
|
+
if (capExhausted || onDisk >= maxOutputBytes) {
|
|
861
|
+
capExhausted = true;
|
|
862
|
+
droppedBytes += chunk.length;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const room = maxOutputBytes - onDisk;
|
|
866
|
+
if (chunk.length > room) {
|
|
867
|
+
capExhausted = true;
|
|
868
|
+
let end = room;
|
|
869
|
+
while (end > 0 && (chunk[end] & 192) === 128) end--;
|
|
870
|
+
droppedBytes += chunk.length - end;
|
|
871
|
+
if (end > 0) outputStream.write(chunk.subarray(0, end));
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
outputStream.write(chunk);
|
|
876
|
+
};
|
|
877
|
+
child.stdout?.on("data", (chunk) => appendChunk(chunk));
|
|
878
|
+
child.stderr?.on("data", (chunk) => appendChunk(chunk));
|
|
879
|
+
const settle = (cause, code, signal, errMessage) => {
|
|
880
|
+
if (state.settled) return;
|
|
881
|
+
state.settled = true;
|
|
882
|
+
clearStallTimer();
|
|
883
|
+
state.endedAt = Date.now();
|
|
884
|
+
const status = signal === "SIGTERM" || state.killRequested ? "killed" : "exited";
|
|
885
|
+
const exitCode = code !== null ? code : signal === "SIGTERM" ? 143 : signal ? 128 : 1;
|
|
886
|
+
state.status = status;
|
|
887
|
+
state.exitCode = exitCode;
|
|
888
|
+
if (signal) state.signal = signal;
|
|
889
|
+
if (errMessage) try {
|
|
890
|
+
outputStream.write(`\n${errMessage}\n`);
|
|
891
|
+
} catch {}
|
|
892
|
+
if (droppedBytes > 0) try {
|
|
893
|
+
outputStream.write(`\n<output-truncated bytes-dropped="${droppedBytes}"/>\n`);
|
|
894
|
+
} catch {}
|
|
895
|
+
outputStream.end(() => {
|
|
896
|
+
const info = stateToTaskExitInfo(state);
|
|
897
|
+
for (const callback of [state.onExit, ...state.exitWatchers.splice(0)]) try {
|
|
898
|
+
callback?.(info);
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} onExit threw: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
state.child = void 0;
|
|
904
|
+
state.outputStream = void 0;
|
|
905
|
+
};
|
|
906
|
+
child.on("close", (code, signal) => settle("close", code, signal));
|
|
907
|
+
child.on("error", (err) => settle("error", null, null, `[spawn error] ${err.message}`));
|
|
908
|
+
child.on("exit", (code, signal) => {
|
|
909
|
+
setTimeout(() => {
|
|
910
|
+
if (state.settled) return;
|
|
911
|
+
child.stdout?.destroy();
|
|
912
|
+
child.stderr?.destroy();
|
|
913
|
+
settle("close", code, signal);
|
|
914
|
+
}, EXIT_PIPE_DRAIN_GRACE_MS).unref?.();
|
|
915
|
+
});
|
|
916
|
+
return {
|
|
917
|
+
taskId,
|
|
918
|
+
pid: state.pid,
|
|
919
|
+
outputPath,
|
|
920
|
+
...tty ? { terminal: true } : {}
|
|
921
|
+
};
|
|
922
|
+
} };
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/contexts/process-exec.ts
|
|
926
|
+
function processExecMethods(s, sh) {
|
|
927
|
+
const { config, baseEnv, defaultEnv, isolation } = s;
|
|
928
|
+
const { shellCommand, isolationBlocker } = sh;
|
|
929
|
+
return { async exec(handle, command, options) {
|
|
930
|
+
const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
|
|
931
|
+
if (options?.signal?.aborted) return {
|
|
932
|
+
stdout: "",
|
|
933
|
+
stderr: "aborted by signal before spawn",
|
|
934
|
+
exitCode: 143
|
|
935
|
+
};
|
|
936
|
+
const blocked = isolationBlocker();
|
|
937
|
+
if (blocked) return blocked;
|
|
938
|
+
const timeoutMs = (options?.timeout ?? config?.limits?.timeout ?? 30) * 1e3;
|
|
939
|
+
const maxBuffer = DEFAULT_MAX_OUTPUT_BUFFER;
|
|
940
|
+
const spawnChild = await loadSpawn();
|
|
941
|
+
return new Promise((resolveP) => {
|
|
942
|
+
const shell = shellCommand(command);
|
|
943
|
+
const child = spawnChild(shell.file, shell.args, {
|
|
944
|
+
cwd,
|
|
945
|
+
env: {
|
|
946
|
+
...baseEnv,
|
|
947
|
+
...defaultEnv,
|
|
948
|
+
...options?.env
|
|
949
|
+
},
|
|
950
|
+
stdio: [
|
|
951
|
+
"ignore",
|
|
952
|
+
"pipe",
|
|
953
|
+
"pipe"
|
|
954
|
+
],
|
|
955
|
+
detached: SUPPORTS_PROCESS_GROUPS
|
|
956
|
+
});
|
|
957
|
+
const stdoutSlot = {
|
|
958
|
+
text: "",
|
|
959
|
+
bytes: 0
|
|
759
960
|
};
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
async exec(handle, command, options) {
|
|
764
|
-
const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
|
|
765
|
-
if (options?.signal?.aborted) return {
|
|
766
|
-
stdout: "",
|
|
767
|
-
stderr: "aborted by signal before spawn",
|
|
768
|
-
exitCode: 143
|
|
961
|
+
const stderrSlot = {
|
|
962
|
+
text: "",
|
|
963
|
+
bytes: 0
|
|
769
964
|
};
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
},
|
|
784
|
-
stdio: [
|
|
785
|
-
"ignore",
|
|
786
|
-
"pipe",
|
|
787
|
-
"pipe"
|
|
788
|
-
],
|
|
789
|
-
detached: SUPPORTS_PROCESS_GROUPS
|
|
790
|
-
});
|
|
791
|
-
const stdoutSlot = {
|
|
792
|
-
text: "",
|
|
793
|
-
bytes: 0
|
|
794
|
-
};
|
|
795
|
-
const stderrSlot = {
|
|
796
|
-
text: "",
|
|
797
|
-
bytes: 0
|
|
798
|
-
};
|
|
799
|
-
let bufferTruncated = false;
|
|
800
|
-
let timedOut = false;
|
|
801
|
-
let killedByAbort = false;
|
|
802
|
-
let settled = false;
|
|
803
|
-
const killWithEscalation = () => {
|
|
804
|
-
killProcessGroup(child, "SIGTERM");
|
|
805
|
-
setTimeout(() => {
|
|
806
|
-
if (!settled) killProcessGroup(child, "SIGKILL");
|
|
807
|
-
}, EXEC_KILL_ESCALATION_GRACE_MS).unref?.();
|
|
808
|
-
};
|
|
809
|
-
const onChunk = (slot, chunk) => {
|
|
810
|
-
if (appendCapped(slot, chunk, maxBuffer) !== "ok" && !bufferTruncated) {
|
|
811
|
-
bufferTruncated = true;
|
|
812
|
-
killWithEscalation();
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
child.stdout?.on("data", (chunk) => onChunk(stdoutSlot, chunk));
|
|
816
|
-
child.stderr?.on("data", (chunk) => onChunk(stderrSlot, chunk));
|
|
817
|
-
const timeoutTimer = timeoutMs > 0 ? setTimeout(() => {
|
|
818
|
-
timedOut = true;
|
|
819
|
-
killWithEscalation();
|
|
820
|
-
}, timeoutMs) : void 0;
|
|
821
|
-
const onAbort = () => {
|
|
822
|
-
killedByAbort = true;
|
|
965
|
+
let bufferTruncated = false;
|
|
966
|
+
let timedOut = false;
|
|
967
|
+
let killedByAbort = false;
|
|
968
|
+
let settled = false;
|
|
969
|
+
const killWithEscalation = () => {
|
|
970
|
+
killProcessGroup(child, "SIGTERM");
|
|
971
|
+
setTimeout(() => {
|
|
972
|
+
if (!settled) killProcessGroup(child, "SIGKILL");
|
|
973
|
+
}, EXEC_KILL_ESCALATION_GRACE_MS).unref?.();
|
|
974
|
+
};
|
|
975
|
+
const onChunk = (slot, chunk) => {
|
|
976
|
+
if (appendCapped(slot, chunk, maxBuffer) !== "ok" && !bufferTruncated) {
|
|
977
|
+
bufferTruncated = true;
|
|
823
978
|
killWithEscalation();
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
child.stdout?.on("data", (chunk) => onChunk(stdoutSlot, chunk));
|
|
982
|
+
child.stderr?.on("data", (chunk) => onChunk(stderrSlot, chunk));
|
|
983
|
+
const timeoutTimer = timeoutMs > 0 ? setTimeout(() => {
|
|
984
|
+
timedOut = true;
|
|
985
|
+
killWithEscalation();
|
|
986
|
+
}, timeoutMs) : void 0;
|
|
987
|
+
const onAbort = () => {
|
|
988
|
+
killedByAbort = true;
|
|
989
|
+
killWithEscalation();
|
|
990
|
+
};
|
|
991
|
+
const userSignal = options?.signal;
|
|
992
|
+
if (userSignal) userSignal.addEventListener("abort", onAbort, { once: true });
|
|
993
|
+
let exitGraceTimer;
|
|
994
|
+
const settle = (exitCode, extraStderr) => {
|
|
995
|
+
if (settled) return;
|
|
996
|
+
settled = true;
|
|
997
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
998
|
+
if (exitGraceTimer) clearTimeout(exitGraceTimer);
|
|
999
|
+
if (userSignal) userSignal.removeEventListener("abort", onAbort);
|
|
1000
|
+
const finalStderr = extraStderr ? stderrSlot.text ? `${stderrSlot.text}\n${extraStderr}` : extraStderr : stderrSlot.text;
|
|
1001
|
+
const result = {
|
|
1002
|
+
stdout: stdoutSlot.text,
|
|
1003
|
+
stderr: finalStderr,
|
|
1004
|
+
exitCode
|
|
824
1005
|
};
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1006
|
+
resolveP(isolation ? annotateDenial(result, isolation) : result);
|
|
1007
|
+
};
|
|
1008
|
+
const settleFromStatus = (code, signal) => {
|
|
1009
|
+
if (killedByAbort) {
|
|
1010
|
+
settle(143, "aborted by signal");
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (timedOut) {
|
|
1014
|
+
settle(124, `command timed out after ${timeoutMs}ms`);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (bufferTruncated) {
|
|
1018
|
+
settle(143, outputExceededMessage(maxBuffer, "process killed"));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (signal) {
|
|
1022
|
+
settle(143, `terminated by signal ${signal}`);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
settle(typeof code === "number" ? code : 1);
|
|
1026
|
+
};
|
|
1027
|
+
child.on("error", (err) => {
|
|
1028
|
+
settle(1, err.message);
|
|
1029
|
+
});
|
|
1030
|
+
child.on("close", (code, signal) => settleFromStatus(code, signal));
|
|
1031
|
+
child.on("exit", (code, signal) => {
|
|
1032
|
+
exitGraceTimer = setTimeout(() => {
|
|
829
1033
|
if (settled) return;
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const result = {
|
|
836
|
-
stdout: stdoutSlot.text,
|
|
837
|
-
stderr: finalStderr,
|
|
838
|
-
exitCode
|
|
839
|
-
};
|
|
840
|
-
resolveP(isolation ? annotateDenial(result, isolation) : result);
|
|
841
|
-
};
|
|
842
|
-
const settleFromStatus = (code, signal) => {
|
|
843
|
-
if (killedByAbort) {
|
|
844
|
-
settle(143, "aborted by signal");
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
if (timedOut) {
|
|
848
|
-
settle(124, `command timed out after ${timeoutMs}ms`);
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
if (bufferTruncated) {
|
|
852
|
-
settle(143, outputExceededMessage(maxBuffer, "process killed"));
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
if (signal) {
|
|
856
|
-
settle(143, `terminated by signal ${signal}`);
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
settle(typeof code === "number" ? code : 1);
|
|
860
|
-
};
|
|
861
|
-
child.on("error", (err) => {
|
|
862
|
-
settle(1, err.message);
|
|
863
|
-
});
|
|
864
|
-
child.on("close", (code, signal) => settleFromStatus(code, signal));
|
|
865
|
-
child.on("exit", (code, signal) => {
|
|
866
|
-
exitGraceTimer = setTimeout(() => {
|
|
867
|
-
if (settled) return;
|
|
868
|
-
child.stdout?.destroy();
|
|
869
|
-
child.stderr?.destroy();
|
|
870
|
-
settleFromStatus(code, signal);
|
|
871
|
-
}, EXIT_PIPE_DRAIN_GRACE_MS);
|
|
872
|
-
exitGraceTimer.unref?.();
|
|
873
|
-
});
|
|
1034
|
+
child.stdout?.destroy();
|
|
1035
|
+
child.stderr?.destroy();
|
|
1036
|
+
settleFromStatus(code, signal);
|
|
1037
|
+
}, EXIT_PIPE_DRAIN_GRACE_MS);
|
|
1038
|
+
exitGraceTimer.unref?.();
|
|
874
1039
|
});
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
|
|
1040
|
+
});
|
|
1041
|
+
} };
|
|
1042
|
+
}
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region src/contexts/git-ls-files.ts
|
|
1045
|
+
/**
|
|
1046
|
+
* Pure (node-free) `git ls-files` helpers shared by the gitignore-aware paths
|
|
1047
|
+
* of the `glob` and `grep` tools.
|
|
1048
|
+
*
|
|
1049
|
+
* `git ls-files --cached --others --exclude-standard` lists tracked files plus
|
|
1050
|
+
* untracked-but-not-ignored files, applying `.gitignore`, `.git/info/exclude`,
|
|
1051
|
+
* and the user's global excludes. Git never descends ignored directories
|
|
1052
|
+
* (`node_modules`, build output, …), so this is both correct and sub-second on
|
|
1053
|
+
* large repos — unlike a raw filesystem walk.
|
|
1054
|
+
*
|
|
1055
|
+
* This module has ZERO node imports so it can be pulled into edge/workerd
|
|
1056
|
+
* bundles (e.g. via the `grep` tool, which drives git through the execution
|
|
1057
|
+
* context's `exec` seam). The node-spawn variant lives in
|
|
1058
|
+
* `./git-ls-files-local`.
|
|
1059
|
+
*/
|
|
1060
|
+
/** Argv for `git ls-files`, NUL-delimited so paths with newlines stay intact. */
|
|
1061
|
+
const GIT_LS_FILES_ARGS = [
|
|
1062
|
+
"ls-files",
|
|
1063
|
+
"--cached",
|
|
1064
|
+
"--others",
|
|
1065
|
+
"--exclude-standard",
|
|
1066
|
+
"-z"
|
|
1067
|
+
];
|
|
1068
|
+
/** The same invocation as a shell command string for the `exec` seam. */
|
|
1069
|
+
const GIT_LS_FILES_COMMAND = `git ${GIT_LS_FILES_ARGS.join(" ")}`;
|
|
1070
|
+
/** Split NUL-delimited `git ls-files -z` output into paths. */
|
|
1071
|
+
function parseGitLsFiles(stdout) {
|
|
1072
|
+
return stdout.split("\0").filter((p) => p.length > 0);
|
|
1073
|
+
}
|
|
1074
|
+
//#endregion
|
|
1075
|
+
//#region src/contexts/git-ls-files-local.ts
|
|
1076
|
+
/**
|
|
1077
|
+
* Node-spawn `git ls-files` runner used by the local process context's glob.
|
|
1078
|
+
*
|
|
1079
|
+
* Kept separate from `./git-ls-files` (which stays node-free for edge bundles)
|
|
1080
|
+
* because this leg imports `node:child_process`.
|
|
1081
|
+
*/
|
|
1082
|
+
/**
|
|
1083
|
+
* Run `git ls-files` in `cwd` and return its non-ignored paths (relative to
|
|
1084
|
+
* `cwd`). Returns `null` when the directory is not a git repo, git is missing,
|
|
1085
|
+
* or the call aborts — callers treat `null` as "fall back to a filesystem
|
|
1086
|
+
* walk". Never throws.
|
|
1087
|
+
*/
|
|
1088
|
+
async function gitListFilesLocal(cwd, signal) {
|
|
1089
|
+
return new Promise((resolveP) => {
|
|
1090
|
+
let child;
|
|
1091
|
+
try {
|
|
1092
|
+
child = spawn("git", [...GIT_LS_FILES_ARGS], {
|
|
1093
|
+
cwd,
|
|
1094
|
+
stdio: [
|
|
1095
|
+
"ignore",
|
|
1096
|
+
"pipe",
|
|
1097
|
+
"pipe"
|
|
1098
|
+
]
|
|
1099
|
+
});
|
|
1100
|
+
} catch {
|
|
1101
|
+
resolveP(null);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (signal?.aborted) {
|
|
1105
|
+
child.kill("SIGTERM");
|
|
1106
|
+
resolveP(null);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const stdoutChunks = [];
|
|
1110
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
1111
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1112
|
+
child.stdout?.setEncoding("utf8");
|
|
1113
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
1114
|
+
child.on("error", () => {
|
|
1115
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1116
|
+
resolveP(null);
|
|
1117
|
+
});
|
|
1118
|
+
child.on("close", (code) => {
|
|
1119
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1120
|
+
if (code !== 0 || signal?.aborted) {
|
|
1121
|
+
resolveP(null);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
resolveP(parseGitLsFiles(stdoutChunks.join("")));
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/contexts/glob.ts
|
|
1130
|
+
const loadNativeNodeGlob = lazyAsync(async () => {
|
|
1131
|
+
const candidate = (await import("node:fs/promises")).glob;
|
|
1132
|
+
return typeof candidate === "function" ? candidate : void 0;
|
|
1133
|
+
});
|
|
1134
|
+
function forcePortable() {
|
|
1135
|
+
return globalThis.__zidaneForcePortableGlobForTests === true;
|
|
1136
|
+
}
|
|
1137
|
+
async function loadNodeGlob() {
|
|
1138
|
+
if (forcePortable()) return void 0;
|
|
1139
|
+
return loadNativeNodeGlob();
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Bun's `node:fs/promises.glob` buffers the entire directory walk before
|
|
1143
|
+
* yielding (so an early `limit` break never fires — a broad `**` pattern over
|
|
1144
|
+
* a large `node_modules` hangs). `Bun.Glob().scan()` streams incrementally and
|
|
1145
|
+
* is the fast path under Bun. Returns undefined off Bun or when tests force the
|
|
1146
|
+
* portable walker.
|
|
1147
|
+
*/
|
|
1148
|
+
function loadBunGlob() {
|
|
1149
|
+
if (forcePortable()) return void 0;
|
|
1150
|
+
const bun = globalThis.Bun;
|
|
1151
|
+
return bun && typeof bun.Glob === "function" ? bun.Glob : void 0;
|
|
1152
|
+
}
|
|
1153
|
+
function escapeRegexChar(char) {
|
|
1154
|
+
return /[\\^$.*+?()[\]{}|]/.test(char) ? `\\${char}` : char;
|
|
1155
|
+
}
|
|
1156
|
+
function expandBraces(pattern) {
|
|
1157
|
+
const start = pattern.indexOf("{");
|
|
1158
|
+
if (start === -1) return [pattern];
|
|
1159
|
+
const end = pattern.indexOf("}", start + 1);
|
|
1160
|
+
if (end === -1) return [pattern];
|
|
1161
|
+
const body = pattern.slice(start + 1, end);
|
|
1162
|
+
if (!body || !body.includes(",")) return [pattern];
|
|
1163
|
+
return body.split(",").flatMap((part) => expandBraces(`${pattern.slice(0, start)}${part}${pattern.slice(end + 1)}`));
|
|
1164
|
+
}
|
|
1165
|
+
function globPatternToRegex(pattern) {
|
|
1166
|
+
let out = "^";
|
|
1167
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
1168
|
+
const char = pattern[i];
|
|
1169
|
+
const next = pattern[i + 1];
|
|
1170
|
+
if (char === "*") {
|
|
1171
|
+
if (next === "*") {
|
|
1172
|
+
if (pattern[i + 2] === "/") {
|
|
1173
|
+
out += "(?:[^/]+/)*";
|
|
1174
|
+
i += 2;
|
|
1175
|
+
} else {
|
|
1176
|
+
out += ".*";
|
|
1177
|
+
i += 1;
|
|
1178
|
+
}
|
|
1179
|
+
} else out += "[^/]*";
|
|
1180
|
+
} else if (char === "?") out += "[^/]";
|
|
1181
|
+
else if (char === "[") {
|
|
1182
|
+
const end = pattern.indexOf("]", i + 1);
|
|
1183
|
+
if (end === -1) out += "\\[";
|
|
1184
|
+
else {
|
|
1185
|
+
let cls = pattern.slice(i + 1, end);
|
|
1186
|
+
if (cls.startsWith("!")) cls = `^${cls.slice(1)}`;
|
|
1187
|
+
out += `[${cls}]`;
|
|
1188
|
+
i = end;
|
|
1189
|
+
}
|
|
1190
|
+
} else out += escapeRegexChar(char);
|
|
1191
|
+
}
|
|
1192
|
+
return new RegExp(`${out}$`);
|
|
1193
|
+
}
|
|
1194
|
+
function globLiteralPrefix(pattern) {
|
|
1195
|
+
const firstMeta = pattern.search(/[*?[\]{}]/);
|
|
1196
|
+
const prefix = firstMeta === -1 ? dirname(pattern) : pattern.slice(0, firstMeta);
|
|
1197
|
+
const slash = prefix.lastIndexOf("/");
|
|
1198
|
+
if (slash === -1) return "";
|
|
1199
|
+
return prefix.slice(0, slash);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* True when a pattern can match files outside `cwd` — an absolute path, a
|
|
1203
|
+
* Windows drive, or any `..` segment. Such patterns can't use the `git
|
|
1204
|
+
* ls-files` path (git only knows files under the repo) and fall back to the
|
|
1205
|
+
* filesystem walk.
|
|
1206
|
+
*/
|
|
1207
|
+
function patternEscapesCwd(pattern) {
|
|
1208
|
+
const norm = pattern.split(sep).join("/");
|
|
1209
|
+
if (norm.startsWith("/") || /^[a-z]:/i.test(norm)) return true;
|
|
1210
|
+
return norm.split("/").includes("..");
|
|
1211
|
+
}
|
|
1212
|
+
async function addMetadata(cwd, rel, includeMetadata) {
|
|
1213
|
+
try {
|
|
1214
|
+
const s = await stat(resolve(cwd, rel));
|
|
1215
|
+
if (!s.isFile()) return null;
|
|
1216
|
+
return includeMetadata ? {
|
|
1217
|
+
path: rel,
|
|
1218
|
+
size: s.size,
|
|
1219
|
+
mtimeMs: s.mtimeMs
|
|
1220
|
+
} : { path: rel };
|
|
1221
|
+
} catch {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function compilePattern(pattern) {
|
|
1226
|
+
return expandBraces(pattern.split(sep).join("/")).map(globPatternToRegex);
|
|
1227
|
+
}
|
|
1228
|
+
/** Match the (already non-ignored) `git ls-files` set against the glob pattern. */
|
|
1229
|
+
async function matchGitFiles(gitFiles, pattern, cwd, limit, includeMetadata, signal) {
|
|
1230
|
+
const regexes = compilePattern(pattern);
|
|
1231
|
+
const out = [];
|
|
1232
|
+
for (const rel of gitFiles) {
|
|
1233
|
+
if (signal?.aborted || out.length >= limit) break;
|
|
1234
|
+
if (!regexes.some((regex) => regex.test(rel))) continue;
|
|
1235
|
+
out.push(includeMetadata ? await addMetadata(cwd, rel, true) ?? { path: rel } : { path: rel });
|
|
1236
|
+
}
|
|
1237
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
1238
|
+
}
|
|
1239
|
+
/** Depth-first walk that matches against `regexes` and stops at `limit`. */
|
|
1240
|
+
async function walkMatch(root, prefix, regexes, cwd, limit, includeMetadata, signal, out) {
|
|
1241
|
+
if (out.length >= limit || signal?.aborted) return;
|
|
1242
|
+
let entries;
|
|
1243
|
+
try {
|
|
1244
|
+
entries = await readdir(resolve(root, prefix), {
|
|
1245
|
+
encoding: "utf8",
|
|
1246
|
+
withFileTypes: true
|
|
1247
|
+
});
|
|
1248
|
+
} catch {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
for (const entry of entries) {
|
|
1252
|
+
if (out.length >= limit || signal?.aborted) return;
|
|
1253
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1254
|
+
if (entry.isDirectory()) await walkMatch(root, rel, regexes, cwd, limit, includeMetadata, signal, out);
|
|
1255
|
+
else if (entry.isFile() && regexes.some((regex) => regex.test(rel))) out.push(includeMetadata ? await addMetadata(cwd, rel, true) ?? { path: rel } : { path: rel });
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async function globPortable(pattern, cwd, limit, includeMetadata, signal) {
|
|
1259
|
+
const normalized = pattern.split(sep).join("/");
|
|
1260
|
+
const startPrefix = globLiteralPrefix(normalized);
|
|
1261
|
+
const regexes = expandBraces(normalized).map(globPatternToRegex);
|
|
1262
|
+
const out = [];
|
|
1263
|
+
await walkMatch(cwd, startPrefix, regexes, cwd, limit, includeMetadata, signal, out);
|
|
1264
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
1265
|
+
}
|
|
1266
|
+
async function collectFromIterable(iter, cwd, limit, includeMetadata, signal) {
|
|
1267
|
+
const results = [];
|
|
1268
|
+
try {
|
|
1269
|
+
for await (const rel of iter) {
|
|
1270
|
+
if (signal?.aborted) break;
|
|
1271
|
+
const entry = await addMetadata(cwd, rel, includeMetadata);
|
|
1272
|
+
if (!entry) continue;
|
|
1273
|
+
results.push(entry);
|
|
1274
|
+
if (results.length >= limit) break;
|
|
1275
|
+
}
|
|
1276
|
+
} catch {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
return results.sort((a, b) => a.path.localeCompare(b.path));
|
|
1280
|
+
}
|
|
1281
|
+
function globViaBun(BunGlob, pattern, cwd, limit, includeMetadata, signal) {
|
|
1282
|
+
return collectFromIterable(new BunGlob(pattern).scan({
|
|
1283
|
+
cwd,
|
|
1284
|
+
onlyFiles: true,
|
|
1285
|
+
dot: true
|
|
1286
|
+
}), cwd, limit, includeMetadata, signal);
|
|
1287
|
+
}
|
|
1288
|
+
async function globFilesLocal(pattern, cwd, options) {
|
|
1289
|
+
const includeMetadata = options.metadata === true;
|
|
1290
|
+
const { limit, signal } = options;
|
|
1291
|
+
if (options.gitignore !== false && !forcePortable() && !patternEscapesCwd(pattern)) {
|
|
1292
|
+
const gitFiles = await gitListFilesLocal(cwd, signal);
|
|
1293
|
+
if (gitFiles) return matchGitFiles(gitFiles, pattern, cwd, limit, includeMetadata, signal);
|
|
1294
|
+
}
|
|
1295
|
+
const bunGlob = loadBunGlob();
|
|
1296
|
+
if (bunGlob) {
|
|
1297
|
+
const viaBun = await globViaBun(bunGlob, pattern, cwd, limit, includeMetadata, signal);
|
|
1298
|
+
if (viaBun) return viaBun;
|
|
1299
|
+
} else {
|
|
1300
|
+
const nodeGlob = await loadNodeGlob();
|
|
1301
|
+
if (nodeGlob) {
|
|
1302
|
+
const viaNode = await collectFromIterable(nodeGlob(pattern, { cwd }), cwd, limit, includeMetadata, signal);
|
|
1303
|
+
if (viaNode) return viaNode;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return globPortable(pattern, cwd, limit, includeMetadata, signal);
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1309
|
+
//#region src/contexts/process-files.ts
|
|
1310
|
+
function processFileMethods(s) {
|
|
1311
|
+
const { workspaceRoot, isolation } = s;
|
|
1312
|
+
/**
|
|
1313
|
+
* Resolve `path` against the handle cwd and, when a `workspaceRoot` is
|
|
1314
|
+
* configured, reject anything that escapes it. Uses `realpath` on the
|
|
1315
|
+
* nearest existing ancestor so symlink escapes (and `..`) are caught for
|
|
1316
|
+
* both existing and not-yet-created targets.
|
|
1317
|
+
*/
|
|
1318
|
+
async function resolveContained(handle, path) {
|
|
1319
|
+
const full = resolve(handle.cwd, path);
|
|
1320
|
+
if (workspaceRoot === void 0) return full;
|
|
1321
|
+
const canonical = await canonicalizeForCheck(full);
|
|
1322
|
+
const canonicalRoot = await realpath(workspaceRoot).catch(() => workspaceRoot);
|
|
1323
|
+
if (!isWithinRoot(canonicalRoot, canonical)) throw new Error(`Path escapes the workspace root: ${path} (resolved outside ${canonicalRoot})`);
|
|
1324
|
+
return full;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Canonicalize the deepest existing ancestor of `full` to defeat symlink
|
|
1328
|
+
* escapes; the non-existent tail (for writes) is appended back verbatim.
|
|
1329
|
+
*/
|
|
1330
|
+
async function canonicalizeForCheck(full) {
|
|
1331
|
+
let existing = full;
|
|
1332
|
+
const tail = [];
|
|
1333
|
+
while (true) try {
|
|
1334
|
+
existing = await realpath(existing);
|
|
1335
|
+
break;
|
|
1336
|
+
} catch {
|
|
1337
|
+
const parent = dirname(existing);
|
|
1338
|
+
if (parent === existing) {
|
|
1339
|
+
existing = parent;
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
tail.unshift(relative(parent, existing));
|
|
1343
|
+
existing = parent;
|
|
1344
|
+
}
|
|
1345
|
+
return tail.length ? resolve(existing, ...tail) : existing;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* In-process counterpart of the sandbox's write policy: `writeFile` (and
|
|
1349
|
+
* so the `write_file` / `edit` tools) may only target `isolation.writableRoots`.
|
|
1350
|
+
* Reads are deliberately unrestricted — same boundary the kernel enforces
|
|
1351
|
+
* for shell commands, so the two never disagree about where writes land.
|
|
1352
|
+
*/
|
|
1353
|
+
async function assertWritable(full, path) {
|
|
1354
|
+
if (!isolation) return;
|
|
1355
|
+
const canonical = await canonicalizeForCheck(full);
|
|
1356
|
+
if (!isolation.writableRoots.some((root) => isWithinRoot(root, canonical))) throw new Error(`[sandbox] write denied: ${path} is outside the writable roots (${isolation.writableRoots.join(", ")}). Ask the user to add it under Settings → "Sandbox writable paths" or turn off "Sandbox shell commands".`);
|
|
1357
|
+
}
|
|
1358
|
+
return {
|
|
1359
|
+
async readFile(handle, path) {
|
|
1360
|
+
return readFile(await resolveContained(handle, path), "utf-8");
|
|
878
1361
|
},
|
|
879
1362
|
async fileSize(handle, path) {
|
|
880
1363
|
try {
|
|
@@ -913,67 +1396,252 @@ function createProcessContext(config) {
|
|
|
913
1396
|
return contained;
|
|
914
1397
|
}
|
|
915
1398
|
return (await readdir(await resolveContained(handle, path), { withFileTypes: true })).map((d) => d.isDirectory() ? `${d.name}/` : d.name);
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
//#endregion
|
|
1403
|
+
//#region src/contexts/ansi-strip.ts
|
|
1404
|
+
/** Parser states: plain text, after ESC, in a CSI, in an OSC/DCS string, after ESC inside one. */
|
|
1405
|
+
const State = {
|
|
1406
|
+
Text: 0,
|
|
1407
|
+
Esc: 1,
|
|
1408
|
+
Csi: 2,
|
|
1409
|
+
Str: 3,
|
|
1410
|
+
StrEsc: 4
|
|
1411
|
+
};
|
|
1412
|
+
function createAnsiStripper(options = {}) {
|
|
1413
|
+
let state = State.Text;
|
|
1414
|
+
/** Body of the OSC / DCS / APC string being read; OSC only is kept. */
|
|
1415
|
+
let str = "";
|
|
1416
|
+
let strIsOsc = false;
|
|
1417
|
+
let line = "";
|
|
1418
|
+
/** A `\r` arrived; the next printable rewinds the line unless `\n` follows. */
|
|
1419
|
+
let pendingCr = false;
|
|
1420
|
+
/** Part of the current line was handed out by `takePartial`: a rewind must start a new line. */
|
|
1421
|
+
let taken = false;
|
|
1422
|
+
const endString = () => {
|
|
1423
|
+
if (strIsOsc && options.onTitle) {
|
|
1424
|
+
const semi = str.indexOf(";");
|
|
1425
|
+
const code = semi === -1 ? str : str.slice(0, semi);
|
|
1426
|
+
if ((code === "0" || code === "2") && semi !== -1) options.onTitle(str.slice(semi + 1));
|
|
1427
|
+
}
|
|
1428
|
+
str = "";
|
|
1429
|
+
state = State.Text;
|
|
1430
|
+
};
|
|
1431
|
+
return {
|
|
1432
|
+
push(chunk) {
|
|
1433
|
+
let out = "";
|
|
1434
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
1435
|
+
const ch = chunk[i];
|
|
1436
|
+
const code = ch.charCodeAt(0);
|
|
1437
|
+
switch (state) {
|
|
1438
|
+
case State.Text:
|
|
1439
|
+
if (code === 27) state = State.Esc;
|
|
1440
|
+
else if (ch === "\n") {
|
|
1441
|
+
out += `${line}\n`;
|
|
1442
|
+
line = "";
|
|
1443
|
+
pendingCr = false;
|
|
1444
|
+
taken = false;
|
|
1445
|
+
} else if (ch === "\r") pendingCr = true;
|
|
1446
|
+
else if (code === 8) line = line.slice(0, -1);
|
|
1447
|
+
else if (code === 155) state = State.Csi;
|
|
1448
|
+
else if (code === 157) {
|
|
1449
|
+
state = State.Str;
|
|
1450
|
+
strIsOsc = true;
|
|
1451
|
+
} else if (ch === " " || code >= 32) {
|
|
1452
|
+
if (code >= 127 && code < 160) break;
|
|
1453
|
+
if (pendingCr) {
|
|
1454
|
+
if (taken) out += "\n";
|
|
1455
|
+
taken = false;
|
|
1456
|
+
line = "";
|
|
1457
|
+
pendingCr = false;
|
|
1458
|
+
}
|
|
1459
|
+
line += ch;
|
|
1460
|
+
}
|
|
1461
|
+
break;
|
|
1462
|
+
case State.Esc:
|
|
1463
|
+
if (ch === "[") state = State.Csi;
|
|
1464
|
+
else if (ch === "]" || ch === "P" || ch === "X" || ch === "^" || ch === "_") {
|
|
1465
|
+
state = State.Str;
|
|
1466
|
+
strIsOsc = ch === "]";
|
|
1467
|
+
str = "";
|
|
1468
|
+
} else if (code >= 32 && code <= 47) {} else state = State.Text;
|
|
1469
|
+
break;
|
|
1470
|
+
case State.Csi:
|
|
1471
|
+
if (code >= 64 && code <= 126) state = State.Text;
|
|
1472
|
+
break;
|
|
1473
|
+
case State.Str:
|
|
1474
|
+
if (code === 7) endString();
|
|
1475
|
+
else if (code === 27) state = State.StrEsc;
|
|
1476
|
+
else if (code === 156) endString();
|
|
1477
|
+
else if (str.length < 4096) str += ch;
|
|
1478
|
+
break;
|
|
1479
|
+
case State.StrEsc: if (ch === "\\") endString();
|
|
1480
|
+
else {
|
|
1481
|
+
str += `\x1B${ch}`;
|
|
1482
|
+
state = State.Str;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return out;
|
|
916
1487
|
},
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1488
|
+
partial: () => line,
|
|
1489
|
+
takePartial() {
|
|
1490
|
+
const text = line;
|
|
1491
|
+
if (text) taken = true;
|
|
1492
|
+
line = "";
|
|
1493
|
+
return text;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
//#endregion
|
|
1498
|
+
//#region src/contexts/process-shell.ts
|
|
1499
|
+
function createProcessShell(s) {
|
|
1500
|
+
const { baseEnv, defaultEnv, isolation, isolationState } = s;
|
|
1501
|
+
/**
|
|
1502
|
+
* `{ file, args }` for `/bin/sh -c command`, wrapped in the sandbox when
|
|
1503
|
+
* isolation is on. Callers check `isolationState.ok` first (the three
|
|
1504
|
+
* shell entry points return `unavailableResult` / throw before spawning).
|
|
1505
|
+
*/
|
|
1506
|
+
function shellCommand(command) {
|
|
1507
|
+
if (isolation && isolationState?.ok) return wrapCommand(isolationState.backend, isolationState.executable, isolation, command);
|
|
1508
|
+
return {
|
|
1509
|
+
file: "/bin/sh",
|
|
1510
|
+
args: ["-c", command]
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
/** Failure result when isolation was requested but cannot be enforced; `undefined` = proceed. */
|
|
1514
|
+
function isolationBlocker() {
|
|
1515
|
+
return isolationState && !isolationState.ok ? unavailableResult(isolationState.reason) : void 0;
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* A background task in a PTY, as a `ChildProcess` look-alike whose stdout
|
|
1519
|
+
* carries the log text. A line still unfinished after a quiet moment (a
|
|
1520
|
+
* prompt waiting for input) is written out too, so the log shows it.
|
|
1521
|
+
*/
|
|
1522
|
+
function spawnTerminalTask(argv, cwd, extraEnv, tty) {
|
|
1523
|
+
const stripper = createAnsiStripper();
|
|
1524
|
+
const decoder = new TextDecoder();
|
|
1525
|
+
let quiet;
|
|
1526
|
+
let emitLog = () => {};
|
|
1527
|
+
const flushPartial = () => {
|
|
1528
|
+
const rest = stripper.takePartial();
|
|
1529
|
+
if (rest) emitLog(rest);
|
|
1530
|
+
};
|
|
1531
|
+
const pty = spawnLocalPty(argv, {
|
|
1532
|
+
cwd,
|
|
1533
|
+
env: terminalEnv(extraEnv),
|
|
1534
|
+
cols: tty.cols,
|
|
1535
|
+
rows: tty.rows,
|
|
1536
|
+
onData: (data) => {
|
|
1537
|
+
tty.onData(data);
|
|
1538
|
+
const text = stripper.push(decoder.decode(data, { stream: true }));
|
|
1539
|
+
if (text) emitLog(text);
|
|
1540
|
+
if (quiet) clearTimeout(quiet);
|
|
1541
|
+
quiet = setTimeout(flushPartial, 300);
|
|
1542
|
+
quiet.unref?.();
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
const { child, stdout } = ptyChildProcess(pty);
|
|
1546
|
+
const encoder = new TextEncoder();
|
|
1547
|
+
emitLog = (text) => stdout.emit("data", encoder.encode(text));
|
|
1548
|
+
pty.exited.then(() => {
|
|
1549
|
+
if (quiet) clearTimeout(quiet);
|
|
1550
|
+
flushPartial();
|
|
1551
|
+
});
|
|
1552
|
+
tty.onReady?.({
|
|
1553
|
+
write: pty.write,
|
|
1554
|
+
resize: pty.resize
|
|
1555
|
+
});
|
|
1556
|
+
return child;
|
|
1557
|
+
}
|
|
1558
|
+
/** The environment of a terminal: what commands get, plus terminal identity, minus `unset`. */
|
|
1559
|
+
function terminalEnv(extra, unset = []) {
|
|
1560
|
+
const env = {};
|
|
1561
|
+
for (const [key, value] of Object.entries({
|
|
1562
|
+
...baseEnv,
|
|
1563
|
+
...defaultEnv,
|
|
1564
|
+
...extra,
|
|
1565
|
+
...TERMINAL_ENV
|
|
1566
|
+
})) if (typeof value === "string" && !unset.includes(key)) env[key] = value;
|
|
1567
|
+
return env;
|
|
1568
|
+
}
|
|
1569
|
+
return {
|
|
1570
|
+
shellCommand,
|
|
1571
|
+
isolationBlocker,
|
|
1572
|
+
spawnTerminalTask,
|
|
1573
|
+
terminalEnv
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
//#endregion
|
|
1577
|
+
//#region src/contexts/process-stream.ts
|
|
1578
|
+
function processStreamMethods(s, sh) {
|
|
1579
|
+
const { baseEnv, defaultEnv, registerExitHandler, streamChildren } = s;
|
|
1580
|
+
const { shellCommand, isolationBlocker } = sh;
|
|
1581
|
+
return {
|
|
1582
|
+
/**
|
|
1583
|
+
* Live bidirectional stream to a child process — see
|
|
1584
|
+
* `ExecutionContext.execStream` for the seam contract.
|
|
1585
|
+
*
|
|
1586
|
+
* The reference implementation: an ordinary piped `spawn`, so both
|
|
1587
|
+
* streams stay separate and no terminal line discipline sits between
|
|
1588
|
+
* the caller and the child. Sandbox-backed contexts have to work
|
|
1589
|
+
* harder to reach the same shape.
|
|
1590
|
+
*
|
|
1591
|
+
* Unlike `execBackground`, nothing here lands in the `tasks` registry:
|
|
1592
|
+
* this handle is owned by its caller (an MCP transport's `close()`),
|
|
1593
|
+
* not by the model-facing background-task list that `shell_kill` /
|
|
1594
|
+
* `wait_task` walk. It IS tracked in `streamChildren` so the shared
|
|
1595
|
+
* orphan reaper and `destroy()` can sweep it — detached process-group
|
|
1596
|
+
* semantics (needed so `kill()` takes the child's own children with
|
|
1597
|
+
* it) are exactly what would otherwise leak a live server per crash.
|
|
1598
|
+
*/
|
|
1599
|
+
async execStream(handle, command, options) {
|
|
1600
|
+
const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
|
|
1601
|
+
const spawnChild = await loadSpawn();
|
|
1602
|
+
options?.signal?.throwIfAborted();
|
|
1603
|
+
const blocked = isolationBlocker();
|
|
1604
|
+
if (blocked) throw new Error(blocked.stderr);
|
|
1605
|
+
const shell = shellCommand(command);
|
|
1606
|
+
const child = spawnChild(shell.file, shell.args, {
|
|
1607
|
+
cwd,
|
|
1608
|
+
env: {
|
|
1609
|
+
...baseEnv,
|
|
1610
|
+
...defaultEnv,
|
|
1611
|
+
...options?.env
|
|
1612
|
+
},
|
|
1613
|
+
stdio: [
|
|
1614
|
+
"pipe",
|
|
1615
|
+
"pipe",
|
|
1616
|
+
"pipe"
|
|
1617
|
+
],
|
|
1618
|
+
detached: SUPPORTS_PROCESS_GROUPS
|
|
1619
|
+
});
|
|
1620
|
+
registerExitHandler();
|
|
1621
|
+
streamChildren.set(child, handle.id);
|
|
1622
|
+
let spawnError = null;
|
|
1623
|
+
child.on("error", (err) => {
|
|
1624
|
+
spawnError = err;
|
|
1625
|
+
});
|
|
1626
|
+
let closed = false;
|
|
1627
|
+
let escalation;
|
|
1628
|
+
const exited = new Promise((resolveExit) => {
|
|
1629
|
+
child.once("close", (code, signalName) => {
|
|
1630
|
+
closed = true;
|
|
1631
|
+
clearTimeout(escalation);
|
|
1632
|
+
streamChildren.delete(child);
|
|
1633
|
+
resolveExit(code !== null ? code : signalName === "SIGTERM" ? 143 : signalName ? 128 : 1);
|
|
966
1634
|
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
if (options
|
|
1635
|
+
});
|
|
1636
|
+
const kill = async () => {
|
|
1637
|
+
if (closed || child.pid === void 0) return;
|
|
1638
|
+
killProcessGroup(child, "SIGTERM");
|
|
1639
|
+
if (escalation) return;
|
|
1640
|
+
escalation = setTimeout(killProcessGroup, EXEC_KILL_ESCALATION_GRACE_MS, child, "SIGKILL");
|
|
1641
|
+
escalation.unref?.();
|
|
1642
|
+
};
|
|
1643
|
+
if (options?.signal) {
|
|
1644
|
+
if (options.signal.aborted) kill();
|
|
977
1645
|
else {
|
|
978
1646
|
const onAbort = () => {
|
|
979
1647
|
kill();
|
|
@@ -982,168 +1650,29 @@ function createProcessContext(config) {
|
|
|
982
1650
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
983
1651
|
exited.then(detach, detach);
|
|
984
1652
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
},
|
|
997
|
-
kill,
|
|
998
|
-
exited
|
|
999
|
-
};
|
|
1000
|
-
},
|
|
1001
|
-
async execBackground(handle, command, options) {
|
|
1002
|
-
const cwd = options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
|
|
1003
|
-
const spawnChild = await loadSpawn();
|
|
1004
|
-
const blocked = isolationBlocker();
|
|
1005
|
-
if (blocked) throw new Error(blocked.stderr);
|
|
1006
|
-
await mkdir(options.outputDir, { recursive: true });
|
|
1007
|
-
const taskId = `bash_${++taskCounter}`;
|
|
1008
|
-
assertSafeTaskId(taskId);
|
|
1009
|
-
const outputPath = resolve(options.outputDir, `${taskId}.${contextTimestamp}.log`);
|
|
1010
|
-
registerExitHandler();
|
|
1011
|
-
const outputStream = createWriteStream(outputPath, { flags: "a" });
|
|
1012
|
-
outputStream.on("error", (err) => {
|
|
1013
|
-
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} log stream error: ${err.message}\n`);
|
|
1014
|
-
});
|
|
1015
|
-
const shell = shellCommand(command);
|
|
1016
|
-
const child = spawnChild(shell.file, shell.args, {
|
|
1017
|
-
cwd,
|
|
1018
|
-
env: {
|
|
1019
|
-
...baseEnv,
|
|
1020
|
-
...defaultEnv,
|
|
1021
|
-
...options.env
|
|
1022
|
-
},
|
|
1023
|
-
stdio: [
|
|
1024
|
-
"ignore",
|
|
1025
|
-
"pipe",
|
|
1026
|
-
"pipe"
|
|
1027
|
-
],
|
|
1028
|
-
detached: SUPPORTS_PROCESS_GROUPS
|
|
1029
|
-
});
|
|
1030
|
-
const state = {
|
|
1031
|
-
taskId,
|
|
1032
|
-
handleId: handle.id,
|
|
1033
|
-
pid: child.pid ?? -1,
|
|
1034
|
-
command,
|
|
1035
|
-
cwd,
|
|
1036
|
-
startedAt: Date.now(),
|
|
1037
|
-
outputPath,
|
|
1038
|
-
outputStream,
|
|
1039
|
-
child,
|
|
1040
|
-
status: "running",
|
|
1041
|
-
bytesWritten: 0,
|
|
1042
|
-
settled: false,
|
|
1043
|
-
onExit: options.onExit
|
|
1044
|
-
};
|
|
1045
|
-
tasks.set(taskId, state);
|
|
1046
|
-
const maxOutputBytes = typeof options.maxOutputBytes === "number" && options.maxOutputBytes > 0 ? options.maxOutputBytes : void 0;
|
|
1047
|
-
let droppedBytes = 0;
|
|
1048
|
-
let capExhausted = false;
|
|
1049
|
-
const stallTimeoutMs = typeof options.stallTimeoutMs === "number" && options.stallTimeoutMs > 0 ? options.stallTimeoutMs : void 0;
|
|
1050
|
-
const onStall = options.onStall;
|
|
1051
|
-
let stallTimer;
|
|
1052
|
-
let lastOutputAt = Date.now();
|
|
1053
|
-
const armStallTimer = () => {
|
|
1054
|
-
if (!stallTimeoutMs || !onStall) return;
|
|
1055
|
-
if (stallTimer) clearTimeout(stallTimer);
|
|
1056
|
-
stallTimer = setTimeout(() => {
|
|
1057
|
-
stallTimer = void 0;
|
|
1058
|
-
if (state.settled) return;
|
|
1059
|
-
try {
|
|
1060
|
-
onStall({
|
|
1061
|
-
taskId,
|
|
1062
|
-
command,
|
|
1063
|
-
outputPath,
|
|
1064
|
-
stalledForMs: Date.now() - lastOutputAt,
|
|
1065
|
-
bytesWritten: state.bytesWritten
|
|
1066
|
-
});
|
|
1067
|
-
} catch (err) {
|
|
1068
|
-
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} onStall threw: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1069
|
-
}
|
|
1070
|
-
}, stallTimeoutMs);
|
|
1071
|
-
stallTimer.unref?.();
|
|
1072
|
-
};
|
|
1073
|
-
const clearStallTimer = () => {
|
|
1074
|
-
if (stallTimer) {
|
|
1075
|
-
clearTimeout(stallTimer);
|
|
1076
|
-
stallTimer = void 0;
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
1079
|
-
armStallTimer();
|
|
1080
|
-
const appendChunk = (chunk) => {
|
|
1081
|
-
state.bytesWritten += chunk.length;
|
|
1082
|
-
lastOutputAt = Date.now();
|
|
1083
|
-
armStallTimer();
|
|
1084
|
-
if (maxOutputBytes !== void 0) {
|
|
1085
|
-
const onDisk = state.bytesWritten - droppedBytes - chunk.length;
|
|
1086
|
-
if (capExhausted || onDisk >= maxOutputBytes) {
|
|
1087
|
-
capExhausted = true;
|
|
1088
|
-
droppedBytes += chunk.length;
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
const room = maxOutputBytes - onDisk;
|
|
1092
|
-
if (chunk.length > room) {
|
|
1093
|
-
capExhausted = true;
|
|
1094
|
-
let end = room;
|
|
1095
|
-
while (end > 0 && (chunk[end] & 192) === 128) end--;
|
|
1096
|
-
droppedBytes += chunk.length - end;
|
|
1097
|
-
if (end > 0) outputStream.write(chunk.subarray(0, end));
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
outputStream.write(chunk);
|
|
1102
|
-
};
|
|
1103
|
-
child.stdout?.on("data", (chunk) => appendChunk(chunk));
|
|
1104
|
-
child.stderr?.on("data", (chunk) => appendChunk(chunk));
|
|
1105
|
-
const settle = (cause, code, signal, errMessage) => {
|
|
1106
|
-
if (state.settled) return;
|
|
1107
|
-
state.settled = true;
|
|
1108
|
-
clearStallTimer();
|
|
1109
|
-
state.endedAt = Date.now();
|
|
1110
|
-
const status = signal === "SIGTERM" || state.killRequested ? "killed" : "exited";
|
|
1111
|
-
const exitCode = code !== null ? code : signal === "SIGTERM" ? 143 : signal ? 128 : 1;
|
|
1112
|
-
state.status = status;
|
|
1113
|
-
state.exitCode = exitCode;
|
|
1114
|
-
if (signal) state.signal = signal;
|
|
1115
|
-
if (errMessage) try {
|
|
1116
|
-
outputStream.write(`\n${errMessage}\n`);
|
|
1117
|
-
} catch {}
|
|
1118
|
-
if (droppedBytes > 0) try {
|
|
1119
|
-
outputStream.write(`\n<output-truncated bytes-dropped="${droppedBytes}"/>\n`);
|
|
1120
|
-
} catch {}
|
|
1121
|
-
outputStream.end(() => {
|
|
1122
|
-
try {
|
|
1123
|
-
state.onExit?.(stateToTaskExitInfo(state));
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} onExit threw: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1126
|
-
}
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
pid: child.pid ?? -1,
|
|
1656
|
+
stdout: child.stdout,
|
|
1657
|
+
stderr: child.stderr,
|
|
1658
|
+
write: async (data) => {
|
|
1659
|
+
if (spawnError) throw spawnError;
|
|
1660
|
+
const stdin = child.stdin;
|
|
1661
|
+
if (!stdin || stdin.destroyed) throw new Error(`execStream: stdin is closed (pid ${child.pid ?? -1})`);
|
|
1662
|
+
await new Promise((resolveWrite, rejectWrite) => {
|
|
1663
|
+
stdin.write(data, (err) => err ? rejectWrite(err) : resolveWrite());
|
|
1127
1664
|
});
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}, EXIT_PIPE_DRAIN_GRACE_MS).unref?.();
|
|
1140
|
-
});
|
|
1141
|
-
return {
|
|
1142
|
-
taskId,
|
|
1143
|
-
pid: state.pid,
|
|
1144
|
-
outputPath
|
|
1145
|
-
};
|
|
1146
|
-
},
|
|
1665
|
+
},
|
|
1666
|
+
kill,
|
|
1667
|
+
exited
|
|
1668
|
+
};
|
|
1669
|
+
} };
|
|
1670
|
+
}
|
|
1671
|
+
//#endregion
|
|
1672
|
+
//#region src/contexts/process-tasks.ts
|
|
1673
|
+
function processTaskMethods(s) {
|
|
1674
|
+
const { tasks, destroyGraceMs, streamChildren, handles } = s;
|
|
1675
|
+
return {
|
|
1147
1676
|
async killBackground(handle, taskId) {
|
|
1148
1677
|
const state = tasks.get(taskId);
|
|
1149
1678
|
if (!state || state.handleId !== handle.id) return null;
|
|
@@ -1154,11 +1683,7 @@ function createProcessContext(config) {
|
|
|
1154
1683
|
resolveP();
|
|
1155
1684
|
return;
|
|
1156
1685
|
}
|
|
1157
|
-
|
|
1158
|
-
state.onExit = (info) => {
|
|
1159
|
-
originalOnExit?.(info);
|
|
1160
|
-
resolveP();
|
|
1161
|
-
};
|
|
1686
|
+
state.exitWatchers.push(() => resolveP());
|
|
1162
1687
|
});
|
|
1163
1688
|
killProcessGroup(state.child, "SIGTERM");
|
|
1164
1689
|
await closed;
|
|
@@ -1177,16 +1702,18 @@ function createProcessContext(config) {
|
|
|
1177
1702
|
done = true;
|
|
1178
1703
|
if (timer) clearTimeout(timer);
|
|
1179
1704
|
signal?.removeEventListener("abort", onAbort);
|
|
1705
|
+
const index = state.exitWatchers.indexOf(watcher);
|
|
1706
|
+
if (index >= 0) state.exitWatchers.splice(index, 1);
|
|
1180
1707
|
resolveP(value);
|
|
1181
1708
|
};
|
|
1182
1709
|
function onAbort() {
|
|
1183
1710
|
finish(null);
|
|
1184
1711
|
}
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1712
|
+
const task = state;
|
|
1713
|
+
function watcher() {
|
|
1714
|
+
finish(stateToTaskExitInfo(task));
|
|
1715
|
+
}
|
|
1716
|
+
state.exitWatchers.push(watcher);
|
|
1190
1717
|
if (signal) {
|
|
1191
1718
|
if (signal.aborted) {
|
|
1192
1719
|
finish(null);
|
|
@@ -1201,13 +1728,13 @@ function createProcessContext(config) {
|
|
|
1201
1728
|
}
|
|
1202
1729
|
});
|
|
1203
1730
|
},
|
|
1204
|
-
async reassignBackgroundTasks(fromHandle, toHandle, newOnExit) {
|
|
1205
|
-
if (fromHandle.id === toHandle.id) return [];
|
|
1731
|
+
async reassignBackgroundTasks(fromHandle, toHandle, newOnExit, newOnStall) {
|
|
1206
1732
|
const promoted = [];
|
|
1207
1733
|
for (const state of tasks.values()) {
|
|
1208
1734
|
if (state.handleId !== fromHandle.id || state.status !== "running") continue;
|
|
1209
1735
|
state.handleId = toHandle.id;
|
|
1210
1736
|
if (newOnExit) state.onExit = newOnExit;
|
|
1737
|
+
if (newOnStall) state.onStall = newOnStall;
|
|
1211
1738
|
promoted.push(stateToTaskEntry(state));
|
|
1212
1739
|
}
|
|
1213
1740
|
return promoted;
|
|
@@ -1221,12 +1748,10 @@ function createProcessContext(config) {
|
|
|
1221
1748
|
state.killRequested = true;
|
|
1222
1749
|
await new Promise((resolveP) => {
|
|
1223
1750
|
let graceTimer;
|
|
1224
|
-
|
|
1225
|
-
state.onExit = (info) => {
|
|
1226
|
-
originalOnExit?.(info);
|
|
1751
|
+
state.exitWatchers.push(() => {
|
|
1227
1752
|
if (graceTimer) clearTimeout(graceTimer);
|
|
1228
1753
|
resolveP();
|
|
1229
|
-
};
|
|
1754
|
+
});
|
|
1230
1755
|
killProcessGroup(state.child, "SIGTERM");
|
|
1231
1756
|
graceTimer = setTimeout(() => {
|
|
1232
1757
|
killProcessGroup(state.child, "SIGKILL");
|
|
@@ -1242,76 +1767,86 @@ function createProcessContext(config) {
|
|
|
1242
1767
|
streamChildren.delete(child);
|
|
1243
1768
|
}
|
|
1244
1769
|
handles.delete(handle.id);
|
|
1245
|
-
if (exitHandlerRegistered && handles.size === 0) {
|
|
1246
|
-
process.off("exit", exitHandler);
|
|
1247
|
-
exitHandlerRegistered = false;
|
|
1770
|
+
if (s.exitHandlerRegistered && handles.size === 0) {
|
|
1771
|
+
process.off("exit", s.exitHandler);
|
|
1772
|
+
s.exitHandlerRegistered = false;
|
|
1248
1773
|
}
|
|
1249
1774
|
}
|
|
1250
1775
|
};
|
|
1251
1776
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
else process.kill(pid, signal);
|
|
1265
|
-
} catch {}
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* Project a `TaskState` to the `TaskEntry` shape `listBackground` and
|
|
1269
|
-
* `reassignBackgroundTasks` return. Single helper keeps the shape
|
|
1270
|
-
* consistent across both call sites (and a future addition of fields
|
|
1271
|
-
* to `TaskEntry` only needs to land here).
|
|
1272
|
-
*/
|
|
1273
|
-
function stateToTaskEntry(state) {
|
|
1274
|
-
return {
|
|
1275
|
-
taskId: state.taskId,
|
|
1276
|
-
pid: state.pid,
|
|
1277
|
-
command: state.command,
|
|
1278
|
-
cwd: state.cwd,
|
|
1279
|
-
startedAt: state.startedAt,
|
|
1280
|
-
...state.endedAt !== void 0 ? { endedAt: state.endedAt } : {},
|
|
1281
|
-
outputPath: state.outputPath,
|
|
1282
|
-
status: state.status,
|
|
1283
|
-
...state.exitCode !== void 0 ? { exitCode: state.exitCode } : {},
|
|
1284
|
-
...state.signal ? { signal: state.signal } : {},
|
|
1285
|
-
bytesWritten: state.bytesWritten
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* Project a settled `TaskState` to the `TaskExitInfo` shape `settle()`
|
|
1290
|
-
* fires from `onExit` and `killBackground` returns on the
|
|
1291
|
-
* cached-exit path. Pre-condition: `state.status !== 'running'`
|
|
1292
|
-
* (callers gate on this).
|
|
1293
|
-
*/
|
|
1294
|
-
function stateToTaskExitInfo(state) {
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region src/contexts/process.ts
|
|
1779
|
+
function createProcessContext(config) {
|
|
1780
|
+
const s = createProcessState(config);
|
|
1781
|
+
const { handles, defaultCwd, isolation, isolationState } = s;
|
|
1782
|
+
const sh = createProcessShell(s);
|
|
1783
|
+
const { shellCommand, isolationBlocker, terminalEnv } = sh;
|
|
1784
|
+
const files = processFileMethods(s);
|
|
1785
|
+
const execM = processExecMethods(s, sh);
|
|
1786
|
+
const streamM = processStreamMethods(s, sh);
|
|
1787
|
+
const bg = processBackgroundMethods(s, sh);
|
|
1788
|
+
const taskM = processTaskMethods(s);
|
|
1295
1789
|
return {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1790
|
+
type: "process",
|
|
1791
|
+
capabilities: {
|
|
1792
|
+
shell: true,
|
|
1793
|
+
filesystem: true,
|
|
1794
|
+
glob: true,
|
|
1795
|
+
network: true,
|
|
1796
|
+
gpu: false,
|
|
1797
|
+
stream: true,
|
|
1798
|
+
isolated: isolation !== void 0 && isolationState?.ok === true,
|
|
1799
|
+
detachedTasks: "process-lifetime"
|
|
1800
|
+
},
|
|
1801
|
+
async spawn(overrides) {
|
|
1802
|
+
const id = `process-${++s.counter}`;
|
|
1803
|
+
const cwd = overrides?.cwd ?? defaultCwd;
|
|
1804
|
+
let needsCreate = false;
|
|
1805
|
+
try {
|
|
1806
|
+
await stat(cwd);
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
needsCreate = err?.code === "ENOENT";
|
|
1809
|
+
}
|
|
1810
|
+
if (needsCreate) await mkdir(cwd, { recursive: true });
|
|
1811
|
+
const handle = {
|
|
1812
|
+
id,
|
|
1813
|
+
type: "process",
|
|
1814
|
+
cwd
|
|
1815
|
+
};
|
|
1816
|
+
handles.set(id, handle);
|
|
1817
|
+
return handle;
|
|
1818
|
+
},
|
|
1819
|
+
exec: execM.exec,
|
|
1820
|
+
readFile: files.readFile,
|
|
1821
|
+
fileSize: files.fileSize,
|
|
1822
|
+
readFileBinary: files.readFileBinary,
|
|
1823
|
+
writeFile: files.writeFile,
|
|
1824
|
+
listFiles: files.listFiles,
|
|
1825
|
+
execStream: streamM.execStream,
|
|
1826
|
+
...hasLocalPty() ? { async openTerminal(handle, options) {
|
|
1827
|
+
const blocked = isolationBlocker();
|
|
1828
|
+
if (blocked) throw new Error(blocked.stderr);
|
|
1829
|
+
const env = terminalEnv(options.env, options.unsetEnv);
|
|
1830
|
+
const shell = loginShell(env);
|
|
1831
|
+
const argv = isolation && isolationState?.ok ? (({ file, args }) => [file, ...args])(shellCommand(`exec ${JSON.stringify(shell)} -l`)) : [shell, "-l"];
|
|
1832
|
+
const pty = spawnLocalPty(argv, {
|
|
1833
|
+
cwd: options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd,
|
|
1834
|
+
env,
|
|
1835
|
+
cols: options.cols,
|
|
1836
|
+
rows: options.rows,
|
|
1837
|
+
onData: options.onData
|
|
1838
|
+
});
|
|
1839
|
+
return localTerminalProcess(pty, { id: `pty-${pty.pid}` });
|
|
1840
|
+
} } : {},
|
|
1841
|
+
execBackground: bg.execBackground,
|
|
1842
|
+
killBackground: taskM.killBackground,
|
|
1843
|
+
waitBackground: taskM.waitBackground,
|
|
1844
|
+
reassignBackgroundTasks: taskM.reassignBackgroundTasks,
|
|
1845
|
+
listBackground: taskM.listBackground,
|
|
1846
|
+
destroy: taskM.destroy
|
|
1303
1847
|
};
|
|
1304
1848
|
}
|
|
1305
1849
|
//#endregion
|
|
1306
|
-
|
|
1307
|
-
/**
|
|
1308
|
-
* Effective {@link DetachedTasksCapability} of a context, with the
|
|
1309
|
-
* backward-compatible inference for contexts that predate the field.
|
|
1310
|
-
*/
|
|
1311
|
-
function resolveDetachedTasksCapability(context) {
|
|
1312
|
-
return context.capabilities.detachedTasks ?? (context.execBackground ? "process-lifetime" : "none");
|
|
1313
|
-
}
|
|
1314
|
-
//#endregion
|
|
1315
|
-
export { DEFAULT_UNREADABLE_DIRS as a, resolveUnreadableDirs as c, killProcessGroup as i, resolveWritableRoots as l, SUPPORTS_PROCESS_GROUPS as n, isolationAvailability as o, createProcessContext as r, parseAllowWriteSetting as s, resolveDetachedTasksCapability as t, resolveWritableRootsAsync as u };
|
|
1850
|
+
export { SUPPORTS_PROCESS_GROUPS as a, isolationAvailability as c, resolveWritableRoots as d, resolveWritableRootsAsync as f, parseGitLsFiles as i, parseAllowWriteSetting as l, createAnsiStripper as n, killProcessGroup as o, resolveDetachedTasksCapability as p, GIT_LS_FILES_COMMAND as r, DEFAULT_UNREADABLE_DIRS as s, createProcessContext as t, resolveUnreadableDirs as u };
|
|
1316
1851
|
|
|
1317
|
-
//# sourceMappingURL=contexts-
|
|
1852
|
+
//# sourceMappingURL=contexts-d7UolYot.js.map
|