@aiworker/web 1.22.2 → 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/bin/cli.js +7 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
- package/dist/assets/DiffView-DXjVsEOw.js +4 -0
- package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-aig6zJjg.js} +1 -1
- package/dist/assets/FilesView-CJdG0-wk.js +17 -0
- package/dist/assets/{GitView-VQQkYuUw.js → GitView-BXsS3jG6.js} +4 -4
- package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-D_hpXBnF.js} +1 -1
- package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-BoUiLQG1.js} +1 -1
- 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-BzcZPlOq.js → PatchDiff-8p6nLJaU.js} +1 -1
- package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
- package/dist/assets/{PlanView-DyDt_nZ_.js → PlanView-CJTmOF6I.js} +2 -2
- package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
- package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Cpb4hM6q.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-DtZLzrSK.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
- package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.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-Bl3LSbjn.js → index-jXSK5RrB.js} +1 -1
- package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-B_x_rVon.js} +1 -1
- package/dist/assets/{isFilePlainText-CWFPwmnU.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-DnBAoktx.js → main-CzdPuL0s.js} +2 -2
- package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
- package/dist/assets/{miniChat-BKyvLseN.js → miniChat-CCkqKOLp.js} +2 -2
- package/dist/assets/{mobile-DKjeFgMN.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-TEQCmzW8.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-D7sNpuL2.js → useAppFontEffects-CPKeOpUo.js} +160 -157
- 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-Dp3KMRQG.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
- package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
- package/dist/assets/{web-components-_mNEMjFY.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 +5 -43
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +2 -3
- package/server/index.d.ts +15 -0
- package/server/index.js +45 -9
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/event-stream/runtime.test.js +12 -12
- package/server/lib/fs/DOCUMENTATION.md +3 -0
- package/server/lib/fs/routes.js +31 -0
- package/server/lib/fs/routes.test.js +110 -1
- package/server/lib/git/DOCUMENTATION.md +29 -5
- package/server/lib/git/routes.js +24 -4
- package/server/lib/git/service.js +184 -198
- package/server/lib/git/service.test.js +422 -25
- package/server/lib/inherited-env.js +16 -9
- package/server/lib/inherited-env.test.js +8 -9
- package/server/lib/message-queue/DOCUMENTATION.md +4 -1
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/opencode/DOCUMENTATION.md +17 -1
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/feature-routes-runtime.js +2 -0
- package/server/lib/opencode/openchamber-routes.js +64 -4
- package/server/lib/opencode/openchamber-routes.test.js +128 -1
- package/server/lib/opencode/routes.js +5 -3
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +85 -41
- package/server/lib/opencode/settings-helpers.test.js +166 -1
- package/server/lib/opencode/settings-registry.json +747 -0
- package/server/lib/opencode/settings-runtime.js +139 -18
- package/server/lib/opencode/settings-runtime.test.js +163 -5
- 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 +15 -0
- 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 +142 -9
- 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 +31 -1
- 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/index.js +7 -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 +31 -0
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +1 -1
- package/server/lib/quota/providers/opencode-go.test.js +26 -1
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/relay/DOCUMENTATION.md +55 -0
- 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 +37 -8
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +66 -17
- package/server/lib/session-assist/runtime.js +6 -10
- package/server/lib/session-goal/runtime.js +4 -9
- package/server/lib/small-model/DOCUMENTATION.md +3 -2
- package/server/lib/small-model/call.js +9 -4
- package/server/lib/small-model/call.test.js +29 -0
- package/server/lib/small-model/index.js +5 -10
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/terminal/DOCUMENTATION.md +4 -4
- package/server/lib/terminal/runtime.js +11 -6
- package/server/lib/terminal/runtime.test.js +20 -13
- package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
- package/server/lib/walkthrough/model-settings.js +8 -12
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/sources.js +23 -2
- package/dist/assets/DiffView-hDBKesA_.js +0 -4
- package/dist/assets/FilesView-CdWzeOp9.js +0 -17
- package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
- package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
- package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
- package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
- package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
- package/dist/assets/de-B5JaA3SA.js +0 -10
- package/dist/assets/es-EPIH88ii.js +0 -16
- package/dist/assets/fr-CAMfxUdZ.js +0 -14
- package/dist/assets/index-DF5kMP-i.js +0 -39
- package/dist/assets/index-Dl3xbAB_.css +0 -1
- package/dist/assets/ja-CifOE1VN.js +0 -16
- package/dist/assets/ko-BBDyAyYQ.js +0 -16
- package/dist/assets/main-MhSaOZoB.js +0 -930
- package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
- package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
- package/dist/assets/pl-DgEHYfFM.js +0 -16
- package/dist/assets/pt-BR-DuWHojUw.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
- package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
- package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
- package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
- package/dist/assets/tr-D5m2yIWL.js +0 -7
- package/dist/assets/uk-D_q60oog.js +0 -16
- package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
- package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
- package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
- package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
package/dist/index.html
CHANGED
|
@@ -509,16 +509,16 @@
|
|
|
509
509
|
pointer-events: none;
|
|
510
510
|
}
|
|
511
511
|
</style>
|
|
512
|
-
<script type="module" crossorigin src="/assets/main-
|
|
512
|
+
<script type="module" crossorigin src="/assets/main-CzdPuL0s.js"></script>
|
|
513
513
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
514
514
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
515
515
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
516
516
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
517
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-
|
|
517
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-BPAANqvH.js">
|
|
518
518
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
519
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
520
|
-
<link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-
|
|
521
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
519
|
+
<link rel="modulepreload" crossorigin href="/assets/index-DVwMarHI.js">
|
|
520
|
+
<link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-BdA6bdky.js">
|
|
521
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B68h5S5B.css">
|
|
522
522
|
</head>
|
|
523
523
|
<body class="h-full bg-background text-foreground">
|
|
524
524
|
<div id="root" class="h-full">
|
|
@@ -603,44 +603,6 @@
|
|
|
603
603
|
}, 10000);
|
|
604
604
|
</script>
|
|
605
605
|
|
|
606
|
-
<!-- CSS Font Loading API for reliable Nerd Font loading. The ~2 MB of
|
|
607
|
-
Nerd Fonts are only used by the terminal viewport, so they are no
|
|
608
|
-
longer downloaded on every cold start: the terminal requests them on
|
|
609
|
-
first mount through this idempotent hook. -->
|
|
610
|
-
<script>
|
|
611
|
-
(function() {
|
|
612
|
-
let nerdFontsPromise = null;
|
|
613
|
-
window.__aiworkerEnsureNerdFonts = function() {
|
|
614
|
-
if (nerdFontsPromise) {
|
|
615
|
-
return nerdFontsPromise;
|
|
616
|
-
}
|
|
617
|
-
const fonts = [
|
|
618
|
-
{
|
|
619
|
-
name: 'JetBrainsMono Nerd Font',
|
|
620
|
-
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2'
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
name: 'FiraCode Nerd Font',
|
|
624
|
-
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2'
|
|
625
|
-
}
|
|
626
|
-
];
|
|
627
|
-
|
|
628
|
-
const fontPromises = fonts.map(font => {
|
|
629
|
-
const fontFace = new FontFace(font.name, `url(${font.url}) format('woff2')`);
|
|
630
|
-
document.fonts.add(fontFace);
|
|
631
|
-
return fontFace.load().catch(err => {
|
|
632
|
-
console.warn(`Failed to load font: ${font.name}`, err);
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
nerdFontsPromise = Promise.allSettled(fontPromises).then(() => {
|
|
637
|
-
document.documentElement.classList.add('fonts-loaded');
|
|
638
|
-
});
|
|
639
|
-
return nerdFontsPromise;
|
|
640
|
-
};
|
|
641
|
-
})();
|
|
642
|
-
</script>
|
|
643
|
-
|
|
644
606
|
<!-- Polyfill for process before loading React -->
|
|
645
607
|
<script>
|
|
646
608
|
if (typeof process === 'undefined') {
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,15 +74,15 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-CCkqKOLp.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
81
81
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
82
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-
|
|
82
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-BPAANqvH.js">
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
84
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
85
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
84
|
+
<link rel="modulepreload" crossorigin href="/assets/index-DVwMarHI.js">
|
|
85
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B68h5S5B.css">
|
|
86
86
|
</head>
|
|
87
87
|
<body class="h-full bg-background text-foreground">
|
|
88
88
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -63,15 +63,15 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-mvxTo055.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
|
|
70
70
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
|
|
71
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-
|
|
71
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-zod-BPAANqvH.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
|
|
73
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
74
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
73
|
+
<link rel="modulepreload" crossorigin href="/assets/index-DVwMarHI.js">
|
|
74
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B68h5S5B.css">
|
|
75
75
|
</head>
|
|
76
76
|
<body class="h-full bg-background text-foreground">
|
|
77
77
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiworker/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./server/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^1.1.0",
|
|
27
27
|
"@octokit/rest": "^22.0.1",
|
|
28
|
-
"@opencode-ai/sdk": "1.18.
|
|
28
|
+
"@opencode-ai/sdk": "1.18.30",
|
|
29
29
|
"@simplewebauthn/server": "13.3.1",
|
|
30
30
|
"bun-pty": "^0.4.5",
|
|
31
31
|
"compression": "^1.8.1",
|
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
"eslint": "^9.33.0",
|
|
78
78
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
79
79
|
"eslint-plugin-react-refresh": "^0.5.0",
|
|
80
|
-
"ghostty-web": "0.4.0",
|
|
81
80
|
"globals": "^16.3.0",
|
|
82
81
|
"next-themes": "^0.4.6",
|
|
83
82
|
"nodemon": "^3.1.7",
|
package/server/index.d.ts
CHANGED
|
@@ -11,12 +11,27 @@ export interface WebUiServerController {
|
|
|
11
11
|
stop: (options?: { exitProcess?: boolean }) => Promise<void>;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface DesktopUpdateInfo {
|
|
15
|
+
available: boolean;
|
|
16
|
+
currentVersion?: string;
|
|
17
|
+
version?: string | null;
|
|
18
|
+
body?: string | null;
|
|
19
|
+
date?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DesktopUpdater {
|
|
23
|
+
check: () => Promise<DesktopUpdateInfo>;
|
|
24
|
+
install: () => Promise<DesktopUpdateInfo>;
|
|
25
|
+
restart: () => Promise<void> | void;
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
export interface StartWebUiServerOptions {
|
|
15
29
|
port?: number;
|
|
16
30
|
host?: string;
|
|
17
31
|
attachSignals?: boolean;
|
|
18
32
|
exitOnShutdown?: boolean;
|
|
19
33
|
uiPassword?: string | null;
|
|
34
|
+
desktopUpdater?: DesktopUpdater;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
export declare function startWebUiServer(
|
package/server/index.js
CHANGED
|
@@ -94,6 +94,7 @@ import { createPermissionAutoAcceptRuntime } from './lib/permission-auto-accept/
|
|
|
94
94
|
import { createMessageQueueRuntime } from './lib/message-queue/runtime.js';
|
|
95
95
|
import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
|
|
96
96
|
import { createProjectConfigRuntime } from './lib/projects/project-config.js';
|
|
97
|
+
import { migrateLegacyUserDirs } from './lib/data-dir-migration.js';
|
|
97
98
|
import { createProjectContextRuntime } from './lib/project-context/runtime.js';
|
|
98
99
|
import { createAgentMemoryRuntime } from './lib/agent-memory/runtime.js';
|
|
99
100
|
import { createAgentMemoryActions } from './lib/agent-memory/actions.js';
|
|
@@ -117,6 +118,10 @@ import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
|
|
|
117
118
|
import { createAiWorkerControlService } from './lib/openchamber-control/service.js';
|
|
118
119
|
import { AiWorkerControlError } from './lib/openchamber-control/error.js';
|
|
119
120
|
import webPush from 'web-push';
|
|
121
|
+
import { applyConnectAttemptTimeout } from './lib/network-defaults.js';
|
|
122
|
+
|
|
123
|
+
// Background CLI launches enter here in a fresh process, without CLI defaults.
|
|
124
|
+
applyConnectAttemptTimeout();
|
|
120
125
|
|
|
121
126
|
const __filename = fileURLToPath(import.meta.url);
|
|
122
127
|
const __dirname = path.dirname(__filename);
|
|
@@ -259,14 +264,20 @@ const normalizeManagedRemoteTunnelPresets = (...args) =>
|
|
|
259
264
|
const normalizeManagedRemoteTunnelPresetTokens = (...args) =>
|
|
260
265
|
settingsNormalizationRuntime.normalizeManagedRemoteTunnelPresetTokens(...args);
|
|
261
266
|
const isUnsafeSkillRelativePath = (...args) => settingsNormalizationRuntime.isUnsafeSkillRelativePath(...args);
|
|
262
|
-
const sanitizeTypographySizesPartial = (...args) =>
|
|
263
|
-
settingsNormalizationRuntime.sanitizeTypographySizesPartial(...args);
|
|
264
267
|
const normalizeStringArray = (...args) => settingsNormalizationRuntime.normalizeStringArray(...args);
|
|
265
268
|
const sanitizeModelRefs = (...args) => settingsNormalizationRuntime.sanitizeModelRefs(...args);
|
|
266
269
|
const sanitizeSkillCatalogs = (...args) => settingsNormalizationRuntime.sanitizeSkillCatalogs(...args);
|
|
267
270
|
const sanitizeProjects = (...args) => settingsNormalizationRuntime.sanitizeProjects(...args);
|
|
268
271
|
|
|
269
|
-
|
|
272
|
+
// Every AiWorker-owned file and folder hangs off one root: the default
|
|
273
|
+
// `~/.config/openchamber`, or `AIWORKER_DATA_DIR` when set. The user
|
|
274
|
+
// folders (`projects/`, `themes/`, `speech-models/`) are copied into a custom
|
|
275
|
+
// root once at startup (`migrateLegacyUserDirs`), because they used to ignore
|
|
276
|
+
// the variable.
|
|
277
|
+
const AIWORKER_DEFAULT_CONFIG_ROOT = path.join(os.homedir(), '.config', 'aiworker');
|
|
278
|
+
const AIWORKER_USER_CONFIG_ROOT = process.env.AIWORKER_DATA_DIR
|
|
279
|
+
? path.resolve(process.env.AIWORKER_DATA_DIR)
|
|
280
|
+
: AIWORKER_DEFAULT_CONFIG_ROOT;
|
|
270
281
|
const AIWORKER_USER_THEMES_DIR = path.join(AIWORKER_USER_CONFIG_ROOT, 'themes');
|
|
271
282
|
const AIWORKER_PROJECTS_CONFIG_DIR = path.join(AIWORKER_USER_CONFIG_ROOT, 'projects');
|
|
272
283
|
// AIWORKER_CHATS_DIR relocates managed chat worktrees — needed when the
|
|
@@ -305,9 +316,7 @@ const maybeCacheSessionInfoFromEvent = (...args) => notificationTemplateRuntime.
|
|
|
305
316
|
const buildTemplateVariables = (...args) => notificationTemplateRuntime.buildTemplateVariables(...args);
|
|
306
317
|
const getCachedZenModels = (...args) => notificationTemplateRuntime.getCachedZenModels(...args);
|
|
307
318
|
|
|
308
|
-
const AIWORKER_DATA_DIR =
|
|
309
|
-
? path.resolve(process.env.AIWORKER_DATA_DIR)
|
|
310
|
-
: path.join(os.homedir(), '.config', 'aiworker');
|
|
319
|
+
const AIWORKER_DATA_DIR = AIWORKER_USER_CONFIG_ROOT;
|
|
311
320
|
const SETTINGS_FILE_PATH = path.join(AIWORKER_DATA_DIR, 'settings.json');
|
|
312
321
|
const PUSH_SUBSCRIPTIONS_FILE_PATH = path.join(AIWORKER_DATA_DIR, 'push-subscriptions.json');
|
|
313
322
|
const APNS_TOKENS_FILE_PATH = path.join(AIWORKER_DATA_DIR, 'apns-tokens.json');
|
|
@@ -345,7 +354,6 @@ const settingsHelpers = createSettingsHelpers({
|
|
|
345
354
|
normalizeManagedRemoteTunnelHostname,
|
|
346
355
|
normalizeManagedRemoteTunnelPresets,
|
|
347
356
|
normalizeManagedRemoteTunnelPresetTokens,
|
|
348
|
-
sanitizeTypographySizesPartial,
|
|
349
357
|
normalizeStringArray,
|
|
350
358
|
sanitizeModelRefs,
|
|
351
359
|
sanitizeSkillCatalogs,
|
|
@@ -484,6 +492,17 @@ const getUpstreamStallTimeoutMs = () => (
|
|
|
484
492
|
: DEFAULT_UPSTREAM_STALL_TIMEOUT_MS
|
|
485
493
|
);
|
|
486
494
|
|
|
495
|
+
const movedUserDirs = await migrateLegacyUserDirs({
|
|
496
|
+
fsPromises,
|
|
497
|
+
path,
|
|
498
|
+
dataDir: AIWORKER_USER_CONFIG_ROOT,
|
|
499
|
+
legacyRoot: AIWORKER_DEFAULT_CONFIG_ROOT,
|
|
500
|
+
warn: (message) => console.warn(`[data-dir] ${message}`),
|
|
501
|
+
});
|
|
502
|
+
if (movedUserDirs.length > 0) {
|
|
503
|
+
console.log(`[data-dir] Copied ${movedUserDirs.join(', ')} into ${AIWORKER_USER_CONFIG_ROOT}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
487
506
|
const projectConfigRuntime = createProjectConfigRuntime({
|
|
488
507
|
fsPromises,
|
|
489
508
|
path,
|
|
@@ -494,6 +513,7 @@ const projectContextRuntime = createProjectContextRuntime({
|
|
|
494
513
|
fsPromises,
|
|
495
514
|
path,
|
|
496
515
|
projectsDirPath: AIWORKER_PROJECTS_CONFIG_DIR,
|
|
516
|
+
resolveSharedPlansDir: (projectId) => projectConfigRuntime.resolveSharedPlansDir(projectId),
|
|
497
517
|
});
|
|
498
518
|
|
|
499
519
|
const agentMemoryRuntime = createAgentMemoryRuntime({
|
|
@@ -893,7 +913,13 @@ const messageQueueRuntime = createMessageQueueRuntime({
|
|
|
893
913
|
buildOpenCodeUrl,
|
|
894
914
|
getOpenCodeAuthHeaders,
|
|
895
915
|
sessionKnowledgeRuntime,
|
|
896
|
-
|
|
916
|
+
// OpenCode's /global/event SSE proxy cannot carry AiWorker-owned events.
|
|
917
|
+
// Use the shared control stream for SSE clients and the existing WS fan-out.
|
|
918
|
+
broadcastGlobalUiEvent: createGlobalUiEventBroadcaster({
|
|
919
|
+
sseClients: uiAiWorkerEventClients,
|
|
920
|
+
wsClients: uiNotificationWsClients,
|
|
921
|
+
writeSseEvent,
|
|
922
|
+
}),
|
|
897
923
|
onPromptSent: (sessionId) => sessionRuntime.markUserMessageSent(sessionId),
|
|
898
924
|
dataDir: AIWORKER_DATA_DIR,
|
|
899
925
|
});
|
|
@@ -1642,6 +1668,12 @@ async function main(options = {}) {
|
|
|
1642
1668
|
const getDesktopRuntimeConfig = typeof options.getDesktopRuntimeConfig === 'function'
|
|
1643
1669
|
? options.getDesktopRuntimeConfig
|
|
1644
1670
|
: null;
|
|
1671
|
+
const desktopUpdater = options.desktopUpdater
|
|
1672
|
+
&& typeof options.desktopUpdater.check === 'function'
|
|
1673
|
+
&& typeof options.desktopUpdater.install === 'function'
|
|
1674
|
+
&& typeof options.desktopUpdater.restart === 'function'
|
|
1675
|
+
? options.desktopUpdater
|
|
1676
|
+
: null;
|
|
1645
1677
|
|
|
1646
1678
|
console.log(`Starting AiWorker on port ${port === 0 ? 'auto' : port}`);
|
|
1647
1679
|
|
|
@@ -1676,7 +1708,10 @@ async function main(options = {}) {
|
|
|
1676
1708
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
1677
1709
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
1678
1710
|
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
1679
|
-
|
|
1711
|
+
// The packaged desktop UI (openchamber-ui://) and the dev UI sit on a
|
|
1712
|
+
// different origin, so every custom request header must be listed here or
|
|
1713
|
+
// the browser refuses the request at preflight, before it reaches a route.
|
|
1714
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,Accept,X-Requested-With,Cache-Control,X-OpenCode-Directory,X-OpenCode-Directory-Encoding,Ngrok-Skip-Browser-Warning,X-AiWorker-Surface');
|
|
1680
1715
|
res.setHeader('Access-Control-Expose-Headers', 'x-next-cursor');
|
|
1681
1716
|
res.setHeader('Vary', 'Origin');
|
|
1682
1717
|
if (req.method === 'OPTIONS') {
|
|
@@ -1817,6 +1852,7 @@ async function main(options = {}) {
|
|
|
1817
1852
|
getCachedZenModels,
|
|
1818
1853
|
setAutoAcceptSession,
|
|
1819
1854
|
agentToolRuntime,
|
|
1855
|
+
desktopUpdater,
|
|
1820
1856
|
});
|
|
1821
1857
|
uiAuthController = bootstrapResult.uiAuthController;
|
|
1822
1858
|
realtimeProxyRuntime = attachRealtimeProxy({
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// One-time copy of the user-authored folders into a custom data directory.
|
|
2
|
+
//
|
|
3
|
+
// `AIWORKER_DATA_DIR` is documented as "the AiWorker data directory",
|
|
4
|
+
// but for a long time only the flat files (settings, auth, push tokens)
|
|
5
|
+
// followed it while `projects/`, `themes/`, and `speech-models/` stayed under
|
|
6
|
+
// `~/.config/openchamber`. Now every folder hangs off the data directory, so an
|
|
7
|
+
// instance that already ran with a custom directory finds those folders in
|
|
8
|
+
// the old place. This copies each one over once: only when the new location
|
|
9
|
+
// does not exist yet and the old one does. It copies rather than moves
|
|
10
|
+
// because a second instance on the same machine (a custom directory next to
|
|
11
|
+
// the default one) must not strip the default instance of its projects.
|
|
12
|
+
|
|
13
|
+
const USER_DIR_ENTRIES = Object.freeze(['projects', 'themes', 'speech-models']);
|
|
14
|
+
|
|
15
|
+
const exists = async (fsPromises, target) => fsPromises.access(target).then(() => true, () => false);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copy the user folders from `legacyRoot` into `dataDir`. Returns the entries
|
|
19
|
+
* copied. A no-op when the two roots are the same directory. A folder whose
|
|
20
|
+
* copy fails is reported through `warn` and its partial copy removed; the
|
|
21
|
+
* server keeps starting, reading the (empty) new location.
|
|
22
|
+
*/
|
|
23
|
+
export const migrateLegacyUserDirs = async ({ fsPromises, path, dataDir, legacyRoot, warn = () => {}, entries = USER_DIR_ENTRIES }) => {
|
|
24
|
+
if (path.resolve(dataDir) === path.resolve(legacyRoot)) return [];
|
|
25
|
+
const moved = [];
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const from = path.join(legacyRoot, entry);
|
|
28
|
+
const to = path.join(dataDir, entry);
|
|
29
|
+
if (await exists(fsPromises, to) || !(await exists(fsPromises, from))) continue;
|
|
30
|
+
try {
|
|
31
|
+
await fsPromises.mkdir(dataDir, { recursive: true });
|
|
32
|
+
await fsPromises.cp(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
33
|
+
moved.push(entry);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
await fsPromises.rm(to, { recursive: true, force: true }).catch(() => {});
|
|
36
|
+
warn(`Failed to copy ${from} to ${to}: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return moved;
|
|
40
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fsPromises from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
import { migrateLegacyUserDirs } from './data-dir-migration.js';
|
|
7
|
+
|
|
8
|
+
const setup = async () => {
|
|
9
|
+
const root = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-data-dir-'));
|
|
10
|
+
const legacyRoot = path.join(root, 'legacy');
|
|
11
|
+
const dataDir = path.join(root, 'custom');
|
|
12
|
+
await fsPromises.mkdir(path.join(legacyRoot, 'projects'), { recursive: true });
|
|
13
|
+
await fsPromises.writeFile(path.join(legacyRoot, 'projects', 'p.json'), '{"a":1}');
|
|
14
|
+
await fsPromises.mkdir(path.join(legacyRoot, 'themes'), { recursive: true });
|
|
15
|
+
return { root, legacyRoot, dataDir, cleanup: () => fsPromises.rm(root, { recursive: true, force: true }) };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('migrateLegacyUserDirs', () => {
|
|
19
|
+
it('copies the user folders once into a custom data dir and leaves the originals', async () => {
|
|
20
|
+
const { legacyRoot, dataDir, cleanup } = await setup();
|
|
21
|
+
try {
|
|
22
|
+
const warnings = [];
|
|
23
|
+
const moved = await migrateLegacyUserDirs({ fsPromises, path, dataDir, legacyRoot, warn: (message) => warnings.push(message) });
|
|
24
|
+
expect(moved).toEqual(['projects', 'themes']);
|
|
25
|
+
expect(warnings).toEqual([]);
|
|
26
|
+
expect(await fsPromises.readFile(path.join(dataDir, 'projects', 'p.json'), 'utf8')).toBe('{"a":1}');
|
|
27
|
+
// The default instance keeps its own copy: a second instance must not strip it.
|
|
28
|
+
expect(await fsPromises.readFile(path.join(legacyRoot, 'projects', 'p.json'), 'utf8')).toBe('{"a":1}');
|
|
29
|
+
// A second start copies nothing more.
|
|
30
|
+
expect(await migrateLegacyUserDirs({ fsPromises, path, dataDir, legacyRoot })).toEqual([]);
|
|
31
|
+
} finally {
|
|
32
|
+
await cleanup();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('never merges into a folder that already exists in the data dir', async () => {
|
|
37
|
+
const { legacyRoot, dataDir, cleanup } = await setup();
|
|
38
|
+
try {
|
|
39
|
+
await fsPromises.mkdir(path.join(dataDir, 'projects'), { recursive: true });
|
|
40
|
+
await fsPromises.writeFile(path.join(dataDir, 'projects', 'q.json'), '{}');
|
|
41
|
+
const moved = await migrateLegacyUserDirs({ fsPromises, path, dataDir, legacyRoot });
|
|
42
|
+
expect(moved).toEqual(['themes']);
|
|
43
|
+
expect(await fsPromises.readdir(path.join(dataDir, 'projects'))).toEqual(['q.json']);
|
|
44
|
+
expect(await fsPromises.readdir(path.join(legacyRoot, 'projects'))).toEqual(['p.json']);
|
|
45
|
+
} finally {
|
|
46
|
+
await cleanup();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('is a no-op when the data dir is the default root', async () => {
|
|
51
|
+
const { legacyRoot, cleanup } = await setup();
|
|
52
|
+
try {
|
|
53
|
+
expect(await migrateLegacyUserDirs({ fsPromises, path, dataDir: legacyRoot, legacyRoot })).toEqual([]);
|
|
54
|
+
expect(await fsPromises.readdir(path.join(legacyRoot, 'projects'))).toEqual(['p.json']);
|
|
55
|
+
} finally {
|
|
56
|
+
await cleanup();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -446,18 +446,18 @@ describe('message stream websocket runtime', () => {
|
|
|
446
446
|
const socket = new FakeSocket();
|
|
447
447
|
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
449
|
+
try {
|
|
450
|
+
// Wait for the reconnect event itself; a 35ms sleep raced the event
|
|
451
|
+
// loop when the workspace build and test workers ran together.
|
|
452
|
+
await expect.poll(() => socket.sent.filter((frame) => frame.type === 'ready').length).toBeGreaterThanOrEqual(2);
|
|
453
|
+
const eventFrames = socket.sent.filter((frame) => frame.type === 'event' && frame.payload?.type === 'server.connected');
|
|
454
|
+
expect(eventFrames.length).toBeGreaterThanOrEqual(2);
|
|
455
|
+
expect(fetchCalls.slice(0, 2)).toEqual([null, 'evt-1']);
|
|
456
|
+
expect(triggerHealthCheckCalls).toBe(0);
|
|
457
|
+
} finally {
|
|
458
|
+
socket.close();
|
|
459
|
+
await runtime.close();
|
|
460
|
+
}
|
|
461
461
|
});
|
|
462
462
|
|
|
463
463
|
it('keeps synthetic event processing on forwarded upstream events', async () => {
|
|
@@ -14,6 +14,8 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
|
|
|
14
14
|
- `POST /api/fs/mkdir`
|
|
15
15
|
- `GET /api/fs/read`
|
|
16
16
|
- `GET /api/fs/raw`
|
|
17
|
+
- `GET /api/fs/stat`
|
|
18
|
+
- `GET /api/fs/directory-stat`
|
|
17
19
|
- `GET /api/fs/serve/:path(*)`
|
|
18
20
|
- `POST /api/fs/write`
|
|
19
21
|
- `POST /api/fs/upload`
|
|
@@ -43,6 +45,7 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
|
|
|
43
45
|
- Workspace checks accept, besides the active workspace and its worktrees, the **managed roots**: the AiWorker config root and the managed chats root (`managedChatsRoot` dependency; `AIWORKER_CHATS_DIR` upstream, default `<config root>/chats`). Chat worktrees may legitimately live outside every project workspace.
|
|
44
46
|
- `GET /api/fs/home` answers `{ home, chatsRoot }`. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
|
|
45
47
|
- Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Policy denials such as workspace-boundary or missing-grant failures must not use that marker because a native folder picker cannot remediate them.
|
|
48
|
+
- `GET /api/fs/directory-stat?path=...` uses one `stat` without listing contents or resolving project topology. It follows the same authenticated directory-discovery path policy as `/api/fs/list`, including targets outside the active workspace. A directory returns `{ isDirectory: true }`; `ENOENT` returns `not-found`, and a file or `ENOTDIR` returns `not-directory`. Permission and other failures remain distinct from a missing path. VS Code explicitly returns 501, so the shared client treats its probe as unknown.
|
|
46
49
|
- Read-only routes authorize the requested path against the workspace before resolving symlinks. A symlink reached through the workspace may therefore target a file outside it, while a directly requested outside path still requires an exact-path grant. Write routes keep canonical-target boundary checks.
|
|
47
50
|
- If adding new `/api/fs/*` endpoints, add them in `routes.js` and extend this document.
|
|
48
51
|
- `GET /api/fs/list` may resolve symlinks with `realpath` to read directory contents, but the response `path` and each entry `path` must stay in the caller's requested path space (`path.join(requestedPath, name)`). Returning real paths breaks file-tree expansion for directories reached through workspace symlinks.
|
package/server/lib/fs/routes.js
CHANGED
|
@@ -864,6 +864,37 @@ export const registerFsRoutes = (app, dependencies) => {
|
|
|
864
864
|
}
|
|
865
865
|
});
|
|
866
866
|
|
|
867
|
+
app.get('/api/fs/directory-stat', async (req, res) => {
|
|
868
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
869
|
+
const paths = new URL(req.url, 'http://openchamber.local').searchParams.getAll('path');
|
|
870
|
+
const directoryPath = paths.length === 1 ? paths[0].trim() : '';
|
|
871
|
+
if (!directoryPath) {
|
|
872
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
// Directory discovery uses the same path policy as /api/fs/list, including
|
|
877
|
+
// paths outside the current workspace. stat follows symlinks without readdir.
|
|
878
|
+
const resolvedPath = path.resolve(normalizeDirectoryPath(directoryPath));
|
|
879
|
+
const stats = await fsPromises.stat(resolvedPath);
|
|
880
|
+
if (!stats.isDirectory()) {
|
|
881
|
+
return res.status(400).json({ error: 'Specified path is not a directory', reason: 'not-directory' });
|
|
882
|
+
}
|
|
883
|
+
return res.json({ isDirectory: true });
|
|
884
|
+
} catch (error) {
|
|
885
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
|
|
886
|
+
return res.status(error.code === 'ENOENT' ? 404 : 400).json({
|
|
887
|
+
error: error.code === 'ENOENT' ? 'Directory not found' : 'Specified path is not a directory',
|
|
888
|
+
reason: error.code === 'ENOENT' ? 'not-found' : 'not-directory',
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
if (isOsPermissionError(error)) {
|
|
892
|
+
return sendOsPermissionDenied(res, 'Access to directory denied');
|
|
893
|
+
}
|
|
894
|
+
return res.status(500).json({ error: 'Failed to stat directory' });
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
867
898
|
app.get('/api/fs/stat', async (req, res) => {
|
|
868
899
|
const filePath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
|
|
869
900
|
const optional = req.query.optional === 'true';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
3
7
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
8
|
|
|
5
9
|
import { mintOutsideFileGrant, registerFsRoutes } from './routes.js';
|
|
@@ -1385,7 +1389,11 @@ describe('fs stat directory scope (issue 3019)', () => {
|
|
|
1385
1389
|
path: path.posix,
|
|
1386
1390
|
fsPromises: {
|
|
1387
1391
|
realpath: async (targetPath) => targetPath,
|
|
1388
|
-
stat: async () => (
|
|
1392
|
+
stat: async (targetPath) => (
|
|
1393
|
+
targetPath === '/repo-b'
|
|
1394
|
+
? { isDirectory: () => true, mtimeMs: 123 }
|
|
1395
|
+
: { isFile: () => true, size: 12, mtimeMs: 456 }
|
|
1396
|
+
),
|
|
1389
1397
|
},
|
|
1390
1398
|
spawn: vi.fn(),
|
|
1391
1399
|
crypto: { randomUUID: () => 'job-0' },
|
|
@@ -1428,6 +1436,107 @@ describe('fs stat directory scope (issue 3019)', () => {
|
|
|
1428
1436
|
expect(res.statusCode).toBe(200);
|
|
1429
1437
|
expect(res.body.isFile).toBe(true);
|
|
1430
1438
|
});
|
|
1439
|
+
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
describe('fs stat directory error handling', () => {
|
|
1443
|
+
it('loads in Node without workspace node_modules, as packaged desktop does', async () => {
|
|
1444
|
+
const directory = await mkdtemp(path.join(tmpdir(), 'openchamber-fs-import-'));
|
|
1445
|
+
try {
|
|
1446
|
+
await mkdir(path.join(directory, 'fs'));
|
|
1447
|
+
await copyFile(new URL('./routes.js', import.meta.url), path.join(directory, 'fs/routes.mjs'));
|
|
1448
|
+
await copyFile(new URL('../path-realpath-cache.js', import.meta.url), path.join(directory, 'path-realpath-cache.js'));
|
|
1449
|
+
expect(() => execFileSync('node', [
|
|
1450
|
+
'--input-type=module',
|
|
1451
|
+
'--eval',
|
|
1452
|
+
'await import(process.argv[1])',
|
|
1453
|
+
pathToFileURL(path.join(directory, 'fs/routes.mjs')).href,
|
|
1454
|
+
], { cwd: directory, stdio: 'pipe' })).not.toThrow();
|
|
1455
|
+
} finally {
|
|
1456
|
+
await rm(directory, { recursive: true, force: true });
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it('returns directory-missing reasons and permission errors for directory stat', async () => {
|
|
1461
|
+
const { app, getRoute } = createRouteRegistry();
|
|
1462
|
+
const enoent = Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
1463
|
+
const enotdir = Object.assign(new Error('not a directory'), { code: 'ENOTDIR' });
|
|
1464
|
+
const eacces = Object.assign(new Error('denied'), { code: 'EACCES' });
|
|
1465
|
+
const stat = vi.fn(async (targetPath) => {
|
|
1466
|
+
if (targetPath === '/repo-b') throw enoent;
|
|
1467
|
+
if (targetPath === '/repo-b/file.txt/child') throw enotdir;
|
|
1468
|
+
if (targetPath === '/repo-b/protected') throw eacces;
|
|
1469
|
+
if (targetPath === '/repo-b/file.txt') return { isDirectory: () => false };
|
|
1470
|
+
if (targetPath === '/repo-b/failure') throw new Error('unavailable');
|
|
1471
|
+
return { isDirectory: () => true, mtimeMs: 1 };
|
|
1472
|
+
});
|
|
1473
|
+
const readdir = vi.fn(async () => []);
|
|
1474
|
+
const callStat = async (handler, { headers = {}, query }) => {
|
|
1475
|
+
const res = createMockResponse();
|
|
1476
|
+
const req = {
|
|
1477
|
+
url: `/api/fs/directory-stat?${new URLSearchParams(query)}`,
|
|
1478
|
+
query,
|
|
1479
|
+
get: (name) => headers[name.toLowerCase()] ?? undefined,
|
|
1480
|
+
};
|
|
1481
|
+
await handler(req, res);
|
|
1482
|
+
return res;
|
|
1483
|
+
};
|
|
1484
|
+
registerFsRoutes(app, {
|
|
1485
|
+
os: { homedir: () => '/home/user' },
|
|
1486
|
+
path: path.posix,
|
|
1487
|
+
fsPromises: {
|
|
1488
|
+
realpath: async (targetPath) => targetPath,
|
|
1489
|
+
stat,
|
|
1490
|
+
readdir,
|
|
1491
|
+
},
|
|
1492
|
+
spawn: vi.fn(),
|
|
1493
|
+
crypto: { randomUUID: () => 'job-0' },
|
|
1494
|
+
normalizeDirectoryPath: (p) => p,
|
|
1495
|
+
resolveProjectDirectory: async () => ({ directory: '/repo' }),
|
|
1496
|
+
buildAugmentedPath: () => '/usr/bin',
|
|
1497
|
+
resolveGitBinaryForSpawn: () => 'git',
|
|
1498
|
+
aiworkerUserConfigRoot: '/home/user/.config',
|
|
1499
|
+
});
|
|
1500
|
+
const handler = getRoute('GET', '/api/fs/directory-stat');
|
|
1501
|
+
|
|
1502
|
+
const available = await callStat(handler, { query: { path: '/other-project' } });
|
|
1503
|
+
expect(available.statusCode).toBe(200);
|
|
1504
|
+
expect(available.body).toEqual({ isDirectory: true });
|
|
1505
|
+
expect(available.getHeader('Cache-Control')).toBe('no-store');
|
|
1506
|
+
expect(stat).toHaveBeenCalledTimes(1);
|
|
1507
|
+
|
|
1508
|
+
const invalid = await callStat(handler, { query: { path: ' ' } });
|
|
1509
|
+
expect(invalid.statusCode).toBe(400);
|
|
1510
|
+
expect(stat).toHaveBeenCalledTimes(1);
|
|
1511
|
+
|
|
1512
|
+
for (const query of ['path=/repo&path=/other', 'path[]=/repo', '']) {
|
|
1513
|
+
const malformed = createMockResponse();
|
|
1514
|
+
await handler({ url: `/api/fs/directory-stat?${query}` }, malformed);
|
|
1515
|
+
expect(malformed.statusCode).toBe(400);
|
|
1516
|
+
}
|
|
1517
|
+
expect(stat).toHaveBeenCalledTimes(1);
|
|
1518
|
+
|
|
1519
|
+
const missing = await callStat(handler, { headers: { 'x-opencode-directory': '/repo-b' }, query: { path: '/repo-b', directory: 'true' } });
|
|
1520
|
+
expect(missing.statusCode).toBe(404);
|
|
1521
|
+
expect(missing.body).toEqual({ error: 'Directory not found', reason: 'not-found' });
|
|
1522
|
+
|
|
1523
|
+
const notDir = await callStat(handler, { headers: { 'x-opencode-directory': '/repo-b' }, query: { path: '/repo-b/file.txt/child', directory: 'true' } });
|
|
1524
|
+
expect(notDir.statusCode).toBe(400);
|
|
1525
|
+
expect(notDir.body).toEqual({ error: 'Specified path is not a directory', reason: 'not-directory' });
|
|
1526
|
+
|
|
1527
|
+
const denied = await callStat(handler, { headers: { 'x-opencode-directory': '/repo-b' }, query: { path: '/repo-b/protected', directory: 'true' } });
|
|
1528
|
+
expect(denied.statusCode).toBe(403);
|
|
1529
|
+
expect(denied.body).toEqual({ error: 'Access to directory denied', reason: 'os-permission' });
|
|
1530
|
+
|
|
1531
|
+
const file = await callStat(handler, { query: { path: '/repo-b/file.txt' } });
|
|
1532
|
+
expect(file.statusCode).toBe(400);
|
|
1533
|
+
expect(file.body.reason).toBe('not-directory');
|
|
1534
|
+
|
|
1535
|
+
const failure = await callStat(handler, { query: { path: '/repo-b/failure' } });
|
|
1536
|
+
expect(failure.statusCode).toBe(500);
|
|
1537
|
+
expect(failure.body).toEqual({ error: 'Failed to stat directory' });
|
|
1538
|
+
expect(readdir).not.toHaveBeenCalled();
|
|
1539
|
+
});
|
|
1431
1540
|
});
|
|
1432
1541
|
|
|
1433
1542
|
describe('fs managed chats root', () => {
|