@aiworker/web 1.22.2 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +7 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
- package/dist/assets/DiffView-DC9RX3MF.js +4 -0
- package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
- package/dist/assets/FilesView-BFZkuu4O.js +13 -0
- package/dist/assets/{GitView-VQQkYuUw.js → GitView-B_x52MHY.js} +4 -4
- package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-cmAzUTiw.js} +1 -1
- package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-Dehh5afm.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DgNOqqhE.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DusmMZ_p.js +12 -0
- package/dist/assets/OnboardingScreen-B8fm1H4m.js +1 -0
- package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-BVr7gJU0.js} +1 -1
- package/dist/assets/PierreDiffViewer-DzLt-nel.js +161 -0
- package/dist/assets/PlanView-BmQUYT_z.js +5 -0
- package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
- package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-cTl_eLIH.js} +5 -5
- package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-C_3iYNJQ.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
- package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DJsQr-p7.js +10 -0
- package/dist/assets/es-BuYMlA9g.js +16 -0
- package/dist/assets/fr-ePDay33C.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-BD8ILbxF.js +40 -0
- package/dist/assets/{index-Bl3LSbjn.js → index-n6O3hklM.js} +1 -1
- package/dist/assets/index-zhgg-SDz.css +1 -0
- package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-GMdPz-71.js} +1 -1
- package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-BVuH1zH9.js} +1 -1
- package/dist/assets/ja-BzR9xOY_.js +16 -0
- package/dist/assets/ko-BEjHAm14.js +16 -0
- package/dist/assets/main-BydQ1tr_.js +930 -0
- package/dist/assets/{main-DnBAoktx.js → main-D3ZDQglc.js} +2 -2
- package/dist/assets/markdownCore-Cb2MdZnV.js +5 -0
- package/dist/assets/{miniChat-BKyvLseN.js → miniChat-d01k801U.js} +2 -2
- package/dist/assets/{mobile-DKjeFgMN.js → mobile-D7VV-Ufz.js} +2 -2
- package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
- package/dist/assets/pl-rQmpxNIF.js +16 -0
- package/dist/assets/pt-BR-CorbsSW-.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
- package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
- package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
- package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
- package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-BzXUjlby.js} +1 -1
- package/dist/assets/surface-COKb77Ua.js +1 -0
- package/dist/assets/tr-CoAMXPtI.js +7 -0
- package/dist/assets/uk-YlAX0rRU.js +16 -0
- package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-Cwyhjfl5.js} +141 -140
- package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
- package/dist/assets/useInlineCommentController-DDkpBEU8.js +2 -0
- package/dist/assets/useRootScrollLock-CwjoWuC4.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
- package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
- package/dist/assets/vendor-zod-B6551dwG.js +39 -0
- package/dist/assets/{web-components-_mNEMjFY.js → web-components-LV3vjTxy.js} +1 -1
- package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
- package/dist/assets/zh-TW-CQKaCayr.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 +48 -22
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -0
- package/server/lib/event-stream/global-hub.js +29 -4
- package/server/lib/event-stream/global-hub.test.js +22 -0
- package/server/lib/event-stream/global-ws-bridge.js +15 -18
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +72 -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 +39 -6
- package/server/lib/git/routes.js +45 -12
- package/server/lib/git/routes.test.js +78 -0
- package/server/lib/git/service.js +343 -202
- package/server/lib/git/service.test.js +496 -25
- package/server/lib/github/DOCUMENTATION.md +5 -0
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +16 -9
- package/server/lib/inherited-env.test.js +8 -9
- package/server/lib/message-queue/DOCUMENTATION.md +11 -1
- package/server/lib/message-queue/runtime.js +13 -0
- package/server/lib/message-queue/runtime.test.js +31 -1
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- package/server/lib/opencode/DOCUMENTATION.md +35 -5
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +17 -1
- package/server/lib/opencode/lifecycle.js +4 -3
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +64 -4
- package/server/lib/opencode/openchamber-routes.test.js +128 -1
- package/server/lib/opencode/proxy.js +33 -0
- package/server/lib/opencode/routes.js +5 -3
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +88 -41
- package/server/lib/opencode/settings-helpers.test.js +176 -1
- package/server/lib/opencode/settings-registry.json +750 -0
- package/server/lib/opencode/settings-runtime.js +175 -27
- package/server/lib/opencode/settings-runtime.test.js +321 -7
- package/server/lib/project-context/DOCUMENTATION.md +38 -4
- 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 +256 -25
- package/server/lib/project-context/runtime.test.js +160 -3
- package/server/lib/projects/DOCUMENTATION.md +77 -0
- package/server/lib/projects/project-config.js +179 -17
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +43 -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 +75 -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 +44 -8
- package/server/lib/relay/host-client.test.js +14 -0
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +66 -17
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +12 -2
- package/server/lib/scheduled-tasks/runtime.test.js +53 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +117 -267
- package/server/lib/session-assist/runtime.test.js +203 -0
- 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 +49 -20
- package/server/lib/walkthrough/model-settings.js +8 -12
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +36 -4
- package/server/opencode-worktree-readiness.test.js +130 -0
- 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/PlanView-DyDt_nZ_.js +0 -5
- 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-legendapp-list-gg7YLRW7.js +0 -1
- 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/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
|
@@ -29,11 +29,13 @@ export const registerAiWorkerRoutes = (app, dependencies) => {
|
|
|
29
29
|
readSettingsFromDiskMigrated,
|
|
30
30
|
fetchFreeZenModels,
|
|
31
31
|
getCachedZenModels,
|
|
32
|
+
desktopUpdater,
|
|
32
33
|
} = dependencies;
|
|
33
34
|
|
|
35
|
+
let desktopRestartError = null;
|
|
36
|
+
|
|
34
37
|
app.get('/api/openchamber/update-check', async (req, res) => {
|
|
35
38
|
try {
|
|
36
|
-
const { checkForUpdates } = await import('../package-manager.js');
|
|
37
39
|
const parseString = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
|
38
40
|
const parseReportUsage = (value) => {
|
|
39
41
|
if (typeof value !== 'string') return true;
|
|
@@ -49,8 +51,7 @@ export const registerAiWorkerRoutes = (app, dependencies) => {
|
|
|
49
51
|
return 'desktop';
|
|
50
52
|
};
|
|
51
53
|
const userAgent = typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'] : '';
|
|
52
|
-
|
|
53
|
-
const updateInfo = await checkForUpdates({
|
|
54
|
+
const updateRequest = {
|
|
54
55
|
appType: parseString(req.query.appType),
|
|
55
56
|
deviceClass: parseString(req.query.deviceClass) || inferDeviceClass(userAgent),
|
|
56
57
|
platform: parseString(req.query.platform),
|
|
@@ -59,7 +60,31 @@ export const registerAiWorkerRoutes = (app, dependencies) => {
|
|
|
59
60
|
currentVersion: parseString(req.query.currentVersion),
|
|
60
61
|
installId: parseString(req.query.installId),
|
|
61
62
|
reportUsage: parseReportUsage(parseString(req.query.reportUsage)),
|
|
62
|
-
}
|
|
63
|
+
};
|
|
64
|
+
let updateInfo;
|
|
65
|
+
if (process.env.AIWORKER_RUNTIME === 'desktop' && updateRequest.appType === 'web') {
|
|
66
|
+
if (desktopRestartError && req.query.updateStatus === 'true') {
|
|
67
|
+
return res.status(503).json({
|
|
68
|
+
code: 'DESKTOP_UPDATE_RESTART_FAILED',
|
|
69
|
+
error: desktopRestartError,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (typeof desktopUpdater?.check !== 'function') {
|
|
73
|
+
return res.status(503).json({
|
|
74
|
+
available: false,
|
|
75
|
+
code: 'DESKTOP_UPDATER_UNAVAILABLE',
|
|
76
|
+
error: 'The desktop updater is not available.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
updateInfo = {
|
|
80
|
+
...await desktopUpdater.check(),
|
|
81
|
+
packageManager: 'electron',
|
|
82
|
+
updateOwner: 'electron-updater',
|
|
83
|
+
};
|
|
84
|
+
} else {
|
|
85
|
+
const { checkForUpdates } = await import('../package-manager.js');
|
|
86
|
+
updateInfo = await checkForUpdates(updateRequest);
|
|
87
|
+
}
|
|
63
88
|
res.json(updateInfo);
|
|
64
89
|
} catch (error) {
|
|
65
90
|
console.error('Failed to check for updates:', error);
|
|
@@ -72,6 +97,41 @@ export const registerAiWorkerRoutes = (app, dependencies) => {
|
|
|
72
97
|
|
|
73
98
|
app.post('/api/openchamber/update-install', async (_req, res) => {
|
|
74
99
|
try {
|
|
100
|
+
if (process.env.AIWORKER_RUNTIME === 'desktop') {
|
|
101
|
+
if (typeof desktopUpdater?.install !== 'function' || typeof desktopUpdater?.restart !== 'function') {
|
|
102
|
+
return res.status(503).json({
|
|
103
|
+
code: 'DESKTOP_UPDATER_UNAVAILABLE',
|
|
104
|
+
error: 'The desktop updater is not available.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
desktopRestartError = null;
|
|
109
|
+
const updateInfo = await desktopUpdater.install();
|
|
110
|
+
if (!updateInfo?.available) {
|
|
111
|
+
return res.status(400).json({ error: 'No update available' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.json({
|
|
115
|
+
success: true,
|
|
116
|
+
message: 'Desktop update downloaded, host will restart shortly',
|
|
117
|
+
version: updateInfo.version,
|
|
118
|
+
packageManager: 'electron',
|
|
119
|
+
updateOwner: 'electron-updater',
|
|
120
|
+
autoRestart: true,
|
|
121
|
+
restartManager: 'electron-updater',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
setImmediate(() => {
|
|
125
|
+
Promise.resolve()
|
|
126
|
+
.then(() => desktopUpdater.restart())
|
|
127
|
+
.catch((error) => {
|
|
128
|
+
desktopRestartError = error instanceof Error ? error.message : 'Failed to restart after desktop update';
|
|
129
|
+
console.error('Failed to restart after desktop update:', error);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
75
135
|
const { spawn: spawnChild, spawnSync } = await import('child_process');
|
|
76
136
|
const {
|
|
77
137
|
checkForUpdates,
|
|
@@ -18,7 +18,7 @@ const childProcess = await import('child_process');
|
|
|
18
18
|
const packageManager = await import('../package-manager.js');
|
|
19
19
|
const { registerAiWorkerRoutes } = await import('./openchamber-routes.js');
|
|
20
20
|
|
|
21
|
-
const createApp = ({ environment = {}, storedOptions = {} } = {}) => {
|
|
21
|
+
const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater } = {}) => {
|
|
22
22
|
const app = express();
|
|
23
23
|
const dependencies = {
|
|
24
24
|
fs: {
|
|
@@ -47,6 +47,7 @@ const createApp = ({ environment = {}, storedOptions = {} } = {}) => {
|
|
|
47
47
|
readSettingsFromDiskMigrated: vi.fn(),
|
|
48
48
|
fetchFreeZenModels: vi.fn(),
|
|
49
49
|
getCachedZenModels: vi.fn(),
|
|
50
|
+
desktopUpdater,
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
registerAiWorkerRoutes(app, dependencies);
|
|
@@ -69,6 +70,132 @@ afterEach(() => {
|
|
|
69
70
|
vi.clearAllMocks();
|
|
70
71
|
});
|
|
71
72
|
|
|
73
|
+
describe('AiWorker desktop host update route', () => {
|
|
74
|
+
it('reports a restart rejection until the user retries installation', async () => {
|
|
75
|
+
const desktopUpdater = {
|
|
76
|
+
check: vi.fn(async () => ({ available: true, currentVersion: '1.17.0', version: '1.17.1' })),
|
|
77
|
+
install: vi.fn(async () => ({ available: true, version: '1.17.1' })),
|
|
78
|
+
restart: vi.fn().mockRejectedValueOnce(new Error('Signature rejected')).mockResolvedValue(undefined),
|
|
79
|
+
};
|
|
80
|
+
const { app } = createApp({ environment: { AIWORKER_RUNTIME: 'desktop' }, desktopUpdater });
|
|
81
|
+
const logError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
82
|
+
|
|
83
|
+
await request(app).post('/api/openchamber/update-install').expect(200);
|
|
84
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
85
|
+
await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false&updateStatus=true').expect(503, {
|
|
86
|
+
code: 'DESKTOP_UPDATE_RESTART_FAILED', error: 'Signature rejected',
|
|
87
|
+
});
|
|
88
|
+
expect(desktopUpdater.check).not.toHaveBeenCalled();
|
|
89
|
+
expect(logError).toHaveBeenCalledOnce();
|
|
90
|
+
// Availability remains reachable after a browser reload, so users can retry.
|
|
91
|
+
await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false').expect(200);
|
|
92
|
+
|
|
93
|
+
await request(app).post('/api/openchamber/update-install').expect(200);
|
|
94
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
95
|
+
const response = await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false').expect(200);
|
|
96
|
+
expect(response.body.currentVersion).toBe('1.17.0');
|
|
97
|
+
expect(response.body.updateOwner).toBe('electron-updater');
|
|
98
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('rejects native checks without a bridge and preserves explicit non-web checks', async () => {
|
|
102
|
+
const { app } = createApp({ environment: { AIWORKER_RUNTIME: 'desktop' } });
|
|
103
|
+
await request(app).get('/api/openchamber/update-check?appType=web').expect(503, {
|
|
104
|
+
available: false, code: 'DESKTOP_UPDATER_UNAVAILABLE', error: 'The desktop updater is not available.',
|
|
105
|
+
});
|
|
106
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
107
|
+
await request(app).get('/api/openchamber/update-check?appType=desktop-electron').expect(200);
|
|
108
|
+
expect(packageManager.checkForUpdates).toHaveBeenCalledOnce();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('uses electron-updater to check for Web client updates', async () => {
|
|
112
|
+
const desktopUpdater = {
|
|
113
|
+
check: vi.fn(async () => ({
|
|
114
|
+
available: true,
|
|
115
|
+
currentVersion: '1.17.0',
|
|
116
|
+
version: '1.17.1',
|
|
117
|
+
})),
|
|
118
|
+
install: vi.fn(),
|
|
119
|
+
restart: vi.fn(),
|
|
120
|
+
};
|
|
121
|
+
const { app } = createApp({
|
|
122
|
+
environment: {
|
|
123
|
+
AIWORKER_RUNTIME: 'desktop',
|
|
124
|
+
},
|
|
125
|
+
desktopUpdater,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await request(app)
|
|
129
|
+
.get('/api/openchamber/update-check?appType=web&reportUsage=false')
|
|
130
|
+
.expect(200, {
|
|
131
|
+
available: true,
|
|
132
|
+
currentVersion: '1.17.0',
|
|
133
|
+
version: '1.17.1',
|
|
134
|
+
packageManager: 'electron',
|
|
135
|
+
updateOwner: 'electron-updater',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(desktopUpdater.check).toHaveBeenCalledOnce();
|
|
139
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('installs through electron-updater and restarts after responding', async () => {
|
|
143
|
+
const desktopUpdater = {
|
|
144
|
+
check: vi.fn(),
|
|
145
|
+
install: vi.fn(async () => ({
|
|
146
|
+
available: true,
|
|
147
|
+
version: '1.17.1',
|
|
148
|
+
})),
|
|
149
|
+
restart: vi.fn(),
|
|
150
|
+
};
|
|
151
|
+
const { app } = createApp({
|
|
152
|
+
environment: {
|
|
153
|
+
AIWORKER_RUNTIME: 'desktop',
|
|
154
|
+
},
|
|
155
|
+
desktopUpdater,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await request(app)
|
|
159
|
+
.post('/api/openchamber/update-install')
|
|
160
|
+
.expect(200, {
|
|
161
|
+
success: true,
|
|
162
|
+
message: 'Desktop update downloaded, host will restart shortly',
|
|
163
|
+
version: '1.17.1',
|
|
164
|
+
packageManager: 'electron',
|
|
165
|
+
updateOwner: 'electron-updater',
|
|
166
|
+
autoRestart: true,
|
|
167
|
+
restartManager: 'electron-updater',
|
|
168
|
+
});
|
|
169
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
170
|
+
|
|
171
|
+
expect(desktopUpdater.install).toHaveBeenCalledOnce();
|
|
172
|
+
expect(desktopUpdater.restart).toHaveBeenCalledOnce();
|
|
173
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
174
|
+
expect(packageManager.detectPackageManagerDetails).not.toHaveBeenCalled();
|
|
175
|
+
expect(packageManager.getUpdateCommand).not.toHaveBeenCalled();
|
|
176
|
+
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
177
|
+
expect(childProcess.spawnSync).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('fails safely when the Electron updater bridge is unavailable', async () => {
|
|
181
|
+
const { app } = createApp({
|
|
182
|
+
environment: {
|
|
183
|
+
AIWORKER_RUNTIME: 'desktop',
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await request(app)
|
|
188
|
+
.post('/api/openchamber/update-install')
|
|
189
|
+
.expect(503, {
|
|
190
|
+
code: 'DESKTOP_UPDATER_UNAVAILABLE',
|
|
191
|
+
error: 'The desktop updater is not available.',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
|
195
|
+
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
72
199
|
describe('AiWorker foreground update route', () => {
|
|
73
200
|
it('rejects a foreground update when the server is not owned by systemd', async () => {
|
|
74
201
|
const { app } = createApp();
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { createRealpathCache } from '../path-realpath-cache.js';
|
|
12
12
|
import { DEFAULT_UPSTREAM_STALL_TIMEOUT_MS } from '../event-stream/upstream-reader.js';
|
|
13
13
|
import { recordStartupPerformance } from './startup-performance.js';
|
|
14
|
+
import { getWorktreeBootstrapStatus } from '../git/service.js';
|
|
14
15
|
|
|
15
16
|
const DEFAULT_SSE_HEARTBEAT_INTERVAL_MS = 20_000;
|
|
16
17
|
|
|
@@ -286,6 +287,8 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
286
287
|
SSE_HEARTBEAT_INTERVAL_MS = DEFAULT_SSE_HEARTBEAT_INTERVAL_MS,
|
|
287
288
|
SSE_UPSTREAM_STALL_TIMEOUT_MS = DEFAULT_UPSTREAM_STALL_TIMEOUT_MS,
|
|
288
289
|
getSseUpstreamStallTimeoutMs = () => SSE_UPSTREAM_STALL_TIMEOUT_MS,
|
|
290
|
+
readWorktreeBootstrapStatus = getWorktreeBootstrapStatus,
|
|
291
|
+
WORKTREE_READY_TIMEOUT_MS = 5 * 60 * 1000,
|
|
289
292
|
} = deps;
|
|
290
293
|
|
|
291
294
|
if (app.get('opencodeProxyConfigured')) {
|
|
@@ -775,6 +778,36 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
775
778
|
}
|
|
776
779
|
});
|
|
777
780
|
|
|
781
|
+
// Any directory-scoped read can initialize OpenCode's cached project/config,
|
|
782
|
+
// before session.create runs. Hold all upstream requests until Git population
|
|
783
|
+
// finishes, independently of the user's optional setup-script wait.
|
|
784
|
+
app.use('/api', async (req, res, next) => {
|
|
785
|
+
normalizeForwardedDirectoryHeaders(req.headers);
|
|
786
|
+
const url = new URL(req.url, 'http://localhost');
|
|
787
|
+
const directory = url.searchParams.get('directory') || req.get('x-opencode-directory');
|
|
788
|
+
if (!directory) return next();
|
|
789
|
+
|
|
790
|
+
const deadline = Date.now() + WORKTREE_READY_TIMEOUT_MS;
|
|
791
|
+
try {
|
|
792
|
+
while (!res.destroyed && !res.writableEnded && !req.aborted) {
|
|
793
|
+
const status = await readWorktreeBootstrapStatus(directory);
|
|
794
|
+
if (res.destroyed || res.writableEnded || req.aborted) return;
|
|
795
|
+
if (status.status === 'failed') {
|
|
796
|
+
return res.status(503).json({ error: status.error || 'Worktree bootstrap failed' });
|
|
797
|
+
}
|
|
798
|
+
if (status.status === 'ready' || status.phase === 'git-ready' || status.phase === 'setup-ready') {
|
|
799
|
+
return next();
|
|
800
|
+
}
|
|
801
|
+
if (Date.now() >= deadline) {
|
|
802
|
+
return res.status(503).json({ error: 'Timed out waiting for worktree checkout' });
|
|
803
|
+
}
|
|
804
|
+
await sleep(75);
|
|
805
|
+
}
|
|
806
|
+
} catch (error) {
|
|
807
|
+
next(error);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
778
811
|
// Windows: session merge for cross-directory session listing
|
|
779
812
|
if (process.platform === 'win32') {
|
|
780
813
|
app.get('/api/session', async (req, res, next) => {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './config-mutation-response.js';
|
|
8
8
|
import { getClaudeCliAuthStatus } from './claude-cli-auth.js';
|
|
9
9
|
import { OPENCODE_CONFIG_DIR } from './shared.js';
|
|
10
|
+
import { settingsSurfaceOf } from './settings-files.js';
|
|
10
11
|
|
|
11
12
|
export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
12
13
|
const {
|
|
@@ -208,9 +209,10 @@ ${desktopReturn ? `<a class="return" href="aiworker://focus/mcp-auth">Return to
|
|
|
208
209
|
}
|
|
209
210
|
};
|
|
210
211
|
|
|
211
|
-
app.get('/api/config/settings', async (
|
|
212
|
+
app.get('/api/config/settings', async (req, res) => {
|
|
212
213
|
try {
|
|
213
|
-
|
|
214
|
+
// The surface kind resolves the per-surface profile keys; absent means base.
|
|
215
|
+
const settings = await readSettingsFromDiskMigrated({ surface: settingsSurfaceOf(req) });
|
|
214
216
|
res.json(formatSettingsResponse(settings));
|
|
215
217
|
} catch (error) {
|
|
216
218
|
console.error('Failed to read settings:', error);
|
|
@@ -422,7 +424,7 @@ ${desktopReturn ? `<a class="return" href="aiworker://focus/mcp-auth">Return to
|
|
|
422
424
|
|
|
423
425
|
app.put('/api/config/settings', async (req, res) => {
|
|
424
426
|
try {
|
|
425
|
-
const updated = await persistSettings(req.body ?? {});
|
|
427
|
+
const updated = await persistSettings(req.body ?? {}, { surface: settingsSurfaceOf(req) });
|
|
426
428
|
res.json(updated);
|
|
427
429
|
} catch (error) {
|
|
428
430
|
console.error('[API:PUT /api/config/settings] Failed to save settings:', error);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// The two settings files and how a merged document is split between them.
|
|
2
|
+
//
|
|
3
|
+
// `settings.json` holds instance facts (and, untouched, whatever legacy keys
|
|
4
|
+
// older builds left there). `preferences.json` holds the user's profile: the
|
|
5
|
+
// keys the settings registry marks `profile`, each with the time the store
|
|
6
|
+
// last accepted a new value for it. Device keys never reach either file.
|
|
7
|
+
//
|
|
8
|
+
// The VS Code extension host writes the same two files with the same shape
|
|
9
|
+
// (`packages/vscode/src/settings-files.ts`); keep the format changes in sync.
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
const registry = createRequire(import.meta.url)('./settings-registry.json');
|
|
13
|
+
|
|
14
|
+
const PREFERENCES_FILE_NAME = 'preferences.json';
|
|
15
|
+
const PREFERENCES_DOCUMENT_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
/** The registry scope for a key, or `null` when the registry does not know it. */
|
|
18
|
+
const getSettingsScope = (key) => registry.fields[key]?.scope ?? null;
|
|
19
|
+
|
|
20
|
+
export const isProfileSettingsKey = (key) => getSettingsScope(key) === 'profile';
|
|
21
|
+
export const isDeviceSettingsKey = (key) => getSettingsScope(key) === 'device';
|
|
22
|
+
|
|
23
|
+
/** Profile keys the owner chose to store per surface kind (a change on a phone stays on phones). */
|
|
24
|
+
const isPerSurfaceSettingsKey = (key) => registry.fields[key]?.perSurface === true;
|
|
25
|
+
|
|
26
|
+
const SETTINGS_SURFACES = Object.freeze(['web', 'desktop', 'vscode', 'mobile']);
|
|
27
|
+
|
|
28
|
+
export const normalizeSettingsSurface = (value) => (
|
|
29
|
+
typeof value === 'string' && SETTINGS_SURFACES.includes(value.trim()) ? value.trim() : null
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Which surface kind a settings request comes from; `null` means "base".
|
|
34
|
+
* Clients send `?surface=<kind>` (a query parameter keeps the request
|
|
35
|
+
* CORS-simple for cross-origin shells and older instances); the
|
|
36
|
+
* `x-openchamber-surface` header is still honoured for clients that sent it.
|
|
37
|
+
*/
|
|
38
|
+
export const settingsSurfaceOf = (req) => (
|
|
39
|
+
normalizeSettingsSurface(req.query?.surface) ?? normalizeSettingsSurface(req.get?.('x-openchamber-surface'))
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const preferencesFilePathFor = (settingsFilePath, path) => path.join(path.dirname(settingsFilePath), PREFERENCES_FILE_NAME);
|
|
43
|
+
|
|
44
|
+
const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
45
|
+
|
|
46
|
+
const sameValue = (left, right) => {
|
|
47
|
+
if (left === right) return true;
|
|
48
|
+
if (left === undefined || right === undefined) return false;
|
|
49
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const parseStamp = (value) => (Number.isFinite(value) ? value : 0);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse the text of a preferences file. A missing file is the caller's case
|
|
56
|
+
* (ENOENT); anything that is not a version-1 document with a `fields` object
|
|
57
|
+
* is a failure, never an empty profile.
|
|
58
|
+
*
|
|
59
|
+
* An entry is `{ value, updatedAt }` for the base value, optionally with
|
|
60
|
+
* `surfaces: { [surface]: { value, updatedAt } }` for per-surface keys; a
|
|
61
|
+
* per-surface key that was only ever set from one surface kind has no base.
|
|
62
|
+
*/
|
|
63
|
+
export const parsePreferencesDocument = (raw) => {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return { ok: false, reason: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
69
|
+
}
|
|
70
|
+
if (!isPlainObject(parsed) || parsed.version !== PREFERENCES_DOCUMENT_VERSION || !isPlainObject(parsed.fields)) {
|
|
71
|
+
return { ok: false, reason: 'not a version-1 preferences document' };
|
|
72
|
+
}
|
|
73
|
+
const fields = {};
|
|
74
|
+
for (const [key, entry] of Object.entries(parsed.fields)) {
|
|
75
|
+
if (!isPlainObject(entry) || (!('value' in entry) && !isPlainObject(entry.surfaces))) {
|
|
76
|
+
return { ok: false, reason: `field "${key}" is not a { value, updatedAt } entry` };
|
|
77
|
+
}
|
|
78
|
+
const next = { updatedAt: parseStamp(entry.updatedAt) };
|
|
79
|
+
if ('value' in entry) next.value = entry.value;
|
|
80
|
+
if (isPlainObject(entry.surfaces)) {
|
|
81
|
+
next.surfaces = {};
|
|
82
|
+
for (const [surface, surfaceEntry] of Object.entries(entry.surfaces)) {
|
|
83
|
+
if (!SETTINGS_SURFACES.includes(surface) || !isPlainObject(surfaceEntry) || !('value' in surfaceEntry)) {
|
|
84
|
+
return { ok: false, reason: `field "${key}" has an invalid surface entry "${surface}"` };
|
|
85
|
+
}
|
|
86
|
+
next.surfaces[surface] = { value: surfaceEntry.value, updatedAt: parseStamp(surfaceEntry.updatedAt) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
fields[key] = next;
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, fields };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const serializePreferencesDocument = (fields) => JSON.stringify({ version: PREFERENCES_DOCUMENT_VERSION, fields }, null, 2);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The plain key → value view of preference fields as one surface kind sees it:
|
|
98
|
+
* that surface's own value first, the base value otherwise; a key with neither
|
|
99
|
+
* is absent (the client keeps what it holds, or its default).
|
|
100
|
+
*/
|
|
101
|
+
export const flattenPreferences = (fields, surface = null) => {
|
|
102
|
+
const values = {};
|
|
103
|
+
for (const [key, entry] of Object.entries(fields)) {
|
|
104
|
+
const own = surface && entry.surfaces ? entry.surfaces[surface] : undefined;
|
|
105
|
+
if (own) {
|
|
106
|
+
values[key] = own.value;
|
|
107
|
+
} else if ('value' in entry) {
|
|
108
|
+
values[key] = entry.value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return values;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The next preference fields for a merged document: every profile key it
|
|
116
|
+
* carries, stamped `now` when its value differs from what the file held and
|
|
117
|
+
* keeping the earlier stamp otherwise. Profile keys the document no longer
|
|
118
|
+
* carries are dropped (that is how a cleared key leaves the file).
|
|
119
|
+
*
|
|
120
|
+
* Per-surface keys: when the write comes from a surface kind (`surface`) and
|
|
121
|
+
* the key is among the keys that write changed (`changedKeys`), the value goes
|
|
122
|
+
* under `surfaces[surface]` and the base is left as it was; a per-surface key
|
|
123
|
+
* the write did not change keeps its whole entry (the document only carries
|
|
124
|
+
* that surface's resolved view of it). Without a surface (migrations, the
|
|
125
|
+
* one-time seed) the base is written.
|
|
126
|
+
*/
|
|
127
|
+
export const buildPreferencesFields = (previousFields, document, now, { surface = null, changedKeys = null } = {}) => {
|
|
128
|
+
const fields = {};
|
|
129
|
+
const changed = changedKeys ? new Set(changedKeys) : null;
|
|
130
|
+
for (const [key, value] of Object.entries(document)) {
|
|
131
|
+
if (value === undefined || !isProfileSettingsKey(key)) continue;
|
|
132
|
+
const previous = previousFields[key];
|
|
133
|
+
if (isPerSurfaceSettingsKey(key) && surface) {
|
|
134
|
+
if (changed && !changed.has(key)) {
|
|
135
|
+
if (previous) fields[key] = previous;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const previousOwn = previous?.surfaces?.[surface];
|
|
139
|
+
const own = previousOwn && sameValue(previousOwn.value, value) ? previousOwn : { value, updatedAt: now };
|
|
140
|
+
fields[key] = {
|
|
141
|
+
...(previous ?? { updatedAt: 0 }),
|
|
142
|
+
surfaces: { ...(previous?.surfaces ?? {}), [surface]: own },
|
|
143
|
+
};
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (previous && 'value' in previous && sameValue(previous.value, value)) {
|
|
147
|
+
fields[key] = previous;
|
|
148
|
+
} else {
|
|
149
|
+
fields[key] = { ...(previous ?? {}), value, updatedAt: now };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return fields;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The part of a merged document that belongs in `settings.json`: everything
|
|
157
|
+
* that is not a profile key. Device keys older builds persisted stay in place
|
|
158
|
+
* as a read-once seed for clients; the write path never adds new ones.
|
|
159
|
+
*/
|
|
160
|
+
export const instancePartOf = (document) => {
|
|
161
|
+
const instance = {};
|
|
162
|
+
for (const [key, value] of Object.entries(document)) {
|
|
163
|
+
if (value === undefined || isProfileSettingsKey(key)) continue;
|
|
164
|
+
instance[key] = value;
|
|
165
|
+
}
|
|
166
|
+
return instance;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** The profile keys of a document (the part `instancePartOf` leaves out). */
|
|
170
|
+
export const profilePartOf = (document) => {
|
|
171
|
+
const profile = {};
|
|
172
|
+
for (const [key, value] of Object.entries(document)) {
|
|
173
|
+
if (value !== undefined && isProfileSettingsKey(key)) profile[key] = value;
|
|
174
|
+
}
|
|
175
|
+
return profile;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* What `settings.json` holds after a write: the instance part plus a copy of
|
|
180
|
+
* the profile's base values. The copy is for builds that predate the split —
|
|
181
|
+
* they read only this file, so a rollback still finds the user's preferences.
|
|
182
|
+
* Current builds ignore it: `preferences.json` wins in the merged read.
|
|
183
|
+
*/
|
|
184
|
+
export const legacySettingsDocumentOf = (document, preferenceFields) => ({
|
|
185
|
+
...instancePartOf(document),
|
|
186
|
+
...flattenPreferences(preferenceFields),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** The profile keys of a document, as they would seed a fresh preferences file. */
|
|
190
|
+
export const seedPreferencesFrom = (document, now) => buildPreferencesFields({}, document, now);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Synchronous merged read for server modules that consult one or two profile
|
|
194
|
+
* keys on a hot path (small-model resolution, goal/assist toggles). A missing
|
|
195
|
+
* or unreadable preferences file contributes nothing, and the caller's own
|
|
196
|
+
* default applies — the same "missing is not default" rule the clients use.
|
|
197
|
+
*/
|
|
198
|
+
export const readMergedSettingsSync = ({ fs, path, settingsFilePath }) => {
|
|
199
|
+
let settings = {};
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(fs.readFileSync(settingsFilePath, 'utf8'));
|
|
202
|
+
if (isPlainObject(parsed)) settings = parsed;
|
|
203
|
+
} catch {
|
|
204
|
+
settings = {};
|
|
205
|
+
}
|
|
206
|
+
let preferences = {};
|
|
207
|
+
try {
|
|
208
|
+
const parsed = parsePreferencesDocument(fs.readFileSync(preferencesFilePathFor(settingsFilePath, path), 'utf8'));
|
|
209
|
+
if (parsed.ok) preferences = flattenPreferences(parsed.fields);
|
|
210
|
+
} catch {
|
|
211
|
+
preferences = {};
|
|
212
|
+
}
|
|
213
|
+
return { ...settings, ...preferences };
|
|
214
|
+
};
|