feat: add manifest schema, TypeScript types, PluginAPI, RPC client, event schemas, sync schemas

This commit is contained in:
2026-06-16 11:55:47 +08:00
parent 622fcf6625
commit 2f02db00f5
15 changed files with 1913 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
// Verstak Plugin SDK — Public API
export * from './types';
export { VerstakPluginAPI, createPluginAPI } from './plugin-api';
export { RPCServer, RPCClient } from './rpc';
export {
createTestManifest,
createTestPluginState,
createMockPluginAPI,
validateManifest,
} from './test-utils';
+166
View File
@@ -0,0 +1,166 @@
// Verstak Plugin SDK — VerstakPluginAPI
// The official runtime API available to all plugins in the frontend context.
import type { PluginSettings } from './types';
/**
* VerstakPluginAPI — единственный способ для frontend плагина
* общаться с core платформы.
*
* Экземпляр API передаётся плагину при активации через глобальную
* переменную `window.__VERSTAK_PLUGIN_API__`.
*/
export class VerstakPluginAPI {
private pluginId: string;
private capabilities = new Set<string>();
constructor(pluginId: string) {
this.pluginId = pluginId;
}
/**
* Инициализация API — вызывается core после загрузки frontend bundle.
* @internal
*/
_init(capabilities: string[]): void {
this.capabilities = new Set(capabilities);
}
// ─── View Registration ─────────────────────────────────────
/**
* Зарегистрировать view для отображения в UI Shell.
*/
registerView(id: string, component: unknown): void {
this._postMessage('register.view', { id, component });
}
/**
* Зарегистрировать панель настроек плагина.
*/
registerSettingsPanel(id: string, title: string, component: unknown): void {
this._postMessage('register.settingsPanel', { id, title, component });
}
/**
* Зарегистрировать команду для command palette.
*/
registerCommand(id: string, title: string, handler: () => void, keybinding?: string): void {
this._postMessage('register.command', { id, title, keybinding, handler: handler.toString() });
}
/**
* Зарегистрировать действия для файлов.
*/
registerFileAction(id: string, label: string, handler: (filePath: string) => void, capability?: string): void {
this._postMessage('register.fileAction', { id, label, handler: handler.toString(), capability });
}
/**
* Зарегистрировать действия для заметок.
*/
registerNoteAction(id: string, label: string, handler: (noteId: string) => void, capability?: string): void {
this._postMessage('register.noteAction', { id, label, handler: handler.toString(), capability });
}
/**
* Зарегистрировать provider поиска.
*/
registerSearchProvider(id: string, label: string, handler: (query: string) => unknown[]): void {
this._postMessage('register.searchProvider', { id, label, handler: handler.toString() });
}
// ─── Capabilities ──────────────────────────────────────────
/**
* Проверить, доступна ли capability.
*/
hasCapability(name: string): boolean {
return this.capabilities.has(name);
}
/**
* Получить список всех доступных capabilities.
*/
getAvailableCapabilities(): string[] {
return Array.from(this.capabilities);
}
// ─── Backend Communication ─────────────────────────────────
/**
* Вызвать backend метод плагина через RPC.
*/
async callBackend(method: string, args: unknown[] = []): Promise<unknown> {
return this._rpcCall(method, args);
}
// ─── Settings ──────────────────────────────────────────────
/**
* Прочитать настройки плагина.
*/
async readSettings(): Promise<PluginSettings> {
const result = await this._rpcCall('readSettings', []);
return result as PluginSettings;
}
/**
* Записать настройки плагина.
*/
async writeSettings(settings: PluginSettings): Promise<void> {
await this._rpcCall('writeSettings', [settings]);
}
// ─── Event Bus ─────────────────────────────────────────────
/**
* Подписаться на событие event bus.
*/
subscribe(event: string, handler: (payload: unknown) => void): void {
this._postMessage('subscribe', { event, handler: handler.toString() });
}
/**
* Опубликовать событие в event bus.
*/
publish(event: string, payload: unknown): void {
this._postMessage('publish', { event, payload });
}
// ─── Internal ──────────────────────────────────────────────
private _postMessage(type: string, data: Record<string, unknown>): void {
window.dispatchEvent(new CustomEvent('verstak:plugin', {
detail: { pluginId: this.pluginId, type, data }
}));
}
private async _rpcCall(method: string, args: unknown[]): Promise<unknown> {
return new Promise((resolve, reject) => {
const callId = `${this.pluginId}:${Date.now()}:${Math.random()}`;
const handler = (event: CustomEvent) => {
if (event.detail.callId === callId) {
window.removeEventListener('verstak:rpc:response', handler as EventListener);
if (event.detail.error) {
reject(new Error(event.detail.error));
} else {
resolve(event.detail.result);
}
}
};
window.addEventListener('verstak:rpc:response', handler as EventListener);
this._postMessage('rpc', { callId, method, args });
});
}
}
/**
* Создать экземпляр VerstakPluginAPI.
* Core вызывает эту функцию после загрузки frontend bundle,
* передавая pluginId и список доступных capabilities.
*/
export function createPluginAPI(pluginId: string): VerstakPluginAPI {
const api = new VerstakPluginAPI(pluginId);
return api;
}
+112
View File
@@ -0,0 +1,112 @@
// Verstak Plugin SDK — RPC Client for Sidecar Communication
export type RPCTransport = 'stdio' | 'tcp';
export interface RPCRequest {
jsonrpc: '2.0';
id: string;
method: string;
params: unknown[];
}
export interface RPCResponse {
jsonrpc: '2.0';
id: string;
result?: unknown;
error?: RPCError;
}
export interface RPCError {
code: number;
message: string;
data?: unknown;
}
/**
* RPC клиент для общения backend sidecar с core платформы.
* Использует JSON-RPC 2.0 протокол.
*/
export class RPCServer {
private handlers = new Map<string, (params: unknown[]) => Promise<unknown>>();
constructor() {
}
/**
* Зарегистрировать обработчик RPC метода.
*/
registerMethod(method: string, handler: (params: unknown[]) => Promise<unknown>): void {
this.handlers.set(method, handler);
}
/**
* Обработать входящий RPC запрос.
*/
async handleRequest(request: RPCRequest): Promise<RPCResponse> {
const handler = this.handlers.get(request.method);
if (!handler) {
return {
jsonrpc: '2.0',
id: request.id,
error: { code: -32601, message: `Method not found: ${request.method}` }
};
}
try {
const result = await handler(request.params);
return { jsonrpc: '2.0', id: request.id, result };
} catch (err) {
return {
jsonrpc: '2.0',
id: request.id,
error: { code: -32000, message: err instanceof Error ? err.message : String(err) }
};
}
}
/**
* Создать RPC запрос.
*/
createRequest(method: string, params: unknown[] = []): RPCRequest {
return {
jsonrpc: '2.0',
id: `${Date.now()}:${Math.random()}`,
method,
params
};
}
/**
* Разобрать RPC ответ.
*/
parseResponse(data: string): RPCResponse {
return JSON.parse(data) as RPCResponse;
}
}
/**
* RPC клиент (для core, вызывает методы sidecar).
*/
export class RPCClient {
private requestId = 0;
/**
* Создать JSON-RPC запрос.
*/
call(method: string, params: unknown[] = []): string {
const request: RPCRequest = {
jsonrpc: '2.0',
id: `${++this.requestId}`,
method,
params
};
return JSON.stringify(request) + '\n';
}
/**
* Разобрать ответ.
*/
parseResponse(data: string): RPCResponse {
return JSON.parse(data.trim()) as RPCResponse;
}
}
+101
View File
@@ -0,0 +1,101 @@
// Verstak Plugin SDK — Test Utilities
import type { PluginManifest, PluginState } from './types';
/**
* Создать тестовый manifest для unit-тестов.
*/
export function createTestManifest(overrides?: Partial<PluginManifest>): PluginManifest {
return {
schemaVersion: 1,
id: 'test.plugin',
name: 'Test Plugin',
version: '0.1.0',
apiVersion: '1',
description: 'A test plugin for platform verification',
source: 'local',
provides: ['test.capability'],
requires: [],
optionalRequires: [],
permissions: ['events.publish', 'events.subscribe'],
...overrides
};
}
/**
* Создать тестовое состояние плагина.
*/
export function createTestPluginState(overrides?: Partial<PluginState>): PluginState {
return {
id: 'test.plugin',
manifest: createTestManifest(),
status: 'loaded',
enabled: true,
loadedAt: new Date().toISOString(),
...overrides
};
}
/**
* Создать заглушку VerstakPluginAPI для тестов.
*/
export function createMockPluginAPI(): {
registerView: ReturnType<typeof vi.fn>;
registerCommand: ReturnType<typeof vi.fn>;
registerSettingsPanel: ReturnType<typeof vi.fn>;
hasCapability: ReturnType<typeof vi.fn>;
callBackend: ReturnType<typeof vi.fn>;
subscribe: ReturnType<typeof vi.fn>;
publish: ReturnType<typeof vi.fn>;
} {
return {
registerView: vi.fn(),
registerCommand: vi.fn(),
registerSettingsPanel: vi.fn(),
hasCapability: vi.fn().mockReturnValue(false),
callBackend: vi.fn().mockResolvedValue(undefined),
subscribe: vi.fn(),
publish: vi.fn(),
};
}
/**
* Валидатор plugin manifest.
*/
export function validateManifest(manifest: unknown): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!manifest || typeof manifest !== 'object') {
return { valid: false, errors: ['Manifest must be an object'] };
}
const m = manifest as Record<string, unknown>;
if (m.schemaVersion !== 1) {
errors.push(`schemaVersion must be 1, got ${m.schemaVersion}`);
}
if (typeof m.id !== 'string' || !m.id) {
errors.push('id must be a non-empty string');
}
if (typeof m.name !== 'string' || !m.name) {
errors.push('name must be a non-empty string');
}
if (typeof m.version !== 'string' || !/^\d+\.\d+\.\d+/.test(m.version as string)) {
errors.push('version must be a valid semver (e.g. 0.1.0)');
}
if (typeof m.apiVersion !== 'string' || !m.apiVersion) {
errors.push('apiVersion must be a non-empty string');
}
if (!Array.isArray(m.provides) || m.provides.length === 0) {
errors.push('provides must be a non-empty array');
}
if (!Array.isArray(m.permissions) || m.permissions.length === 0) {
errors.push('permissions must be a non-empty array');
}
return { valid: errors.length === 0, errors };
}
// Re-export vi for test files
import { vi } from 'vitest';
export { vi };
+345
View File
@@ -0,0 +1,345 @@
// Verstak Plugin SDK — Core TypeScript Types
// ─── Manifest ────────────────────────────────────────────────
export type PluginSource = 'official' | 'local' | 'third-party';
export interface PluginManifest {
schemaVersion: 1;
id: string;
name: string;
version: string;
apiVersion: string;
description?: string;
source?: PluginSource;
icon?: string;
provides: string[];
requires?: string[];
optionalRequires?: string[];
permissions: Permission[];
frontend?: FrontendConfig;
backend?: BackendConfig;
migrations?: MigrationConfig;
contributes?: ContributionPoints;
sync?: SyncConfig;
}
export interface FrontendConfig {
entry: string;
style?: string;
}
export interface BackendConfig {
type: 'sidecar';
entry: Record<string, string>;
healthCheck?: HealthCheckConfig;
}
export interface HealthCheckConfig {
type?: 'rpc' | 'stdio' | 'tcp';
timeout?: number;
}
export interface MigrationConfig {
path: string;
}
export interface SyncConfig {
namespaces?: string[];
participate?: boolean;
}
// ─── Capabilities ────────────────────────────────────────────
export type CapabilityName = string;
export interface CapabilityEntry {
name: CapabilityName;
description: string;
status: 'stable' | 'draft' | 'deprecated';
}
// ─── Permissions ─────────────────────────────────────────────
export type Permission =
| 'vault.read'
| 'vault.write'
| 'vault.watch'
| 'storage.namespace'
| 'storage.migrations'
| 'events.publish'
| 'events.subscribe'
| 'ui.register'
| 'commands.register'
| 'network.local'
| 'network.remote'
| 'process.spawn'
| 'secrets.read'
| 'secrets.write'
| 'sync.participate';
export interface PermissionEntry {
name: Permission;
description: string;
dangerous: boolean;
}
// ─── Contribution Points ─────────────────────────────────────
export interface ContributionPoints {
views?: ContributionView[];
commands?: ContributionCommand[];
settingsPanels?: ContributionSettingsPanel[];
sidebarItems?: ContributionSidebarItem[];
fileActions?: ContributionAction[];
noteActions?: ContributionAction[];
contextMenuEntries?: ContributionContextMenuEntry[];
searchProviders?: ContributionSearchProvider[];
activityProviders?: ContributionActivityProvider[];
statusBarItems?: ContributionStatusBarItem[];
}
export interface ContributionView {
id: string;
title: string;
icon?: string;
component: string;
}
export interface ContributionCommand {
id: string;
title: string;
keybinding?: string;
icon?: string;
handler?: string;
}
export interface ContributionSettingsPanel {
id: string;
title: string;
component: string;
icon?: string;
}
export interface ContributionSidebarItem {
id: string;
title: string;
icon?: string;
view: string;
position?: number;
}
export interface ContributionAction {
id: string;
label: string;
icon?: string;
capability?: CapabilityName;
handler?: string;
}
export interface ContributionContextMenuEntry {
id: string;
label: string;
context: 'file' | 'note' | 'case' | 'folder';
group?: string;
capability?: CapabilityName;
handler?: string;
}
export interface ContributionSearchProvider {
id: string;
label: string;
handler: string;
}
export interface ContributionActivityProvider {
id: string;
events?: string[];
handler: string;
}
export interface ContributionStatusBarItem {
id: string;
label: string;
position?: 'left' | 'right';
handler?: string;
}
// ─── Plugin State ────────────────────────────────────────────
export type PluginStatus =
| 'discovered'
| 'disabled'
| 'loading'
| 'loaded'
| 'degraded'
| 'failed'
| 'incompatible'
| 'missing-required-capability';
export interface PluginState {
id: string;
manifest: PluginManifest;
status: PluginStatus;
error?: string;
enabled: boolean;
loadedAt?: string;
}
// ─── Events ──────────────────────────────────────────────────
export interface VerstakEvent {
name: string;
timestamp: string;
payload: Record<string, unknown>;
}
// Browser events
export interface BrowserCapturePageEvent extends VerstakEvent {
name: 'browser.capture.page';
payload: {
url: string;
title: string;
html?: string;
text?: string;
capturedAt: string;
domain?: string;
};
}
export interface BrowserCaptureSelectionEvent extends VerstakEvent {
name: 'browser.capture.selection';
payload: {
url: string;
title: string;
text: string;
capturedAt: string;
domain?: string;
};
}
export interface BrowserCaptureLinkEvent extends VerstakEvent {
name: 'browser.capture.link';
payload: {
url: string;
title?: string;
capturedAt: string;
domain?: string;
};
}
// Vault events
export interface VaultOpenedEvent extends VerstakEvent {
name: 'vault.opened';
payload: {
path: string;
version?: string;
openedAt: string;
};
}
export interface CaseSelectedEvent extends VerstakEvent {
name: 'case.selected';
payload: {
caseId: string;
casePath: string;
caseType?: string;
selectedAt: string;
};
}
export interface FileChangedEvent extends VerstakEvent {
name: 'file.changed';
payload: {
path: string;
size?: number;
changedAt: string;
};
}
export interface NoteSavedEvent extends VerstakEvent {
name: 'note.saved';
payload: {
noteId: string;
title?: string;
path: string;
caseId?: string;
savedAt: string;
};
}
// Lifecycle events
export interface PluginEnabledEvent extends VerstakEvent {
name: 'plugin.enabled';
payload: {
pluginId: string;
version?: string;
enabledAt: string;
};
}
export interface PluginDisabledEvent extends VerstakEvent {
name: 'plugin.disabled';
payload: {
pluginId: string;
disabledAt: string;
};
}
// ─── Sync Types ──────────────────────────────────────────────
export type SyncOpType = 'add' | 'modify' | 'delete' | 'rename';
export type SyncEntityType = 'file' | 'note' | 'plugin_state' | 'vault_meta';
export interface SyncOperation {
op: SyncOpType;
id: string;
timestamp: string;
deviceId?: string;
entityType?: SyncEntityType;
entityPath?: string;
hash?: string;
size?: number;
mimeType?: string;
pluginNamespace?: string;
oldPath?: string;
metadata?: Record<string, string>;
}
export interface SyncBatch {
batchId: string;
deviceId: string;
operations: SyncOperation[];
timestamp: string;
lastSyncTimestamp?: string;
sequence?: number;
}
export interface SyncManifestEntry {
path: string;
hash: string;
size?: number;
updatedAt: string;
deleted?: boolean;
}
export interface SyncManifest {
deviceId: string;
entries: SyncManifestEntry[];
}
export interface Conflict {
entityPath: string;
localHash: string;
remoteHash: string;
localTimestamp: string;
remoteTimestamp: string;
resolution?: 'local_wins' | 'remote_wins' | 'manual';
resolvedAt?: string;
}
// ─── Settings ────────────────────────────────────────────────
export interface PluginSettings {
[key: string]: unknown;
}