|
1 | | -import Store from "electron-store" |
| 1 | +import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs" |
| 2 | +import path from "node:path" |
2 | 3 | import electron from "electron" |
3 | 4 |
|
4 | 5 | import { SETTINGS_STORE } from "./keys" |
5 | 6 |
|
6 | | -const cache = new Map<string, Store>() |
| 7 | +const cache = new Map<string, SettingsStore>() |
| 8 | + |
| 9 | +export type SettingsStore = ReturnType<typeof createSettingsStore> |
7 | 10 |
|
8 | 11 | // Main-process settings only (onboarding, default server, window list, appearance, updater). |
9 | | -// These are read synchronously before the storage layer exists and written on user action, so |
10 | | -// electron-store's synchronous file write is acceptable here. Renderer state goes through |
11 | | -// DesktopStorage instead. |
| 12 | +// These are read synchronously before the storage layer exists and written on user action, so a |
| 13 | +// synchronous whole-file write is acceptable here. Renderer state goes through DesktopStorage. |
| 14 | +// |
| 15 | +// The file is the tab-indented JSON object electron-store used to write, so existing profiles |
| 16 | +// (and older builds) read the same data. |
12 | 17 | // |
13 | | -// We cannot instantiate the electron-store at module load time because |
14 | | -// module import hoisting causes this to run before app.setPath("userData", ...) |
15 | | -// in index.ts has executed, which would result in files being written to the default directory |
16 | | -// (e.g. bad: %APPDATA%\@opencode\desktop\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings). |
| 18 | +// We cannot instantiate the store at module load time because module import hoisting causes this |
| 19 | +// to run before app.setPath("userData", ...) has executed, which would result in files being |
| 20 | +// written to the default directory (e.g. bad: %APPDATA%\@opencode\desktop\opencode.settings vs |
| 21 | +// good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings). |
17 | 22 | export function getStore(name = SETTINGS_STORE) { |
18 | 23 | const cached = cache.get(name) |
19 | 24 | if (cached) return cached |
20 | | - const next = new Store({ |
21 | | - name, |
22 | | - cwd: electron.app.getPath("userData"), |
23 | | - fileExtension: "", |
24 | | - accessPropertiesByDotNotation: false, |
25 | | - }) |
| 25 | + const next = createSettingsStore(path.join(electron.app.getPath("userData"), name)) |
26 | 26 | cache.set(name, next) |
27 | 27 | return next |
28 | 28 | } |
| 29 | + |
| 30 | +export function createSettingsStore(file: string) { |
| 31 | + let data = read(file) |
| 32 | + const write = () => { |
| 33 | + mkdirSync(path.dirname(file), { recursive: true }) |
| 34 | + writeFileSync(`${file}.tmp`, JSON.stringify(data, null, "\t")) |
| 35 | + renameSync(`${file}.tmp`, file) |
| 36 | + } |
| 37 | + return { |
| 38 | + path: file, |
| 39 | + get: (key: string): unknown => data[key], |
| 40 | + set: (key: string, value: unknown) => { |
| 41 | + if (value === undefined) throw new TypeError(`Use \`delete()\` to clear values: ${key}`) |
| 42 | + data = { ...data, [key]: value } |
| 43 | + write() |
| 44 | + }, |
| 45 | + delete: (key: string) => { |
| 46 | + data = Object.fromEntries(Object.entries(data).filter(([item]) => item !== key)) |
| 47 | + write() |
| 48 | + }, |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +// A missing file is an empty store. An unreadable one is set aside instead of blocking startup. |
| 53 | +function read(file: string): Record<string, unknown> { |
| 54 | + if (!existsSync(file)) return {} |
| 55 | + try { |
| 56 | + const parsed: unknown = JSON.parse(readFileSync(file, "utf8")) |
| 57 | + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {} |
| 58 | + } catch { |
| 59 | + renameSync(file, `${file}.corrupt`) |
| 60 | + return {} |
| 61 | + } |
| 62 | +} |
0 commit comments