Implement milestone 6b workbench routing skeleton
This commit is contained in:
+167
-4
@@ -3,7 +3,11 @@
|
||||
import Sidebar from './lib/shell/Sidebar.svelte';
|
||||
import ViewContainer from './lib/shell/ViewContainer.svelte';
|
||||
import VaultSelection from './lib/shell/VaultSelection.svelte';
|
||||
import WorkbenchHost from './lib/shell/WorkbenchHost.svelte';
|
||||
import * as App from '../wailsjs/go/api/App';
|
||||
import { debug } from './lib/log/debug.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
let currentView = 'plugin-manager';
|
||||
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
|
||||
@@ -14,47 +18,81 @@
|
||||
let activeViewPluginId = '';
|
||||
let activeSettingsPluginId = '';
|
||||
let activeSettingsPanelId = '';
|
||||
let openedResource = null;
|
||||
|
||||
function flog(msg) {
|
||||
App.WriteFrontendLog('App', msg);
|
||||
}
|
||||
|
||||
async function checkVault() {
|
||||
debug.log('[App] checkVault: START');
|
||||
flog('checkVault: START');
|
||||
loading = true;
|
||||
try {
|
||||
debug.log('[App] checkVault: calling GetAppSettings...');
|
||||
const settings = await App.GetAppSettings();
|
||||
debug.log('[App] checkVault: GetAppSettings returned', settings);
|
||||
flog('checkVault: GetAppSettings returned');
|
||||
|
||||
debug.log('[App] checkVault: calling GetVaultStatus...');
|
||||
vaultStatus = await App.GetVaultStatus() || { status: 'unknown', path: '', vaultId: '' };
|
||||
debug.log('[App] checkVault: GetVaultStatus returned', vaultStatus);
|
||||
flog('checkVault: vaultStatus=' + vaultStatus.status);
|
||||
|
||||
if (!settings.currentVaultPath || vaultStatus.status !== 'open') {
|
||||
debug.log('[App] checkVault: vault not open, needsVaultSelection=true');
|
||||
flog('checkVault: needsVaultSelection=true');
|
||||
needsVaultSelection = true;
|
||||
} else {
|
||||
debug.log('[App] checkVault: vault open, needsVaultSelection=false');
|
||||
flog('checkVault: needsVaultSelection=false');
|
||||
needsVaultSelection = false;
|
||||
}
|
||||
} catch (e) {
|
||||
debug.log('[App] checkVault: ERROR', String(e));
|
||||
flog('checkVault: ERROR: ' + String(e));
|
||||
console.error('[App] startup check failed:', e);
|
||||
needsVaultSelection = true;
|
||||
}
|
||||
loading = false;
|
||||
await tick();
|
||||
debug.log('[App] checkVault: END, loading=false');
|
||||
flog('checkVault: END, loading=false');
|
||||
}
|
||||
|
||||
function onVaultOpened() {
|
||||
debug.log('[App] onVaultOpened');
|
||||
needsVaultSelection = false;
|
||||
vaultStatus = { status: 'open', path: '', vaultId: '' };
|
||||
}
|
||||
|
||||
function onNav(e) {
|
||||
debug.log('[App] onNav:', e.detail.viewId);
|
||||
currentView = e.detail.viewId;
|
||||
}
|
||||
|
||||
function onOpenView(e) {
|
||||
debug.log('[App] onOpenView:', e.detail.viewId, 'plugin:', e.detail.pluginId);
|
||||
activeView = e.detail.viewId;
|
||||
activeViewPluginId = e.detail.pluginId || '';
|
||||
currentView = 'plugin-view';
|
||||
}
|
||||
|
||||
function onOpenSettings(e) {
|
||||
debug.log('[App] onOpenSettings:', e.detail.pluginId, e.detail.panelId);
|
||||
activeSettingsPluginId = e.detail.pluginId;
|
||||
activeSettingsPanelId = e.detail.panelId || '';
|
||||
currentView = 'plugin-manager';
|
||||
}
|
||||
|
||||
function onWorkbenchOpened(e) {
|
||||
debug.log('[App] onWorkbenchOpened:', e.detail?.request?.path, e.detail?.providerId);
|
||||
openedResource = e.detail;
|
||||
currentView = 'workbench';
|
||||
}
|
||||
|
||||
function onCloseSettings() {
|
||||
debug.log('[App] onCloseSettings');
|
||||
activeSettingsPluginId = '';
|
||||
activeSettingsPanelId = '';
|
||||
}
|
||||
@@ -66,9 +104,10 @@
|
||||
window.addEventListener('verstak:open-view', onOpenView);
|
||||
window.addEventListener('verstak:open-settings', onOpenSettings);
|
||||
window.addEventListener('verstak:close-settings', onCloseSettings);
|
||||
window.addEventListener('verstak:workbench-opened', onWorkbenchOpened);
|
||||
}
|
||||
|
||||
checkVault();
|
||||
onMount(() => { checkVault(); });
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
@@ -81,9 +120,11 @@
|
||||
<main>
|
||||
<Sidebar />
|
||||
|
||||
<section class="content">
|
||||
<section class="content scroll-surface">
|
||||
{#if currentView === 'plugin-manager'}
|
||||
<PluginManager {activeSettingsPluginId} {activeSettingsPanelId} />
|
||||
{:else if currentView === 'workbench'}
|
||||
<WorkbenchHost {openedResource} />
|
||||
{:else}
|
||||
<ViewContainer {activeView} {activeViewPluginId} />
|
||||
{/if}
|
||||
@@ -98,6 +139,13 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(html),
|
||||
:global(body),
|
||||
:global(#app) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1a1a2e;
|
||||
@@ -105,6 +153,118 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(button) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
min-height: 2rem;
|
||||
padding: 0.4rem 0.85rem;
|
||||
border: 1px solid #1a3a5c;
|
||||
border-radius: 6px;
|
||||
background: #0f3460;
|
||||
color: #e0e0f0;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
:global(button:hover:not(:disabled)) {
|
||||
background: #1a3a5c;
|
||||
border-color: #4ecca3;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:global(button:focus-visible) {
|
||||
outline: 2px solid #4ecca3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
:global(button:disabled) {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:global(.btn-primary) {
|
||||
background: #4ecca3;
|
||||
border-color: #4ecca3;
|
||||
color: #101827;
|
||||
}
|
||||
|
||||
:global(.btn-primary:hover:not(:disabled)) {
|
||||
background: #63d9b3;
|
||||
border-color: #63d9b3;
|
||||
color: #101827;
|
||||
}
|
||||
|
||||
:global(.btn-secondary) {
|
||||
background: #0f3460;
|
||||
border-color: #533483;
|
||||
color: #e0e0f0;
|
||||
}
|
||||
|
||||
:global(.btn-danger) {
|
||||
background: #e94560;
|
||||
border-color: #e94560;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:global(.btn-danger:hover:not(:disabled)) {
|
||||
background: #ff5b73;
|
||||
border-color: #ff5b73;
|
||||
}
|
||||
|
||||
:global(.btn-ghost) {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: #a0a0b8;
|
||||
}
|
||||
|
||||
:global(.btn-ghost:hover:not(:disabled)) {
|
||||
background: rgba(15, 52, 96, 0.55);
|
||||
border-color: #0f3460;
|
||||
color: #e0e0f0;
|
||||
}
|
||||
|
||||
:global(.btn-icon) {
|
||||
width: 2rem;
|
||||
min-width: 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.scroll-surface) {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
:global(*) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #0f3460 #1a1a2e;
|
||||
}
|
||||
|
||||
:global(*::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
:global(*::-webkit-scrollbar-track) {
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
:global(*::-webkit-scrollbar-thumb) {
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global(*::-webkit-scrollbar-thumb:hover) {
|
||||
background: #1a4a7a;
|
||||
}
|
||||
|
||||
.app-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -118,14 +278,17 @@
|
||||
main {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background: #1a1a2e;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 1.5rem;
|
||||
padding: clamp(1rem, 2vw, 1.5rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Frontend debug logger.
|
||||
*
|
||||
* Enabled when:
|
||||
* 1. URL has ?debug query param, OR
|
||||
* 2. localStorage has verstak-debug = "true"
|
||||
*
|
||||
* Writes to:
|
||||
* - console (always, with [debug] prefix)
|
||||
* - localStorage buffer (last 1000 entries, key: verstak-debug-log)
|
||||
*
|
||||
* Usage:
|
||||
* import { debug } from '../log/debug.js';
|
||||
* debug.log('[ComponentName]', 'message', data);
|
||||
* debug.logf('[ComponentName]', 'format %s', arg);
|
||||
*
|
||||
* To enable: open app with ?debug or run in console:
|
||||
* localStorage.setItem('verstak-debug', 'true')
|
||||
*
|
||||
* To export log: run in console:
|
||||
* copy(JSON.parse(localStorage.getItem('verstak-debug-log')))
|
||||
*/
|
||||
|
||||
var ENABLED = false;
|
||||
var BUFFER_KEY = 'verstak-debug-log';
|
||||
var MAX_ENTRIES = 1000;
|
||||
|
||||
// Check enable conditions
|
||||
function checkEnabled() {
|
||||
try {
|
||||
if (window.location && window.location.search && window.location.search.indexOf('debug') !== -1) {
|
||||
return true;
|
||||
}
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('verstak-debug') === 'true') {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage not available
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ENABLED = checkEnabled();
|
||||
|
||||
function getTimestamp() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function formatMessage(args) {
|
||||
var parts = [];
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var a = args[i];
|
||||
if (typeof a === 'object') {
|
||||
try {
|
||||
parts.push(JSON.stringify(a));
|
||||
} catch (e) {
|
||||
parts.push(String(a));
|
||||
}
|
||||
} else {
|
||||
parts.push(String(a));
|
||||
}
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function writeToBuffer(entry) {
|
||||
try {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
var raw = localStorage.getItem(BUFFER_KEY);
|
||||
var log = raw ? JSON.parse(raw) : [];
|
||||
log.push(entry);
|
||||
if (log.length > MAX_ENTRIES) {
|
||||
log = log.slice(log.length - MAX_ENTRIES);
|
||||
}
|
||||
localStorage.setItem(BUFFER_KEY, JSON.stringify(log));
|
||||
} catch (e) {
|
||||
// Ignore quota errors
|
||||
}
|
||||
}
|
||||
|
||||
function log() {
|
||||
if (!ENABLED) return;
|
||||
var msg = formatMessage(Array.prototype.slice.call(arguments));
|
||||
var entry = { ts: getTimestamp(), msg: msg };
|
||||
writeToBuffer(entry);
|
||||
console.log('[debug]', msg);
|
||||
}
|
||||
|
||||
function logf() {
|
||||
if (!ENABLED) return;
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var format = args.shift();
|
||||
var i = 0;
|
||||
var msg = format.replace(/%[sdfo]/g, function () {
|
||||
return i < args.length ? String(args[i++]) : '';
|
||||
});
|
||||
var entry = { ts: getTimestamp(), msg: msg };
|
||||
writeToBuffer(entry);
|
||||
console.log('[debug]', msg);
|
||||
}
|
||||
|
||||
function getLog() {
|
||||
try {
|
||||
if (typeof localStorage === 'undefined') return [];
|
||||
var raw = localStorage.getItem(BUFFER_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(BUFFER_KEY);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function exportLog() {
|
||||
var entries = getLog();
|
||||
return entries.map(function (e) { return e.ts + ' ' + e.msg; }).join('\n');
|
||||
}
|
||||
|
||||
// Named export for Svelte/Vite
|
||||
export var debug = {
|
||||
log: log,
|
||||
logf: logf,
|
||||
isEnabled: function () { return ENABLED; },
|
||||
enable: function () {
|
||||
ENABLED = true;
|
||||
try { localStorage.setItem('verstak-debug', 'true'); } catch (e) {}
|
||||
},
|
||||
disable: function () {
|
||||
ENABLED = false;
|
||||
try { localStorage.removeItem('verstak-debug'); } catch (e) {}
|
||||
},
|
||||
getLog: getLog,
|
||||
clearLog: clearLog,
|
||||
exportLog: exportLog
|
||||
};
|
||||
|
||||
// Also expose globally for console access
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__verstakDebug = debug;
|
||||
}
|
||||
|
||||
if (ENABLED) {
|
||||
console.log('[debug] frontend debug logger enabled');
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
|
||||
// Import the VerstakPluginAPI contract
|
||||
import './VerstakPluginAPI.js';
|
||||
import { createPluginAPI } from './VerstakPluginAPI.js';
|
||||
|
||||
export let pluginId = null;
|
||||
export let componentId = null;
|
||||
export let viewPluginId = null;
|
||||
export let componentProps = {};
|
||||
|
||||
let loadState = 'idle'; // idle | loading | loaded | error
|
||||
let pluginInfo = null;
|
||||
@@ -16,6 +16,7 @@
|
||||
let mountContainer = null;
|
||||
let currentPluginId = null;
|
||||
let currentComponent = null;
|
||||
let currentAPI = null;
|
||||
|
||||
$: activePluginId = pluginId || viewPluginId;
|
||||
$: activeComponent = componentId;
|
||||
@@ -33,6 +34,13 @@
|
||||
});
|
||||
|
||||
function cleanup() {
|
||||
if (currentAPI && typeof currentAPI.dispose === 'function') {
|
||||
try {
|
||||
currentAPI.dispose();
|
||||
} catch (e) {
|
||||
console.error('[PluginBundleHost] API dispose error:', e);
|
||||
}
|
||||
}
|
||||
const reg = window.__VERSTAK_PLUGIN_REGISTRY__;
|
||||
if (currentPluginId && currentComponent && reg && reg[currentPluginId]) {
|
||||
const comp = reg[currentPluginId][currentComponent];
|
||||
@@ -49,6 +57,14 @@
|
||||
}
|
||||
currentPluginId = null;
|
||||
currentComponent = null;
|
||||
currentAPI = null;
|
||||
}
|
||||
|
||||
function unpackBackendResult(result) {
|
||||
if (Array.isArray(result) && result.length === 2 && (typeof result[1] === 'string' || result[1] == null)) {
|
||||
return { value: result[0], error: result[1] || '' };
|
||||
}
|
||||
return { value: result, error: '' };
|
||||
}
|
||||
|
||||
async function loadAndMount(pId, compId) {
|
||||
@@ -82,10 +98,11 @@
|
||||
const reg = window.__VERSTAK_PLUGIN_REGISTRY__ || {};
|
||||
if (!reg[pId]) {
|
||||
// Load the bundle JS content via backend API
|
||||
const [content, err] = await App.GetPluginAssetContent(pId, info.entry);
|
||||
if (err || !content) {
|
||||
const assetResult = unpackBackendResult(await App.GetPluginAssetContent(pId, info.entry));
|
||||
const content = assetResult.value;
|
||||
if (assetResult.error || !content) {
|
||||
loadState = 'error';
|
||||
errorText = 'Failed to load bundle: ' + (err || 'empty content');
|
||||
errorText = 'Failed to load bundle: ' + (assetResult.error || 'empty content');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,7 +137,8 @@
|
||||
}
|
||||
|
||||
// Create API
|
||||
const api = window.VerstakPluginAPI(pId);
|
||||
const api = createPluginAPI(pId);
|
||||
currentAPI = api;
|
||||
|
||||
// Mount component
|
||||
if (!mountContainer) {
|
||||
@@ -129,7 +147,7 @@
|
||||
}
|
||||
if (mountContainer) {
|
||||
try {
|
||||
comp.mount(mountContainer, { componentId: compId }, api);
|
||||
comp.mount(mountContainer, Object.assign({ componentId: compId }, componentProps || {}), api);
|
||||
loadState = 'loaded';
|
||||
errorText = '';
|
||||
} catch (e) {
|
||||
@@ -161,12 +179,6 @@
|
||||
<p>Select a plugin view from the sidebar</p>
|
||||
</div>
|
||||
|
||||
{:else if loadState === 'loading'}
|
||||
<div class="host-state loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading plugin bundle...</p>
|
||||
</div>
|
||||
|
||||
{:else if loadState === 'error'}
|
||||
<div class="host-state error">
|
||||
<Icon name="warning" size={24} className="error-icon" />
|
||||
@@ -184,9 +196,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if loadState === 'loaded'}
|
||||
{:else}
|
||||
{#if loadState === 'loading'}
|
||||
<div class="host-state loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading plugin bundle...</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="plugin-mount-container"
|
||||
class:mount-hidden={loadState !== 'loaded'}
|
||||
bind:this={mountContainer}
|
||||
data-plugin-id={currentPluginId}
|
||||
data-component={currentComponent}
|
||||
@@ -197,10 +216,10 @@
|
||||
<style>
|
||||
.plugin-bundle-host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.host-state {
|
||||
@@ -286,8 +305,14 @@
|
||||
}
|
||||
|
||||
.plugin-mount-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.plugin-mount-container.mount-hidden {
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
// VerstakPluginAPI is the restricted API passed to plugin frontend bundles.
|
||||
// Plugins do NOT get direct access to Wails bridge — only what's exposed here.
|
||||
// All methods are stubs or limited implementations.
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
|
||||
(function() {
|
||||
// Store registered components per plugin
|
||||
window.__VERSTAK_PLUGIN_REGISTRY__ = window.__VERSTAK_PLUGIN_REGISTRY__ || {};
|
||||
|
||||
// Original register function
|
||||
const origRegister = window.VerstakPluginRegister;
|
||||
if (origRegister) {
|
||||
// Already defined — don't override
|
||||
return;
|
||||
}
|
||||
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);
|
||||
@@ -21,48 +13,291 @@
|
||||
console.log('[VerstakPluginRegister] registered:', pluginId, Object.keys(bundle.components));
|
||||
window.__VERSTAK_PLUGIN_REGISTRY__[pluginId] = bundle.components;
|
||||
};
|
||||
}
|
||||
|
||||
// Create the restricted API object for a plugin host context
|
||||
window.VerstakPluginAPI = function(pluginId) {
|
||||
return {
|
||||
pluginId: pluginId,
|
||||
function unpack(result) {
|
||||
if (Array.isArray(result) && result.length === 2 && (typeof result[1] === 'string' || result[1] == null)) {
|
||||
return [result[0], result[1] || ''];
|
||||
}
|
||||
return [result, ''];
|
||||
}
|
||||
|
||||
capabilities: {
|
||||
has: function(capId) {
|
||||
// planned: query backend cap registry
|
||||
console.log('[plugin:' + pluginId + '] capabilities.has(' + capId + ') — stub');
|
||||
return false;
|
||||
}
|
||||
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 commandKey(pluginId, commandId) {
|
||||
return pluginId + ':' + commandId;
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
capabilities: {
|
||||
has: async function(capId) {
|
||||
const info = await callBackend(pluginId, 'capabilities.has(' + capId + ')', function() {
|
||||
return App.GetPluginCapability(pluginId, capId);
|
||||
});
|
||||
return !!(info && info.available);
|
||||
},
|
||||
|
||||
events: {
|
||||
publish: function(type, payload) {
|
||||
console.log('[plugin:' + pluginId + '] event publish:', type, payload);
|
||||
// planned: actual event bus bridge
|
||||
},
|
||||
subscribe: function(type, handler) {
|
||||
console.log('[plugin:' + pluginId + '] event subscribe:', type, '(stub)');
|
||||
// planned: actual event bus bridge
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
settings: {
|
||||
read: function(key) {
|
||||
console.log('[plugin:' + pluginId + '] settings.read(' + key + ') — stub');
|
||||
return null;
|
||||
},
|
||||
write: function(key, value) {
|
||||
console.log('[plugin:' + pluginId + '] settings.write(' + key + ',', value, ') — stub');
|
||||
// planned: backend storage namespace
|
||||
}
|
||||
events: {
|
||||
publish: async function(type, payload) {
|
||||
await callBackendErrorString(pluginId, 'events.publish(' + type + ')', function() {
|
||||
return App.PublishPluginEvent(pluginId, type, payload || {});
|
||||
});
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
commands: {
|
||||
execute: function(cmdId, args) {
|
||||
console.log('[plugin:' + pluginId + '] commands.execute(' + cmdId + ') — stub');
|
||||
// planned: command execution
|
||||
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 || {});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
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 || {});
|
||||
});
|
||||
},
|
||||
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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
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 + ')');
|
||||
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
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.plugin-card.disabled {
|
||||
@@ -225,6 +226,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -232,6 +235,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.plugin-id strong {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -268,7 +277,7 @@
|
||||
|
||||
.card-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
@@ -277,6 +286,7 @@
|
||||
.meta-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
@@ -288,6 +298,8 @@
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #a0a0b8;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -316,6 +328,8 @@
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
color: #e0e0e0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tag.provides {
|
||||
@@ -375,6 +389,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #0f3460;
|
||||
@@ -432,4 +447,19 @@
|
||||
font-size: 0.75rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.card-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
import PluginCard from './PluginCard.svelte';
|
||||
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { GetPlugins, GetCapabilities, GetPermissions, GetContributions, ReloadPlugins, GetVaultStatus, GetVaultPluginState, EnablePlugin, DisablePlugin, ReadPluginSettings, WritePluginSettings, GetPluginFrontendInfo } from '../../../wailsjs/go/api/App';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { GetPlugins, GetCapabilities, GetPermissions, GetContributions, ReloadPlugins, GetVaultStatus, GetVaultPluginState, EnablePlugin, DisablePlugin, ReadPluginSettings, WritePluginSettings, GetPluginFrontendInfo, WriteFrontendLog } from '../../../wailsjs/go/api/App';
|
||||
import { debug } from '../log/debug.js';
|
||||
|
||||
let plugins = [];
|
||||
let capabilities = [];
|
||||
@@ -45,6 +46,28 @@
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function notifyPluginsChanged() {
|
||||
window.dispatchEvent(new CustomEvent('verstak:plugins-changed'));
|
||||
}
|
||||
|
||||
function unpackBackendResult(result) {
|
||||
if (Array.isArray(result) && result.length === 2 && (typeof result[1] === 'string' || result[1] == null)) {
|
||||
return { value: result[0], error: result[1] || '' };
|
||||
}
|
||||
return { value: result, error: '' };
|
||||
}
|
||||
|
||||
function unpackReloadResult(result) {
|
||||
if (Array.isArray(result)) {
|
||||
return {
|
||||
count: Number(result[0] || 0),
|
||||
summary: result[1] || `Reloaded ${Number(result[0] || 0)} plugin(s).`,
|
||||
};
|
||||
}
|
||||
const count = Number(result || 0);
|
||||
return { count, summary: `Reloaded ${count} plugin(s).` };
|
||||
}
|
||||
|
||||
async function openSettingsFromProps(pluginId, panelId) {
|
||||
const panel = (contributions.settingsPanels || []).find(sp => sp.pluginId === pluginId && (!panelId || sp.id === panelId));
|
||||
if (panel) {
|
||||
@@ -55,8 +78,14 @@
|
||||
const info = await GetPluginFrontendInfo(pluginId);
|
||||
settingsPluginInfo = info;
|
||||
} catch { settingsPluginInfo = null; }
|
||||
ReadPluginSettings(pluginId).then(data => {
|
||||
settingsData = data || {};
|
||||
ReadPluginSettings(pluginId).then(result => {
|
||||
const unpacked = unpackBackendResult(result);
|
||||
if (unpacked.error) {
|
||||
settingsError = unpacked.error;
|
||||
settingsData = {};
|
||||
return;
|
||||
}
|
||||
settingsData = unpacked.value || {};
|
||||
}).catch(() => { settingsData = {}; });
|
||||
} else {
|
||||
settingsError = `Settings panel not found for plugin "${pluginId}". Check that the plugin is enabled and has settingsPanels in its manifest.`;
|
||||
@@ -73,18 +102,27 @@
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
debug.log('[PluginManager] loadAll: START');
|
||||
error = '';
|
||||
loading = true;
|
||||
try {
|
||||
debug.log('[PluginManager] loadAll: calling GetPlugins...');
|
||||
const p = await GetPlugins();
|
||||
plugins = p || [];
|
||||
debug.log('[PluginManager] loadAll: GetPlugins returned', plugins.length, 'plugins');
|
||||
for (var i = 0; i < plugins.length; i++) {
|
||||
debug.log('[PluginManager] loadAll: plugin[' + i + ']:', plugins[i].manifest?.id, 'status:', plugins[i].status, 'enabled:', plugins[i].enabled);
|
||||
}
|
||||
} catch (e) {
|
||||
debug.log('[PluginManager] loadAll: GetPlugins ERROR:', String(e));
|
||||
WriteFrontendLog('PluginManager', 'loadAll: GetPlugins ERROR: ' + String(e));
|
||||
error = 'GetPlugins: ' + String(e);
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
// Collect all async loads but await them so loading stays true until all are done
|
||||
try {
|
||||
debug.log('[PluginManager] loadAll: loading vault/capabilities/permissions/contributions...');
|
||||
const [v, caps, perms, contribs] = await Promise.all([
|
||||
GetVaultStatus().catch(() => ({ status: 'unknown', path: '', vaultId: '' })),
|
||||
GetCapabilities().catch(() => []),
|
||||
@@ -95,66 +133,93 @@
|
||||
capabilities = caps || [];
|
||||
permissions = perms || [];
|
||||
contributions = contribs || {};
|
||||
debug.log('[PluginManager] loadAll: vault=' + vaultStatus.status + ' caps=' + capabilities.length + ' perms=' + permissions.length);
|
||||
WriteFrontendLog('PluginManager', 'loadAll: vault=' + vaultStatus.status + ' caps=' + capabilities.length + ' perms=' + permissions.length);
|
||||
} catch (e) {
|
||||
// Non-critical — log but don't fail
|
||||
debug.log('[PluginManager] loadAll: non-critical load ERROR:', String(e));
|
||||
WriteFrontendLog('PluginManager', 'loadAll: non-critical ERROR: ' + String(e));
|
||||
console.error('[PluginManager] non-critical load error:', e);
|
||||
}
|
||||
if (vaultStatus.status === 'open') {
|
||||
try {
|
||||
debug.log('[PluginManager] loadAll: calling GetVaultPluginState...');
|
||||
vaultPluginState = await GetVaultPluginState() || { enabledPlugins: [], disabledPlugins: [], desiredPlugins: [] };
|
||||
} catch { /* non-critical */ }
|
||||
WriteFrontendLog('PluginManager', 'loadAll: GetVaultPluginState returned');
|
||||
} catch (e) {
|
||||
WriteFrontendLog('PluginManager', 'loadAll: GetVaultPluginState ERROR: ' + String(e));
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
await tick();
|
||||
debug.log('[PluginManager] loadAll: END, loading=false');
|
||||
WriteFrontendLog('PluginManager', 'loadAll: END, loading=false');
|
||||
}
|
||||
|
||||
onMount(() => { loadAll(); });
|
||||
|
||||
async function reload() {
|
||||
debug.log('[PluginManager] reload: START');
|
||||
reloading = true;
|
||||
error = '';
|
||||
let resultMsg = '';
|
||||
try {
|
||||
const [count, summary] = await ReloadPlugins();
|
||||
debug.log('[PluginManager] reload: calling ReloadPlugins...');
|
||||
const { count, summary } = unpackReloadResult(await ReloadPlugins());
|
||||
debug.log('[PluginManager] reload: ReloadPlugins returned count=' + count + ' summary=' + summary);
|
||||
resultMsg = `Reloaded ${count} plugin(s). ${summary}`;
|
||||
} catch (e) {
|
||||
debug.log('[PluginManager] reload: ReloadPlugins ERROR:', String(e));
|
||||
error = 'Reload: ' + String(e);
|
||||
reloading = false;
|
||||
return;
|
||||
}
|
||||
debug.log('[PluginManager] reload: calling loadAll after reload...');
|
||||
await loadAll();
|
||||
notifyPluginsChanged();
|
||||
reloading = false;
|
||||
debug.log('[PluginManager] reload: END');
|
||||
showToast(resultMsg, 'success');
|
||||
}
|
||||
|
||||
async function enablePlugin(pluginId) {
|
||||
debug.log('[PluginManager] enablePlugin:', pluginId);
|
||||
actionFeedback = { ...actionFeedback, [pluginId]: 'enabling' };
|
||||
error = '';
|
||||
const err = await EnablePlugin(pluginId);
|
||||
if (err) {
|
||||
debug.log('[PluginManager] enablePlugin: ERROR:', err);
|
||||
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
||||
error = 'Enable: ' + err;
|
||||
return;
|
||||
}
|
||||
debug.log('[PluginManager] enablePlugin: success, reloading...');
|
||||
// Reload to get updated state
|
||||
try { await ReloadPlugins(); } catch (e) { /* ignore */ }
|
||||
await loadAll();
|
||||
notifyPluginsChanged();
|
||||
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
||||
debug.log('[PluginManager] enablePlugin: done');
|
||||
showToast(`Plugin "${pluginId}" enabled`, 'success');
|
||||
}
|
||||
|
||||
async function disablePlugin(pluginId) {
|
||||
debug.log('[PluginManager] disablePlugin:', pluginId);
|
||||
actionFeedback = { ...actionFeedback, [pluginId]: 'disabling' };
|
||||
error = '';
|
||||
const err = await DisablePlugin(pluginId);
|
||||
if (err) {
|
||||
debug.log('[PluginManager] disablePlugin: ERROR:', err);
|
||||
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
||||
error = 'Disable: ' + err;
|
||||
return;
|
||||
}
|
||||
debug.log('[PluginManager] disablePlugin: success, reloading...');
|
||||
// Reload to get updated state
|
||||
try { await ReloadPlugins(); } catch (e) { /* ignore */ }
|
||||
await loadAll();
|
||||
notifyPluginsChanged();
|
||||
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
||||
debug.log('[PluginManager] disablePlugin: done');
|
||||
showToast(`Plugin "${pluginId}" disabled`, 'info');
|
||||
}
|
||||
|
||||
@@ -327,14 +392,18 @@
|
||||
|
||||
<style>
|
||||
.plugin-manager {
|
||||
max-width: 900px;
|
||||
padding-top: 0.5rem;
|
||||
flex: 1;
|
||||
width: min(100%, 1100px);
|
||||
min-height: 0;
|
||||
padding: 0.5rem 0.5rem 1.5rem 0;
|
||||
position: relative;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
@@ -343,14 +412,20 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
h2 { color: #e0e0e0; font-size: 1.3rem; margin: 0; }
|
||||
.vault-badge {
|
||||
max-width: 100%;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.vault-open { background: rgba(78, 204, 163, 0.15); color: #4ecca3; border-color: #4ecca3; }
|
||||
.vault-not-created { background: rgba(255, 200, 87, 0.15); color: #ffc857; border-color: #ffc857; }
|
||||
@@ -404,7 +479,7 @@
|
||||
.hint-list { list-style: none; padding: 0; margin: 0.5rem 0; font-size: 0.8rem; opacity: 0.7; }
|
||||
.hint-list li { margin: 0.25rem 0; }
|
||||
.hint code { background: #0f3460; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
||||
.plugin-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; }
|
||||
.plugin-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; min-width: 0; }
|
||||
|
||||
.missing-section { margin-bottom: 1.5rem; }
|
||||
.missing-section h3 { color: #e94560; font-size: 1rem; margin: 0 0 0.25rem; }
|
||||
@@ -450,4 +525,24 @@
|
||||
.modal-body { padding: 1rem; overflow-y: auto; }
|
||||
.settings-hint { color: #666; font-size: 0.8rem; margin: 0.25rem 0; }
|
||||
.settings-hint code { color: #4ecca3; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.plugin-manager {
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.reload-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: min(480px, calc(100vw - 2rem));
|
||||
max-height: calc(100vh - 2rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
import WorkspaceTree from './WorkspaceTree.svelte';
|
||||
import Icon from '../ui/Icon.svelte';
|
||||
import { debug } from '../log/debug.js';
|
||||
|
||||
function flog(msg) {
|
||||
App.WriteFrontendLog('Sidebar', msg);
|
||||
}
|
||||
|
||||
let plugins = [];
|
||||
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
|
||||
@@ -15,9 +20,13 @@
|
||||
|
||||
$: vaultOpen = vaultStatus.status === 'open';
|
||||
|
||||
onMount(async () => {
|
||||
async function loadSidebar() {
|
||||
debug.log('[Sidebar] onMount: START');
|
||||
flog('onMount: START');
|
||||
let contribErr = false;
|
||||
try {
|
||||
debug.log('[Sidebar] onMount: loading plugins/vault/contributions...');
|
||||
flog('onMount: loading plugins/vault/contributions...');
|
||||
const [p, v, contribs] = await Promise.all([
|
||||
App.GetPlugins().catch(() => []),
|
||||
App.GetVaultStatus().catch(() => ({ status: 'unknown', path: '', vaultId: '' })),
|
||||
@@ -25,6 +34,8 @@
|
||||
]);
|
||||
plugins = p || [];
|
||||
vaultStatus = v;
|
||||
debug.log('[Sidebar] onMount: plugins=' + plugins.length + ' vault=' + vaultStatus.status);
|
||||
flog('onMount: plugins=' + plugins.length + ' vault=' + vaultStatus.status);
|
||||
if (contribErr) {
|
||||
errorMessage = 'Failed to load plugin contributions';
|
||||
}
|
||||
@@ -34,17 +45,34 @@
|
||||
return plugin.status !== 'disabled' && plugin.status !== 'failed' && plugin.status !== 'incompatible' && plugin.status !== 'missing-required-capability';
|
||||
});
|
||||
sidebarItems.sort((a, b) => (a.position || 100) - (b.position || 100));
|
||||
debug.log('[Sidebar] onMount: sidebarItems=' + sidebarItems.length);
|
||||
flog('onMount: sidebarItems=' + sidebarItems.length);
|
||||
} catch (e) {
|
||||
debug.log('[Sidebar] onMount: ERROR:', String(e));
|
||||
flog('onMount: ERROR: ' + String(e));
|
||||
console.error('[Sidebar] load error:', e);
|
||||
errorMessage = 'Failed to load sidebar';
|
||||
}
|
||||
debug.log('[Sidebar] onMount: END');
|
||||
flog('onMount: END');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSidebar();
|
||||
window.addEventListener('verstak:plugins-changed', loadSidebar);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('verstak:plugins-changed', loadSidebar);
|
||||
});
|
||||
|
||||
function handleNav(id) {
|
||||
debug.log('[Sidebar] handleNav:', id);
|
||||
window.dispatchEvent(new CustomEvent('verstak:nav', { detail: { viewId: id } }));
|
||||
}
|
||||
|
||||
function handleSidebarItem(item) {
|
||||
debug.log('[Sidebar] handleSidebarItem:', item.id, '-> view:', item.view);
|
||||
// Use item.view (the view contribution ID) if available, fall back to item.id
|
||||
const viewId = item.view || item.id;
|
||||
window.dispatchEvent(new CustomEvent('verstak:open-view', { detail: { viewId, pluginId: item.pluginId } }));
|
||||
@@ -171,6 +199,7 @@
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.6rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: none;
|
||||
|
||||
@@ -101,13 +101,14 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
.view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.view.degraded {
|
||||
@@ -155,7 +156,7 @@
|
||||
}
|
||||
.view-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.placeholder {
|
||||
color: #666;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<script>
|
||||
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
||||
|
||||
export let openedResource = null;
|
||||
|
||||
$: providerPluginId = openedResource?.providerPluginId || '';
|
||||
$: providerComponent = openedResource?.providerComponent || '';
|
||||
$: resourcePath = openedResource?.request?.path || '';
|
||||
$: providerId = openedResource?.providerId || '';
|
||||
$: requestMode = openedResource?.request?.mode || 'view';
|
||||
$: requestContext = openedResource?.request?.context?.notesMode || openedResource?.request?.context?.isInsideNotesFolder
|
||||
? 'notes-markdown'
|
||||
: ((openedResource?.request?.extension === '.md' || openedResource?.request?.extension === '.markdown') ? 'generic-markdown' : 'generic-text');
|
||||
$: componentProps = openedResource || {};
|
||||
$: mountKey = [
|
||||
providerPluginId,
|
||||
providerComponent,
|
||||
resourcePath,
|
||||
requestMode,
|
||||
requestContext,
|
||||
].join(':');
|
||||
</script>
|
||||
|
||||
<div class="workbench-host">
|
||||
{#if openedResource?.status === 'no-provider'}
|
||||
<div class="workbench-header">
|
||||
<span class="workbench-title">{resourcePath}</span>
|
||||
<span class="workbench-provider">no-provider</span>
|
||||
</div>
|
||||
<div class="workbench-empty no-provider" data-workbench-status="no-provider">
|
||||
<p>No viewer/editor available</p>
|
||||
<p class="workbench-meta">{requestMode} · {requestContext}</p>
|
||||
</div>
|
||||
{:else if openedResource}
|
||||
<div class="workbench-header">
|
||||
<span class="workbench-title">{resourcePath}</span>
|
||||
<span class="workbench-provider">{providerId}</span>
|
||||
</div>
|
||||
<div class="workbench-content">
|
||||
{#key mountKey}
|
||||
<PluginBundleHost
|
||||
pluginId={providerPluginId}
|
||||
componentId={providerComponent}
|
||||
{componentProps}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="workbench-empty">
|
||||
<p>No resource opened</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.workbench-host {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
.workbench-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #16213e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workbench-title {
|
||||
color: #e0e0f0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workbench-provider {
|
||||
color: #4ecca3;
|
||||
font-size: 0.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.workbench-content {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.workbench-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.workbench-empty.no-provider {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.workbench-meta {
|
||||
margin: 0;
|
||||
color: #8b8ba8;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,15 @@
|
||||
<script context="module">
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const activeWorkspaceNodeId = writable('');
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
|
||||
export let nodes = [];
|
||||
export let node = null;
|
||||
export let currentNodeId = '';
|
||||
export let expandedNodes = {};
|
||||
export let depth = 0;
|
||||
@@ -32,6 +39,7 @@
|
||||
} else {
|
||||
nodes = result.nodes || [];
|
||||
currentNodeId = result.currentNodeId || '';
|
||||
activeWorkspaceNodeId.set(currentNodeId);
|
||||
const root = nodes.find(n => !n.parentId);
|
||||
if (root) expandedNodes[root.id] = true;
|
||||
}
|
||||
@@ -69,6 +77,7 @@
|
||||
const err = await App.SetCurrentWorkspaceNode(id);
|
||||
if (err) { localError = err; return; }
|
||||
currentNodeId = id;
|
||||
activeWorkspaceNodeId.set(id);
|
||||
}
|
||||
|
||||
function openCreate(parentId, type) {
|
||||
@@ -128,7 +137,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="wt-node" class:selected={node.id === currentNodeId} class:archived={node.status === 'archived'} class:sleeping={node.status === 'sleeping'}>
|
||||
<div class="wt-node" class:selected={node.id === $activeWorkspaceNodeId} class:archived={node.status === 'archived'} class:sleeping={node.status === 'sleeping'}>
|
||||
<div class="wt-row" style="padding-left: {depth * 1.0 + 0.4}rem;">
|
||||
{#if hasKids(node.id)}
|
||||
<button class="wt-expand" on:click={() => toggle(node.id)} type="button">{expandedNodes[node.id] ? '\u25BE' : '\u25B8'}</button>
|
||||
@@ -153,7 +162,7 @@
|
||||
.wt { display: flex; flex-direction: column; flex: 1; overflow: hidden; position: relative; }
|
||||
.wt-header { display: flex; align-items: center; justify-content: space-between; padding: 0.4rem 0.6rem; border-bottom: 1px solid #0f3460; flex-shrink: 0; }
|
||||
.wt-title { color: #a0a0b8; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
|
||||
.wt-btn { background: none; border: none; color: #666; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
||||
.wt-btn { min-height: 0; background: none; border: none; color: #666; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
||||
.wt-btn:hover { color: #4ecca3; background: rgba(78,204,163,0.1); }
|
||||
.wt-btn-small { font-size: 0.7rem; opacity: 0; }
|
||||
.wt-row:hover .wt-btn-small { opacity: 1; }
|
||||
@@ -162,12 +171,12 @@
|
||||
.wt-node { }
|
||||
.wt-row { display: flex; align-items: center; gap: 0.2rem; padding: 0.15rem 0; }
|
||||
.wt-row:hover { background: rgba(15,52,96,0.4); }
|
||||
.wt-row.selected { background: rgba(78,204,163,0.1); }
|
||||
.wt-expand { width: 1rem; height: 1rem; display: flex; align-items: center; justify-content: center; font-size: 0.65rem; color: #666; background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; }
|
||||
.wt-node.selected > .wt-row { background: rgba(78,204,163,0.1); }
|
||||
.wt-expand { width: 1rem; height: 1rem; min-height: 0; display: flex; align-items: center; justify-content: center; font-size: 0.65rem; color: #666; background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; }
|
||||
.wt-expand:hover { color: #e0e0f0; }
|
||||
.wt-expand-spacer { width: 1rem; flex-shrink: 0; }
|
||||
.wt-icon { font-size: 0.8rem; flex-shrink: 0; }
|
||||
.wt-label { flex: 1; background: none; border: none; color: #e0e0f0; font-size: 0.78rem; text-align: left; cursor: pointer; padding: 0.1rem 0.2rem; border-radius: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wt-label { flex: 1; min-height: 0; justify-content: flex-start; background: none; border: none; color: #e0e0f0; font-size: 0.78rem; text-align: left; cursor: pointer; padding: 0.1rem 0.2rem; border-radius: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wt-label:hover { color: #4ecca3; }
|
||||
.wt-node.archived .wt-label { text-decoration: line-through; opacity: 0.5; }
|
||||
.wt-node.sleeping .wt-label { opacity: 0.6; }
|
||||
|
||||
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* Wails Mock Bridge — эмулирует window['go']['api']['App'] для тестового окружения.
|
||||
*
|
||||
* Каждый метод возвращает Promise с данными, совместимыми с Wails-контрактом.
|
||||
* Состояние мутабельно — тесты могут менять его между сценариями.
|
||||
*/
|
||||
(function () {
|
||||
if (window.__wailsMockReady) return;
|
||||
|
||||
// ── Mutable state ──────────────────────────────────────────────────
|
||||
var pluginStates = {
|
||||
'verstak.platform-test': {
|
||||
status: 'loaded',
|
||||
enabled: true,
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
id: 'verstak.platform-test',
|
||||
name: 'Platform Test',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
description: 'Runtime test plugin for verifying the Verstak platform.',
|
||||
source: 'official',
|
||||
icon: '🧪',
|
||||
provides: ['verstak/platform-test/v1', 'verstak/diagnostics/v1'],
|
||||
requires: ['verstak/core/plugin-manager/v1', 'verstak/core/capability-registry/v1'],
|
||||
optionalRequires: ['verstak/core/vault/v1', 'verstak/core/sync/v1', 'verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['vault.read', 'events.publish', 'events.subscribe', 'ui.register', 'commands.register', 'storage.namespace', 'files.read', 'files.write', 'files.delete', 'workbench.open'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
views: [
|
||||
{ id: 'verstak.platform-test.diagnostics', title: 'Platform Diagnostics', icon: '🧪', component: 'DiagnosticsPanel' }
|
||||
],
|
||||
commands: [
|
||||
{ id: 'verstak.platform-test.run-tests', title: 'Run Platform Tests', handler: 'runAllTests' },
|
||||
{ id: 'verstak.platform-test.show-version', title: 'Show Version Info', handler: 'showVersion' }
|
||||
],
|
||||
sidebarItems: [
|
||||
{ id: 'verstak.platform-test.sidebar', title: 'Platform Test', icon: '🧪', view: 'verstak.platform-test.diagnostics', position: 100 }
|
||||
],
|
||||
statusBarItems: [
|
||||
{ id: 'verstak.platform-test.status', label: '🧪 All Tests Pass', position: 'right', handler: 'openDiagnostics' }
|
||||
],
|
||||
settingsPanels: [
|
||||
{ id: 'verstak.platform-test.settings', title: 'Platform Test Settings', icon: '🧪', component: 'PlatformTestSettings' }
|
||||
],
|
||||
openProviders: [
|
||||
{
|
||||
id: 'verstak.platform-test.markdown-diagnostic',
|
||||
title: 'Platform Test Markdown Diagnostic',
|
||||
priority: 100,
|
||||
component: 'MarkdownDiagnosticProvider',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
|
||||
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/platform-test',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
|
||||
var vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||||
var vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
|
||||
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||
var workbenchPreferences = {};
|
||||
var openedResources = [];
|
||||
var pluginSettings = {
|
||||
'verstak.platform-test': { savedText: 'initial value' }
|
||||
};
|
||||
var vaultFiles = makeDefaultVaultFiles();
|
||||
var workspaceTree = makeDefaultWorkspaceTree();
|
||||
var reloadResponseMode = 'tuple';
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
function makeDefaultWorkspaceTree() {
|
||||
return {
|
||||
status: 'initialized',
|
||||
currentNodeId: 'case-alpha',
|
||||
nodes: [
|
||||
{ id: 'space-main', parentId: '', type: 'space', title: 'Main Space', status: 'active', order: 1 },
|
||||
{ id: 'case-alpha', parentId: 'space-main', type: 'case', title: 'Alpha Case', status: 'active', order: 1 },
|
||||
{ id: 'case-beta', parentId: 'space-main', type: 'case', title: 'Beta Case', status: 'active', order: 2 }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function cloneWorkspaceTree() {
|
||||
return {
|
||||
status: workspaceTree.status,
|
||||
currentNodeId: workspaceTree.currentNodeId,
|
||||
nodes: workspaceTree.nodes.map(function (n) { return Object.assign({}, n); })
|
||||
};
|
||||
}
|
||||
|
||||
function makeDefaultVaultFiles() {
|
||||
return {
|
||||
'': { type: 'folder', modifiedAt: new Date().toISOString() }
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeVaultPath(relativePath, allowRoot) {
|
||||
var p = String(relativePath || '');
|
||||
if (p.indexOf('\x00') !== -1) return { error: 'invalid-path: null-byte' };
|
||||
if (p.indexOf('\\') !== -1) return { error: 'invalid-path: backslash not allowed' };
|
||||
if (p.indexOf('./') === 0) p = p.slice(2);
|
||||
if (!allowRoot && !p) return { error: 'invalid-path: empty path' };
|
||||
if (p.charAt(0) === '/' || /^[A-Za-z]:/.test(p)) return { error: 'invalid-path: absolute path rejected' };
|
||||
var parts = p.split('/').filter(Boolean);
|
||||
if (parts.indexOf('..') !== -1) return { error: 'invalid-path: path-traversal' };
|
||||
if (parts[0] && parts[0].toLowerCase() === '.verstak') return { error: 'reserved-path: .verstak is internal' };
|
||||
return { path: parts.join('/') };
|
||||
}
|
||||
|
||||
function parentPath(path) {
|
||||
var idx = path.lastIndexOf('/');
|
||||
return idx === -1 ? '' : path.slice(0, idx);
|
||||
}
|
||||
|
||||
function baseName(path) {
|
||||
var idx = path.lastIndexOf('/');
|
||||
return idx === -1 ? path : path.slice(idx + 1);
|
||||
}
|
||||
|
||||
function fileEntry(path, node) {
|
||||
var name = path ? baseName(path) : '';
|
||||
var ext = '';
|
||||
var dot = name.lastIndexOf('.');
|
||||
if (dot > 0) ext = name.slice(dot + 1);
|
||||
return {
|
||||
name: name,
|
||||
relativePath: path,
|
||||
type: node.type,
|
||||
size: node.type === 'file' ? (node.content || '').length : 0,
|
||||
modifiedAt: node.modifiedAt || new Date().toISOString(),
|
||||
extension: ext,
|
||||
isHidden: name.charAt(0) === '.',
|
||||
isReserved: false,
|
||||
canRead: node.type === 'file' || node.type === 'folder',
|
||||
canWrite: node.type === 'file' || node.type === 'folder'
|
||||
};
|
||||
}
|
||||
|
||||
function requirePluginPermission(pluginId, permission) {
|
||||
var s = pluginStates[pluginId];
|
||||
if (!s || !s.enabled || (s.status !== 'loaded' && s.status !== 'degraded')) {
|
||||
return 'plugin not enabled and loaded';
|
||||
}
|
||||
if (!s.manifest.permissions || s.manifest.permissions.indexOf(permission) === -1) {
|
||||
return 'plugin lacks required permission ' + permission;
|
||||
}
|
||||
if (vaultStatus.status !== 'open') return 'vault-not-open';
|
||||
return '';
|
||||
}
|
||||
|
||||
function makePlugin(id) {
|
||||
var s = pluginStates[id];
|
||||
if (!s) return null;
|
||||
return {
|
||||
manifest: s.manifest,
|
||||
status: s.status,
|
||||
enabled: s.enabled,
|
||||
rootPath: s.rootPath,
|
||||
error: s.error
|
||||
};
|
||||
}
|
||||
|
||||
function allPlugins() {
|
||||
return Object.keys(pluginStates).map(makePlugin).filter(Boolean);
|
||||
}
|
||||
|
||||
function allCapabilities() {
|
||||
var caps = [];
|
||||
caps.push({ name: 'verstak/core/plugin-manager/v1', description: 'Plugin management', pluginId: 'verstak-desktop', status: 'stable' });
|
||||
caps.push({ name: 'verstak/core/capability-registry/v1', description: 'Capability registry', pluginId: 'verstak-desktop', status: 'stable' });
|
||||
caps.push({ name: 'verstak/core/files/v1', description: 'Files API', pluginId: 'verstak-desktop', status: 'stable' });
|
||||
caps.push({ name: 'verstak/core/workbench/v1', description: 'Workbench routing', pluginId: 'verstak-desktop', status: 'stable' });
|
||||
for (var id in pluginStates) {
|
||||
var s = pluginStates[id];
|
||||
if (s.status === 'loaded' && s.enabled && s.manifest && s.manifest.provides) {
|
||||
s.manifest.provides.forEach(function (p) {
|
||||
caps.push({ name: p, description: '', pluginId: id, status: 'stable' });
|
||||
});
|
||||
}
|
||||
}
|
||||
return caps;
|
||||
}
|
||||
|
||||
function allPermissions() {
|
||||
return [
|
||||
{ name: 'vault.read', description: 'Read vault data', dangerous: false },
|
||||
{ name: 'events.publish', description: 'Publish events', dangerous: false },
|
||||
{ name: 'events.subscribe', description: 'Subscribe to events', dangerous: false },
|
||||
{ name: 'ui.register', description: 'Register UI contributions', dangerous: false },
|
||||
{ name: 'commands.register', description: 'Register commands', dangerous: false },
|
||||
{ name: 'storage.namespace', description: 'Access plugin storage', dangerous: false },
|
||||
{ name: 'files.read', description: 'Read vault files', dangerous: false },
|
||||
{ name: 'files.write', description: 'Write vault files', dangerous: true },
|
||||
{ name: 'files.delete', description: 'Trash vault files', dangerous: true },
|
||||
{ name: 'workbench.open', description: 'Request Workbench open/edit routing', dangerous: false }
|
||||
];
|
||||
}
|
||||
|
||||
function allContributions() {
|
||||
var views = [], commands = [], sidebarItems = [], statusBarItems = [], settingsPanels = [], openProviders = [];
|
||||
for (var id in pluginStates) {
|
||||
var s = pluginStates[id];
|
||||
var c = (s.manifest && s.manifest.contributes) || {};
|
||||
if (c.views) c.views.forEach(function (v) { views.push(Object.assign({}, v, { pluginId: id })); });
|
||||
if (c.commands) c.commands.forEach(function (cmd) { commands.push(Object.assign({}, cmd, { pluginId: id })); });
|
||||
if (c.sidebarItems) c.sidebarItems.forEach(function (sb) { sidebarItems.push(Object.assign({}, sb, { pluginId: id })); });
|
||||
if (c.statusBarItems) c.statusBarItems.forEach(function (st) { statusBarItems.push(Object.assign({}, st, { pluginId: id })); });
|
||||
if (c.settingsPanels) c.settingsPanels.forEach(function (sp) { settingsPanels.push(Object.assign({}, sp, { pluginId: id })); });
|
||||
if (c.openProviders) c.openProviders.forEach(function (op) { openProviders.push(Object.assign({}, op, { pluginId: id })); });
|
||||
}
|
||||
return { views: views, commands: commands, sidebarItems: sidebarItems, statusBarItems: statusBarItems, settingsPanels: settingsPanels, openProviders: openProviders };
|
||||
}
|
||||
|
||||
function requestExtension(request) {
|
||||
if (request && request.extension) {
|
||||
var explicit = String(request.extension).toLowerCase();
|
||||
return explicit.charAt(0) === '.' ? explicit : '.' + explicit;
|
||||
}
|
||||
var p = String((request && request.path) || '').toLowerCase();
|
||||
var slash = p.lastIndexOf('/');
|
||||
var name = slash === -1 ? p : p.slice(slash + 1);
|
||||
var dot = name.lastIndexOf('.');
|
||||
return dot > 0 ? name.slice(dot) : '';
|
||||
}
|
||||
|
||||
function requestContextName(request) {
|
||||
var ctx = (request && request.context) || {};
|
||||
if (ctx.notesMode || ctx.isInsideNotesFolder || ctx.sourceView === 'notes') return 'notes-markdown';
|
||||
var ext = requestExtension(request);
|
||||
if (ext === '.md' || ext === '.markdown') return 'generic-markdown';
|
||||
return 'generic-text';
|
||||
}
|
||||
|
||||
function providerSupports(provider, request) {
|
||||
var ext = requestExtension(request);
|
||||
var contextName = requestContextName(request);
|
||||
return (provider.supports || []).some(function (support) {
|
||||
if (support.kind && support.kind !== request.kind) return false;
|
||||
if (support.extensions && support.extensions.length && support.extensions.map(function (e) { return String(e).toLowerCase(); }).indexOf(ext) === -1) return false;
|
||||
if (support.contexts && support.contexts.length && support.contexts.indexOf(contextName) === -1) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function selectOpenProvider(request) {
|
||||
var providers = allContributions().openProviders.filter(function (provider) {
|
||||
var s = pluginStates[provider.pluginId];
|
||||
return s && s.enabled && (s.status === 'loaded' || s.status === 'degraded') && providerSupports(provider, request);
|
||||
});
|
||||
providers.sort(function (a, b) {
|
||||
var byPriority = (b.priority || 0) - (a.priority || 0);
|
||||
if (byPriority) return byPriority;
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
return providers[0] || null;
|
||||
}
|
||||
|
||||
function openWorkbenchResource(pluginId, request, forcedMode) {
|
||||
var s = pluginStates[pluginId];
|
||||
if (!s || !s.enabled || (s.status !== 'loaded' && s.status !== 'degraded')) {
|
||||
return Promise.resolve([{}, 'plugin not enabled and loaded']);
|
||||
}
|
||||
if (!s.manifest.permissions || s.manifest.permissions.indexOf('workbench.open') === -1) {
|
||||
return Promise.resolve([{}, 'plugin lacks required permission workbench.open']);
|
||||
}
|
||||
var normalized = Object.assign({}, request || {});
|
||||
normalized.kind = normalized.kind || 'vault-file';
|
||||
normalized.mode = forcedMode || normalized.mode || 'view';
|
||||
normalized.extension = requestExtension(normalized);
|
||||
normalized.context = Object.assign({}, normalized.context || {}, { sourcePluginId: pluginId });
|
||||
var provider = selectOpenProvider(normalized);
|
||||
if (!provider) {
|
||||
return Promise.resolve([{
|
||||
status: 'no-provider',
|
||||
request: normalized,
|
||||
message: 'no open provider for resource'
|
||||
}, '']);
|
||||
}
|
||||
var result = {
|
||||
status: 'opened',
|
||||
providerId: provider.id,
|
||||
providerPluginId: provider.pluginId,
|
||||
providerComponent: provider.component,
|
||||
request: normalized
|
||||
};
|
||||
openedResources.push(Object.assign({ id: provider.id + ':' + openedResources.length, openedAt: new Date().toISOString() }, result));
|
||||
return Promise.resolve([result, '']);
|
||||
}
|
||||
|
||||
function platformTestBundle() {
|
||||
return [
|
||||
"(function(){",
|
||||
"var DiagnosticsPanel={",
|
||||
"mount:function(containerEl,props,api){",
|
||||
"containerEl.innerHTML='';",
|
||||
"containerEl.__ptCleanup=[];",
|
||||
"function track(fn){if(typeof fn==='function')containerEl.__ptCleanup.push(fn);}",
|
||||
"var root=document.createElement('div');",
|
||||
"root.className='pt-root';",
|
||||
"var title=document.createElement('h2');",
|
||||
"title.className='pt-plugin-name';",
|
||||
"title.textContent='Platform Diagnostics';",
|
||||
"var pluginId=document.createElement('p');",
|
||||
"pluginId.className='pt-plugin-id';",
|
||||
"pluginId.textContent=api.pluginId;",
|
||||
"var status=document.createElement('div');",
|
||||
"status.className='pt-badge pt-badge-success';",
|
||||
"status.textContent='Frontend Bundle Loaded';",
|
||||
"var saved=document.createElement('div');",
|
||||
"saved.className='pt-card pt-saved-setting';",
|
||||
"saved.textContent='Saved setting: loading...';",
|
||||
"var cap=document.createElement('div');",
|
||||
"cap.className='pt-capability-result';",
|
||||
"cap.textContent='Capabilities: loading...';",
|
||||
"api.capabilities.list().then(function(caps){cap.textContent='Capabilities: '+caps.length+' available';});",
|
||||
"api.settings.read('savedText').then(function(value){saved.textContent='Saved setting: '+(value||'');});",
|
||||
"var input=document.createElement('input');",
|
||||
"input.className='pt-setting-input';",
|
||||
"input.setAttribute('aria-label','Saved setting');",
|
||||
"input.value='changed value';",
|
||||
"var button=document.createElement('button');",
|
||||
"button.className='btn btn-primary pt-save-setting';",
|
||||
"button.textContent='Save Setting';",
|
||||
"button.addEventListener('click',function(){api.settings.write('savedText',input.value).then(function(){saved.textContent='Saved setting: '+input.value;});});",
|
||||
"api.capabilities.has('verstak/platform-test/v1').then(function(ok){status.textContent='Frontend Bundle Loaded | capability '+(ok?'available':'missing');});",
|
||||
"var command=document.createElement('div');",
|
||||
"command.className='pt-command-result';",
|
||||
"command.textContent='Command: registering...';",
|
||||
"api.commands.register('verstak.platform-test.show-version',function(){return {version:'0.1.0',source:'bundled-frontend'};}).then(function(unregister){track(unregister);return api.commands.execute('verstak.platform-test.show-version',{});}).then(function(result){status.setAttribute('data-command-status',result.status||'');command.textContent='Command: '+result.status+' '+result.result.version+' from '+result.result.source;});",
|
||||
"var eventResult=document.createElement('div');",
|
||||
"eventResult.className='pt-event-result';",
|
||||
"eventResult.textContent='Event: subscribing...';",
|
||||
"api.events.subscribe('verstak.platform-test.echo',function(event){eventResult.textContent='Event: received '+event.payload.message;eventResult.setAttribute('data-event-status','received');}).then(function(unsubscribe){track(unsubscribe);return api.events.publish('verstak.platform-test.echo',{message:'hello-event'});});",
|
||||
"var filesResult=document.createElement('div');",
|
||||
"filesResult.className='pt-files-result';",
|
||||
"filesResult.textContent='Files: running...';",
|
||||
"var filesError=document.createElement('div');",
|
||||
"filesError.className='pt-files-error-result';",
|
||||
"filesError.textContent='Files error path: checking...';",
|
||||
"var workbenchResult=document.createElement('div');",
|
||||
"workbenchResult.className='pt-workbench-result';",
|
||||
"workbenchResult.textContent='Workbench: ready';",
|
||||
"function makeWorkbenchButton(cls,label,request){var b=document.createElement('button');b.className='btn btn-primary '+cls;b.textContent=label;b.addEventListener('click',function(){workbenchResult.textContent='Workbench: opening...';api.workbench.editResource(request).then(function(result){workbenchResult.textContent='Workbench: opened '+result.request.path+' with '+(result.providerId||'no-provider');workbenchResult.setAttribute('data-workbench-status',result.status==='opened'?'ok':result.status);}).catch(function(err){workbenchResult.textContent='Workbench error: '+(err&&err.message?err.message:String(err));workbenchResult.setAttribute('data-workbench-status','error');});});return b;}",
|
||||
"var textWorkbenchButton=makeWorkbenchButton('pt-open-workbench-text','Open Text Diagnostic',{kind:'vault-file',path:'Docs/todo.txt',extension:'.txt',mime:'text/plain',context:{sourceView:'files'}});",
|
||||
"var markdownWorkbenchButton=makeWorkbenchButton('pt-open-workbench-markdown','Open Markdown Diagnostic',{kind:'vault-file',path:'Docs/readme.md',extension:'.md',context:{sourceView:'files'}});",
|
||||
"var notesWorkbenchButton=makeWorkbenchButton('pt-open-workbench-notes','Open Notes Diagnostic',{kind:'vault-file',path:'Notes/Overview.md',extension:'.md',context:{sourceView:'notes',isInsideNotesFolder:true,notesMode:true}});",
|
||||
"api.files.createFolder('PlatformTest').catch(function(e){if(String(e).indexOf('conflict')===-1)throw e;}).then(function(){return api.files.writeText('PlatformTest/files-api.txt','hello files',{createIfMissing:true,overwrite:true});}).then(function(){return api.files.readText('PlatformTest/files-api.txt');}).then(function(text){if(text!=='hello files')throw new Error('read mismatch');return api.files.list('PlatformTest');}).then(function(entries){if(!entries.some(function(e){return e.relativePath==='PlatformTest/files-api.txt';}))throw new Error('list missing file');return api.files.move('PlatformTest/files-api.txt','PlatformTest/files-api-moved.txt',{overwrite:true});}).then(function(){return api.files.trash('PlatformTest/files-api-moved.txt');}).then(function(){filesResult.textContent='Files: wrote/read/listed/moved/trashed';filesResult.setAttribute('data-files-status','ok');}).catch(function(err){filesResult.textContent='Files error: '+(err&&err.message?err.message:String(err));filesResult.setAttribute('data-files-status','error');});",
|
||||
"api.files.readText('.verstak/vault.json').then(function(){filesError.textContent='Files error path: unexpectedly allowed';filesError.setAttribute('data-files-error-status','error');}).catch(function(err){var message=err&&err.message?err.message:String(err);if(message.indexOf('reserved-path')===-1&&message.indexOf('.verstak')===-1){filesError.textContent='Files error path: wrong error '+message;filesError.setAttribute('data-files-error-status','error');return;}filesError.textContent='Files error path: rejected reserved-path';filesError.setAttribute('data-files-error-status','expected');});",
|
||||
"root.appendChild(title);",
|
||||
"root.appendChild(pluginId);",
|
||||
"root.appendChild(status);",
|
||||
"root.appendChild(saved);",
|
||||
"root.appendChild(input);",
|
||||
"root.appendChild(button);",
|
||||
"root.appendChild(cap);",
|
||||
"root.appendChild(command);",
|
||||
"root.appendChild(eventResult);",
|
||||
"root.appendChild(filesResult);",
|
||||
"root.appendChild(filesError);",
|
||||
"root.appendChild(textWorkbenchButton);",
|
||||
"root.appendChild(markdownWorkbenchButton);",
|
||||
"root.appendChild(notesWorkbenchButton);",
|
||||
"root.appendChild(workbenchResult);",
|
||||
"containerEl.appendChild(root);",
|
||||
"},",
|
||||
"unmount:function(containerEl){while(containerEl.__ptCleanup&&containerEl.__ptCleanup.length){containerEl.__ptCleanup.pop()();}containerEl.innerHTML='';}",
|
||||
"};",
|
||||
"var MarkdownDiagnosticProvider={",
|
||||
"mount:function(containerEl,props,api){",
|
||||
"containerEl.innerHTML='';",
|
||||
"var root=document.createElement('div');",
|
||||
"root.className='pt-root pt-workbench-result';",
|
||||
"root.setAttribute('data-workbench-status','ok');",
|
||||
"var req=(props&&props.request)||{};",
|
||||
"var ctx=(req.context&&req.context.notesMode)||false?'notes-markdown':((req.extension==='.md'||req.extension==='.markdown')?'generic-markdown':'generic-text');",
|
||||
"root.setAttribute('data-resource-path',req.path||'');",
|
||||
"root.setAttribute('data-resource-mode',req.mode||'');",
|
||||
"root.setAttribute('data-resource-context',ctx);",
|
||||
"root.textContent='Workbench: opened '+(req.path||'')+' with '+((props&&props.providerId)||'')+' mode='+(req.mode||'')+' context='+ctx;",
|
||||
"containerEl.appendChild(root);",
|
||||
"},",
|
||||
"unmount:function(containerEl){containerEl.innerHTML='';}",
|
||||
"};",
|
||||
"var PlatformTestSettings={",
|
||||
"mount:function(containerEl,props,api){",
|
||||
"containerEl.innerHTML='<div class=\"pt-root\"><h2>Platform Test Settings</h2><p>'+api.pluginId+'</p></div>';",
|
||||
"},",
|
||||
"unmount:function(containerEl){containerEl.innerHTML='';}",
|
||||
"};",
|
||||
"window.VerstakPluginRegister('verstak.platform-test',{components:{DiagnosticsPanel:DiagnosticsPanel,PlatformTestSettings:PlatformTestSettings,MarkdownDiagnosticProvider:MarkdownDiagnosticProvider}});",
|
||||
"})();"
|
||||
].join('');
|
||||
}
|
||||
|
||||
// ── Mock API ───────────────────────────────────────────────────────
|
||||
var mock = {
|
||||
GetPlugins: function () { return Promise.resolve(allPlugins()); },
|
||||
GetCapabilities: function () { return Promise.resolve(allCapabilities()); },
|
||||
GetPermissions: function () { return Promise.resolve(allPermissions()); },
|
||||
GetContributions: function () { return Promise.resolve(allContributions()); },
|
||||
GetVaultStatus: function () { return Promise.resolve(vaultStatus); },
|
||||
GetVaultPluginState: function () { return Promise.resolve(vaultPluginState); },
|
||||
GetAppSettings: function () { return Promise.resolve(appSettings); },
|
||||
GetPluginFrontendInfo: function (pluginId) {
|
||||
var s = pluginStates[pluginId];
|
||||
if (s && s.manifest && s.manifest.frontend) {
|
||||
return Promise.resolve({ entry: s.manifest.frontend.entry });
|
||||
}
|
||||
return Promise.resolve({});
|
||||
},
|
||||
ReadPluginSettings: function (pluginId) {
|
||||
return Promise.resolve([Object.assign({}, pluginSettings[pluginId] || {}), '']);
|
||||
},
|
||||
WritePluginSettings: function (pluginId, settings) {
|
||||
pluginSettings[pluginId] = Object.assign({}, settings || {});
|
||||
return Promise.resolve('');
|
||||
},
|
||||
ReadPluginSetting: function () { return Promise.resolve(null); },
|
||||
WritePluginSetting: function () { return Promise.resolve(null); },
|
||||
ReadPluginDataJSON: function () { return Promise.resolve({}); },
|
||||
WritePluginDataJSON: function () { return Promise.resolve(null); },
|
||||
OpenWorkbenchResource: function (pluginId, request) {
|
||||
return openWorkbenchResource(pluginId, request || {}, '');
|
||||
},
|
||||
EditWorkbenchResource: function (pluginId, request) {
|
||||
return openWorkbenchResource(pluginId, request || {}, 'edit');
|
||||
},
|
||||
GetWorkbenchOpenedResources: function () {
|
||||
return Promise.resolve(openedResources.map(function (resource) {
|
||||
return Object.assign({}, resource, { request: Object.assign({}, resource.request || {}) });
|
||||
}));
|
||||
},
|
||||
GetWorkbenchPreferences: function () {
|
||||
return Promise.resolve(Object.assign({}, workbenchPreferences));
|
||||
},
|
||||
UpdateWorkbenchPreferences: function (preferences) {
|
||||
workbenchPreferences = Object.assign({}, workbenchPreferences, preferences || {});
|
||||
return Promise.resolve('');
|
||||
},
|
||||
GetPluginAssetContent: function (pluginId, assetPath) {
|
||||
if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') {
|
||||
return Promise.resolve(platformTestBundle());
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
GetPluginCapability: function (pluginId, capId) {
|
||||
var caps = allCapabilities();
|
||||
var found = caps.find(function (cap) { return cap.name === capId; });
|
||||
return Promise.resolve([found ? Object.assign({ available: true }, found) : { available: false, name: capId }, '']);
|
||||
},
|
||||
ListPluginCapabilities: function () { return Promise.resolve([allCapabilities(), '']); },
|
||||
ExecutePluginCommand: function (pluginId, commandId, args) {
|
||||
var s = pluginStates[pluginId];
|
||||
var commands = ((s && s.manifest && s.manifest.contributes && s.manifest.contributes.commands) || []);
|
||||
var found = commands.find(function (cmd) { return cmd.id === commandId; });
|
||||
if (!found) return Promise.resolve([{}, 'command not declared']);
|
||||
return Promise.resolve([{ status: 'declared', pluginId: pluginId, commandId: commandId, handler: found.handler, args: args || {} }, '']);
|
||||
},
|
||||
PublishPluginEvent: function () { return Promise.resolve(''); },
|
||||
SubscribePluginEvent: function (pluginId, eventName) {
|
||||
var s = pluginStates[pluginId];
|
||||
if (!s || !s.enabled || s.status !== 'loaded') return Promise.resolve('plugin not enabled and loaded');
|
||||
if (!eventName) return Promise.resolve('event name is empty');
|
||||
if (!s.manifest.permissions || s.manifest.permissions.indexOf('events.subscribe') === -1) {
|
||||
return Promise.resolve('plugin lacks required permission events.subscribe');
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
ListVaultFiles: function (pluginId, relativeDir) {
|
||||
var err = requirePluginPermission(pluginId, 'files.read');
|
||||
if (err) return Promise.resolve([[], err]);
|
||||
var norm = normalizeVaultPath(relativeDir, true);
|
||||
if (norm.error) return Promise.resolve([[], norm.error]);
|
||||
var dir = norm.path;
|
||||
if (!vaultFiles[dir] || vaultFiles[dir].type !== 'folder') return Promise.resolve([[], 'not-found: ' + dir]);
|
||||
var prefix = dir ? dir + '/' : '';
|
||||
var entries = [];
|
||||
Object.keys(vaultFiles).forEach(function (path) {
|
||||
if (path === dir || path.indexOf(prefix) !== 0) return;
|
||||
var rest = path.slice(prefix.length);
|
||||
if (!rest || rest.indexOf('/') !== -1) return;
|
||||
entries.push(fileEntry(path, vaultFiles[path]));
|
||||
});
|
||||
return Promise.resolve([entries, '']);
|
||||
},
|
||||
GetVaultFileMetadata: function (pluginId, relativePath) {
|
||||
var err = requirePluginPermission(pluginId, 'files.read');
|
||||
if (err) return Promise.resolve([{}, err]);
|
||||
var norm = normalizeVaultPath(relativePath, false);
|
||||
if (norm.error) return Promise.resolve([{}, norm.error]);
|
||||
var node = vaultFiles[norm.path];
|
||||
if (!node) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
||||
return Promise.resolve([fileEntry(norm.path, node), '']);
|
||||
},
|
||||
ReadVaultTextFile: function (pluginId, relativePath) {
|
||||
var err = requirePluginPermission(pluginId, 'files.read');
|
||||
if (err) return Promise.resolve(['', err]);
|
||||
var norm = normalizeVaultPath(relativePath, false);
|
||||
if (norm.error) return Promise.resolve(['', norm.error]);
|
||||
var node = vaultFiles[norm.path];
|
||||
if (!node) return Promise.resolve(['', 'not-found: ' + norm.path]);
|
||||
if (node.type !== 'file') return Promise.resolve(['', 'not-regular-file: ' + norm.path]);
|
||||
return Promise.resolve([node.content || '', '']);
|
||||
},
|
||||
WriteVaultTextFile: function (pluginId, relativePath, content, options) {
|
||||
var err = requirePluginPermission(pluginId, 'files.write');
|
||||
if (err) return Promise.resolve(err);
|
||||
var norm = normalizeVaultPath(relativePath, false);
|
||||
if (norm.error) return Promise.resolve(norm.error);
|
||||
options = options || {};
|
||||
var existing = vaultFiles[norm.path];
|
||||
if (existing && existing.type !== 'file') return Promise.resolve('not-regular-file: ' + norm.path);
|
||||
if (existing && !options.overwrite) return Promise.resolve('conflict: ' + norm.path);
|
||||
if (!existing && !options.createIfMissing) return Promise.resolve('not-found: ' + norm.path);
|
||||
var parent = parentPath(norm.path);
|
||||
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
|
||||
vaultFiles[norm.path] = { type: 'file', content: String(content == null ? '' : content), modifiedAt: new Date().toISOString() };
|
||||
return Promise.resolve('');
|
||||
},
|
||||
CreateVaultFolder: function (pluginId, relativePath) {
|
||||
var err = requirePluginPermission(pluginId, 'files.write');
|
||||
if (err) return Promise.resolve(err);
|
||||
var norm = normalizeVaultPath(relativePath, false);
|
||||
if (norm.error) return Promise.resolve(norm.error);
|
||||
if (vaultFiles[norm.path]) return Promise.resolve('conflict: ' + norm.path);
|
||||
var parent = parentPath(norm.path);
|
||||
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
|
||||
vaultFiles[norm.path] = { type: 'folder', modifiedAt: new Date().toISOString() };
|
||||
return Promise.resolve('');
|
||||
},
|
||||
MoveVaultPath: function (pluginId, fromRelativePath, toRelativePath, options) {
|
||||
var err = requirePluginPermission(pluginId, 'files.write');
|
||||
if (err) return Promise.resolve(err);
|
||||
var from = normalizeVaultPath(fromRelativePath, false);
|
||||
var to = normalizeVaultPath(toRelativePath, false);
|
||||
if (from.error) return Promise.resolve(from.error);
|
||||
if (to.error) return Promise.resolve(to.error);
|
||||
options = options || {};
|
||||
if (!vaultFiles[from.path]) return Promise.resolve('not-found: ' + from.path);
|
||||
if (vaultFiles[from.path].type === 'folder' && (to.path === from.path || to.path.indexOf(from.path + '/') === 0)) {
|
||||
return Promise.resolve('move-into-self: ' + from.path + ' -> ' + to.path);
|
||||
}
|
||||
if (vaultFiles[to.path] && !options.overwrite) return Promise.resolve('conflict: ' + to.path);
|
||||
var parent = parentPath(to.path);
|
||||
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
|
||||
var moving = Object.keys(vaultFiles).filter(function (path) { return path === from.path || path.indexOf(from.path + '/') === 0; });
|
||||
moving.forEach(function (path) {
|
||||
var suffix = path.slice(from.path.length);
|
||||
vaultFiles[to.path + suffix] = vaultFiles[path];
|
||||
delete vaultFiles[path];
|
||||
});
|
||||
return Promise.resolve('');
|
||||
},
|
||||
TrashVaultPath: function (pluginId, relativePath) {
|
||||
var err = requirePluginPermission(pluginId, 'files.delete');
|
||||
if (err) return Promise.resolve([{}, err]);
|
||||
var norm = normalizeVaultPath(relativePath, false);
|
||||
if (norm.error) return Promise.resolve([{}, norm.error]);
|
||||
if (!vaultFiles[norm.path]) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
||||
var trashId = 'mock-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
||||
var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path);
|
||||
var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
|
||||
moving.forEach(function (path) { delete vaultFiles[path]; });
|
||||
return Promise.resolve([{ originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString() }, '']);
|
||||
},
|
||||
GetCurrentWorkspaceNode: function () { return Promise.resolve(null); },
|
||||
GetWorkspaceTree: function () { return Promise.resolve(cloneWorkspaceTree()); },
|
||||
ArchiveWorkspaceNode: function () { return Promise.resolve(''); },
|
||||
CreateWorkspaceNode: function () { return Promise.resolve({}); },
|
||||
MoveWorkspaceNode: function () { return Promise.resolve(''); },
|
||||
RenameWorkspaceNode: function () { return Promise.resolve(''); },
|
||||
SetCurrentWorkspaceNode: function (id) {
|
||||
var found = workspaceTree.nodes.some(function (n) { return n.id === id; });
|
||||
if (!found) return Promise.resolve('workspace node not found: ' + id);
|
||||
workspaceTree.currentNodeId = id;
|
||||
return Promise.resolve('');
|
||||
},
|
||||
SelectDirectory: function () { return Promise.resolve(''); },
|
||||
SelectVaultForOpen: function () { return Promise.resolve(''); },
|
||||
CreateVault: function () { return Promise.resolve(null); },
|
||||
OpenVault: function () { return Promise.resolve(null); },
|
||||
CloseVault: function () { return Promise.resolve(null); },
|
||||
SetCurrentVault: function () { return Promise.resolve(''); },
|
||||
UpdateAppSettings: function () { return Promise.resolve(''); },
|
||||
RecordDesiredPlugin: function () { return Promise.resolve(''); },
|
||||
WriteFrontendLog: function () { return Promise.resolve(); },
|
||||
|
||||
EnablePlugin: function (pluginId) {
|
||||
if (pluginStates[pluginId]) {
|
||||
pluginStates[pluginId].status = 'loaded';
|
||||
pluginStates[pluginId].enabled = true;
|
||||
if (vaultPluginState.disabledPlugins.indexOf(pluginId) !== -1) {
|
||||
vaultPluginState.disabledPlugins = vaultPluginState.disabledPlugins.filter(function (id) { return id !== pluginId; });
|
||||
}
|
||||
if (vaultPluginState.enabledPlugins.indexOf(pluginId) === -1) {
|
||||
vaultPluginState.enabledPlugins.push(pluginId);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
DisablePlugin: function (pluginId) {
|
||||
if (pluginStates[pluginId]) {
|
||||
pluginStates[pluginId].status = 'disabled';
|
||||
pluginStates[pluginId].enabled = false;
|
||||
if (vaultPluginState.enabledPlugins.indexOf(pluginId) !== -1) {
|
||||
vaultPluginState.enabledPlugins = vaultPluginState.enabledPlugins.filter(function (id) { return id !== pluginId; });
|
||||
}
|
||||
if (vaultPluginState.disabledPlugins.indexOf(pluginId) === -1) {
|
||||
vaultPluginState.disabledPlugins.push(pluginId);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
ReloadPlugins: function () {
|
||||
if (reloadResponseMode === 'raw-count') {
|
||||
return Promise.resolve(Object.keys(pluginStates).length);
|
||||
}
|
||||
return Promise.resolve([Object.keys(pluginStates).length, 'Reloaded ' + Object.keys(pluginStates).length + ' plugin(s).']);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Install bridge ─────────────────────────────────────────────────
|
||||
if (!window['go']) window['go'] = {};
|
||||
if (!window['go']['api']) window['go']['api'] = {};
|
||||
window['go']['api']['App'] = mock;
|
||||
|
||||
// ── Test helpers (exposed for Playwright) ──────────────────────────
|
||||
window.__wailsMock = {
|
||||
reset: function () {
|
||||
pluginStates = {
|
||||
'verstak.platform-test': {
|
||||
status: 'loaded',
|
||||
enabled: true,
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
id: 'verstak.platform-test',
|
||||
name: 'Platform Test',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
description: 'Runtime test plugin for verifying the Verstak platform.',
|
||||
source: 'official',
|
||||
icon: '🧪',
|
||||
provides: ['verstak/platform-test/v1', 'verstak/diagnostics/v1'],
|
||||
requires: ['verstak/core/plugin-manager/v1', 'verstak/core/capability-registry/v1'],
|
||||
optionalRequires: ['verstak/core/vault/v1', 'verstak/core/sync/v1', 'verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['vault.read', 'events.publish', 'events.subscribe', 'ui.register', 'commands.register', 'storage.namespace', 'files.read', 'files.write', 'files.delete', 'workbench.open'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
views: [
|
||||
{ id: 'verstak.platform-test.diagnostics', title: 'Platform Diagnostics', icon: '🧪', component: 'DiagnosticsPanel' }
|
||||
],
|
||||
commands: [
|
||||
{ id: 'verstak.platform-test.run-tests', title: 'Run Platform Tests', handler: 'runAllTests' },
|
||||
{ id: 'verstak.platform-test.show-version', title: 'Show Version Info', handler: 'showVersion' }
|
||||
],
|
||||
sidebarItems: [
|
||||
{ id: 'verstak.platform-test.sidebar', title: 'Platform Test', icon: '🧪', view: 'verstak.platform-test.diagnostics', position: 100 }
|
||||
],
|
||||
statusBarItems: [
|
||||
{ id: 'verstak.platform-test.status', label: '🧪 All Tests Pass', position: 'right', handler: 'openDiagnostics' }
|
||||
],
|
||||
settingsPanels: [
|
||||
{ id: 'verstak.platform-test.settings', title: 'Platform Test Settings', icon: '🧪', component: 'PlatformTestSettings' }
|
||||
],
|
||||
openProviders: [
|
||||
{
|
||||
id: 'verstak.platform-test.markdown-diagnostic',
|
||||
title: 'Platform Test Markdown Diagnostic',
|
||||
priority: 100,
|
||||
component: 'MarkdownDiagnosticProvider',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
|
||||
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/platform-test',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||||
vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
|
||||
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||
workbenchPreferences = {};
|
||||
openedResources = [];
|
||||
pluginSettings = { 'verstak.platform-test': { savedText: 'initial value' } };
|
||||
vaultFiles = makeDefaultVaultFiles();
|
||||
workspaceTree = makeDefaultWorkspaceTree();
|
||||
reloadResponseMode = 'tuple';
|
||||
},
|
||||
setPluginStatus: function (pluginId, status, enabled) {
|
||||
if (pluginStates[pluginId]) {
|
||||
pluginStates[pluginId].status = status;
|
||||
pluginStates[pluginId].enabled = enabled;
|
||||
}
|
||||
},
|
||||
getPluginState: function (pluginId) {
|
||||
return pluginStates[pluginId] ? Object.assign({}, pluginStates[pluginId]) : null;
|
||||
},
|
||||
addSyntheticPlugins: function (count) {
|
||||
var total = Number(count || 0);
|
||||
for (var i = 1; i <= total; i++) {
|
||||
var id = 'verstak.synthetic-layout-' + String(i).padStart(2, '0');
|
||||
pluginStates[id] = {
|
||||
status: 'loaded',
|
||||
enabled: true,
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
id: id,
|
||||
name: 'Synthetic Layout Plugin ' + i,
|
||||
version: '0.0.' + i,
|
||||
apiVersion: '0.1.0',
|
||||
description: 'Synthetic plugin used by frontend layout tests.',
|
||||
source: 'test',
|
||||
provides: ['verstak/synthetic-layout-' + i + '/v1'],
|
||||
requires: [],
|
||||
optionalRequires: [],
|
||||
permissions: [],
|
||||
contributes: {
|
||||
views: [],
|
||||
commands: [],
|
||||
sidebarItems: [],
|
||||
statusBarItems: [],
|
||||
settingsPanels: []
|
||||
}
|
||||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/synthetic-layout-' + i + '/with/a/long/path/for/responsive-checks',
|
||||
error: ''
|
||||
};
|
||||
if (vaultPluginState.enabledPlugins.indexOf(id) === -1) {
|
||||
vaultPluginState.enabledPlugins.push(id);
|
||||
}
|
||||
if (!vaultPluginState.desiredPlugins.some(function (p) { return p.id === id; })) {
|
||||
vaultPluginState.desiredPlugins.push({ id: id, version: '0.0.' + i, source: 'test' });
|
||||
}
|
||||
}
|
||||
},
|
||||
setVaultStatus: function (status) { vaultStatus = status; },
|
||||
setVaultPluginState: function (state) { vaultPluginState = state; },
|
||||
setReloadResponseMode: function (mode) { reloadResponseMode = mode || 'tuple'; }
|
||||
};
|
||||
|
||||
window.__wailsMockReady = true;
|
||||
console.log('[wails-mock] bridge installed');
|
||||
})();
|
||||
Reference in New Issue
Block a user