@vantran-se/openchamber-web 1.24.3 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-BjJHrzmU.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
- package/dist/assets/DiffView-CjxUovbM.js +4 -0
- package/dist/assets/FilesView-DFWD0RQR.js +17 -0
- package/dist/assets/{GitView-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-D0C9nU7K.js → JsonTreeView-3sGwqexh.js} +1 -1
- package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
- package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
- package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
- package/dist/assets/{PatchDiff-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-DyxZVVVR.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
- package/dist/assets/PlanView-BCk_VobP.js +5 -0
- package/dist/assets/PluginPane-BPP1dbck.js +3 -0
- package/dist/assets/SettingsView-kMmrpn43.js +29 -0
- package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
- package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
- package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
- package/dist/assets/de-CndEg67T.js +10 -0
- package/dist/assets/es-DglvPlaC.js +16 -0
- package/dist/assets/fr-DejPCpbS.js +14 -0
- package/dist/assets/index-C2r64QfD.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-CL2vsrIZ.js → isFilePlainText-B-mGgeKl.js} +1 -1
- package/dist/assets/ja-CVpQawCd.js +16 -0
- package/dist/assets/ko-Bw446h9j.js +16 -0
- package/dist/assets/main-Bz4WWXbr.js +2 -0
- package/dist/assets/main-oreqDsOV.js +896 -0
- package/dist/assets/{markdownCore-Dh7WEsdM.js → markdownCore-D2e5sp9y.js} +4 -4
- package/dist/assets/miniChat-D1v2gsBI.js +2 -0
- package/dist/assets/mobile-DBj3lCf2.js +2 -0
- package/dist/assets/{patchFileDiff-Bsba8KFp.js → patchFileDiff-BpeUOV91.js} +3 -3
- package/dist/assets/pl-BYNlPdhm.js +16 -0
- package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
- package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
- package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
- package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
- package/dist/assets/{shikiHighlight-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
- package/dist/assets/tr-DzmbxLy-.js +7 -0
- package/dist/assets/uk-C7vQnwro.js +16 -0
- package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
- package/dist/assets/{useInlineCommentController-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-B8IcVQAA.js → web-components-B6tKQyJN.js} +2 -2
- package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
- package/dist/assets/zh-TW-DtII5bDb.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-COVQhWGd.js +0 -4
- package/dist/assets/FilesView-B1V5JxFX.js +0 -13
- package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
- package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
- package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
- package/dist/assets/PlanView-LW2z_EAP.js +0 -5
- package/dist/assets/PluginPane-B4tKno2h.js +0 -3
- package/dist/assets/SettingsView-DEaMulN9.js +0 -29
- package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
- package/dist/assets/de-DgxgFyGG.js +0 -10
- package/dist/assets/es-BFsZj5F_.js +0 -16
- package/dist/assets/fr-jdZ5KKQi.js +0 -14
- package/dist/assets/index-CKHjPKbq.js +0 -1
- package/dist/assets/index-Cbci4i1W.js +0 -40
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/ja-C5kKrhO4.js +0 -16
- package/dist/assets/ko-DXDTT_0O.js +0 -16
- package/dist/assets/main-CZALtVek.js +0 -896
- package/dist/assets/main-CgXMBNvG.js +0 -2
- package/dist/assets/miniChat-CwTmDixi.js +0 -2
- package/dist/assets/mobile-CLf2vCtO.js +0 -2
- package/dist/assets/pl-D-7x4Y8Q.js +0 -16
- package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
- package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
- package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
- package/dist/assets/tr-Dr-oJ22L.js +0 -7
- package/dist/assets/uk-9Nx83UcI.js +0 -16
- package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
- package/dist/assets/useRootScrollLock-DdvHf9Pt.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-Bxy_Hj3r.js +0 -16
- package/dist/assets/zh-TW-0BB4gpHM.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// Code in: the project's code travels from the user's repository into a space, as git objects over
|
|
2
|
+
// the place's exec channel. No network, no ports, no archives, no shared folders. The host drives
|
|
3
|
+
// every step. The command sequences follow docs/isolated-spaces/stage-0/e4-git-over-exec.md.
|
|
4
|
+
//
|
|
5
|
+
// The space is hostile from the first moment, the receiving side of every push included: it can
|
|
6
|
+
// refuse, hang, lie about what it has, or print a lot. None of that may change the user's
|
|
7
|
+
// repository or leave a process running on the host.
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { SpaceError } from './errors.js';
|
|
14
|
+
import { tail } from './exec-http.js';
|
|
15
|
+
import { isGitVersionAtLeast, parseGitVersion } from './host-git.js';
|
|
16
|
+
import { requireSpaceId } from './labels.js';
|
|
17
|
+
import { IMAGE_GIT, IMAGE_ONLY_PATH, IMAGE_SH, IMAGE_TIMEOUT, requireSpaceProjectPath, spaceHistoryPath, spaceProjectPath } from './layout.js';
|
|
18
|
+
|
|
19
|
+
const SNAPSHOT_TIMEOUT_MS = 10 * 60_000;
|
|
20
|
+
const LIST_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
const TRANSFER_TIMEOUT_MS = 10 * 60_000;
|
|
22
|
+
const HISTORY_TIMEOUT_MS = 30 * 60_000;
|
|
23
|
+
// A push prints nothing with `--quiet`. What it does print comes from the receiving side, which
|
|
24
|
+
// is the space. More than this, and the transfer is killed like a hanging one.
|
|
25
|
+
const PUSH_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
26
|
+
// The limit inside is the host's limit plus this margin, so it never ends a push the host still
|
|
27
|
+
// waits for, and a push the host killed leaves nothing running inside once it passes.
|
|
28
|
+
const INNER_MARGIN_SECONDS = 5;
|
|
29
|
+
const MAX_INNER_MARGIN_SECONDS = 3600;
|
|
30
|
+
const INSIDE_TIMEOUT_MS = 5 * 60_000;
|
|
31
|
+
// The oldest host git code in works with. `git init --object-format` first appears in the git-init
|
|
32
|
+
// manual of 2.27.0; `rev-parse --show-object-format` and `add --pathspec-from-file` in 2.25.0; every
|
|
33
|
+
// other option used here is older. See DOCUMENTATION.md, "Host git".
|
|
34
|
+
const GIT_VERSION_FLOOR = Object.freeze({ major: 2, minor: 27, patch: 0 });
|
|
35
|
+
const GIT_VERSION_FLOOR_TEXT = '2.27';
|
|
36
|
+
|
|
37
|
+
// The snapshot commits carry a fixed identity, so a user without `user.name` can still create a space.
|
|
38
|
+
const SNAPSHOT_IDENTITY = Object.freeze({
|
|
39
|
+
GIT_AUTHOR_NAME: 'OpenChamber',
|
|
40
|
+
GIT_AUTHOR_EMAIL: 'spaces@openchamber.invalid',
|
|
41
|
+
GIT_COMMITTER_NAME: 'OpenChamber',
|
|
42
|
+
GIT_COMMITTER_EMAIL: 'spaces@openchamber.invalid',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const MODES = new Set(['uncommitted', 'clean']);
|
|
46
|
+
// A git `ext::` URL splits on spaces itself and cannot carry these at all.
|
|
47
|
+
const EXT_UNCARRIABLE = /[\x00-\x1f\x7f]/;
|
|
48
|
+
// A value that is copied into the space's git config: one line, and short.
|
|
49
|
+
const COPYABLE_VALUE = /^[^\x00-\x1f\x7f]{1,256}$/;
|
|
50
|
+
|
|
51
|
+
// Refs inside the space. The host pushes only here, never to refs/heads/*.
|
|
52
|
+
const INSIDE_BASE = 'refs/openchamber/base';
|
|
53
|
+
const INSIDE_START_INDEX = 'refs/openchamber/start-index';
|
|
54
|
+
const INSIDE_START = 'refs/openchamber/start';
|
|
55
|
+
|
|
56
|
+
const spaceRefPrefix = (spaceId) => `refs/openchamber/spaces/${requireSpaceId(spaceId)}/`;
|
|
57
|
+
const startRef = (spaceId) => `${spaceRefPrefix(spaceId)}start`;
|
|
58
|
+
|
|
59
|
+
// The fixed scripts that run inside the space. Every value is a positional argument.
|
|
60
|
+
// Exit code 3 of the init script means the project path is already there.
|
|
61
|
+
const PATH_IN_USE_EXIT_CODE = 3;
|
|
62
|
+
// The repository starts on the host's branch, so no stray `master` shows up in the agent's reflog.
|
|
63
|
+
// `--initial-branch` needs git 2.28 inside; the image has 2.39.5. A detached host HEAD keeps git's default.
|
|
64
|
+
const INIT_SCRIPT = [
|
|
65
|
+
IMAGE_ONLY_PATH,
|
|
66
|
+
`{ [ ! -e "$1" ] && [ ! -L "$1" ]; } || exit ${PATH_IN_USE_EXIT_CODE};`,
|
|
67
|
+
'if [ -n "$3" ]; then git init --quiet --object-format="$2" --initial-branch="$3" "$1"; else git init --quiet --object-format="$2" "$1"; fi',
|
|
68
|
+
'&& git -C "$1" config receive.shallowUpdate true',
|
|
69
|
+
].join(' ');
|
|
70
|
+
// Prints `true` or `false`: whether the space's repository still waits for its history.
|
|
71
|
+
const IS_SHALLOW_SCRIPT = `${IMAGE_ONLY_PATH} cd "$1" && git rev-parse --is-shallow-repository`;
|
|
72
|
+
// Identity first, so the reflog of the checkout carries it. Then the working tree becomes the
|
|
73
|
+
// start snapshot, the branch moves back to the base, and the index becomes the staged snapshot.
|
|
74
|
+
const UNFOLD_SCRIPT = [
|
|
75
|
+
IMAGE_ONLY_PATH,
|
|
76
|
+
'cd "$1" || exit 1;',
|
|
77
|
+
'if [ -n "$3" ]; then git config user.name "$3" || exit 1; fi;',
|
|
78
|
+
'if [ -n "$4" ]; then git config user.email "$4" || exit 1; fi;',
|
|
79
|
+
`if [ -n "$2" ]; then git checkout --quiet -B "$2" ${INSIDE_START}; else git checkout --quiet --detach ${INSIDE_START}; fi || exit 1;`,
|
|
80
|
+
`git reset --quiet --soft ${INSIDE_BASE} && git read-tree ${INSIDE_START_INDEX}`,
|
|
81
|
+
].join(' ');
|
|
82
|
+
// The side repository is made fresh: whatever the agent left under that name goes first.
|
|
83
|
+
const INIT_SIDE_SCRIPT = `${IMAGE_ONLY_PATH} rm -rf "$1" && git init --quiet --bare --object-format="$2" "$1"`;
|
|
84
|
+
// The fetch has a limit of its own inside, because the host's limit on `exec` ends the docker
|
|
85
|
+
// CLI only. The side repository goes whatever the fetch did.
|
|
86
|
+
const UNSHALLOW_SCRIPT = [
|
|
87
|
+
IMAGE_ONLY_PATH,
|
|
88
|
+
'cd "$1" || { rm -rf "$2"; exit 1; };',
|
|
89
|
+
`timeout -s KILL "$3" git fetch --quiet --unshallow --no-tags "$2" +${INSIDE_BASE}:${INSIDE_BASE};`,
|
|
90
|
+
'status=$?; rm -rf "$2"; exit $status',
|
|
91
|
+
].join(' ');
|
|
92
|
+
const REMOVE_SIDE_SCRIPT = `${IMAGE_ONLY_PATH} rm -rf "$1"`;
|
|
93
|
+
|
|
94
|
+
// One line of git output, without its line end.
|
|
95
|
+
const line = (text) => text.replace(/\r?\n$/, '');
|
|
96
|
+
const zeroObjectId = (objectFormat) => '0'.repeat(objectFormat === 'sha256' ? 64 : 40);
|
|
97
|
+
const objectIdPattern = (objectFormat) => (objectFormat === 'sha256' ? /^[0-9a-f]{64}$/ : /^[0-9a-f]{40}$/);
|
|
98
|
+
const innerSeconds = (timeoutMs, marginSeconds) => Math.ceil(timeoutMs / 1000) + marginSeconds;
|
|
99
|
+
|
|
100
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
101
|
+
const MAX_TIMEOUT_MS = 24 * 60 * 60_000;
|
|
102
|
+
|
|
103
|
+
/** The host's limit on a push, in whole milliseconds from a second to a day. */
|
|
104
|
+
const requireTimeout = (value) => {
|
|
105
|
+
if (!Number.isInteger(value) || value < MIN_TIMEOUT_MS || value > MAX_TIMEOUT_MS) {
|
|
106
|
+
throw new SpaceError('invalid_timeout', `The time limit of a transfer is a whole number of milliseconds from ${MIN_TIMEOUT_MS} to ${MAX_TIMEOUT_MS}`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** The margin between the host's limit and the one inside, in whole seconds: a length, and nothing else. */
|
|
112
|
+
const requireInnerMargin = (value) => {
|
|
113
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_INNER_MARGIN_SECONDS) {
|
|
114
|
+
throw new SpaceError('invalid_inner_margin', `The margin of the limit inside the space is a whole number of seconds from 1 to ${MAX_INNER_MARGIN_SECONDS}`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A git `ext::` URL for an argv. git-remote-ext splits the text after `ext::` on spaces itself, so a
|
|
121
|
+
* space inside an argument is written `% ` and a percent `%%`, as its documentation says. The docker
|
|
122
|
+
* CLI on Windows can live under a path with spaces. An empty argument or one with a control
|
|
123
|
+
* character cannot be carried, and is refused. The URL goes to git as one argv element, never
|
|
124
|
+
* through a shell.
|
|
125
|
+
*/
|
|
126
|
+
export function buildExtUrl(argv) {
|
|
127
|
+
if (!Array.isArray(argv) || argv.length === 0) {
|
|
128
|
+
throw new SpaceError('invalid_ext_argument', 'A command for git is a non-empty list of arguments');
|
|
129
|
+
}
|
|
130
|
+
const escaped = argv.map((argument) => {
|
|
131
|
+
const text = String(argument);
|
|
132
|
+
if (text === '' || EXT_UNCARRIABLE.test(text)) {
|
|
133
|
+
throw new SpaceError('invalid_ext_argument', 'A command for git cannot carry an empty argument or a control character');
|
|
134
|
+
}
|
|
135
|
+
return text.replaceAll('%', '%%').replaceAll(' ', '% ');
|
|
136
|
+
});
|
|
137
|
+
return `ext::${escaped.join(' ')}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The receiving side of a push into the space: fixed programs by absolute path, under the space's own time limit. */
|
|
141
|
+
const receivePackUrl = (execArgv, seconds, target) => buildExtUrl([
|
|
142
|
+
...execArgv, IMAGE_TIMEOUT, '-s', 'KILL', String(seconds), IMAGE_GIT, 'receive-pack', target,
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `git` is a host git from `createHostGit`, `place` the place the space lives on. `temporaryDirectory`
|
|
147
|
+
* is where the index copy, the sending repository and an empty hooks folder live while one call runs.
|
|
148
|
+
*/
|
|
149
|
+
// What `bringCodeIn` passes through as it is: the refusals of the snapshot, then its own codes after
|
|
150
|
+
// it. Everything else, a failure of the place or of the runner among it, becomes code_transfer_failed
|
|
151
|
+
// with that code as `details.cause`.
|
|
152
|
+
const SNAPSHOT_REFUSALS = new Set([
|
|
153
|
+
'invalid_space_id', 'invalid_snapshot_mode', 'git_version_unreadable', 'git_too_old', 'project_folder_missing',
|
|
154
|
+
'not_a_git_work_tree', 'repository_has_no_commit', 'repository_has_unmerged_changes', 'space_ref_exists',
|
|
155
|
+
'invalid_branch_name', 'project_folder_does_not_travel', 'git_command_failed',
|
|
156
|
+
]);
|
|
157
|
+
const BRING_CODES = new Set(['code_transfer_failed', 'code_unfold_failed', 'space_path_in_use', 'git_command_failed']);
|
|
158
|
+
|
|
159
|
+
/** `error` as `bringCodeIn` rejects with it: as it is when `passing` names its code, else as code_transfer_failed. */
|
|
160
|
+
const asBringFailure = (error, passing, step) => {
|
|
161
|
+
const ours = error instanceof SpaceError;
|
|
162
|
+
if (ours && passing.has(error.code)) return error;
|
|
163
|
+
return new SpaceError(
|
|
164
|
+
'code_transfer_failed',
|
|
165
|
+
ours ? error.message : `Code in failed on the host: ${error.message}`,
|
|
166
|
+
{ step: ours ? step : 'host side', ...error.details, cause: error.code ?? null },
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// History runs under way in this process, by space id: `{ spacePath, base, checkout, promise }`. At module
|
|
171
|
+
// level, so two `createCodeIn` instances share them too. Two processes are not coordinated.
|
|
172
|
+
const historyRuns = new Map();
|
|
173
|
+
const OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
174
|
+
|
|
175
|
+
/** `error` as `sendHistory` rejects with it: always history_transfer_failed, with a step and the root cause. */
|
|
176
|
+
const asHistoryFailure = (error, step) => {
|
|
177
|
+
if (error instanceof SpaceError && error.code === 'history_transfer_failed') return error;
|
|
178
|
+
const ours = error instanceof SpaceError;
|
|
179
|
+
// A failed push already names its own step and cause.
|
|
180
|
+
const details = ours && error.code === 'code_transfer_failed' ? error.details : { step, ...error.details, cause: error.code ?? null };
|
|
181
|
+
return new SpaceError('history_transfer_failed', ours ? error.message : `The history failed on the host: ${error.message}`, details);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const removeWithRetries = (directory) => fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
185
|
+
|
|
186
|
+
export function createCodeIn({ git, place, temporaryDirectory = os.tmpdir(), removeDirectory = removeWithRetries }) {
|
|
187
|
+
/**
|
|
188
|
+
* Runs `work(g, directory)` with a temporary directory and a host git `g` that starts nothing of
|
|
189
|
+
* the user's. `core.hooksPath` points at an empty folder of ours: without it the user's
|
|
190
|
+
* `post-index-change` hook ran three times per snapshot, with `GIT_INDEX_FILE` pointing at our
|
|
191
|
+
* copy, and their `reference-transaction` hook ran on every write of our ref. `core.fsmonitor` is
|
|
192
|
+
* empty: a user's fsmonitor program ran ten times per snapshot, and `core.fsmonitor=true` left a
|
|
193
|
+
* `git fsmonitor--daemon` running on the host with its files in the user's `.git`. Empty, and not
|
|
194
|
+
* `false`, because before git 2.36 the value is a command path, where `false` names a program.
|
|
195
|
+
*
|
|
196
|
+
* A failure that is not a SpaceError becomes one with `failureCode`. Removing the folder is retried,
|
|
197
|
+
* and it never replaces the error of the work: when it still fails, the error says which folder was
|
|
198
|
+
* left in `details.temporaryDirectoryLeft`. After a successful call a leftover folder is let go.
|
|
199
|
+
*/
|
|
200
|
+
const withHostGit = async (failureCode, work) => {
|
|
201
|
+
let directory;
|
|
202
|
+
try {
|
|
203
|
+
directory = await fs.mkdtemp(path.join(temporaryDirectory, 'openchamber-code-in-'));
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new SpaceError(failureCode, `Could not make a temporary folder for code in: ${error.message}`, { step: 'make a temporary folder', cause: error.code ?? null });
|
|
206
|
+
}
|
|
207
|
+
let outcome;
|
|
208
|
+
let failed = false;
|
|
209
|
+
try {
|
|
210
|
+
const hooks = path.join(directory, 'no-hooks');
|
|
211
|
+
await fs.mkdir(hooks);
|
|
212
|
+
const isolated = ['-c', `core.hooksPath=${hooks.replaceAll('\\', '/')}`, '-c', 'core.fsmonitor='];
|
|
213
|
+
const g = {
|
|
214
|
+
run: (where, args, options) => git.run(where, [...isolated, ...args], options),
|
|
215
|
+
output: (where, args, options) => git.output(where, [...isolated, ...args], options),
|
|
216
|
+
};
|
|
217
|
+
outcome = await work(g, directory);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
failed = true;
|
|
220
|
+
outcome = error instanceof SpaceError
|
|
221
|
+
? error
|
|
222
|
+
: new SpaceError(failureCode, `Code in failed on the host: ${error.message}`, { step: 'host side', cause: error.code ?? null });
|
|
223
|
+
}
|
|
224
|
+
const left = await removeDirectory(directory).then(() => false, () => true);
|
|
225
|
+
if (failed) {
|
|
226
|
+
if (left) outcome.details = { ...outcome.details, temporaryDirectoryLeft: directory };
|
|
227
|
+
throw outcome;
|
|
228
|
+
}
|
|
229
|
+
return outcome;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Refuses a host git older than the floor, with the version it needs. */
|
|
233
|
+
const requireGitVersion = async (g, directory) => {
|
|
234
|
+
const unreadable = (cause) => new SpaceError('git_version_unreadable', 'Could not tell which git this computer has. Code in needs git to be installed and working.', { cause });
|
|
235
|
+
let result;
|
|
236
|
+
try {
|
|
237
|
+
result = await g.run(directory, ['version']);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// A git that cannot be started at all, missing among them.
|
|
240
|
+
throw unreadable(error.code ?? null);
|
|
241
|
+
}
|
|
242
|
+
const version = result.code === 0 ? parseGitVersion(result.stdout) : null;
|
|
243
|
+
if (!version) {
|
|
244
|
+
throw unreadable(null);
|
|
245
|
+
}
|
|
246
|
+
if (!isGitVersionAtLeast(version, GIT_VERSION_FLOOR)) {
|
|
247
|
+
throw new SpaceError('git_too_old', `Code in needs git ${GIT_VERSION_FLOOR_TEXT} or newer, and this computer has git ${version.major}.${version.minor}.${version.patch}. Update git, then create the space.`, { version });
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** The top level of the work tree that `repository` is in, as a native path, or a refusal. */
|
|
252
|
+
const requireWorkTree = async (g, repository) => {
|
|
253
|
+
const found = await fs.stat(repository).then((stats) => stats.isDirectory(), () => false);
|
|
254
|
+
if (!found) {
|
|
255
|
+
throw new SpaceError('project_folder_missing', `${repository} does not exist, or is not a folder.`);
|
|
256
|
+
}
|
|
257
|
+
const inside = await g.run(repository, ['rev-parse', '--is-inside-work-tree']);
|
|
258
|
+
if (inside.code !== 0 || line(inside.stdout) !== 'true') {
|
|
259
|
+
throw new SpaceError('not_a_git_work_tree', `${repository} is not inside a git working tree. Code can travel into a space only from a git repository.`);
|
|
260
|
+
}
|
|
261
|
+
// git answers with forward slashes on Windows too.
|
|
262
|
+
return path.resolve(line(await g.output(repository, ['rev-parse', '--show-toplevel'])));
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** What a transfer needs to know about the repository, after the refusals that come first. */
|
|
266
|
+
const inspectRepository = async (g, repository) => {
|
|
267
|
+
const top = await requireWorkTree(g, repository);
|
|
268
|
+
const head = await g.run(top, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}']);
|
|
269
|
+
if (head.code !== 0) {
|
|
270
|
+
throw new SpaceError('repository_has_no_commit', 'The repository has no commit yet. Make a first commit, then create the space.');
|
|
271
|
+
}
|
|
272
|
+
if ((await g.output(top, ['ls-files', '--unmerged', '-z'], { maxOutputBytes: LIST_MAX_OUTPUT_BYTES })) !== '') {
|
|
273
|
+
throw new SpaceError('repository_has_unmerged_changes', 'The repository is in the middle of a merge or a rebase with unresolved conflicts. Finish or abort it, then create the space.');
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
top,
|
|
277
|
+
prefix: line(await g.output(repository, ['rev-parse', '--show-prefix'])),
|
|
278
|
+
base: line(head.stdout),
|
|
279
|
+
objectFormat: line(await g.output(top, ['rev-parse', '--show-object-format'])),
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** The host's current branch, checked by git's own rule, or null for a detached HEAD. */
|
|
284
|
+
const readBranch = async (g, top) => {
|
|
285
|
+
const head = await g.run(top, ['symbolic-ref', '--quiet', 'HEAD']);
|
|
286
|
+
const full = head.code === 0 ? line(head.stdout) : '';
|
|
287
|
+
if (!full.startsWith('refs/heads/')) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const name = full.slice('refs/heads/'.length);
|
|
291
|
+
const check = await g.run(top, ['check-ref-format', '--branch', name]);
|
|
292
|
+
if (check.code !== 0 || line(check.stdout) !== name || name.startsWith('-') || EXT_UNCARRIABLE.test(name)) {
|
|
293
|
+
throw new SpaceError('invalid_branch_name', 'The name of the current branch is not one git accepts as a branch name.');
|
|
294
|
+
}
|
|
295
|
+
return name;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The user's name and email from their git config, each only when it is one short line. Nothing
|
|
300
|
+
* else travels, and nothing is guessed: git's own guess would put the host's machine name into the space.
|
|
301
|
+
*/
|
|
302
|
+
const readIdentity = async (g, top) => {
|
|
303
|
+
const value = async (key) => {
|
|
304
|
+
const result = await g.run(top, ['config', '--get', key]);
|
|
305
|
+
const text = result.code === 0 ? line(result.stdout) : '';
|
|
306
|
+
return COPYABLE_VALUE.test(text) ? text : '';
|
|
307
|
+
};
|
|
308
|
+
return { name: await value('user.name'), email: await value('user.email') };
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const listUntracked = async (g, top) => (await g.output(top, ['ls-files', '--others', '--exclude-standard', '-z'], { maxOutputBytes: LIST_MAX_OUTPUT_BYTES }))
|
|
312
|
+
.split('\0')
|
|
313
|
+
.filter(Boolean);
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The untracked files that travel with uncommitted changes: exactly what the snapshot's `add` adds,
|
|
317
|
+
* because both read the same ignore rules with the same config. An untracked folder that is a git
|
|
318
|
+
* repository of its own does not travel at all, see `snapshotUncommitted`, and is listed under
|
|
319
|
+
* `notTravelling`. Sizes are the sizes on disk, and null for a file that vanished in between.
|
|
320
|
+
*/
|
|
321
|
+
const listTravellingFiles = (repository) => withHostGit('code_in_failed', async (g) => {
|
|
322
|
+
const top = await requireWorkTree(g, repository);
|
|
323
|
+
const files = [];
|
|
324
|
+
const notTravelling = [];
|
|
325
|
+
for (const entry of await listUntracked(g, top)) {
|
|
326
|
+
if (entry.endsWith('/')) {
|
|
327
|
+
notTravelling.push({ path: entry.slice(0, -1), kind: 'repository' });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const stats = await fs.lstat(path.join(top, entry));
|
|
332
|
+
files.push({ path: entry, kind: stats.isSymbolicLink() ? 'symlink' : 'file', size: stats.size });
|
|
333
|
+
} catch {
|
|
334
|
+
files.push({ path: entry, kind: 'file', size: null });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { files, notTravelling, totalBytes: files.reduce((sum, file) => sum + (file.size ?? 0), 0) };
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The first-release limits a create dialog warns about: submodule paths, which arrive empty, and
|
|
342
|
+
* whether Git LFS is in use, whose files arrive as pointers.
|
|
343
|
+
*/
|
|
344
|
+
const readTransferLimits = (repository) => withHostGit('code_in_failed', async (g) => {
|
|
345
|
+
const top = await requireWorkTree(g, repository);
|
|
346
|
+
const staged = (await g.output(top, ['ls-files', '--stage', '-z'], { maxOutputBytes: LIST_MAX_OUTPUT_BYTES })).split('\0').filter(Boolean);
|
|
347
|
+
const submodules = staged.filter((entry) => entry.startsWith('160000 ')).map((entry) => entry.slice(entry.indexOf('\t') + 1));
|
|
348
|
+
const lfsFiles = (await g.output(top, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', ':(attr:filter=lfs)'], { maxOutputBytes: LIST_MAX_OUTPUT_BYTES }))
|
|
349
|
+
.split('\0')
|
|
350
|
+
.filter(Boolean);
|
|
351
|
+
return { submodules, usesLfs: lfsFiles.length > 0 };
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const commitTree = async (g, top, tree, parent, message) => line(await g.output(
|
|
355
|
+
top,
|
|
356
|
+
['commit-tree', '--no-gpg-sign', tree, '-p', parent, '-m', message],
|
|
357
|
+
{ env: SNAPSHOT_IDENTITY },
|
|
358
|
+
));
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Two snapshot commits from a copy of the index, with the user's normal config. The working
|
|
362
|
+
* tree, the real index and every ref stay as they were. `add` does write loose objects into
|
|
363
|
+
* the repository and runs the user's clean filters, as their own `git add` would.
|
|
364
|
+
* `core.splitIndex=false` keeps the copy from writing a shared index file into the user's `.git`.
|
|
365
|
+
*
|
|
366
|
+
* Untracked folders that are git repositories of their own stay out, by pathspec with literal
|
|
367
|
+
* magic so no name is read as a pattern. `add` would record each as a gitlink, it would arrive
|
|
368
|
+
* as an empty folder the space's git does not see, and the start would then differ from the
|
|
369
|
+
* space's working tree before the agent did anything.
|
|
370
|
+
*/
|
|
371
|
+
const snapshotUncommitted = async (g, directory, top, base) => {
|
|
372
|
+
const index = path.join(directory, 'index');
|
|
373
|
+
const realIndex = path.resolve(top, line(await g.output(top, ['rev-parse', '--git-path', 'index'])));
|
|
374
|
+
try {
|
|
375
|
+
await fs.copyFile(realIndex, index);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
// No index file is an empty index to git, and a missing copy is the same to it.
|
|
378
|
+
if (error.code !== 'ENOENT') throw error;
|
|
379
|
+
}
|
|
380
|
+
const options = { env: { GIT_INDEX_FILE: index }, timeoutMs: SNAPSHOT_TIMEOUT_MS };
|
|
381
|
+
const unsplit = ['-c', 'core.splitIndex=false'];
|
|
382
|
+
const nested = (await listUntracked(g, top)).filter((entry) => entry.endsWith('/')).map((entry) => entry.slice(0, -1));
|
|
383
|
+
const pathspecs = ['.', ...nested.map((entry) => `:(exclude,literal)${entry}`)];
|
|
384
|
+
const stagedTree = line(await g.output(top, [...unsplit, 'write-tree'], options));
|
|
385
|
+
await g.output(top, [...unsplit, 'add', '--all', '--pathspec-from-file=-', '--pathspec-file-nul'], { ...options, stdin: `${pathspecs.join('\0')}\0` });
|
|
386
|
+
const workTree = line(await g.output(top, [...unsplit, 'write-tree'], options));
|
|
387
|
+
const staged = await commitTree(g, top, stagedTree, base, 'openchamber: staged snapshot');
|
|
388
|
+
const start = await commitTree(g, top, workTree, staged, 'openchamber: working tree snapshot');
|
|
389
|
+
return { staged, start };
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const snapshot = async (g, directory, { repository, spaceId, mode }) => {
|
|
393
|
+
requireSpaceId(spaceId);
|
|
394
|
+
if (!MODES.has(mode)) {
|
|
395
|
+
throw new SpaceError('invalid_snapshot_mode', 'A snapshot is taken with the uncommitted changes or from the last commit');
|
|
396
|
+
}
|
|
397
|
+
await requireGitVersion(g, directory);
|
|
398
|
+
const { top, prefix, base, objectFormat } = await inspectRepository(g, repository);
|
|
399
|
+
const ref = startRef(spaceId);
|
|
400
|
+
if ((await g.run(top, ['rev-parse', '--verify', '--quiet', ref])).code === 0) {
|
|
401
|
+
throw new SpaceError('space_ref_exists', `The repository already holds a start snapshot for space ${spaceId}: code already went into that space once. Remove the space, which removes this snapshot too, and create a new one.`);
|
|
402
|
+
}
|
|
403
|
+
const branch = await readBranch(g, top);
|
|
404
|
+
const identity = await readIdentity(g, top);
|
|
405
|
+
let staged = base;
|
|
406
|
+
let start = base;
|
|
407
|
+
if (mode === 'uncommitted') {
|
|
408
|
+
({ staged, start } = await snapshotUncommitted(g, directory, top, base));
|
|
409
|
+
}
|
|
410
|
+
// A project in a subfolder needs that folder in the start, or the project path inside would not exist.
|
|
411
|
+
if (prefix !== '' && line((await g.run(top, ['cat-file', '-t', `${start}:${prefix.replace(/\/$/, '')}`])).stdout) !== 'tree') {
|
|
412
|
+
throw new SpaceError('project_folder_does_not_travel', `The project folder ${prefix.replace(/\/$/, '')} is empty or ignored by git, so none of it would reach the space. Open the repository folder, or a folder that holds files git keeps, and create the space from there.`);
|
|
413
|
+
}
|
|
414
|
+
// The old value of zero makes this a create: it never moves a ref that appeared in between.
|
|
415
|
+
await g.output(top, ['update-ref', ref, start, zeroObjectId(objectFormat)]);
|
|
416
|
+
return { repository: top, prefix, objectFormat, base, staged, start, branch, identity, ref };
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Takes the snapshot and keeps its start alive with the host ref `refs/openchamber/spaces/<id>/start`,
|
|
421
|
+
* which 3b needs as the base of the result patch. `mode` is `uncommitted`, or `clean` for a start at
|
|
422
|
+
* HEAD with nothing uncommitted. Refuses a directory outside a git working tree, a repository with no
|
|
423
|
+
* commit, unresolved conflicts, a project folder that would not travel, and a space that already has
|
|
424
|
+
* a start ref.
|
|
425
|
+
*/
|
|
426
|
+
const takeSnapshot = (request) => withHostGit('code_in_failed', (g, directory) => snapshot(g, directory, request ?? {}));
|
|
427
|
+
|
|
428
|
+
/** Deletes every host ref of this space under `refs/openchamber/spaces/<id>/`, in one transaction. Resolves the deleted names. */
|
|
429
|
+
const removeSpaceRefs = (request) => withHostGit('code_in_failed', async (g) => {
|
|
430
|
+
const { repository, spaceId } = request ?? {};
|
|
431
|
+
requireSpaceId(spaceId);
|
|
432
|
+
const top = await requireWorkTree(g, repository);
|
|
433
|
+
const refs = (await g.output(top, ['for-each-ref', '--format=%(refname)', spaceRefPrefix(spaceId)])).split('\n').filter(Boolean);
|
|
434
|
+
if (refs.length > 0) {
|
|
435
|
+
await g.output(top, ['update-ref', '--stdin'], { stdin: refs.map((name) => `delete ${name}\n`).join('') });
|
|
436
|
+
}
|
|
437
|
+
return refs;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
/** A bare repository in `directory` that reads the project's objects through alternates and copies none. */
|
|
441
|
+
const makeSender = async (g, directory, top, objectFormat, shallowBase) => {
|
|
442
|
+
const sender = path.join(directory, 'sender.git');
|
|
443
|
+
// No template, so no hook of anybody's lands in it.
|
|
444
|
+
await g.output(directory, ['init', '--quiet', '--bare', '--template=', `--object-format=${objectFormat}`, sender]);
|
|
445
|
+
// `--git-path objects` names the common object store, which is right for a linked worktree too.
|
|
446
|
+
const objects = path.resolve(top, line(await g.output(top, ['rev-parse', '--git-path', 'objects'])));
|
|
447
|
+
await fs.mkdir(path.join(sender, 'objects', 'info'), { recursive: true });
|
|
448
|
+
await fs.writeFile(path.join(sender, 'objects', 'info', 'alternates'), `${objects.replaceAll('\\', '/')}\n`);
|
|
449
|
+
if (shallowBase) {
|
|
450
|
+
await fs.writeFile(path.join(sender, 'shallow'), `${shallowBase}\n`);
|
|
451
|
+
}
|
|
452
|
+
return sender;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* One push from the sending repository into the space. The user's config must not make it do
|
|
457
|
+
* anything: no hook (`--no-verify`, on top of the empty hooks folder), no signing, no tags, no
|
|
458
|
+
* submodules, no push options. `protocol.ext.allow` is set here and nowhere else. A timeout
|
|
459
|
+
* kills the whole process tree, because killing git alone leaves `docker exec` running.
|
|
460
|
+
*/
|
|
461
|
+
const push = async (g, sender, url, refspecs, timeoutMs, what) => {
|
|
462
|
+
let result;
|
|
463
|
+
try {
|
|
464
|
+
result = await g.run(sender, [
|
|
465
|
+
'-c', 'protocol.ext.allow=always', '-c', 'push.pushOption=',
|
|
466
|
+
'push', '--quiet', '--no-verify', '--no-signed', '--no-follow-tags', '--no-recurse-submodules',
|
|
467
|
+
url, ...refspecs,
|
|
468
|
+
], { timeoutMs, maxOutputBytes: PUSH_MAX_OUTPUT_BYTES, killTree: true });
|
|
469
|
+
} catch (error) {
|
|
470
|
+
throw new SpaceError('code_transfer_failed', `Sending ${what} into the space did not finish: ${error.message}`, { step: what, cause: error.code ?? null });
|
|
471
|
+
}
|
|
472
|
+
if (result.code !== 0) {
|
|
473
|
+
// The text comes from the receiving side, which is the space. It is shown, never parsed.
|
|
474
|
+
throw new SpaceError('code_transfer_failed', `Sending ${what} into the space failed: ${tail(result.stderr) || `exit code ${result.code}`}`, { step: what, cause: null });
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
/** A fixed script inside the space. Resolves the result for any exit code. */
|
|
479
|
+
const inside = (spaceId, script, args, timeoutMs = INSIDE_TIMEOUT_MS) => place.exec(spaceId, [IMAGE_SH, '-c', script, 'sh', ...args], { timeoutMs });
|
|
480
|
+
|
|
481
|
+
const insideFailure = (code, what, result) => new SpaceError(
|
|
482
|
+
code,
|
|
483
|
+
`Could not ${what} inside the space: ${tail(result.stderr) || `exit code ${result.code}`}`,
|
|
484
|
+
{ step: what, cause: 'inside_command_failed', exitCode: result.code },
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Brings the project's code into a new space, snapshot first: a plain repository at the space's
|
|
489
|
+
* project path whose branch, index and working tree match the host's HEAD, index and working tree.
|
|
490
|
+
* The history comes later, from `sendHistory`. On failure the host ref this call wrote is deleted;
|
|
491
|
+
* what reached the space stays there, and the space itself is the unit to remove.
|
|
492
|
+
*
|
|
493
|
+
* Resolves `{ spacePath, projectPath, base, staged, start, branch, objectFormat, identityCopied }`.
|
|
494
|
+
* `projectPath` is the project directory inside, which differs from `spacePath` when the project is
|
|
495
|
+
* a subfolder of its repository. `identityCopied` is `{ name, email }`, each true when the user's
|
|
496
|
+
* value went into the space repository's config; the agent can commit only when both are true.
|
|
497
|
+
*/
|
|
498
|
+
const bringCodeIn = (request) => withHostGit('code_transfer_failed', async (g, directory) => {
|
|
499
|
+
const { repository, spaceId, mode = 'uncommitted', timeoutMs = TRANSFER_TIMEOUT_MS, innerMarginSeconds = INNER_MARGIN_SECONDS } = request ?? {};
|
|
500
|
+
requireInnerMargin(innerMarginSeconds);
|
|
501
|
+
requireTimeout(timeoutMs);
|
|
502
|
+
let taken;
|
|
503
|
+
try {
|
|
504
|
+
taken = await snapshot(g, directory, { repository, spaceId, mode });
|
|
505
|
+
} catch (error) {
|
|
506
|
+
throw asBringFailure(error, SNAPSHOT_REFUSALS, 'take the snapshot');
|
|
507
|
+
}
|
|
508
|
+
const spacePath = spaceProjectPath(spaceId, taken.repository);
|
|
509
|
+
try {
|
|
510
|
+
// Asked first: it refuses a space that is gone, stopped or not ours before anything runs inside.
|
|
511
|
+
const execArgv = await place.execArgv(spaceId);
|
|
512
|
+
const init = await inside(spaceId, INIT_SCRIPT, [spacePath, taken.objectFormat, taken.branch ?? '']);
|
|
513
|
+
if (init.code === PATH_IN_USE_EXIT_CODE) {
|
|
514
|
+
throw new SpaceError('space_path_in_use', `The space already holds a project at ${spacePath}. Code goes into a space once: remove this space and create a new one.`);
|
|
515
|
+
}
|
|
516
|
+
if (init.code !== 0) {
|
|
517
|
+
throw insideFailure('code_transfer_failed', 'make the repository', init);
|
|
518
|
+
}
|
|
519
|
+
const url = receivePackUrl(execArgv, innerSeconds(timeoutMs, innerMarginSeconds), spacePath);
|
|
520
|
+
const sender = await makeSender(g, directory, taken.repository, taken.objectFormat, taken.base);
|
|
521
|
+
// Two pushes. One push of the base and the snapshot together failed on git 2.39.5, the image's git.
|
|
522
|
+
await push(g, sender, url, [`${taken.base}:${INSIDE_BASE}`], timeoutMs, 'the base commit');
|
|
523
|
+
await push(g, sender, url, [`${taken.staged}:${INSIDE_START_INDEX}`, `${taken.start}:${INSIDE_START}`], timeoutMs, 'the snapshot');
|
|
524
|
+
const unfold = await inside(spaceId, UNFOLD_SCRIPT, [spacePath, taken.branch ?? '', taken.identity.name, taken.identity.email]);
|
|
525
|
+
if (unfold.code !== 0) {
|
|
526
|
+
throw insideFailure('code_unfold_failed', 'unfold the snapshot', unfold);
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
const failure = asBringFailure(error, BRING_CODES, 'reach the space');
|
|
530
|
+
const forget = await g.run(taken.repository, ['update-ref', '-d', taken.ref, taken.start]).catch((cause) => ({ code: -1, stderr: cause.message }));
|
|
531
|
+
if (forget.code !== 0) {
|
|
532
|
+
failure.details = { ...failure.details, refNotRemoved: taken.ref };
|
|
533
|
+
}
|
|
534
|
+
throw failure;
|
|
535
|
+
}
|
|
536
|
+
const projectPath = taken.prefix === '' ? spacePath : `${spacePath}/${taken.prefix.replace(/\/$/, '')}`;
|
|
537
|
+
const { base, staged, start, branch, objectFormat, identity } = taken;
|
|
538
|
+
return { spacePath, projectPath, base, staged, start, branch, objectFormat, identityCopied: { name: identity.name !== '', email: identity.email !== '' } };
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The history behind `base`, sent after the snapshot, while the agent may already be working. A
|
|
543
|
+
* push cannot deepen a shallow receiver, so the history goes to a side repository inside the space
|
|
544
|
+
* and a local `fetch --unshallow` takes it from there; the side repository is removed afterwards.
|
|
545
|
+
* Nothing in the working tree, the index or HEAD inside changes, and commit ids stay the same.
|
|
546
|
+
* `spacePath` is the one `bringCodeIn` returned: the host repository may have been renamed or
|
|
547
|
+
* checked out elsewhere since, so it is not worked out again.
|
|
548
|
+
*
|
|
549
|
+
* Resolves `{ status: 'sent' }`, `{ status: 'already_complete' }` when the space's repository has its
|
|
550
|
+
* history already and nothing was sent, or `{ status: 'host_shallow' }` when the host repository is
|
|
551
|
+
* shallow itself and has no history to send. Calls for one space in this process share one run. Every failure rejects with `history_transfer_failed`, with the
|
|
552
|
+
* step in `details.step` and the root cause in `details.cause`; the caller reports it, and the space
|
|
553
|
+
* keeps working with a shallow history.
|
|
554
|
+
*/
|
|
555
|
+
const sendHistory = (request) => {
|
|
556
|
+
const { repository, spaceId, spacePath, base, timeoutMs = HISTORY_TIMEOUT_MS, innerMarginSeconds = INNER_MARGIN_SECONDS } = request ?? {};
|
|
557
|
+
// Each call's own arguments are checked before it may join a run.
|
|
558
|
+
let checkedPath;
|
|
559
|
+
try {
|
|
560
|
+
requireSpaceId(spaceId);
|
|
561
|
+
checkedPath = requireSpaceProjectPath(spaceId, spacePath);
|
|
562
|
+
requireInnerMargin(innerMarginSeconds);
|
|
563
|
+
requireTimeout(timeoutMs);
|
|
564
|
+
if (!OBJECT_ID.test(String(base ?? ''))) {
|
|
565
|
+
throw new SpaceError('not_a_commit', 'The base of the history is not a commit id.');
|
|
566
|
+
}
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return Promise.reject(asHistoryFailure(error, 'check the request'));
|
|
569
|
+
}
|
|
570
|
+
// The side repository is one per space, so two runs for one space would break each other. A call
|
|
571
|
+
// joins the run under way only when it asks for the same thing, from the same checkout; otherwise
|
|
572
|
+
// it is refused.
|
|
573
|
+
const checkout = path.resolve(String(repository ?? ''));
|
|
574
|
+
const running = historyRuns.get(spaceId);
|
|
575
|
+
if (running) {
|
|
576
|
+
if (running.spacePath === checkedPath && running.base === base && running.checkout === checkout) return running.promise;
|
|
577
|
+
return Promise.reject(new SpaceError('history_transfer_failed', `A history transfer for space ${spaceId} with other arguments is still running. Wait for it to end, then try again.`, { step: 'check the request', cause: 'history_in_progress' }));
|
|
578
|
+
}
|
|
579
|
+
const run = { spacePath: checkedPath, base, checkout, promise: null };
|
|
580
|
+
run.promise = sendHistoryOnce({ repository, spaceId, spacePath: checkedPath, base, timeoutMs, innerMarginSeconds }).finally(() => {
|
|
581
|
+
if (historyRuns.get(spaceId) === run) historyRuns.delete(spaceId);
|
|
582
|
+
});
|
|
583
|
+
historyRuns.set(spaceId, run);
|
|
584
|
+
return run.promise;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const sendHistoryOnce = async ({ repository, spaceId, spacePath, base, timeoutMs, innerMarginSeconds }) => {
|
|
588
|
+
try {
|
|
589
|
+
return await withHostGit('history_transfer_failed', (g, directory) => history(g, directory, { repository, spaceId, spacePath, base, timeoutMs, innerMarginSeconds }));
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw asHistoryFailure(error, 'send the history');
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
const history = async (g, directory, { repository, spaceId, spacePath: requestedPath, base, timeoutMs, innerMarginSeconds }) => {
|
|
597
|
+
const spacePath = requireSpaceProjectPath(spaceId, requestedPath);
|
|
598
|
+
await requireGitVersion(g, directory);
|
|
599
|
+
// Only what the history needs. The user may be in the middle of a merge on the host by now,
|
|
600
|
+
// and that has nothing to do with the history behind a commit that already travelled.
|
|
601
|
+
const top = await requireWorkTree(g, repository);
|
|
602
|
+
const objectFormat = line(await g.output(top, ['rev-parse', '--show-object-format']));
|
|
603
|
+
if (line(await g.output(top, ['rev-parse', '--is-shallow-repository'])) === 'true') {
|
|
604
|
+
return { status: 'host_shallow' };
|
|
605
|
+
}
|
|
606
|
+
if (!objectIdPattern(objectFormat).test(String(base ?? '')) || (await g.run(top, ['cat-file', '-e', `${base}^{commit}`])).code !== 0) {
|
|
607
|
+
throw new SpaceError('history_transfer_failed', 'The base of the history is not a commit of this repository.', { step: 'check the base', cause: 'not_a_commit' });
|
|
608
|
+
}
|
|
609
|
+
// Asked first: it refuses a space that is gone, stopped or not ours before anything runs inside.
|
|
610
|
+
const execArgv = await place.execArgv(spaceId);
|
|
611
|
+
// A repository that is complete already has its history: from an earlier call, or from the agent.
|
|
612
|
+
// The answer comes from inside and only decides whether to send; `false` costs the space itself.
|
|
613
|
+
const shallow = await inside(spaceId, IS_SHALLOW_SCRIPT, [spacePath]);
|
|
614
|
+
const answer = shallow.code === 0 ? shallow.stdout.trim() : '';
|
|
615
|
+
if (answer === 'false') {
|
|
616
|
+
return { status: 'already_complete' };
|
|
617
|
+
}
|
|
618
|
+
if (answer !== 'true') {
|
|
619
|
+
throw insideFailure('history_transfer_failed', 'tell whether the repository still needs its history', shallow);
|
|
620
|
+
}
|
|
621
|
+
const side = spaceHistoryPath(spaceId);
|
|
622
|
+
try {
|
|
623
|
+
const init = await inside(spaceId, INIT_SIDE_SCRIPT, [side, objectFormat]);
|
|
624
|
+
if (init.code !== 0) {
|
|
625
|
+
throw insideFailure('history_transfer_failed', 'make the side repository for the history', init);
|
|
626
|
+
}
|
|
627
|
+
const url = receivePackUrl(execArgv, innerSeconds(timeoutMs, innerMarginSeconds), side);
|
|
628
|
+
await push(g, await makeSender(g, directory, top, objectFormat, null), url, [`${base}:${INSIDE_BASE}`], timeoutMs, 'the history');
|
|
629
|
+
const fetched = await inside(spaceId, UNSHALLOW_SCRIPT, [spacePath, side, String(innerSeconds(timeoutMs, innerMarginSeconds))], timeoutMs + INSIDE_TIMEOUT_MS);
|
|
630
|
+
if (fetched.code !== 0) {
|
|
631
|
+
throw insideFailure('history_transfer_failed', 'take the history into the repository', fetched);
|
|
632
|
+
}
|
|
633
|
+
} catch (error) {
|
|
634
|
+
await inside(spaceId, REMOVE_SIDE_SCRIPT, [side]).catch(() => null);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
return { status: 'sent' };
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
return { takeSnapshot, listTravellingFiles, readTransferLimits, removeSpaceRefs, bringCodeIn, sendHistory };
|
|
641
|
+
}
|