From 6784ab3f3676baaa7e292ad93ea8464cf248fb72 Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 15 Feb 2026 21:56:45 +0100 Subject: [PATCH] fix: removed duplicated type declarations --- src/main/preload.ts | 208 +------- src/main/shared/electronApi.ts | 486 ++++++++++++++++++ src/renderer/types/electron.d.ts | 453 +--------------- tests/engine/PreloadContract.test.ts | 28 + .../types/electronApiContract.test.ts | 9 + 5 files changed, 542 insertions(+), 642 deletions(-) create mode 100644 src/main/shared/electronApi.ts create mode 100644 tests/engine/PreloadContract.test.ts create mode 100644 tests/renderer/types/electronApiContract.test.ts diff --git a/src/main/preload.ts b/src/main/preload.ts index c16734b..46cdcc6 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,8 +1,9 @@ import { contextBridge, ipcRenderer } from 'electron'; +import type { ElectronAPI } from './shared/electronApi'; // Expose protected methods that allow the renderer process to use // ipcRenderer without exposing the entire object -contextBridge.exposeInMainWorld('electronAPI', { +export const electronAPI: ElectronAPI = { // Projects projects: { create: (data: { name: string; description?: string; slug?: string; dataPath?: string }) => ipcRenderer.invoke('projects:create', data), @@ -82,6 +83,17 @@ contextBridge.exposeInMainWorld('electronAPI', { rebuild: () => ipcRenderer.invoke('postMedia:rebuild'), }, + // Sync + sync: { + configure: (config: unknown) => ipcRenderer.invoke('sync:configure', config), + start: (direction?: 'push' | 'pull' | 'bidirectional') => ipcRenderer.invoke('sync:start', direction), + getStatus: () => ipcRenderer.invoke('sync:getStatus'), + isConfigured: () => ipcRenderer.invoke('sync:isConfigured'), + getPendingCount: () => ipcRenderer.invoke('sync:getPendingCount'), + getLog: (limit?: number) => ipcRenderer.invoke('sync:getLog', limit), + stopAutoSync: () => ipcRenderer.invoke('sync:stopAutoSync'), + }, + // Tasks tasks: { getAll: () => ipcRenderer.invoke('tasks:getAll'), @@ -242,8 +254,8 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('chat-stream-delta', subscription); return () => ipcRenderer.removeListener('chat-stream-delta', subscription); }, - onToolCall: (callback: (data: { conversationId: string; toolCall: unknown }) => void) => { - const subscription = (_event: Electron.IpcRendererEvent, data: { conversationId: string; toolCall: unknown }) => callback(data); + onToolCall: (callback: (data: { conversationId: string; toolCall: { name: string; arguments: Record } }) => void) => { + const subscription = (_event: Electron.IpcRendererEvent, data: { conversationId: string; toolCall: { name: string; arguments: Record } }) => callback(data); ipcRenderer.on('chat-tool-call', subscription); return () => ipcRenderer.removeListener('chat-tool-call', subscription); }, @@ -269,192 +281,6 @@ contextBridge.exposeInMainWorld('electronAPI', { once: (channel: string, callback: (...args: unknown[]) => void) => { ipcRenderer.once(channel, (_event, ...args) => callback(...args)); }, -}); +}; -// Type definitions for the exposed API -export interface ElectronAPI { - projects: { - create: (data: { name: string; description?: string; slug?: string; dataPath?: string }) => Promise; - update: (id: string, data: unknown) => Promise; - delete: (id: string) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getActive: () => Promise; - setActive: (id: string) => Promise; - }; - posts: { - create: (data: unknown) => Promise; - update: (id: string, data: unknown) => Promise; - delete: (id: string) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getByStatus: (status: string) => Promise; - publish: (id: string) => Promise; - unpublish: (id: string) => Promise; - rebuildFromFiles: () => Promise; - search: (query: string) => Promise; - filter: (filter: unknown) => Promise; - getTags: () => Promise; - getCategories: () => Promise; - getByYearMonth: () => Promise<{ year: number; month: number; count: number }[]>; - getTagsWithCounts: () => Promise<{ tag: string; count: number }[]>; - getCategoriesWithCounts: () => Promise<{ category: string; count: number }[]>; - getDashboardStats: () => Promise<{ totalPosts: number; draftCount: number; publishedCount: number; archivedCount: number }>; - getLinksTo: (id: string) => Promise<{ id: string; title: string; slug: string }[]>; - getLinkedBy: (id: string) => Promise<{ id: string; title: string; slug: string }[]>; - rebuildLinks: () => Promise; - }; - media: { - import: (sourcePath: string, metadata?: unknown) => Promise; - importDialog: () => Promise; - update: (id: string, data: unknown) => Promise; - delete: (id: string) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - rebuildFromFiles: () => Promise; - }; - dropbox: { - configure: (config: unknown) => Promise; - isConfigured: () => Promise; - getStatus: () => Promise; - syncAll: () => Promise; - startWatching: () => Promise; - stopWatching: () => Promise; - startPolling: () => Promise; - stopPolling: () => Promise; - getConflicts: () => Promise; - resolveConflict: (conflictId: string, resolution: string) => Promise; - getLastSyncTime: () => Promise; - }; - tasks: { - getAll: () => Promise; - getRunning: () => Promise; - cancel: (taskId: string) => Promise; - clearCompleted: () => Promise; - }; - app: { - getDataPaths: () => Promise<{ database: string; posts: string; media: string }>; - openFolder: (folderPath: string) => Promise; - showItemInFolder: (itemPath: string) => Promise; - selectFolder: (title?: string) => Promise; - getDefaultProjectPath: (projectId: string) => Promise; - }; - meta: { - getTags: () => Promise; - getCategories: () => Promise; - addTag: (tag: string) => Promise; - removeTag: (tag: string) => Promise; - addCategory: (category: string) => Promise; - removeCategory: (category: string) => Promise; - syncOnStartup: () => Promise<{ tags: string[]; categories: string[] }>; - }; - tags: { - getAll: () => Promise; - getWithCounts: () => Promise; - get: (id: string) => Promise; - getByName: (name: string) => Promise; - create: (data: { name: string; color?: string }) => Promise; - update: (id: string, data: { name?: string; color?: string | null }) => Promise; - delete: (id: string) => Promise; - merge: (sourceTagIds: string[], targetTagId: string) => Promise; - rename: (id: string, newName: string) => Promise; - getPostsWithTag: (tagId: string) => Promise; - syncFromPosts: () => Promise; - }; - import: { - selectAndAnalyze: (uploadsFolder?: string) => Promise; - analyzeFile: (filePath: string, uploadsFolder?: string) => Promise; - selectUploadsFolder: () => Promise; - execute: (reportJson: string, uploadsFolder?: string) => Promise<{ taskId: string; totalItems: number }>; - onProgress: (callback: (data: { step: string; detail?: string }) => void) => () => void; - onExecutionProgress: (callback: (data: { - taskId: string; - phase: string; - current: number; - total: number; - detail?: string; - eta?: number; - }) => void) => () => void; - }; - importDefinitions: { - create: (name?: string) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - update: (id: string, updates: unknown) => Promise; - delete: (id: string) => Promise; - }; - metadataDiff: { - getStats: () => Promise<{ - totalPosts: number; - publishedPosts: number; - draftPosts: number; - totalMedia: number; - }>; - scan: () => Promise<{ - totalScanned: number; - postsWithDifferences: number; - differences: Array<{ - postId: string; - title: string; - slug: string; - filePath?: string; - hasDifferences: boolean; - differences: Record; - }>; - groups: Array<{ - field: string; - label: string; - posts: Array<{ - postId: string; - title: string; - slug: string; - dbValue: unknown; - fileValue: unknown; - }>; - }>; - }>; - syncDbToFile: (postIds: string[], groupLabel: string) => Promise<{ success: number; failed: number }>; - syncFileToDb: (postIds: string[], field: string, groupLabel: string) => Promise<{ success: number; failed: number }>; - }; - chat: { - // API Key Management - checkReady: () => Promise<{ ready: boolean; error?: string; backend?: string }>; - validateApiKey: (apiKey: string) => Promise<{ isValid: boolean; models: Array<{ id: string; name: string }> }>; - setApiKey: (apiKey: string) => Promise<{ success: boolean; error?: string }>; - getApiKey: () => Promise<{ hasKey: boolean; maskedKey: string }>; - - // Settings - getAvailableModels: () => Promise<{ success: boolean; models?: Array<{ id: string; name: string }>; selectedModel?: string; error?: string }>; - setDefaultModel: (modelId: string) => Promise<{ success: boolean; error?: string }>; - getSystemPrompt: () => Promise<{ success: boolean; prompt?: string; error?: string }>; - setSystemPrompt: (prompt: string) => Promise<{ success: boolean; error?: string }>; - - // Conversations - getConversations: () => Promise; - createConversation: (title?: string, model?: string) => Promise; - getConversation: (id: string) => Promise; - updateConversation: (id: string, updates: { title?: string; model?: string }) => Promise; - deleteConversation: (id: string) => Promise; - - // Messaging - sendMessage: (conversationId: string, message: string) => Promise; - abortMessage: (conversationId: string) => Promise; - getHistory: (conversationId: string) => Promise; - clearMessages: (conversationId: string) => Promise; - setConversationModel: (conversationId: string, modelId: string) => Promise; - - // Event listeners - onStreamDelta: (callback: (data: { conversationId: string; delta: string }) => void) => () => void; - onToolCall: (callback: (data: { conversationId: string; toolCall: unknown }) => void) => () => void; - onToolResult: (callback: (data: { conversationId: string; result: unknown }) => void) => () => void; - onTitleUpdated: (callback: (data: { conversationId: string; title: string }) => void) => () => void; - }; - on: (channel: string, callback: (...args: unknown[]) => void) => () => void; - once: (channel: string, callback: (...args: unknown[]) => void) => void; -} - -declare global { - interface Window { - electronAPI: ElectronAPI; - } -} +contextBridge.exposeInMainWorld('electronAPI', electronAPI); diff --git a/src/main/shared/electronApi.ts b/src/main/shared/electronApi.ts new file mode 100644 index 0000000..bc85093 --- /dev/null +++ b/src/main/shared/electronApi.ts @@ -0,0 +1,486 @@ +// Type definitions for the Electron API exposed via preload + +export interface ImportExecuteResult { + taskId: string; + totalItems: number; +} + +export interface ImportExecutionProgress { + taskId: string; + phase: string; + current: number; + total: number; + detail?: string; + eta?: number; +} + +export interface ImportCompleteResult { + taskId: string; + success: boolean; + posts: { imported: number; skipped: number; errors: number }; + media: { imported: number; skipped: number; errors: number }; + pages: { imported: number; skipped: number; errors: number }; + tags: { created: number; skipped: number }; +} + +export interface ImportDefinitionData { + id: string; + projectId: string; + name: string; + wxrFilePath: string | null; + uploadsFolderPath: string | null; + lastAnalysisResult: unknown | null; + createdAt: string; + updatedAt: string; +} + +export interface ProjectMetadata { + name: string; + description?: string; + dataPath?: string; + mainLanguage?: string; +} + +export interface ProjectData { + id: string; + name: string; + slug: string; + description?: string; + dataPath?: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export interface PostData { + id: string; + projectId: string; + title: string; + slug: string; + excerpt?: string; + content: string; + status: 'draft' | 'published' | 'archived'; + author?: string; + createdAt: string; + updatedAt: string; + publishedAt?: string; + tags: string[]; + categories: string[]; +} + +export interface PostFilter { + status?: 'draft' | 'published' | 'archived'; + tags?: string[]; + categories?: string[]; + year?: number; + month?: number; + from?: string; + to?: string; +} + +export interface SearchResult { + id: string; + title: string; + slug: string; + excerpt?: string; +} + +export interface MediaData { + id: string; + projectId: string; + filename: string; + originalName: string; + mimeType: string; + size: number; + width?: number; + height?: number; + title?: string; + alt?: string; + caption?: string; + author?: string; + createdAt: string; + updatedAt: string; + tags: string[]; +} + +export interface MediaFilter { + tags?: string[]; + year?: number; + month?: number; +} + +export interface MediaSearchResult { + id: string; + originalName: string; + title?: string; + mimeType: string; + createdAt: string; +} + +export interface TaskProgress { + taskId: string; + status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; + progress: number; + message: string; + startTime: string; + endTime?: string; + error?: string; +} + +export interface SyncConfig { + autoSync: boolean; + syncInterval: number; +} + +export interface SyncResult { + success: boolean; + pushed: number; + pulled: number; + conflicts: number; + errors: string[]; +} + +export interface PaginatedPostsResult { + items: PostData[]; + hasMore: boolean; + total: number; +} + +export interface DashboardStats { + totalPosts: number; + draftCount: number; + publishedCount: number; + archivedCount: number; +} + +export interface TagCount { + tag: string; + count: number; +} + +export interface CategoryCount { + category: string; + count: number; +} + +export interface TagData { + id: string; + projectId: string; + name: string; + color?: string; + createdAt: string; + updatedAt: string; +} + +export interface TagWithCount { + name: string; + color: string | null; + count: number; +} + +export interface DeleteTagResult { + success: boolean; + postsUpdated: number; +} + +export interface MergeTagsResult { + success: boolean; + postsUpdated: number; + tagsDeleted: number; + targetTag: string; +} + +export interface RenameTagResult { + success: boolean; + postsUpdated: number; + oldName: string; + newName: string; +} + +export interface SyncTagsResult { + discovered: number; + added: string[]; +} + +// Post-Media Link types +export interface MediaLinkData { + id: string; + projectId: string; + postId: string; + mediaId: string; + sortOrder: number; + createdAt: string; +} + +// Chat/AI types +export interface ChatConversation { + id: string; + title: string; + model?: string; + createdAt: string; + updatedAt: string; +} + +export interface ChatMessage { + id: string; + conversationId: string; + role: 'user' | 'assistant' | 'system' | 'tool'; + content: string; + toolCallId?: string; + toolCalls?: string; + createdAt: string; +} + +export interface ChatModel { + id: string; + name: string; + provider?: string; +} + +export interface ChatReadyStatus { + ready: boolean; + error?: string; + backend?: string; +} + +export interface ChatApiKeyStatus { + hasKey: boolean; + maskedKey: string; +} + +export interface ChatStreamDelta { + conversationId: string; + delta: string; +} + +export interface ChatToolCall { + conversationId: string; + toolCall: { + name: string; + arguments: Record; + }; +} + +export interface ChatToolResult { + conversationId: string; + result: unknown; +} + +export interface ChatTitleUpdate { + conversationId: string; + title: string; +} + +export interface ElectronAPI { + projects: { + create: (data: { name: string; description?: string; slug?: string; dataPath?: string }) => Promise; + update: (id: string, data: Partial) => Promise; + delete: (id: string) => Promise; + deleteWithData: (id: string) => Promise; + get: (id: string) => Promise; + getAll: () => Promise; + getActive: () => Promise; + setActive: (id: string) => Promise; + }; + posts: { + create: (data: Partial) => Promise; + update: (id: string, data: Partial) => Promise; + delete: (id: string) => Promise; + get: (id: string) => Promise; + getAll: (options?: { limit?: number; offset?: number }) => Promise; + getByStatus: (status: string) => Promise; + publish: (id: string) => Promise; + discard: (id: string) => Promise; + hasPublishedVersion: (id: string) => Promise; + rebuildFromFiles: () => Promise; + reindexText: () => Promise; + search: (query: string) => Promise; + filter: (filter: PostFilter) => Promise; + getTags: () => Promise; + getCategories: () => Promise; + getByYearMonth: () => Promise<{ year: number; month: number; count: number }[]>; + getDashboardStats: () => Promise; + getTagsWithCounts: () => Promise; + getCategoriesWithCounts: () => Promise; + getLinksTo: (id: string) => Promise; + getLinkedBy: (id: string) => Promise; + rebuildLinks: () => Promise; + isSlugAvailable: (slug: string, excludePostId?: string) => Promise; + generateUniqueSlug: (title: string, excludePostId?: string) => Promise; + }; + media: { + import: (sourcePath: string, metadata?: Partial) => Promise; + importDialog: () => Promise; + update: (id: string, data: Partial) => Promise; + replaceFile: (id: string, newSourcePath: string) => Promise; + replaceFileDialog: (id: string) => Promise; + delete: (id: string) => Promise; + get: (id: string) => Promise; + getUrl: (id: string) => Promise; + getFilePath: (id: string) => Promise; + getAll: () => Promise; + rebuildFromFiles: () => Promise; + reindexText: () => Promise; + getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => Promise; + regenerateThumbnails: (id: string) => Promise | null>; + regenerateMissingThumbnails: () => Promise<{ processed: number; generated: number; failed: number }>; + filter: (filter: MediaFilter) => Promise; + search: (query: string) => Promise; + getByYearMonth: () => Promise<{ year: number; month: number; count: number }[]>; + getTags: () => Promise; + getTagsWithCounts: () => Promise; + }; + postMedia: { + link: (postId: string, mediaId: string) => Promise; + unlink: (postId: string, mediaId: string) => Promise; + linkMany: (postId: string, mediaIds: string[]) => Promise<{ linked: string[]; skipped: string[] }>; + unlinkMany: (postId: string, mediaIds: string[]) => Promise<{ unlinked: string[] }>; + getForPost: (postId: string) => Promise; + getForMedia: (mediaId: string) => Promise; + getMediaDataForPost: (postId: string) => Promise>; + reorder: (postId: string, mediaIds: string[]) => Promise; + isLinked: (postId: string, mediaId: string) => Promise; + import: (postId: string, filePath: string) => Promise; + rebuild: () => Promise; + }; + sync: { + configure: (config: SyncConfig) => Promise; + start: (direction?: 'push' | 'pull' | 'bidirectional') => Promise; + getStatus: () => Promise<'idle' | 'syncing' | 'error'>; + isConfigured: () => Promise; + getPendingCount: () => Promise<{ posts: number; media: number }>; + getLog: (limit?: number) => Promise; + stopAutoSync: () => Promise; + }; + tasks: { + getAll: () => Promise; + getRunning: () => Promise; + cancel: (taskId: string) => Promise; + clearCompleted: () => Promise; + }; + app: { + getDataPaths: () => Promise<{ database: string; posts: string; media: string }>; + openFolder: (folderPath: string) => Promise; + showItemInFolder: (itemPath: string) => Promise; + selectFolder: (title?: string) => Promise; + getDefaultProjectPath: (projectId: string) => Promise; + readProjectMetadata: (folderPath: string) => Promise<{ name?: string; description?: string; mainLanguage?: string } | null>; + }; + meta: { + getTags: () => Promise; + getCategories: () => Promise; + addTag: (tag: string) => Promise; + removeTag: (tag: string) => Promise; + addCategory: (category: string) => Promise; + removeCategory: (category: string) => Promise; + syncOnStartup: () => Promise<{ tags: string[]; categories: string[]; projectMetadata: ProjectMetadata | null }>; + getProjectMetadata: () => Promise; + setProjectMetadata: (metadata: { name: string; description?: string }) => Promise; + updateProjectMetadata: (updates: { name?: string; description?: string; dataPath?: string; mainLanguage?: string }) => Promise; + }; + tags: { + getAll: () => Promise; + getWithCounts: () => Promise; + get: (id: string) => Promise; + getByName: (name: string) => Promise; + create: (data: { name: string; color?: string }) => Promise; + update: (id: string, data: { name?: string; color?: string | null }) => Promise; + delete: (id: string) => Promise; + merge: (sourceTagIds: string[], targetTagId: string) => Promise; + rename: (id: string, newName: string) => Promise; + getPostsWithTag: (tagId: string) => Promise; + syncFromPosts: () => Promise; + }; + import: { + selectAndAnalyze: (uploadsFolder?: string) => Promise; + analyzeFile: (filePath: string, uploadsFolder?: string) => Promise; + selectUploadsFolder: () => Promise; + execute: (reportJson: string, uploadsFolder?: string) => Promise; + onProgress: (callback: (data: { step: string; detail?: string }) => void) => () => void; + onExecutionProgress: (callback: (data: ImportExecutionProgress) => void) => () => void; + onComplete: (callback: (data: ImportCompleteResult) => void) => () => void; + }; + importDefinitions: { + create: (name?: string) => Promise; + get: (id: string) => Promise; + getAll: () => Promise; + update: (id: string, updates: Partial>) => Promise; + delete: (id: string) => Promise; + onNameUpdated: (callback: (data: { definitionId: string; name: string }) => void) => () => void; + }; + metadataDiff: { + getStats: () => Promise<{ + totalPosts: number; + publishedPosts: number; + draftPosts: number; + totalMedia: number; + }>; + scan: () => Promise<{ + totalScanned: number; + postsWithDifferences: number; + differences: Array<{ + postId: string; + title: string; + slug: string; + filePath?: string; + hasDifferences: boolean; + differences: Record; + }>; + groups: Array<{ + field: string; + label: string; + posts: Array<{ + postId: string; + title: string; + slug: string; + dbValue: unknown; + fileValue: unknown; + }>; + }>; + }>; + syncDbToFile: (postIds: string[], groupLabel: string) => Promise<{ success: number; failed: number }>; + syncFileToDb: (postIds: string[], field: string, groupLabel: string) => Promise<{ success: number; failed: number }>; + }; + chat: { + // API Key Management + checkReady: () => Promise; + validateApiKey: (apiKey: string) => Promise<{ isValid: boolean; models: ChatModel[] }>; + setApiKey: (apiKey: string) => Promise<{ success: boolean; error?: string }>; + getApiKey: () => Promise; + + // Settings + getAvailableModels: () => Promise<{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }>; + setDefaultModel: (modelId: string) => Promise<{ success: boolean; error?: string }>; + getSystemPrompt: () => Promise<{ success: boolean; prompt?: string; error?: string }>; + setSystemPrompt: (prompt: string) => Promise<{ success: boolean; error?: string }>; + + // Conversations + getConversations: () => Promise; + createConversation: (title?: string, model?: string) => Promise; + getConversation: (id: string) => Promise; + updateConversation: (id: string, updates: { title?: string; model?: string }) => Promise; + deleteConversation: (id: string) => Promise; + + // Messaging + sendMessage: (conversationId: string, message: string) => Promise<{ success: boolean; message?: string; error?: string }>; + abortMessage: (conversationId: string) => Promise; + getHistory: (conversationId: string) => Promise; + clearMessages: (conversationId: string) => Promise; + setConversationModel: (conversationId: string, modelId: string) => Promise; + + // Taxonomy Analysis + analyzeTaxonomy: (categories: Array<{ name: string; slug: string; existsInProject: boolean }>, tags: Array<{ name: string; slug: string; existsInProject: boolean }>, modelId: string) => Promise<{ success: boolean; categoryMappings?: Record; tagMappings?: Record; error?: string }>; + + // Media Analysis + analyzeMediaImage: (mediaId: string, language?: string) => Promise<{ success: boolean; title?: string; alt?: string; caption?: string; error?: string }>; + + // Event listeners for streaming/progress + onStreamDelta: (callback: (data: ChatStreamDelta) => void) => () => void; + onToolCall: (callback: (data: ChatToolCall) => void) => () => void; + onToolResult: (callback: (data: ChatToolResult) => void) => () => void; + onTitleUpdated: (callback: (data: ChatTitleUpdate) => void) => () => void; + }; + on: (channel: string, callback: (...args: unknown[]) => void) => () => void; + once: (channel: string, callback: (...args: unknown[]) => void) => void; +} + diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index 122a7f9..613fb7b 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -1,455 +1,6 @@ -// Type definitions for the Electron API exposed via preload +export * from '../../main/shared/electronApi'; -export interface ImportExecuteResult { - taskId: string; - totalItems: number; -} - -export interface ImportExecutionProgress { - taskId: string; - phase: string; - current: number; - total: number; - detail?: string; - eta?: number; -} - -export interface ImportCompleteResult { - taskId: string; - success: boolean; - posts: { imported: number; skipped: number; errors: number }; - media: { imported: number; skipped: number; errors: number }; - pages: { imported: number; skipped: number; errors: number }; - tags: { created: number; skipped: number }; -} - -export interface ImportDefinitionData { - id: string; - projectId: string; - name: string; - wxrFilePath: string | null; - uploadsFolderPath: string | null; - lastAnalysisResult: unknown | null; - createdAt: string; - updatedAt: string; -} - -export interface ProjectMetadata { - name: string; - description?: string; - dataPath?: string; - mainLanguage?: string; -} - -export interface ProjectData { - id: string; - name: string; - slug: string; - description?: string; - dataPath?: string; - isActive: boolean; - createdAt: string; - updatedAt: string; -} - -export interface PostData { - id: string; - projectId: string; - title: string; - slug: string; - excerpt?: string; - content: string; - status: 'draft' | 'published' | 'archived'; - author?: string; - createdAt: string; - updatedAt: string; - publishedAt?: string; - tags: string[]; - categories: string[]; -} - -export interface PostFilter { - status?: 'draft' | 'published' | 'archived'; - tags?: string[]; - categories?: string[]; - year?: number; - month?: number; - from?: string; - to?: string; -} - -export interface SearchResult { - id: string; - title: string; - slug: string; - excerpt?: string; -} - -export interface MediaData { - id: string; - projectId: string; - filename: string; - originalName: string; - mimeType: string; - size: number; - width?: number; - height?: number; - title?: string; - alt?: string; - caption?: string; - author?: string; - createdAt: string; - updatedAt: string; - tags: string[]; -} - -export interface MediaFilter { - tags?: string[]; - year?: number; - month?: number; -} - -export interface MediaSearchResult { - id: string; - originalName: string; - title?: string; - mimeType: string; - createdAt: string; -} - -export interface TaskProgress { - taskId: string; - status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; - progress: number; - message: string; - startTime: string; - endTime?: string; - error?: string; -} - -export interface SyncConfig { - autoSync: boolean; - syncInterval: number; -} - -export interface SyncResult { - success: boolean; - pushed: number; - pulled: number; - conflicts: number; - errors: string[]; -} - -export interface PaginatedPostsResult { - items: PostData[]; - hasMore: boolean; - total: number; -} - -export interface DashboardStats { - totalPosts: number; - draftCount: number; - publishedCount: number; - archivedCount: number; -} - -export interface TagCount { - tag: string; - count: number; -} - -export interface CategoryCount { - category: string; - count: number; -} - -export interface TagData { - id: string; - projectId: string; - name: string; - color?: string; - createdAt: string; - updatedAt: string; -} - -export interface TagWithCount { - name: string; - color: string | null; - count: number; -} - -export interface DeleteTagResult { - success: boolean; - postsUpdated: number; -} - -export interface MergeTagsResult { - success: boolean; - postsUpdated: number; - tagsDeleted: number; - targetTag: string; -} - -export interface RenameTagResult { - success: boolean; - postsUpdated: number; - oldName: string; - newName: string; -} - -export interface SyncTagsResult { - discovered: number; - added: string[]; -} - -// Post-Media Link types -export interface MediaLinkData { - id: string; - projectId: string; - postId: string; - mediaId: string; - sortOrder: number; - createdAt: string; -} - -// Chat/AI types -export interface ChatConversation { - id: string; - title: string; - model?: string; - createdAt: string; - updatedAt: string; -} - -export interface ChatMessage { - id: string; - conversationId: string; - role: 'user' | 'assistant' | 'system' | 'tool'; - content: string; - toolCallId?: string; - toolCalls?: string; - createdAt: string; -} - -export interface ChatModel { - id: string; - name: string; - provider?: string; -} - -export interface ChatReadyStatus { - ready: boolean; - error?: string; - backend?: string; -} - -export interface ChatApiKeyStatus { - hasKey: boolean; - maskedKey: string; -} - -export interface ChatStreamDelta { - conversationId: string; - delta: string; -} - -export interface ChatToolCall { - conversationId: string; - toolCall: { - name: string; - arguments: Record; - }; -} - -export interface ChatToolResult { - conversationId: string; - result: unknown; -} - -export interface ChatTitleUpdate { - conversationId: string; - title: string; -} - -export interface ElectronAPI { - projects: { - create: (data: { name: string; description?: string; slug?: string; dataPath?: string }) => Promise; - update: (id: string, data: Partial) => Promise; - delete: (id: string) => Promise; - deleteWithData: (id: string) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getActive: () => Promise; - setActive: (id: string) => Promise; - }; - posts: { - create: (data: Partial) => Promise; - update: (id: string, data: Partial) => Promise; - delete: (id: string) => Promise; - get: (id: string) => Promise; - getAll: (options?: { limit?: number; offset?: number }) => Promise; - getByStatus: (status: string) => Promise; - publish: (id: string) => Promise; - discard: (id: string) => Promise; - hasPublishedVersion: (id: string) => Promise; - rebuildFromFiles: () => Promise; - reindexText: () => Promise; - search: (query: string) => Promise; - filter: (filter: PostFilter) => Promise; - getTags: () => Promise; - getCategories: () => Promise; - getByYearMonth: () => Promise<{ year: number; month: number; count: number }[]>; - getDashboardStats: () => Promise; - getTagsWithCounts: () => Promise; - getCategoriesWithCounts: () => Promise; - getLinksTo: (id: string) => Promise; - getLinkedBy: (id: string) => Promise; - rebuildLinks: () => Promise; - isSlugAvailable: (slug: string, excludePostId?: string) => Promise; - generateUniqueSlug: (title: string, excludePostId?: string) => Promise; - }; - media: { - import: (sourcePath: string, metadata?: Partial) => Promise; - importDialog: () => Promise; - update: (id: string, data: Partial) => Promise; - replaceFile: (id: string, newSourcePath: string) => Promise; - replaceFileDialog: (id: string) => Promise; - delete: (id: string) => Promise; - get: (id: string) => Promise; - getUrl: (id: string) => Promise; - getFilePath: (id: string) => Promise; - getAll: () => Promise; - rebuildFromFiles: () => Promise; - reindexText: () => Promise; - getThumbnail: (id: string, size?: 'small' | 'medium' | 'large') => Promise; - regenerateThumbnails: (id: string) => Promise | null>; - regenerateMissingThumbnails: () => Promise<{ processed: number; generated: number; failed: number }>; - filter: (filter: MediaFilter) => Promise; - search: (query: string) => Promise; - getByYearMonth: () => Promise<{ year: number; month: number; count: number }[]>; - getTags: () => Promise; - getTagsWithCounts: () => Promise; - }; - postMedia: { - link: (postId: string, mediaId: string) => Promise; - unlink: (postId: string, mediaId: string) => Promise; - linkMany: (postId: string, mediaIds: string[]) => Promise<{ linked: string[]; skipped: string[] }>; - unlinkMany: (postId: string, mediaIds: string[]) => Promise<{ unlinked: string[] }>; - getForPost: (postId: string) => Promise; - getForMedia: (mediaId: string) => Promise; - getMediaDataForPost: (postId: string) => Promise>; - reorder: (postId: string, mediaIds: string[]) => Promise; - isLinked: (postId: string, mediaId: string) => Promise; - import: (postId: string, filePath: string) => Promise; - rebuild: () => Promise; - }; - sync: { - configure: (config: SyncConfig) => Promise; - start: (direction?: 'push' | 'pull' | 'bidirectional') => Promise; - getStatus: () => Promise<'idle' | 'syncing' | 'error'>; - isConfigured: () => Promise; - getPendingCount: () => Promise<{ posts: number; media: number }>; - getLog: (limit?: number) => Promise; - stopAutoSync: () => Promise; - }; - tasks: { - getAll: () => Promise; - getRunning: () => Promise; - cancel: (taskId: string) => Promise; - clearCompleted: () => Promise; - }; - app: { - getDataPaths: () => Promise<{ database: string; posts: string; media: string }>; - openFolder: (folderPath: string) => Promise; - showItemInFolder: (itemPath: string) => Promise; - selectFolder: (title?: string) => Promise; - getDefaultProjectPath: (projectId: string) => Promise; - readProjectMetadata: (folderPath: string) => Promise<{ name?: string; description?: string; mainLanguage?: string } | null>; - }; - meta: { - getTags: () => Promise; - getCategories: () => Promise; - addTag: (tag: string) => Promise; - removeTag: (tag: string) => Promise; - addCategory: (category: string) => Promise; - removeCategory: (category: string) => Promise; - syncOnStartup: () => Promise<{ tags: string[]; categories: string[]; projectMetadata: ProjectMetadata | null }>; - getProjectMetadata: () => Promise; - setProjectMetadata: (metadata: { name: string; description?: string }) => Promise; - updateProjectMetadata: (updates: { name?: string; description?: string; dataPath?: string; mainLanguage?: string }) => Promise; - }; - tags: { - getAll: () => Promise; - getWithCounts: () => Promise; - get: (id: string) => Promise; - getByName: (name: string) => Promise; - create: (data: { name: string; color?: string }) => Promise; - update: (id: string, data: { name?: string; color?: string | null }) => Promise; - delete: (id: string) => Promise; - merge: (sourceTagIds: string[], targetTagId: string) => Promise; - rename: (id: string, newName: string) => Promise; - getPostsWithTag: (tagId: string) => Promise; - syncFromPosts: () => Promise; - }; - import: { - selectAndAnalyze: (uploadsFolder?: string) => Promise; - analyzeFile: (filePath: string, uploadsFolder?: string) => Promise; - selectUploadsFolder: () => Promise; - execute: (reportJson: string, uploadsFolder?: string) => Promise; - onProgress: (callback: (data: { step: string; detail?: string }) => void) => () => void; - onExecutionProgress: (callback: (data: ImportExecutionProgress) => void) => () => void; - onComplete: (callback: (data: ImportCompleteResult) => void) => () => void; - }; - importDefinitions: { - create: (name?: string) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - update: (id: string, updates: Partial>) => Promise; - delete: (id: string) => Promise; - onNameUpdated: (callback: (data: { definitionId: string; name: string }) => void) => () => void; - }; - chat: { - // API Key Management - checkReady: () => Promise; - validateApiKey: (apiKey: string) => Promise<{ isValid: boolean; models: ChatModel[] }>; - setApiKey: (apiKey: string) => Promise<{ success: boolean; error?: string }>; - getApiKey: () => Promise; - - // Settings - getAvailableModels: () => Promise<{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }>; - setDefaultModel: (modelId: string) => Promise<{ success: boolean; error?: string }>; - getSystemPrompt: () => Promise<{ success: boolean; prompt?: string; error?: string }>; - setSystemPrompt: (prompt: string) => Promise<{ success: boolean; error?: string }>; - - // Conversations - getConversations: () => Promise; - createConversation: (title?: string, model?: string) => Promise; - getConversation: (id: string) => Promise; - updateConversation: (id: string, updates: { title?: string; model?: string }) => Promise; - deleteConversation: (id: string) => Promise; - - // Messaging - sendMessage: (conversationId: string, message: string) => Promise<{ success: boolean; message?: string; error?: string }>; - abortMessage: (conversationId: string) => Promise; - getHistory: (conversationId: string) => Promise; - clearMessages: (conversationId: string) => Promise; - setConversationModel: (conversationId: string, modelId: string) => Promise; - - // Taxonomy Analysis - analyzeTaxonomy: (categories: Array<{ name: string; slug: string; existsInProject: boolean }>, tags: Array<{ name: string; slug: string; existsInProject: boolean }>, modelId: string) => Promise<{ success: boolean; categoryMappings?: Record; tagMappings?: Record; error?: string }>; - - // Media Analysis - analyzeMediaImage: (mediaId: string, language?: string) => Promise<{ success: boolean; title?: string; alt?: string; caption?: string; error?: string }>; - - // Event listeners for streaming/progress - onStreamDelta: (callback: (data: ChatStreamDelta) => void) => () => void; - onToolCall: (callback: (data: ChatToolCall) => void) => () => void; - onToolResult: (callback: (data: ChatToolResult) => void) => () => void; - onTitleUpdated: (callback: (data: ChatTitleUpdate) => void) => () => void; - }; - on: (channel: string, callback: (...args: unknown[]) => void) => () => void; - once: (channel: string, callback: (...args: unknown[]) => void) => void; -} +import type { ElectronAPI } from '../../main/shared/electronApi'; declare global { interface Window { diff --git a/tests/engine/PreloadContract.test.ts b/tests/engine/PreloadContract.test.ts new file mode 100644 index 0000000..b5da9fb --- /dev/null +++ b/tests/engine/PreloadContract.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const exposeInMainWorld = vi.fn(); + +vi.mock('electron', () => ({ + contextBridge: { + exposeInMainWorld, + }, + ipcRenderer: { + invoke: vi.fn(), + on: vi.fn(), + removeListener: vi.fn(), + once: vi.fn(), + }, +})); + +describe('preload contract', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('exports the electronAPI contract object and exposes it via contextBridge', async () => { + const preloadModule = await import('../../src/main/preload'); + + expect(preloadModule).toHaveProperty('electronAPI'); + expect(exposeInMainWorld).toHaveBeenCalledWith('electronAPI', (preloadModule as { electronAPI: unknown }).electronAPI); + }); +}); diff --git a/tests/renderer/types/electronApiContract.test.ts b/tests/renderer/types/electronApiContract.test.ts new file mode 100644 index 0000000..ccc3d2c --- /dev/null +++ b/tests/renderer/types/electronApiContract.test.ts @@ -0,0 +1,9 @@ +import { describe, it, expectTypeOf } from 'vitest'; +import type { ElectronAPI as SharedElectronAPI } from '../../../src/main/shared/electronApi'; +import type { ElectronAPI as RendererElectronAPI } from '../../../src/renderer/types/electron'; + +describe('Electron API type contract', () => { + it('keeps renderer and shared ElectronAPI contracts in sync', () => { + expectTypeOf().toEqualTypeOf(); + }); +});