migrate.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import { app } from "electron"
  2. import log from "electron-log/main.js"
  3. import { existsSync, readdirSync, readFileSync } from "node:fs"
  4. import { homedir } from "node:os"
  5. import { join } from "node:path"
  6. import { CHANNEL } from "./constants"
  7. import { getStore, store } from "./store"
  8. const TAURI_MIGRATED_KEY = "tauriMigrated"
  9. // Resolve the directory where Tauri stored its .dat files for the given app identifier.
  10. // Mirrors Tauri's AppLocalData / AppData resolution per OS.
  11. function tauriDir(id: string) {
  12. switch (process.platform) {
  13. case "darwin":
  14. return join(homedir(), "Library", "Application Support", id)
  15. case "win32":
  16. return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), id)
  17. default:
  18. return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), id)
  19. }
  20. }
  21. // The Tauri app identifier changes between dev/beta/prod builds.
  22. const TAURI_APP_IDS: Record<string, string> = {
  23. dev: "ai.opencode.desktop.dev",
  24. beta: "ai.opencode.desktop.beta",
  25. prod: "ai.opencode.desktop",
  26. }
  27. function tauriAppId() {
  28. return app.isPackaged ? TAURI_APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
  29. }
  30. // Migrate a single Tauri .dat file into the corresponding electron-store.
  31. // `opencode.settings.dat` is special: it maps to the `opencode.settings` store
  32. // (the electron-store name without the `.dat` extension). All other .dat files
  33. // keep their full filename as the electron-store name so they match what the
  34. // renderer already passes via IPC (e.g. `"default.dat"`, `"opencode.global.dat"`).
  35. function migrateFile(datPath: string, filename: string) {
  36. let data: Record<string, unknown>
  37. try {
  38. data = JSON.parse(readFileSync(datPath, "utf-8"))
  39. } catch (err) {
  40. log.warn("tauri migration: failed to parse", filename, err)
  41. return
  42. }
  43. // opencode.settings.dat → the electron settings store ("opencode.settings").
  44. // All other .dat files keep their full filename as the store name so they match
  45. // what the renderer passes via IPC (e.g. "default.dat", "opencode.global.dat").
  46. const storeName = filename === "opencode.settings.dat" ? "opencode.settings" : filename
  47. const target = getStore(storeName)
  48. const migrated: string[] = []
  49. const skipped: string[] = []
  50. for (const [key, value] of Object.entries(data)) {
  51. // Don't overwrite values the user has already set in the Electron app.
  52. if (target.has(key)) {
  53. skipped.push(key)
  54. continue
  55. }
  56. target.set(key, value)
  57. migrated.push(key)
  58. }
  59. log.log("tauri migration: migrated", filename, "→", storeName, { migrated, skipped })
  60. }
  61. export function migrate() {
  62. if (store.get(TAURI_MIGRATED_KEY)) {
  63. log.log("tauri migration: already done, skipping")
  64. return
  65. }
  66. const dir = tauriDir(tauriAppId())
  67. log.log("tauri migration: starting", { dir })
  68. if (!existsSync(dir)) {
  69. log.log("tauri migration: no tauri data directory found, nothing to migrate")
  70. store.set(TAURI_MIGRATED_KEY, true)
  71. return
  72. }
  73. for (const filename of readdirSync(dir)) {
  74. if (!filename.endsWith(".dat")) continue
  75. migrateFile(join(dir, filename), filename)
  76. }
  77. log.log("tauri migration: complete")
  78. store.set(TAURI_MIGRATED_KEY, true)
  79. }