feat: add app settings, vault plugin state, and first-run vault selection
- internal/core/appsettings/ — app settings manager (~/.config/verstak/config.json) - internal/core/pluginstate/ — vault plugin state (.verstak/plugins.json) - internal/api/app.go — Wails bindings for app settings + plugin state - main.go — init app settings, auto-open vault, init plugin state, disabled plugin filtering - Plugin state: enable/disable, desired plugins, missing-installed tracking - App settings: currentVaultPath, recentVaults, theme, devMode, windowState
This commit is contained in:
parent
c0ea1972f6
commit
04dbfa056e
|
|
@ -8,11 +8,13 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/verstak/verstak-desktop/internal/core/appsettings"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/capability"
|
"github.com/verstak/verstak-desktop/internal/core/capability"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/events"
|
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
||||||
"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/storage"
|
"github.com/verstak/verstak-desktop/internal/core/storage"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/vault"
|
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||||
)
|
)
|
||||||
|
|
@ -26,6 +28,8 @@ type App struct {
|
||||||
plugins []plugin.Plugin
|
plugins []plugin.Plugin
|
||||||
vault *vault.Vault
|
vault *vault.Vault
|
||||||
storage *storage.Storage
|
storage *storage.Storage
|
||||||
|
appSettings *appsettings.Manager
|
||||||
|
pluginState *pluginstate.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new App instance.
|
// NewApp creates a new App instance.
|
||||||
|
|
@ -37,6 +41,8 @@ func NewApp(
|
||||||
plugins []plugin.Plugin,
|
plugins []plugin.Plugin,
|
||||||
vaultService *vault.Vault,
|
vaultService *vault.Vault,
|
||||||
storageService *storage.Storage,
|
storageService *storage.Storage,
|
||||||
|
appSettingsMgr *appsettings.Manager,
|
||||||
|
pluginStateMgr *pluginstate.Manager,
|
||||||
) *App {
|
) *App {
|
||||||
return &App{
|
return &App{
|
||||||
capRegistry: capReg,
|
capRegistry: capReg,
|
||||||
|
|
@ -46,6 +52,8 @@ func NewApp(
|
||||||
plugins: plugins,
|
plugins: plugins,
|
||||||
vault: vaultService,
|
vault: vaultService,
|
||||||
storage: storageService,
|
storage: storageService,
|
||||||
|
appSettings: appSettingsMgr,
|
||||||
|
pluginState: pluginStateMgr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +157,14 @@ func (a *App) ReloadPlugins() (int, string) {
|
||||||
for i := range plugins {
|
for i := range plugins {
|
||||||
p := &plugins[i]
|
p := &plugins[i]
|
||||||
|
|
||||||
|
// Skip disabled plugins
|
||||||
|
if a.pluginState != nil && a.pluginState.IsDisabled(p.Manifest.ID) {
|
||||||
|
log.Printf("[plugin] %s: disabled in vault plugin state — skipping", p.Manifest.ID)
|
||||||
|
p.Status = plugin.StatusDisabled
|
||||||
|
p.Enabled = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if len(p.Manifest.Provides) > 0 {
|
if len(p.Manifest.Provides) > 0 {
|
||||||
if err := a.capRegistry.Register(p.Manifest.ID, p.Manifest.Provides); err != nil {
|
if err := a.capRegistry.Register(p.Manifest.ID, p.Manifest.Provides); err != nil {
|
||||||
log.Printf("[plugin] %s: capability registration failed: %v", p.Manifest.ID, err)
|
log.Printf("[plugin] %s: capability registration failed: %v", p.Manifest.ID, err)
|
||||||
|
|
@ -329,6 +345,87 @@ func (a *App) WritePluginDataJSON(pluginID, name string, data map[string]interfa
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── App Settings API ──────────────────────────────────────
|
||||||
|
|
||||||
|
// GetAppSettings returns the current app settings.
|
||||||
|
func (a *App) GetAppSettings() map[string]interface{} {
|
||||||
|
if a.appSettings == nil {
|
||||||
|
return map[string]interface{}{"status": "not initialized"}
|
||||||
|
}
|
||||||
|
cfg := a.appSettings.Get()
|
||||||
|
return map[string]interface{}{
|
||||||
|
"schemaVersion": cfg.SchemaVersion,
|
||||||
|
"currentVaultPath": cfg.CurrentVaultPath,
|
||||||
|
"recentVaults": cfg.RecentVaults,
|
||||||
|
"theme": cfg.Theme,
|
||||||
|
"devMode": cfg.DevMode,
|
||||||
|
"userPluginsDir": cfg.UserPluginsDir,
|
||||||
|
"lastOpenedAt": cfg.LastOpenedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAppSettings patches and saves app settings.
|
||||||
|
func (a *App) UpdateAppSettings(patch map[string]interface{}) string {
|
||||||
|
if a.appSettings == nil {
|
||||||
|
return "app settings not initialized"
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &appsettings.Config{}
|
||||||
|
if v, ok := patch["theme"].(string); ok && v != "" {
|
||||||
|
cfg.Theme = v
|
||||||
|
}
|
||||||
|
if v, ok := patch["devMode"].(bool); ok {
|
||||||
|
cfg.DevMode = v
|
||||||
|
}
|
||||||
|
if v, ok := patch["userPluginsDir"].(string); ok && v != "" {
|
||||||
|
cfg.UserPluginsDir = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.appSettings.Update(cfg); err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Vault Plugin State API ────────────────────────────────
|
||||||
|
|
||||||
|
// GetVaultPluginState returns the current vault plugin state.
|
||||||
|
func (a *App) GetVaultPluginState() map[string]interface{} {
|
||||||
|
if a.pluginState == nil {
|
||||||
|
return map[string]interface{}{"status": "not initialized"}
|
||||||
|
}
|
||||||
|
state := a.pluginState.Get()
|
||||||
|
return map[string]interface{}{
|
||||||
|
"schemaVersion": state.SchemaVersion,
|
||||||
|
"enabledPlugins": state.EnabledPlugins,
|
||||||
|
"disabledPlugins": state.DisabledPlugins,
|
||||||
|
"desiredPlugins": state.DesiredPlugins,
|
||||||
|
"updatedAt": state.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnablePlugin enables a plugin in the vault.
|
||||||
|
func (a *App) EnablePlugin(pluginID string) string {
|
||||||
|
if a.pluginState == nil {
|
||||||
|
return "plugin state not initialized"
|
||||||
|
}
|
||||||
|
if err := a.pluginState.EnablePlugin(pluginID); err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisablePlugin disables a plugin in the vault.
|
||||||
|
func (a *App) DisablePlugin(pluginID string) string {
|
||||||
|
if a.pluginState == nil {
|
||||||
|
return "plugin state not initialized"
|
||||||
|
}
|
||||||
|
if err := a.pluginState.DisablePlugin(pluginID); 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"`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
// Package appsettings provides application-level settings for Verstak desktop.
|
||||||
|
// App settings are stored locally (NOT inside the vault) and contain installation-specific
|
||||||
|
// configuration like the current vault path, recent vaults, theme, dev mode, etc.
|
||||||
|
package appsettings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application settings stored in ~/.config/verstak/config.json.
|
||||||
|
type Config struct {
|
||||||
|
SchemaVersion int `json:"schemaVersion"`
|
||||||
|
CurrentVaultPath string `json:"currentVaultPath"`
|
||||||
|
RecentVaults []string `json:"recentVaults"`
|
||||||
|
Theme string `json:"theme"`
|
||||||
|
DevMode bool `json:"devMode"`
|
||||||
|
UserPluginsDir string `json:"userPluginsDir"`
|
||||||
|
WindowState *WindowState `json:"windowState,omitempty"`
|
||||||
|
LastOpenedAt string `json:"lastOpenedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowState stores the last window position and size.
|
||||||
|
type WindowState struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Maximized bool `json:"maximized"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager provides thread-safe access to app settings.
|
||||||
|
type Manager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
config *Config
|
||||||
|
configPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfigPath returns the default path for app settings.
|
||||||
|
func DefaultConfigPath() string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
home = "."
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".config", "verstak", "config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new app settings manager.
|
||||||
|
func NewManager(configPath string) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
config: nil,
|
||||||
|
configPath: configPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultManager creates a manager with the default config path.
|
||||||
|
func NewDefaultManager() *Manager {
|
||||||
|
return NewManager(DefaultConfigPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads app settings from disk, creating defaults if missing.
|
||||||
|
func (m *Manager) Load() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
m.config = defaultConfig()
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read app settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
// Corrupt config: backup and create defaults
|
||||||
|
backupPath := m.configPath + ".corrupt." + time.Now().Format("20060102-150405")
|
||||||
|
os.WriteFile(backupPath, data, 0o600)
|
||||||
|
m.config = defaultConfig()
|
||||||
|
if saveErr := m.saveLocked(); saveErr != nil {
|
||||||
|
return fmt.Errorf("corrupt config (backed up to %s), failed to save defaults: %w", backupPath, saveErr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("corrupt config (backed up to %s), defaults created", backupPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.SchemaVersion != 1 {
|
||||||
|
cfg.SchemaVersion = 1
|
||||||
|
}
|
||||||
|
if cfg.Theme == "" {
|
||||||
|
cfg.Theme = "dark"
|
||||||
|
}
|
||||||
|
if cfg.RecentVaults == nil {
|
||||||
|
cfg.RecentVaults = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.config = &cfg
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save writes app settings to disk.
|
||||||
|
func (m *Manager) Save() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) saveLocked() error {
|
||||||
|
dir := filepath.Dir(m.configPath)
|
||||||
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(m.config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal app settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := m.configPath + ".tmp"
|
||||||
|
if err := os.WriteFile(tmpFile, data, 0o600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write app settings: %w", err)
|
||||||
|
}
|
||||||
|
return os.Rename(tmpFile, m.configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a copy of the current config.
|
||||||
|
func (m *Manager) Get() *Config {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if m.config == nil {
|
||||||
|
return defaultConfig()
|
||||||
|
}
|
||||||
|
return copyConfig(m.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update patches the config with non-zero values and saves.
|
||||||
|
func (m *Manager) Update(patch *Config) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.config == nil {
|
||||||
|
m.config = defaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
if patch.CurrentVaultPath != "" {
|
||||||
|
m.config.CurrentVaultPath = patch.CurrentVaultPath
|
||||||
|
}
|
||||||
|
if patch.Theme != "" {
|
||||||
|
m.config.Theme = patch.Theme
|
||||||
|
}
|
||||||
|
if patch.UserPluginsDir != "" {
|
||||||
|
m.config.UserPluginsDir = patch.UserPluginsDir
|
||||||
|
}
|
||||||
|
if patch.WindowState != nil {
|
||||||
|
m.config.WindowState = patch.WindowState
|
||||||
|
}
|
||||||
|
m.config.DevMode = patch.DevMode
|
||||||
|
|
||||||
|
m.config.LastOpenedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCurrentVault updates the current vault path and adds to recents.
|
||||||
|
func (m *Manager) SetCurrentVault(path string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.config == nil {
|
||||||
|
m.config = defaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.config.CurrentVaultPath = path
|
||||||
|
m.config.LastOpenedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
// Add to recents (deduplicate, keep max 10)
|
||||||
|
m.config.RecentVaults = addRecent(m.config.RecentVaults, path, 10)
|
||||||
|
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCurrentVault clears the current vault path (e.g. on close).
|
||||||
|
func (m *Manager) ClearCurrentVault() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.config == nil {
|
||||||
|
m.config = defaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.config.CurrentVaultPath = ""
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
SchemaVersion: 1,
|
||||||
|
CurrentVaultPath: "",
|
||||||
|
RecentVaults: []string{},
|
||||||
|
Theme: "dark",
|
||||||
|
DevMode: false,
|
||||||
|
UserPluginsDir: filepath.Join(os.Getenv("HOME"), ".config", "verstak", "plugins"),
|
||||||
|
WindowState: &WindowState{Width: 1200, Height: 800},
|
||||||
|
LastOpenedAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyConfig(c *Config) *Config {
|
||||||
|
recent := make([]string, len(c.RecentVaults))
|
||||||
|
copy(recent, c.RecentVaults)
|
||||||
|
cfg := &Config{
|
||||||
|
SchemaVersion: c.SchemaVersion,
|
||||||
|
CurrentVaultPath: c.CurrentVaultPath,
|
||||||
|
RecentVaults: recent,
|
||||||
|
Theme: c.Theme,
|
||||||
|
DevMode: c.DevMode,
|
||||||
|
UserPluginsDir: c.UserPluginsDir,
|
||||||
|
LastOpenedAt: c.LastOpenedAt,
|
||||||
|
}
|
||||||
|
if c.WindowState != nil {
|
||||||
|
cfg.WindowState = &WindowState{
|
||||||
|
Width: c.WindowState.Width,
|
||||||
|
Height: c.WindowState.Height,
|
||||||
|
Maximized: c.WindowState.Maximized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRecent(list []string, path string, max int) []string {
|
||||||
|
// Remove if already exists
|
||||||
|
filtered := make([]string, 0, len(list))
|
||||||
|
for _, p := range list {
|
||||||
|
if p != path {
|
||||||
|
filtered = append(filtered, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prepend
|
||||||
|
result := append([]string{path}, filtered...)
|
||||||
|
// Trim
|
||||||
|
if len(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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
package appsettings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad_DefaultCreation(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
|
m := NewManager(path)
|
||||||
|
if err := m.Load(); err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := m.Get()
|
||||||
|
if cfg.SchemaVersion != 1 {
|
||||||
|
t.Errorf("SchemaVersion: got %d, want 1", cfg.SchemaVersion)
|
||||||
|
}
|
||||||
|
if cfg.Theme != "dark" {
|
||||||
|
t.Errorf("Theme: got %q, want %q", cfg.Theme, "dark")
|
||||||
|
}
|
||||||
|
if cfg.CurrentVaultPath != "" {
|
||||||
|
t.Errorf("CurrentVaultPath: got %q, want empty", cfg.CurrentVaultPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_CorruptConfig(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
|
// Write corrupt JSON
|
||||||
|
if err := os.WriteFile(path, []byte("{not json"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(path)
|
||||||
|
err := m.Load()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for corrupt config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have created defaults
|
||||||
|
cfg := m.Get()
|
||||||
|
if cfg.SchemaVersion != 1 {
|
||||||
|
t.Errorf("SchemaVersion: got %d, want 1", cfg.SchemaVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check backup exists
|
||||||
|
// Just verify no panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCurrentVault(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
|
m := NewManager(path)
|
||||||
|
if err := m.Load(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.SetCurrentVault("/home/user/vault1"); err != nil {
|
||||||
|
t.Fatalf("SetCurrentVault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := m.Get()
|
||||||
|
if cfg.CurrentVaultPath != "/home/user/vault1" {
|
||||||
|
t.Errorf("CurrentVaultPath: got %q, want %q", cfg.CurrentVaultPath, "/home/user/vault1")
|
||||||
|
}
|
||||||
|
if len(cfg.RecentVaults) != 1 || cfg.RecentVaults[0] != "/home/user/vault1" {
|
||||||
|
t.Errorf("RecentVaults: got %v", cfg.RecentVaults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecentVaults_NoDuplicates(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
|
m := NewManager(path)
|
||||||
|
if err := m.Load(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetCurrentVault("/vault/a")
|
||||||
|
m.SetCurrentVault("/vault/b")
|
||||||
|
m.SetCurrentVault("/vault/a") // duplicate
|
||||||
|
|
||||||
|
cfg := m.Get()
|
||||||
|
count := 0
|
||||||
|
for _, r := range cfg.RecentVaults {
|
||||||
|
if r == "/vault/a" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("expected 1 occurrence of /vault/a in recents, got %d: %v", count, cfg.RecentVaults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate_Patch(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
|
m := NewManager(path)
|
||||||
|
if err := m.Load(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Update(&Config{
|
||||||
|
Theme: "light",
|
||||||
|
DevMode: true,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := m.Get()
|
||||||
|
if cfg.Theme != "light" {
|
||||||
|
t.Errorf("Theme: got %q, want %q", cfg.Theme, "light")
|
||||||
|
}
|
||||||
|
if !cfg.DevMode {
|
||||||
|
t.Error("DevMode: got false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppSettings_NotInsideVault(t *testing.T) {
|
||||||
|
// App settings path should be under ~/.config/verstak/, not inside vault
|
||||||
|
path := DefaultConfigPath()
|
||||||
|
if filepath.Base(filepath.Dir(path)) != "verstak" {
|
||||||
|
t.Errorf("app settings path should be under .config/verstak, got %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
// Package pluginstate manages the vault-level plugin state (enabled/disabled, desired plugins).
|
||||||
|
// This is stored inside the vault at .verstak/plugins.json, separate from app settings
|
||||||
|
// and separate from individual plugin settings.
|
||||||
|
package pluginstate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VaultPluginState represents the plugin state for a specific vault.
|
||||||
|
type VaultPluginState struct {
|
||||||
|
SchemaVersion int `json:"schemaVersion"`
|
||||||
|
EnabledPlugins []string `json:"enabledPlugins"`
|
||||||
|
DisabledPlugins []string `json:"disabledPlugins"`
|
||||||
|
DesiredPlugins []DesiredPlugin `json:"desiredPlugins"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DesiredPlugin records a plugin that should be available in this vault.
|
||||||
|
type DesiredPlugin struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager provides thread-safe access to vault plugin state.
|
||||||
|
type Manager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
state *VaultPluginState
|
||||||
|
vault *vault.Vault
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new vault plugin state manager.
|
||||||
|
func NewManager(v *vault.Vault) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
vault: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads the vault plugin state from .verstak/plugins.json.
|
||||||
|
func (m *Manager) Load() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.vault.GetVaultStatus() != vault.StatusOpen {
|
||||||
|
return fmt.Errorf("vault is not open")
|
||||||
|
}
|
||||||
|
|
||||||
|
vaultPath := m.vault.GetVaultPath()
|
||||||
|
statePath := filepath.Join(vaultPath, ".verstak", "plugins.json")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(statePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
m.state = defaultState()
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read vault plugin state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var state VaultPluginState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
// Corrupt: backup and create defaults
|
||||||
|
backupPath := statePath + ".corrupt." + time.Now().Format("20060102-150405")
|
||||||
|
os.WriteFile(backupPath, data, 0o600)
|
||||||
|
m.state = defaultState()
|
||||||
|
if saveErr := m.saveLocked(); saveErr != nil {
|
||||||
|
return fmt.Errorf("corrupt plugins.json (backed up to %s), failed to save defaults: %w", backupPath, saveErr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("corrupt plugins.json (backed up to %s), defaults created", backupPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.SchemaVersion != 1 {
|
||||||
|
state.SchemaVersion = 1
|
||||||
|
}
|
||||||
|
if state.EnabledPlugins == nil {
|
||||||
|
state.EnabledPlugins = []string{}
|
||||||
|
}
|
||||||
|
if state.DisabledPlugins == nil {
|
||||||
|
state.DisabledPlugins = []string{}
|
||||||
|
}
|
||||||
|
if state.DesiredPlugins == nil {
|
||||||
|
state.DesiredPlugins = []DesiredPlugin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.state = &state
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save writes the vault plugin state to disk.
|
||||||
|
func (m *Manager) Save() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) saveLocked() error {
|
||||||
|
if m.state == nil {
|
||||||
|
m.state = defaultState()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.state.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
vaultPath := m.vault.GetVaultPath()
|
||||||
|
statePath := filepath.Join(vaultPath, ".verstak", "plugins.json")
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(m.state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal vault plugin state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := statePath + ".tmp"
|
||||||
|
if err := os.WriteFile(tmpFile, data, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write vault plugin state: %w", err)
|
||||||
|
}
|
||||||
|
return os.Rename(tmpFile, statePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a copy of the current state.
|
||||||
|
func (m *Manager) Get() *VaultPluginState {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if m.state == nil {
|
||||||
|
return defaultState()
|
||||||
|
}
|
||||||
|
return copyState(m.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled checks if a plugin is enabled.
|
||||||
|
func (m *Manager) IsEnabled(pluginID string) bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if m.state == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, id := range m.state.EnabledPlugins {
|
||||||
|
if id == pluginID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDisabled checks if a plugin is explicitly disabled.
|
||||||
|
func (m *Manager) IsDisabled(pluginID string) bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if m.state == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, id := range m.state.DisabledPlugins {
|
||||||
|
if id == pluginID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnablePlugin enables a plugin.
|
||||||
|
func (m *Manager) EnablePlugin(pluginID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.state == nil {
|
||||||
|
m.state = defaultState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from disabled
|
||||||
|
m.state.DisabledPlugins = removeString(m.state.DisabledPlugins, pluginID)
|
||||||
|
|
||||||
|
// Add to enabled if not already there
|
||||||
|
if !containsString(m.state.EnabledPlugins, pluginID) {
|
||||||
|
m.state.EnabledPlugins = append(m.state.EnabledPlugins, pluginID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisablePlugin disables a plugin.
|
||||||
|
func (m *Manager) DisablePlugin(pluginID string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.state == nil {
|
||||||
|
m.state = defaultState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from enabled
|
||||||
|
m.state.EnabledPlugins = removeString(m.state.EnabledPlugins, pluginID)
|
||||||
|
|
||||||
|
// Add to disabled if not already there
|
||||||
|
if !containsString(m.state.DisabledPlugins, pluginID) {
|
||||||
|
m.state.DisabledPlugins = append(m.state.DisabledPlugins, pluginID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordDesiredPlugin adds or updates a desired plugin entry.
|
||||||
|
func (m *Manager) RecordDesiredPlugin(id, version, source string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.state == nil {
|
||||||
|
m.state = defaultState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update if exists
|
||||||
|
for i, dp := range m.state.DesiredPlugins {
|
||||||
|
if dp.ID == id {
|
||||||
|
m.state.DesiredPlugins[i].Version = version
|
||||||
|
m.state.DesiredPlugins[i].Source = source
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new
|
||||||
|
m.state.DesiredPlugins = append(m.state.DesiredPlugins, DesiredPlugin{
|
||||||
|
ID: id,
|
||||||
|
Version: version,
|
||||||
|
Source: source,
|
||||||
|
})
|
||||||
|
|
||||||
|
return m.saveLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMissingInstalled returns desired plugins that are not currently installed.
|
||||||
|
func (m *Manager) ListMissingInstalled(installedIDs []string) []DesiredPlugin {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
installed := make(map[string]bool)
|
||||||
|
for _, id := range installedIDs {
|
||||||
|
installed[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing []DesiredPlugin
|
||||||
|
for _, dp := range m.state.DesiredPlugins {
|
||||||
|
if !installed[dp.ID] {
|
||||||
|
missing = append(missing, dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missing
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultState() *VaultPluginState {
|
||||||
|
return &VaultPluginState{
|
||||||
|
SchemaVersion: 1,
|
||||||
|
EnabledPlugins: []string{},
|
||||||
|
DisabledPlugins: []string{},
|
||||||
|
DesiredPlugins: []DesiredPlugin{},
|
||||||
|
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyState(s *VaultPluginState) *VaultPluginState {
|
||||||
|
enabled := make([]string, len(s.EnabledPlugins))
|
||||||
|
copy(enabled, s.EnabledPlugins)
|
||||||
|
disabled := make([]string, len(s.DisabledPlugins))
|
||||||
|
copy(disabled, s.DisabledPlugins)
|
||||||
|
desired := make([]DesiredPlugin, len(s.DesiredPlugins))
|
||||||
|
copy(desired, s.DesiredPlugins)
|
||||||
|
return &VaultPluginState{
|
||||||
|
SchemaVersion: s.SchemaVersion,
|
||||||
|
EnabledPlugins: enabled,
|
||||||
|
DisabledPlugins: disabled,
|
||||||
|
DesiredPlugins: desired,
|
||||||
|
UpdatedAt: s.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsString(list []string, s string) bool {
|
||||||
|
for _, item := range list {
|
||||||
|
if item == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeString(list []string, s string) []string {
|
||||||
|
result := make([]string, 0, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
if item != s {
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
package pluginstate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad_DefaultCreation(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
if err := v.CreateVault(dir); err != nil {
|
||||||
|
t.Fatalf("CreateVault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(v)
|
||||||
|
if err := m.Load(); err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := m.Get()
|
||||||
|
if state.SchemaVersion != 1 {
|
||||||
|
t.Errorf("SchemaVersion: got %d, want 1", state.SchemaVersion)
|
||||||
|
}
|
||||||
|
if len(state.EnabledPlugins) != 0 {
|
||||||
|
t.Errorf("EnabledPlugins: expected empty, got %v", state.EnabledPlugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnableDisable(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
if err := v.CreateVault(dir); err != nil {
|
||||||
|
t.Fatalf("CreateVault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(v)
|
||||||
|
if err := m.Load(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable
|
||||||
|
if err := m.EnablePlugin("test-plugin"); err != nil {
|
||||||
|
t.Fatalf("EnablePlugin: %v", err)
|
||||||
|
}
|
||||||
|
if !m.IsEnabled("test-plugin") {
|
||||||
|
t.Error("expected test-plugin to be enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable
|
||||||
|
if err := m.DisablePlugin("test-plugin"); err != nil {
|
||||||
|
t.Fatalf("DisablePlugin: %v", err)
|
||||||
|
}
|
||||||
|
if !m.IsDisabled("test-plugin") {
|
||||||
|
t.Error("expected test-plugin to be disabled")
|
||||||
|
}
|
||||||
|
if m.IsEnabled("test-plugin") {
|
||||||
|
t.Error("expected test-plugin to NOT be enabled after disable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisablePlugin_Persists(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
if err := v.CreateVault(dir); err != nil {
|
||||||
|
t.Fatalf("CreateVault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(v)
|
||||||
|
m.Load()
|
||||||
|
|
||||||
|
m.EnablePlugin("test-plugin")
|
||||||
|
m.DisablePlugin("test-plugin")
|
||||||
|
|
||||||
|
// Re-load from disk
|
||||||
|
m2 := NewManager(v)
|
||||||
|
m2.Load()
|
||||||
|
|
||||||
|
if m2.IsEnabled("test-plugin") {
|
||||||
|
t.Error("disabled plugin should not be enabled after reload")
|
||||||
|
}
|
||||||
|
if !m2.IsDisabled("test-plugin") {
|
||||||
|
t.Error("disabled plugin should be disabled after reload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordDesiredPlugin(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
if err := v.CreateVault(dir); err != nil {
|
||||||
|
t.Fatalf("CreateVault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(v)
|
||||||
|
m.Load()
|
||||||
|
|
||||||
|
if err := m.RecordDesiredPlugin("test-plugin", "1.0.0", "official"); err != nil {
|
||||||
|
t.Fatalf("RecordDesiredPlugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := m.Get()
|
||||||
|
if len(state.DesiredPlugins) != 1 {
|
||||||
|
t.Fatalf("DesiredPlugins: expected 1, got %d", len(state.DesiredPlugins))
|
||||||
|
}
|
||||||
|
if state.DesiredPlugins[0].ID != "test-plugin" {
|
||||||
|
t.Errorf("DesiredPlugin ID: got %q, want %q", state.DesiredPlugins[0].ID, "test-plugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingInstalled(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
if err := v.CreateVault(dir); err != nil {
|
||||||
|
t.Fatalf("CreateVault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewManager(v)
|
||||||
|
m.Load()
|
||||||
|
|
||||||
|
m.RecordDesiredPlugin("plugin-a", "1.0.0", "official")
|
||||||
|
m.RecordDesiredPlugin("plugin-b", "2.0.0", "local")
|
||||||
|
m.RecordDesiredPlugin("plugin-c", "3.0.0", "official")
|
||||||
|
|
||||||
|
// Only plugin-a is installed
|
||||||
|
missing := m.ListMissingInstalled([]string{"plugin-a"})
|
||||||
|
if len(missing) != 2 {
|
||||||
|
t.Fatalf("expected 2 missing, got %d", len(missing))
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make(map[string]bool)
|
||||||
|
for _, dp := range missing {
|
||||||
|
ids[dp.ID] = true
|
||||||
|
}
|
||||||
|
if !ids["plugin-b"] || !ids["plugin-c"] {
|
||||||
|
t.Errorf("expected plugin-b and plugin-c in missing, got %v", ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorruptPluginsJSON(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
if err := v.CreateVault(dir); err != nil {
|
||||||
|
t.Fatalf("CreateVault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create corrupt plugins.json
|
||||||
|
vaultPath := v.GetVaultPath()
|
||||||
|
pluginsPath := filepath.Join(vaultPath, ".verstak", "plugins.json")
|
||||||
|
os.WriteFile(pluginsPath, []byte("{not json"), 0o644)
|
||||||
|
|
||||||
|
m := NewManager(v)
|
||||||
|
err := m.Load()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for corrupt plugins.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have created defaults
|
||||||
|
state := m.Get()
|
||||||
|
if state.SchemaVersion != 1 {
|
||||||
|
t.Errorf("SchemaVersion: got %d, want 1", state.SchemaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultClosed_StateUnavailable(t *testing.T) {
|
||||||
|
v := vault.NewVault(nil)
|
||||||
|
// Don't open vault — state should fail
|
||||||
|
|
||||||
|
m := NewManager(v)
|
||||||
|
err := m.Load()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when vault is not open")
|
||||||
|
}
|
||||||
|
}
|
||||||
37
main.go
37
main.go
|
|
@ -13,11 +13,13 @@ import (
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
|
||||||
"github.com/verstak/verstak-desktop/internal/api"
|
"github.com/verstak/verstak-desktop/internal/api"
|
||||||
|
"github.com/verstak/verstak-desktop/internal/core/appsettings"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/capability"
|
"github.com/verstak/verstak-desktop/internal/core/capability"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/events"
|
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
||||||
"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/storage"
|
"github.com/verstak/verstak-desktop/internal/core/storage"
|
||||||
"github.com/verstak/verstak-desktop/internal/core/vault"
|
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||||||
)
|
)
|
||||||
|
|
@ -71,6 +73,31 @@ func main() {
|
||||||
}
|
}
|
||||||
log.Printf("[main] registered vault capability")
|
log.Printf("[main] registered vault capability")
|
||||||
|
|
||||||
|
// ─── Initialize App Settings ─────────────────────────────
|
||||||
|
appSettingsMgr := appsettings.NewDefaultManager()
|
||||||
|
if err := appSettingsMgr.Load(); err != nil {
|
||||||
|
log.Printf("[main] app settings: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Vault Auto-Open ─────────────────────────────────────
|
||||||
|
// If currentVaultPath is set in app settings, try to open it.
|
||||||
|
cfg := appSettingsMgr.Get()
|
||||||
|
if cfg.CurrentVaultPath != "" {
|
||||||
|
if err := vaultService.OpenVault(cfg.CurrentVaultPath); err != nil {
|
||||||
|
log.Printf("[main] failed to auto-open vault at %s: %v", cfg.CurrentVaultPath, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[main] auto-opened vault at %s", cfg.CurrentVaultPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Initialize Vault Plugin State ───────────────────────
|
||||||
|
pluginStateMgr := pluginstate.NewManager(vaultService)
|
||||||
|
if vaultService.GetVaultStatus() == vault.StatusOpen {
|
||||||
|
if err := pluginStateMgr.Load(); err != nil {
|
||||||
|
log.Printf("[main] vault plugin state: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Plugin Discovery ───────────────────────────────────
|
// ─── Plugin Discovery ───────────────────────────────────
|
||||||
// Resolve plugin directories relative to the binary location,
|
// Resolve plugin directories relative to the binary location,
|
||||||
// not CWD (Wails may launch from a different directory).
|
// not CWD (Wails may launch from a different directory).
|
||||||
|
|
@ -100,6 +127,14 @@ func main() {
|
||||||
for i := range plugins {
|
for i := range plugins {
|
||||||
p := &plugins[i]
|
p := &plugins[i]
|
||||||
|
|
||||||
|
// Check if plugin is disabled in vault plugin state
|
||||||
|
if pluginStateMgr != nil && pluginStateMgr.IsDisabled(p.Manifest.ID) {
|
||||||
|
log.Printf("[plugin] %s: disabled in vault plugin state — skipping", p.Manifest.ID)
|
||||||
|
p.Status = plugin.StatusDisabled
|
||||||
|
p.Enabled = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Register provided capabilities
|
// Register provided capabilities
|
||||||
if len(p.Manifest.Provides) > 0 {
|
if len(p.Manifest.Provides) > 0 {
|
||||||
if err := capRegistry.Register(p.Manifest.ID, p.Manifest.Provides); err != nil {
|
if err := capRegistry.Register(p.Manifest.ID, p.Manifest.Provides); err != nil {
|
||||||
|
|
@ -157,7 +192,7 @@ func main() {
|
||||||
|
|
||||||
// Create the App struct
|
// Create the App struct
|
||||||
storageService := storage.New(vaultService)
|
storageService := storage.New(vaultService)
|
||||||
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService)
|
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, appSettingsMgr, pluginStateMgr)
|
||||||
|
|
||||||
// ─── Wails App ───────────────────────────────────────────
|
// ─── Wails App ───────────────────────────────────────────
|
||||||
err := wails.Run(&options.App{
|
err := wails.Run(&options.App{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue