feat(m4b): add vault selection UI, enable/disable toggle, missing-installed UI
- Add VaultSelection.svelte: first-run vault create/open/recent UI - Update App.svelte: vault check on startup, show VaultSelection when needed - Update PluginCard.svelte: enable/disable buttons, vault state awareness - Update PluginManager.svelte: enable/disable handlers, missing-installed section - Add SetCurrentVault Wails API binding - Add RecordDesiredPlugin Wails API binding - Record desired plugins on discovery (only when vault open) - Fix addRecent: remove duplicate sort, clean up unused import - Update smoke-platform.sh: enable/disable lifecycle test - Add runEnableDisableTest: vault create/open, disable/enable, plugins.json verify
This commit is contained in:
parent
c8d2560bb2
commit
a6f9e85f13
|
|
@ -3,15 +3,20 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/verstak/verstak-desktop/internal/core/capability"
|
"github.com/verstak/verstak-desktop/internal/core/capability"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||||||
|
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
|
||||||
|
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
testEnableDisable := flag.Bool("test-enable-disable", false, "Test enable/disable lifecycle")
|
||||||
|
flag.Parse()
|
||||||
exitCode := 0
|
exitCode := 0
|
||||||
defer func() {
|
defer func() {
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
|
|
@ -20,6 +25,11 @@ func main() {
|
||||||
root, _ := os.Getwd()
|
root, _ := os.Getwd()
|
||||||
pluginDir := filepath.Join(root, "plugins")
|
pluginDir := filepath.Join(root, "plugins")
|
||||||
|
|
||||||
|
if *testEnableDisable {
|
||||||
|
runEnableDisableTest(root)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("=== smoke-platform: headless plugin verification ===\n\n")
|
fmt.Printf("=== smoke-platform: headless plugin verification ===\n\n")
|
||||||
fmt.Printf(" plugin dir: %s\n", pluginDir)
|
fmt.Printf(" plugin dir: %s\n", pluginDir)
|
||||||
|
|
||||||
|
|
@ -256,3 +266,171 @@ func main() {
|
||||||
exitCode = 1
|
exitCode = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runEnableDisableTest tests the enable/disable lifecycle with vault plugin state.
|
||||||
|
func runEnableDisableTest(root string) {
|
||||||
|
exitCode := 0
|
||||||
|
defer func() {
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("=== smoke-platform: enable/disable test ===\n\n")
|
||||||
|
|
||||||
|
// Create a temp vault
|
||||||
|
tmpDir, err := os.MkdirTemp("", "verstak-smoke-*")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ❌ failed to create temp dir: %v\n", err)
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
vaultPath := filepath.Join(tmpDir, "testvault")
|
||||||
|
fmt.Printf(" vault path: %s\n", vaultPath)
|
||||||
|
|
||||||
|
// Initialize vault
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
if err := v.CreateVault(vaultPath); err != nil {
|
||||||
|
fmt.Printf(" ❌ create vault: %v\n", err)
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ vault created\n")
|
||||||
|
|
||||||
|
// Open the vault at the path returned by CreateVault (path/VerstakVault)
|
||||||
|
openedPath := v.GetVaultPath()
|
||||||
|
if err := v.OpenVault(openedPath); err != nil {
|
||||||
|
fmt.Printf(" ❌ open vault: %v\n", err)
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ vault opened at %s\n", openedPath)
|
||||||
|
|
||||||
|
// Initialize plugin state
|
||||||
|
psm := pluginstate.NewManager(v)
|
||||||
|
if err := psm.Load(); err != nil {
|
||||||
|
fmt.Printf(" ❌ load plugin state: %v\n", err)
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ plugin state loaded\n")
|
||||||
|
|
||||||
|
// Discover plugins
|
||||||
|
pluginDir := filepath.Join(root, "plugins")
|
||||||
|
plugins, _ := plugin.DiscoverPlugins([]string{pluginDir})
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
fmt.Printf(" ❌ no plugins discovered\n")
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ discovered %d plugin(s)\n", len(plugins))
|
||||||
|
|
||||||
|
// Find platform-test
|
||||||
|
var target *plugin.Plugin
|
||||||
|
for i, p := range plugins {
|
||||||
|
if p.Manifest.ID == "verstak.platform-test" {
|
||||||
|
target = &plugins[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if target == nil {
|
||||||
|
fmt.Printf(" ❌ platform-test not found\n")
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ platform-test found\n")
|
||||||
|
|
||||||
|
// Register capabilities
|
||||||
|
reg := capability.NewRegistry()
|
||||||
|
coreCaps := []string{
|
||||||
|
"verstak/core/plugin-manager/v1",
|
||||||
|
"verstak/core/capability-registry/v1",
|
||||||
|
"verstak/core/contribution-registry/v1",
|
||||||
|
"verstak/core/permissions/v1",
|
||||||
|
"verstak/core/events/v1",
|
||||||
|
}
|
||||||
|
reg.Register("verstak-desktop", coreCaps)
|
||||||
|
reg.Register("verstak-desktop", []string{"verstak/core/vault/v1"})
|
||||||
|
for _, cap := range target.Manifest.Provides {
|
||||||
|
reg.Register(target.Manifest.ID, []string{cap})
|
||||||
|
}
|
||||||
|
totalCaps := len(reg.List())
|
||||||
|
fmt.Printf(" ✅ registered %d capabilities (core + plugin)\n", totalCaps)
|
||||||
|
|
||||||
|
// ── Test 1: Disable platform-test ──
|
||||||
|
fmt.Printf("\n[disable]\n")
|
||||||
|
if err := psm.DisablePlugin("verstak.platform-test"); err != nil {
|
||||||
|
fmt.Printf(" ❌ disable: %v\n", err)
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ disabled platform-test\n")
|
||||||
|
|
||||||
|
if !psm.IsDisabled("verstak.platform-test") {
|
||||||
|
fmt.Printf(" ❌ IsDisabled returned false after disable\n")
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ IsDisabled: true\n")
|
||||||
|
|
||||||
|
if psm.IsEnabled("verstak.platform-test") {
|
||||||
|
fmt.Printf(" ❌ IsEnabled returned true after disable\n")
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ IsEnabled: false\n")
|
||||||
|
|
||||||
|
// Check plugins.json
|
||||||
|
state := psm.Get()
|
||||||
|
found := false
|
||||||
|
for _, dp := range state.DesiredPlugins {
|
||||||
|
if dp.ID == "verstak.platform-test" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
fmt.Printf(" ✅ platform-test in desiredPlugins\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ℹ️ platform-test not in desiredPlugins (ok if not recorded)\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 2: Enable platform-test ──
|
||||||
|
fmt.Printf("\n[enable]\n")
|
||||||
|
if err := psm.EnablePlugin("verstak.platform-test"); err != nil {
|
||||||
|
fmt.Printf(" ❌ enable: %v\n", err)
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ enabled platform-test\n")
|
||||||
|
|
||||||
|
if !psm.IsEnabled("verstak.platform-test") {
|
||||||
|
fmt.Printf(" ❌ IsEnabled returned false after enable\n")
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ IsEnabled: true\n")
|
||||||
|
|
||||||
|
if psm.IsDisabled("verstak.platform-test") {
|
||||||
|
fmt.Printf(" ❌ IsDisabled returned true after enable\n")
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ IsDisabled: false\n")
|
||||||
|
|
||||||
|
// ── Test 3: Verify plugins.json on disk ──
|
||||||
|
fmt.Printf("\n[plugins.json verification]\n")
|
||||||
|
statePath := filepath.Join(v.GetVaultPath(), ".verstak", "plugins.json")
|
||||||
|
data, err := os.ReadFile(statePath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ❌ read plugins.json: %v\n", err)
|
||||||
|
exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✅ plugins.json exists on disk\n")
|
||||||
|
fmt.Printf(" content:\n%s\n", string(data))
|
||||||
|
|
||||||
|
// ── Summary ──
|
||||||
|
fmt.Printf("\n=== summary ===\n")
|
||||||
|
fmt.Printf("✅ enable/disable test passed\n")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,53 @@
|
||||||
import PluginManager from './lib/plugin-manager/PluginManager.svelte';
|
import PluginManager from './lib/plugin-manager/PluginManager.svelte';
|
||||||
import Sidebar from './lib/shell/Sidebar.svelte';
|
import Sidebar from './lib/shell/Sidebar.svelte';
|
||||||
import ViewContainer from './lib/shell/ViewContainer.svelte';
|
import ViewContainer from './lib/shell/ViewContainer.svelte';
|
||||||
|
import VaultSelection from './lib/shell/VaultSelection.svelte';
|
||||||
|
import * as App from '../wailsjs/go/api/App';
|
||||||
|
|
||||||
let currentView = 'plugin-manager';
|
let currentView = 'plugin-manager';
|
||||||
|
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
|
||||||
|
let needsVaultSelection = false;
|
||||||
|
let loading = true;
|
||||||
|
|
||||||
|
async function checkVault() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const settings = await App.GetAppSettings();
|
||||||
|
vaultStatus = await App.GetVaultStatus() || { status: 'unknown', path: '', vaultId: '' };
|
||||||
|
|
||||||
|
if (!settings.currentVaultPath || vaultStatus.status !== 'open') {
|
||||||
|
needsVaultSelection = true;
|
||||||
|
} else {
|
||||||
|
needsVaultSelection = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[App] startup check failed:', e);
|
||||||
|
needsVaultSelection = true;
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVaultOpened() {
|
||||||
|
needsVaultSelection = false;
|
||||||
|
vaultStatus = { status: 'open', path: '', vaultId: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for vault-opened event from VaultSelection
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('verstak:vault-opened', onVaultOpened);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkVault();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
{#if loading}
|
||||||
|
<div class="app-loading">
|
||||||
|
<p>Loading Verstak...</p>
|
||||||
|
</div>
|
||||||
|
{:else if needsVaultSelection}
|
||||||
|
<VaultSelection />
|
||||||
|
{:else}
|
||||||
|
<main>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
<section class="content">
|
<section class="content">
|
||||||
|
|
@ -16,9 +58,19 @@
|
||||||
<ViewContainer />
|
<ViewContainer />
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.app-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
@ -29,5 +81,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,14 @@
|
||||||
export let p = {};
|
export let p = {};
|
||||||
export let capabilities = [];
|
export let capabilities = [];
|
||||||
export let permissions = [];
|
export let permissions = [];
|
||||||
|
export let contributions = {};
|
||||||
|
export let vaultOpen = false;
|
||||||
export let onSettings = () => {};
|
export let onSettings = () => {};
|
||||||
|
export let onEnable = () => {};
|
||||||
|
export let onDisable = () => {};
|
||||||
|
|
||||||
|
$: m = p.manifest || {};
|
||||||
|
$: pluginId = m.id || 'unknown';
|
||||||
$: hasSettingsPanel = (contributions.settingsPanels || []).some(sp => sp.pluginId === pluginId);
|
$: hasSettingsPanel = (contributions.settingsPanels || []).some(sp => sp.pluginId === pluginId);
|
||||||
$: hasUIPermission = (m.permissions || []).includes('ui.register');
|
$: hasUIPermission = (m.permissions || []).includes('ui.register');
|
||||||
$: hasStoragePermission = (m.permissions || []).includes('storage.namespace');
|
$: hasStoragePermission = (m.permissions || []).includes('storage.namespace');
|
||||||
|
|
@ -20,8 +26,6 @@
|
||||||
discovered: '#a0a0b8',
|
discovered: '#a0a0b8',
|
||||||
}[p.status] || '#a0a0b8');
|
}[p.status] || '#a0a0b8');
|
||||||
|
|
||||||
$: pluginId = m.id || 'unknown';
|
|
||||||
|
|
||||||
$: contribCounts = {
|
$: contribCounts = {
|
||||||
views: (contributions.views || []).filter(v => v.pluginId === pluginId).length,
|
views: (contributions.views || []).filter(v => v.pluginId === pluginId).length,
|
||||||
commands: (contributions.commands || []).filter(c => c.pluginId === pluginId).length,
|
commands: (contributions.commands || []).filter(c => c.pluginId === pluginId).length,
|
||||||
|
|
@ -54,13 +58,16 @@
|
||||||
$: missingOptional = (m.optionalRequires || []).filter(opt =>
|
$: missingOptional = (m.optionalRequires || []).filter(opt =>
|
||||||
!capabilities.some(c => c.name === opt)
|
!capabilities.some(c => c.name === opt)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$: isDisabled = p.status === 'disabled' || !p.enabled;
|
||||||
|
$: canToggle = p.status !== 'failed' && p.status !== 'incompatible' && p.status !== 'missing-required-capability' && p.status !== 'discovered';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="plugin-card" class:disabled={!p.enabled} class:failed={p.status === 'failed'}>
|
<div class="plugin-card" class:disabled={isDisabled} class:failed={p.status === 'failed'}>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="plugin-id">
|
<div class="plugin-id">
|
||||||
<span class="status-dot" style="background: {statusColor}"></span>
|
<span class="status-dot" style="background: {statusColor}"></span>
|
||||||
<strong>{m.id || 'unknown'}</strong>
|
<strong>{pluginId}</strong>
|
||||||
<span class="version">v{m.version || '?'}</span>
|
<span class="version">v{m.version || '?'}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="status-badge" style="color: {statusColor}">{p.status}</span>
|
<span class="status-badge" style="color: {statusColor}">{p.status}</span>
|
||||||
|
|
@ -171,6 +178,20 @@
|
||||||
⚙ Settings
|
⚙ Settings
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if vaultOpen && canToggle}
|
||||||
|
{#if isDisabled}
|
||||||
|
<button class="btn-enable" on:click={() => onEnable(m.id)} type="button">
|
||||||
|
▶ Enable
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn-disable" on:click={() => onDisable(m.id)} type="button">
|
||||||
|
⏸ Disable
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if !vaultOpen && canToggle}
|
||||||
|
<span class="vault-hint">Open a vault to manage plugin state</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Permission warnings -->
|
<!-- Permission warnings -->
|
||||||
|
|
@ -347,6 +368,7 @@
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
|
|
@ -366,4 +388,40 @@
|
||||||
.btn-settings:hover {
|
.btn-settings:hover {
|
||||||
background: #1a3a5c;
|
background: #1a3a5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-enable {
|
||||||
|
background: #4ecca3;
|
||||||
|
color: #1a1a2e;
|
||||||
|
border: none;
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-enable:hover {
|
||||||
|
background: #3dbb92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disable {
|
||||||
|
background: #533483;
|
||||||
|
color: #e0e0f0;
|
||||||
|
border: none;
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disable:hover {
|
||||||
|
background: #6b44a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-hint {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import PluginCard from './PluginCard.svelte';
|
import PluginCard from './PluginCard.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { GetPlugins, GetCapabilities, GetPermissions, GetContributions, ReloadPlugins, GetVaultStatus } from '../../../wailsjs/go/api/App';
|
import { GetPlugins, GetCapabilities, GetPermissions, GetContributions, ReloadPlugins, GetVaultStatus, GetVaultPluginState, EnablePlugin, DisablePlugin } from '../../../wailsjs/go/api/App';
|
||||||
|
|
||||||
let plugins = [];
|
let plugins = [];
|
||||||
let capabilities = [];
|
let capabilities = [];
|
||||||
|
|
@ -10,10 +10,20 @@
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let error = '';
|
let error = '';
|
||||||
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
|
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
|
||||||
|
let vaultPluginState = { enabledPlugins: [], disabledPlugins: [], desiredPlugins: [] };
|
||||||
let settingsPanel = null;
|
let settingsPanel = null;
|
||||||
let settingsData = {};
|
let settingsData = {};
|
||||||
let settingsPluginId = '';
|
let settingsPluginId = '';
|
||||||
|
|
||||||
|
$: vaultOpen = vaultStatus.status === 'open';
|
||||||
|
$: missingInstalled = computeMissingInstalled();
|
||||||
|
|
||||||
|
function computeMissingInstalled() {
|
||||||
|
if (!vaultPluginState.desiredPlugins) return [];
|
||||||
|
const installedIDs = new Set(plugins.map(p => p.manifest?.id).filter(Boolean));
|
||||||
|
return (vaultPluginState.desiredPlugins || []).filter(dp => !installedIDs.has(dp.id));
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
error = '';
|
error = '';
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
@ -27,6 +37,10 @@
|
||||||
}
|
}
|
||||||
// Vault status — non-critical
|
// Vault status — non-critical
|
||||||
GetVaultStatus().then(v => { vaultStatus = v || { status: 'unknown', path: '', vaultId: '' }; }).catch(() => {});
|
GetVaultStatus().then(v => { vaultStatus = v || { status: 'unknown', path: '', vaultId: '' }; }).catch(() => {});
|
||||||
|
// Vault plugin state
|
||||||
|
if (vaultStatus.status === 'open') {
|
||||||
|
GetVaultPluginState().then(s => { vaultPluginState = s || { enabledPlugins: [], disabledPlugins: [], desiredPlugins: [] }; }).catch(() => {});
|
||||||
|
}
|
||||||
// Capabilities and permissions are non-critical — load async
|
// Capabilities and permissions are non-critical — load async
|
||||||
GetCapabilities().then(c => { capabilities = c || []; }).catch(() => {});
|
GetCapabilities().then(c => { capabilities = c || []; }).catch(() => {});
|
||||||
GetPermissions().then(p => { permissions = p || []; }).catch(() => {});
|
GetPermissions().then(p => { permissions = p || []; }).catch(() => {});
|
||||||
|
|
@ -49,6 +63,24 @@
|
||||||
await loadAll();
|
await loadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enablePlugin(pluginId) {
|
||||||
|
const err = await EnablePlugin(pluginId);
|
||||||
|
if (err) {
|
||||||
|
error = 'Enable: ' + err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disablePlugin(pluginId) {
|
||||||
|
const err = await DisablePlugin(pluginId);
|
||||||
|
if (err) {
|
||||||
|
error = 'Disable: ' + err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await reload();
|
||||||
|
}
|
||||||
|
|
||||||
$: totalPlugins = plugins.length;
|
$: totalPlugins = plugins.length;
|
||||||
$: totalCaps = capabilities.length;
|
$: totalCaps = capabilities.length;
|
||||||
$: totalPerms = permissions.length;
|
$: totalPerms = permissions.length;
|
||||||
|
|
@ -58,24 +90,22 @@
|
||||||
if (panel) {
|
if (panel) {
|
||||||
settingsPanel = panel;
|
settingsPanel = panel;
|
||||||
settingsPluginId = pluginId;
|
settingsPluginId = pluginId;
|
||||||
// Load existing settings
|
// Load existing settings from Wails backend
|
||||||
try {
|
import('../../../wailsjs/go/api/App').then(mod => {
|
||||||
const data = JSON.parse(localStorage.getItem('verstak-settings-' + pluginId) || '{}');
|
mod.ReadPluginSettings(pluginId).then(data => {
|
||||||
settingsData = data;
|
settingsData = data || {};
|
||||||
} catch (e) {
|
}).catch(() => { settingsData = {}; });
|
||||||
settingsData = {};
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('verstak-settings-' + settingsPluginId, JSON.stringify(settingsData));
|
import('../../../wailsjs/go/api/App').then(mod => {
|
||||||
// Also try Wails backend
|
mod.WritePluginSettings(settingsPluginId, settingsData).then(err => {
|
||||||
const { WritePluginSettings } = require('../../../wailsjs/go/api/App');
|
|
||||||
WritePluginSettings(settingsPluginId, settingsData).then(err => {
|
|
||||||
if (err) console.error('WritePluginSettings:', err);
|
if (err) console.error('WritePluginSettings:', err);
|
||||||
}).catch(() => {});
|
}).catch(e => console.error('WritePluginSettings:', e));
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('saveSettings:', e);
|
console.error('saveSettings:', e);
|
||||||
}
|
}
|
||||||
|
|
@ -112,13 +142,14 @@
|
||||||
<span class="badge">{totalPerms} permissions known</span>
|
<span class="badge">{totalPerms} permissions known</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if plugins.length === 0}
|
{#if plugins.length === 0 && missingInstalled.length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div> <p>No plugins found</p>
|
</div>
|
||||||
|
<p>No plugins found</p>
|
||||||
<p class="hint">Plugin directories scanned:</p>
|
<p class="hint">Plugin directories scanned:</p>
|
||||||
<ul class="hint-list">
|
<ul class="hint-list">
|
||||||
<li><code>~/.config/verstak/plugins/</code> — user plugins</li>
|
<li><code>~/.config/verstak/plugins/</code> — user plugins</li>
|
||||||
|
|
@ -129,11 +160,38 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="plugin-list">
|
<div class="plugin-list">
|
||||||
{#each plugins as p}
|
{#each plugins as p}
|
||||||
<PluginCard {p} {capabilities} {permissions} {contributions} onSettings={openSettings} />
|
<PluginCard {p} {capabilities} {permissions} {contributions} {vaultOpen} onSettings={openSettings} onEnable={enablePlugin} onDisable={disablePlugin} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if missingInstalled.length > 0}
|
||||||
|
<div class="missing-section">
|
||||||
|
<h3>Missing Installed Plugins</h3>
|
||||||
|
<p class="missing-hint">These plugins are required by this vault but their packages are not installed locally.</p>
|
||||||
|
<div class="plugin-list">
|
||||||
|
{#each missingInstalled as mp}
|
||||||
|
<div class="plugin-card missing-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="plugin-id">
|
||||||
|
<span class="status-dot" style="background: #e94560"></span>
|
||||||
|
<strong>{mp.id}</strong>
|
||||||
|
{#if mp.version}<span class="version">v{mp.version}</span>{/if}
|
||||||
|
</div>
|
||||||
|
<span class="status-badge" style="color: #e94560">missing</span>
|
||||||
|
</div>
|
||||||
|
<p class="missing-text">
|
||||||
|
This plugin is listed in the vault's desired plugins but the package is not installed.
|
||||||
|
{#if mp.source && mp.source !== 'unknown'}
|
||||||
|
<span class="source-hint">Source: {mp.source}</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if capabilities.length > 0}
|
{#if capabilities.length > 0}
|
||||||
<details class="registry-section">
|
<details class="registry-section">
|
||||||
<summary>Capability Registry ({totalCaps})</summary>
|
<summary>Capability Registry ({totalCaps})</summary>
|
||||||
|
|
@ -191,10 +249,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.plugin-manager { max-width: 900px; }
|
.plugin-manager {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -270,6 +330,37 @@
|
||||||
.hint-list li { margin: 0.25rem 0; }
|
.hint-list li { margin: 0.25rem 0; }
|
||||||
.hint code { background: #0f3460; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
.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; }
|
||||||
|
|
||||||
|
/* Missing installed section */
|
||||||
|
.missing-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.missing-section h3 {
|
||||||
|
color: #e94560;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
}
|
||||||
|
.missing-hint {
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
.missing-card {
|
||||||
|
border-color: #e94560;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.missing-text {
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
.source-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.registry-section {
|
.registry-section {
|
||||||
background: #16213e; border: 1px solid #0f3460;
|
background: #16213e; border: 1px solid #0f3460;
|
||||||
border-radius: 8px; padding: 0.75rem; margin-top: 1rem;
|
border-radius: 8px; padding: 0.75rem; margin-top: 1rem;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,309 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import * as App from '../../../wailsjs/go/api/App';
|
||||||
|
|
||||||
|
let appSettings = {};
|
||||||
|
let recentVaults = [];
|
||||||
|
let error = '';
|
||||||
|
let loading = true;
|
||||||
|
let creating = false;
|
||||||
|
let opening = false;
|
||||||
|
let newVaultPath = '';
|
||||||
|
let openVaultPath = '';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
appSettings = await App.GetAppSettings() || {};
|
||||||
|
recentVaults = appSettings.recentVaults || [];
|
||||||
|
} catch (e) {
|
||||||
|
error = 'Failed to load app settings: ' + String(e);
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createVault() {
|
||||||
|
error = '';
|
||||||
|
if (!newVaultPath.trim()) {
|
||||||
|
error = 'Please enter a path for the new vault';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
creating = true;
|
||||||
|
try {
|
||||||
|
// Create the vault
|
||||||
|
const createErr = await App.CreateVault(newVaultPath.trim());
|
||||||
|
if (createErr) {
|
||||||
|
error = 'Create vault: ' + createErr;
|
||||||
|
creating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Open it and save to app settings
|
||||||
|
const setErr = await App.SetCurrentVault(newVaultPath.trim());
|
||||||
|
if (setErr) {
|
||||||
|
error = 'Set current vault: ' + setErr;
|
||||||
|
creating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Success — dispatch event for app to transition
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
creating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openExistingVault() {
|
||||||
|
error = '';
|
||||||
|
if (!openVaultPath.trim()) {
|
||||||
|
error = 'Please enter a path to an existing vault';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
opening = true;
|
||||||
|
try {
|
||||||
|
const setErr = await App.SetCurrentVault(openVaultPath.trim());
|
||||||
|
if (setErr) {
|
||||||
|
error = setErr;
|
||||||
|
opening = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
opening = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openRecent(path) {
|
||||||
|
error = '';
|
||||||
|
opening = true;
|
||||||
|
try {
|
||||||
|
const setErr = await App.SetCurrentVault(path);
|
||||||
|
if (setErr) {
|
||||||
|
error = setErr;
|
||||||
|
opening = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
opening = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="vault-selection">
|
||||||
|
<div class="vault-selection-inner">
|
||||||
|
<div class="logo">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#4ecca3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||||
|
<line x1="12" y1="11" x2="12" y2="17"/>
|
||||||
|
<line x1="9" y1="14" x2="15" y2="14"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Verstak</h1>
|
||||||
|
<p class="subtitle">Choose a vault to get started</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error-box">
|
||||||
|
<span class="error-icon">⚠</span>
|
||||||
|
<span class="error-text">{error}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<div class="action-card">
|
||||||
|
<h3>Create New Vault</h3>
|
||||||
|
<p class="hint">Create a new vault folder. This will be your workspace.</p>
|
||||||
|
<div class="input-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newVaultPath}
|
||||||
|
placeholder="~/Documents/MyVault"
|
||||||
|
disabled={creating}
|
||||||
|
/>
|
||||||
|
<button class="btn-primary" on:click={createVault} type="button" disabled={creating}>
|
||||||
|
{creating ? 'Creating...' : 'Create & Open'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-card">
|
||||||
|
<h3>Open Existing Vault</h3>
|
||||||
|
<p class="hint">Open a vault that already exists on this computer.</p>
|
||||||
|
<div class="input-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={openVaultPath}
|
||||||
|
placeholder="~/Documents/ExistingVault"
|
||||||
|
disabled={opening}
|
||||||
|
/>
|
||||||
|
<button class="btn-primary" on:click={openExistingVault} type="button" disabled={opening}>
|
||||||
|
{opening ? 'Opening...' : 'Open'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if recentVaults.length > 0}
|
||||||
|
<div class="recent-section">
|
||||||
|
<h3>Recent Vaults</h3>
|
||||||
|
<ul class="recent-list">
|
||||||
|
{#each recentVaults as path}
|
||||||
|
<li>
|
||||||
|
<button class="recent-item" on:click={() => openRecent(path)} type="button" disabled={opening}>
|
||||||
|
<span class="recent-icon">📁</span>
|
||||||
|
<span class="recent-path">{path}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vault-selection {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: #1a1a2e;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.vault-selection-inner {
|
||||||
|
max-width: 520px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.logo h1 {
|
||||||
|
color: #e0e0f0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin: 0.5rem 0 0.25rem;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.error-box {
|
||||||
|
background: rgba(233, 69, 96, 0.1);
|
||||||
|
border: 1px solid #e94560;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #e94560;
|
||||||
|
}
|
||||||
|
.error-icon { flex-shrink: 0; }
|
||||||
|
.error-text { word-break: break-word; }
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.action-card {
|
||||||
|
background: #16213e;
|
||||||
|
border: 1px solid #0f3460;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
.action-card h3 {
|
||||||
|
color: #e0e0f0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.input-row input {
|
||||||
|
flex: 1;
|
||||||
|
background: #0f3460;
|
||||||
|
border: 1px solid #1a3a5c;
|
||||||
|
color: #e0e0f0;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.input-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4ecca3;
|
||||||
|
}
|
||||||
|
.input-row input::placeholder {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #4ecca3;
|
||||||
|
color: #1a1a2e;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #3dbb92;
|
||||||
|
}
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.recent-section {
|
||||||
|
background: #16213e;
|
||||||
|
border: 1px solid #0f3460;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
.recent-section h3 {
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
.recent-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.recent-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #e0e0f0;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.recent-item:hover {
|
||||||
|
color: #4ecca3;
|
||||||
|
}
|
||||||
|
.recent-item:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.recent-icon { flex-shrink: 0; }
|
||||||
|
.recent-path {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -191,6 +191,17 @@ func (a *App) ReloadPlugins() (int, string) {
|
||||||
if p.Manifest.Contributes != nil {
|
if p.Manifest.Contributes != nil {
|
||||||
a.contribRegistry.Register(p.Manifest.ID, p.Manifest.Contributes)
|
a.contribRegistry.Register(p.Manifest.ID, p.Manifest.Contributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record as desired plugin in vault state (only if vault is open)
|
||||||
|
if a.pluginState != nil && a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen {
|
||||||
|
source := p.Manifest.Source
|
||||||
|
if source == "" {
|
||||||
|
source = "unknown"
|
||||||
|
}
|
||||||
|
if err := a.pluginState.RecordDesiredPlugin(p.Manifest.ID, p.Manifest.Version, source); err != nil {
|
||||||
|
log.Printf("[plugin] %s: failed to record desired: %v", p.Manifest.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.plugins = plugins
|
a.plugins = plugins
|
||||||
|
|
@ -387,6 +398,35 @@ func (a *App) UpdateAppSettings(patch map[string]interface{}) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCurrentVault sets the current vault path in app settings and re-opens the vault.
|
||||||
|
func (a *App) SetCurrentVault(path string) string {
|
||||||
|
if a.appSettings == nil {
|
||||||
|
return "app settings not initialized"
|
||||||
|
}
|
||||||
|
if a.vault == nil {
|
||||||
|
return "vault service not initialized"
|
||||||
|
}
|
||||||
|
// Try to open the vault first
|
||||||
|
if err := a.vault.OpenVault(path); err != nil {
|
||||||
|
return fmt.Sprintf("failed to open vault: %v", err)
|
||||||
|
}
|
||||||
|
// Save to app settings
|
||||||
|
if err := a.appSettings.SetCurrentVault(path); err != nil {
|
||||||
|
return fmt.Sprintf("failed to save app settings: %v", err)
|
||||||
|
}
|
||||||
|
// Load plugin state for the vault
|
||||||
|
if a.pluginState != nil {
|
||||||
|
if err := a.pluginState.Load(); err != nil {
|
||||||
|
log.Printf("[api] SetCurrentVault: warning loading plugin state: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Register vault capability
|
||||||
|
if err := a.capRegistry.Register("verstak-desktop", []string{"verstak/core/vault/v1"}); err != nil {
|
||||||
|
log.Printf("[api] SetCurrentVault: failed to register vault capability: %v", err)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Vault Plugin State API ────────────────────────────────
|
// ─── Vault Plugin State API ────────────────────────────────
|
||||||
|
|
||||||
// GetVaultPluginState returns the current vault plugin state.
|
// GetVaultPluginState returns the current vault plugin state.
|
||||||
|
|
@ -426,6 +466,17 @@ func (a *App) DisablePlugin(pluginID string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecordDesiredPlugin records a plugin as desired for this vault.
|
||||||
|
func (a *App) RecordDesiredPlugin(pluginID, version, source string) string {
|
||||||
|
if a.pluginState == nil {
|
||||||
|
return "plugin state not initialized"
|
||||||
|
}
|
||||||
|
if err := a.pluginState.RecordDesiredPlugin(pluginID, version, source); err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// ContributionSummary aggregates all contribution types for the frontend.
|
// ContributionSummary aggregates all contribution types for the frontend.
|
||||||
type ContributionSummary struct {
|
type ContributionSummary struct {
|
||||||
Views []contribution.ContributionView `json:"views"`
|
Views []contribution.ContributionView `json:"views"`
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -237,19 +236,11 @@ func addRecent(list []string, path string, max int) []string {
|
||||||
filtered = append(filtered, p)
|
filtered = append(filtered, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Prepend
|
// Prepend (most recent first)
|
||||||
result := append([]string{path}, filtered...)
|
result := append([]string{path}, filtered...)
|
||||||
// Trim
|
// Trim
|
||||||
if len(result) > max {
|
if len(result) > max {
|
||||||
result = result[:max]
|
result = result[:max]
|
||||||
}
|
}
|
||||||
sort.SliceStable(result, func(i, j int) bool {
|
|
||||||
return result[i] < result[j]
|
|
||||||
})
|
|
||||||
// Actually keep insertion order: prepend is correct, just trim
|
|
||||||
result = append([]string{path}, filtered...)
|
|
||||||
if len(result) > max {
|
|
||||||
result = result[:max]
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
main.go
11
main.go
|
|
@ -170,6 +170,17 @@ func main() {
|
||||||
log.Printf("[plugin] %s: contributions registered", p.Manifest.ID)
|
log.Printf("[plugin] %s: contributions registered", p.Manifest.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record as desired plugin in vault state (only if vault is open)
|
||||||
|
if pluginStateMgr != nil && vaultService.GetVaultStatus() == vault.StatusOpen {
|
||||||
|
source := p.Manifest.Source
|
||||||
|
if source == "" {
|
||||||
|
source = "unknown"
|
||||||
|
}
|
||||||
|
if err := pluginStateMgr.RecordDesiredPlugin(p.Manifest.ID, p.Manifest.Version, source); err != nil {
|
||||||
|
log.Printf("[plugin] %s: failed to record desired: %v", p.Manifest.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("[plugin] %s: status=%s", p.Manifest.ID, p.Status)
|
log.Printf("[plugin] %s: status=%s", p.Manifest.ID, p.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,5 +63,15 @@ if [ "$SMOKE_EXIT" -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── test enable/disable via Go smoke ──
|
||||||
|
echo ""
|
||||||
|
echo "[go smoke: enable/disable]"
|
||||||
|
(cd "$ROOT" && go run -mod=mod ./cmd/smoke-platform/ -test-enable-disable 2>&1)
|
||||||
|
SMOKE_ED_EXIT=$?
|
||||||
|
if [ "$SMOKE_ED_EXIT" -ne 0 ]; then
|
||||||
|
echo " ❌ smoke-platform: enable/disable test failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ smoke-platform done"
|
echo "✅ smoke-platform done"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue