/** * IPC Handlers * Bridges renderer requests to main process modules * Based on REQUIREMENTS.md:228-268 */ import { ipcMain, dialog, app } from "electron"; import { autoUpdater } from "electron-updater"; import { v4 as uuidv4 } from "uuid"; import { saveDomain, getDomain, deleteDomain, listDomains, listVersions, deleteVersion, saveDownload, saveBackup, getLocale, setLocale, } from "./storage"; import { KintoneClient, createKintoneClient } from "./kintone-api"; import type { Result } from "@shared/types/ipc"; import type { CreateDomainParams, UpdateDomainParams, TestDomainConnectionParams, GetAppsParams, GetAppDetailParams, GetFileContentParams, DeployParams, DeployResult, DownloadParams, DownloadResult, GetVersionsParams, RollbackParams, SetLocaleParams, CheckUpdateResult, } from "@shared/types/ipc"; import type { LocaleCode } from "@shared/types/locale"; import type { Domain, DomainWithStatus, DomainWithPassword, } from "@shared/types/domain"; import type { Version, DownloadMetadata, BackupMetadata, DownloadFile, } from "@shared/types/version"; import type { AppCustomizeParameter, AppDetail } from "@shared/types/kintone"; import { getDisplayName, getFileKey } from "@shared/utils/fileDisplay"; import { getErrorMessage } from "./errors"; // Cache for Kintone clients const clientCache = new Map(); /** * Get or create a Kintone client for a domain */ async function getClient(domainId: string): Promise { if (clientCache.has(domainId)) { return clientCache.get(domainId)!; } const domainWithPassword = await getDomain(domainId); if (!domainWithPassword) { throw new Error(getErrorMessage("domainNotFound")); } const client = createKintoneClient(domainWithPassword); clientCache.set(domainId, client); return client; } /** * Helper to wrap IPC handlers with error handling */ function handle

( channel: string, handler: (params: P) => Promise, ): void { ipcMain.handle(channel, async (_event, params: P): Promise> => { try { // For handlers without params (P=void), params will be undefined but handler ignores it // For handlers with params, params will be the typed value const data = await handler(params as P); return { success: true, data }; } catch (error) { const message = error instanceof Error ? error.message : getErrorMessage("unknownError"); return { success: false, error: message }; } }); } // ==================== Domain Management IPC Handlers ==================== /** * Get all domains */ function registerGetDomains(): void { handle("getDomains", async () => { return listDomains(); }); } /** * Create a new domain * Deduplication: Check if domain+username already exists */ function registerCreateDomain(): void { handle("createDomain", async (params) => { // Check for duplicate domain+username const existingDomains = await listDomains(); const duplicate = existingDomains.find( (d) => d.domain.toLowerCase() === params.domain.toLowerCase() && d.username.toLowerCase() === params.username.toLowerCase(), ); if (duplicate) { throw new Error( getErrorMessage("domainDuplicate", { domain: params.domain, username: params.username, }), ); } const now = new Date().toISOString(); const domain: Domain = { id: uuidv4(), name: params.name, domain: params.domain, username: params.username, createdAt: now, updatedAt: now, }; await saveDomain(domain, params.password); return domain; }); } /** * Update an existing domain */ function registerUpdateDomain(): void { handle("updateDomain", async (params) => { const domains = await listDomains(); const existing = domains.find((d) => d.id === params.id); if (!existing) { throw new Error(getErrorMessage("domainNotFound")); } const updated: Domain = { ...existing, name: params.name ?? existing.name, domain: params.domain ?? existing.domain, username: params.username ?? existing.username, updatedAt: new Date().toISOString(), }; // If password is provided, update it if (params.password) { await saveDomain(updated, params.password); } else { // Get existing password and re-save const existingWithPassword = await getDomain(params.id); if (existingWithPassword) { await saveDomain(updated, existingWithPassword.password); } } // Clear cached client clientCache.delete(params.id); return updated; }); } /** * Delete a domain */ function registerDeleteDomain(): void { handle("deleteDomain", async (id) => { await deleteDomain(id); clientCache.delete(id); }); } /** * Test domain connection */ function registerTestConnection(): void { handle("testConnection", async (id) => { const domainWithPassword = await getDomain(id); if (!domainWithPassword) { throw new Error(getErrorMessage("domainNotFound")); } const client = createKintoneClient(domainWithPassword); const result = await client.testConnection(); return { ...domainWithPassword, connectionStatus: result.success ? "connected" : "error", connectionError: result.error, }; }); } /** * Test domain connection with temporary credentials */ function registerTestDomainConnection(): void { handle( "testDomainConnection", async (params) => { const tempDomain: DomainWithPassword = { id: "temp", name: "temp", domain: params.domain, username: params.username, password: params.password || "", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const client = createKintoneClient(tempDomain); const result = await client.testConnection(); if (!result.success) { throw new Error(result.error || getErrorMessage("connectionFailed")); } return true; }, ); } // ==================== Browse IPC Handlers ==================== /** * Get apps */ function registerGetApps(): void { handle>>( "getApps", async (params) => { const client = await getClient(params.domainId); return client.getApps({ limit: params.limit, offset: params.offset, }); }, ); } /** * Get app detail */ function registerGetAppDetail(): void { handle< GetAppDetailParams, Awaited> >("getAppDetail", async (params) => { const client = await getClient(params.domainId); return client.getAppDetail(params.appId); }); } /** * Get file content */ function registerGetFileContent(): void { handle< GetFileContentParams, Awaited> >("getFileContent", async (params) => { const client = await getClient(params.domainId); return client.getFileContent(params.fileKey); }); } // ==================== Deploy IPC Handlers ==================== /** * Add files to backup for a specific platform and file type */ async function addFilesToBackup( platform: "desktop" | "mobile", fileType: "js" | "css", client: KintoneClient, appDetail: AppDetail, backupFiles: Map, backupFileList: DownloadFile[] ): Promise { const files = appDetail.customization?.[platform]?.[fileType] || []; for (const [index, file] of files.entries()) { const fileKey = getFileKey(file); if (fileKey) { const fileContent = await client.getFileContent(fileKey); const content = Buffer.from(fileContent.content || "", "base64"); const fileName = getDisplayName(file, fileType, index); const type = platform === "desktop" ? "pc" : "mobile"; backupFiles.set(`${type}/${fileName}`, content); backupFileList.push({ type, fileType, fileName, fileKey, size: content.length, path: `${type}/${fileName}`, }); } } } /** * Deploy files to Kintone */ function registerDeploy(): void { handle("deploy", async (params) => { const client = await getClient(params.domainId); const domainWithPassword = await getDomain(params.domainId); if (!domainWithPassword) { throw new Error(getErrorMessage("domainNotFound")); } // Get current app config for backup const appDetail = await client.getAppDetail(params.appId); // Create backup of current configuration const backupFiles = new Map(); const backupFileList: BackupMetadata["files"] = []; // Add desktop files to backup await addFilesToBackup("desktop", "js", client, appDetail, backupFiles, backupFileList); await addFilesToBackup("desktop", "css", client, appDetail, backupFiles, backupFileList); // Add mobile files to backup await addFilesToBackup("mobile", "js", client, appDetail, backupFiles, backupFileList); await addFilesToBackup("mobile", "css", client, appDetail, backupFiles, backupFileList); // Save backup const backupMetadata: BackupMetadata = { backedUpAt: new Date().toISOString(), domain: domainWithPassword.domain, domainId: params.domainId, appId: params.appId, appName: appDetail.name, files: backupFileList, trigger: "deploy", }; const backupPath = await saveBackup(backupMetadata, backupFiles); // Upload new files and build customization config directly const newConfig: AppCustomizeParameter = { app: params.appId, scope: "ALL", desktop: { js: [], css: [], }, mobile: { js: [], css: [], }, }; for (const file of params.files) { const fileKey = await client.uploadFile(file.content, file.fileName); const fileEntry = { type: "FILE" as const, file: { fileKey: fileKey.fileKey } }; // Add to corresponding field based on file type and position if (file.fileType === "js") { if (file.position.startsWith("pc_")) { newConfig.desktop!.js!.push(fileEntry); } else if (file.position.startsWith("mobile_")) { newConfig.mobile!.js!.push(fileEntry); } } else if (file.fileType === "css") { if (file.position === "pc_css") { newConfig.desktop!.css!.push(fileEntry); } else if (file.position === "mobile_css") { newConfig.mobile!.css!.push(fileEntry); } } } // Update app customization await client.updateAppCustomize(params.appId, newConfig); // Deploy the changes await client.deployApp(params.appId); return { success: true, backupPath, backupMetadata, }; }); } // ==================== Download IPC Handlers ==================== /** * Download files from Kintone */ function registerDownload(): void { handle("download", async (params) => { const client = await getClient(params.domainId); const domainWithPassword = await getDomain(params.domainId); if (!domainWithPassword) { throw new Error(getErrorMessage("domainNotFound")); } const appDetail = await client.getAppDetail(params.appId); const downloadFiles = new Map(); const downloadFileList: DownloadMetadata["files"] = []; // Download based on requested file types const fileTypes = params.fileTypes || [ "pc_js", "pc_css", "mobile_js", "mobile_css", ]; for (const fileType of fileTypes) { const files = fileType === "pc_js" ? appDetail.customization?.desktop?.js : fileType === "pc_css" ? appDetail.customization?.desktop?.css : fileType === "mobile_js" ? appDetail.customization?.mobile?.js : appDetail.customization?.mobile?.css; for (const [index, file] of (files || []).entries()) { const fileKey = getFileKey(file); if (fileKey) { const content = await client.getFileContent(fileKey); const buffer = Buffer.from(content.content || "", "base64"); const ext = fileType.includes("js") ? "js" : "css"; const fileName = getDisplayName(file, ext, index); const type = fileType.includes("mobile") ? "mobile" : "pc"; downloadFiles.set(`${type}/${fileName}`, buffer); downloadFileList.push({ type, fileType: ext, fileName, fileKey, size: buffer.length, path: `${type}/${fileName}`, }); } } } // Save download metadata const metadata: DownloadMetadata = { downloadedAt: new Date().toISOString(), domain: domainWithPassword.domain, domainId: params.domainId, appId: params.appId, appName: appDetail.name, spaceId: appDetail.spaceId || "", spaceName: "", // Would need to fetch from space API files: downloadFileList, source: "kintone", }; const path = await saveDownload(metadata, downloadFiles); return { success: true, path, metadata, }; }); } // ==================== Version IPC Handlers ==================== /** * Get versions for an app */ function registerGetVersions(): void { handle("getVersions", async (params) => { return listVersions(params.domainId, params.appId); }); } /** * Delete a version */ function registerDeleteVersion(): void { handle("deleteVersion", async (id) => { return deleteVersion(id); }); } /** * Rollback to a previous version */ function registerRollback(): void { handle("rollback", async (_params) => { // This would read the version file and redeploy // Simplified implementation - would need full implementation throw new Error(getErrorMessage("rollbackNotImplemented")); }); } // ==================== Locale IPC Handlers ==================== /** * Get the current locale */ function registerGetLocale(): void { handle("getLocale", async () => { return getLocale(); }); } /** * Set the locale */ function registerSetLocale(): void { handle("setLocale", async (params) => { setLocale(params.locale); }); } // ==================== App Version & Update IPC Handlers ==================== /** * Get the current app version */ function registerGetAppVersion(): void { handle("getAppVersion", async () => { return app.getVersion(); }); } /** * Check for app updates */ function registerCheckForUpdates(): void { handle("checkForUpdates", async () => { try { // In development, autoUpdater.checkForUpdates will throw an error // because there's no update server configured if (process.env.NODE_ENV === "development" || !app.isPackaged) { // Return mock result for development return { hasUpdate: false, updateInfo: undefined, }; } const result = await autoUpdater.checkForUpdates(); if (result && result.updateInfo) { const currentVersion = app.getVersion(); const latestVersion = result.updateInfo.version; // Compare versions const hasUpdate = latestVersion !== currentVersion; return { hasUpdate, updateInfo: hasUpdate ? { version: result.updateInfo.version, releaseDate: result.updateInfo.releaseDate, releaseNotes: result.releaseNotes as string | undefined, } : undefined, }; } return { hasUpdate: false, updateInfo: undefined, }; } catch (error) { // If update check fails, return no update available console.error("Update check failed:", error); return { hasUpdate: false, updateInfo: undefined, }; } }); } // ==================== Register All Handlers ==================== /** * Register all IPC handlers */ export function registerIpcHandlers(): void { // Domain management registerGetDomains(); registerCreateDomain(); registerUpdateDomain(); registerDeleteDomain(); registerTestConnection(); registerTestDomainConnection(); // Browse registerGetApps(); registerGetAppDetail(); registerGetFileContent(); // Deploy registerDeploy(); // Download registerDownload(); // Version registerGetVersions(); registerDeleteVersion(); registerRollback(); // Locale registerGetLocale(); registerSetLocale(); // App Version & Update registerGetAppVersion(); registerCheckForUpdates(); // Dialog registerShowSaveDialog(); registerSaveFileContent(); console.log("IPC handlers registered"); } // ==================== Dialog IPC Handlers ==================== /** * Show save dialog */ function registerShowSaveDialog(): void { handle<{ defaultPath?: string }, string | null>("showSaveDialog", async (params) => { const result = await dialog.showSaveDialog({ defaultPath: params.defaultPath, filters: [ { name: "JavaScript", extensions: ["js"] }, { name: "CSS", extensions: ["css"] }, { name: "All Files", extensions: ["*"] }, ], }); return result.filePath || null; }); } /** * Save file content to disk */ function registerSaveFileContent(): void { handle<{ filePath: string; content: string }, void>("saveFileContent", async (params) => { const fs = await import("fs"); const buffer = Buffer.from(params.content, "base64"); await fs.promises.writeFile(params.filePath, buffer); }); }