@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
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
1
|
+
import { createProjectIdFromPath, projectConfigFileStemOf } from '../projects/project-id.js';
|
|
2
|
+
import {
|
|
3
|
+
buildPreferencesFields,
|
|
4
|
+
flattenPreferences,
|
|
5
|
+
instancePartOf,
|
|
6
|
+
legacySettingsDocumentOf,
|
|
7
|
+
profilePartOf,
|
|
8
|
+
isDeviceSettingsKey,
|
|
9
|
+
isProfileSettingsKey,
|
|
10
|
+
normalizeSettingsSurface,
|
|
11
|
+
parsePreferencesDocument,
|
|
12
|
+
preferencesFilePathFor,
|
|
13
|
+
seedPreferencesFrom,
|
|
14
|
+
serializePreferencesDocument,
|
|
15
|
+
} from './settings-files.js';
|
|
2
16
|
|
|
3
17
|
const DEFAULT_NOTIFICATION_TEMPLATES = {
|
|
4
18
|
completion: { title: '{agent_name} is ready', message: '{model_name} completed the task' },
|
|
@@ -48,6 +62,13 @@ export const createSettingsRuntime = (deps) => {
|
|
|
48
62
|
|
|
49
63
|
let persistSettingsLock = Promise.resolve();
|
|
50
64
|
|
|
65
|
+
const PREFERENCES_FILE_PATH = preferencesFilePathFor(SETTINGS_FILE_PATH, path);
|
|
66
|
+
// True while preferences.json exists but cannot be read. Profile writes are
|
|
67
|
+
// refused meanwhile so a corrupt file is never overwritten with a seed or a
|
|
68
|
+
// partial document; clients keep the values they hold.
|
|
69
|
+
let preferencesUnavailable = false;
|
|
70
|
+
let preferencesFailureLogged = false;
|
|
71
|
+
|
|
51
72
|
// Orphan recovery is a one-shot best-effort scan: when orphans can't be
|
|
52
73
|
// matched on first pass they stay on disk and every subsequent settings
|
|
53
74
|
// read would re-scan them. In-process (Electron) this runs in the main
|
|
@@ -267,10 +288,10 @@ export const createSettingsRuntime = (deps) => {
|
|
|
267
288
|
return;
|
|
268
289
|
}
|
|
269
290
|
|
|
270
|
-
const oldConfigPath = path.join(PROJECTS_ROOT_DIR, `${oldId}.json`);
|
|
271
|
-
const newConfigPath = path.join(PROJECTS_ROOT_DIR, `${newId}.json`);
|
|
272
|
-
const oldStorageDir = path.join(PROJECTS_ROOT_DIR, oldId);
|
|
273
|
-
const newStorageDir = path.join(PROJECTS_ROOT_DIR, newId);
|
|
291
|
+
const oldConfigPath = path.join(PROJECTS_ROOT_DIR, `${projectConfigFileStemOf(oldId)}.json`);
|
|
292
|
+
const newConfigPath = path.join(PROJECTS_ROOT_DIR, `${projectConfigFileStemOf(newId)}.json`);
|
|
293
|
+
const oldStorageDir = path.join(PROJECTS_ROOT_DIR, projectConfigFileStemOf(oldId));
|
|
294
|
+
const newStorageDir = path.join(PROJECTS_ROOT_DIR, projectConfigFileStemOf(newId));
|
|
274
295
|
|
|
275
296
|
const [oldConfig, newConfig] = await Promise.all([
|
|
276
297
|
readJsonFile(oldConfigPath),
|
|
@@ -287,6 +308,28 @@ export const createSettingsRuntime = (deps) => {
|
|
|
287
308
|
await fsPromises.rm(oldConfigPath, { force: true });
|
|
288
309
|
};
|
|
289
310
|
|
|
311
|
+
/**
|
|
312
|
+
* A build before the bounded folder name created `<projectId>/` for an id
|
|
313
|
+
* the filesystem still accepted (201 to 255 characters; longer ids never
|
|
314
|
+
* got a folder). Only the bounded folder is read now, so that folder's
|
|
315
|
+
* notes, plans, and memory are moved over once. A raw name the filesystem
|
|
316
|
+
* cannot hold (ENAMETOOLONG) means no such folder ever existed.
|
|
317
|
+
*/
|
|
318
|
+
const migrateRawIdStorageFolder = async (projectId) => {
|
|
319
|
+
const stem = projectConfigFileStemOf(projectId);
|
|
320
|
+
if (stem === projectId) return;
|
|
321
|
+
const rawStorageDir = path.join(PROJECTS_ROOT_DIR, projectId);
|
|
322
|
+
try {
|
|
323
|
+
await fsPromises.stat(rawStorageDir);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENAMETOOLONG') return;
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
const boundedStorageDir = path.join(PROJECTS_ROOT_DIR, stem);
|
|
329
|
+
await mergeProjectContextFiles(rawStorageDir, boundedStorageDir);
|
|
330
|
+
await moveDirectoryContents(rawStorageDir, boundedStorageDir);
|
|
331
|
+
};
|
|
332
|
+
|
|
290
333
|
const migrateSettingsToDeterministicProjectIds = async (current) => {
|
|
291
334
|
const settings = current && typeof current === 'object' ? current : {};
|
|
292
335
|
const projects = sanitizeProjects(settings.projects) || [];
|
|
@@ -307,6 +350,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
307
350
|
await migrateProjectScopedStorage({ oldId: project.id, newId: nextId, projectPath: project.path });
|
|
308
351
|
await migrateProjectIconFiles({ oldId: project.id, newId: nextId });
|
|
309
352
|
}
|
|
353
|
+
await migrateRawIdStorageFolder(nextId);
|
|
310
354
|
nextProjects.push({ ...project, id: nextId });
|
|
311
355
|
}
|
|
312
356
|
|
|
@@ -447,19 +491,23 @@ export const createSettingsRuntime = (deps) => {
|
|
|
447
491
|
for (const [projectId, orphansForProject] of matches.entries()) {
|
|
448
492
|
const project = canonicalProjects.find((p) => p.id === projectId);
|
|
449
493
|
if (!project) continue;
|
|
450
|
-
const
|
|
494
|
+
const targetStem = projectConfigFileStemOf(project.id);
|
|
495
|
+
const targetPath = path.join(PROJECTS_ROOT_DIR, `${targetStem}.json`);
|
|
496
|
+
const targetStorageDir = path.join(PROJECTS_ROOT_DIR, targetStem);
|
|
451
497
|
|
|
452
498
|
for (const orphan of orphansForProject) {
|
|
453
499
|
const targetExisting = (await readJsonFile(targetPath)) || {};
|
|
500
|
+
// An orphan is named by the file found on disk, so its folder is the raw name.
|
|
501
|
+
const orphanStorageDir = path.join(PROJECTS_ROOT_DIR, orphan.orphanId);
|
|
454
502
|
const merged = mergeProjectConfigData({
|
|
455
503
|
oldConfig: orphan.content,
|
|
456
504
|
newConfig: targetExisting,
|
|
457
|
-
oldStorageDir:
|
|
458
|
-
newStorageDir:
|
|
505
|
+
oldStorageDir: orphanStorageDir,
|
|
506
|
+
newStorageDir: targetStorageDir,
|
|
459
507
|
projectPath: project.path,
|
|
460
508
|
});
|
|
461
509
|
await writeJsonFile(targetPath, merged);
|
|
462
|
-
await moveDirectoryContents(
|
|
510
|
+
await moveDirectoryContents(orphanStorageDir, targetStorageDir);
|
|
463
511
|
await fsPromises.rm(orphan.filePath, { force: true });
|
|
464
512
|
orphansConsumed.add(orphan.orphanId);
|
|
465
513
|
console.log(`[projects] Recovered orphan ${orphan.orphanId} -> ${project.id} (${project.path})`);
|
|
@@ -472,7 +520,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
472
520
|
}
|
|
473
521
|
};
|
|
474
522
|
|
|
475
|
-
const
|
|
523
|
+
const readInstanceSettingsFromDisk = async () => {
|
|
476
524
|
try {
|
|
477
525
|
const raw = await fsPromises.readFile(SETTINGS_FILE_PATH, 'utf8');
|
|
478
526
|
const parsed = JSON.parse(raw);
|
|
@@ -489,6 +537,67 @@ export const createSettingsRuntime = (deps) => {
|
|
|
489
537
|
}
|
|
490
538
|
};
|
|
491
539
|
|
|
540
|
+
/**
|
|
541
|
+
* `{ status: 'missing' }` when the file does not exist, `{ status: 'ok',
|
|
542
|
+
* fields }` when it parsed, `{ status: 'failed' }` for anything else. Only
|
|
543
|
+
* "missing" may be seeded; "failed" must leave the file alone.
|
|
544
|
+
*/
|
|
545
|
+
const readPreferenceFields = async () => {
|
|
546
|
+
let raw;
|
|
547
|
+
try {
|
|
548
|
+
raw = await fsPromises.readFile(PREFERENCES_FILE_PATH, 'utf8');
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
551
|
+
return { status: 'missing' };
|
|
552
|
+
}
|
|
553
|
+
if (!preferencesFailureLogged) {
|
|
554
|
+
preferencesFailureLogged = true;
|
|
555
|
+
console.warn('Failed to read preferences file:', error);
|
|
556
|
+
}
|
|
557
|
+
return { status: 'failed' };
|
|
558
|
+
}
|
|
559
|
+
const parsed = parsePreferencesDocument(raw);
|
|
560
|
+
if (!parsed.ok) {
|
|
561
|
+
if (!preferencesFailureLogged) {
|
|
562
|
+
preferencesFailureLogged = true;
|
|
563
|
+
console.warn(`Preferences file is unreadable (${parsed.reason}); profile writes are paused until it is fixed or removed.`);
|
|
564
|
+
}
|
|
565
|
+
return { status: 'failed' };
|
|
566
|
+
}
|
|
567
|
+
preferencesFailureLogged = false;
|
|
568
|
+
return { status: 'ok', fields: parsed.fields };
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const writePreferencesToDisk = async (fields) => {
|
|
572
|
+
await writeJsonFileAtomic(PREFERENCES_FILE_PATH, serializePreferencesDocument(fields));
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// The merged document every consumer sees: instance facts from settings.json
|
|
576
|
+
// plus the profile from preferences.json. On the first read of an install
|
|
577
|
+
// that predates the split, the profile keys still sitting in settings.json
|
|
578
|
+
// seed preferences.json. settings.json keeps a copy of the profile's base
|
|
579
|
+
// values on every write too, so an older build (which reads only that file)
|
|
580
|
+
// still finds everything where it used to be.
|
|
581
|
+
const readSettingsFromDisk = async ({ surface = null } = {}) => {
|
|
582
|
+
const instance = await readInstanceSettingsFromDisk();
|
|
583
|
+
const preferences = await readPreferenceFields();
|
|
584
|
+
if (preferences.status === 'failed') {
|
|
585
|
+
preferencesUnavailable = true;
|
|
586
|
+
return instance;
|
|
587
|
+
}
|
|
588
|
+
preferencesUnavailable = false;
|
|
589
|
+
if (preferences.status === 'missing') {
|
|
590
|
+
const seeded = seedPreferencesFrom(instance, Date.now());
|
|
591
|
+
try {
|
|
592
|
+
await writePreferencesToDisk(seeded);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.warn('Failed to seed preferences file:', error);
|
|
595
|
+
}
|
|
596
|
+
return instance;
|
|
597
|
+
}
|
|
598
|
+
return { ...instance, ...flattenPreferences(preferences.fields, normalizeSettingsSurface(surface)) };
|
|
599
|
+
};
|
|
600
|
+
|
|
492
601
|
// Strict variant for callers that REGENERATE persisted identity when a key is
|
|
493
602
|
// absent (relay signing/encryption keys). The lenient reader above maps every
|
|
494
603
|
// failure — corrupt JSON, EACCES, transient I/O — to `{}`, which such callers
|
|
@@ -558,7 +667,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
558
667
|
try {
|
|
559
668
|
const entries = await fsPromises.readdir(directory, { withFileTypes: true });
|
|
560
669
|
const cleanupTasks = entries
|
|
561
|
-
.filter((entry) => entry.isFile() && entry.name.startsWith('settings.json.tmp-'))
|
|
670
|
+
.filter((entry) => entry.isFile() && (entry.name.startsWith('settings.json.tmp-') || entry.name.startsWith('preferences.json.tmp-')))
|
|
562
671
|
.map((entry) => fsPromises.rm(path.join(directory, entry.name), { force: true }).catch(() => {}));
|
|
563
672
|
await Promise.all(cleanupTasks);
|
|
564
673
|
} catch {
|
|
@@ -566,27 +675,54 @@ export const createSettingsRuntime = (deps) => {
|
|
|
566
675
|
}
|
|
567
676
|
};
|
|
568
677
|
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
await fsPromises.mkdir(
|
|
572
|
-
if (process.platform !== 'win32') await fsPromises.chmod(
|
|
573
|
-
// Atomic write: Electron main and ssh-manager read
|
|
678
|
+
const writeJsonFileAtomic = async (filePath, text) => {
|
|
679
|
+
const directory = path.dirname(filePath);
|
|
680
|
+
await fsPromises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
681
|
+
if (process.platform !== 'win32') await fsPromises.chmod(directory, 0o700);
|
|
682
|
+
// Atomic write: Electron main and ssh-manager read these files via plain
|
|
574
683
|
// readFile + JSON.parse and silently coerce parse errors to {}. A
|
|
575
684
|
// partial read during a non-atomic writeFile would make their next
|
|
576
|
-
// read-modify-write wipe the
|
|
577
|
-
const tmp = `${
|
|
685
|
+
// read-modify-write wipe the file.
|
|
686
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
578
687
|
try {
|
|
579
|
-
await fsPromises.writeFile(tmp,
|
|
688
|
+
await fsPromises.writeFile(tmp, text, { encoding: 'utf8', mode: 0o600 });
|
|
580
689
|
if (process.platform !== 'win32') await fsPromises.chmod(tmp, 0o600);
|
|
581
|
-
await replaceFile(tmp,
|
|
582
|
-
if (process.platform !== 'win32') await fsPromises.chmod(
|
|
690
|
+
await replaceFile(tmp, filePath);
|
|
691
|
+
if (process.platform !== 'win32') await fsPromises.chmod(filePath, 0o600);
|
|
583
692
|
} catch (error) {
|
|
584
693
|
await fsPromises.rm(tmp, { force: true }).catch(() => {});
|
|
585
|
-
console.warn(
|
|
694
|
+
console.warn(`Failed to write ${path.basename(filePath)}:`, error);
|
|
586
695
|
throw error;
|
|
587
696
|
}
|
|
588
697
|
};
|
|
589
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Persist a merged document: profile keys go to preferences.json (stamped
|
|
701
|
+
* when their value changed), everything else to settings.json. While
|
|
702
|
+
* preferences.json is unreadable its part is skipped rather than replaced.
|
|
703
|
+
*/
|
|
704
|
+
const writeSettingsToDisk = async (settings, { surface = null, changedKeys = null } = {}) => {
|
|
705
|
+
const current = preferencesUnavailable ? { status: 'failed' } : await readPreferenceFields();
|
|
706
|
+
if (current.status === 'failed') {
|
|
707
|
+
// The profile part is not saved; settings.json keeps whatever legacy
|
|
708
|
+
// profile copy it already holds rather than losing it too.
|
|
709
|
+
preferencesUnavailable = true;
|
|
710
|
+
const onDisk = await readInstanceSettingsFromDisk();
|
|
711
|
+
await writeJsonFileAtomic(SETTINGS_FILE_PATH, JSON.stringify({
|
|
712
|
+
...instancePartOf(settings),
|
|
713
|
+
...profilePartOf(onDisk),
|
|
714
|
+
}, null, 2));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const previousFields = current.status === 'ok' ? current.fields : {};
|
|
718
|
+
const nextFields = buildPreferencesFields(previousFields, settings, Date.now(), {
|
|
719
|
+
surface: normalizeSettingsSurface(surface),
|
|
720
|
+
changedKeys,
|
|
721
|
+
});
|
|
722
|
+
await writeJsonFileAtomic(SETTINGS_FILE_PATH, JSON.stringify(legacySettingsDocumentOf(settings, nextFields), null, 2));
|
|
723
|
+
await writePreferencesToDisk(nextFields);
|
|
724
|
+
};
|
|
725
|
+
|
|
590
726
|
const validateProjectEntries = async (projects) => {
|
|
591
727
|
if (!Array.isArray(projects)) {
|
|
592
728
|
return [];
|
|
@@ -872,7 +1008,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
872
1008
|
|
|
873
1009
|
let hasCleanedOrphanedTempFiles = false;
|
|
874
1010
|
|
|
875
|
-
const readSettingsFromDiskMigrated = async () => {
|
|
1011
|
+
const readSettingsFromDiskMigrated = async ({ surface = null } = {}) => {
|
|
876
1012
|
if (!hasCleanedOrphanedTempFiles) {
|
|
877
1013
|
hasCleanedOrphanedTempFiles = true;
|
|
878
1014
|
await cleanupOrphanedSettingsTempFiles(path.dirname(SETTINGS_FILE_PATH));
|
|
@@ -889,16 +1025,28 @@ export const createSettingsRuntime = (deps) => {
|
|
|
889
1025
|
if (migration1.changed || migration2.changed || migration3.changed || migration4.changed || migration5.changed || migration6.changed || migration7.changed || migration8.changed) {
|
|
890
1026
|
await writeSettingsToDisk(migration8.settings);
|
|
891
1027
|
}
|
|
892
|
-
|
|
1028
|
+
// Migrations run on the base view; a surface asks for its own resolution
|
|
1029
|
+
// of the per-surface keys on top of the migrated files.
|
|
1030
|
+
return normalizeSettingsSurface(surface) ? readSettingsFromDisk({ surface }) : migration8.settings;
|
|
893
1031
|
};
|
|
894
1032
|
|
|
895
|
-
const persistSettings = async (changes) => {
|
|
1033
|
+
const persistSettings = async (changes, { surface = null } = {}) => {
|
|
896
1034
|
persistSettingsLock = persistSettingsLock.then(async () => {
|
|
897
1035
|
// Log field names only — changes can carry credentials (UI password,
|
|
898
1036
|
// client tokens, tunnel tokens) that must never reach the log file.
|
|
899
1037
|
console.log('[persistSettings] Updating fields:', Object.keys(changes || {}).join(', ') || '(none)');
|
|
900
|
-
const current = await readSettingsFromDisk();
|
|
1038
|
+
const current = await readSettingsFromDisk({ surface });
|
|
901
1039
|
const sanitized = sanitizeSettingsUpdate(changes);
|
|
1040
|
+
for (const key of Object.keys(sanitized)) {
|
|
1041
|
+
// Device state belongs to the install in front of the user, never to
|
|
1042
|
+
// the instance; a client that still sends it is simply ignored.
|
|
1043
|
+
if (isDeviceSettingsKey(key)) {
|
|
1044
|
+
delete sanitized[key];
|
|
1045
|
+
} else if (preferencesUnavailable && isProfileSettingsKey(key)) {
|
|
1046
|
+
console.warn(`[persistSettings] Dropping ${key}: preferences file is unreadable`);
|
|
1047
|
+
delete sanitized[key];
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
902
1050
|
let next = mergePersistedSettings(current, sanitized);
|
|
903
1051
|
|
|
904
1052
|
const normalizedState = normalizeSettingsPaths(next);
|
|
@@ -962,7 +1110,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
962
1110
|
}
|
|
963
1111
|
}
|
|
964
1112
|
|
|
965
|
-
await writeSettingsToDisk(next);
|
|
1113
|
+
await writeSettingsToDisk(next, { surface, changedKeys: Object.keys(sanitized) });
|
|
966
1114
|
return formatSettingsResponse(next);
|
|
967
1115
|
});
|
|
968
1116
|
|