fix: next round of cleanups
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Python Scripting — Remaining Work (Implementation-First)
|
# 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**.
|
This document is intentionally reduced to **what is still left to implement**.
|
||||||
When plan and code differ, code is the source of truth.
|
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.
|
- **`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`).
|
- **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.
|
- **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.
|
||||||
|
|||||||
56
src/main/engine/AppApiAdapter.ts
Normal file
56
src/main/engine/AppApiAdapter.ts
Normal file
@@ -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<string> {
|
||||||
|
return app.getLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefaultProjectPath(projectId: string): Promise<string> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
80
src/main/engine/GitApiAdapter.ts
Normal file
80
src/main/engine/GitApiAdapter.ts
Normal file
@@ -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<string> {
|
||||||
|
const project = await getProjectEngine().getActiveProject();
|
||||||
|
if (!project?.dataPath) {
|
||||||
|
throw new Error('No active project with a data path');
|
||||||
|
}
|
||||||
|
return project.dataPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAvailability(): Promise<GitAvailability> {
|
||||||
|
return getGitEngine().checkAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRepoState(): Promise<RepoState> {
|
||||||
|
const projectPath = await this.resolveProjectPath();
|
||||||
|
return getGitEngine().getRepoState(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus(): Promise<GitStatusDto> {
|
||||||
|
const projectPath = await this.resolveProjectPath();
|
||||||
|
return getGitEngine().getStatus(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHistory(limit?: number): Promise<GitHistoryEntry[]> {
|
||||||
|
const projectPath = await this.resolveProjectPath();
|
||||||
|
return getGitEngine().getHistory(projectPath, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRemoteState(): Promise<GitRemoteStateDto> {
|
||||||
|
const projectPath = await this.resolveProjectPath();
|
||||||
|
return getGitEngine().getRemoteState(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(): Promise<GitActionResult> {
|
||||||
|
const projectPath = await this.resolveProjectPath();
|
||||||
|
return getGitEngine().fetch(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pull(): Promise<GitActionResult> {
|
||||||
|
const projectPath = await this.resolveProjectPath();
|
||||||
|
return getGitEngine().pull(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async push(): Promise<GitActionResult> {
|
||||||
|
const projectPath = await this.resolveProjectPath();
|
||||||
|
return getGitEngine().push(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitAll(message: string): Promise<GitActionResult> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
73
src/main/engine/PublishApiAdapter.ts
Normal file
73
src/main/engine/PublishApiAdapter.ts
Normal file
@@ -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<PublishSiteResult> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -94,6 +94,18 @@ export const ENGINE_MAP: Record<string, EngineGetter> = {
|
|||||||
const { taskManager } = require('../engine/TaskManager');
|
const { taskManager } = require('../engine/TaskManager');
|
||||||
return 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
|
// Map API method names to engine method names where they differ
|
||||||
@@ -199,10 +211,7 @@ export async function invokeMainProcessPythonApi(method: string, args: Record<st
|
|||||||
'media.importDialog', 'media.replaceFileDialog', 'media.getFilePath',
|
'media.importDialog', 'media.replaceFileDialog', 'media.getFilePath',
|
||||||
'app.openFolder', 'app.selectFolder', 'app.showItemInFolder',
|
'app.openFolder', 'app.selectFolder', 'app.showItemInFolder',
|
||||||
'app.getTitleBarMetrics', 'app.notifyRendererReady', 'app.triggerMenuAction',
|
'app.getTitleBarMetrics', 'app.notifyRendererReady', 'app.triggerMenuAction',
|
||||||
'app.getBlogmarkBookmarklet', 'app.copyToClipboard',
|
'app.getBlogmarkBookmarklet', 'app.copyToClipboard', 'app.setPreviewPostTarget',
|
||||||
'chat.sendMessage', 'chat.abortMessage', 'chat.analyzeTaxonomy',
|
|
||||||
'chat.analyzeMediaImage',
|
|
||||||
'sync.configure', 'sync.start', 'sync.stopAutoSync',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (unsafeMethods.has(method)) {
|
if (unsafeMethods.has(method)) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getTagEngine } from '../engine/TagEngine';
|
|||||||
import { getPostMediaEngine } from '../engine/PostMediaEngine';
|
import { getPostMediaEngine } from '../engine/PostMediaEngine';
|
||||||
import { getScriptEngine, type CreateScriptInput, type UpdateScriptInput } from '../engine/ScriptEngine';
|
import { getScriptEngine, type CreateScriptInput, type UpdateScriptInput } from '../engine/ScriptEngine';
|
||||||
import { getGitEngine } from '../engine/GitEngine';
|
import { getGitEngine } from '../engine/GitEngine';
|
||||||
|
import { getGitApiAdapter } from '../engine/GitApiAdapter';
|
||||||
import { taskManager, TaskProgress } from '../engine/TaskManager';
|
import { taskManager, TaskProgress } from '../engine/TaskManager';
|
||||||
import { getDatabase } from '../database';
|
import { getDatabase } from '../database';
|
||||||
import { media } from '../database/schema';
|
import { media } from '../database/schema';
|
||||||
@@ -811,6 +812,44 @@ export function registerIpcHandlers(): void {
|
|||||||
return taskManager.clearCompletedTasks();
|
return taskManager.clearCompletedTasks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============ Sync Handlers (git operations via GitApiAdapter) ============
|
||||||
|
|
||||||
|
safeHandle('sync:checkAvailability', async () => {
|
||||||
|
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 ============
|
// ============ App Handlers ============
|
||||||
|
|
||||||
safeHandle('app:getDataPaths', async () => {
|
safeHandle('app:getDataPaths', async () => {
|
||||||
|
|||||||
@@ -127,15 +127,17 @@ export const electronAPI: ElectronAPI = {
|
|||||||
rebuild: () => ipcRenderer.invoke('postMedia:rebuild'),
|
rebuild: () => ipcRenderer.invoke('postMedia:rebuild'),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Sync
|
// Sync (git operations via GitApiAdapter)
|
||||||
sync: {
|
sync: {
|
||||||
configure: (config: unknown) => ipcRenderer.invoke('sync:configure', config),
|
checkAvailability: () => ipcRenderer.invoke('sync:checkAvailability'),
|
||||||
start: (direction?: 'push' | 'pull' | 'bidirectional') => ipcRenderer.invoke('sync:start', direction),
|
getRepoState: () => ipcRenderer.invoke('sync:getRepoState'),
|
||||||
getStatus: () => ipcRenderer.invoke('sync:getStatus'),
|
getStatus: () => ipcRenderer.invoke('sync:getStatus'),
|
||||||
isConfigured: () => ipcRenderer.invoke('sync:isConfigured'),
|
getHistory: (limit?: number) => ipcRenderer.invoke('sync:getHistory', limit),
|
||||||
getPendingCount: () => ipcRenderer.invoke('sync:getPendingCount'),
|
getRemoteState: () => ipcRenderer.invoke('sync:getRemoteState'),
|
||||||
getLog: (limit?: number) => ipcRenderer.invoke('sync:getLog', limit),
|
fetch: () => ipcRenderer.invoke('sync:fetch'),
|
||||||
stopAutoSync: () => ipcRenderer.invoke('sync:stopAutoSync'),
|
pull: () => ipcRenderer.invoke('sync:pull'),
|
||||||
|
push: () => ipcRenderer.invoke('sync:push'),
|
||||||
|
commitAll: (message: string) => ipcRenderer.invoke('sync:commitAll', message),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
|
|||||||
@@ -164,18 +164,6 @@ export interface TaskProgress {
|
|||||||
groupName?: string;
|
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 {
|
export interface PaginatedPostsResult {
|
||||||
items: PostData[];
|
items: PostData[];
|
||||||
@@ -607,13 +595,15 @@ export interface ElectronAPI {
|
|||||||
rebuild: () => Promise<void>;
|
rebuild: () => Promise<void>;
|
||||||
};
|
};
|
||||||
sync: {
|
sync: {
|
||||||
configure: (config: SyncConfig) => Promise<void>;
|
checkAvailability: () => Promise<{ gitFound: boolean; version?: string }>;
|
||||||
start: (direction?: 'push' | 'pull' | 'bidirectional') => Promise<SyncResult>;
|
getRepoState: () => Promise<{ isRepo: boolean; rootPath?: string; currentBranch?: string; hasRemote: boolean }>;
|
||||||
getStatus: () => Promise<'idle' | 'syncing' | 'error'>;
|
getStatus: () => Promise<{ files: Array<{ path: string; status: string; previousPath?: string }>; counts: { untracked: number; modified: number; deleted: number; renamed: number; staged: number } }>;
|
||||||
isConfigured: () => Promise<boolean>;
|
getHistory: (limit?: number) => Promise<Array<{ hash: string; shortHash: string; date: string; subject: string; author: string }>>;
|
||||||
getPendingCount: () => Promise<{ posts: number; media: number }>;
|
getRemoteState: () => Promise<{ localBranch: string | null; upstreamBranch: string | null; hasUpstream: boolean; ahead: number; behind: number }>;
|
||||||
getLog: (limit?: number) => Promise<unknown[]>;
|
fetch: () => Promise<{ success: boolean; code?: string; error?: string; guidance?: string[] }>;
|
||||||
stopAutoSync: () => Promise<void>;
|
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: {
|
tasks: {
|
||||||
getAll: () => Promise<TaskProgress[]>;
|
getAll: () => Promise<TaskProgress[]>;
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ const optionalNumber = (name: string): PythonApiParamContractV1 => ({ name, type
|
|||||||
const requiredObject = (name: string): PythonApiParamContractV1 => ({ name, type: 'object', required: true });
|
const requiredObject = (name: string): PythonApiParamContractV1 => ({ name, type: 'object', required: true });
|
||||||
const optionalObject = (name: string): PythonApiParamContractV1 => ({ name, type: 'object', required: false });
|
const optionalObject = (name: string): PythonApiParamContractV1 => ({ name, type: 'object', required: false });
|
||||||
const requiredArray = (name: string): PythonApiParamContractV1 => ({ name, type: 'array', required: true });
|
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 });
|
const requiredStringOrNull = (name: string): PythonApiParamContractV1 => ({ name, type: 'stringOrNull', required: true });
|
||||||
|
|
||||||
function method(
|
function method(
|
||||||
@@ -172,34 +171,23 @@ const METHODS_V1: PythonApiMethodContractV1[] = [
|
|||||||
method('tags.getPostsWithTag', 'Get posts using a tag.', [requiredString('tagId')], 'string[]'),
|
method('tags.getPostsWithTag', 'Get posts using a tag.', [requiredString('tagId')], 'string[]'),
|
||||||
method('tags.syncFromPosts', 'Sync tag index from posts.', [], 'SyncTagsResult'),
|
method('tags.syncFromPosts', 'Sync tag index from posts.', [], 'SyncTagsResult'),
|
||||||
|
|
||||||
method('chat.checkReady', 'Check chat backend readiness.', [], 'ChatReadyStatus'),
|
// NOTE: chat namespace intentionally excluded from Python API.
|
||||||
method('chat.validateApiKey', 'Validate chat API key and list available models.', [requiredString('apiKey')], '{ isValid: boolean; models: ChatModel[] }'),
|
// AI/chat features (sendMessage, analyzeTaxonomy, analyzeMediaImage, etc.) are
|
||||||
method('chat.setApiKey', 'Store chat API key.', [requiredString('apiKey')], '{ success: boolean; error?: string }'),
|
// expensive external API calls that require user oversight and interactive streaming.
|
||||||
method('chat.getApiKey', 'Get stored chat API key status.', [], 'ChatApiKeyStatus'),
|
// This namespace can be re-added in a future version if AI-from-Python becomes a
|
||||||
method('chat.getAvailableModels', 'Get available chat models and selected default.', [], '{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }'),
|
// supported use case with proper rate limiting and cost controls.
|
||||||
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<string, string>; tagMappings?: Record<string, string>; 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.checkAvailability', 'Check if git is available.', [], 'GitAvailability'),
|
||||||
method('sync.start', 'Start sync operation.', [optionalString('direction')], 'SyncResult'),
|
method('sync.getRepoState', 'Get repository state for active project.', [], 'RepoState'),
|
||||||
method('sync.getStatus', 'Get sync status.', [], "'idle' | 'syncing' | 'error'"),
|
method('sync.getStatus', 'Get working tree status for active project.', [], 'GitStatusDto'),
|
||||||
method('sync.isConfigured', 'Check if sync is configured.', [], 'boolean'),
|
method('sync.getHistory', 'Get commit history for active project.', [optionalNumber('limit')], 'GitHistoryEntry[]'),
|
||||||
method('sync.getPendingCount', 'Get pending sync item count.', [], '{ posts: number; media: number }'),
|
method('sync.getRemoteState', 'Get remote tracking state for active project.', [], 'GitRemoteStateDto'),
|
||||||
method('sync.getLog', 'Get sync log.', [optionalNumber('limit')], 'unknown[]'),
|
method('sync.fetch', 'Fetch from remote for active project.', [], 'GitActionResult'),
|
||||||
method('sync.stopAutoSync', 'Stop automatic sync.', [], 'void'),
|
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[] = [
|
const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
|
||||||
@@ -310,60 +298,67 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ChatConversation',
|
name: 'GitAvailability',
|
||||||
description: 'Chat conversation container.',
|
description: 'Git installation availability check result.',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'id', type: 'string', required: true, description: 'Unique conversation identifier.' },
|
{ name: 'gitFound', type: 'boolean', required: true, description: 'Whether git executable was found.' },
|
||||||
{ name: 'title', type: 'string', required: true, description: 'Conversation title.' },
|
{ name: 'version', type: 'string', required: false, description: 'Git version string when available.' },
|
||||||
{ 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',
|
name: 'RepoState',
|
||||||
description: 'Single message entry in a conversation history.',
|
description: 'Repository state for the active project.',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'id', type: 'string', required: true, description: 'Unique message identifier.' },
|
{ name: 'isRepo', type: 'boolean', required: true, description: 'Whether the project directory is a git repository.' },
|
||||||
{ name: 'conversationId', type: 'string', required: true, description: 'Owning conversation id.' },
|
{ name: 'rootPath', type: 'string', required: false, description: 'Repository root path.' },
|
||||||
{ name: 'role', type: "'user' | 'assistant' | 'system' | 'tool'", required: true, description: 'Message author role.' },
|
{ name: 'currentBranch', type: 'string', required: false, description: 'Current branch name.' },
|
||||||
{ name: 'content', type: 'string', required: true, description: 'Message text content.' },
|
{ name: 'hasRemote', type: 'boolean', required: true, description: 'Whether a remote is configured.' },
|
||||||
{ 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',
|
name: 'GitStatusDto',
|
||||||
description: 'Available chat model descriptor.',
|
description: 'Working tree status with file list and counts.',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'id', type: 'string', required: true, description: 'Model identifier.' },
|
{ name: 'files', type: 'Array<{ path: string; status: string; previousPath?: string }>', required: true, description: 'List of changed files with status.' },
|
||||||
{ name: 'name', type: 'string', required: true, description: 'Human-readable model name.' },
|
{ name: 'counts', type: '{ untracked: number; modified: number; deleted: number; renamed: number; staged: number }', required: true, description: 'Counts by change type.' },
|
||||||
{ name: 'provider', type: 'string', required: false, description: 'Model provider name.' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ChatReadyStatus',
|
name: 'GitRemoteStateDto',
|
||||||
description: 'Chat backend readiness status.',
|
description: 'Remote tracking state for the active project branch.',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'ready', type: 'boolean', required: true, description: 'Whether chat backend is ready.' },
|
{ name: 'localBranch', type: 'string | null', required: true, description: 'Local branch name.' },
|
||||||
{ name: 'error', type: 'string', required: false, description: 'Error description when not ready.' },
|
{ name: 'upstreamBranch', type: 'string | null', required: true, description: 'Upstream tracking branch name.' },
|
||||||
{ name: 'backend', type: 'string', required: false, description: 'Selected backend identifier.' },
|
{ 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',
|
name: 'GitActionResult',
|
||||||
description: 'Stored API key state for chat provider.',
|
description: 'Result from a git operation (fetch, pull, push, commit).',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'hasKey', type: 'boolean', required: true, description: 'Whether a key is configured.' },
|
{ name: 'success', type: 'boolean', required: true, description: 'Whether the operation succeeded.' },
|
||||||
{ name: 'maskedKey', type: 'string', required: true, description: 'Masked key representation for UI display.' },
|
{ 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 = {
|
export const BDS_PYTHON_API_CONTRACT_V1: PythonApiContractV1 = {
|
||||||
version: '1.6.0',
|
version: '1.7.0',
|
||||||
generatedAt: '2026-02-25T00:00:00.000Z',
|
generatedAt: '2026-02-27T00:00:00.000Z',
|
||||||
methods: METHODS_V1,
|
methods: METHODS_V1,
|
||||||
dataStructures: DATA_STRUCTURES_V1,
|
dataStructures: DATA_STRUCTURES_V1,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export declare function generatePythonApiModuleV1(): string;
|
|
||||||
//# sourceMappingURL=generatePythonApiModuleV1.d.ts.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"generatePythonApiModuleV1.d.ts","sourceRoot":"","sources":["generatePythonApiModuleV1.ts"],"names":[],"mappings":"AAyHA,wBAAgB,yBAAyB,IAAI,MAAM,CA+DlD"}
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"generatePythonApiModuleV1.js","sourceRoot":"","sources":["generatePythonApiModuleV1.ts"],"names":[],"mappings":";;AAyHA,8DA+DC;AAxLD,+DAAmE;AAEnE,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC;IACvC,OAAO;IACP,MAAM;IACN,MAAM;IACN,KAAK;IACL,IAAI;IACJ,QAAQ;IACR,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,UAAU;IACV,KAAK;IACL,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,SAAS;IACT,KAAK;IACL,MAAM;IACN,QAAQ;IACR,IAAI;IACJ,QAAQ;IACR,IAAI;IACJ,IAAI;IACJ,QAAQ;IACR,UAAU;IACV,KAAK;IACL,IAAI;IACJ,MAAM;IACN,OAAO;IACP,QAAQ;IACR,KAAK;IACL,OAAO;IACP,MAAM;IACN,OAAO;IACP,OAAO;IACP,MAAM;CACP,CAAC,CAAC;AAEH,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK;SACT,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC;SAC9B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,WAAW,EAAE,CAAC;AACnB,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa;IACvC,IAAI,UAAU,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,UAAU,GAAG,GAAG,CAAC;IACnB,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,UAAU,GAAG,IAAI,UAAU,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,wBAAwB,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7C,UAAU,GAAG,GAAG,UAAU,GAAG,CAAC;IAChC,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,iBAAiB,CAAC,MAI1B;IACC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrD,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,gBAAgB,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjD,UAAU,EAAE,KAAK,CAAC,IAAI;QACtB,UAAU,EAAE,kBAAkB,CAAC,KAAK,CAAC,IAAI,CAAC;QAC1C,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,CAAC,CAAC,CAAC;IAEJ,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC;QACvC,CAAC,CAAC,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,UAAU,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACjH,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QACvC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,YAAY,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QAChH,CAAC,CAAC,IAAI,CAAC;IAET,OAAO;QACL,iBAAiB,gBAAgB,QAAQ,SAAS,IAAI;QACtD,iBAAiB,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ;QACxD,8CAA8C,MAAM,CAAC,MAAM,MAAM,QAAQ,GAAG;QAC5E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,yBAAyB,CAChC,SAAiB,EACjB,OAA2G;IAE3G,MAAM,SAAS,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IAC1E,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEjF,OAAO;QACL,SAAS,SAAS,GAAG;QACrB,oCAAoC;QACpC,qCAAqC;QACrC,EAAE;QACF,YAAY,CAAC,OAAO,EAAE;QACtB,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAgB,yBAAyB;IACvC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA8G,CAAC;IAE3I,KAAK,MAAM,MAAM,IAAI,gDAA0B,CAAC,OAAO,EAAE,CAAC;QACxD,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC;YACX,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACpC,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB,CAAC,CAAC;SACJ,CAAC,CAAC;QACH,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,eAAe,GAAG,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;SACvD,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;SACpD,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,yBAAyB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;SAC5E,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,oBAAoB,GAAG,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;SACzD,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;SAChD,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,kBAAkB,CAAC,SAAS,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC;SACtI,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,OAAO;QACL,kDAAkD;QAClD,uBAAuB,gDAA0B,CAAC,OAAO,EAAE;QAC3D,EAAE;QACF,aAAa;QACb,EAAE;QACF,+BAA+B;QAC/B,UAAU;QACV,EAAE;QACF,eAAe,CAAC,OAAO,EAAE;QACzB,EAAE;QACF,eAAe;QACf,oCAAoC;QACpC,qCAAqC;QACrC,oBAAoB;QACpB,EAAE;QACF,mBAAmB;QACnB,oCAAoC;QACpC,qCAAqC;QACrC,EAAE;QACF,yCAAyC;QACzC,sEAAsE;QACtE,oDAAoD;QACpD,yBAAyB;QACzB,uCAAuC;QACvC,EAAE;QACF,iCAAiC;QACjC,wCAAwC;QACxC,8BAA8B;QAC9B,gBAAgB;QAChB,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
||||||
41
src/renderer/python/pythonApiContractV1.d.ts
vendored
41
src/renderer/python/pythonApiContractV1.d.ts
vendored
@@ -1,41 +0,0 @@
|
|||||||
import type { ElectronAPI } from '../../main/shared/electronApi';
|
|
||||||
type PythonPromiseMethodPath = {
|
|
||||||
[Group in keyof ElectronAPI]: ElectronAPI[Group] extends Record<string, (...args: never[]) => unknown> ? {
|
|
||||||
[Method in keyof ElectronAPI[Group]]: ElectronAPI[Group][Method] extends (...args: never[]) => Promise<unknown> ? `${Extract<Group, string>}.${Extract<Method, string>}` : 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
|
|
||||||
@@ -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"}
|
|
||||||
@@ -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<string, string> | 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<string, string>; tagMappings?: Record<string, string>; 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
|
|
||||||
File diff suppressed because one or more lines are too long
87
tests/engine/AppApiAdapter.test.ts
Normal file
87
tests/engine/AppApiAdapter.test.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
98
tests/engine/GitApiAdapter.test.ts
Normal file
98
tests/engine/GitApiAdapter.test.ts
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
80
tests/engine/PublishApiAdapter.test.ts
Normal file
80
tests/engine/PublishApiAdapter.test.ts
Normal file
@@ -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<unknown> }) => {
|
||||||
|
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<unknown> }) => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -104,6 +104,29 @@ const mockTaskManager: Record<string, ReturnType<typeof vi.fn>> = {
|
|||||||
clearCompletedTasks: vi.fn().mockResolvedValue(undefined),
|
clearCompletedTasks: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockGitApiAdapter: Record<string, ReturnType<typeof vi.fn>> = {
|
||||||
|
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<string, ReturnType<typeof vi.fn>> = {
|
||||||
|
uploadSite: vi.fn().mockResolvedValue({ htmlFilesUploaded: 0, thumbnailFilesUploaded: 0, mediaFilesUploaded: 0, filesSkipped: 0 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockAppApiAdapter: Record<string, ReturnType<typeof vi.fn>> = {
|
||||||
|
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 ────────────────────────────────
|
// ── Override ENGINE_MAP for testing ────────────────────────────────
|
||||||
|
|
||||||
const originalEngineMap: Record<string, typeof ENGINE_MAP[string]> = {};
|
const originalEngineMap: Record<string, typeof ENGINE_MAP[string]> = {};
|
||||||
@@ -123,6 +146,9 @@ describe('invokeMainProcessPythonApi', () => {
|
|||||||
ENGINE_MAP.tags = () => mockTagEngine as Record<string, (...args: unknown[]) => unknown>;
|
ENGINE_MAP.tags = () => mockTagEngine as Record<string, (...args: unknown[]) => unknown>;
|
||||||
ENGINE_MAP.scripts = () => mockScriptEngine as Record<string, (...args: unknown[]) => unknown>;
|
ENGINE_MAP.scripts = () => mockScriptEngine as Record<string, (...args: unknown[]) => unknown>;
|
||||||
ENGINE_MAP.tasks = () => mockTaskManager as Record<string, (...args: unknown[]) => unknown>;
|
ENGINE_MAP.tasks = () => mockTaskManager as Record<string, (...args: unknown[]) => unknown>;
|
||||||
|
ENGINE_MAP.sync = () => mockGitApiAdapter as Record<string, (...args: unknown[]) => unknown>;
|
||||||
|
ENGINE_MAP.publish = () => mockPublishApiAdapter as Record<string, (...args: unknown[]) => unknown>;
|
||||||
|
ENGINE_MAP.app = () => mockAppApiAdapter as Record<string, (...args: unknown[]) => unknown>;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -203,6 +229,58 @@ describe('invokeMainProcessPythonApi', () => {
|
|||||||
const result = await invokeMainProcessPythonApi('posts.get', { postId: 'p1' });
|
const result = await invokeMainProcessPythonApi('posts.get', { postId: 'p1' });
|
||||||
expect(result).toEqual({ id: 'p1', title: 'Found' });
|
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 ──────────────────────────────────
|
// ── Unknown/unsupported methods ──────────────────────────────────
|
||||||
@@ -244,13 +322,7 @@ describe('invokeMainProcessPythonApi', () => {
|
|||||||
'app.triggerMenuAction',
|
'app.triggerMenuAction',
|
||||||
'app.getBlogmarkBookmarklet',
|
'app.getBlogmarkBookmarklet',
|
||||||
'app.copyToClipboard',
|
'app.copyToClipboard',
|
||||||
'chat.sendMessage',
|
'app.setPreviewPostTarget',
|
||||||
'chat.abortMessage',
|
|
||||||
'chat.analyzeTaxonomy',
|
|
||||||
'chat.analyzeMediaImage',
|
|
||||||
'sync.configure',
|
|
||||||
'sync.start',
|
|
||||||
'sync.stopAutoSync',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const method of unsafeMethods) {
|
for (const method of unsafeMethods) {
|
||||||
|
|||||||
@@ -27,14 +27,18 @@ describe('generateApiDocumentationMarkdownV1', () => {
|
|||||||
expect(markdown).toContain('[↑ Back to Table of contents](#table-of-contents)');
|
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();
|
const markdown = generateApiDocumentationMarkdownV1();
|
||||||
|
|
||||||
expect(markdown).toContain('## chat');
|
expect(markdown).toContain('## sync');
|
||||||
expect(markdown).toContain('### chat.getConversations');
|
expect(markdown).toContain('### sync.getRepoState');
|
||||||
expect(markdown).toContain('### chat.sendMessage');
|
expect(markdown).toContain('### sync.commitAll');
|
||||||
expect(markdown).toContain('- [chat](#chat)');
|
expect(markdown).toContain('- [sync](#sync)');
|
||||||
expect(markdown).toContain('- [chat.sendMessage](#chatsendmessage)');
|
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', () => {
|
it('includes a dedicated Data Structures section with core object shapes', () => {
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ describe('pythonApiContractV1', () => {
|
|||||||
'scripts.getAll',
|
'scripts.getAll',
|
||||||
'tasks.getAll',
|
'tasks.getAll',
|
||||||
'app.getSystemLanguage',
|
'app.getSystemLanguage',
|
||||||
'chat.getConversations',
|
'sync.getRepoState',
|
||||||
'chat.sendMessage',
|
'sync.commitAll',
|
||||||
|
'publish.uploadSite',
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,34 +44,30 @@ describe('pythonApiContractV1', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('documents chat.sendMessage return contract and metadata input', () => {
|
it('documents sync.commitAll contract with required message param', () => {
|
||||||
expect(getPythonApiMethodContract('chat.sendMessage')).toEqual({
|
expect(getPythonApiMethodContract('sync.commitAll')).toEqual({
|
||||||
method: 'chat.sendMessage',
|
method: 'sync.commitAll',
|
||||||
description: 'Send message to chat conversation.',
|
description: 'Stage all changes and commit for active project.',
|
||||||
params: [
|
params: [
|
||||||
{
|
|
||||||
name: 'conversationId',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'message',
|
name: 'message',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
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', () => {
|
it('contains semantic version metadata for compatibility checks', () => {
|
||||||
expect(BDS_PYTHON_API_CONTRACT_V1).toMatchObject({
|
expect(BDS_PYTHON_API_CONTRACT_V1).toMatchObject({
|
||||||
version: '1.6.0',
|
version: '1.7.0',
|
||||||
generatedAt: expect.any(String),
|
generatedAt: expect.any(String),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -93,15 +90,17 @@ describe('generatePythonApiModuleV1', () => {
|
|||||||
expect(moduleCode).toContain('class PostsApi:');
|
expect(moduleCode).toContain('class PostsApi:');
|
||||||
expect(moduleCode).toContain('class MediaApi:');
|
expect(moduleCode).toContain('class MediaApi:');
|
||||||
expect(moduleCode).toContain('class MetaApi:');
|
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(self, post_id):');
|
||||||
expect(moduleCode).toContain('async def get_all(self, options=None):');
|
expect(moduleCode).toContain('async def get_all(self, options=None):');
|
||||||
expect(moduleCode).toContain('async def search(self, query):');
|
expect(moduleCode).toContain('async def search(self, query):');
|
||||||
expect(moduleCode).toContain('async def get_project_metadata(self):');
|
expect(moduleCode).toContain('async def get_project_metadata(self):');
|
||||||
expect(moduleCode).toContain('async def get_conversations(self):');
|
expect(moduleCode).toContain('async def commit_all(self, message):');
|
||||||
expect(moduleCode).toContain('async def send_message(self, conversation_id, message, metadata=None):');
|
expect(moduleCode).toContain('async def upload_site(self, credentials):');
|
||||||
expect(moduleCode).toContain('class BdsApi:');
|
expect(moduleCode).toContain('class BdsApi:');
|
||||||
expect(moduleCode).toContain('bds = BdsApi(_transport)');
|
expect(moduleCode).toContain('bds = BdsApi(_transport)');
|
||||||
|
expect(moduleCode).not.toContain('class ChatApi:');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('escapes python keyword method names to valid identifiers', () => {
|
it('escapes python keyword method names to valid identifiers', () => {
|
||||||
|
|||||||
@@ -87,13 +87,15 @@ Object.defineProperty(globalThis, 'window', {
|
|||||||
getFilePath: vi.fn(),
|
getFilePath: vi.fn(),
|
||||||
},
|
},
|
||||||
sync: {
|
sync: {
|
||||||
configure: vi.fn(),
|
checkAvailability: vi.fn(),
|
||||||
start: vi.fn(),
|
getRepoState: vi.fn(),
|
||||||
getStatus: vi.fn(),
|
getStatus: vi.fn(),
|
||||||
isConfigured: vi.fn(),
|
getHistory: vi.fn(),
|
||||||
getPendingCount: vi.fn(),
|
getRemoteState: vi.fn(),
|
||||||
getLog: vi.fn(),
|
fetch: vi.fn(),
|
||||||
stopAutoSync: vi.fn(),
|
pull: vi.fn(),
|
||||||
|
push: vi.fn(),
|
||||||
|
commitAll: vi.fn(),
|
||||||
},
|
},
|
||||||
dropbox: {
|
dropbox: {
|
||||||
configure: vi.fn(),
|
configure: vi.fn(),
|
||||||
|
|||||||
Reference in New Issue
Block a user