diff --git a/internal/plugin/database.go b/internal/plugin/database.go index fd4ef42..6e669dc 100644 --- a/internal/plugin/database.go +++ b/internal/plugin/database.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path" "sync" ) @@ -58,3 +59,34 @@ func (d *PluginDatabase) Save() error { return nil } + +// Find all extract directories that are not referenced in the Plugins map and remove them +func (d *PluginDatabase) CleanupExtractDirectories() error { + extractDirectories, err := os.ReadDir(pluginsExtractsFolder) + if err != nil { + return fmt.Errorf("failed to read extract directories: %v", err) + } + + for _, extractDir := range extractDirectories { + found := false + for _, pluginInstall := range d.Plugins { + for _, extractedFolder := range pluginInstall.ExtractedVersions { + if extractDir.Name() == extractedFolder { + found = true + break + } + } + if found { + break + } + } + + if !found { + if err := os.RemoveAll(path.Join(pluginsExtractsFolder, extractDir.Name())); err != nil { + return fmt.Errorf("failed to remove extract directory: %v", err) + } + } + } + + return nil +} diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index f9dcdf8..5aa0ccb 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -168,7 +168,7 @@ func RpcPluginList() ([]PluginStatus, error) { return plugins, nil } -func RpcUpdateConfig(name string, enabled bool) (*PluginStatus, error) { +func RpcPluginUpdateConfig(name string, enabled bool) (*PluginStatus, error) { pluginInstall, ok := pluginDatabase.Plugins[name] if !ok { return nil, fmt.Errorf("plugin not found: %s", name) @@ -193,6 +193,32 @@ func RpcUpdateConfig(name string, enabled bool) (*PluginStatus, error) { return status, nil } +func RpcPluginUninstall(name string) error { + pluginInstall, ok := pluginDatabase.Plugins[name] + if !ok { + return fmt.Errorf("plugin not found: %s", name) + } + + pluginInstall.Enabled = false + + err := pluginInstall.ReconcileSubprocess() + if err != nil { + return fmt.Errorf("failed to stop plugin %s: %v", name, err) + } + + delete(pluginDatabase.Plugins, name) + if err := pluginDatabase.Save(); err != nil { + return fmt.Errorf("failed to save plugin database: %v", err) + } + + err = pluginDatabase.CleanupExtractDirectories() + if err != nil { + return fmt.Errorf("failed to cleanup extract directories: %v", err) + } + + return nil +} + func readManifest(extractFolder string) (*PluginManifest, error) { manifestPath := path.Join(extractFolder, "manifest.json") manifestFile, err := os.Open(manifestPath) diff --git a/jsonrpc.go b/jsonrpc.go index 05babd5..aea624c 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -559,5 +559,6 @@ var rpcHandlers = map[string]RPCHandler{ "pluginExtract": {Func: plugin.RpcPluginExtract, Params: []string{"filename"}}, "pluginInstall": {Func: plugin.RpcPluginInstall, Params: []string{"name", "version"}}, "pluginList": {Func: plugin.RpcPluginList}, - "pluginUpdateConfig": {Func: plugin.RpcUpdateConfig, Params: []string{"name", "enabled"}}, + "pluginUpdateConfig": {Func: plugin.RpcPluginUpdateConfig, Params: []string{"name", "enabled"}}, + "pluginUninstall": {Func: plugin.RpcPluginUninstall, Params: []string{"name"}}, } diff --git a/ui/src/components/PluginConfigureDialog.tsx b/ui/src/components/PluginConfigureDialog.tsx index c53eb89..04d139f 100644 --- a/ui/src/components/PluginConfigureDialog.tsx +++ b/ui/src/components/PluginConfigureDialog.tsx @@ -46,6 +46,24 @@ function Dialog({ plugin, setOpen }: { plugin: PluginStatus | null, setOpen: (op setLoading(true); send("pluginUpdateConfig", { name: plugin.name, enabled }, resp => { if ("error" in resp) { + setLoading(false); + setError(resp.error.message); + return + } + setOpen(false); + }); + }, [send, plugin, setOpen]) + + const uninstallPlugin = useCallback(() => { + if (!plugin) return; + if (!window.confirm("Are you sure you want to uninstall this plugin? This will not delete any data.")) { + return; + } + + setLoading(true); + send("pluginUninstall", { name: plugin.name }, resp => { + if ("error" in resp) { + setLoading(false); setError(resp.error.message); return } @@ -104,6 +122,15 @@ function Dialog({ plugin, setOpen }: { plugin: PluginStatus | null, setOpen: (op animationDelay: "0.1s", }} > +
+