Add open provider contracts to SDK

This commit is contained in:
2026-06-19 07:52:00 +08:00
parent 24d500a7b5
commit 0a0da4c1cb
20 changed files with 1030 additions and 432 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
// Verstak Plugin SDK — Public API
export * from './types';
export { VerstakPluginAPI, createPluginAPI } from './plugin-api';
export { createPluginAPI } from './plugin-api';
export { RPCServer, RPCClient } from './rpc';
export { createTestManifest, createTestPluginState, createMockPluginAPI, validateManifest, } from './test-utils';
//# sourceMappingURL=index.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAElC,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAElC,cAAc,SAAS,CAAC;AACxB,OAAO,EAAoB,eAAe,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
+69 -77
View File
@@ -1,79 +1,71 @@
import type { PluginSettings } from './types';
/**
* VerstakPluginAPI — единственный способ для frontend плагина
* общаться с core платформы.
*
* Экземпляр API передаётся плагину при активации через глобальную
* переменную `window.__VERSTAK_PLUGIN_API__`.
*/
export declare class VerstakPluginAPI {
private pluginId;
private capabilities;
constructor(pluginId: string);
/**
* Инициализация API — вызывается core после загрузки frontend bundle.
* @internal
*/
_init(capabilities: string[]): void;
/**
* Зарегистрировать view для отображения в UI Shell.
*/
registerView(id: string, component: unknown): void;
/**
* Зарегистрировать панель настроек плагина.
*/
registerSettingsPanel(id: string, title: string, component: unknown): void;
/**
* Зарегистрировать команду для command palette.
*/
registerCommand(id: string, title: string, handler: () => void, keybinding?: string): void;
/**
* Зарегистрировать действия для файлов.
*/
registerFileAction(id: string, label: string, handler: (filePath: string) => void, capability?: string): void;
/**
* Зарегистрировать действия для заметок.
*/
registerNoteAction(id: string, label: string, handler: (noteId: string) => void, capability?: string): void;
/**
* Зарегистрировать provider поиска.
*/
registerSearchProvider(id: string, label: string, handler: (query: string) => unknown[]): void;
/**
* Проверить, доступна ли capability.
*/
hasCapability(name: string): boolean;
/**
* Получить список всех доступных capabilities.
*/
getAvailableCapabilities(): string[];
/**
* Вызвать backend метод плагина через RPC.
*/
callBackend(method: string, args?: unknown[]): Promise<unknown>;
/**
* Прочитать настройки плагина.
*/
readSettings(): Promise<PluginSettings>;
/**
* Записать настройки плагина.
*/
writeSettings(settings: PluginSettings): Promise<void>;
/**
* Подписаться на событие event bus.
*/
subscribe(event: string, handler: (payload: unknown) => void): void;
/**
* Опубликовать событие в event bus.
*/
publish(event: string, payload: unknown): void;
private _postMessage;
private _rpcCall;
import type { CapabilityEntry, FileEntry, FileMetadata, MovePathOptions, OpenResourceRequest, OpenResourceResult, PluginSettings, TrashResult, WriteTextOptions } from './types';
export type PluginCommandArgs = Record<string, unknown>;
export type PluginCommandHandler = (args: PluginCommandArgs, declaration: PluginCommandDeclaration) => unknown | Promise<unknown>;
export type Unsubscribe = () => void;
export interface PluginCommandDeclaration {
status: 'declared';
pluginId: string;
commandId: string;
handler?: string;
args?: PluginCommandArgs;
}
/**
* Создать экземпляр VerstakPluginAPI.
* Core вызывает эту функцию после загрузки frontend bundle,
* передавая pluginId и список доступных capabilities.
*/
export declare function createPluginAPI(pluginId: string): VerstakPluginAPI;
export interface PluginCommandResult {
status: 'handled';
pluginId: string;
commandId: string;
result: unknown;
}
export interface PluginEvent<TPayload = Record<string, unknown>> {
name: string;
pluginId: string;
payload: TPayload;
timestamp: string;
}
export interface VerstakPluginAPI {
readonly pluginId: string;
settings: {
read(): Promise<PluginSettings>;
read<T = unknown>(key: string): Promise<T | undefined>;
write(key: string, value: unknown): Promise<PluginSettings>;
writeAll(settings: PluginSettings): Promise<void>;
};
capabilities: {
has(capability: string): Promise<boolean>;
get(capability: string): Promise<{
available: boolean;
name?: string;
pluginId?: string;
status?: string;
}>;
list(): Promise<CapabilityEntry[]>;
};
commands: {
register(commandId: string, handler: PluginCommandHandler): Promise<Unsubscribe>;
execute(commandId: string, args?: PluginCommandArgs): Promise<PluginCommandResult>;
};
events: {
publish(eventName: string, payload?: Record<string, unknown>): Promise<void>;
subscribe<TPayload = Record<string, unknown>>(eventName: string, handler: (event: PluginEvent<TPayload>) => void): Promise<Unsubscribe>;
};
files: {
/**
* Files API uses canonical vault-relative slash paths. Backslashes,
* Windows/UNC absolute paths, traversal, null bytes, `.verstak` variants,
* and symlink read/write/move/trash operations are rejected by the host.
*/
list(relativeDir?: string): Promise<FileEntry[]>;
metadata(relativePath: string): Promise<FileMetadata>;
readText(relativePath: string): Promise<string>;
writeText(relativePath: string, content: string, options?: WriteTextOptions): Promise<void>;
createFolder(relativePath: string): Promise<void>;
move(fromRelativePath: string, toRelativePath: string, options?: MovePathOptions): Promise<void>;
trash(relativePath: string): Promise<TrashResult>;
};
workbench: {
openResource(request: OpenResourceRequest): Promise<OpenResourceResult>;
editResource(request: OpenResourceRequest): Promise<OpenResourceResult>;
};
dispose?: () => void;
}
export declare function createPluginAPI(_pluginId: string): VerstakPluginAPI;
//# sourceMappingURL=plugin-api.d.ts.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAqB;gBAE7B,QAAQ,EAAE,MAAM;IAI5B;;;OAGG;IACH,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI;IAMnC;;OAEG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI;IAIlD;;OAEG;IACH,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI;IAI1E;;OAEG;IACH,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAI1F;;OAEG;IACH,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAI7G;;OAEG;IACH,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAI3G;;OAEG;IACH,sBAAsB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,EAAE,GAAG,IAAI;IAM9F;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACH,wBAAwB,IAAI,MAAM,EAAE;IAMpC;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAMzE;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,cAAc,CAAC;IAK7C;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAInE;;OAEG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAM9C,OAAO,CAAC,YAAY;YAMN,QAAQ;CAiBvB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAGlE"}
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
+8 -139
View File
@@ -1,141 +1,10 @@
// Verstak Plugin SDK — VerstakPluginAPI
// The official runtime API available to all plugins in the frontend context.
/**
* VerstakPluginAPI — единственный способ для frontend плагина
* общаться с core платформы.
*
* Экземпляр API передаётся плагину при активации через глобальную
* переменную `window.__VERSTAK_PLUGIN_API__`.
*/
export class VerstakPluginAPI {
pluginId;
capabilities = new Set();
constructor(pluginId) {
this.pluginId = pluginId;
}
/**
* Инициализация API — вызывается core после загрузки frontend bundle.
* @internal
*/
_init(capabilities) {
this.capabilities = new Set(capabilities);
}
// ─── View Registration ─────────────────────────────────────
/**
* Зарегистрировать view для отображения в UI Shell.
*/
registerView(id, component) {
this._postMessage('register.view', { id, component });
}
/**
* Зарегистрировать панель настроек плагина.
*/
registerSettingsPanel(id, title, component) {
this._postMessage('register.settingsPanel', { id, title, component });
}
/**
* Зарегистрировать команду для command palette.
*/
registerCommand(id, title, handler, keybinding) {
this._postMessage('register.command', { id, title, keybinding, handler: handler.toString() });
}
/**
* Зарегистрировать действия для файлов.
*/
registerFileAction(id, label, handler, capability) {
this._postMessage('register.fileAction', { id, label, handler: handler.toString(), capability });
}
/**
* Зарегистрировать действия для заметок.
*/
registerNoteAction(id, label, handler, capability) {
this._postMessage('register.noteAction', { id, label, handler: handler.toString(), capability });
}
/**
* Зарегистрировать provider поиска.
*/
registerSearchProvider(id, label, handler) {
this._postMessage('register.searchProvider', { id, label, handler: handler.toString() });
}
// ─── Capabilities ──────────────────────────────────────────
/**
* Проверить, доступна ли capability.
*/
hasCapability(name) {
return this.capabilities.has(name);
}
/**
* Получить список всех доступных capabilities.
*/
getAvailableCapabilities() {
return Array.from(this.capabilities);
}
// ─── Backend Communication ─────────────────────────────────
/**
* Вызвать backend метод плагина через RPC.
*/
async callBackend(method, args = []) {
return this._rpcCall(method, args);
}
// ─── Settings ──────────────────────────────────────────────
/**
* Прочитать настройки плагина.
*/
async readSettings() {
const result = await this._rpcCall('readSettings', []);
return result;
}
/**
* Записать настройки плагина.
*/
async writeSettings(settings) {
await this._rpcCall('writeSettings', [settings]);
}
// ─── Event Bus ─────────────────────────────────────────────
/**
* Подписаться на событие event bus.
*/
subscribe(event, handler) {
this._postMessage('subscribe', { event, handler: handler.toString() });
}
/**
* Опубликовать событие в event bus.
*/
publish(event, payload) {
this._postMessage('publish', { event, payload });
}
// ─── Internal ──────────────────────────────────────────────
_postMessage(type, data) {
window.dispatchEvent(new CustomEvent('verstak:plugin', {
detail: { pluginId: this.pluginId, type, data }
}));
}
async _rpcCall(method, args) {
return new Promise((resolve, reject) => {
const callId = `${this.pluginId}:${Date.now()}:${Math.random()}`;
const handler = (event) => {
if (event.detail.callId === callId) {
window.removeEventListener('verstak:rpc:response', handler);
if (event.detail.error) {
reject(new Error(event.detail.error));
}
else {
resolve(event.detail.result);
}
}
};
window.addEventListener('verstak:rpc:response', handler);
this._postMessage('rpc', { callId, method, args });
});
}
}
/**
* Создать экземпляр VerstakPluginAPI.
* Core вызывает эту функцию после загрузки frontend bundle,
* передавая pluginId и список доступных capabilities.
*/
export function createPluginAPI(pluginId) {
const api = new VerstakPluginAPI(pluginId);
return api;
// Verstak Plugin SDK — bundled frontend plugin API contract.
//
// The desktop host creates the real API with createPluginAPI(pluginId) inside
// VerstakPluginAPI.js and passes it to bundled plugin components at mount time.
// This SDK file intentionally exposes the TypeScript contract only; it is not
// a standalone security boundary or RPC client.
export function createPluginAPI(_pluginId) {
throw new Error('createPluginAPI is provided by Verstak Desktop at plugin runtime');
}
//# sourceMappingURL=plugin-api.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AA+FhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}
+2 -9
View File
@@ -1,4 +1,5 @@
import type { PluginManifest, PluginState } from './types';
import type { VerstakPluginAPI } from './plugin-api';
/**
* Создать тестовый manifest для unit-тестов.
*/
@@ -10,15 +11,7 @@ export declare function createTestPluginState(overrides?: Partial<PluginState>):
/**
* Создать заглушку VerstakPluginAPI для тестов.
*/
export declare 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>;
};
export declare function createMockPluginAPI(pluginId?: string): VerstakPluginAPI;
/**
* Валидатор plugin manifest.
*/
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI;IACrC,YAAY,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACvC,eAAe,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1C,qBAAqB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAChD,aAAa,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACxC,WAAW,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACtC,SAAS,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACpC,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CACnC,CAUA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,GAAG,gBAAgB,CAsL9E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}
+195 -8
View File
@@ -34,15 +34,202 @@ export function createTestPluginState(overrides) {
/**
* Создать заглушку VerstakPluginAPI для тестов.
*/
export function createMockPluginAPI() {
export function createMockPluginAPI(pluginId = 'test.plugin') {
const settings = {};
const commands = new Map();
const eventHandlers = new Map();
const files = new Map();
files.set('', { type: 'folder', modifiedAt: new Date().toISOString() });
function normalizePath(path, allowRoot = false) {
const raw = String(path || '');
if (raw.includes('\0'))
throw new Error('invalid-path: null-byte');
if (raw.includes('\\'))
throw new Error('invalid-path: backslash not allowed');
const normalized = raw.replace(/^\.\//, '');
const parts = normalized.split('/').filter(Boolean);
if (!allowRoot && parts.length === 0)
throw new Error('invalid-path: empty path');
if (normalized.startsWith('/') || /^[A-Za-z]:/.test(normalized))
throw new Error('invalid-path: absolute path rejected');
if (parts.includes('..'))
throw new Error('invalid-path: path-traversal');
if (parts[0] && parts[0].toLowerCase() === '.verstak')
throw new Error('reserved-path: .verstak is internal');
return parts.join('/');
}
function parentPath(path) {
const idx = path.lastIndexOf('/');
return idx === -1 ? '' : path.slice(0, idx);
}
function baseName(path) {
const idx = path.lastIndexOf('/');
return idx === -1 ? path : path.slice(idx + 1);
}
function entry(path, node) {
const name = baseName(path);
const dot = name.lastIndexOf('.');
const extension = dot > 0 ? name.slice(dot + 1) : '';
return {
name,
relativePath: path,
type: node.type,
size: node.type === 'file' ? (node.content || '').length : 0,
modifiedAt: node.modifiedAt,
extension,
isHidden: name.startsWith('.'),
isReserved: false,
canRead: true,
canWrite: true,
};
}
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(),
pluginId,
settings: {
read: vi.fn(async (key) => key ? settings[key] : { ...settings }),
write: vi.fn(async (key, value) => {
settings[key] = value;
return { ...settings };
}),
writeAll: vi.fn(async (nextSettings) => {
Object.keys(settings).forEach((key) => delete settings[key]);
Object.assign(settings, nextSettings);
}),
},
capabilities: {
has: vi.fn(async () => false),
get: vi.fn(async (name) => ({ available: false, name })),
list: vi.fn(async () => []),
},
commands: {
register: vi.fn(async (commandId, handler) => {
commands.set(commandId, handler);
return () => { commands.delete(commandId); };
}),
execute: vi.fn(async (commandId, args = {}) => {
const handler = commands.get(commandId);
if (!handler) {
throw new Error(`declared-but-unhandled: ${commandId}`);
}
return { status: 'handled', pluginId, commandId, result: await handler(args) };
}),
},
events: {
publish: vi.fn(async (eventName, payload = {}) => {
const event = { name: eventName, pluginId, payload, timestamp: new Date().toISOString() };
(eventHandlers.get(eventName) || []).slice().forEach((handler) => handler(event));
}),
subscribe: vi.fn(async (eventName, handler) => {
const handlers = eventHandlers.get(eventName) || [];
handlers.push(handler);
eventHandlers.set(eventName, handlers);
return () => {
eventHandlers.set(eventName, (eventHandlers.get(eventName) || []).filter((item) => item !== handler));
};
}),
},
files: {
list: vi.fn(async (relativeDir = '') => {
const dir = normalizePath(relativeDir, true);
const node = files.get(dir);
if (!node || node.type !== 'folder')
throw new Error(`not-found: ${dir}`);
const prefix = dir ? `${dir}/` : '';
return Array.from(files.entries())
.filter(([path]) => path !== dir && path.startsWith(prefix) && !path.slice(prefix.length).includes('/'))
.map(([path, node]) => entry(path, node));
}),
metadata: vi.fn(async (relativePath) => {
const path = normalizePath(relativePath);
const node = files.get(path);
if (!node)
throw new Error(`not-found: ${path}`);
return { ...entry(path, node), mimeHint: '', isText: node.type === 'file' };
}),
readText: vi.fn(async (relativePath) => {
const path = normalizePath(relativePath);
const node = files.get(path);
if (!node)
throw new Error(`not-found: ${path}`);
if (node.type !== 'file')
throw new Error(`not-regular-file: ${path}`);
return node.content || '';
}),
writeText: vi.fn(async (relativePath, content, options = {}) => {
const path = normalizePath(relativePath);
const node = files.get(path);
if (node && node.type !== 'file')
throw new Error(`not-regular-file: ${path}`);
if (node && !options.overwrite)
throw new Error(`conflict: ${path}`);
if (!node && !options.createIfMissing)
throw new Error(`not-found: ${path}`);
const parent = parentPath(path);
if (!files.get(parent) || files.get(parent)?.type !== 'folder')
throw new Error(`parent-not-found: ${parent}`);
files.set(path, { type: 'file', content, modifiedAt: new Date().toISOString() });
}),
createFolder: vi.fn(async (relativePath) => {
const path = normalizePath(relativePath);
if (files.has(path))
throw new Error(`conflict: ${path}`);
const parent = parentPath(path);
if (!files.get(parent) || files.get(parent)?.type !== 'folder')
throw new Error(`parent-not-found: ${parent}`);
files.set(path, { type: 'folder', modifiedAt: new Date().toISOString() });
}),
move: vi.fn(async (fromRelativePath, toRelativePath, options = {}) => {
const from = normalizePath(fromRelativePath);
const to = normalizePath(toRelativePath);
const node = files.get(from);
if (!node)
throw new Error(`not-found: ${from}`);
if (node.type === 'folder' && (to === from || to.startsWith(`${from}/`))) {
throw new Error(`move-into-self: ${from} -> ${to}`);
}
if (files.has(to) && !options.overwrite)
throw new Error(`conflict: ${to}`);
const parent = parentPath(to);
if (!files.get(parent) || files.get(parent)?.type !== 'folder')
throw new Error(`parent-not-found: ${parent}`);
const moving = Array.from(files.entries()).filter(([path]) => path === from || path.startsWith(`${from}/`));
moving.forEach(([path, movingNode]) => {
const suffix = path.slice(from.length);
files.set(`${to}${suffix}`, movingNode);
files.delete(path);
});
}),
trash: vi.fn(async (relativePath) => {
const path = normalizePath(relativePath);
if (!files.has(path))
throw new Error(`not-found: ${path}`);
files.delete(path);
const trashId = `mock-${Date.now()}`;
return {
originalPath: path,
trashPath: `.verstak/trash/files/${trashId}/${baseName(path)}`,
trashId,
deletedAt: new Date().toISOString(),
};
}),
},
workbench: {
openResource: vi.fn(async (request) => ({
status: 'opened',
providerId: request.context?.notesMode ? 'mock.notes-markdown-provider' : 'mock.open-provider',
providerPluginId: 'mock.editor',
providerComponent: 'MockEditor',
request: { ...request, mode: request.mode || 'view' },
})),
editResource: vi.fn(async (request) => ({
status: 'opened',
providerId: request.context?.notesMode ? 'mock.notes-markdown-provider' : 'mock.open-provider',
providerPluginId: 'mock.editor',
providerComponent: 'MockEditor',
request: { ...request, mode: 'edit' },
})),
},
dispose: vi.fn(),
};
}
/**
+1 -1
View File
File diff suppressed because one or more lines are too long
+86 -2
View File
@@ -41,15 +41,59 @@ export interface SyncConfig {
export type CapabilityName = string;
export interface CapabilityEntry {
name: CapabilityName;
description: string;
description?: string;
pluginId: string;
status: 'stable' | 'draft' | 'deprecated';
}
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 type Permission = 'vault.read' | 'vault.write' | 'vault.watch' | 'files.read' | 'files.write' | 'files.delete' | 'workbench.open' | '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;
}
export type FileEntryType = 'file' | 'folder' | 'symlink' | 'unknown';
export interface FileEntry {
name: string;
relativePath: string;
type: FileEntryType;
size: number;
modifiedAt: string;
extension: string;
isHidden: boolean;
isReserved: boolean;
canRead: boolean;
canWrite: boolean;
}
export interface FileMetadata {
relativePath: string;
type: FileEntryType;
size: number;
modifiedAt: string;
createdAt?: string;
extension: string;
mimeHint: string;
isText: boolean;
isHidden: boolean;
isReserved: boolean;
canRead: boolean;
canWrite: boolean;
}
export interface WriteTextOptions {
/** Create the file when it is missing. Parent folder must already exist. */
createIfMissing?: boolean;
/** Replace an existing regular file. Existing folders/symlinks are rejected. */
overwrite?: boolean;
}
export interface MovePathOptions {
/** Replace an existing target path when the host supports it. */
overwrite?: boolean;
}
export interface TrashResult {
originalPath: string;
trashPath: string;
trashId: string;
deletedAt: string;
}
export interface ContributionPoints {
views?: ContributionView[];
commands?: ContributionCommand[];
@@ -61,6 +105,7 @@ export interface ContributionPoints {
searchProviders?: ContributionSearchProvider[];
activityProviders?: ContributionActivityProvider[];
statusBarItems?: ContributionStatusBarItem[];
openProviders?: ContributionOpenProvider[];
}
export interface ContributionView {
id: string;
@@ -119,6 +164,45 @@ export interface ContributionStatusBarItem {
position?: 'left' | 'right';
handler?: string;
}
export type OpenResourceKind = 'vault-file';
export type OpenResourceMode = 'view' | 'edit';
export type OpenResourceContextName = 'generic-text' | 'generic-markdown' | 'notes-markdown' | string;
export interface OpenProviderSupport {
kind: OpenResourceKind;
extensions?: string[];
mime?: string[];
contexts?: OpenResourceContextName[];
}
export interface ContributionOpenProvider {
id: string;
title: string;
priority?: number;
component: string;
supports: OpenProviderSupport[];
}
export interface OpenResourceContext {
sourcePluginId?: string;
sourceView?: 'files' | 'notes' | string;
isInsideNotesFolder?: boolean;
notesScopePath?: string;
notesMode?: boolean;
}
export interface OpenResourceRequest {
kind: OpenResourceKind;
path: string;
mode?: OpenResourceMode;
mime?: string;
extension?: string;
context?: OpenResourceContext;
}
export interface OpenResourceResult {
status: 'opened' | 'no-provider';
providerId?: string;
providerPluginId?: string;
providerComponent?: string;
request: OpenResourceRequest;
message?: string;
}
export type PluginStatus = 'discovered' | 'disabled' | 'loading' | 'loaded' | 'degraded' | 'failed' | 'incompatible' | 'missing-required-capability';
export interface PluginState {
id: string;
+1 -1
View File
File diff suppressed because one or more lines are too long