@aiworker/web 1.22.0 → 1.22.2
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 +4 -4
- package/bin/cli.js +7 -4
- package/bin/cli.test.js +165 -73
- package/bin/lib/cli-api-target.js +2 -2
- package/bin/lib/cli-args.js +119 -119
- package/bin/lib/cli-lifecycle.js +9 -9
- package/bin/lib/cli-network.js +3 -3
- package/bin/lib/cli-paths.js +3 -3
- package/bin/lib/cli-ports.js +2 -2
- package/bin/lib/cli-process.js +4 -4
- package/bin/lib/cli-startup.js +64 -15
- package/bin/lib/cli-tunnel-profiles.js +1 -1
- package/bin/lib/cli-tunnel-utils.js +2 -2
- package/bin/lib/commands-connect-url.js +19 -19
- package/bin/lib/commands-lifecycle.js +27 -27
- package/bin/lib/commands-logs.js +5 -5
- package/bin/lib/commands-models.js +1 -1
- package/bin/lib/commands-projects.js +1 -1
- package/bin/lib/commands-schedule.js +1 -1
- package/bin/lib/commands-serve.js +29 -29
- package/bin/lib/commands-session.js +1 -1
- package/bin/lib/commands-startup.js +45 -10
- package/bin/lib/commands-status.js +1 -1
- package/bin/lib/commands-tunnel.js +40 -40
- package/bin/lib/commands-update.js +1 -1
- package/bin/lib/commands-update.test.js +6 -6
- package/dist/assets/{CodeMirrorCommentWidgets-B3u7UIcE.js → CodeMirrorCommentWidgets-CSICpWQD.js} +1 -1
- package/dist/assets/{DiffView-DluiRSla.js → DiffView-hDBKesA_.js} +3 -3
- package/dist/assets/DirtyBranchSwitchDialog-C22HdWj4.js +1 -0
- package/dist/assets/FilesView-CdWzeOp9.js +17 -0
- package/dist/assets/GitView-VQQkYuUw.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-6lrhwD3q.js} +1 -1
- package/dist/assets/LinearIssuesView-Ba6Q9569.js +1 -0
- package/dist/assets/{MarkdownImageGallery-ClOvuZxC.js → MarkdownImageGallery-Czv_mhvS.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +12 -0
- package/dist/assets/{OnboardingScreen-D2DG7K6b.js → OnboardingScreen-ChxQoAkx.js} +1 -1
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-BzcZPlOq.js} +1 -1
- package/dist/assets/{PierreDiffViewer-B7m3f0d7.js → PierreDiffViewer-BchBtZlp.js} +2 -2
- package/dist/assets/PlanView-DyDt_nZ_.js +5 -0
- package/dist/assets/SettingsView-BKf5xBsP.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-DDQYaKWQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-iYF8Y2bz.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-CdjbOVMM.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-fFFm2QgH.js} +1 -1
- package/dist/assets/{WalkthroughView-egQgxel3.js → WalkthroughView-DOeCsMju.js} +2 -2
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-CzgLum_K.js} +2 -2
- package/dist/assets/{contextPanelEmbeddedChat-C3fjDbdg.js → contextPanelEmbeddedChat-C0ZLAepL.js} +1 -1
- package/dist/assets/{de-C1P6X5Rs.js → de-B5JaA3SA.js} +6 -5
- package/dist/assets/{es-CDgaF1_t.js → es-EPIH88ii.js} +6 -5
- package/dist/assets/{fr-Bgxqvvz_.js → fr-CAMfxUdZ.js} +6 -5
- package/dist/assets/{index-CODwDsU_.js → index-Bl3LSbjn.js} +1 -1
- package/dist/assets/index-DF5kMP-i.js +39 -0
- package/dist/assets/index-Dl3xbAB_.css +1 -0
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-Dx-dB_8l.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-CWFPwmnU.js} +1 -1
- package/dist/assets/{ja-BDRrEcNP.js → ja-CifOE1VN.js} +7 -6
- package/dist/assets/{ko-Ba8sQV79.js → ko-BBDyAyYQ.js} +7 -6
- package/dist/assets/{main-C5DVOxc6.js → main-DnBAoktx.js} +2 -2
- package/dist/assets/main-MhSaOZoB.js +930 -0
- package/dist/assets/{markdownCore-CZwzz5gw.js → markdownCore-BrBwOOMn.js} +2 -2
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-BKyvLseN.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-DKjeFgMN.js} +2 -2
- package/dist/assets/{patchFileDiff-yNfWXN4E.js → patchFileDiff-o8ms1me8.js} +1 -1
- package/dist/assets/pl-DgEHYfFM.js +16 -0
- package/dist/assets/{pt-BR-DIs6ElwV.js → pt-BR-DuWHojUw.js} +6 -5
- package/dist/assets/{renderElectronMiniChatApp-BcKLHXKe.js → renderElectronMiniChatApp-CDgBwwCS.js} +2 -2
- package/dist/assets/renderMobileApp-DJIKIK1r.js +25 -0
- package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +17 -0
- package/dist/assets/{scheduledTasksApi-DLW0mkQK.js → scheduledTasksApi-D8yamh1b.js} +1 -1
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-TEQCmzW8.js} +1 -1
- package/dist/assets/tr-D5m2yIWL.js +7 -0
- package/dist/assets/{uk-ZrCNjM7Y.js → uk-D_q60oog.js} +6 -5
- package/dist/assets/{useAppFontEffects-CjrntdL0.js → useAppFontEffects-D7sNpuL2.js} +161 -190
- package/dist/assets/useInlineCommentController-Brz_cLcQ.js +2 -0
- package/dist/assets/{useRootScrollLock-BHYPkrLv.js → useRootScrollLock-lXEnWlaU.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Dp3KMRQG.js} +2 -2
- package/dist/assets/vendor-zod-CK21ihK-.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-_mNEMjFY.js} +1 -1
- package/dist/assets/{zh-CN-iv_AV4EC.js → zh-CN-D8QvsVeg.js} +6 -5
- package/dist/assets/{zh-TW-CzEUk38j.js → zh-TW-B9t7rQQO.js} +6 -5
- package/dist/index.html +22 -22
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -3
- package/server/index.js +118 -85
- package/server/lib/agent-memory/actions.js +2 -2
- package/server/lib/agent-memory/feature-flag.js +1 -1
- package/server/lib/agent-memory/feature-flag.test.js +8 -8
- package/server/lib/agent-memory/routes.js +2 -2
- package/server/lib/agent-tool/runtime.js +37 -54
- package/server/lib/agent-tool/runtime.test.js +59 -59
- package/server/lib/browser-control/broker.js +3 -3
- package/server/lib/browser-control/broker.test.js +1 -1
- package/server/lib/changelog/update-notes.js +82 -0
- package/server/lib/changelog/update-notes.test.js +59 -0
- package/server/lib/client-auth/remote-clients.test.js +5 -5
- package/server/lib/context-obligatory/runtime.js +7 -7
- package/server/lib/context-obligatory/runtime.test.js +5 -5
- package/server/lib/dev-servers/parse.js +2 -2
- package/server/lib/dev-servers/parse.test.js +2 -2
- package/server/lib/dev-servers/routes.js +1 -1
- package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
- package/server/lib/dev-tunnel/client.js +1 -1
- package/server/lib/dev-tunnel/runtime.js +5 -5
- package/server/lib/dev-tunnel/tunnel.test.js +21 -1
- package/server/lib/dictation/local/model-catalog.js +1 -1
- package/server/lib/dictation/local/worker-process.js +2 -2
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-ws-bridge.js +1 -1
- package/server/lib/event-stream/protocol.test.js +4 -4
- package/server/lib/event-stream/runtime.test.js +6 -6
- package/server/lib/fs/DOCUMENTATION.md +2 -0
- package/server/lib/fs/routes.js +37 -26
- package/server/lib/fs/routes.test.js +98 -21
- package/server/lib/git/DOCUMENTATION.md +3 -1
- package/server/lib/git/identity-storage.js +1 -1
- package/server/lib/git/issue-2746-longpaths.test.js +4 -4
- package/server/lib/git/routes.js +17 -1
- package/server/lib/git/service.js +116 -26
- package/server/lib/git/service.test.js +174 -3
- package/server/lib/github/auth.js +8 -8
- package/server/lib/github/pr-status.js +1 -1
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +1 -1
- package/server/lib/inherited-env.test.js +2 -2
- package/server/lib/linear/auth.js +8 -8
- package/server/lib/linear/auth.test.js +24 -24
- package/server/lib/linear/issues.test.js +6 -6
- package/server/lib/linear/mapping.test.js +4 -4
- package/server/lib/linear/oauth.js +2 -2
- package/server/lib/linear/oauth.test.js +14 -14
- package/server/lib/linear/routes.js +5 -5
- package/server/lib/linear/routes.test.js +16 -16
- package/server/lib/linear/status-runtime.test.js +9 -9
- package/server/lib/linear/status.js +1 -1
- package/server/lib/linear/status.test.js +15 -15
- package/server/lib/linear/teams.test.js +4 -4
- package/server/lib/magic-prompts/routes.js +2 -2
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
- package/server/lib/mcp-reconnect/runtime.js +122 -0
- package/server/lib/mcp-reconnect/runtime.test.js +140 -0
- package/server/lib/message-queue/DOCUMENTATION.md +158 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/notifications/apns-runtime.js +17 -17
- package/server/lib/notifications/apns-runtime.test.js +11 -11
- package/server/lib/notifications/emitter-runtime.js +1 -1
- package/server/lib/notifications/emitter-runtime.test.js +1 -1
- package/server/lib/notifications/push-runtime.js +7 -7
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +1 -1
- package/server/lib/openchamber-control/action-resolution.test.js +10 -10
- package/server/lib/openchamber-control/actions.js +24 -24
- package/server/lib/openchamber-control/error.js +4 -4
- package/server/lib/openchamber-control/routes.js +2 -2
- package/server/lib/openchamber-control/routes.test.js +5 -5
- package/server/lib/openchamber-control/screenshots.js +1 -1
- package/server/lib/openchamber-control/screenshots.test.js +1 -1
- package/server/lib/openchamber-control/service.js +50 -50
- package/server/lib/openchamber-control/service.test.js +5 -5
- package/server/lib/openchamber-sessions/routes.js +124 -22
- package/server/lib/openchamber-sessions/routes.test.js +99 -11
- package/server/lib/opencode/DOCUMENTATION.md +13 -5
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +6 -6
- package/server/lib/opencode/cli-options.js +8 -8
- package/server/lib/opencode/config-paths.test.js +116 -0
- package/server/lib/opencode/core-routes.js +8 -7
- package/server/lib/opencode/core-routes.test.js +11 -11
- package/server/lib/opencode/env-config.js +5 -5
- package/server/lib/opencode/env-config.test.js +7 -7
- package/server/lib/opencode/env-runtime.js +10 -10
- package/server/lib/opencode/env-runtime.test.js +18 -18
- package/server/lib/opencode/feature-routes-runtime.js +17 -12
- package/server/lib/opencode/hmr-state-runtime.js +2 -2
- package/server/lib/opencode/hmr-state-runtime.test.js +1 -1
- package/server/lib/opencode/input-history-scope.js +14 -0
- package/server/lib/opencode/lifecycle.js +8 -8
- package/server/lib/opencode/lifecycle.test.js +4 -4
- package/server/lib/opencode/managed-plugin-config.js +35 -0
- package/server/lib/opencode/managed-process-registry.js +3 -3
- package/server/lib/opencode/mcp-oauth-callback.test.js +2 -2
- package/server/lib/opencode/openchamber-routes.js +15 -15
- package/server/lib/opencode/openchamber-routes.test.js +11 -11
- package/server/lib/opencode/plugins.js +2 -2
- package/server/lib/opencode/plugins.test.js +1 -1
- package/server/lib/opencode/project-icon-routes.js +2 -2
- package/server/lib/opencode/project-icon-routes.test.js +1 -1
- package/server/lib/opencode/providers.js +44 -2
- package/server/lib/opencode/providers.test.js +131 -4
- package/server/lib/opencode/proxy.js +2 -2
- package/server/lib/opencode/pwa-manifest-routes.js +1 -1
- package/server/lib/opencode/restart-session-recovery.test.js +1 -1
- package/server/lib/opencode/routes-upgrade.test.js +4 -4
- package/server/lib/opencode/routes.js +14 -14
- package/server/lib/opencode/server-startup-runtime.js +6 -6
- package/server/lib/opencode/session-runtime.js +3 -3
- package/server/lib/opencode/session-runtime.test.js +6 -6
- package/server/lib/opencode/settings-helpers.js +31 -3
- package/server/lib/opencode/settings-helpers.test.js +189 -12
- package/server/lib/opencode/settings-runtime.js +2 -2
- package/server/lib/opencode/settings-runtime.test.js +29 -0
- package/server/lib/opencode/shared.js +5 -2
- package/server/lib/opencode/shared.test.js +7 -7
- package/server/lib/opencode/shutdown-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +2 -1
- package/server/lib/opencode/snippets.js +1 -2
- package/server/lib/opencode/startup-performance.js +1 -1
- package/server/lib/opencode/startup-performance.test.js +6 -6
- package/server/lib/opencode/static-routes-runtime.js +8 -8
- package/server/lib/opencode/static-routes-runtime.test.js +7 -7
- package/server/lib/opencode/upgrade-capability.js +2 -2
- package/server/lib/opencode/upgrade-capability.test.js +3 -3
- package/server/lib/package-manager.js +17 -42
- package/server/lib/package-manager.test.js +19 -19
- package/server/lib/permission-auto-accept/runtime.js +1 -1
- package/server/lib/project-context/routes.js +1 -1
- package/server/lib/projects/project-config.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +9 -1
- package/server/lib/quota/credentials/providers.js +4 -0
- package/server/lib/quota/credentials/store.js +4 -4
- package/server/lib/quota/credentials/store.test.js +8 -8
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/exe-dev.js +75 -0
- package/server/lib/quota/providers/exe-dev.test.js +47 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +15 -0
- package/server/lib/quota/providers/ollama-cloud.js +1 -1
- package/server/lib/quota/providers/opencode-go.js +2 -1
- package/server/lib/quota/providers/opencode-go.test.js +9 -5
- package/server/lib/quota/providers/xai.js +1 -1
- package/server/lib/quota/routes.js +2 -0
- package/server/lib/quota/utils/auth.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/server/lib/relay/host-client.js +1 -1
- package/server/lib/relay/host-client.test.js +9 -1
- package/server/lib/relay/host-lock.js +1 -1
- package/server/lib/relay/service.js +5 -5
- package/server/lib/relay/tunnel-host.js +3 -1
- package/server/lib/relay/tunnel-host.test.js +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +2 -2
- package/server/lib/scheduled-tasks/routes.js +5 -5
- package/server/lib/scheduled-tasks/service.js +25 -25
- package/server/lib/scheduled-tasks/service.test.js +1 -1
- package/server/lib/security/bind-host.js +4 -4
- package/server/lib/security/bind-host.test.js +1 -1
- package/server/lib/security/request-security.js +11 -5
- package/server/lib/security/request-security.test.js +14 -14
- package/server/lib/session-assist/runtime.js +10 -9
- package/server/lib/session-folders/routes.js +2 -2
- package/server/lib/session-folders/routes.test.js +8 -8
- package/server/lib/session-goal/DOCUMENTATION.md +20 -7
- package/server/lib/session-goal/create.js +4 -2
- package/server/lib/session-goal/create.test.js +1 -1
- package/server/lib/session-goal/objectives.js +4 -4
- package/server/lib/session-goal/routes.js +1 -1
- package/server/lib/session-goal/runtime.js +88 -22
- package/server/lib/session-goal/runtime.test.js +428 -2
- package/server/lib/session-knowledge/runtime.js +11 -11
- package/server/lib/session-knowledge/runtime.test.js +5 -5
- package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
- package/server/lib/skills-catalog/cache.test.js +2 -2
- package/server/lib/skills-catalog/disk-cache.js +5 -5
- package/server/lib/skills-catalog/github-meta.test.js +2 -2
- package/server/lib/skills-catalog/install.js +3 -2
- package/server/lib/small-model/DOCUMENTATION.md +4 -0
- package/server/lib/small-model/call.js +9 -3
- package/server/lib/small-model/call.test.js +29 -6
- package/server/lib/small-model/index.js +8 -7
- package/server/lib/small-model/index.test.js +1 -1
- package/server/lib/small-model/resolve.js +1 -1
- package/server/lib/small-model/resolve.test.js +1 -1
- package/server/lib/small-model/routes.js +2 -1
- package/server/lib/system-prompt/runtime.js +3 -22
- package/server/lib/system-prompt/runtime.test.js +3 -3
- package/server/lib/terminal/DOCUMENTATION.md +11 -7
- package/server/lib/terminal/runtime.js +180 -26
- package/server/lib/terminal/runtime.test.js +554 -27
- package/server/lib/terminal/shells.js +20 -3
- package/server/lib/terminal/shells.test.js +33 -2
- package/server/lib/tts/base-url.js +3 -3
- package/server/lib/tts/routes.test.js +20 -20
- package/server/lib/ui-auth/ui-auth.js +12 -9
- package/server/lib/ui-auth/ui-auth.test.js +19 -3
- package/server/lib/ui-auth/ui-passkeys.js +7 -7
- package/server/lib/walkthrough/index.js +1 -0
- package/server/lib/walkthrough/jobs.test.js +1 -1
- package/server/lib/walkthrough/language.test.js +1 -1
- package/server/lib/walkthrough/model-settings.js +3 -3
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/pull-request.test.js +5 -5
- package/server/lib/walkthrough/reproduce-2607.test.js +2 -2
- package/server/lib/walkthrough/routes.test.js +26 -5
- package/server/lib/walkthrough/store.js +3 -3
- package/server/lib/walkthrough/store.test.js +1 -1
- package/server/opencode-proxy.test.js +2 -2
- package/server/proxy-headers.js +1 -1
- package/server/sse-routes.test.js +4 -4
- package/README.md.bak +0 -250
- package/bin/lib/DOCUMENTATION.md.bak +0 -163
- package/dist/assets/CommitSection-SGBCAW0w.js +0 -1
- package/dist/assets/FilesView-Bdq8ppva.js +0 -17
- package/dist/assets/GitView-Znn7VJcP.js +0 -6
- package/dist/assets/LinearIssuesView-jP-hXyac.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/PlanView-B0F3BtPZ.js +0 -5
- package/dist/assets/SettingsView-BBW-S-gh.js +0 -28
- package/dist/assets/ToolPartDiffPreview-CUNe28U1.js +0 -8
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/server/index.d.ts.bak +0 -39
- package/server/lib/agent-tool/DOCUMENTATION.md.bak +0 -119
- package/server/lib/browser-control/DOCUMENTATION.md.bak +0 -50
- package/server/lib/context-obligatory/DOCUMENTATION.md.bak +0 -19
- package/server/lib/dev-tunnel/DOCUMENTATION.md.bak +0 -57
- package/server/lib/dictation/DOCUMENTATION.md.bak +0 -100
- package/server/lib/event-stream/DOCUMENTATION.md.bak +0 -62
- package/server/lib/fs/DOCUMENTATION.md.bak +0 -47
- package/server/lib/git/DOCUMENTATION.md.bak +0 -193
- package/server/lib/github/DOCUMENTATION.md.bak +0 -183
- package/server/lib/linear/DOCUMENTATION.md.bak +0 -97
- package/server/lib/linear/OAUTH-BROKER-HANDOFF.md.bak +0 -91
- package/server/lib/markdown-image-grants/DOCUMENTATION.md.bak +0 -40
- package/server/lib/notifications/APNS.md.bak +0 -135
- package/server/lib/notifications/DOCUMENTATION.md.bak +0 -138
- package/server/lib/openchamber-control/DOCUMENTATION.md.bak +0 -64
- package/server/lib/opencode/DOCUMENTATION.md.bak +0 -431
- package/server/lib/opencode/managed-process-registry.d.ts.bak +0 -13
- package/server/lib/opencode/provider-env-aliases.d.ts.bak +0 -3
- package/server/lib/permission-auto-accept/DOCUMENTATION.md.bak +0 -34
- package/server/lib/project-context/DOCUMENTATION.md.bak +0 -160
- package/server/lib/quota/DOCUMENTATION.md.bak +0 -123
- package/server/lib/relay/DOCUMENTATION.md.bak +0 -100
- package/server/lib/scheduled-tasks/DOCUMENTATION.md.bak +0 -182
- package/server/lib/session-assist/DOCUMENTATION.md.bak +0 -66
- package/server/lib/session-goal/DOCUMENTATION.md.bak +0 -213
- package/server/lib/session-knowledge/DOCUMENTATION.md.bak +0 -89
- package/server/lib/skills-catalog/DOCUMENTATION.md.bak +0 -151
- package/server/lib/small-model/DOCUMENTATION.md.bak +0 -212
- package/server/lib/system-prompt/DOCUMENTATION.md.bak +0 -36
- package/server/lib/terminal/DOCUMENTATION.md.bak +0 -50
- package/server/lib/text/DOCUMENTATION.md.bak +0 -35
- package/server/lib/tts/DOCUMENTATION.md.bak +0 -147
- package/server/lib/tunnels/DOCUMENTATION.md.bak +0 -21
- package/server/lib/ui-auth/DOCUMENTATION.md.bak +0 -44
- package/server/lib/walkthrough/DOCUMENTATION.md.bak +0 -411
|
@@ -3,7 +3,6 @@ import http from 'node:http';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { describe, expect, it } from 'vitest';
|
|
6
|
-
import express from 'express';
|
|
7
6
|
import { WebSocket } from 'ws';
|
|
8
7
|
|
|
9
8
|
import { createTerminalRuntime } from './runtime.js';
|
|
@@ -24,6 +23,98 @@ function createResponse() {
|
|
|
24
23
|
};
|
|
25
24
|
}
|
|
26
25
|
|
|
26
|
+
async function openTerminalSocket(socketUrl) {
|
|
27
|
+
const socket = new WebSocket(socketUrl);
|
|
28
|
+
const messages = [];
|
|
29
|
+
socket.on('message', (raw) => messages.push(readTerminalWsControlFrame(raw)));
|
|
30
|
+
await new Promise((resolve, reject) => {
|
|
31
|
+
socket.once('open', resolve);
|
|
32
|
+
socket.once('error', reject);
|
|
33
|
+
});
|
|
34
|
+
const next = async (type, sessionId) => {
|
|
35
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
36
|
+
const index = messages.findIndex((message) => message?.t === type && (!sessionId || message.s === sessionId));
|
|
37
|
+
if (index >= 0) return messages.splice(index, 1)[0];
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Timed out waiting for ${type}`);
|
|
41
|
+
};
|
|
42
|
+
await next('hello');
|
|
43
|
+
return { socket, next, messages };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function deferred() {
|
|
47
|
+
let resolve;
|
|
48
|
+
let reject;
|
|
49
|
+
const promise = new Promise((res, rej) => {
|
|
50
|
+
resolve = res;
|
|
51
|
+
reject = rej;
|
|
52
|
+
});
|
|
53
|
+
return { promise, resolve, reject };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createHttpTestApp() {
|
|
57
|
+
const routes = { GET: [], POST: [], DELETE: [] };
|
|
58
|
+
const app = (req, res) => {
|
|
59
|
+
const methodRoutes = routes[req.method] ?? [];
|
|
60
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
61
|
+
const chunks = [];
|
|
62
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
63
|
+
req.on('end', async () => {
|
|
64
|
+
const bodyText = Buffer.concat(chunks).toString('utf8');
|
|
65
|
+
const route = methodRoutes.find(({ pattern }) => pattern.test(url.pathname));
|
|
66
|
+
if (!route) {
|
|
67
|
+
res.statusCode = 404;
|
|
68
|
+
res.end('Not found');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const match = route.pattern.exec(url.pathname);
|
|
72
|
+
const response = {
|
|
73
|
+
statusCode: 200,
|
|
74
|
+
headers: { 'content-type': 'application/json' },
|
|
75
|
+
body: null,
|
|
76
|
+
status(code) {
|
|
77
|
+
this.statusCode = code;
|
|
78
|
+
return this;
|
|
79
|
+
},
|
|
80
|
+
json(payload) {
|
|
81
|
+
this.body = payload;
|
|
82
|
+
return this;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
await route.handler({
|
|
87
|
+
method: req.method,
|
|
88
|
+
url: req.url,
|
|
89
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
90
|
+
params: route.params.reduce((acc, name, index) => ({ ...acc, [name]: match[index + 1] }), {}),
|
|
91
|
+
body: bodyText ? JSON.parse(bodyText) : {},
|
|
92
|
+
}, response);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
response.status(500).json({ error: error?.message || 'Route failed' });
|
|
95
|
+
}
|
|
96
|
+
res.writeHead(response.statusCode, response.headers);
|
|
97
|
+
res.end(JSON.stringify(response.body));
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const register = (method, route, handler) => {
|
|
101
|
+
const params = [];
|
|
102
|
+
const escaped = route.replace(/:([^/]+)/g, (_, name) => {
|
|
103
|
+
params.push(name);
|
|
104
|
+
return '([^/]+)';
|
|
105
|
+
});
|
|
106
|
+
routes[method].push({
|
|
107
|
+
pattern: new RegExp(`^${escaped}$`),
|
|
108
|
+
params,
|
|
109
|
+
handler,
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
app.get = (route, handler) => register('GET', route, handler);
|
|
113
|
+
app.post = (route, handler) => register('POST', route, handler);
|
|
114
|
+
app.delete = (route, handler) => register('DELETE', route, handler);
|
|
115
|
+
return app;
|
|
116
|
+
}
|
|
117
|
+
|
|
27
118
|
function createRuntime(server, overrides = {}) {
|
|
28
119
|
const app = overrides.app ?? {
|
|
29
120
|
post() {},
|
|
@@ -54,6 +145,7 @@ describe('terminal runtime', () => {
|
|
|
54
145
|
const createHarness = (overrides = {}) => {
|
|
55
146
|
const routes = { get: new Map(), post: new Map(), delete: new Map() };
|
|
56
147
|
const processes = [];
|
|
148
|
+
const spawnDeferred = overrides.spawnDeferred ?? null;
|
|
57
149
|
const app = {
|
|
58
150
|
post(route, handler) { routes.post.set(route, handler); },
|
|
59
151
|
get(route, handler) { routes.get.set(route, handler); },
|
|
@@ -61,7 +153,8 @@ describe('terminal runtime', () => {
|
|
|
61
153
|
};
|
|
62
154
|
const loadPtyProvider = async () => ({
|
|
63
155
|
backend: 'fake-pty',
|
|
64
|
-
spawn: (shell, args, options) => {
|
|
156
|
+
spawn: async (shell, args, options) => {
|
|
157
|
+
await spawnDeferred?.promise;
|
|
65
158
|
const dataHandlers = new Set();
|
|
66
159
|
const exitHandlers = new Set();
|
|
67
160
|
const process = {
|
|
@@ -98,6 +191,45 @@ describe('terminal runtime', () => {
|
|
|
98
191
|
return { routes, processes, runtime };
|
|
99
192
|
};
|
|
100
193
|
|
|
194
|
+
it('replaces only exited action runs and keeps session capacity available across reruns', async () => {
|
|
195
|
+
const harness = createHarness();
|
|
196
|
+
try {
|
|
197
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
198
|
+
await create({ body: { sessionId: 'interactive', cwd: '/repo' } }, createResponse());
|
|
199
|
+
for (let index = 0; index < 25; index += 1) {
|
|
200
|
+
const id = `execution-${index}`;
|
|
201
|
+
const response = createResponse();
|
|
202
|
+
await create({ body: { sessionId: id, cwd: '/repo', mode: 'command', command: 'echo hello', purpose: { type: 'project-action', actionId: 'build', executionId: id } } }, response);
|
|
203
|
+
expect(response.statusCode).toBe(200);
|
|
204
|
+
const listed = createResponse();
|
|
205
|
+
harness.routes.get.get('/api/terminal/sessions')({ query: { cwd: '/repo' } }, listed);
|
|
206
|
+
expect(listed.body.sessions.map(session => session.sessionId)).toEqual(['interactive', id]);
|
|
207
|
+
expect(harness.processes[0].killed).toBe(false);
|
|
208
|
+
harness.processes.at(-1).emitExit(0);
|
|
209
|
+
}
|
|
210
|
+
} finally { await harness.runtime.shutdown(); }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('retains completed output when the replacement command fails to start', async () => {
|
|
214
|
+
let available = true;
|
|
215
|
+
const harness = createHarness({ fs: { promises: { stat: async () => ({ isDirectory: () => available }) } } });
|
|
216
|
+
try {
|
|
217
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
218
|
+
const options = { cwd: '/repo', mode: 'command', command: 'echo hello', purpose: { type: 'project-action', actionId: 'build', executionId: 'old' } };
|
|
219
|
+
await create({ body: { ...options, sessionId: 'old' } }, createResponse());
|
|
220
|
+
harness.processes[0].emitData('old output');
|
|
221
|
+
harness.processes[0].emitExit(0);
|
|
222
|
+
available = false;
|
|
223
|
+
const response = createResponse();
|
|
224
|
+
await create({ body: { ...options, sessionId: 'new', purpose: { ...options.purpose, executionId: 'new' } } }, response);
|
|
225
|
+
expect(response.statusCode).toBe(400);
|
|
226
|
+
const listed = createResponse();
|
|
227
|
+
harness.routes.get.get('/api/terminal/sessions')({ query: { cwd: '/repo' } }, listed);
|
|
228
|
+
expect(listed.body.sessions.map(session => session.sessionId)).toEqual(['old']);
|
|
229
|
+
expect(harness.processes).toHaveLength(1);
|
|
230
|
+
} finally { await harness.runtime.shutdown(); }
|
|
231
|
+
});
|
|
232
|
+
|
|
101
233
|
it('rejects regular files as terminal working directories', async () => {
|
|
102
234
|
const postRoutes = new Map();
|
|
103
235
|
const app = {
|
|
@@ -134,6 +266,40 @@ describe('terminal runtime', () => {
|
|
|
134
266
|
}
|
|
135
267
|
});
|
|
136
268
|
|
|
269
|
+
it('names a missing working directory so the client can recover the session', async () => {
|
|
270
|
+
let cwdMissing = false;
|
|
271
|
+
const harness = createHarness({
|
|
272
|
+
fs: {
|
|
273
|
+
promises: {
|
|
274
|
+
stat: async () => {
|
|
275
|
+
if (cwdMissing) throw Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
|
|
276
|
+
return { isDirectory: () => true };
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
try {
|
|
282
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
283
|
+
const created = createResponse();
|
|
284
|
+
await create({ body: { sessionId: 'worktree-terminal', cwd: '/repo/.worktrees/feature' } }, created);
|
|
285
|
+
expect(created.statusCode).toBe(200);
|
|
286
|
+
|
|
287
|
+
cwdMissing = true;
|
|
288
|
+
const recreated = createResponse();
|
|
289
|
+
await create({ body: { sessionId: 'worktree-terminal-2', cwd: '/repo/.worktrees/feature' } }, recreated);
|
|
290
|
+
expect(recreated.statusCode).toBe(400);
|
|
291
|
+
expect(recreated.body).toEqual({ error: 'Invalid working directory', code: 'TERMINAL_CWD_MISSING' });
|
|
292
|
+
|
|
293
|
+
const restarted = createResponse();
|
|
294
|
+
await harness.routes.post.get('/api/terminal/:sessionId/restart')(
|
|
295
|
+
{ params: { sessionId: 'worktree-terminal' }, body: { cwd: '/repo/.worktrees/feature' } },
|
|
296
|
+
restarted,
|
|
297
|
+
);
|
|
298
|
+
expect(restarted.statusCode).toBe(400);
|
|
299
|
+
expect(restarted.body).toEqual({ error: 'Invalid working directory', code: 'TERMINAL_CWD_MISSING' });
|
|
300
|
+
} finally { await harness.runtime.shutdown(); }
|
|
301
|
+
});
|
|
302
|
+
|
|
137
303
|
it('removes its websocket upgrade listener on shutdown', async () => {
|
|
138
304
|
const server = new EventEmitter();
|
|
139
305
|
const runtime = createRuntime(server);
|
|
@@ -150,7 +316,7 @@ describe('terminal runtime', () => {
|
|
|
150
316
|
try {
|
|
151
317
|
const response = createResponse();
|
|
152
318
|
await harness.routes.post.get('/api/terminal/create')({ body: { sessionId: 'term-1', cwd: '/repo', cols: 120, rows: 40, themeMode: 'light', terminalBackground: '#faf8f0', terminalForeground: '#1b1b1b' } }, response);
|
|
153
|
-
expect(response.body).toEqual({ sessionId: 'term-1', cols: 120, rows: 40, status: 'running' });
|
|
319
|
+
expect(response.body).toEqual({ sessionId: 'term-1', cols: 120, rows: 40, status: 'running', mode: 'interactive', purpose: { type: 'terminal' } });
|
|
154
320
|
expect(harness.processes[0].options.cwd).toBe('/repo');
|
|
155
321
|
expect(harness.processes[0].options.env.COLORFGBG).toBe('0;15');
|
|
156
322
|
expect(harness.processes[0].options.env.NODE_CHANNEL_FD).toBe('');
|
|
@@ -192,7 +358,7 @@ describe('terminal runtime', () => {
|
|
|
192
358
|
const scoped = createResponse();
|
|
193
359
|
harness.routes.get.get('/api/terminal/sessions')({ query: { cwd: '/repo' } }, scoped);
|
|
194
360
|
expect(scoped.body.sessions).toEqual([
|
|
195
|
-
{ sessionId: 'term-a', cwd: '/repo', status: 'running', createdAt: expect.any(Number) },
|
|
361
|
+
{ sessionId: 'term-a', cwd: '/repo', status: 'running', createdAt: expect.any(Number), mode: 'interactive', purpose: { type: 'terminal' } },
|
|
196
362
|
]);
|
|
197
363
|
|
|
198
364
|
const touch = createResponse();
|
|
@@ -207,7 +373,7 @@ describe('terminal runtime', () => {
|
|
|
207
373
|
|
|
208
374
|
it('strips AppImage ARGV0 from PTY child environments', async () => {
|
|
209
375
|
const previousArgv0 = process.env.ARGV0;
|
|
210
|
-
process.env.ARGV0 = '/path/to/
|
|
376
|
+
process.env.ARGV0 = '/path/to/AiWorker/AiWorker-1.17.2-linux-x86_64.AppImage';
|
|
211
377
|
const harness = createHarness();
|
|
212
378
|
try {
|
|
213
379
|
const response = createResponse();
|
|
@@ -466,8 +632,7 @@ describe('terminal runtime', () => {
|
|
|
466
632
|
});
|
|
467
633
|
|
|
468
634
|
it('runs snapshot-first attach, scoped I/O, replay, reconnect, and close over a real websocket', async () => {
|
|
469
|
-
const app =
|
|
470
|
-
app.use(express.json());
|
|
635
|
+
const app = createHttpTestApp();
|
|
471
636
|
const server = http.createServer(app);
|
|
472
637
|
const processes = [];
|
|
473
638
|
const loadPtyProvider = async () => ({
|
|
@@ -501,24 +666,6 @@ describe('terminal runtime', () => {
|
|
|
501
666
|
const socketUrl = `ws://127.0.0.1:${address.port}/api/terminal/ws`;
|
|
502
667
|
const sockets = [];
|
|
503
668
|
|
|
504
|
-
const open = async () => {
|
|
505
|
-
const socket = new WebSocket(socketUrl);
|
|
506
|
-
sockets.push(socket);
|
|
507
|
-
const messages = [];
|
|
508
|
-
socket.on('message', (raw) => messages.push(readTerminalWsControlFrame(raw)));
|
|
509
|
-
await new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); });
|
|
510
|
-
const next = async (type, sessionId) => {
|
|
511
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
512
|
-
const index = messages.findIndex((message) => message?.t === type && (!sessionId || message.s === sessionId));
|
|
513
|
-
if (index >= 0) return messages.splice(index, 1)[0];
|
|
514
|
-
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
515
|
-
}
|
|
516
|
-
throw new Error(`Timed out waiting for ${type}`);
|
|
517
|
-
};
|
|
518
|
-
await next('hello');
|
|
519
|
-
return { socket, next, messages };
|
|
520
|
-
};
|
|
521
|
-
|
|
522
669
|
try {
|
|
523
670
|
const created = await fetch(`${base}/api/terminal/create`, {
|
|
524
671
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
@@ -531,7 +678,8 @@ describe('terminal runtime', () => {
|
|
|
531
678
|
});
|
|
532
679
|
expect(secondCreated.status).toBe(200);
|
|
533
680
|
|
|
534
|
-
const first = await
|
|
681
|
+
const first = await openTerminalSocket(socketUrl);
|
|
682
|
+
sockets.push(first.socket);
|
|
535
683
|
first.socket.send(createTerminalWsControlFrame({ t: 'attach', v: 3, s: 'term-live' }));
|
|
536
684
|
first.socket.send(createTerminalWsControlFrame({ t: 'attach', v: 3, s: 'term-second' }));
|
|
537
685
|
expect(await first.next('snapshot', 'term-live')).toMatchObject({ s: 'term-live', q: 0, history: '', status: 'running' });
|
|
@@ -559,7 +707,8 @@ describe('terminal runtime', () => {
|
|
|
559
707
|
expect(secondClosed.status).toBe(200);
|
|
560
708
|
first.socket.close();
|
|
561
709
|
|
|
562
|
-
const second = await
|
|
710
|
+
const second = await openTerminalSocket(socketUrl);
|
|
711
|
+
sockets.push(second.socket);
|
|
563
712
|
second.socket.send(createTerminalWsControlFrame({ t: 'attach', v: 3, s: 'term-live' }));
|
|
564
713
|
expect(await second.next('snapshot')).toMatchObject({ s: 'term-live', q: 2, history: 'ok\r\n', status: 'running' });
|
|
565
714
|
processes[0].emitExit(7);
|
|
@@ -589,4 +738,382 @@ describe('terminal runtime', () => {
|
|
|
589
738
|
await new Promise((resolve) => server.close(resolve));
|
|
590
739
|
}
|
|
591
740
|
}, 15_000);
|
|
741
|
+
|
|
742
|
+
it('creates command-mode sessions and echoes the effective mode', async () => {
|
|
743
|
+
const harness = createHarness({
|
|
744
|
+
searchPathFor: (name) => name === 'bash' ? '/bin/bash' : '/bin/sh',
|
|
745
|
+
isExecutable: (candidate) => candidate === '/bin/bash' || candidate === '/bin/sh',
|
|
746
|
+
});
|
|
747
|
+
try {
|
|
748
|
+
const response = createResponse();
|
|
749
|
+
await harness.routes.post.get('/api/terminal/create')({ body: { sessionId: 'term-command', cwd: '/repo', mode: 'command', command: 'printf ready', shell: 'bash', loginShell: true } }, response);
|
|
750
|
+
|
|
751
|
+
expect(response.statusCode).toBe(200);
|
|
752
|
+
expect(response.body).toEqual({ sessionId: 'term-command', cols: 80, rows: 24, status: 'running', mode: 'command', purpose: { type: 'terminal' } });
|
|
753
|
+
if (process.platform === 'linux') {
|
|
754
|
+
expect(harness.processes[0].shell).toMatch(/\/env$/);
|
|
755
|
+
expect(harness.processes[0].args).toEqual(['-u', 'ARGV0', '/bin/bash', '-l', '-i', '-c', 'printf ready']);
|
|
756
|
+
} else {
|
|
757
|
+
expect(harness.processes[0].args).toEqual(['-l', '-i', '-c', 'printf ready']);
|
|
758
|
+
}
|
|
759
|
+
} finally { await harness.runtime.shutdown(); }
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('rejects invalid terminal mode and command combinations', async () => {
|
|
763
|
+
const harness = createHarness();
|
|
764
|
+
try {
|
|
765
|
+
for (const [body, error] of [
|
|
766
|
+
[{ cwd: '/repo', mode: 'script' }, 'Invalid terminal mode'],
|
|
767
|
+
[{ cwd: '/repo', mode: 'command' }, 'Terminal command is required'],
|
|
768
|
+
[{ cwd: '/repo', mode: 'command', command: ' ' }, 'Terminal command is required'],
|
|
769
|
+
[{ cwd: '/repo', mode: 'interactive', command: 'echo nope' }, 'Interactive terminal create does not accept a command'],
|
|
770
|
+
[{ cwd: '/repo', mode: 'command', command: 'x'.repeat(65_537) }, 'Terminal command exceeds the input limit'],
|
|
771
|
+
]) {
|
|
772
|
+
const response = createResponse();
|
|
773
|
+
await harness.routes.post.get('/api/terminal/create')({ body }, response);
|
|
774
|
+
expect(response.statusCode).toBe(400);
|
|
775
|
+
expect(response.body).toEqual({ error });
|
|
776
|
+
}
|
|
777
|
+
expect(harness.processes).toHaveLength(0);
|
|
778
|
+
} finally { await harness.runtime.shutdown(); }
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('rejects same-id running creates when mode or command do not match', async () => {
|
|
782
|
+
const harness = createHarness();
|
|
783
|
+
try {
|
|
784
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
785
|
+
await create({ body: { sessionId: 'term-shared', cwd: '/repo' } }, createResponse());
|
|
786
|
+
|
|
787
|
+
const modeMismatch = createResponse();
|
|
788
|
+
await create({ body: { sessionId: 'term-shared', cwd: '/repo', mode: 'command', command: 'printf ready' } }, modeMismatch);
|
|
789
|
+
expect(modeMismatch.statusCode).toBe(400);
|
|
790
|
+
expect(modeMismatch.body).toEqual({ error: 'Terminal session is already running with a different mode' });
|
|
791
|
+
|
|
792
|
+
await create({ body: { sessionId: 'term-command', cwd: '/repo', mode: 'command', command: 'printf ready' } }, createResponse());
|
|
793
|
+
|
|
794
|
+
const commandMismatch = createResponse();
|
|
795
|
+
await create({ body: { sessionId: 'term-command', cwd: '/repo', mode: 'command', command: 'printf other' } }, commandMismatch);
|
|
796
|
+
expect(commandMismatch.statusCode).toBe(400);
|
|
797
|
+
expect(commandMismatch.body).toEqual({ error: 'Terminal session is already running with a different command' });
|
|
798
|
+
} finally { await harness.runtime.shutdown(); }
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('rejects pending creates when command mode does not match the in-flight request', async () => {
|
|
802
|
+
const spawnDeferred = deferred();
|
|
803
|
+
const harness = createHarness({ spawnDeferred });
|
|
804
|
+
try {
|
|
805
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
806
|
+
const first = createResponse();
|
|
807
|
+
const conflictingMode = createResponse();
|
|
808
|
+
const conflictingCommand = createResponse();
|
|
809
|
+
|
|
810
|
+
const firstPromise = create({ body: { sessionId: 'term-pending', cwd: '/repo', mode: 'command', command: 'printf ready' } }, first);
|
|
811
|
+
await Promise.resolve();
|
|
812
|
+
const secondPromise = create({ body: { sessionId: 'term-pending', cwd: '/repo' } }, conflictingMode);
|
|
813
|
+
const thirdPromise = create({ body: { sessionId: 'term-pending', cwd: '/repo', mode: 'command', command: 'printf other' } }, conflictingCommand);
|
|
814
|
+
spawnDeferred.resolve();
|
|
815
|
+
await Promise.all([firstPromise, secondPromise, thirdPromise]);
|
|
816
|
+
|
|
817
|
+
expect(first.statusCode).toBe(200);
|
|
818
|
+
expect(conflictingMode.statusCode).toBe(400);
|
|
819
|
+
expect(conflictingMode.body).toEqual({ error: 'Terminal session is already being created with a different mode' });
|
|
820
|
+
expect(conflictingCommand.statusCode).toBe(400);
|
|
821
|
+
expect(conflictingCommand.body).toEqual({ error: 'Terminal session is already being created with a different command' });
|
|
822
|
+
expect(harness.processes).toHaveLength(1);
|
|
823
|
+
} finally { await harness.runtime.shutdown(); }
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('validates purpose payloads and round-trips purpose through create, list, and snapshot without listing command text', async () => {
|
|
827
|
+
const app = createHttpTestApp();
|
|
828
|
+
const server = http.createServer(app);
|
|
829
|
+
const runtime = createRuntime(server, {
|
|
830
|
+
app,
|
|
831
|
+
loadPtyProvider: async () => ({
|
|
832
|
+
backend: 'fake-pty',
|
|
833
|
+
spawn: async () => ({
|
|
834
|
+
pid: 42,
|
|
835
|
+
write() {},
|
|
836
|
+
resize() {},
|
|
837
|
+
kill() {},
|
|
838
|
+
onData() { return { dispose() {} }; },
|
|
839
|
+
onExit() { return { dispose() {} }; },
|
|
840
|
+
}),
|
|
841
|
+
}),
|
|
842
|
+
terminalTerminationGraceMs: 10,
|
|
843
|
+
fs: { promises: { stat: async () => ({ isDirectory: () => true }) } },
|
|
844
|
+
searchPathFor: () => '/bin/sh',
|
|
845
|
+
isExecutable: () => true,
|
|
846
|
+
});
|
|
847
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
848
|
+
const { port } = server.address();
|
|
849
|
+
const base = `http://127.0.0.1:${port}`;
|
|
850
|
+
const socketUrl = `ws://127.0.0.1:${port}/api/terminal/ws`;
|
|
851
|
+
const sockets = [];
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
for (const [body, error] of [
|
|
855
|
+
[{ cwd: '/repo', purpose: 'terminal' }, 'Invalid terminal purpose'],
|
|
856
|
+
[{ cwd: '/repo', purpose: { type: 'project-action' } }, 'Terminal project action id is required'],
|
|
857
|
+
[{ cwd: '/repo', purpose: { type: 'project-action', actionId: 'build' } }, 'Terminal execution id is required'],
|
|
858
|
+
[{ cwd: '/repo', purpose: { type: 'project-action', actionId: ' ', executionId: 'exec-1' } }, 'Terminal project action id is required'],
|
|
859
|
+
[{ cwd: '/repo', purpose: { type: 'project-action', actionId: 'build', executionId: ' ' } }, 'Terminal execution id is required'],
|
|
860
|
+
]) {
|
|
861
|
+
const response = await fetch(`${base}/api/terminal/create`, {
|
|
862
|
+
method: 'POST',
|
|
863
|
+
headers: { 'content-type': 'application/json' },
|
|
864
|
+
body: JSON.stringify(body),
|
|
865
|
+
});
|
|
866
|
+
expect(response.status).toBe(400);
|
|
867
|
+
expect(await response.json()).toEqual({ error });
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const created = await fetch(`${base}/api/terminal/create`, {
|
|
871
|
+
method: 'POST',
|
|
872
|
+
headers: { 'content-type': 'application/json' },
|
|
873
|
+
body: JSON.stringify({
|
|
874
|
+
sessionId: 'action-tab',
|
|
875
|
+
cwd: '/repo',
|
|
876
|
+
mode: 'command',
|
|
877
|
+
command: 'printf ready',
|
|
878
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
879
|
+
}),
|
|
880
|
+
});
|
|
881
|
+
expect(created.status).toBe(200);
|
|
882
|
+
expect(await created.json()).toEqual({
|
|
883
|
+
sessionId: 'action-tab',
|
|
884
|
+
cols: 80,
|
|
885
|
+
rows: 24,
|
|
886
|
+
status: 'running',
|
|
887
|
+
mode: 'command',
|
|
888
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const listed = await fetch(`${base}/api/terminal/sessions?cwd=%2Frepo`);
|
|
892
|
+
expect(listed.status).toBe(200);
|
|
893
|
+
expect(await listed.json()).toEqual({
|
|
894
|
+
sessions: [{
|
|
895
|
+
sessionId: 'action-tab',
|
|
896
|
+
cwd: '/repo',
|
|
897
|
+
status: 'running',
|
|
898
|
+
createdAt: expect.any(Number),
|
|
899
|
+
mode: 'command',
|
|
900
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
901
|
+
}],
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
const socket = await openTerminalSocket(socketUrl);
|
|
905
|
+
sockets.push(socket.socket);
|
|
906
|
+
socket.socket.send(createTerminalWsControlFrame({ t: 'attach', v: 3, s: 'action-tab' }));
|
|
907
|
+
expect(await socket.next('snapshot', 'action-tab')).toMatchObject({
|
|
908
|
+
s: 'action-tab',
|
|
909
|
+
status: 'running',
|
|
910
|
+
mode: 'command',
|
|
911
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
912
|
+
});
|
|
913
|
+
} finally {
|
|
914
|
+
for (const socket of sockets) socket.terminate();
|
|
915
|
+
await runtime.shutdown();
|
|
916
|
+
server.closeAllConnections?.();
|
|
917
|
+
await new Promise((resolve) => server.close(resolve));
|
|
918
|
+
}
|
|
919
|
+
}, 15_000);
|
|
920
|
+
|
|
921
|
+
it('deduplicates running project actions by resolved cwd and action id across session ids and clients', async () => {
|
|
922
|
+
const harness = createHarness();
|
|
923
|
+
try {
|
|
924
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
925
|
+
const first = createResponse();
|
|
926
|
+
await create({
|
|
927
|
+
body: {
|
|
928
|
+
sessionId: 'action-a',
|
|
929
|
+
cwd: '/repo/./nested/..',
|
|
930
|
+
mode: 'command',
|
|
931
|
+
command: 'npm run build',
|
|
932
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
933
|
+
},
|
|
934
|
+
}, first);
|
|
935
|
+
expect(first.statusCode).toBe(200);
|
|
936
|
+
|
|
937
|
+
const adopted = createResponse();
|
|
938
|
+
await create({
|
|
939
|
+
body: {
|
|
940
|
+
sessionId: 'action-b',
|
|
941
|
+
cwd: '/repo',
|
|
942
|
+
mode: 'command',
|
|
943
|
+
command: 'npm run build --watch',
|
|
944
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-2' },
|
|
945
|
+
},
|
|
946
|
+
}, adopted);
|
|
947
|
+
|
|
948
|
+
expect(adopted.statusCode).toBe(200);
|
|
949
|
+
expect(adopted.body).toEqual({
|
|
950
|
+
sessionId: 'action-a',
|
|
951
|
+
cols: 80,
|
|
952
|
+
rows: 24,
|
|
953
|
+
status: 'running',
|
|
954
|
+
mode: 'command',
|
|
955
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
956
|
+
});
|
|
957
|
+
expect(harness.processes).toHaveLength(1);
|
|
958
|
+
} finally { await harness.runtime.shutdown(); }
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('rejects purpose mismatches when the same session id is reused for a different action', async () => {
|
|
962
|
+
const harness = createHarness();
|
|
963
|
+
try {
|
|
964
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
965
|
+
await create({
|
|
966
|
+
body: {
|
|
967
|
+
sessionId: 'action-tab',
|
|
968
|
+
cwd: '/repo',
|
|
969
|
+
mode: 'command',
|
|
970
|
+
command: 'npm run build',
|
|
971
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
972
|
+
},
|
|
973
|
+
}, createResponse());
|
|
974
|
+
|
|
975
|
+
const mismatch = createResponse();
|
|
976
|
+
await create({
|
|
977
|
+
body: {
|
|
978
|
+
sessionId: 'action-tab',
|
|
979
|
+
cwd: '/repo',
|
|
980
|
+
mode: 'command',
|
|
981
|
+
command: 'npm run test',
|
|
982
|
+
purpose: { type: 'project-action', actionId: 'test', executionId: 'exec-2' },
|
|
983
|
+
},
|
|
984
|
+
}, mismatch);
|
|
985
|
+
|
|
986
|
+
expect(mismatch.statusCode).toBe(400);
|
|
987
|
+
expect(mismatch.body).toEqual({ error: 'Terminal session is already running with a different purpose' });
|
|
988
|
+
} finally { await harness.runtime.shutdown(); }
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it('keeps an immediately exited command session attachable once listeners are registered', async () => {
|
|
992
|
+
const app = createHttpTestApp();
|
|
993
|
+
const server = http.createServer(app);
|
|
994
|
+
const runtime = createRuntime(server, {
|
|
995
|
+
app,
|
|
996
|
+
loadPtyProvider: async () => ({
|
|
997
|
+
backend: 'fake-pty',
|
|
998
|
+
spawn: async () => {
|
|
999
|
+
const dataHandlers = new Set();
|
|
1000
|
+
const exitHandlers = new Set();
|
|
1001
|
+
return {
|
|
1002
|
+
pid: 404,
|
|
1003
|
+
write() {},
|
|
1004
|
+
resize() {},
|
|
1005
|
+
kill() {},
|
|
1006
|
+
onData(handler) { dataHandlers.add(handler); return { dispose: () => dataHandlers.delete(handler) }; },
|
|
1007
|
+
onExit(handler) {
|
|
1008
|
+
exitHandlers.add(handler);
|
|
1009
|
+
queueMicrotask(() => {
|
|
1010
|
+
for (const registered of exitHandlers) registered({ exitCode: 0, signal: 0 });
|
|
1011
|
+
});
|
|
1012
|
+
return { dispose: () => exitHandlers.delete(handler) };
|
|
1013
|
+
},
|
|
1014
|
+
};
|
|
1015
|
+
},
|
|
1016
|
+
}),
|
|
1017
|
+
terminalTerminationGraceMs: 10,
|
|
1018
|
+
fs: { promises: { stat: async () => ({ isDirectory: () => true }) } },
|
|
1019
|
+
searchPathFor: () => '/bin/sh',
|
|
1020
|
+
isExecutable: () => true,
|
|
1021
|
+
});
|
|
1022
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
1023
|
+
const { port } = server.address();
|
|
1024
|
+
const base = `http://127.0.0.1:${port}`;
|
|
1025
|
+
const socketUrl = `ws://127.0.0.1:${port}/api/terminal/ws`;
|
|
1026
|
+
const sockets = [];
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
const created = await fetch(`${base}/api/terminal/create`, {
|
|
1030
|
+
method: 'POST',
|
|
1031
|
+
headers: { 'content-type': 'application/json' },
|
|
1032
|
+
body: JSON.stringify({
|
|
1033
|
+
sessionId: 'fast-exit',
|
|
1034
|
+
cwd: '/repo',
|
|
1035
|
+
mode: 'command',
|
|
1036
|
+
command: 'true',
|
|
1037
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-fast' },
|
|
1038
|
+
}),
|
|
1039
|
+
});
|
|
1040
|
+
expect(created.status).toBe(200);
|
|
1041
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1042
|
+
|
|
1043
|
+
const socket = await openTerminalSocket(socketUrl);
|
|
1044
|
+
sockets.push(socket.socket);
|
|
1045
|
+
socket.socket.send(createTerminalWsControlFrame({ t: 'attach', v: 3, s: 'fast-exit' }));
|
|
1046
|
+
expect(await socket.next('snapshot', 'fast-exit')).toMatchObject({
|
|
1047
|
+
s: 'fast-exit',
|
|
1048
|
+
status: 'exited',
|
|
1049
|
+
exitCode: 0,
|
|
1050
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-fast' },
|
|
1051
|
+
});
|
|
1052
|
+
} finally {
|
|
1053
|
+
for (const socket of sockets) socket.terminate();
|
|
1054
|
+
await runtime.shutdown();
|
|
1055
|
+
server.closeAllConnections?.();
|
|
1056
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1057
|
+
}
|
|
1058
|
+
}, 15_000);
|
|
1059
|
+
|
|
1060
|
+
it('tombstones a pending create when delete arrives first and kills the eventual pty', async () => {
|
|
1061
|
+
const spawnDeferred = deferred();
|
|
1062
|
+
const harness = createHarness({ spawnDeferred });
|
|
1063
|
+
try {
|
|
1064
|
+
const create = harness.routes.post.get('/api/terminal/create');
|
|
1065
|
+
const close = harness.routes.delete.get('/api/terminal/:sessionId');
|
|
1066
|
+
const created = createResponse();
|
|
1067
|
+
const closed = createResponse();
|
|
1068
|
+
|
|
1069
|
+
const createPromise = create({
|
|
1070
|
+
body: {
|
|
1071
|
+
sessionId: 'pending-action',
|
|
1072
|
+
cwd: '/repo',
|
|
1073
|
+
mode: 'command',
|
|
1074
|
+
command: 'npm run build',
|
|
1075
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-pending' },
|
|
1076
|
+
},
|
|
1077
|
+
}, created);
|
|
1078
|
+
await Promise.resolve();
|
|
1079
|
+
|
|
1080
|
+
const closePromise = close({ params: { sessionId: 'pending-action' } }, closed);
|
|
1081
|
+
spawnDeferred.resolve();
|
|
1082
|
+
await Promise.all([createPromise, closePromise]);
|
|
1083
|
+
|
|
1084
|
+
expect(closed.statusCode).toBe(200);
|
|
1085
|
+
expect(closed.body).toEqual({ success: true });
|
|
1086
|
+
expect(created.statusCode).toBe(400);
|
|
1087
|
+
expect(created.body).toEqual({ error: 'Terminal session was closed during creation' });
|
|
1088
|
+
|
|
1089
|
+
const listed = createResponse();
|
|
1090
|
+
harness.routes.get.get('/api/terminal/sessions')({ query: {} }, listed);
|
|
1091
|
+
expect(listed.body).toEqual({ sessions: [] });
|
|
1092
|
+
expect(harness.processes).toHaveLength(1);
|
|
1093
|
+
expect(harness.processes[0].killed).toBe(true);
|
|
1094
|
+
} finally { await harness.runtime.shutdown(); }
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it('rejects restart for command-mode sessions', async () => {
|
|
1098
|
+
const harness = createHarness();
|
|
1099
|
+
try {
|
|
1100
|
+
await harness.routes.post.get('/api/terminal/create')({
|
|
1101
|
+
body: {
|
|
1102
|
+
sessionId: 'action-tab',
|
|
1103
|
+
cwd: '/repo',
|
|
1104
|
+
mode: 'command',
|
|
1105
|
+
command: 'npm run build',
|
|
1106
|
+
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
|
1107
|
+
},
|
|
1108
|
+
}, createResponse());
|
|
1109
|
+
|
|
1110
|
+
const restarted = createResponse();
|
|
1111
|
+
await harness.routes.post.get('/api/terminal/:sessionId/restart')({ params: { sessionId: 'action-tab' }, body: {} }, restarted);
|
|
1112
|
+
|
|
1113
|
+
expect(restarted.statusCode).toBe(400);
|
|
1114
|
+
expect(restarted.body).toEqual({ error: 'Command-mode terminal sessions cannot be restarted' });
|
|
1115
|
+
expect(harness.processes).toHaveLength(1);
|
|
1116
|
+
expect(harness.processes[0].killed).toBe(false);
|
|
1117
|
+
} finally { await harness.runtime.shutdown(); }
|
|
1118
|
+
});
|
|
592
1119
|
});
|