From 184919b56255ef76e59f89bdd3e1ef6ec2b11145 Mon Sep 17 00:00:00 2001 From: xue jiahao Date: Thu, 12 Mar 2026 11:03:49 +0800 Subject: [PATCH] fix(main): replace electron-store with native fs for ESM compatibility - Remove electron-store dependency usage (ESM-only) - Implement JSON file storage using native fs module - Read/write config.json and secure.json directly - Maintain same API for domain and version storage --- .sisyphus/boulder.json | 5 + .sisyphus/notepads/core-features/learnings.md | 57 ++++++++++ src/main/storage.ts | 105 +++++++++++------- 3 files changed, 128 insertions(+), 39 deletions(-) create mode 100644 .sisyphus/notepads/core-features/learnings.md diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json index e3b758a..9bb339c 100644 --- a/.sisyphus/boulder.json +++ b/.sisyphus/boulder.json @@ -6,6 +6,11 @@ "agent": "atlas", "worktree_path": "C:\\dev\\workspace\\kintone\\kintone-customize-manager", "progress": { + "completed_waves": 6, + "total_waves": 6, + "status": "completed", + "last_updated": "2026-03-12T02:00:00.000Z" + } "completed_waves": 4, "total_waves": 6, "status": "in_progress", diff --git a/.sisyphus/notepads/core-features/learnings.md b/.sisyphus/notepads/core-features/learnings.md new file mode 100644 index 0000000..2b64813 --- /dev/null +++ b/.sisyphus/notepads/core-features/learnings.md @@ -0,0 +1,57 @@ +# Learnings + +## [2026-03-12] Kintone Customize Manager Core Features + +### Technical Decisions + +1. **Type System Organization** + - Created separate type files for domain, kintone, version, and ipc types + - Used `Result` pattern for unified IPC response handling + - Added path alias `@renderer/*` to tsconfig.node.json for main process imports + +2. **SafeStorage for Password Encryption** + - Used Electron's built-in `safeStorage` API instead of deprecated `keytar` + - Implemented `isSecureStorageAvailable()` check for Linux compatibility + - Store encrypted passwords as base64 strings in separate secure store + +3. **Kintone API Client** + - Used native `fetch` API (Node.js 18+) instead of axios + - Implemented 30-second timeout with AbortController + - Support both password authentication and API token authentication + +4. **IPC Architecture** + - All handlers return `{ success: boolean, data?: T, error?: string }` + - Used `contextBridge` to expose safe API to renderer + - Created typed `ElectronAPI` interface for window.api + +5. **State Management** + - Zustand with persist middleware for domain persistence + - Separate stores for domain, app, deploy, and version state + - IPC calls in store actions, not in components + +6. **UI Components** + - LobeHub UI + Ant Design 6 + antd-style for styling + - CodeMirror 6 for code viewing with syntax highlighting + - Step-by-step deploy dialog with confirmation + +### Gotchas + +1. **tsconfig.node.json needs @renderer/\* path alias** + - Main process files import from `@renderer/types` + - Without the alias, build fails + +2. **JSON import in preload requires named exports** + - Use `{ Component }` syntax, not `import Component from ...` + - Default exports don't work with contextBridge + +3. **CodeMirror extensions must match file type** + - Use `javascript()` for JS, `css()` for CSS files + - Extensions are loaded dynamically based on file type + +### Files Created + +- **Types**: domain.ts, kintone.ts, version.ts, ipc.ts +- **Main**: storage.ts, kintone-api.ts, ipc-handlers.ts +- **Preload**: index.ts, index.d.ts (updated) +- **Stores**: domainStore.ts, appStore.ts, deployStore.ts, versionStore.ts +- **Components**: DomainManager/, SpaceTree/, AppDetail/, CodeViewer/, FileUploader/, DeployDialog/, VersionHistory/ diff --git a/src/main/storage.ts b/src/main/storage.ts index b52b97d..feeb9ce 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -5,7 +5,6 @@ */ import { app, safeStorage } from "electron"; -import Store from "electron-store"; import * as fs from "fs"; import * as path from "path"; import type { Domain, DomainWithPassword } from "@renderer/types/domain"; @@ -15,28 +14,6 @@ import type { BackupMetadata, } from "@renderer/types/version"; -// ==================== Store Configuration ==================== - -interface AppConfig { - domains: Domain[]; -} - -interface SecureStore { - [key: string]: string; // encrypted passwords -} - -const configStore = new Store({ - name: "config", - defaults: { - domains: [], - }, -}); - -const secureStore = new Store({ - name: "secure", - defaults: {}, -}); - // ==================== Path Helpers ==================== /** @@ -62,6 +39,41 @@ function ensureDir(dirPath: string): void { } } +// ==================== JSON File Store ==================== + +interface AppConfig { + domains: Domain[]; +} + +interface SecureStore { + [key: string]: string; // encrypted passwords +} + +function getConfigPath(): string { + return path.join(getStorageBase(), "config.json"); +} + +function getSecureStorePath(): string { + return path.join(getStorageBase(), "secure.json"); +} + +function readJsonFile(filePath: string, defaultValue: T): T { + try { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(content) as T; + } + return defaultValue; + } catch { + return defaultValue; + } +} + +function writeJsonFile(filePath: string, data: T): void { + ensureDir(path.dirname(filePath)); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); +} + // ==================== Password Encryption ==================== /** @@ -115,20 +127,24 @@ export async function saveDomain( domain: Domain, password: string, ): Promise { - const domains = configStore.get("domains") as Domain[]; - const existingIndex = domains.findIndex((d) => d.id === domain.id); + const configPath = getConfigPath(); + const config = readJsonFile(configPath, { domains: [] }); + const existingIndex = config.domains.findIndex((d) => d.id === domain.id); // Encrypt and store password const encrypted = encryptPassword(password); - secureStore.set(`password_${domain.id}`, encrypted.toString("base64")); + const securePath = getSecureStorePath(); + const secureStore = readJsonFile(securePath, {}); + secureStore[`password_${domain.id}`] = encrypted.toString("base64"); + writeJsonFile(securePath, secureStore); if (existingIndex >= 0) { - domains[existingIndex] = domain; + config.domains[existingIndex] = domain; } else { - domains.push(domain); + config.domains.push(domain); } - configStore.set("domains", domains); + writeJsonFile(configPath, config); } /** @@ -137,14 +153,18 @@ export async function saveDomain( export async function getDomain( id: string, ): Promise { - const domains = configStore.get("domains") as Domain[]; - const domain = domains.find((d) => d.id === id); + const configPath = getConfigPath(); + const config = readJsonFile(configPath, { domains: [] }); + const domain = config.domains.find((d) => d.id === id); if (!domain) { return null; } - const encryptedBase64 = secureStore.get(`password_${id}`); + const securePath = getSecureStorePath(); + const secureStore = readJsonFile(securePath, {}); + const encryptedBase64 = secureStore[`password_${id}`]; + if (!encryptedBase64) { return null; } @@ -167,17 +187,24 @@ export async function getDomain( * Get all domains (without passwords) */ export async function listDomains(): Promise { - return configStore.get("domains") as Domain[]; + const configPath = getConfigPath(); + const config = readJsonFile(configPath, { domains: [] }); + return config.domains; } /** * Delete a domain and its password */ export async function deleteDomain(id: string): Promise { - const domains = configStore.get("domains") as Domain[]; - const filtered = domains.filter((d) => d.id !== id); - configStore.set("domains", filtered); - secureStore.delete(`password_${id}`); + const configPath = getConfigPath(); + const config = readJsonFile(configPath, { domains: [] }); + config.domains = config.domains.filter((d) => d.id !== id); + writeJsonFile(configPath, config); + + const securePath = getSecureStorePath(); + const secureStore = readJsonFile(securePath, {}); + delete secureStore[`password_${id}`]; + writeJsonFile(securePath, secureStore); } // ==================== Version Management ==================== @@ -367,8 +394,8 @@ export function getStorageStats(): { storageBackend: string; } { return { - configPath: configStore.path, - securePath: secureStore.path, + configPath: getConfigPath(), + securePath: getSecureStorePath(), storageBase: getStorageBase(), isSecureStorageAvailable: isSecureStorageAvailable(), storageBackend: getStorageBackend(),