verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js

522 lines
18 KiB
JavaScript

import * as App from '../../../wailsjs/go/api/App';
window.__VERSTAK_PLUGIN_REGISTRY__ = window.__VERSTAK_PLUGIN_REGISTRY__ || {};
window.__VERSTAK_EVENT_HANDLERS__ = window.__VERSTAK_EVENT_HANDLERS__ || {};
window.__VERSTAK_COMMAND_HANDLERS__ = window.__VERSTAK_COMMAND_HANDLERS__ || {};
if (!window.VerstakPluginRegister) {
window.VerstakPluginRegister = function(pluginId, bundle) {
if (!pluginId || !bundle || !bundle.components) {
console.error('[VerstakPluginRegister] invalid registration:', pluginId);
return;
}
console.log('[VerstakPluginRegister] registered:', pluginId, Object.keys(bundle.components));
window.__VERSTAK_PLUGIN_REGISTRY__[pluginId] = bundle.components;
};
}
function unpack(result) {
if (Array.isArray(result) && result.length === 2 && (typeof result[1] === 'string' || result[1] == null)) {
return [result[0], result[1] || ''];
}
return [result, ''];
}
async function callBackend(pluginId, label, fn) {
try {
const [value, err] = unpack(await fn());
if (err) {
throw new Error(err);
}
return value;
} catch (e) {
const message = e && e.message ? e.message : String(e);
throw new Error('[plugin:' + pluginId + '] ' + label + ' failed: ' + message);
}
}
async function callBackendErrorString(pluginId, label, fn) {
try {
const err = await fn();
if (err) {
throw new Error(err);
}
} catch (e) {
const message = e && e.message ? e.message : String(e);
throw new Error('[plugin:' + pluginId + '] ' + label + ' failed: ' + message);
}
}
function getEventHandlers(eventName) {
if (!window.__VERSTAK_EVENT_HANDLERS__[eventName]) {
window.__VERSTAK_EVENT_HANDLERS__[eventName] = [];
}
return window.__VERSTAK_EVENT_HANDLERS__[eventName];
}
function dispatchLocalEvent(pluginId, eventName, payload) {
const event = {
name: eventName,
pluginId: pluginId,
payload: payload || {},
timestamp: new Date().toISOString()
};
const handlers = getEventHandlers(eventName).slice();
handlers.forEach(function(handler) {
try {
handler(event);
} catch (e) {
console.error('[VerstakPluginAPI] event handler error:', e);
}
});
}
function dispatchBackendEvent(event) {
if (!event || !event.name) return;
const handlers = getEventHandlers(event.name).slice();
handlers.forEach(function(handler) {
try {
handler(event);
} catch (e) {
console.error('[VerstakPluginAPI] backend event handler error:', e);
}
});
}
window.__VERSTAK_DISPATCH_BACKEND_EVENT__ = dispatchBackendEvent;
if (!window.__VERSTAK_BACKEND_EVENT_BRIDGE__ && window.runtime && typeof window.runtime.EventsOnMultiple === 'function') {
window.__VERSTAK_BACKEND_EVENT_BRIDGE__ = window.runtime.EventsOnMultiple('verstak:plugin-event', dispatchBackendEvent, -1);
}
function commandKey(pluginId, commandId) {
return pluginId + ':' + commandId;
}
export async function executePluginCommand(pluginId, cmdId, args) {
if (!pluginId) {
throw new Error('executePluginCommand requires pluginId');
}
if (!cmdId) {
throw new Error('executePluginCommand requires command id');
}
const declared = await callBackend(pluginId, 'commands.execute(' + cmdId + ')', function() {
return App.ExecutePluginCommand(pluginId, cmdId, args || {});
});
const handler = window.__VERSTAK_COMMAND_HANDLERS__[commandKey(pluginId, cmdId)];
if (!handler) {
throw new Error('[plugin:' + pluginId + '] commands.execute(' + cmdId + ') failed: declared-but-unhandled');
}
const result = await handler(args || {}, declared);
return {
status: 'handled',
pluginId: pluginId,
commandId: cmdId,
result: result
};
}
export function createPluginAPI(pluginId) {
if (!pluginId) {
throw new Error('createPluginAPI requires pluginId');
}
const cleanups = [];
let disposed = false;
function assertActive(label) {
if (disposed) {
throw new Error('[plugin:' + pluginId + '] ' + label + ' failed: API disposed');
}
}
function trackCleanup(fn) {
cleanups.push(fn);
return function untrackAndRun() {
const idx = cleanups.indexOf(fn);
if (idx !== -1) {
cleanups.splice(idx, 1);
}
fn();
};
}
return {
pluginId: pluginId,
ui: {
openSettings: function(panelId) {
assertActive('ui.openSettings');
window.dispatchEvent(new CustomEvent('verstak:open-settings', {
detail: { pluginId: pluginId, panelId: panelId || '' }
}));
}
},
capabilities: {
has: async function(capId) {
const info = await callBackend(pluginId, 'capabilities.has(' + capId + ')', function() {
return App.GetPluginCapability(pluginId, capId);
});
return !!(info && info.available);
},
get: function(capId) {
return callBackend(pluginId, 'capabilities.get(' + capId + ')', function() {
return App.GetPluginCapability(pluginId, capId);
});
},
list: function() {
return callBackend(pluginId, 'capabilities.list', function() {
return App.ListPluginCapabilities(pluginId);
});
}
},
events: {
publish: async function(type, payload) {
await callBackendErrorString(pluginId, 'events.publish(' + type + ')', function() {
return App.PublishPluginEvent(pluginId, type, payload || {});
});
if (!window.__VERSTAK_BACKEND_EVENT_BRIDGE__) {
dispatchLocalEvent(pluginId, type, payload || {});
}
},
subscribe: function(type, handler) {
assertActive('events.subscribe(' + type + ')');
if (typeof handler !== 'function') {
throw new Error('events.subscribe requires a handler function');
}
return callBackendErrorString(pluginId, 'events.subscribe(' + type + ')', function() {
return App.SubscribePluginEvent(pluginId, type);
}).then(function() {
const handlers = getEventHandlers(type);
handlers.push(handler);
return trackCleanup(function unsubscribe() {
const current = getEventHandlers(type);
window.__VERSTAK_EVENT_HANDLERS__[type] = current.filter(function(item) {
return item !== handler;
});
});
});
}
},
settings: {
read: async function(key) {
assertActive('settings.read');
const settings = await callBackend(pluginId, 'settings.read', function() {
return App.ReadPluginSettings(pluginId);
});
if (!key) {
return settings || {};
}
return settings ? settings[key] : undefined;
},
write: async function(key, value) {
assertActive('settings.write(' + key + ')');
if (!key) {
throw new Error('settings.write requires a key');
}
const settings = await this.read();
settings[key] = value;
await callBackendErrorString(pluginId, 'settings.write(' + key + ')', function() {
return App.WritePluginSettings(pluginId, settings);
});
return settings;
},
writeAll: function(settings) {
assertActive('settings.writeAll');
return callBackendErrorString(pluginId, 'settings.writeAll', function() {
return App.WritePluginSettings(pluginId, settings || {});
});
}
},
storage: {
data: {
read: function(name) {
assertActive('storage.data.read(' + name + ')');
if (!name) {
throw new Error('storage.data.read requires a name');
}
return callBackend(pluginId, 'storage.data.read(' + name + ')', function() {
return App.ReadPluginDataJSON(pluginId, name);
}).then(function(data) {
return data || {};
});
},
write: function(name, data) {
assertActive('storage.data.write(' + name + ')');
if (!name) {
throw new Error('storage.data.write requires a name');
}
return callBackendErrorString(pluginId, 'storage.data.write(' + name + ')', function() {
return App.WritePluginDataJSON(pluginId, name, data || {});
});
}
}
},
secrets: {
status: function() {
assertActive('secrets.status');
return callBackend(pluginId, 'secrets.status', function() {
return App.PluginSecretsStatus(pluginId);
});
},
unlock: function(masterPassword) {
assertActive('secrets.unlock');
return callBackendErrorString(pluginId, 'secrets.unlock', function() {
return App.PluginSecretsUnlock(pluginId, String(masterPassword == null ? '' : masterPassword));
});
},
list: function() {
assertActive('secrets.list');
return callBackend(pluginId, 'secrets.list', function() {
return App.PluginSecretsList(pluginId);
}).then(function(records) {
return Array.isArray(records) ? records : [];
});
},
read: function(secretId) {
assertActive('secrets.read(' + secretId + ')');
if (!secretId) {
throw new Error('secrets.read requires a secret id');
}
return callBackend(pluginId, 'secrets.read(' + secretId + ')', function() {
return App.PluginSecretsRead(pluginId, secretId);
});
},
write: function(record) {
assertActive('secrets.write');
return callBackend(pluginId, 'secrets.write', function() {
return App.PluginSecretsWrite(pluginId, record || {});
});
},
delete: function(secretId) {
assertActive('secrets.delete(' + secretId + ')');
if (!secretId) {
throw new Error('secrets.delete requires a secret id');
}
return callBackendErrorString(pluginId, 'secrets.delete(' + secretId + ')', function() {
return App.PluginSecretsDelete(pluginId, secretId);
});
},
copyLink: function(secretId) {
assertActive('secrets.copyLink(' + secretId + ')');
if (!secretId) {
throw new Error('secrets.copyLink requires a secret id');
}
return callBackend(pluginId, 'secrets.copyLink(' + secretId + ')', function() {
return App.PluginSecretsCopyLink(pluginId, secretId);
});
}
},
files: {
list: function(relativeDir) {
assertActive('files.list');
return callBackend(pluginId, 'files.list(' + (relativeDir || '') + ')', function() {
return App.ListVaultFiles(pluginId, relativeDir || '');
});
},
metadata: function(relativePath) {
assertActive('files.metadata(' + relativePath + ')');
return callBackend(pluginId, 'files.metadata(' + relativePath + ')', function() {
return App.GetVaultFileMetadata(pluginId, relativePath);
});
},
readText: function(relativePath) {
assertActive('files.readText(' + relativePath + ')');
return callBackend(pluginId, 'files.readText(' + relativePath + ')', function() {
return App.ReadVaultTextFile(pluginId, relativePath);
});
},
readBytes: function(relativePath) {
assertActive('files.readBytes(' + relativePath + ')');
return callBackend(pluginId, 'files.readBytes(' + relativePath + ')', function() {
return App.ReadVaultFileBytes(pluginId, relativePath);
});
},
writeText: function(relativePath, content, options) {
assertActive('files.writeText(' + relativePath + ')');
return callBackendErrorString(pluginId, 'files.writeText(' + relativePath + ')', function() {
return App.WriteVaultTextFile(pluginId, relativePath, String(content == null ? '' : content), options || {});
});
},
writeBytes: function(relativePath, dataBase64, options) {
assertActive('files.writeBytes(' + relativePath + ')');
return callBackendErrorString(pluginId, 'files.writeBytes(' + relativePath + ')', function() {
return App.WriteVaultFileBytes(pluginId, relativePath, String(dataBase64 == null ? '' : dataBase64), options || {});
});
},
createFolder: function(relativePath) {
assertActive('files.createFolder(' + relativePath + ')');
return callBackendErrorString(pluginId, 'files.createFolder(' + relativePath + ')', function() {
return App.CreateVaultFolder(pluginId, relativePath);
});
},
move: function(fromRelativePath, toRelativePath, options) {
assertActive('files.move(' + fromRelativePath + ')');
return callBackendErrorString(pluginId, 'files.move(' + fromRelativePath + ')', function() {
return App.MoveVaultPath(pluginId, fromRelativePath, toRelativePath, options || {});
});
},
trash: function(relativePath) {
assertActive('files.trash(' + relativePath + ')');
return callBackend(pluginId, 'files.trash(' + relativePath + ')', function() {
return App.TrashVaultPath(pluginId, relativePath);
});
},
listTrash: function() {
assertActive('files.listTrash');
return callBackend(pluginId, 'files.listTrash', function() {
return App.ListVaultTrash(pluginId);
});
},
restoreTrash: function(trashId, options) {
assertActive('files.restoreTrash(' + trashId + ')');
return callBackend(pluginId, 'files.restoreTrash(' + trashId + ')', function() {
return App.RestoreVaultTrash(pluginId, trashId, options || {});
});
},
openExternal: function(relativePath) {
assertActive('files.openExternal(' + relativePath + ')');
return callBackendErrorString(pluginId, 'files.openExternal(' + relativePath + ')', function() {
return App.OpenVaultPathExternal(pluginId, relativePath);
});
},
showInFolder: function(relativePath) {
assertActive('files.showInFolder(' + relativePath + ')');
return callBackendErrorString(pluginId, 'files.showInFolder(' + relativePath + ')', function() {
return App.ShowVaultPathInFolder(pluginId, relativePath);
});
}
},
sync: {
status: function() {
assertActive('sync.status');
return callBackend(pluginId, 'sync.status', function() {
return App.PluginSyncStatus(pluginId);
});
},
configure: function(serverURL, username, password) {
assertActive('sync.configure');
return callBackendErrorString(pluginId, 'sync.configure', function() {
return App.PluginSyncConfigure(pluginId, serverURL || '', username || '', password || '');
});
},
disconnect: function() {
assertActive('sync.disconnect');
return callBackendErrorString(pluginId, 'sync.disconnect', function() {
return App.PluginSyncDisconnect(pluginId);
});
},
testConnection: function(serverURL, username, password) {
assertActive('sync.testConnection');
return callBackendErrorString(pluginId, 'sync.testConnection', function() {
return App.PluginSyncTestConnection(pluginId, serverURL || '', username || '', password || '');
});
},
setInterval: function(minutes) {
assertActive('sync.setInterval');
return callBackendErrorString(pluginId, 'sync.setInterval', function() {
return App.PluginSyncSetInterval(pluginId, Number(minutes) || 0);
});
},
resetKey: function() {
assertActive('sync.resetKey');
return callBackendErrorString(pluginId, 'sync.resetKey', function() {
return App.PluginSyncResetKey(pluginId);
});
},
now: function() {
assertActive('sync.now');
return callBackend(pluginId, 'sync.now', function() {
return App.PluginSyncNow(pluginId);
});
}
},
workbench: {
openResource: async function(request) {
assertActive('workbench.openResource');
const result = await callBackend(pluginId, 'workbench.openResource', function() {
return App.OpenWorkbenchResource(pluginId, request || {});
});
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
return result;
},
editResource: async function(request) {
assertActive('workbench.editResource');
const result = await callBackend(pluginId, 'workbench.editResource', function() {
return App.EditWorkbenchResource(pluginId, request || {});
});
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
return result;
}
},
contributions: {
list: async function(point) {
assertActive('contributions.list');
const summary = await callBackend(pluginId, 'contributions.list', function() {
return App.GetContributions();
});
if (!point) {
return summary || {};
}
return Array.isArray((summary || {})[point]) ? summary[point] : [];
}
},
commands: {
register: function(cmdId, handler) {
assertActive('commands.register(' + cmdId + ')');
if (!cmdId) {
throw new Error('commands.register requires a command id');
}
if (typeof handler !== 'function') {
throw new Error('commands.register requires a handler function');
}
return callBackend(pluginId, 'commands.register(' + cmdId + ')', function() {
return App.ExecutePluginCommand(pluginId, cmdId, { validateOnly: true });
}).then(function() {
const key = commandKey(pluginId, cmdId);
window.__VERSTAK_COMMAND_HANDLERS__[key] = handler;
return trackCleanup(function unregisterCommand() {
if (window.__VERSTAK_COMMAND_HANDLERS__[key] === handler) {
delete window.__VERSTAK_COMMAND_HANDLERS__[key];
}
});
});
},
execute: async function(cmdId, args) {
assertActive('commands.execute(' + cmdId + ')');
return executePluginCommand(pluginId, cmdId, args || {});
},
executeFor: async function(targetPluginId, cmdId, args) {
assertActive('commands.executeFor(' + targetPluginId + ':' + cmdId + ')');
return executePluginCommand(targetPluginId, cmdId, args || {});
}
},
dispose: function() {
if (disposed) return;
disposed = true;
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
try {
cleanup();
} catch (e) {
console.error('[VerstakPluginAPI] cleanup error:', e);
}
}
}
};
}
window.createPluginAPI = createPluginAPI;
window.VerstakPluginAPI = createPluginAPI;