@aiworker/web 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/cli.js +14 -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-B1Gizi3a.js} +1 -1
- package/dist/assets/DiffView-DXjVsEOw.js +4 -0
- package/dist/assets/DirtyBranchSwitchDialog-aig6zJjg.js +1 -0
- package/dist/assets/FilesView-CJdG0-wk.js +17 -0
- package/dist/assets/GitView-BXsS3jG6.js +6 -0
- package/dist/assets/{JsonTreeView-B0BdZEHG.js → JsonTreeView-D_hpXBnF.js} +1 -1
- package/dist/assets/LinearIssuesView-BoUiLQG1.js +1 -0
- package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
- package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
- package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
- package/dist/assets/{PatchDiff-DvfqDql7.js → PatchDiff-8p6nLJaU.js} +1 -1
- package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
- package/dist/assets/PlanView-CJTmOF6I.js +5 -0
- package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
- package/dist/assets/{SettingsWindow-i45U1Cy1.js → SettingsWindow-Cpb4hM6q.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-BTwyh5OF.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
- package/dist/assets/ToolPartDiffPreview-DtZLzrSK.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-DD8lDVej.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
- package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BMIRgMHv.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DCrsQ5zS.js +10 -0
- package/dist/assets/es-Dwv4YfH6.js +16 -0
- package/dist/assets/fr-BCWg4Cnx.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-B68h5S5B.css +1 -0
- package/dist/assets/index-DVwMarHI.js +39 -0
- package/dist/assets/{index-CODwDsU_.js → index-jXSK5RrB.js} +1 -1
- package/dist/assets/{isDiffPlainText-BqayB8d3.js → isDiffPlainText-B_x_rVon.js} +1 -1
- package/dist/assets/{isFilePlainText-QfoiKJNY.js → isFilePlainText-DW_xMer1.js} +1 -1
- package/dist/assets/ja-Bruw3RYC.js +16 -0
- package/dist/assets/ko-idhXGF8j.js +16 -0
- package/dist/assets/main-BkyaQhJX.js +930 -0
- package/dist/assets/{main-C5DVOxc6.js → main-CzdPuL0s.js} +2 -2
- package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
- package/dist/assets/{miniChat-DUJ4irZc.js → miniChat-CCkqKOLp.js} +2 -2
- package/dist/assets/{mobile-CcaxB0n4.js → mobile-mvxTo055.js} +2 -2
- package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
- package/dist/assets/pl-CGZWUqZJ.js +16 -0
- package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
- package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
- package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
- package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
- package/dist/assets/{shikiHighlight-DYOWuY65.js → shikiHighlight-CRl0SUC9.js} +1 -1
- package/dist/assets/surface-oMvhsog3.js +1 -0
- package/dist/assets/tr-BK_KiO2V.js +7 -0
- package/dist/assets/uk-CG74xV8N.js +16 -0
- package/dist/assets/useAppFontEffects-CPKeOpUo.js +7284 -0
- package/dist/assets/useGitComparison-DH220WmO.js +1 -0
- package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
- package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DFZik9R2.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-Bhg83tjf.js → web-components-z7hfnS8n.js} +1 -1
- package/dist/assets/zh-CN-T4AXofZI.js +16 -0
- package/dist/assets/zh-TW-Dte40sxp.js +16 -0
- package/dist/index.html +21 -59
- package/dist/mini-chat.html +7 -7
- package/dist/mobile.html +5 -5
- package/package.json +3 -4
- package/server/index.d.ts +15 -0
- package/server/index.js +158 -89
- 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/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- 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 +18 -18
- package/server/lib/fs/DOCUMENTATION.md +5 -0
- package/server/lib/fs/routes.js +68 -26
- package/server/lib/fs/routes.test.js +208 -22
- package/server/lib/git/DOCUMENTATION.md +32 -6
- 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 +41 -5
- package/server/lib/git/service.js +300 -224
- package/server/lib/git/service.test.js +596 -28
- 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 +17 -10
- package/server/lib/inherited-env.test.js +10 -11
- 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 +161 -0
- package/server/lib/message-queue/runtime.js +870 -0
- package/server/lib/message-queue/runtime.test.js +482 -0
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -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 +30 -6
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +8 -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 +19 -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 +79 -19
- package/server/lib/opencode/openchamber-routes.test.js +139 -12
- 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 +19 -17
- 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-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +116 -44
- package/server/lib/opencode/settings-helpers.test.js +354 -12
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +141 -20
- package/server/lib/opencode/settings-runtime.test.js +191 -4
- 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/DOCUMENTATION.md +25 -1
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +16 -1
- package/server/lib/project-context/runtime.js +248 -23
- package/server/lib/project-context/runtime.test.js +106 -3
- package/server/lib/projects/DOCUMENTATION.md +76 -0
- package/server/lib/projects/project-config.js +144 -11
- package/server/lib/projects/project-id.js +17 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +40 -2
- 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/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -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 +22 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +32 -1
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +3 -2
- package/server/lib/quota/providers/opencode-go.test.js +35 -6
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- 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 +56 -1
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +38 -9
- 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-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +69 -18
- 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 +15 -18
- 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 +91 -30
- 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 +7 -2
- package/server/lib/small-model/call.js +18 -7
- package/server/lib/small-model/call.test.js +58 -6
- package/server/lib/small-model/index.js +12 -16
- 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/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -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 +14 -10
- package/server/lib/terminal/runtime.js +190 -31
- package/server/lib/terminal/runtime.test.js +572 -38
- 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/DOCUMENTATION.md +33 -14
- 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 +11 -15
- package/server/lib/walkthrough/model-settings.test.js +1 -1
- package/server/lib/walkthrough/prompt.js +3 -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/sources.js +23 -2
- 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/DiffView-DluiRSla.js +0 -4
- 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/MarkdownImageGallery-ClOvuZxC.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Dt9mjTRN.js +0 -12
- package/dist/assets/OnboardingScreen-D2DG7K6b.js +0 -1
- package/dist/assets/PierreDiffViewer-B7m3f0d7.js +0 -157
- 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/WalkthroughView-egQgxel3.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C3fjDbdg.js +0 -1
- package/dist/assets/de-C1P6X5Rs.js +0 -9
- package/dist/assets/es-CDgaF1_t.js +0 -15
- package/dist/assets/fr-Bgxqvvz_.js +0 -13
- package/dist/assets/index-C57jT0Sa.css +0 -1
- package/dist/assets/index-DW_E3rnL.js +0 -7
- package/dist/assets/ja-BDRrEcNP.js +0 -15
- package/dist/assets/ko-Ba8sQV79.js +0 -15
- package/dist/assets/main-Bv5Pkngh.js +0 -928
- package/dist/assets/markdownCore-CZwzz5gw.js +0 -3
- package/dist/assets/patchFileDiff-yNfWXN4E.js +0 -10
- package/dist/assets/pl-Dioz29BI.js +0 -15
- package/dist/assets/pt-BR-DIs6ElwV.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BcKLHXKe.js +0 -2
- package/dist/assets/renderMobileApp-D9yQJy0L.js +0 -25
- package/dist/assets/runtimeEndpointReset-CjhvxLH0.js +0 -16
- package/dist/assets/scheduledTasksApi-DLW0mkQK.js +0 -1
- package/dist/assets/tr-DFnMlPxY.js +0 -6
- package/dist/assets/uk-ZrCNjM7Y.js +0 -15
- package/dist/assets/useAppFontEffects-CjrntdL0.js +0 -7310
- package/dist/assets/useInlineCommentController-CDEuSlPH.js +0 -2
- package/dist/assets/useRootScrollLock-BHYPkrLv.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
- package/dist/assets/zh-CN-iv_AV4EC.js +0 -15
- package/dist/assets/zh-TW-CzEUk38j.js +0 -15
- 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
|
@@ -164,15 +164,22 @@ const sanitizePlanLinks = (value, now) => {
|
|
|
164
164
|
const id = asNonEmptyString(entry.id);
|
|
165
165
|
const file = asNonEmptyString(entry.file);
|
|
166
166
|
if (!id || !file || !PLAN_FILE_PATTERN.test(file)) continue;
|
|
167
|
-
|
|
167
|
+
// A personal file and a shared file may carry the same name: they live in different folders.
|
|
168
|
+
const shared = entry.shared === true;
|
|
169
|
+
const fileKey = `${shared ? 'shared' : 'personal'}/${file}`;
|
|
170
|
+
if (seenIds.has(id) || seenFiles.has(fileKey)) continue;
|
|
168
171
|
seenIds.add(id);
|
|
169
|
-
seenFiles.add(
|
|
172
|
+
seenFiles.add(fileKey);
|
|
170
173
|
result.push({
|
|
171
174
|
id,
|
|
172
175
|
file,
|
|
173
176
|
title: sanitizePlanTitle(entry.title) || 'Plan',
|
|
174
177
|
createdAt: Number.isFinite(entry.createdAt) && entry.createdAt >= 0 ? entry.createdAt : now,
|
|
175
178
|
pinned: entry.pinned === true,
|
|
179
|
+
// The user's own plan that was moved into the team's folder: it keeps
|
|
180
|
+
// its id (session attachments still point at it) and this flag says
|
|
181
|
+
// which folder holds the file.
|
|
182
|
+
shared,
|
|
176
183
|
});
|
|
177
184
|
}
|
|
178
185
|
return result.sort((a, b) => b.createdAt - a.createdAt);
|
|
@@ -185,8 +192,15 @@ const createEmptyContext = () => ({
|
|
|
185
192
|
plans: [],
|
|
186
193
|
});
|
|
187
194
|
|
|
195
|
+
const SHARED_PLAN_ID_PREFIX = 'shared:';
|
|
196
|
+
|
|
188
197
|
export const createProjectContextRuntime = (deps) => {
|
|
189
|
-
const { fsPromises, path, projectsDirPath, createId } = deps;
|
|
198
|
+
const { fsPromises, path, projectsDirPath, createId, resolveSharedPlansDir } = deps;
|
|
199
|
+
// The team's shared plans folder for a project (absolute path) or null; the
|
|
200
|
+
// project config runtime owns that answer (`plansDir` in the shared file).
|
|
201
|
+
const sharedPlansDirFor = typeof resolveSharedPlansDir === 'function'
|
|
202
|
+
? resolveSharedPlansDir
|
|
203
|
+
: async () => null;
|
|
190
204
|
|
|
191
205
|
const idFactory = typeof createId === 'function'
|
|
192
206
|
? createId
|
|
@@ -356,7 +370,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
356
370
|
* of identical content, so concurrent migrations converge instead of
|
|
357
371
|
* interleaving.
|
|
358
372
|
*/
|
|
359
|
-
const
|
|
373
|
+
const readStoredContext = async (projectId) => {
|
|
360
374
|
const now = Date.now();
|
|
361
375
|
const stored = await readJson(contextPathFor(projectId));
|
|
362
376
|
|
|
@@ -385,6 +399,67 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
385
399
|
};
|
|
386
400
|
};
|
|
387
401
|
|
|
402
|
+
/** The file name behind a shared plan id, or null when the id is not one. */
|
|
403
|
+
const sharedPlanFileOf = (planId) => {
|
|
404
|
+
if (typeof planId !== 'string' || !planId.startsWith(SHARED_PLAN_ID_PREFIX)) return null;
|
|
405
|
+
const file = planId.slice(SHARED_PLAN_ID_PREFIX.length);
|
|
406
|
+
return PLAN_FILE_PATTERN.test(file) ? file : null;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Plans in the team's shared folder: every `.md` file there, newest first,
|
|
411
|
+
* addressed by `shared:<file>`. Plans written by other tools have no
|
|
412
|
+
* manifest entry, so the title comes from the file each time.
|
|
413
|
+
*/
|
|
414
|
+
const listSharedPlans = async (projectId, claimedFiles = new Set()) => {
|
|
415
|
+
const dir = await sharedPlansDirFor(projectId);
|
|
416
|
+
if (!dir) return { dir: null, plans: [] };
|
|
417
|
+
let entries;
|
|
418
|
+
try {
|
|
419
|
+
entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (error && error.code === 'ENOENT') return { dir, plans: [] };
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
const plans = [];
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
if (!entry.isFile() || !PLAN_FILE_PATTERN.test(entry.name)) continue;
|
|
427
|
+
// Listed under its own id by the manifest entry that moved it there.
|
|
428
|
+
if (claimedFiles.has(entry.name)) continue;
|
|
429
|
+
const filePath = path.join(dir, entry.name);
|
|
430
|
+
const [raw, stat] = await Promise.all([fsPromises.readFile(filePath, 'utf8'), fsPromises.stat(filePath)]);
|
|
431
|
+
plans.push({
|
|
432
|
+
id: `${SHARED_PLAN_ID_PREFIX}${entry.name}`,
|
|
433
|
+
file: entry.name,
|
|
434
|
+
title: parsePlanMarkdown(raw).title,
|
|
435
|
+
createdAt: Math.round(stat.mtimeMs),
|
|
436
|
+
pinned: false,
|
|
437
|
+
source: 'shared',
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
plans.sort((left, right) => right.createdAt - left.createdAt);
|
|
441
|
+
return { dir, plans };
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/** What clients see: the stored context plus the team's shared plans, each marked with its source. */
|
|
445
|
+
const readContext = async (projectId) => {
|
|
446
|
+
const stored = await readStoredContext(projectId);
|
|
447
|
+
const claimed = new Set(stored.plans.filter((plan) => plan.shared).map((plan) => plan.file));
|
|
448
|
+
const shared = await listSharedPlans(projectId, claimed);
|
|
449
|
+
const own = stored.plans
|
|
450
|
+
// A moved plan is only reachable while the project still has a shared folder.
|
|
451
|
+
.filter((plan) => !plan.shared || shared.dir)
|
|
452
|
+
.map(({ shared: isShared, ...plan }) => ({ ...plan, source: isShared ? 'shared' : 'personal' }));
|
|
453
|
+
return {
|
|
454
|
+
...stored,
|
|
455
|
+
plans: [...own, ...shared.plans],
|
|
456
|
+
sharedPlansDir: shared.dir,
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/** The folder a manifest plan's file lives in; null for a moved plan when the project lost its shared folder. */
|
|
461
|
+
const folderOfLink = async (projectId, link) => (link.shared ? sharedPlansDirFor(projectId) : plansDirFor(projectId));
|
|
462
|
+
|
|
388
463
|
const writeContext = async (projectId, context) => {
|
|
389
464
|
await writeJsonAtomic(contextPathFor(projectId), {
|
|
390
465
|
version: PROJECT_CONTEXT_VERSION,
|
|
@@ -397,7 +472,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
397
472
|
const saveTodos = async (projectId, todos) => {
|
|
398
473
|
return withWriteLock(projectId, async () => {
|
|
399
474
|
const now = Date.now();
|
|
400
|
-
const current = await
|
|
475
|
+
const current = await readStoredContext(projectId);
|
|
401
476
|
const next = { ...current, todos: sanitizeTodos(todos, now) };
|
|
402
477
|
await writeContext(projectId, next);
|
|
403
478
|
return next;
|
|
@@ -420,7 +495,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
420
495
|
|
|
421
496
|
return withWriteLock(projectId, async () => {
|
|
422
497
|
const now = Date.now();
|
|
423
|
-
const current = await
|
|
498
|
+
const current = await readStoredContext(projectId);
|
|
424
499
|
if (current.notes.length >= PROJECT_NOTE_MAX_ITEMS) {
|
|
425
500
|
throw new Error(`A project can hold at most ${PROJECT_NOTE_MAX_ITEMS} notes`);
|
|
426
501
|
}
|
|
@@ -462,7 +537,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
462
537
|
|
|
463
538
|
return withWriteLock(projectId, async () => {
|
|
464
539
|
const now = Date.now();
|
|
465
|
-
const current = await
|
|
540
|
+
const current = await readStoredContext(projectId);
|
|
466
541
|
const existing = current.notes.find((note) => note.id === id);
|
|
467
542
|
if (!existing) {
|
|
468
543
|
return null;
|
|
@@ -486,7 +561,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
486
561
|
}
|
|
487
562
|
|
|
488
563
|
return withWriteLock(projectId, async () => {
|
|
489
|
-
const current = await
|
|
564
|
+
const current = await readStoredContext(projectId);
|
|
490
565
|
if (!current.notes.some((note) => note.id === id)) {
|
|
491
566
|
return { deleted: false, context: current };
|
|
492
567
|
}
|
|
@@ -501,22 +576,38 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
501
576
|
if (!id) {
|
|
502
577
|
throw new Error('planId is required');
|
|
503
578
|
}
|
|
504
|
-
const
|
|
579
|
+
const sharedFile = sharedPlanFileOf(id);
|
|
580
|
+
if (sharedFile) {
|
|
581
|
+
const dir = await sharedPlansDirFor(projectId);
|
|
582
|
+
if (!dir) return null;
|
|
583
|
+
let raw;
|
|
584
|
+
try {
|
|
585
|
+
raw = await fsPromises.readFile(path.join(dir, sharedFile), 'utf8');
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (error && error.code === 'ENOENT') return null;
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
const parsed = parsePlanMarkdown(raw);
|
|
591
|
+
return { id, file: sharedFile, createdAt: 0, title: parsed.title, body: parsed.body, raw, source: 'shared' };
|
|
592
|
+
}
|
|
593
|
+
const context = await readStoredContext(projectId);
|
|
505
594
|
const link = context.plans.find((entry) => entry.id === id);
|
|
506
595
|
if (!link) {
|
|
507
596
|
return null;
|
|
508
597
|
}
|
|
509
598
|
|
|
599
|
+
const folder = await folderOfLink(projectId, link);
|
|
600
|
+
if (!folder) return null;
|
|
510
601
|
let raw;
|
|
511
602
|
try {
|
|
512
|
-
raw = await fsPromises.readFile(path.join(
|
|
603
|
+
raw = await fsPromises.readFile(path.join(folder, link.file), 'utf8');
|
|
513
604
|
} catch (error) {
|
|
514
605
|
if (error && error.code === 'ENOENT') return null;
|
|
515
606
|
throw error;
|
|
516
607
|
}
|
|
517
608
|
|
|
518
609
|
const parsed = parsePlanMarkdown(raw);
|
|
519
|
-
return { id: link.id, file: link.file, createdAt: link.createdAt, title: parsed.title, body: parsed.body, raw };
|
|
610
|
+
return { id: link.id, file: link.file, createdAt: link.createdAt, title: parsed.title, body: parsed.body, raw, source: link.shared ? 'shared' : 'personal' };
|
|
520
611
|
};
|
|
521
612
|
|
|
522
613
|
/**
|
|
@@ -541,14 +632,37 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
541
632
|
}
|
|
542
633
|
const raw = clampLength(value.raw, PROJECT_PLAN_BODY_MAX_LENGTH);
|
|
543
634
|
|
|
635
|
+
const sharedFile = sharedPlanFileOf(id);
|
|
636
|
+
if (sharedFile) {
|
|
637
|
+
// A shared plan is the file itself: written verbatim, no manifest.
|
|
638
|
+
return withWriteLock(projectId, async () => {
|
|
639
|
+
const dir = await sharedPlansDirFor(projectId);
|
|
640
|
+
if (!dir) return null;
|
|
641
|
+
const filePath = path.join(dir, sharedFile);
|
|
642
|
+
try {
|
|
643
|
+
await fsPromises.access(filePath);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (error && error.code === 'ENOENT') return null;
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
await fsPromises.writeFile(filePath, raw, 'utf8');
|
|
649
|
+
const parsed = parsePlanMarkdown(raw);
|
|
650
|
+
const context = await readContext(projectId);
|
|
651
|
+
const plan = context.plans.find((entry) => entry.id === id) ?? { id, file: sharedFile, title: parsed.title, createdAt: Date.now(), pinned: false, source: 'shared' };
|
|
652
|
+
return { plan, context, title: parsed.title, body: parsed.body, raw };
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
544
656
|
return withWriteLock(projectId, async () => {
|
|
545
|
-
const current = await
|
|
657
|
+
const current = await readStoredContext(projectId);
|
|
546
658
|
const link = current.plans.find((entry) => entry.id === id);
|
|
547
659
|
if (!link) {
|
|
548
660
|
return null;
|
|
549
661
|
}
|
|
550
662
|
|
|
551
|
-
const
|
|
663
|
+
const folder = await folderOfLink(projectId, link);
|
|
664
|
+
if (!folder) return null;
|
|
665
|
+
const filePath = path.join(folder, link.file);
|
|
552
666
|
// Refuse to recreate a file that was deleted underneath us: the link is
|
|
553
667
|
// already dead, and writing here would resurrect it with editor content
|
|
554
668
|
// the user believed was discarded.
|
|
@@ -569,7 +683,8 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
569
683
|
};
|
|
570
684
|
await writeContext(projectId, next);
|
|
571
685
|
|
|
572
|
-
|
|
686
|
+
const { shared: isShared, ...publicLink } = nextLink;
|
|
687
|
+
return { plan: { ...publicLink, source: isShared ? 'shared' : 'personal' }, context: await readContext(projectId), title: parsed.title, body: parsed.body, raw };
|
|
573
688
|
});
|
|
574
689
|
};
|
|
575
690
|
|
|
@@ -586,7 +701,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
586
701
|
const body = clampLength(typeof value?.body === 'string' ? value.body : '', PROJECT_PLAN_BODY_MAX_LENGTH);
|
|
587
702
|
|
|
588
703
|
return withWriteLock(projectId, async () => {
|
|
589
|
-
const current = await
|
|
704
|
+
const current = await readStoredContext(projectId);
|
|
590
705
|
const createdAt = Date.now();
|
|
591
706
|
const plansDir = plansDirFor(projectId);
|
|
592
707
|
await fsPromises.mkdir(plansDir, { recursive: true });
|
|
@@ -594,7 +709,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
594
709
|
const baseName = `${createdAt}-${slugifyPlanTitle(title)}`;
|
|
595
710
|
let file = `${baseName}.md`;
|
|
596
711
|
let attempt = 1;
|
|
597
|
-
while (current.plans.some((entry) => entry.file === file)) {
|
|
712
|
+
while (current.plans.some((entry) => !entry.shared && entry.file === file)) {
|
|
598
713
|
file = `${baseName}-${attempt}.md`;
|
|
599
714
|
attempt += 1;
|
|
600
715
|
}
|
|
@@ -604,7 +719,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
604
719
|
const link = { id: idFactory(), file, title, createdAt, pinned: false };
|
|
605
720
|
const next = { ...current, plans: [link, ...current.plans] };
|
|
606
721
|
await writeContext(projectId, next);
|
|
607
|
-
return { plan: link, context:
|
|
722
|
+
return { plan: { ...link, source: 'personal' }, context: await readContext(projectId) };
|
|
608
723
|
});
|
|
609
724
|
};
|
|
610
725
|
|
|
@@ -623,7 +738,7 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
623
738
|
}
|
|
624
739
|
|
|
625
740
|
return withWriteLock(projectId, async () => {
|
|
626
|
-
const current = await
|
|
741
|
+
const current = await readStoredContext(projectId);
|
|
627
742
|
const existing = current.plans.find((entry) => entry.id === id);
|
|
628
743
|
if (!existing) {
|
|
629
744
|
return null;
|
|
@@ -631,7 +746,8 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
631
746
|
const plan = { ...existing, pinned: pinned === true };
|
|
632
747
|
const next = { ...current, plans: current.plans.map((entry) => (entry.id === id ? plan : entry)) };
|
|
633
748
|
await writeContext(projectId, next);
|
|
634
|
-
|
|
749
|
+
const { shared: isShared, ...publicPlan } = plan;
|
|
750
|
+
return { plan: { ...publicPlan, source: isShared ? 'shared' : 'personal' }, context: await readContext(projectId) };
|
|
635
751
|
});
|
|
636
752
|
};
|
|
637
753
|
|
|
@@ -641,17 +757,124 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
641
757
|
throw new Error('planId is required');
|
|
642
758
|
}
|
|
643
759
|
|
|
760
|
+
const sharedFile = sharedPlanFileOf(id);
|
|
761
|
+
if (sharedFile) {
|
|
762
|
+
return withWriteLock(projectId, async () => {
|
|
763
|
+
const dir = await sharedPlansDirFor(projectId);
|
|
764
|
+
const filePath = dir ? path.join(dir, sharedFile) : null;
|
|
765
|
+
const exists = filePath ? await fsPromises.access(filePath).then(() => true, () => false) : false;
|
|
766
|
+
if (!exists) return { deleted: false, context: await readContext(projectId) };
|
|
767
|
+
await fsPromises.rm(filePath, { force: true });
|
|
768
|
+
return { deleted: true, context: await readContext(projectId) };
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
644
772
|
return withWriteLock(projectId, async () => {
|
|
645
|
-
const current = await
|
|
773
|
+
const current = await readStoredContext(projectId);
|
|
646
774
|
const link = current.plans.find((entry) => entry.id === id);
|
|
647
775
|
if (!link) {
|
|
648
|
-
return { deleted: false, context:
|
|
776
|
+
return { deleted: false, context: await readContext(projectId) };
|
|
649
777
|
}
|
|
650
778
|
|
|
651
779
|
const next = { ...current, plans: current.plans.filter((entry) => entry.id !== id) };
|
|
652
780
|
await writeContext(projectId, next);
|
|
653
|
-
await
|
|
654
|
-
|
|
781
|
+
const folder = await folderOfLink(projectId, link);
|
|
782
|
+
if (folder) await fsPromises.rm(path.join(folder, link.file), { force: true });
|
|
783
|
+
return { deleted: true, context: await readContext(projectId) };
|
|
784
|
+
});
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
/** A free file name in `dir`, keeping the wanted base name unless it is taken. */
|
|
788
|
+
const freeFileNameIn = async (dir, wanted, taken = new Set()) => {
|
|
789
|
+
const base = wanted.replace(/\.md$/, '');
|
|
790
|
+
let file = wanted;
|
|
791
|
+
let attempt = 1;
|
|
792
|
+
while (taken.has(file) || await fsPromises.access(path.join(dir, file)).then(() => true, () => false)) {
|
|
793
|
+
file = `${base}-${attempt}.md`;
|
|
794
|
+
attempt += 1;
|
|
795
|
+
}
|
|
796
|
+
return file;
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const moveFile = async (from, to) => {
|
|
800
|
+
try {
|
|
801
|
+
await fsPromises.rename(from, to);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (!error || error.code !== 'EXDEV') throw error;
|
|
804
|
+
await fsPromises.copyFile(from, to);
|
|
805
|
+
await fsPromises.rm(from, { force: true });
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Move one of the user's plans into the team's shared folder. The plan
|
|
811
|
+
* keeps its id: the manifest entry stays and gets the `shared` flag, so a
|
|
812
|
+
* session that attached the plan still finds it. The markdown moves first,
|
|
813
|
+
* then the manifest is written; a failure in between leaves the file in the
|
|
814
|
+
* shared folder (listed there as `shared:<file>`) and a dead personal entry
|
|
815
|
+
* that `readPlan` reports as gone. Needs a shared plans folder; refused
|
|
816
|
+
* (validation error) when the project has none.
|
|
817
|
+
*/
|
|
818
|
+
const sharePlan = async (projectId, planId) => {
|
|
819
|
+
const id = asNonEmptyString(planId);
|
|
820
|
+
if (!id) throw new Error('planId is required');
|
|
821
|
+
return withWriteLock(projectId, async () => {
|
|
822
|
+
const dir = await sharedPlansDirFor(projectId);
|
|
823
|
+
if (!dir) throw new Error('shared plans folder is required');
|
|
824
|
+
const current = await readStoredContext(projectId);
|
|
825
|
+
const link = current.plans.find((entry) => entry.id === id);
|
|
826
|
+
if (!link || link.shared) return null;
|
|
827
|
+
const from = path.join(plansDirFor(projectId), link.file);
|
|
828
|
+
const exists = await fsPromises.access(from).then(() => true, () => false);
|
|
829
|
+
if (!exists) return null;
|
|
830
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
831
|
+
const file = await freeFileNameIn(dir, link.file);
|
|
832
|
+
await moveFile(from, path.join(dir, file));
|
|
833
|
+
const moved = { ...link, file, shared: true };
|
|
834
|
+
await writeContext(projectId, { ...current, plans: current.plans.map((entry) => (entry.id === id ? moved : entry)) });
|
|
835
|
+
const context = await readContext(projectId);
|
|
836
|
+
return { plan: context.plans.find((entry) => entry.id === id), context };
|
|
837
|
+
});
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Move a shared plan back into the user's own plans; the reverse of
|
|
842
|
+
* `sharePlan`. A plan the user moved keeps its id; a plan that only ever
|
|
843
|
+
* lived in the team's folder (`shared:<file>`) gets a manifest entry now.
|
|
844
|
+
*/
|
|
845
|
+
const unsharePlan = async (projectId, planId) => {
|
|
846
|
+
const id = asNonEmptyString(planId);
|
|
847
|
+
if (!id) throw new Error('planId is required');
|
|
848
|
+
return withWriteLock(projectId, async () => {
|
|
849
|
+
const dir = await sharedPlansDirFor(projectId);
|
|
850
|
+
if (!dir) return null;
|
|
851
|
+
const current = await readStoredContext(projectId);
|
|
852
|
+
const ownLink = current.plans.find((entry) => entry.id === id && entry.shared);
|
|
853
|
+
const sharedFile = ownLink ? ownLink.file : sharedPlanFileOf(id);
|
|
854
|
+
if (!sharedFile) return null;
|
|
855
|
+
const from = path.join(dir, sharedFile);
|
|
856
|
+
let raw;
|
|
857
|
+
try {
|
|
858
|
+
raw = await fsPromises.readFile(from, 'utf8');
|
|
859
|
+
} catch (error) {
|
|
860
|
+
if (error && error.code === 'ENOENT') return null;
|
|
861
|
+
throw error;
|
|
862
|
+
}
|
|
863
|
+
const plansDir = plansDirFor(projectId);
|
|
864
|
+
await fsPromises.mkdir(plansDir, { recursive: true });
|
|
865
|
+
const taken = new Set(current.plans.filter((entry) => !entry.shared).map((entry) => entry.file));
|
|
866
|
+
const file = await freeFileNameIn(plansDir, sharedFile, taken);
|
|
867
|
+
await moveFile(from, path.join(plansDir, file));
|
|
868
|
+
const title = parsePlanMarkdown(raw).title;
|
|
869
|
+
const link = ownLink
|
|
870
|
+
? { ...ownLink, file, title, shared: false }
|
|
871
|
+
: { id: idFactory(), file, title, createdAt: Date.now(), pinned: false, shared: false };
|
|
872
|
+
const plans = ownLink
|
|
873
|
+
? current.plans.map((entry) => (entry.id === id ? link : entry))
|
|
874
|
+
: [link, ...current.plans];
|
|
875
|
+
await writeContext(projectId, { ...current, plans });
|
|
876
|
+
const { shared: _movedFlag, ...publicLink } = link;
|
|
877
|
+
return { plan: { ...publicLink, source: 'personal' }, context: await readContext(projectId) };
|
|
655
878
|
});
|
|
656
879
|
};
|
|
657
880
|
|
|
@@ -666,6 +889,8 @@ export const createProjectContextRuntime = (deps) => {
|
|
|
666
889
|
createPlan,
|
|
667
890
|
setPlanPinned,
|
|
668
891
|
deletePlan,
|
|
892
|
+
sharePlan,
|
|
893
|
+
unsharePlan,
|
|
669
894
|
contextPathFor,
|
|
670
895
|
plansDirFor,
|
|
671
896
|
};
|
|
@@ -52,6 +52,7 @@ describe('readContext', () => {
|
|
|
52
52
|
notes: [],
|
|
53
53
|
todos: [],
|
|
54
54
|
plans: [],
|
|
55
|
+
sharedPlansDir: null,
|
|
55
56
|
});
|
|
56
57
|
});
|
|
57
58
|
|
|
@@ -134,7 +135,7 @@ describe('legacy migration', () => {
|
|
|
134
135
|
const context = await runtime.readContext(PROJECT_ID);
|
|
135
136
|
expect(context.notes.map((note) => note.body)).toEqual(['legacy notes']);
|
|
136
137
|
expect(context.todos).toEqual([{ id: 't1', text: 'legacy todo', completed: true, createdAt: 5 }]);
|
|
137
|
-
expect(context.plans).toEqual([{ id: 'p1', file: '10-old.md', title: 'Old plan', createdAt: 10, pinned: false }]);
|
|
138
|
+
expect(context.plans).toEqual([{ id: 'p1', file: '10-old.md', title: 'Old plan', createdAt: 10, pinned: false, source: 'personal' }]);
|
|
138
139
|
|
|
139
140
|
const remaining = await readJson(legacyConfigPath());
|
|
140
141
|
expect(remaining).toEqual({
|
|
@@ -152,7 +153,7 @@ describe('legacy migration', () => {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
const context = await runtime.readContext(PROJECT_ID);
|
|
155
|
-
expect(context.plans).toEqual([{ id: 'p1', file: 'stray.md', title: 'Stray', createdAt: 10, pinned: false }]);
|
|
156
|
+
expect(context.plans).toEqual([{ id: 'p1', file: 'stray.md', title: 'Stray', createdAt: 10, pinned: false, source: 'personal' }]);
|
|
156
157
|
expect(await fsPromises.readFile(path.join(plansDir(), 'stray.md'), 'utf8')).toContain('recovered');
|
|
157
158
|
});
|
|
158
159
|
|
|
@@ -170,7 +171,7 @@ describe('legacy migration', () => {
|
|
|
170
171
|
test('does not run when the legacy config holds no context keys', async () => {
|
|
171
172
|
await writeJson(legacyConfigPath(), { 'setup-worktree': ['bun install'] });
|
|
172
173
|
|
|
173
|
-
expect(await runtime.readContext(PROJECT_ID)).toEqual({ version: 2, notes: [], todos: [], plans: [] });
|
|
174
|
+
expect(await runtime.readContext(PROJECT_ID)).toEqual({ version: 2, notes: [], todos: [], plans: [], sharedPlansDir: null });
|
|
174
175
|
await expect(fsPromises.access(contextPath())).rejects.toThrow();
|
|
175
176
|
expect(await readJson(legacyConfigPath())).toEqual({ 'setup-worktree': ['bun install'] });
|
|
176
177
|
});
|
|
@@ -479,6 +480,108 @@ describe('plans', () => {
|
|
|
479
480
|
});
|
|
480
481
|
});
|
|
481
482
|
|
|
483
|
+
describe('shared plans', () => {
|
|
484
|
+
let sharedDir;
|
|
485
|
+
let sharedRuntime;
|
|
486
|
+
|
|
487
|
+
beforeEach(async () => {
|
|
488
|
+
sharedDir = path.join(projectsDirPath, 'repo', 'docs', 'plans');
|
|
489
|
+
sharedRuntime = createProjectContextRuntime({
|
|
490
|
+
fsPromises,
|
|
491
|
+
path,
|
|
492
|
+
projectsDirPath,
|
|
493
|
+
createId: () => `plan-${++idCounter}`,
|
|
494
|
+
resolveSharedPlansDir: async () => sharedDir,
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('lists the shared folder\'s markdown files after the personal plans, marked shared, and reads them by file', async () => {
|
|
499
|
+
await fsPromises.mkdir(sharedDir, { recursive: true });
|
|
500
|
+
await fsPromises.writeFile(path.join(sharedDir, 'roadmap.md'), '# Roadmap\n\n- ship it\n');
|
|
501
|
+
await fsPromises.writeFile(path.join(sharedDir, 'notes.txt'), 'not a plan');
|
|
502
|
+
await fsPromises.writeFile(path.join(sharedDir, 'untitled.md'), 'first line only');
|
|
503
|
+
const mine = await sharedRuntime.createPlan(PROJECT_ID, { title: 'Mine', body: 'x' });
|
|
504
|
+
|
|
505
|
+
const context = await sharedRuntime.readContext(PROJECT_ID);
|
|
506
|
+
expect(context.sharedPlansDir).toBe(sharedDir);
|
|
507
|
+
expect(context.plans.map((plan) => `${plan.id}:${plan.source}`)).toEqual(expect.arrayContaining([
|
|
508
|
+
`${mine.plan.id}:personal`, 'shared:roadmap.md:shared', 'shared:untitled.md:shared',
|
|
509
|
+
]));
|
|
510
|
+
expect(context.plans[0].id).toBe(mine.plan.id);
|
|
511
|
+
expect(context.plans.find((plan) => plan.id === 'shared:untitled.md').title).toBe('first line only');
|
|
512
|
+
|
|
513
|
+
const read = await sharedRuntime.readPlan(PROJECT_ID, 'shared:roadmap.md');
|
|
514
|
+
expect(read.title).toBe('Roadmap');
|
|
515
|
+
expect(read.raw).toBe('# Roadmap\n\n- ship it\n');
|
|
516
|
+
expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:missing.md')).toBeNull();
|
|
517
|
+
expect(await sharedRuntime.readPlan(PROJECT_ID, 'shared:../escape.md')).toBeNull();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test('a missing shared folder lists nothing; no folder configured lists nothing', async () => {
|
|
521
|
+
expect((await sharedRuntime.readContext(PROJECT_ID)).plans).toEqual([]);
|
|
522
|
+
expect((await runtime.readContext(PROJECT_ID)).sharedPlansDir).toBeNull();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test('updates and deletes a shared plan in place, verbatim', async () => {
|
|
526
|
+
await fsPromises.mkdir(sharedDir, { recursive: true });
|
|
527
|
+
await fsPromises.writeFile(path.join(sharedDir, 'a.md'), '# A\n');
|
|
528
|
+
const updated = await sharedRuntime.updatePlan(PROJECT_ID, 'shared:a.md', { raw: '# Renamed\n\nbody\n' });
|
|
529
|
+
expect(updated.plan.title).toBe('Renamed');
|
|
530
|
+
expect(updated.plan.source).toBe('shared');
|
|
531
|
+
expect(await fsPromises.readFile(path.join(sharedDir, 'a.md'), 'utf8')).toBe('# Renamed\n\nbody\n');
|
|
532
|
+
expect(await sharedRuntime.updatePlan(PROJECT_ID, 'shared:gone.md', { raw: 'x' })).toBeNull();
|
|
533
|
+
expect(await sharedRuntime.setPlanPinned(PROJECT_ID, 'shared:a.md', true)).toBeNull();
|
|
534
|
+
|
|
535
|
+
const deleted = await sharedRuntime.deletePlan(PROJECT_ID, 'shared:a.md');
|
|
536
|
+
expect(deleted.deleted).toBe(true);
|
|
537
|
+
await expect(fsPromises.access(path.join(sharedDir, 'a.md'))).rejects.toThrow();
|
|
538
|
+
expect((await sharedRuntime.deletePlan(PROJECT_ID, 'shared:a.md')).deleted).toBe(false);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('share moves a personal plan into the shared folder and unshare brings it back, avoiding name collisions', async () => {
|
|
542
|
+
const { plan } = await sharedRuntime.createPlan(PROJECT_ID, { title: 'Mine', body: 'body' });
|
|
543
|
+
const shared = await sharedRuntime.sharePlan(PROJECT_ID, plan.id);
|
|
544
|
+
// The id survives the move: a session that attached the plan still finds it.
|
|
545
|
+
expect(shared.plan.id).toBe(plan.id);
|
|
546
|
+
expect(shared.plan.source).toBe('shared');
|
|
547
|
+
expect(shared.context.plans.map((entry) => `${entry.id}:${entry.source}`)).toEqual([`${plan.id}:shared`]);
|
|
548
|
+
await expect(fsPromises.access(path.join(plansDir(), plan.file))).rejects.toThrow();
|
|
549
|
+
expect(await fsPromises.readFile(path.join(sharedDir, plan.file), 'utf8')).toBe('# Mine\n\nbody');
|
|
550
|
+
expect((await readJson(path.join(projectsDirPath, PROJECT_ID, 'context.json'))).plans[0]).toMatchObject({ id: plan.id, shared: true });
|
|
551
|
+
const readMoved = await sharedRuntime.readPlan(PROJECT_ID, plan.id);
|
|
552
|
+
expect(readMoved.source).toBe('shared');
|
|
553
|
+
expect(readMoved.raw).toBe('# Mine\n\nbody');
|
|
554
|
+
expect((await sharedRuntime.updatePlan(PROJECT_ID, plan.id, { raw: '# Mine v2\n' })).plan).toMatchObject({ id: plan.id, title: 'Mine v2', source: 'shared' });
|
|
555
|
+
expect(await fsPromises.readFile(path.join(sharedDir, plan.file), 'utf8')).toBe('# Mine v2\n');
|
|
556
|
+
|
|
557
|
+
// A personal file with the same name already exists: the returning plan gets a suffix, same id.
|
|
558
|
+
await fsPromises.mkdir(plansDir(), { recursive: true });
|
|
559
|
+
await fsPromises.writeFile(path.join(plansDir(), plan.file), 'squatter');
|
|
560
|
+
const back = await sharedRuntime.unsharePlan(PROJECT_ID, plan.id);
|
|
561
|
+
expect(back.plan.id).toBe(plan.id);
|
|
562
|
+
expect(back.plan.source).toBe('personal');
|
|
563
|
+
expect(back.plan.file).toBe(plan.file.replace(/\.md$/, '-1.md'));
|
|
564
|
+
expect(back.plan.title).toBe('Mine v2');
|
|
565
|
+
expect(back.context.plans.map((entry) => `${entry.id}:${entry.source}`)).toEqual([`${plan.id}:personal`]);
|
|
566
|
+
await expect(fsPromises.access(path.join(sharedDir, plan.file))).rejects.toThrow();
|
|
567
|
+
expect(await sharedRuntime.unsharePlan(PROJECT_ID, plan.id)).toBeNull();
|
|
568
|
+
expect(await sharedRuntime.sharePlan(PROJECT_ID, 'nope')).toBeNull();
|
|
569
|
+
|
|
570
|
+
// A plan that only ever lived in the team's folder gets an id of its own on the way in.
|
|
571
|
+
await fsPromises.writeFile(path.join(sharedDir, 'foreign.md'), '# Foreign\n');
|
|
572
|
+
const adopted = await sharedRuntime.unsharePlan(PROJECT_ID, 'shared:foreign.md');
|
|
573
|
+
expect(adopted.plan.id).not.toMatch(/^shared:/);
|
|
574
|
+
expect(adopted.plan.title).toBe('Foreign');
|
|
575
|
+
expect(adopted.plan.source).toBe('personal');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test('share is refused without a shared plans folder', async () => {
|
|
579
|
+
const { plan } = await runtime.createPlan(PROJECT_ID, { title: 'Mine', body: 'x' });
|
|
580
|
+
await expect(runtime.sharePlan(PROJECT_ID, plan.id)).rejects.toThrow('shared plans folder is required');
|
|
581
|
+
expect((await runtime.readContext(PROJECT_ID)).plans.map((entry) => entry.id)).toEqual([plan.id]);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
482
585
|
describe('parsePlanMarkdown', () => {
|
|
483
586
|
test('reads the leading heading as the title', () => {
|
|
484
587
|
expect(parsePlanMarkdown('# Title\n\nbody')).toEqual({ title: 'Title', body: 'body' });
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Projects
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Server-owned storage for a project's per-user config file,
|
|
6
|
+
`~/.config/openchamber/projects/<projectId>.json`. The file holds two
|
|
7
|
+
families of keys with different writers, and this module is the only place
|
|
8
|
+
that writes it:
|
|
9
|
+
|
|
10
|
+
| Keys | Owner | Reached through |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `version`, `scheduledTasks` | `project-config.js` (scheduled-task runtime) | `/api/projects/:projectId/scheduled-tasks/*` |
|
|
13
|
+
| `setup-worktree`, `setup-worktree-wait`, `projectActions`, `projectActionsPrimaryId`, `draftStarters`, `projectPath` | `project-setup.js` via `readProjectSetup` / `updateProjectSetup` on the same runtime | `GET/PUT /api/projects/:projectId/config` (`routes.js`) |
|
|
14
|
+
|
|
15
|
+
Notes, todos, and plans moved out of this file to `packages/web/server/lib/project-context`.
|
|
16
|
+
|
|
17
|
+
A second, optional source is the team's shared file, `<repo>/.aiworker/project.json`
|
|
18
|
+
(`version: 1`; `setupWorktree`, `setupWorktreeWait`, `projectActions`, `draftStarters`,
|
|
19
|
+
`plansDir`). The server reads it from the checkout the project id names
|
|
20
|
+
(`projectPathFromId`). `GET /api/projects/:projectId/config`
|
|
21
|
+
returns one merged view: what runs at the top level, plus `shared` and `personal`
|
|
22
|
+
blocks so a page can edit the personal file without copying a teammate's entry into it.
|
|
23
|
+
|
|
24
|
+
| Field | Merge rule |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `setupWorktree` | shared first, then personal; personal `setupWorktreeMode: "replace"` uses the personal list only |
|
|
27
|
+
| `setupWorktreeWait` | personal when the personal file sets it, else shared, else `false` |
|
|
28
|
+
| `projectActions` | union by `id`; a personal action replaces the shared one with the same id; ids in personal `hiddenSharedActionIds` are dropped; every entry carries `source` |
|
|
29
|
+
| `projectActionsPrimaryId` | personal only |
|
|
30
|
+
| `draftStarters` | union by `type:name`, shared first, every entry carries `source` |
|
|
31
|
+
| `plansDir` | shared only |
|
|
32
|
+
|
|
33
|
+
A shared file that exists but cannot be parsed (or names a `plansDir` outside the
|
|
34
|
+
repo) is `shared.status: "invalid"` with a `reason`; the personal setup is still
|
|
35
|
+
served. It is never treated as "no shared setup".
|
|
36
|
+
|
|
37
|
+
### Writing the shared file
|
|
38
|
+
|
|
39
|
+
`PUT /api/projects/:projectId/config/shared` (`updateSharedProjectSetup`) is
|
|
40
|
+
the only writer. The patch replaces the keys it names over the current file
|
|
41
|
+
(a broken file counts as empty, so a write repairs it); the result is written
|
|
42
|
+
pretty-printed with `version` first and only the keys that carry something
|
|
43
|
+
(`serializeSharedProjectConfig`), because the file is committed and reviewed.
|
|
44
|
+
A result with nothing in it removes the file and the `.aiworker` folder
|
|
45
|
+
when that leaves it empty, so unsharing the last item leaves no trace. The
|
|
46
|
+
write refuses a checkout that does not exist and a `plansDir` outside the
|
|
47
|
+
repo. The writer has seen what it shared, so its personal trust record is set
|
|
48
|
+
to the new hash; teammates still get the prompt. The shared UI composes
|
|
49
|
+
"share" and "make personal" as a shared write followed by a personal write.
|
|
50
|
+
|
|
51
|
+
### Trust
|
|
52
|
+
|
|
53
|
+
Shared setup commands and shared actions run on the machine of whoever pulls
|
|
54
|
+
the repo, so they run only after the user has seen them. The view carries
|
|
55
|
+
`trust: { hash, trusted }`: `hash` is `sharedTrustHashOf(shared)`, a SHA-256
|
|
56
|
+
over the executable parts (`setupWorktree` and each action's `id`, `command`,
|
|
57
|
+
`runIn`, actions sorted by id; names and icons do not count), or `null` when
|
|
58
|
+
nothing executes. `trusted` is true when nothing executes or the personal
|
|
59
|
+
file's `sharedTrust.hash` equals the current hash, so a pull that changes a
|
|
60
|
+
command brings the prompt back. The client records an answer with a PUT of
|
|
61
|
+
`sharedTrustHash` (`null` forgets it). The prompt itself lives in the shared
|
|
62
|
+
UI (`packages/ui/src/lib/sharedTrustConfirmation.ts`).
|
|
63
|
+
|
|
64
|
+
## Modules
|
|
65
|
+
|
|
66
|
+
- `project-id.js` — `createProjectIdFromPath` / `projectPathFromId`: the path-derived id (`path_<base64url>`) that names the file, and the checkout path back from it. The shared UI derives the same id (`packages/ui/src/lib/projectId.ts`); both sides must agree.
|
|
67
|
+
- `project-config.js` — `createProjectConfigRuntime`: raw read, atomic write, the cross-process file lock (Electron and a CLI `serve` can share one projects dir), scheduled-task normalization, and the project-setup read/update.
|
|
68
|
+
- `project-setup.js` — sanitizers, the shared-file parser (`parseSharedProjectConfig`, `normalizePlansDir`), the merge (`mergeProjectSetup`), and the personal view for the setup keys. Mirrored in the VS Code extension host (`packages/vscode/src/project-setup.ts`), which owns the same file when the webview has no AiWorker server; keep the two in sync.
|
|
69
|
+
- `routes.js` — the setup routes. `/api/projects` is on the JSON-body allowlist in `opencode/core-routes.js`.
|
|
70
|
+
|
|
71
|
+
## Invariants
|
|
72
|
+
|
|
73
|
+
- **Every write is a locked read-modify-write of the whole document.** Keys the writer does not own, and keys from newer builds, come back out unchanged. A setup update and a scheduled-task update never clobber each other.
|
|
74
|
+
- **A wrongly shaped key is a 400, not a silent drop.** `projectSetupPatchToStored` throws; the file is untouched. Values inside a well-shaped key are sanitized (trimmed, capped, deduplicated) rather than rejected.
|
|
75
|
+
- **The client never composes the path.** `packages/ui/src/lib/openchamberConfig.ts` speaks only HTTP; the same code serves web, desktop, VS Code, and the phone, including a phone on a remote instance.
|
|
76
|
+
- **`AIWORKER_DATA_DIR` moves this directory too.** Every AiWorker folder hangs off the one root; a custom root gets `projects/`, `themes/`, and `speech-models/` copied in from `~/.config/openchamber` once at startup (copied, not moved: a second instance beside the default one must not strip it) (`lib/data-dir-migration.js`). A scratch server started with its own data dir therefore never touches the real project configs.
|