diff --git a/API.md b/API.md index a8e30ca..2a08d67 100644 --- a/API.md +++ b/API.md @@ -1,6 +1,6 @@ # API Documentation -Contract version: 1.6.0 +Contract version: 1.7.0 This reference documents all Python runtime API calls available through `bds_api` in embedded Pyodide. @@ -25,8 +25,8 @@ project = await bds.meta.get_project_metadata() - [app](#app) - [meta](#meta) - [tags](#tags) -- [chat](#chat) - [sync](#sync) +- [publish](#publish) - [Data Structures](#data-structures) ## projects @@ -3067,674 +3067,23 @@ result = await bds.tags.sync_from_posts() [↑ Back to Table of contents](#table-of-contents) -## chat - -**Module APIs** - -- [chat.checkReady](#chatcheckready) -- [chat.validateApiKey](#chatvalidateapikey) -- [chat.setApiKey](#chatsetapikey) -- [chat.getApiKey](#chatgetapikey) -- [chat.getAvailableModels](#chatgetavailablemodels) -- [chat.setDefaultModel](#chatsetdefaultmodel) -- [chat.getSystemPrompt](#chatgetsystemprompt) -- [chat.setSystemPrompt](#chatsetsystemprompt) -- [chat.getConversations](#chatgetconversations) -- [chat.createConversation](#chatcreateconversation) -- [chat.getConversation](#chatgetconversation) -- [chat.updateConversation](#chatupdateconversation) -- [chat.deleteConversation](#chatdeleteconversation) -- [chat.sendMessage](#chatsendmessage) -- [chat.abortMessage](#chatabortmessage) -- [chat.getHistory](#chatgethistory) -- [chat.clearMessages](#chatclearmessages) -- [chat.setConversationModel](#chatsetconversationmodel) -- [chat.analyzeTaxonomy](#chatanalyzetaxonomy) -- [chat.analyzeMediaImage](#chatanalyzemediaimage) - -### chat.checkReady - -Check chat backend readiness. - -**Parameters** - -- None - -**Response specification** - -- Return type: `ChatReadyStatus` -- Data structures: `ChatReadyStatus` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.check_ready() -``` - -**Example response** - -```python -{ - 'ready': False, - 'error': 'value', - 'backend': 'value' -} -``` - -### chat.validateApiKey - -Validate chat API key and list available models. - -**Parameters** - -- apiKey (str, required) - -**Response specification** - -- Return type: `{ isValid: boolean; models: ChatModel[] }` -- Data structures: `ChatModel` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.validate_api_key(api_key='api_key') -``` - -**Example response** - -```python -[ -{ - 'id': 'value', - 'name': 'value', - 'provider': 'value' -} -] -``` - -### chat.setApiKey - -Store chat API key. - -**Parameters** - -- apiKey (str, required) - -**Response specification** - -- Return type: `{ success: boolean; error?: string }` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.set_api_key(api_key='api_key') -``` - -**Example response** - -```python -{} -``` - -### chat.getApiKey - -Get stored chat API key status. - -**Parameters** - -- None - -**Response specification** - -- Return type: `ChatApiKeyStatus` -- Data structures: `ChatApiKeyStatus` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.get_api_key() -``` - -**Example response** - -```python -{ - 'hasKey': False, - 'maskedKey': 'value' -} -``` - -### chat.getAvailableModels - -Get available chat models and selected default. - -**Parameters** - -- None - -**Response specification** - -- Return type: `{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }` -- Data structures: `ChatModel` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.get_available_models() -``` - -**Example response** - -```python -[ -{ - 'id': 'value', - 'name': 'value', - 'provider': 'value' -} -] -``` - -### chat.setDefaultModel - -Set default chat model. - -**Parameters** - -- modelId (str, required) - -**Response specification** - -- Return type: `{ success: boolean; error?: string }` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.set_default_model(model_id='model-1') -``` - -**Example response** - -```python -{} -``` - -### chat.getSystemPrompt - -Get configured system prompt. - -**Parameters** - -- None - -**Response specification** - -- Return type: `{ success: boolean; prompt?: string; error?: string }` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.get_system_prompt() -``` - -**Example response** - -```python -{} -``` - -### chat.setSystemPrompt - -Set system prompt. - -**Parameters** - -- prompt (str, required) - -**Response specification** - -- Return type: `{ success: boolean; error?: string }` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.set_system_prompt(prompt='prompt') -``` - -**Example response** - -```python -{} -``` - -### chat.getConversations - -Fetch all chat conversations. - -**Parameters** - -- None - -**Response specification** - -- Return type: `ChatConversation[]` -- Data structures: `ChatConversation` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.get_conversations() -``` - -**Example response** - -```python -[ -{ - 'id': 'value', - 'title': 'value', - 'model': 'value', - 'createdAt': 'value', - 'updatedAt': 'value' -} -] -``` - -### chat.createConversation - -Create a chat conversation. - -**Parameters** - -- title (str, optional) -- model (str, optional) - -**Response specification** - -- Return type: `ChatConversation` -- Data structures: `ChatConversation` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.create_conversation() -``` - -**Example response** - -```python -{ - 'id': 'value', - 'title': 'value', - 'model': 'value', - 'createdAt': 'value', - 'updatedAt': 'value' -} -``` - -### chat.getConversation - -Fetch one chat conversation by id. - -**Parameters** - -- id (str, required) - -**Response specification** - -- Return type: `ChatConversation | null` -- Nullability: Returns `None` when no matching value exists. -- Data structures: `ChatConversation` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.get_conversation(id='id-1') -``` - -**Example response** - -```python -None # or -{ - 'id': 'value', - 'title': 'value', - 'model': 'value', - 'createdAt': 'value', - 'updatedAt': 'value' -} -``` - -### chat.updateConversation - -Update chat conversation metadata. - -**Parameters** - -- id (str, required) -- updates (dict, required) - -**Response specification** - -- Return type: `ChatConversation | null` -- Nullability: Returns `None` when no matching value exists. -- Data structures: `ChatConversation` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.update_conversation(id='id-1', updates={}) -``` - -**Example response** - -```python -None # or -{ - 'id': 'value', - 'title': 'value', - 'model': 'value', - 'createdAt': 'value', - 'updatedAt': 'value' -} -``` - -### chat.deleteConversation - -Delete chat conversation by id. - -**Parameters** - -- id (str, required) - -**Response specification** - -- Return type: `boolean` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.delete_conversation(id='id-1') -``` - -**Example response** - -```python -True -``` - -### chat.sendMessage - -Send message to chat conversation. - -**Parameters** - -- conversationId (str, required) -- message (str, required) -- metadata (dict, optional) - -**Response specification** - -- Return type: `{ success: boolean; message?: string; error?: string }` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.send_message(conversation_id='conversation-1', message='message') -``` - -**Example response** - -```python -{} -``` - -### chat.abortMessage - -Abort active streaming chat response. - -**Parameters** - -- conversationId (str, required) - -**Response specification** - -- Return type: `void` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.abort_message(conversation_id='conversation-1') -``` - -**Example response** - -```python -None -``` - -### chat.getHistory - -Get message history for conversation. - -**Parameters** - -- conversationId (str, required) - -**Response specification** - -- Return type: `ChatMessage[]` -- Data structures: `ChatMessage` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.get_history(conversation_id='conversation-1') -``` - -**Example response** - -```python -[ -{ - 'id': 'value', - 'conversationId': 'value', - 'role': None, - 'content': 'value', - 'toolCallId': 'value', - 'toolCalls': 'value', - 'createdAt': 'value' -} -] -``` - -### chat.clearMessages - -Clear messages for conversation. - -**Parameters** - -- conversationId (str, required) - -**Response specification** - -- Return type: `void` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.clear_messages(conversation_id='conversation-1') -``` - -**Example response** - -```python -None -``` - -### chat.setConversationModel - -Set model for a conversation. - -**Parameters** - -- conversationId (str, required) -- modelId (str, required) - -**Response specification** - -- Return type: `void` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.set_conversation_model(conversation_id='conversation-1', model_id='model-1') -``` - -**Example response** - -```python -None -``` - -### chat.analyzeTaxonomy - -Analyze categories and tags using AI. - -**Parameters** - -- categories (list, required) -- tags (list, required) -- modelId (str, required) - -**Response specification** - -- Return type: `{ success: boolean; categoryMappings?: Record; tagMappings?: Record; error?: string }` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.analyze_taxonomy(categories=[], tags=[], model_id='model-1') -``` - -**Example response** - -```python -{} -``` - -### chat.analyzeMediaImage - -Analyze media image and propose metadata. - -**Parameters** - -- mediaId (str, required) -- language (str, optional) - -**Response specification** - -- Return type: `{ success: boolean; title?: string; alt?: string; caption?: string; error?: string }` - -**Example call** - -```python -from bds_api import bds -result = await bds.chat.analyze_media_image(media_id='media-1') -``` - -**Example response** - -```python -{} -``` - -[↑ Back to Table of contents](#table-of-contents) - ## sync **Module APIs** -- [sync.configure](#syncconfigure) -- [sync.start](#syncstart) +- [sync.checkAvailability](#synccheckavailability) +- [sync.getRepoState](#syncgetrepostate) - [sync.getStatus](#syncgetstatus) -- [sync.isConfigured](#syncisconfigured) -- [sync.getPendingCount](#syncgetpendingcount) -- [sync.getLog](#syncgetlog) -- [sync.stopAutoSync](#syncstopautosync) +- [sync.getHistory](#syncgethistory) +- [sync.getRemoteState](#syncgetremotestate) +- [sync.fetch](#syncfetch) +- [sync.pull](#syncpull) +- [sync.push](#syncpush) +- [sync.commitAll](#synccommitall) -### sync.configure +### sync.checkAvailability -Configure sync. - -**Parameters** - -- config (dict, required) - -**Response specification** - -- Return type: `void` - -**Example call** - -```python -from bds_api import bds -result = await bds.sync.configure(config={}) -``` - -**Example response** - -```python -None -``` - -### sync.start - -Start sync operation. - -**Parameters** - -- direction (str, optional) - -**Response specification** - -- Return type: `SyncResult` - -**Example call** - -```python -from bds_api import bds -result = await bds.sync.start() -``` - -**Example response** - -```python -{} -``` - -### sync.getStatus - -Get sync status. +Check if git is available. **Parameters** @@ -3742,7 +3091,68 @@ Get sync status. **Response specification** -- Return type: `'idle' | 'syncing' | 'error'` +- Return type: `GitAvailability` +- Data structures: `GitAvailability` + +**Example call** + +```python +from bds_api import bds +result = await bds.sync.check_availability() +``` + +**Example response** + +```python +{ + 'gitFound': False, + 'version': 'value' +} +``` + +### sync.getRepoState + +Get repository state for active project. + +**Parameters** + +- None + +**Response specification** + +- Return type: `RepoState` +- Data structures: `RepoState` + +**Example call** + +```python +from bds_api import bds +result = await bds.sync.get_repo_state() +``` + +**Example response** + +```python +{ + 'isRepo': False, + 'rootPath': 'value', + 'currentBranch': 'value', + 'hasRemote': False +} +``` + +### sync.getStatus + +Get working tree status for active project. + +**Parameters** + +- None + +**Response specification** + +- Return type: `GitStatusDto` +- Data structures: `GitStatusDto` **Example call** @@ -3754,62 +3164,15 @@ result = await bds.sync.get_status() **Example response** ```python -{} +{ + 'files': 'value', + 'counts': 0 +} ``` -### sync.isConfigured +### sync.getHistory -Check if sync is configured. - -**Parameters** - -- None - -**Response specification** - -- Return type: `boolean` - -**Example call** - -```python -from bds_api import bds -result = await bds.sync.is_configured() -``` - -**Example response** - -```python -True -``` - -### sync.getPendingCount - -Get pending sync item count. - -**Parameters** - -- None - -**Response specification** - -- Return type: `{ posts: number; media: number }` - -**Example call** - -```python -from bds_api import bds -result = await bds.sync.get_pending_count() -``` - -**Example response** - -```python -{} -``` - -### sync.getLog - -Get sync log. +Get commit history for active project. **Parameters** @@ -3817,13 +3180,13 @@ Get sync log. **Response specification** -- Return type: `unknown[]` +- Return type: `GitHistoryEntry[]` **Example call** ```python from bds_api import bds -result = await bds.sync.get_log() +result = await bds.sync.get_history() ``` **Example response** @@ -3832,9 +3195,9 @@ result = await bds.sync.get_log() [] ``` -### sync.stopAutoSync +### sync.getRemoteState -Stop automatic sync. +Get remote tracking state for active project. **Parameters** @@ -3842,19 +3205,189 @@ Stop automatic sync. **Response specification** -- Return type: `void` +- Return type: `GitRemoteStateDto` +- Data structures: `GitRemoteStateDto` **Example call** ```python from bds_api import bds -result = await bds.sync.stop_auto_sync() +result = await bds.sync.get_remote_state() ``` **Example response** ```python -None +{ + 'localBranch': 'value', + 'upstreamBranch': 'value', + 'hasUpstream': False, + 'ahead': 0, + 'behind': 0 +} +``` + +### sync.fetch + +Fetch from remote for active project. + +**Parameters** + +- None + +**Response specification** + +- Return type: `GitActionResult` +- Data structures: `GitActionResult` + +**Example call** + +```python +from bds_api import bds +result = await bds.sync.fetch() +``` + +**Example response** + +```python +{ + 'success': False, + 'code': 'value', + 'error': 'value', + 'guidance': 'value' +} +``` + +### sync.pull + +Pull from remote for active project. + +**Parameters** + +- None + +**Response specification** + +- Return type: `GitActionResult` +- Data structures: `GitActionResult` + +**Example call** + +```python +from bds_api import bds +result = await bds.sync.pull() +``` + +**Example response** + +```python +{ + 'success': False, + 'code': 'value', + 'error': 'value', + 'guidance': 'value' +} +``` + +### sync.push + +Push to remote for active project. + +**Parameters** + +- None + +**Response specification** + +- Return type: `GitActionResult` +- Data structures: `GitActionResult` + +**Example call** + +```python +from bds_api import bds +result = await bds.sync.push() +``` + +**Example response** + +```python +{ + 'success': False, + 'code': 'value', + 'error': 'value', + 'guidance': 'value' +} +``` + +### sync.commitAll + +Stage all changes and commit for active project. + +**Parameters** + +- message (str, required) + +**Response specification** + +- Return type: `GitActionResult` +- Data structures: `GitActionResult` + +**Example call** + +```python +from bds_api import bds +result = await bds.sync.commit_all(message='message') +``` + +**Example response** + +```python +{ + 'success': False, + 'code': 'value', + 'error': 'value', + 'guidance': 'value' +} +``` + +[↑ Back to Table of contents](#table-of-contents) + +## publish + +**Module APIs** + +- [publish.uploadSite](#publishuploadsite) + +### publish.uploadSite + +Upload rendered site to remote server via SSH. + +**Parameters** + +- credentials (dict, required) + +**Response specification** + +- Return type: `PublishSiteResult` +- Data structures: `PublishSiteResult` + +**Example call** + +```python +from bds_api import bds +result = await bds.publish.upload_site(credentials={}) +``` + +**Example response** + +```python +{ + 'htmlFilesUploaded': 0, + 'thumbnailFilesUploaded': 0, + 'mediaFilesUploaded': 0, + 'filesSkipped': 0 +} ``` [↑ Back to Table of contents](#table-of-contents) @@ -3987,71 +3520,81 @@ Extended project metadata from project settings. [↑ Back to Table of contents](#table-of-contents) -### ChatConversation +### GitAvailability -Chat conversation container. +Git installation availability check result. **Fields** -- id (`string`, required): Unique conversation identifier. -- title (`string`, required): Conversation title. -- model (`string`, optional): Optional model id used by this conversation. -- createdAt (`string`, required): Creation timestamp (ISO string). -- updatedAt (`string`, required): Last update timestamp (ISO string). +- gitFound (`boolean`, required): Whether git executable was found. +- version (`string`, optional): Git version string when available. [↑ Back to Table of contents](#table-of-contents) -### ChatMessage +### RepoState -Single message entry in a conversation history. +Repository state for the active project. **Fields** -- id (`string`, required): Unique message identifier. -- conversationId (`string`, required): Owning conversation id. -- role (`'user' | 'assistant' | 'system' | 'tool'`, required): Message author role. -- content (`string`, required): Message text content. -- toolCallId (`string`, optional): Tool call id when associated with tool output. -- toolCalls (`string`, optional): Serialized tool call payload when present. -- createdAt (`string`, required): Creation timestamp (ISO string). +- isRepo (`boolean`, required): Whether the project directory is a git repository. +- rootPath (`string`, optional): Repository root path. +- currentBranch (`string`, optional): Current branch name. +- hasRemote (`boolean`, required): Whether a remote is configured. [↑ Back to Table of contents](#table-of-contents) -### ChatModel +### GitStatusDto -Available chat model descriptor. +Working tree status with file list and counts. **Fields** -- id (`string`, required): Model identifier. -- name (`string`, required): Human-readable model name. -- provider (`string`, optional): Model provider name. +- files (`Array<{ path: string; status: string; previousPath?: string }>`, required): List of changed files with status. +- counts (`{ untracked: number; modified: number; deleted: number; renamed: number; staged: number }`, required): Counts by change type. [↑ Back to Table of contents](#table-of-contents) -### ChatReadyStatus +### GitRemoteStateDto -Chat backend readiness status. +Remote tracking state for the active project branch. **Fields** -- ready (`boolean`, required): Whether chat backend is ready. -- error (`string`, optional): Error description when not ready. -- backend (`string`, optional): Selected backend identifier. +- localBranch (`string | null`, required): Local branch name. +- upstreamBranch (`string | null`, required): Upstream tracking branch name. +- hasUpstream (`boolean`, required): Whether an upstream is configured. +- ahead (`number`, required): Commits ahead of upstream. +- behind (`number`, required): Commits behind upstream. [↑ Back to Table of contents](#table-of-contents) -### ChatApiKeyStatus +### GitActionResult -Stored API key state for chat provider. +Result from a git operation (fetch, pull, push, commit). **Fields** -- hasKey (`boolean`, required): Whether a key is configured. -- maskedKey (`string`, required): Masked key representation for UI display. +- success (`boolean`, required): Whether the operation succeeded. +- code (`string`, optional): Error code when failed ('auth-required', 'conflict', 'network', 'action-failed'). +- error (`string`, optional): Error message when failed. +- guidance (`string[]`, optional): Guidance messages for resolving failures. + +[↑ Back to Table of contents](#table-of-contents) + +### PublishSiteResult + +Aggregate result from uploading the rendered site. + +**Fields** + +- htmlFilesUploaded (`number`, required): Number of HTML files uploaded. +- thumbnailFilesUploaded (`number`, required): Number of thumbnail files uploaded. +- mediaFilesUploaded (`number`, required): Number of media files uploaded. +- filesSkipped (`number`, required): Total files skipped (already up-to-date). [↑ Back to Table of contents](#table-of-contents) --- -Generated from contract at 2026-02-25T00:00:00.000Z. +Generated from contract at 2026-02-27T00:00:00.000Z. diff --git a/PYTHON_SCRIPTING.md b/PYTHON_SCRIPTING.md index 40ef8ee..cbb35c9 100644 --- a/PYTHON_SCRIPTING.md +++ b/PYTHON_SCRIPTING.md @@ -1,6 +1,6 @@ # Python Scripting — Remaining Work (Implementation-First) -Last verified: 26 Feb 2026 +Last verified: 27 Feb 2026 This document is intentionally reduced to **what is still left to implement**. When plan and code differ, code is the source of truth. @@ -85,6 +85,12 @@ These are current realities and should be treated as authoritative unless we exp - **`bds_api` module**: Python macros can call app APIs via the `bds_api` module. In the main-process worker (`PythonMacroWorkerRuntime`), API calls are dispatched via `mainProcessPythonApiInvoker.ts` which routes to engine methods directly. In the renderer worker, API calls go through the existing IPC bridge. The `bds_api` module is auto-generated by `generatePythonApiModuleV1.ts` and installed in both workers at bootstrap. +- **Python API namespaces wired to engines (v1.7.0)**: + - `sync` → `GitApiAdapter` (wraps `GitEngine`, auto-resolves `projectPath` from active project). 9 methods: `checkAvailability`, `getRepoState`, `getStatus`, `getHistory`, `getRemoteState`, `fetch`, `pull`, `push`, `commitAll`. All git write operations (`fetch`, `pull`, `push`, `commitAll`) are available to utility scripts. + - `publish` → `PublishApiAdapter` (wraps `PublishEngine` + `TaskManager`). 1 method: `uploadSite(credentials)` — runs 3 parallel SSH upload tasks (HTML, thumbnails, media) and returns aggregate result. + - `app` → `AppApiAdapter`. 4 methods: `getDataPaths`, `getSystemLanguage`, `getDefaultProjectPath`, `readProjectMetadata`. Read-only app-level utilities useful for scripts that need project paths or locale info. + - `chat` — **intentionally excluded** from the Python API contract. AI/chat features (`sendMessage`, `analyzeTaxonomy`, `analyzeMediaImage`, etc.) are expensive external API calls that require user oversight and interactive streaming UI. The chat namespace remains fully functional in the app UI and IPC layer — it is only excluded from the Python scripting bridge. This can be revisited in a future version if AI-from-Python becomes a supported use case with proper rate limiting, cost controls, and non-interactive execution patterns. + - **Editor macro detection**: The editor macro plugin uses `hasMacro()` to distinguish known macros from unknown ones. `hasMacro()` checks both the JS macro registry and a set of known Python macro slugs fetched via `scripts:getEnabledMacroSlugs` IPC. The slug set is refreshed on startup and whenever scripts change (`BDS_EVENT_SCRIPTS_CHANGED`). - **Precedence**: JS built-in macros (youtube, vimeo, gallery, photo_archive, tag_cloud) always take priority over Python scripts with the same slug. Python macros only activate for names not registered in the JS macro registry. diff --git a/src/main/engine/AppApiAdapter.ts b/src/main/engine/AppApiAdapter.ts new file mode 100644 index 0000000..09ca430 --- /dev/null +++ b/src/main/engine/AppApiAdapter.ts @@ -0,0 +1,56 @@ +import * as path from 'path'; +import * as fsPromises from 'fs/promises'; +import { app } from 'electron'; +import { getProjectEngine } from './ProjectEngine'; +import { getDatabase } from '../database'; + +/** + * Adapter that wraps app-level IPC handler logic for use by the Python API layer. + * Provides safe, read-only app methods without requiring Electron UI facilities. + */ +export class AppApiAdapter { + async getDataPaths(): Promise<{ database: string; posts: string; media: string }> { + const projectEngine = getProjectEngine(); + const activeProject = await projectEngine.getActiveProject(); + const projectId = activeProject?.id || 'default'; + const paths = projectEngine.getProjectPaths(projectId, activeProject?.dataPath); + return { + database: getDatabase().getDataPaths().database, + posts: paths.posts, + media: paths.media, + }; + } + + async getSystemLanguage(): Promise { + return app.getLocale(); + } + + async getDefaultProjectPath(projectId: string): Promise { + return getProjectEngine().getDefaultProjectBaseDir(projectId); + } + + async readProjectMetadata(folderPath: string): Promise<{ name?: string; description?: string; publicUrl?: string; mainLanguage?: string } | null> { + const metaPath = path.join(folderPath, 'meta', 'project.json'); + try { + const content = await fsPromises.readFile(metaPath, 'utf-8'); + const metadata = JSON.parse(content); + return { + name: metadata.name || undefined, + description: metadata.description || undefined, + publicUrl: metadata.publicUrl || undefined, + mainLanguage: metadata.mainLanguage || undefined, + }; + } catch { + return null; + } + } +} + +let instance: AppApiAdapter | null = null; + +export function getAppApiAdapter(): AppApiAdapter { + if (!instance) { + instance = new AppApiAdapter(); + } + return instance; +} diff --git a/src/main/engine/GitApiAdapter.ts b/src/main/engine/GitApiAdapter.ts new file mode 100644 index 0000000..d3c4ea6 --- /dev/null +++ b/src/main/engine/GitApiAdapter.ts @@ -0,0 +1,80 @@ +import { getGitEngine } from './GitEngine'; +import { getProjectEngine } from './ProjectEngine'; +import type { + GitAvailability, + RepoState, + GitStatusDto, + GitHistoryEntry, + GitRemoteStateDto, + GitActionResult, +} from './GitEngine'; + +export type { GitAvailability, RepoState, GitStatusDto, GitHistoryEntry, GitRemoteStateDto, GitActionResult }; + +/** + * Adapter that wraps GitEngine for use by the Python API layer. + * Auto-resolves projectPath from the active project so Python scripts + * don't need to pass it. + */ +export class GitApiAdapter { + private async resolveProjectPath(): Promise { + const project = await getProjectEngine().getActiveProject(); + if (!project?.dataPath) { + throw new Error('No active project with a data path'); + } + return project.dataPath; + } + + async checkAvailability(): Promise { + return getGitEngine().checkAvailability(); + } + + async getRepoState(): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().getRepoState(projectPath); + } + + async getStatus(): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().getStatus(projectPath); + } + + async getHistory(limit?: number): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().getHistory(projectPath, limit); + } + + async getRemoteState(): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().getRemoteState(projectPath); + } + + async fetch(): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().fetch(projectPath); + } + + async pull(): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().pull(projectPath); + } + + async push(): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().push(projectPath); + } + + async commitAll(message: string): Promise { + const projectPath = await this.resolveProjectPath(); + return getGitEngine().commitAll(projectPath, message); + } +} + +let instance: GitApiAdapter | null = null; + +export function getGitApiAdapter(): GitApiAdapter { + if (!instance) { + instance = new GitApiAdapter(); + } + return instance; +} diff --git a/src/main/engine/PublishApiAdapter.ts b/src/main/engine/PublishApiAdapter.ts new file mode 100644 index 0000000..458e8dc --- /dev/null +++ b/src/main/engine/PublishApiAdapter.ts @@ -0,0 +1,73 @@ +import { getProjectEngine } from './ProjectEngine'; +import { getPublishEngine, type PublishCredentials } from './PublishEngine'; +import { taskManager } from './TaskManager'; + +export interface PublishSiteResult { + htmlFilesUploaded: number; + thumbnailFilesUploaded: number; + mediaFilesUploaded: number; + filesSkipped: number; +} + +/** + * Adapter that wraps PublishEngine for use by the Python API layer. + * Mirrors the orchestration logic from publishHandlers.ts: sets project + * context, launches three parallel upload tasks, and returns aggregate results. + */ +export class PublishApiAdapter { + async uploadSite(credentials: PublishCredentials): Promise { + const project = await getProjectEngine().getActiveProject(); + if (!project) { + throw new Error('No active project'); + } + + const publishEngine = getPublishEngine(); + publishEngine.setProjectContext(project.id, project.dataPath!); + + const ts = Date.now(); + const groupId = `publish-${ts}`; + const groupName = 'Site Publishing'; + + const htmlTask = taskManager.runTask({ + id: `publish-html-${ts}`, + name: 'Upload HTML', + groupId, + groupName, + execute: (onProgress) => publishEngine.uploadHtml(credentials, onProgress), + }); + + const thumbsTask = taskManager.runTask({ + id: `publish-thumbnails-${ts}`, + name: 'Upload Thumbnails', + groupId, + groupName, + execute: (onProgress) => publishEngine.uploadThumbnails(credentials, onProgress), + }); + + const mediaTask = taskManager.runTask({ + id: `publish-media-${ts}`, + name: 'Upload Media', + groupId, + groupName, + execute: (onProgress) => publishEngine.uploadMedia(credentials, onProgress), + }); + + const [html, thumbnails, media] = await Promise.all([htmlTask, thumbsTask, mediaTask]); + + return { + htmlFilesUploaded: html.filesUploaded, + thumbnailFilesUploaded: thumbnails.filesUploaded, + mediaFilesUploaded: media.filesUploaded, + filesSkipped: html.filesSkipped + thumbnails.filesSkipped + media.filesSkipped, + }; + } +} + +let instance: PublishApiAdapter | null = null; + +export function getPublishApiAdapter(): PublishApiAdapter { + if (!instance) { + instance = new PublishApiAdapter(); + } + return instance; +} diff --git a/src/main/engine/mainProcessPythonApiInvoker.ts b/src/main/engine/mainProcessPythonApiInvoker.ts index 1d6619b..83e920b 100644 --- a/src/main/engine/mainProcessPythonApiInvoker.ts +++ b/src/main/engine/mainProcessPythonApiInvoker.ts @@ -94,6 +94,18 @@ export const ENGINE_MAP: Record = { const { taskManager } = require('../engine/TaskManager'); return taskManager; }, + sync: () => { + const { getGitApiAdapter } = require('../engine/GitApiAdapter'); + return getGitApiAdapter(); + }, + publish: () => { + const { getPublishApiAdapter } = require('../engine/PublishApiAdapter'); + return getPublishApiAdapter(); + }, + app: () => { + const { getAppApiAdapter } = require('../engine/AppApiAdapter'); + return getAppApiAdapter(); + }, }; // Map API method names to engine method names where they differ @@ -199,10 +211,7 @@ export async function invokeMainProcessPythonApi(method: string, args: Record { + return getGitApiAdapter().checkAvailability(); + }); + + safeHandle('sync:getRepoState', async () => { + return getGitApiAdapter().getRepoState(); + }); + + safeHandle('sync:getStatus', async () => { + return getGitApiAdapter().getStatus(); + }); + + safeHandle('sync:getHistory', async (_, limit?: number) => { + return getGitApiAdapter().getHistory(limit); + }); + + safeHandle('sync:getRemoteState', async () => { + return getGitApiAdapter().getRemoteState(); + }); + + safeHandle('sync:fetch', async () => { + return getGitApiAdapter().fetch(); + }); + + safeHandle('sync:pull', async () => { + return getGitApiAdapter().pull(); + }); + + safeHandle('sync:push', async () => { + return getGitApiAdapter().push(); + }); + + safeHandle('sync:commitAll', async (_, message: string) => { + return getGitApiAdapter().commitAll(message); + }); + // ============ App Handlers ============ safeHandle('app:getDataPaths', async () => { diff --git a/src/main/preload.ts b/src/main/preload.ts index f2e04c2..7c5f8ae 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -127,15 +127,17 @@ export const electronAPI: ElectronAPI = { rebuild: () => ipcRenderer.invoke('postMedia:rebuild'), }, - // Sync + // Sync (git operations via GitApiAdapter) sync: { - configure: (config: unknown) => ipcRenderer.invoke('sync:configure', config), - start: (direction?: 'push' | 'pull' | 'bidirectional') => ipcRenderer.invoke('sync:start', direction), + checkAvailability: () => ipcRenderer.invoke('sync:checkAvailability'), + getRepoState: () => ipcRenderer.invoke('sync:getRepoState'), 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'), + getHistory: (limit?: number) => ipcRenderer.invoke('sync:getHistory', limit), + getRemoteState: () => ipcRenderer.invoke('sync:getRemoteState'), + fetch: () => ipcRenderer.invoke('sync:fetch'), + pull: () => ipcRenderer.invoke('sync:pull'), + push: () => ipcRenderer.invoke('sync:push'), + commitAll: (message: string) => ipcRenderer.invoke('sync:commitAll', message), }, // Tasks diff --git a/src/main/shared/electronApi.ts b/src/main/shared/electronApi.ts index da2c850..f9a01e6 100644 --- a/src/main/shared/electronApi.ts +++ b/src/main/shared/electronApi.ts @@ -164,18 +164,6 @@ export interface TaskProgress { groupName?: 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[]; @@ -607,13 +595,15 @@ export interface ElectronAPI { 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; + checkAvailability: () => Promise<{ gitFound: boolean; version?: string }>; + getRepoState: () => Promise<{ isRepo: boolean; rootPath?: string; currentBranch?: string; hasRemote: boolean }>; + getStatus: () => Promise<{ files: Array<{ path: string; status: string; previousPath?: string }>; counts: { untracked: number; modified: number; deleted: number; renamed: number; staged: number } }>; + getHistory: (limit?: number) => Promise>; + getRemoteState: () => Promise<{ localBranch: string | null; upstreamBranch: string | null; hasUpstream: boolean; ahead: number; behind: number }>; + fetch: () => Promise<{ success: boolean; code?: string; error?: string; guidance?: string[] }>; + pull: () => Promise<{ success: boolean; code?: string; error?: string; guidance?: string[] }>; + push: () => Promise<{ success: boolean; code?: string; error?: string; guidance?: string[] }>; + commitAll: (message: string) => Promise<{ success: boolean; code?: string; error?: string; guidance?: string[] }>; }; tasks: { getAll: () => Promise; diff --git a/src/main/shared/pythonApiContractV1.ts b/src/main/shared/pythonApiContractV1.ts index 1ee90b2..3efa136 100644 --- a/src/main/shared/pythonApiContractV1.ts +++ b/src/main/shared/pythonApiContractV1.ts @@ -51,7 +51,6 @@ const optionalNumber = (name: string): PythonApiParamContractV1 => ({ name, type const requiredObject = (name: string): PythonApiParamContractV1 => ({ name, type: 'object', required: true }); const optionalObject = (name: string): PythonApiParamContractV1 => ({ name, type: 'object', required: false }); const requiredArray = (name: string): PythonApiParamContractV1 => ({ name, type: 'array', required: true }); -const requiredAny = (name: string): PythonApiParamContractV1 => ({ name, type: 'any', required: true }); const requiredStringOrNull = (name: string): PythonApiParamContractV1 => ({ name, type: 'stringOrNull', required: true }); function method( @@ -172,34 +171,23 @@ const METHODS_V1: PythonApiMethodContractV1[] = [ method('tags.getPostsWithTag', 'Get posts using a tag.', [requiredString('tagId')], 'string[]'), method('tags.syncFromPosts', 'Sync tag index from posts.', [], 'SyncTagsResult'), - method('chat.checkReady', 'Check chat backend readiness.', [], 'ChatReadyStatus'), - method('chat.validateApiKey', 'Validate chat API key and list available models.', [requiredString('apiKey')], '{ isValid: boolean; models: ChatModel[] }'), - method('chat.setApiKey', 'Store chat API key.', [requiredString('apiKey')], '{ success: boolean; error?: string }'), - method('chat.getApiKey', 'Get stored chat API key status.', [], 'ChatApiKeyStatus'), - method('chat.getAvailableModels', 'Get available chat models and selected default.', [], '{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }'), - method('chat.setDefaultModel', 'Set default chat model.', [requiredString('modelId')], '{ success: boolean; error?: string }'), - method('chat.getSystemPrompt', 'Get configured system prompt.', [], '{ success: boolean; prompt?: string; error?: string }'), - method('chat.setSystemPrompt', 'Set system prompt.', [requiredString('prompt')], '{ success: boolean; error?: string }'), - method('chat.getConversations', 'Fetch all chat conversations.', [], 'ChatConversation[]'), - method('chat.createConversation', 'Create a chat conversation.', [optionalString('title'), optionalString('model')], 'ChatConversation'), - method('chat.getConversation', 'Fetch one chat conversation by id.', [requiredString('id')], 'ChatConversation | null'), - method('chat.updateConversation', 'Update chat conversation metadata.', [requiredString('id'), requiredObject('updates')], 'ChatConversation | null'), - method('chat.deleteConversation', 'Delete chat conversation by id.', [requiredString('id')], 'boolean'), - method('chat.sendMessage', 'Send message to chat conversation.', [requiredString('conversationId'), requiredString('message'), optionalObject('metadata')], '{ success: boolean; message?: string; error?: string }'), - method('chat.abortMessage', 'Abort active streaming chat response.', [requiredString('conversationId')], 'void'), - method('chat.getHistory', 'Get message history for conversation.', [requiredString('conversationId')], 'ChatMessage[]'), - method('chat.clearMessages', 'Clear messages for conversation.', [requiredString('conversationId')], 'void'), - method('chat.setConversationModel', 'Set model for a conversation.', [requiredString('conversationId'), requiredString('modelId')], 'void'), - method('chat.analyzeTaxonomy', 'Analyze categories and tags using AI.', [requiredArray('categories'), requiredArray('tags'), requiredString('modelId')], '{ success: boolean; categoryMappings?: Record; tagMappings?: Record; error?: string }'), - method('chat.analyzeMediaImage', 'Analyze media image and propose metadata.', [requiredString('mediaId'), optionalString('language')], '{ success: boolean; title?: string; alt?: string; caption?: string; error?: string }'), + // NOTE: chat namespace intentionally excluded from Python API. + // AI/chat features (sendMessage, analyzeTaxonomy, analyzeMediaImage, etc.) are + // expensive external API calls that require user oversight and interactive streaming. + // This namespace can be re-added in a future version if AI-from-Python becomes a + // supported use case with proper rate limiting and cost controls. - method('sync.configure', 'Configure sync.', [requiredObject('config')], 'void'), - method('sync.start', 'Start sync operation.', [optionalString('direction')], 'SyncResult'), - method('sync.getStatus', 'Get sync status.', [], "'idle' | 'syncing' | 'error'"), - method('sync.isConfigured', 'Check if sync is configured.', [], 'boolean'), - method('sync.getPendingCount', 'Get pending sync item count.', [], '{ posts: number; media: number }'), - method('sync.getLog', 'Get sync log.', [optionalNumber('limit')], 'unknown[]'), - method('sync.stopAutoSync', 'Stop automatic sync.', [], 'void'), + method('sync.checkAvailability', 'Check if git is available.', [], 'GitAvailability'), + method('sync.getRepoState', 'Get repository state for active project.', [], 'RepoState'), + method('sync.getStatus', 'Get working tree status for active project.', [], 'GitStatusDto'), + method('sync.getHistory', 'Get commit history for active project.', [optionalNumber('limit')], 'GitHistoryEntry[]'), + method('sync.getRemoteState', 'Get remote tracking state for active project.', [], 'GitRemoteStateDto'), + method('sync.fetch', 'Fetch from remote for active project.', [], 'GitActionResult'), + method('sync.pull', 'Pull from remote for active project.', [], 'GitActionResult'), + method('sync.push', 'Push to remote for active project.', [], 'GitActionResult'), + method('sync.commitAll', 'Stage all changes and commit for active project.', [requiredString('message')], 'GitActionResult'), + + method('publish.uploadSite', 'Upload rendered site to remote server via SSH.', [requiredObject('credentials')], 'PublishSiteResult'), ]; const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [ @@ -310,60 +298,67 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [ ], }, { - name: 'ChatConversation', - description: 'Chat conversation container.', + name: 'GitAvailability', + description: 'Git installation availability check result.', fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique conversation identifier.' }, - { name: 'title', type: 'string', required: true, description: 'Conversation title.' }, - { name: 'model', type: 'string', required: false, description: 'Optional model id used by this conversation.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, - { name: 'updatedAt', type: 'string', required: true, description: 'Last update timestamp (ISO string).' }, + { name: 'gitFound', type: 'boolean', required: true, description: 'Whether git executable was found.' }, + { name: 'version', type: 'string', required: false, description: 'Git version string when available.' }, ], }, { - name: 'ChatMessage', - description: 'Single message entry in a conversation history.', + name: 'RepoState', + description: 'Repository state for the active project.', fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique message identifier.' }, - { name: 'conversationId', type: 'string', required: true, description: 'Owning conversation id.' }, - { name: 'role', type: "'user' | 'assistant' | 'system' | 'tool'", required: true, description: 'Message author role.' }, - { name: 'content', type: 'string', required: true, description: 'Message text content.' }, - { name: 'toolCallId', type: 'string', required: false, description: 'Tool call id when associated with tool output.' }, - { name: 'toolCalls', type: 'string', required: false, description: 'Serialized tool call payload when present.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, + { name: 'isRepo', type: 'boolean', required: true, description: 'Whether the project directory is a git repository.' }, + { name: 'rootPath', type: 'string', required: false, description: 'Repository root path.' }, + { name: 'currentBranch', type: 'string', required: false, description: 'Current branch name.' }, + { name: 'hasRemote', type: 'boolean', required: true, description: 'Whether a remote is configured.' }, ], }, { - name: 'ChatModel', - description: 'Available chat model descriptor.', + name: 'GitStatusDto', + description: 'Working tree status with file list and counts.', fields: [ - { name: 'id', type: 'string', required: true, description: 'Model identifier.' }, - { name: 'name', type: 'string', required: true, description: 'Human-readable model name.' }, - { name: 'provider', type: 'string', required: false, description: 'Model provider name.' }, + { name: 'files', type: 'Array<{ path: string; status: string; previousPath?: string }>', required: true, description: 'List of changed files with status.' }, + { name: 'counts', type: '{ untracked: number; modified: number; deleted: number; renamed: number; staged: number }', required: true, description: 'Counts by change type.' }, ], }, { - name: 'ChatReadyStatus', - description: 'Chat backend readiness status.', + name: 'GitRemoteStateDto', + description: 'Remote tracking state for the active project branch.', fields: [ - { name: 'ready', type: 'boolean', required: true, description: 'Whether chat backend is ready.' }, - { name: 'error', type: 'string', required: false, description: 'Error description when not ready.' }, - { name: 'backend', type: 'string', required: false, description: 'Selected backend identifier.' }, + { name: 'localBranch', type: 'string | null', required: true, description: 'Local branch name.' }, + { name: 'upstreamBranch', type: 'string | null', required: true, description: 'Upstream tracking branch name.' }, + { name: 'hasUpstream', type: 'boolean', required: true, description: 'Whether an upstream is configured.' }, + { name: 'ahead', type: 'number', required: true, description: 'Commits ahead of upstream.' }, + { name: 'behind', type: 'number', required: true, description: 'Commits behind upstream.' }, ], }, { - name: 'ChatApiKeyStatus', - description: 'Stored API key state for chat provider.', + name: 'GitActionResult', + description: 'Result from a git operation (fetch, pull, push, commit).', fields: [ - { name: 'hasKey', type: 'boolean', required: true, description: 'Whether a key is configured.' }, - { name: 'maskedKey', type: 'string', required: true, description: 'Masked key representation for UI display.' }, + { name: 'success', type: 'boolean', required: true, description: 'Whether the operation succeeded.' }, + { name: 'code', type: 'string', required: false, description: "Error code when failed ('auth-required', 'conflict', 'network', 'action-failed')." }, + { name: 'error', type: 'string', required: false, description: 'Error message when failed.' }, + { name: 'guidance', type: 'string[]', required: false, description: 'Guidance messages for resolving failures.' }, + ], + }, + { + name: 'PublishSiteResult', + description: 'Aggregate result from uploading the rendered site.', + fields: [ + { name: 'htmlFilesUploaded', type: 'number', required: true, description: 'Number of HTML files uploaded.' }, + { name: 'thumbnailFilesUploaded', type: 'number', required: true, description: 'Number of thumbnail files uploaded.' }, + { name: 'mediaFilesUploaded', type: 'number', required: true, description: 'Number of media files uploaded.' }, + { name: 'filesSkipped', type: 'number', required: true, description: 'Total files skipped (already up-to-date).' }, ], }, ]; export const BDS_PYTHON_API_CONTRACT_V1: PythonApiContractV1 = { - version: '1.6.0', - generatedAt: '2026-02-25T00:00:00.000Z', + version: '1.7.0', + generatedAt: '2026-02-27T00:00:00.000Z', methods: METHODS_V1, dataStructures: DATA_STRUCTURES_V1, }; diff --git a/src/renderer/python/generatePythonApiModuleV1.d.ts b/src/renderer/python/generatePythonApiModuleV1.d.ts deleted file mode 100644 index 10aec7d..0000000 --- a/src/renderer/python/generatePythonApiModuleV1.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function generatePythonApiModuleV1(): string; -//# sourceMappingURL=generatePythonApiModuleV1.d.ts.map \ No newline at end of file diff --git a/src/renderer/python/generatePythonApiModuleV1.d.ts.map b/src/renderer/python/generatePythonApiModuleV1.d.ts.map deleted file mode 100644 index 7d36fde..0000000 --- a/src/renderer/python/generatePythonApiModuleV1.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"generatePythonApiModuleV1.d.ts","sourceRoot":"","sources":["generatePythonApiModuleV1.ts"],"names":[],"mappings":"AAyHA,wBAAgB,yBAAyB,IAAI,MAAM,CA+DlD"} \ No newline at end of file diff --git a/src/renderer/python/generatePythonApiModuleV1.js b/src/renderer/python/generatePythonApiModuleV1.js deleted file mode 100644 index a321ca1..0000000 --- a/src/renderer/python/generatePythonApiModuleV1.js +++ /dev/null @@ -1,162 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePythonApiModuleV1 = generatePythonApiModuleV1; -const pythonApiContractV1_1 = require("./pythonApiContractV1"); -const PYTHON_RESERVED_KEYWORDS = new Set([ - 'false', - 'none', - 'true', - 'and', - 'as', - 'assert', - 'async', - 'await', - 'break', - 'class', - 'continue', - 'def', - 'del', - 'elif', - 'else', - 'except', - 'finally', - 'for', - 'from', - 'global', - 'if', - 'import', - 'in', - 'is', - 'lambda', - 'nonlocal', - 'not', - 'or', - 'pass', - 'raise', - 'return', - 'try', - 'while', - 'with', - 'yield', - 'match', - 'case', -]); -function toSnakeCase(value) { - return value - .replace(/([a-z0-9])([A-Z])/g, '$1_$2') - .replace(/[^a-zA-Z0-9]+/g, '_') - .replace(/^_+|_+$/g, '') - .toLowerCase(); -} -function quotePython(value) { - return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); -} -function toPythonIdentifier(value) { - let identifier = toSnakeCase(value); - if (!identifier) { - identifier = '_'; - } - if (/^[0-9]/.test(identifier)) { - identifier = `_${identifier}`; - } - if (PYTHON_RESERVED_KEYWORDS.has(identifier)) { - identifier = `${identifier}_`; - } - return identifier; -} -function buildPythonMethod(method) { - const [namespace, member] = method.method.split('.'); - if (!namespace || !member) { - return ''; - } - const pythonMethodName = toPythonIdentifier(member); - const pythonParams = method.params.map((param) => ({ - sourceName: param.name, - pythonName: toPythonIdentifier(param.name), - required: param.required, - })); - const signature = pythonParams.length > 0 - ? `, ${pythonParams.map((param) => (param.required ? param.pythonName : `${param.pythonName}=None`)).join(', ')}` - : ''; - const argsDict = method.params.length > 0 - ? `{ ${method.params.map((param, index) => `"${param.name}": ${pythonParams[index]?.pythonName}`).join(', ')} }` - : '{}'; - return [ - ` async def ${pythonMethodName}(self${signature}):`, - ` \"\"\"${quotePython(method.description)}\"\"\"`, - ` return await self._transport.call("${method.method}", ${argsDict})`, - '', - ].join('\n'); -} -function buildPythonNamespaceClass(namespace, methods) { - const className = `${namespace[0].toUpperCase()}${namespace.slice(1)}Api`; - const methodBlocks = methods.map((method) => buildPythonMethod(method)).join(''); - return [ - `class ${className}:`, - ' def __init__(self, transport):', - ' self._transport = transport', - '', - methodBlocks.trimEnd(), - '', - ].join('\n'); -} -function generatePythonApiModuleV1() { - const namespaceMap = new Map(); - for (const method of pythonApiContractV1_1.BDS_PYTHON_API_CONTRACT_V1.methods) { - const [namespace] = method.method.split('.'); - if (!namespace) { - continue; - } - const entries = namespaceMap.get(namespace) ?? []; - entries.push({ - method: method.method, - description: method.description, - params: method.params.map((param) => ({ - name: param.name, - required: param.required, - })), - }); - namespaceMap.set(namespace, entries); - } - const namespaceBlocks = Array.from(namespaceMap.entries()) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([namespace, methods]) => buildPythonNamespaceClass(namespace, methods)) - .join('\n'); - const namespaceAssignments = Array.from(namespaceMap.keys()) - .sort((left, right) => left.localeCompare(right)) - .map((namespace) => ` self.${toPythonIdentifier(namespace)} = ${namespace[0].toUpperCase()}${namespace.slice(1)}Api(transport)`) - .join('\n'); - return [ - '# Auto-generated by generatePythonApiModuleV1.ts', - `# Contract version: ${pythonApiContractV1_1.BDS_PYTHON_API_CONTRACT_V1.version}`, - '', - 'import json', - '', - 'class BdsApiError(Exception):', - ' pass', - '', - namespaceBlocks.trimEnd(), - '', - 'class BdsApi:', - ' def __init__(self, transport):', - ' self._transport = transport', - namespaceAssignments, - '', - 'class _Transport:', - ' def __init__(self, call_impl):', - ' self._call_impl = call_impl', - '', - ' async def call(self, method, args):', - ' raw_result = await self._call_impl(method, json.dumps(args))', - ' if raw_result is None or raw_result == "":', - ' return None', - ' return json.loads(raw_result)', - '', - 'def install_bds_api(call_impl):', - ' _transport = _Transport(call_impl)', - ' bds = BdsApi(_transport)', - ' return bds', - '', - ].join('\n'); -} -//# sourceMappingURL=generatePythonApiModuleV1.js.map \ No newline at end of file diff --git a/src/renderer/python/generatePythonApiModuleV1.js.map b/src/renderer/python/generatePythonApiModuleV1.js.map deleted file mode 100644 index c7f38c4..0000000 --- a/src/renderer/python/generatePythonApiModuleV1.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"generatePythonApiModuleV1.js","sourceRoot":"","sources":["generatePythonApiModuleV1.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/src/renderer/python/pythonApiContractV1.d.ts b/src/renderer/python/pythonApiContractV1.d.ts deleted file mode 100644 index 99be49e..0000000 --- a/src/renderer/python/pythonApiContractV1.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ElectronAPI } from '../../main/shared/electronApi'; -type PythonPromiseMethodPath = { - [Group in keyof ElectronAPI]: ElectronAPI[Group] extends Record unknown> ? { - [Method in keyof ElectronAPI[Group]]: ElectronAPI[Group][Method] extends (...args: never[]) => Promise ? `${Extract}.${Extract}` : never; - }[keyof ElectronAPI[Group]] : never; -}[keyof ElectronAPI]; -export type PythonApiParamType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any' | 'stringOrNull'; -export interface PythonApiParamContractV1 { - name: string; - type: PythonApiParamType; - required: boolean; -} -export interface PythonApiMethodContractV1 { - method: PythonPromiseMethodPath; - description: string; - params: PythonApiParamContractV1[]; - returns: string; -} -export interface PythonApiDataStructureFieldContractV1 { - name: string; - type: string; - required: boolean; - description: string; -} -export interface PythonApiDataStructureContractV1 { - name: string; - description: string; - fields: PythonApiDataStructureFieldContractV1[]; -} -export interface PythonApiContractV1 { - version: string; - generatedAt: string; - methods: PythonApiMethodContractV1[]; - dataStructures: PythonApiDataStructureContractV1[]; -} -export declare const BDS_PYTHON_API_CONTRACT_V1: PythonApiContractV1; -export declare function listPythonApiMethodNames(): string[]; -export declare function getPythonApiMethodContract(methodName: string): PythonApiMethodContractV1 | undefined; -export declare function getPythonApiDataStructureContracts(): PythonApiDataStructureContractV1[]; -export {}; -//# sourceMappingURL=pythonApiContractV1.d.ts.map \ No newline at end of file diff --git a/src/renderer/python/pythonApiContractV1.d.ts.map b/src/renderer/python/pythonApiContractV1.d.ts.map deleted file mode 100644 index ec88317..0000000 --- a/src/renderer/python/pythonApiContractV1.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"pythonApiContractV1.d.ts","sourceRoot":"","sources":["pythonApiContractV1.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,KAAK,uBAAuB,GAAG;KAC5B,KAAK,IAAI,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAClG;SACG,MAAM,IAAI,MAAM,WAAW,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,GAC3G,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GACtD,KAAK;KACV,CAAC,MAAM,WAAW,CAAC,KAAK,CAAC,CAAC,GAC3B,KAAK;CACV,CAAC,MAAM,WAAW,CAAC,CAAC;AAErB,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,cAAc,CAAC;AAE/G,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,wBAAwB,EAAE,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qCAAqC;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,qCAAqC,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,yBAAyB,EAAE,CAAC;IACrC,cAAc,EAAE,gCAAgC,EAAE,CAAC;CACpD;AA8TD,eAAO,MAAM,0BAA0B,EAAE,mBAKxC,CAAC;AAEF,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAEnD;AAED,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,yBAAyB,GAAG,SAAS,CAEpG;AAED,wBAAgB,kCAAkC,IAAI,gCAAgC,EAAE,CAEvF"} \ No newline at end of file diff --git a/src/renderer/python/pythonApiContractV1.js b/src/renderer/python/pythonApiContractV1.js deleted file mode 100644 index 2a2c68d..0000000 --- a/src/renderer/python/pythonApiContractV1.js +++ /dev/null @@ -1,320 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BDS_PYTHON_API_CONTRACT_V1 = void 0; -exports.listPythonApiMethodNames = listPythonApiMethodNames; -exports.getPythonApiMethodContract = getPythonApiMethodContract; -exports.getPythonApiDataStructureContracts = getPythonApiDataStructureContracts; -const requiredString = (name) => ({ name, type: 'string', required: true }); -const optionalString = (name) => ({ name, type: 'string', required: false }); -const optionalNumber = (name) => ({ name, type: 'number', required: false }); -const requiredObject = (name) => ({ name, type: 'object', required: true }); -const optionalObject = (name) => ({ name, type: 'object', required: false }); -const requiredArray = (name) => ({ name, type: 'array', required: true }); -const requiredAny = (name) => ({ name, type: 'any', required: true }); -const requiredStringOrNull = (name) => ({ name, type: 'stringOrNull', required: true }); -function method(methodName, description, params, returns) { - return { - method: methodName, - description, - params, - returns, - }; -} -const METHODS_V1 = [ - method('projects.create', 'Create a project.', [requiredObject('data')], 'ProjectData'), - method('projects.update', 'Update a project by id.', [requiredString('id'), requiredObject('data')], 'ProjectData | null'), - method('projects.delete', 'Delete a project by id.', [requiredString('id')], 'boolean'), - method('projects.deleteWithData', 'Delete a project and data by id.', [requiredString('id')], 'boolean'), - method('projects.get', 'Fetch one project by id.', [requiredString('id')], 'ProjectData | null'), - method('projects.getAll', 'Fetch all projects.', [], 'ProjectData[]'), - method('projects.getActive', 'Fetch active project.', [], 'ProjectData | null'), - method('projects.setActive', 'Set active project by id.', [requiredString('id')], 'ProjectData | null'), - method('posts.create', 'Create a post.', [requiredObject('data')], 'PostData'), - method('posts.update', 'Update a post by id.', [requiredString('id'), requiredObject('data')], 'PostData | null'), - method('posts.delete', 'Delete a post by id.', [requiredString('id')], 'boolean'), - method('posts.get', 'Fetch one post by id.', [requiredString('postId')], 'PostData | null'), - method('posts.getPreviewUrl', 'Get preview URL for post.', [requiredString('id'), optionalObject('options')], 'string | null'), - method('posts.getAll', 'Fetch posts with pagination.', [optionalObject('options')], 'PaginatedPostsResult'), - method('posts.getByStatus', 'Fetch posts by status.', [requiredString('status')], 'PostData[]'), - method('posts.publish', 'Publish a post by id.', [requiredString('id')], 'PostData | null'), - method('posts.discard', 'Discard draft changes for post.', [requiredString('id')], 'PostData | null'), - method('posts.hasPublishedVersion', 'Check if post has published version.', [requiredString('id')], 'boolean'), - method('posts.rebuildFromFiles', 'Rebuild posts database from files.', [], 'void'), - method('posts.reindexText', 'Reindex post search text.', [], 'void'), - method('posts.search', 'Search posts by free-text query.', [requiredString('query')], 'SearchResult[]'), - method('posts.filter', 'Filter posts by criteria.', [requiredObject('filter')], 'PostData[]'), - method('posts.getTags', 'Get all post tags.', [], 'string[]'), - method('posts.getCategories', 'Get all post categories.', [], 'string[]'), - method('posts.getByYearMonth', 'Get post counts grouped by year/month.', [], 'Array<{ year: number; month: number; count: number } >'), - method('posts.getDashboardStats', 'Get post dashboard stats.', [], 'DashboardStats'), - method('posts.getTagsWithCounts', 'Get post tags with counts.', [], 'TagCount[]'), - method('posts.getCategoriesWithCounts', 'Get post categories with counts.', [], 'CategoryCount[]'), - method('posts.getLinksTo', 'Get posts linked to given post.', [requiredString('id')], 'PostData[]'), - method('posts.getLinkedBy', 'Get posts linking to given post.', [requiredString('id')], 'PostData[]'), - method('posts.rebuildLinks', 'Rebuild post link graph.', [], 'void'), - method('posts.isSlugAvailable', 'Check if post slug is available.', [requiredString('slug'), optionalString('excludePostId')], 'boolean'), - method('posts.generateUniqueSlug', 'Generate unique slug from title.', [requiredString('title'), optionalString('excludePostId')], 'string'), - method('media.import', 'Import media file.', [requiredString('sourcePath'), optionalObject('metadata')], 'MediaData'), - method('media.update', 'Update media metadata by id.', [requiredString('id'), requiredObject('data')], 'MediaData | null'), - method('media.replaceFile', 'Replace media file by id.', [requiredString('id'), requiredString('newSourcePath')], 'MediaData | null'), - method('media.delete', 'Delete media by id.', [requiredString('id')], 'boolean'), - method('media.get', 'Fetch one media by id.', [requiredString('id')], 'MediaData | null'), - method('media.getUrl', 'Get media URL by id.', [requiredString('id')], 'string | null'), - method('media.getFilePath', 'Get media file path by id.', [requiredString('id')], 'string | null'), - method('media.getAll', 'Fetch all media.', [], 'MediaData[]'), - method('media.rebuildFromFiles', 'Rebuild media database from files.', [], 'void'), - method('media.reindexText', 'Reindex media search text.', [], 'void'), - method('media.getThumbnail', 'Get media thumbnail URL.', [requiredString('id'), optionalString('size')], 'string | null'), - method('media.regenerateThumbnails', 'Regenerate thumbnails for media.', [requiredString('id')], 'Record | null'), - method('media.regenerateMissingThumbnails', 'Regenerate all missing thumbnails.', [], '{ processed: number; generated: number; failed: number }'), - method('media.filter', 'Filter media by criteria.', [requiredObject('filter')], 'MediaData[]'), - method('media.search', 'Search media by free-text query.', [requiredString('query')], 'MediaSearchResult[]'), - method('media.getByYearMonth', 'Get media counts grouped by year/month.', [], 'Array<{ year: number; month: number; count: number } >'), - method('media.getTags', 'Get all media tags.', [], 'string[]'), - method('media.getTagsWithCounts', 'Get media tags with counts.', [], 'TagCount[]'), - method('scripts.create', 'Create script.', [requiredObject('data')], 'ScriptData'), - method('scripts.update', 'Update script by id.', [requiredString('id'), requiredObject('data')], 'ScriptData | null'), - method('scripts.delete', 'Delete script by id.', [requiredString('id')], 'boolean'), - method('scripts.get', 'Fetch script by id.', [requiredString('id')], 'ScriptData | null'), - method('scripts.getAll', 'Fetch all scripts.', [], 'ScriptData[]'), - method('scripts.rebuildFromFiles', 'Rebuild scripts from files.', [], 'void'), - method('tasks.getAll', 'Fetch all tasks.', [], 'TaskProgress[]'), - method('tasks.getRunning', 'Fetch running tasks.', [], 'TaskProgress[]'), - method('tasks.cancel', 'Cancel task by id.', [requiredString('taskId')], 'boolean'), - method('tasks.clearCompleted', 'Clear completed tasks.', [], 'void'), - method('app.getDataPaths', 'Get app data paths.', [], '{ database: string; posts: string; media: string }'), - method('app.getSystemLanguage', 'Get system language.', [], 'string'), - method('app.getTitleBarMetrics', 'Get title bar metrics.', [], '{ macosLeftInset: number } | null'), - method('app.openFolder', 'Open folder in system file manager.', [requiredString('folderPath')], 'string'), - method('app.showItemInFolder', 'Reveal item in system file manager.', [requiredString('itemPath')], 'void'), - method('app.selectFolder', 'Show folder picker dialog.', [optionalString('title')], 'string | null'), - method('app.getDefaultProjectPath', 'Get default project path.', [requiredString('projectId')], 'string'), - method('app.readProjectMetadata', 'Read project metadata from path.', [requiredString('folderPath')], '{ name?: string; description?: string; publicUrl?: string; mainLanguage?: string } | null'), - method('app.getBlogmarkBookmarklet', 'Get blogmark bookmarklet script.', [], 'string'), - method('app.copyToClipboard', 'Copy text to clipboard.', [requiredString('text')], 'boolean'), - method('app.notifyRendererReady', 'Notify main process renderer is ready.', [], 'boolean'), - method('app.setPreviewPostTarget', 'Set preview post target.', [requiredStringOrNull('postId')], 'void'), - method('app.triggerMenuAction', 'Trigger menu action.', [requiredString('action')], 'void'), - method('meta.getTags', 'Get project tags.', [], 'string[]'), - method('meta.getCategories', 'Get project categories.', [], 'string[]'), - method('meta.addTag', 'Add project tag.', [requiredString('tag')], 'string[]'), - method('meta.removeTag', 'Remove project tag.', [requiredString('tag')], 'string[]'), - method('meta.addCategory', 'Add project category.', [requiredString('category')], 'string[]'), - method('meta.removeCategory', 'Remove project category.', [requiredString('category')], 'string[]'), - method('meta.syncOnStartup', 'Sync meta values on startup.', [], '{ tags: string[]; categories: string[]; projectMetadata: ProjectMetadata | null }'), - method('meta.getProjectMetadata', 'Read active project metadata.', [], 'ProjectMetadata | null'), - method('meta.setProjectMetadata', 'Set project metadata.', [requiredObject('metadata')], 'ProjectMetadata | null'), - method('meta.updateProjectMetadata', 'Update project metadata.', [requiredObject('updates')], 'ProjectMetadata | null'), - method('tags.getAll', 'Fetch all tags.', [], 'TagData[]'), - method('tags.getWithCounts', 'Fetch tags with counts.', [], 'TagWithCount[]'), - method('tags.get', 'Fetch tag by id.', [requiredString('id')], 'TagData | null'), - method('tags.getByName', 'Fetch tag by name.', [requiredString('name')], 'TagData | null'), - method('tags.create', 'Create tag.', [requiredObject('data')], 'TagData'), - method('tags.update', 'Update tag by id.', [requiredString('id'), requiredObject('data')], 'TagData | null'), - method('tags.delete', 'Delete tag by id.', [requiredString('id')], 'DeleteTagResult'), - method('tags.merge', 'Merge tags into target tag.', [requiredArray('sourceTagIds'), requiredString('targetTagId')], 'MergeTagsResult'), - method('tags.rename', 'Rename tag by id.', [requiredString('id'), requiredString('newName')], 'RenameTagResult'), - method('tags.getPostsWithTag', 'Get posts using a tag.', [requiredString('tagId')], 'string[]'), - method('tags.syncFromPosts', 'Sync tag index from posts.', [], 'SyncTagsResult'), - method('chat.checkReady', 'Check chat backend readiness.', [], 'ChatReadyStatus'), - method('chat.validateApiKey', 'Validate chat API key and list available models.', [requiredString('apiKey')], '{ isValid: boolean; models: ChatModel[] }'), - method('chat.setApiKey', 'Store chat API key.', [requiredString('apiKey')], '{ success: boolean; error?: string }'), - method('chat.getApiKey', 'Get stored chat API key status.', [], 'ChatApiKeyStatus'), - method('chat.getAvailableModels', 'Get available chat models and selected default.', [], '{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }'), - method('chat.setDefaultModel', 'Set default chat model.', [requiredString('modelId')], '{ success: boolean; error?: string }'), - method('chat.getSystemPrompt', 'Get configured system prompt.', [], '{ success: boolean; prompt?: string; error?: string }'), - method('chat.setSystemPrompt', 'Set system prompt.', [requiredString('prompt')], '{ success: boolean; error?: string }'), - method('chat.getConversations', 'Fetch all chat conversations.', [], 'ChatConversation[]'), - method('chat.createConversation', 'Create a chat conversation.', [optionalString('title'), optionalString('model')], 'ChatConversation'), - method('chat.getConversation', 'Fetch one chat conversation by id.', [requiredString('id')], 'ChatConversation | null'), - method('chat.updateConversation', 'Update chat conversation metadata.', [requiredString('id'), requiredObject('updates')], 'ChatConversation | null'), - method('chat.deleteConversation', 'Delete chat conversation by id.', [requiredString('id')], 'boolean'), - method('chat.sendMessage', 'Send message to chat conversation.', [requiredString('conversationId'), requiredString('message'), optionalObject('metadata')], '{ success: boolean; message?: string; error?: string }'), - method('chat.abortMessage', 'Abort active streaming chat response.', [requiredString('conversationId')], 'void'), - method('chat.getHistory', 'Get message history for conversation.', [requiredString('conversationId')], 'ChatMessage[]'), - method('chat.clearMessages', 'Clear messages for conversation.', [requiredString('conversationId')], 'void'), - method('chat.setConversationModel', 'Set model for a conversation.', [requiredString('conversationId'), requiredString('modelId')], 'void'), - method('chat.analyzeTaxonomy', 'Analyze categories and tags using AI.', [requiredArray('categories'), requiredArray('tags'), requiredString('modelId')], '{ success: boolean; categoryMappings?: Record; tagMappings?: Record; error?: string }'), - method('chat.analyzeMediaImage', 'Analyze media image and propose metadata.', [requiredString('mediaId'), optionalString('language')], '{ success: boolean; title?: string; alt?: string; caption?: string; error?: string }'), - method('sync.configure', 'Configure sync.', [requiredObject('config')], 'void'), - method('sync.start', 'Start sync operation.', [optionalString('direction')], 'SyncResult'), - method('sync.getStatus', 'Get sync status.', [], "'idle' | 'syncing' | 'error'"), - method('sync.isConfigured', 'Check if sync is configured.', [], 'boolean'), - method('sync.getPendingCount', 'Get pending sync item count.', [], '{ posts: number; media: number }'), - method('sync.getLog', 'Get sync log.', [optionalNumber('limit')], 'unknown[]'), - method('sync.stopAutoSync', 'Stop automatic sync.', [], 'void'), -]; -const DATA_STRUCTURES_V1 = [ - { - name: 'ProjectData', - description: 'Project metadata stored in the app database.', - fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique project identifier.' }, - { name: 'name', type: 'string', required: true, description: 'Human-readable project name.' }, - { name: 'slug', type: 'string', required: true, description: 'URL-friendly project slug.' }, - { name: 'description', type: 'string', required: false, description: 'Optional project description.' }, - { name: 'dataPath', type: 'string', required: false, description: 'Filesystem path for project data.' }, - { name: 'isActive', type: 'boolean', required: true, description: 'Whether this project is currently active.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, - { name: 'updatedAt', type: 'string', required: true, description: 'Last update timestamp (ISO string).' }, - ], - }, - { - name: 'PostData', - description: 'Canonical post object used across editor and generation flows.', - fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique post identifier.' }, - { name: 'projectId', type: 'string', required: true, description: 'Owning project id.' }, - { name: 'title', type: 'string', required: true, description: 'Post title.' }, - { name: 'slug', type: 'string', required: true, description: 'URL slug used for generated routes.' }, - { name: 'excerpt', type: 'string', required: false, description: 'Optional short summary.' }, - { name: 'content', type: 'string', required: true, description: 'Markdown body content.' }, - { name: 'status', type: "'draft' | 'published' | 'archived'", required: true, description: 'Publication lifecycle state.' }, - { name: 'author', type: 'string', required: false, description: 'Optional author name.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, - { name: 'updatedAt', type: 'string', required: true, description: 'Last update timestamp (ISO string).' }, - { name: 'publishedAt', type: 'string', required: false, description: 'Publication timestamp for published posts.' }, - { name: 'tags', type: 'string[]', required: true, description: 'List of tag names.' }, - { name: 'categories', type: 'string[]', required: true, description: 'List of category names.' }, - ], - }, - { - name: 'MediaData', - description: 'Canonical media object representing imported files and metadata.', - fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique media identifier.' }, - { name: 'projectId', type: 'string', required: true, description: 'Owning project id.' }, - { name: 'filename', type: 'string', required: true, description: 'Stored filename in project media folder.' }, - { name: 'originalName', type: 'string', required: true, description: 'Original imported filename.' }, - { name: 'mimeType', type: 'string', required: true, description: 'Detected MIME type.' }, - { name: 'size', type: 'number', required: true, description: 'File size in bytes.' }, - { name: 'width', type: 'number', required: false, description: 'Image width in pixels when available.' }, - { name: 'height', type: 'number', required: false, description: 'Image height in pixels when available.' }, - { name: 'title', type: 'string', required: false, description: 'Optional display title.' }, - { name: 'alt', type: 'string', required: false, description: 'Optional alternative text.' }, - { name: 'caption', type: 'string', required: false, description: 'Optional caption text.' }, - { name: 'author', type: 'string', required: false, description: 'Optional author credit.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, - { name: 'updatedAt', type: 'string', required: true, description: 'Last update timestamp (ISO string).' }, - { name: 'tags', type: 'string[]', required: true, description: 'List of media tags.' }, - ], - }, - { - name: 'ScriptData', - description: 'Script definition for Python macros, utilities, and transforms.', - fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique script identifier.' }, - { name: 'projectId', type: 'string', required: true, description: 'Owning project id.' }, - { name: 'slug', type: 'string', required: true, description: 'Stable script slug.' }, - { name: 'title', type: 'string', required: true, description: 'Human-readable script title.' }, - { name: 'kind', type: "'macro' | 'utility' | 'transform'", required: true, description: 'Script category.' }, - { name: 'entrypoint', type: 'string', required: true, description: 'Python entrypoint function name.' }, - { name: 'enabled', type: 'boolean', required: true, description: 'Whether script is enabled.' }, - { name: 'version', type: 'number', required: true, description: 'Incrementing script version.' }, - { name: 'filePath', type: 'string', required: true, description: 'Filesystem path to script file.' }, - { name: 'content', type: 'string', required: true, description: 'Script source code.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, - { name: 'updatedAt', type: 'string', required: true, description: 'Last update timestamp (ISO string).' }, - ], - }, - { - name: 'TaskProgress', - description: 'Task queue status object for long-running operations.', - fields: [ - { name: 'taskId', type: 'string', required: true, description: 'Unique task identifier.' }, - { name: 'name', type: 'string', required: true, description: 'Task display name.' }, - { name: 'status', type: "'pending' | 'running' | 'completed' | 'failed' | 'cancelled'", required: true, description: 'Current task status.' }, - { name: 'progress', type: 'number', required: true, description: 'Progress percentage from 0-100.' }, - { name: 'message', type: 'string', required: true, description: 'Current progress message.' }, - { name: 'startTime', type: 'string', required: true, description: 'Task start time (ISO string).' }, - { name: 'endTime', type: 'string', required: false, description: 'Task completion time (ISO string).' }, - { name: 'error', type: 'string', required: false, description: 'Error message when failed.' }, - { name: 'groupId', type: 'string', required: false, description: 'Optional grouping id.' }, - { name: 'groupName', type: 'string', required: false, description: 'Optional grouping label.' }, - ], - }, - { - name: 'ProjectMetadata', - description: 'Extended project metadata from project settings.', - fields: [ - { name: 'name', type: 'string', required: true, description: 'Project display name.' }, - { name: 'description', type: 'string', required: false, description: 'Optional project description.' }, - { name: 'dataPath', type: 'string', required: false, description: 'Optional custom data path.' }, - { name: 'publicUrl', type: 'string', required: false, description: 'Optional public site URL.' }, - { name: 'mainLanguage', type: 'string', required: false, description: 'Main render language code.' }, - { name: 'defaultAuthor', type: 'string', required: false, description: 'Default author for new posts.' }, - { name: 'maxPostsPerPage', type: 'number', required: false, description: 'Pagination size for generated lists.' }, - { name: 'blogmarkCategory', type: 'string', required: false, description: 'Default category for blogmark imports.' }, - { name: 'pythonRuntimeMode', type: "'webworker' | 'main-thread'", required: false, description: 'Python runtime execution mode.' }, - { name: 'picoTheme', type: 'string', required: false, description: 'Preferred Pico theme token.' }, - { name: 'categoryMetadata', type: 'object', required: false, description: 'Category metadata keyed by category slug.' }, - { name: 'categorySettings', type: 'object', required: false, description: 'Category render settings keyed by category slug.' }, - ], - }, - { - name: 'ChatConversation', - description: 'Chat conversation container.', - fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique conversation identifier.' }, - { name: 'title', type: 'string', required: true, description: 'Conversation title.' }, - { name: 'model', type: 'string', required: false, description: 'Optional model id used by this conversation.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, - { name: 'updatedAt', type: 'string', required: true, description: 'Last update timestamp (ISO string).' }, - ], - }, - { - name: 'ChatMessage', - description: 'Single message entry in a conversation history.', - fields: [ - { name: 'id', type: 'string', required: true, description: 'Unique message identifier.' }, - { name: 'conversationId', type: 'string', required: true, description: 'Owning conversation id.' }, - { name: 'role', type: "'user' | 'assistant' | 'system' | 'tool'", required: true, description: 'Message author role.' }, - { name: 'content', type: 'string', required: true, description: 'Message text content.' }, - { name: 'toolCallId', type: 'string', required: false, description: 'Tool call id when associated with tool output.' }, - { name: 'toolCalls', type: 'string', required: false, description: 'Serialized tool call payload when present.' }, - { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' }, - ], - }, - { - name: 'ChatModel', - description: 'Available chat model descriptor.', - fields: [ - { name: 'id', type: 'string', required: true, description: 'Model identifier.' }, - { name: 'name', type: 'string', required: true, description: 'Human-readable model name.' }, - { name: 'provider', type: 'string', required: false, description: 'Model provider name.' }, - ], - }, - { - name: 'ChatReadyStatus', - description: 'Chat backend readiness status.', - fields: [ - { name: 'ready', type: 'boolean', required: true, description: 'Whether chat backend is ready.' }, - { name: 'error', type: 'string', required: false, description: 'Error description when not ready.' }, - { name: 'backend', type: 'string', required: false, description: 'Selected backend identifier.' }, - ], - }, - { - name: 'ChatApiKeyStatus', - description: 'Stored API key state for chat provider.', - fields: [ - { name: 'hasKey', type: 'boolean', required: true, description: 'Whether a key is configured.' }, - { name: 'maskedKey', type: 'string', required: true, description: 'Masked key representation for UI display.' }, - ], - }, -]; -exports.BDS_PYTHON_API_CONTRACT_V1 = { - version: '1.6.0', - generatedAt: '2026-02-25T00:00:00.000Z', - methods: METHODS_V1, - dataStructures: DATA_STRUCTURES_V1, -}; -function listPythonApiMethodNames() { - return exports.BDS_PYTHON_API_CONTRACT_V1.methods.map((entry) => entry.method); -} -function getPythonApiMethodContract(methodName) { - return exports.BDS_PYTHON_API_CONTRACT_V1.methods.find((entry) => entry.method === methodName); -} -function getPythonApiDataStructureContracts() { - return exports.BDS_PYTHON_API_CONTRACT_V1.dataStructures; -} -//# sourceMappingURL=pythonApiContractV1.js.map \ No newline at end of file diff --git a/src/renderer/python/pythonApiContractV1.js.map b/src/renderer/python/pythonApiContractV1.js.map deleted file mode 100644 index 9891599..0000000 --- a/src/renderer/python/pythonApiContractV1.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"pythonApiContractV1.js","sourceRoot":"","sources":["pythonApiContractV1.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/tests/engine/AppApiAdapter.test.ts b/tests/engine/AppApiAdapter.test.ts new file mode 100644 index 0000000..4c24074 --- /dev/null +++ b/tests/engine/AppApiAdapter.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { AppApiAdapter } from '../../src/main/engine/AppApiAdapter'; + +const { mockProjectEngine, mockDatabase, mockReadFile } = vi.hoisted(() => ({ + mockProjectEngine: { + getActiveProject: vi.fn().mockResolvedValue({ id: 'p1', dataPath: '/projects/blog' }), + getProjectPaths: vi.fn().mockReturnValue({ posts: '/projects/blog/posts', media: '/projects/blog/media' }), + getDefaultProjectBaseDir: vi.fn().mockResolvedValue('/home/user/bDS/p1'), + }, + mockDatabase: { + getDataPaths: vi.fn().mockReturnValue({ database: '/data/bds.db' }), + }, + mockReadFile: vi.fn(), +})); + +vi.mock('../../src/main/engine/ProjectEngine', () => ({ + getProjectEngine: () => mockProjectEngine, +})); + +vi.mock('../../src/main/database', () => ({ + getDatabase: () => mockDatabase, +})); + +vi.mock('electron', () => ({ + app: { getLocale: () => 'en-US' }, +})); + +vi.mock('fs/promises', () => ({ + default: { readFile: mockReadFile }, + readFile: mockReadFile, +})); + +describe('AppApiAdapter', () => { + let adapter: AppApiAdapter; + + beforeEach(() => { + vi.clearAllMocks(); + mockProjectEngine.getActiveProject.mockResolvedValue({ id: 'p1', dataPath: '/projects/blog' }); + mockProjectEngine.getProjectPaths.mockReturnValue({ posts: '/projects/blog/posts', media: '/projects/blog/media' }); + mockProjectEngine.getDefaultProjectBaseDir.mockResolvedValue('/home/user/bDS/p1'); + mockDatabase.getDataPaths.mockReturnValue({ database: '/data/bds.db' }); + adapter = new AppApiAdapter(); + }); + + it('getDataPaths returns database, posts, and media paths', async () => { + const result = await adapter.getDataPaths(); + expect(result).toEqual({ + database: '/data/bds.db', + posts: '/projects/blog/posts', + media: '/projects/blog/media', + }); + }); + + it('getSystemLanguage returns electron app locale', async () => { + const result = await adapter.getSystemLanguage(); + expect(result).toBe('en-US'); + }); + + it('getDefaultProjectPath delegates to ProjectEngine', async () => { + const result = await adapter.getDefaultProjectPath('p1'); + expect(mockProjectEngine.getDefaultProjectBaseDir).toHaveBeenCalledWith('p1'); + expect(result).toBe('/home/user/bDS/p1'); + }); + + it('readProjectMetadata returns metadata from project.json', async () => { + mockReadFile.mockResolvedValueOnce( + JSON.stringify({ name: 'My Blog', description: 'Test', publicUrl: 'https://blog.example.com', mainLanguage: 'en', dataPath: '/secret' }), + ); + + const result = await adapter.readProjectMetadata('/projects/blog'); + expect(result).toEqual({ + name: 'My Blog', + description: 'Test', + publicUrl: 'https://blog.example.com', + mainLanguage: 'en', + }); + // dataPath should be excluded + expect(result).not.toHaveProperty('dataPath'); + }); + + it('readProjectMetadata returns null when file does not exist', async () => { + mockReadFile.mockRejectedValueOnce(new Error('ENOENT')); + + const result = await adapter.readProjectMetadata('/nonexistent'); + expect(result).toBeNull(); + }); +}); diff --git a/tests/engine/GitApiAdapter.test.ts b/tests/engine/GitApiAdapter.test.ts new file mode 100644 index 0000000..358ad43 --- /dev/null +++ b/tests/engine/GitApiAdapter.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GitApiAdapter } from '../../src/main/engine/GitApiAdapter'; + +const { mockGitEngine, mockProjectEngine } = vi.hoisted(() => ({ + mockGitEngine: { + checkAvailability: vi.fn().mockResolvedValue({ gitFound: true, version: '2.40.0' }), + getRepoState: vi.fn().mockResolvedValue({ isRepo: true, currentBranch: 'main', hasRemote: true }), + getStatus: vi.fn().mockResolvedValue({ files: [], counts: { untracked: 0, modified: 0, deleted: 0, renamed: 0, staged: 0 } }), + getHistory: vi.fn().mockResolvedValue([]), + getRemoteState: vi.fn().mockResolvedValue({ localBranch: 'main', upstreamBranch: 'origin/main', hasUpstream: true, ahead: 0, behind: 0 }), + fetch: vi.fn().mockResolvedValue({ success: true }), + pull: vi.fn().mockResolvedValue({ success: true }), + push: vi.fn().mockResolvedValue({ success: true }), + commitAll: vi.fn().mockResolvedValue({ success: true }), + }, + mockProjectEngine: { + getActiveProject: vi.fn().mockResolvedValue({ id: 'p1', dataPath: '/projects/blog' }), + }, +})); + +vi.mock('../../src/main/engine/GitEngine', () => ({ + getGitEngine: () => mockGitEngine, +})); + +vi.mock('../../src/main/engine/ProjectEngine', () => ({ + getProjectEngine: () => mockProjectEngine, +})); + +describe('GitApiAdapter', () => { + let adapter: GitApiAdapter; + + beforeEach(() => { + vi.clearAllMocks(); + adapter = new GitApiAdapter(); + }); + + it('checkAvailability delegates directly (no projectPath)', async () => { + const result = await adapter.checkAvailability(); + expect(mockGitEngine.checkAvailability).toHaveBeenCalledWith(); + expect(result).toEqual({ gitFound: true, version: '2.40.0' }); + }); + + it('getRepoState resolves projectPath from active project', async () => { + await adapter.getRepoState(); + expect(mockProjectEngine.getActiveProject).toHaveBeenCalled(); + expect(mockGitEngine.getRepoState).toHaveBeenCalledWith('/projects/blog'); + }); + + it('getStatus resolves projectPath', async () => { + await adapter.getStatus(); + expect(mockGitEngine.getStatus).toHaveBeenCalledWith('/projects/blog'); + }); + + it('getHistory passes limit and resolves projectPath', async () => { + await adapter.getHistory(10); + expect(mockGitEngine.getHistory).toHaveBeenCalledWith('/projects/blog', 10); + }); + + it('getHistory passes undefined limit', async () => { + await adapter.getHistory(); + expect(mockGitEngine.getHistory).toHaveBeenCalledWith('/projects/blog', undefined); + }); + + it('getRemoteState resolves projectPath', async () => { + await adapter.getRemoteState(); + expect(mockGitEngine.getRemoteState).toHaveBeenCalledWith('/projects/blog'); + }); + + it('fetch resolves projectPath', async () => { + await adapter.fetch(); + expect(mockGitEngine.fetch).toHaveBeenCalledWith('/projects/blog'); + }); + + it('pull resolves projectPath', async () => { + await adapter.pull(); + expect(mockGitEngine.pull).toHaveBeenCalledWith('/projects/blog'); + }); + + it('push resolves projectPath', async () => { + await adapter.push(); + expect(mockGitEngine.push).toHaveBeenCalledWith('/projects/blog'); + }); + + it('commitAll resolves projectPath and passes message', async () => { + await adapter.commitAll('update files'); + expect(mockGitEngine.commitAll).toHaveBeenCalledWith('/projects/blog', 'update files'); + }); + + it('throws when no active project', async () => { + mockProjectEngine.getActiveProject.mockResolvedValueOnce(null); + await expect(adapter.getRepoState()).rejects.toThrow('No active project with a data path'); + }); + + it('throws when active project has no dataPath', async () => { + mockProjectEngine.getActiveProject.mockResolvedValueOnce({ id: 'p1', dataPath: null }); + await expect(adapter.fetch()).rejects.toThrow('No active project with a data path'); + }); +}); diff --git a/tests/engine/PublishApiAdapter.test.ts b/tests/engine/PublishApiAdapter.test.ts new file mode 100644 index 0000000..ebc8eca --- /dev/null +++ b/tests/engine/PublishApiAdapter.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { PublishApiAdapter } from '../../src/main/engine/PublishApiAdapter'; + +const { mockPublishEngine, mockProjectEngine, mockTaskManager } = vi.hoisted(() => ({ + mockPublishEngine: { + setProjectContext: vi.fn(), + uploadHtml: vi.fn().mockResolvedValue({ filesUploaded: 5, filesSkipped: 2 }), + uploadThumbnails: vi.fn().mockResolvedValue({ filesUploaded: 3, filesSkipped: 1 }), + uploadMedia: vi.fn().mockResolvedValue({ filesUploaded: 10, filesSkipped: 4 }), + }, + mockProjectEngine: { + getActiveProject: vi.fn().mockResolvedValue({ id: 'p1', dataPath: '/projects/blog' }), + }, + mockTaskManager: { + runTask: vi.fn().mockImplementation((opts: { execute: (onProgress: () => void) => Promise }) => { + return opts.execute(() => {}); + }), + }, +})); + +vi.mock('../../src/main/engine/PublishEngine', () => ({ + getPublishEngine: () => mockPublishEngine, +})); + +vi.mock('../../src/main/engine/ProjectEngine', () => ({ + getProjectEngine: () => mockProjectEngine, +})); + +vi.mock('../../src/main/engine/TaskManager', () => ({ + taskManager: mockTaskManager, +})); + +describe('PublishApiAdapter', () => { + let adapter: PublishApiAdapter; + const creds = { sshHost: 'example.com', sshUser: 'deploy', sshRemotePath: '/var/www', sshMode: 'rsync' as const }; + + beforeEach(() => { + vi.clearAllMocks(); + mockPublishEngine.uploadHtml.mockResolvedValue({ filesUploaded: 5, filesSkipped: 2 }); + mockPublishEngine.uploadThumbnails.mockResolvedValue({ filesUploaded: 3, filesSkipped: 1 }); + mockPublishEngine.uploadMedia.mockResolvedValue({ filesUploaded: 10, filesSkipped: 4 }); + mockProjectEngine.getActiveProject.mockResolvedValue({ id: 'p1', dataPath: '/projects/blog' }); + mockTaskManager.runTask.mockImplementation((opts: { execute: (onProgress: () => void) => Promise }) => { + return opts.execute(() => {}); + }); + adapter = new PublishApiAdapter(); + }); + + it('sets project context before uploading', async () => { + await adapter.uploadSite(creds); + expect(mockPublishEngine.setProjectContext).toHaveBeenCalledWith('p1', '/projects/blog'); + }); + + it('runs three parallel upload tasks', async () => { + await adapter.uploadSite(creds); + expect(mockTaskManager.runTask).toHaveBeenCalledTimes(3); + }); + + it('returns aggregate upload results', async () => { + const result = await adapter.uploadSite(creds); + expect(result).toEqual({ + htmlFilesUploaded: 5, + thumbnailFilesUploaded: 3, + mediaFilesUploaded: 10, + filesSkipped: 7, // 2 + 1 + 4 + }); + }); + + it('passes credentials to upload methods', async () => { + await adapter.uploadSite(creds); + expect(mockPublishEngine.uploadHtml).toHaveBeenCalledWith(creds, expect.any(Function)); + expect(mockPublishEngine.uploadThumbnails).toHaveBeenCalledWith(creds, expect.any(Function)); + expect(mockPublishEngine.uploadMedia).toHaveBeenCalledWith(creds, expect.any(Function)); + }); + + it('throws when no active project', async () => { + mockProjectEngine.getActiveProject.mockResolvedValueOnce(null); + await expect(adapter.uploadSite(creds)).rejects.toThrow('No active project'); + }); +}); diff --git a/tests/engine/mainProcessPythonApiInvoker.test.ts b/tests/engine/mainProcessPythonApiInvoker.test.ts index 39c166a..2c6f8a0 100644 --- a/tests/engine/mainProcessPythonApiInvoker.test.ts +++ b/tests/engine/mainProcessPythonApiInvoker.test.ts @@ -104,6 +104,29 @@ const mockTaskManager: Record> = { clearCompletedTasks: vi.fn().mockResolvedValue(undefined), }; +const mockGitApiAdapter: Record> = { + checkAvailability: vi.fn().mockResolvedValue({ gitFound: true }), + getRepoState: vi.fn().mockResolvedValue({ isRepo: true }), + getStatus: vi.fn().mockResolvedValue({ files: [], counts: {} }), + getHistory: vi.fn().mockResolvedValue([]), + getRemoteState: vi.fn().mockResolvedValue({ hasUpstream: false }), + fetch: vi.fn().mockResolvedValue({ success: true }), + pull: vi.fn().mockResolvedValue({ success: true }), + push: vi.fn().mockResolvedValue({ success: true }), + commitAll: vi.fn().mockResolvedValue({ success: true }), +}; + +const mockPublishApiAdapter: Record> = { + uploadSite: vi.fn().mockResolvedValue({ htmlFilesUploaded: 0, thumbnailFilesUploaded: 0, mediaFilesUploaded: 0, filesSkipped: 0 }), +}; + +const mockAppApiAdapter: Record> = { + getDataPaths: vi.fn().mockResolvedValue({ database: '/db', posts: '/posts', media: '/media' }), + getSystemLanguage: vi.fn().mockResolvedValue('en-US'), + getDefaultProjectPath: vi.fn().mockResolvedValue('/path'), + readProjectMetadata: vi.fn().mockResolvedValue(null), +}; + // ── Override ENGINE_MAP for testing ──────────────────────────────── const originalEngineMap: Record = {}; @@ -123,6 +146,9 @@ describe('invokeMainProcessPythonApi', () => { ENGINE_MAP.tags = () => mockTagEngine as Record unknown>; ENGINE_MAP.scripts = () => mockScriptEngine as Record unknown>; ENGINE_MAP.tasks = () => mockTaskManager as Record unknown>; + ENGINE_MAP.sync = () => mockGitApiAdapter as Record unknown>; + ENGINE_MAP.publish = () => mockPublishApiAdapter as Record unknown>; + ENGINE_MAP.app = () => mockAppApiAdapter as Record unknown>; }); afterEach(() => { @@ -203,6 +229,58 @@ describe('invokeMainProcessPythonApi', () => { const result = await invokeMainProcessPythonApi('posts.get', { postId: 'p1' }); expect(result).toEqual({ id: 'p1', title: 'Found' }); }); + + // ── Sync (git) routing ──────────────────────────────────────── + + it('routes sync.checkAvailability to GitApiAdapter.checkAvailability', async () => { + await invokeMainProcessPythonApi('sync.checkAvailability', {}); + expect(mockGitApiAdapter.checkAvailability).toHaveBeenCalledWith(); + }); + + it('routes sync.getRepoState to GitApiAdapter.getRepoState', async () => { + await invokeMainProcessPythonApi('sync.getRepoState', {}); + expect(mockGitApiAdapter.getRepoState).toHaveBeenCalledWith(); + }); + + it('routes sync.commitAll to GitApiAdapter.commitAll with message', async () => { + await invokeMainProcessPythonApi('sync.commitAll', { message: 'update files' }); + expect(mockGitApiAdapter.commitAll).toHaveBeenCalledWith('update files'); + }); + + it('routes sync.getHistory with optional limit', async () => { + await invokeMainProcessPythonApi('sync.getHistory', { limit: 5 }); + expect(mockGitApiAdapter.getHistory).toHaveBeenCalledWith(5); + }); + + // ── Publish routing ──────────────────────────────────────── + + it('routes publish.uploadSite to PublishApiAdapter.uploadSite', async () => { + const creds = { sshHost: 'example.com', sshUser: 'deploy', sshRemotePath: '/var/www', sshMode: 'rsync' }; + await invokeMainProcessPythonApi('publish.uploadSite', { credentials: creds }); + expect(mockPublishApiAdapter.uploadSite).toHaveBeenCalledWith(creds); + }); + + // ── App routing ──────────────────────────────────────── + + it('routes app.getDataPaths to AppApiAdapter.getDataPaths', async () => { + await invokeMainProcessPythonApi('app.getDataPaths', {}); + expect(mockAppApiAdapter.getDataPaths).toHaveBeenCalledWith(); + }); + + it('routes app.getDefaultProjectPath to AppApiAdapter', async () => { + await invokeMainProcessPythonApi('app.getDefaultProjectPath', { projectId: 'p1' }); + expect(mockAppApiAdapter.getDefaultProjectPath).toHaveBeenCalledWith('p1'); + }); + + it('routes app.readProjectMetadata to AppApiAdapter', async () => { + await invokeMainProcessPythonApi('app.readProjectMetadata', { folderPath: '/some/path' }); + expect(mockAppApiAdapter.readProjectMetadata).toHaveBeenCalledWith('/some/path'); + }); + + it('routes app.getSystemLanguage to AppApiAdapter', async () => { + await invokeMainProcessPythonApi('app.getSystemLanguage', {}); + expect(mockAppApiAdapter.getSystemLanguage).toHaveBeenCalledWith(); + }); }); // ── Unknown/unsupported methods ────────────────────────────────── @@ -244,13 +322,7 @@ describe('invokeMainProcessPythonApi', () => { 'app.triggerMenuAction', 'app.getBlogmarkBookmarklet', 'app.copyToClipboard', - 'chat.sendMessage', - 'chat.abortMessage', - 'chat.analyzeTaxonomy', - 'chat.analyzeMediaImage', - 'sync.configure', - 'sync.start', - 'sync.stopAutoSync', + 'app.setPreviewPostTarget', ]; for (const method of unsafeMethods) { diff --git a/tests/renderer/python/generateApiDocumentationMarkdownV1.test.ts b/tests/renderer/python/generateApiDocumentationMarkdownV1.test.ts index 2d310a5..d1578f4 100644 --- a/tests/renderer/python/generateApiDocumentationMarkdownV1.test.ts +++ b/tests/renderer/python/generateApiDocumentationMarkdownV1.test.ts @@ -27,14 +27,18 @@ describe('generateApiDocumentationMarkdownV1', () => { expect(markdown).toContain('[↑ Back to Table of contents](#table-of-contents)'); }); - it('documents chat APIs in a dedicated module section', () => { + it('documents sync and publish APIs in dedicated module sections', () => { const markdown = generateApiDocumentationMarkdownV1(); - expect(markdown).toContain('## chat'); - expect(markdown).toContain('### chat.getConversations'); - expect(markdown).toContain('### chat.sendMessage'); - expect(markdown).toContain('- [chat](#chat)'); - expect(markdown).toContain('- [chat.sendMessage](#chatsendmessage)'); + expect(markdown).toContain('## sync'); + expect(markdown).toContain('### sync.getRepoState'); + expect(markdown).toContain('### sync.commitAll'); + expect(markdown).toContain('- [sync](#sync)'); + expect(markdown).toContain('## publish'); + expect(markdown).toContain('### publish.uploadSite'); + expect(markdown).toContain('- [publish](#publish)'); + // chat namespace should not be present + expect(markdown).not.toContain('## chat'); }); it('includes a dedicated Data Structures section with core object shapes', () => { diff --git a/tests/renderer/python/pythonApiContractV1.test.ts b/tests/renderer/python/pythonApiContractV1.test.ts index 699fa46..a22ccc8 100644 --- a/tests/renderer/python/pythonApiContractV1.test.ts +++ b/tests/renderer/python/pythonApiContractV1.test.ts @@ -23,8 +23,9 @@ describe('pythonApiContractV1', () => { 'scripts.getAll', 'tasks.getAll', 'app.getSystemLanguage', - 'chat.getConversations', - 'chat.sendMessage', + 'sync.getRepoState', + 'sync.commitAll', + 'publish.uploadSite', ])); }); @@ -43,34 +44,30 @@ describe('pythonApiContractV1', () => { }); }); - it('documents chat.sendMessage return contract and metadata input', () => { - expect(getPythonApiMethodContract('chat.sendMessage')).toEqual({ - method: 'chat.sendMessage', - description: 'Send message to chat conversation.', + it('documents sync.commitAll contract with required message param', () => { + expect(getPythonApiMethodContract('sync.commitAll')).toEqual({ + method: 'sync.commitAll', + description: 'Stage all changes and commit for active project.', params: [ - { - name: 'conversationId', - type: 'string', - required: true, - }, { name: 'message', type: 'string', required: true, }, - { - name: 'metadata', - type: 'object', - required: false, - }, ], - returns: '{ success: boolean; message?: string; error?: string }', + returns: 'GitActionResult', }); }); + it('does not include chat namespace (removed in v1.7.0)', () => { + const methodNames = listPythonApiMethodNames(); + const chatMethods = methodNames.filter((m) => m.startsWith('chat.')); + expect(chatMethods).toHaveLength(0); + }); + it('contains semantic version metadata for compatibility checks', () => { expect(BDS_PYTHON_API_CONTRACT_V1).toMatchObject({ - version: '1.6.0', + version: '1.7.0', generatedAt: expect.any(String), }); }); @@ -93,15 +90,17 @@ describe('generatePythonApiModuleV1', () => { expect(moduleCode).toContain('class PostsApi:'); expect(moduleCode).toContain('class MediaApi:'); expect(moduleCode).toContain('class MetaApi:'); - expect(moduleCode).toContain('class ChatApi:'); + expect(moduleCode).toContain('class SyncApi:'); + expect(moduleCode).toContain('class PublishApi:'); expect(moduleCode).toContain('async def get(self, post_id):'); expect(moduleCode).toContain('async def get_all(self, options=None):'); expect(moduleCode).toContain('async def search(self, query):'); expect(moduleCode).toContain('async def get_project_metadata(self):'); - expect(moduleCode).toContain('async def get_conversations(self):'); - expect(moduleCode).toContain('async def send_message(self, conversation_id, message, metadata=None):'); + expect(moduleCode).toContain('async def commit_all(self, message):'); + expect(moduleCode).toContain('async def upload_site(self, credentials):'); expect(moduleCode).toContain('class BdsApi:'); expect(moduleCode).toContain('bds = BdsApi(_transport)'); + expect(moduleCode).not.toContain('class ChatApi:'); }); it('escapes python keyword method names to valid identifiers', () => { diff --git a/tests/setup.ts b/tests/setup.ts index 88b5601..cef0aba 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -87,13 +87,15 @@ Object.defineProperty(globalThis, 'window', { getFilePath: vi.fn(), }, sync: { - configure: vi.fn(), - start: vi.fn(), + checkAvailability: vi.fn(), + getRepoState: vi.fn(), getStatus: vi.fn(), - isConfigured: vi.fn(), - getPendingCount: vi.fn(), - getLog: vi.fn(), - stopAutoSync: vi.fn(), + getHistory: vi.fn(), + getRemoteState: vi.fn(), + fetch: vi.fn(), + pull: vi.fn(), + push: vi.fn(), + commitAll: vi.fn(), }, dropbox: { configure: vi.fn(),