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
This commit is contained in:
@@ -6,6 +6,11 @@
|
|||||||
"agent": "atlas",
|
"agent": "atlas",
|
||||||
"worktree_path": "C:\\dev\\workspace\\kintone\\kintone-customize-manager",
|
"worktree_path": "C:\\dev\\workspace\\kintone\\kintone-customize-manager",
|
||||||
"progress": {
|
"progress": {
|
||||||
|
"completed_waves": 6,
|
||||||
|
"total_waves": 6,
|
||||||
|
"status": "completed",
|
||||||
|
"last_updated": "2026-03-12T02:00:00.000Z"
|
||||||
|
}
|
||||||
"completed_waves": 4,
|
"completed_waves": 4,
|
||||||
"total_waves": 6,
|
"total_waves": 6,
|
||||||
"status": "in_progress",
|
"status": "in_progress",
|
||||||
|
|||||||
57
.sisyphus/notepads/core-features/learnings.md
Normal file
57
.sisyphus/notepads/core-features/learnings.md
Normal file
@@ -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<T>` 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/
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, safeStorage } from "electron";
|
import { app, safeStorage } from "electron";
|
||||||
import Store from "electron-store";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import type { Domain, DomainWithPassword } from "@renderer/types/domain";
|
import type { Domain, DomainWithPassword } from "@renderer/types/domain";
|
||||||
@@ -15,28 +14,6 @@ import type {
|
|||||||
BackupMetadata,
|
BackupMetadata,
|
||||||
} from "@renderer/types/version";
|
} from "@renderer/types/version";
|
||||||
|
|
||||||
// ==================== Store Configuration ====================
|
|
||||||
|
|
||||||
interface AppConfig {
|
|
||||||
domains: Domain[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecureStore {
|
|
||||||
[key: string]: string; // encrypted passwords
|
|
||||||
}
|
|
||||||
|
|
||||||
const configStore = new Store<AppConfig>({
|
|
||||||
name: "config",
|
|
||||||
defaults: {
|
|
||||||
domains: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const secureStore = new Store<SecureStore>({
|
|
||||||
name: "secure",
|
|
||||||
defaults: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==================== Path Helpers ====================
|
// ==================== 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<T>(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<T>(filePath: string, data: T): void {
|
||||||
|
ensureDir(path.dirname(filePath));
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Password Encryption ====================
|
// ==================== Password Encryption ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,20 +127,24 @@ export async function saveDomain(
|
|||||||
domain: Domain,
|
domain: Domain,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const domains = configStore.get("domains") as Domain[];
|
const configPath = getConfigPath();
|
||||||
const existingIndex = domains.findIndex((d) => d.id === domain.id);
|
const config = readJsonFile<AppConfig>(configPath, { domains: [] });
|
||||||
|
const existingIndex = config.domains.findIndex((d) => d.id === domain.id);
|
||||||
|
|
||||||
// Encrypt and store password
|
// Encrypt and store password
|
||||||
const encrypted = encryptPassword(password);
|
const encrypted = encryptPassword(password);
|
||||||
secureStore.set(`password_${domain.id}`, encrypted.toString("base64"));
|
const securePath = getSecureStorePath();
|
||||||
|
const secureStore = readJsonFile<SecureStore>(securePath, {});
|
||||||
|
secureStore[`password_${domain.id}`] = encrypted.toString("base64");
|
||||||
|
writeJsonFile(securePath, secureStore);
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
domains[existingIndex] = domain;
|
config.domains[existingIndex] = domain;
|
||||||
} else {
|
} 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(
|
export async function getDomain(
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<DomainWithPassword | null> {
|
): Promise<DomainWithPassword | null> {
|
||||||
const domains = configStore.get("domains") as Domain[];
|
const configPath = getConfigPath();
|
||||||
const domain = domains.find((d) => d.id === id);
|
const config = readJsonFile<AppConfig>(configPath, { domains: [] });
|
||||||
|
const domain = config.domains.find((d) => d.id === id);
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedBase64 = secureStore.get(`password_${id}`);
|
const securePath = getSecureStorePath();
|
||||||
|
const secureStore = readJsonFile<SecureStore>(securePath, {});
|
||||||
|
const encryptedBase64 = secureStore[`password_${id}`];
|
||||||
|
|
||||||
if (!encryptedBase64) {
|
if (!encryptedBase64) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -167,17 +187,24 @@ export async function getDomain(
|
|||||||
* Get all domains (without passwords)
|
* Get all domains (without passwords)
|
||||||
*/
|
*/
|
||||||
export async function listDomains(): Promise<Domain[]> {
|
export async function listDomains(): Promise<Domain[]> {
|
||||||
return configStore.get("domains") as Domain[];
|
const configPath = getConfigPath();
|
||||||
|
const config = readJsonFile<AppConfig>(configPath, { domains: [] });
|
||||||
|
return config.domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a domain and its password
|
* Delete a domain and its password
|
||||||
*/
|
*/
|
||||||
export async function deleteDomain(id: string): Promise<void> {
|
export async function deleteDomain(id: string): Promise<void> {
|
||||||
const domains = configStore.get("domains") as Domain[];
|
const configPath = getConfigPath();
|
||||||
const filtered = domains.filter((d) => d.id !== id);
|
const config = readJsonFile<AppConfig>(configPath, { domains: [] });
|
||||||
configStore.set("domains", filtered);
|
config.domains = config.domains.filter((d) => d.id !== id);
|
||||||
secureStore.delete(`password_${id}`);
|
writeJsonFile(configPath, config);
|
||||||
|
|
||||||
|
const securePath = getSecureStorePath();
|
||||||
|
const secureStore = readJsonFile<SecureStore>(securePath, {});
|
||||||
|
delete secureStore[`password_${id}`];
|
||||||
|
writeJsonFile(securePath, secureStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Version Management ====================
|
// ==================== Version Management ====================
|
||||||
@@ -367,8 +394,8 @@ export function getStorageStats(): {
|
|||||||
storageBackend: string;
|
storageBackend: string;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
configPath: configStore.path,
|
configPath: getConfigPath(),
|
||||||
securePath: secureStore.path,
|
securePath: getSecureStorePath(),
|
||||||
storageBase: getStorageBase(),
|
storageBase: getStorageBase(),
|
||||||
isSecureStorageAvailable: isSecureStorageAvailable(),
|
isSecureStorageAvailable: isSecureStorageAvailable(),
|
||||||
storageBackend: getStorageBackend(),
|
storageBackend: getStorageBackend(),
|
||||||
|
|||||||
Reference in New Issue
Block a user