Skip to content

Commit 9065a1b

Browse files
committed
feat: Add channel selection for plugin installation and enhance pre-built plugin handling
1 parent ae4a2b6 commit 9065a1b

6 files changed

Lines changed: 303 additions & 42 deletions

File tree

app/plugins/loader.go

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,31 @@ func (p *PluginLoader) LoadPlugins() ([]types.Plugin, error) {
123123
pluginDir := filepath.Join(p.pluginsDir, entry.Name())
124124
pluginJSONPath := filepath.Join(pluginDir, "plugin.json")
125125
pluginGoPath := filepath.Join(pluginDir, "plugin.go")
126+
prebuiltMarker := filepath.Join(pluginDir, ".prebuilt")
126127

127-
// Check if plugin.json exists
128+
// Check if plugin.json exists (required for all plugins)
128129
if _, err := os.Stat(pluginJSONPath); os.IsNotExist(err) {
129130
continue
130131
}
131132

132-
// Check if plugin.go exists
133-
if _, err := os.Stat(pluginGoPath); os.IsNotExist(err) {
134-
continue
133+
// Check if this is a pre-built plugin (has .prebuilt marker or binary but no plugin.go)
134+
isPrebuilt := false
135+
if _, err := os.Stat(prebuiltMarker); !os.IsNotExist(err) {
136+
isPrebuilt = true
137+
} else if _, err := os.Stat(pluginGoPath); os.IsNotExist(err) {
138+
// No plugin.go - check if there's a binary with the plugin ID name
139+
pluginID := entry.Name()
140+
binaryPath := filepath.Join(pluginDir, pluginID)
141+
if _, err := os.Stat(binaryPath); !os.IsNotExist(err) {
142+
isPrebuilt = true
143+
}
144+
}
145+
146+
// For non-prebuilt plugins, plugin.go must exist
147+
if !isPrebuilt {
148+
if _, err := os.Stat(pluginGoPath); os.IsNotExist(err) {
149+
continue
150+
}
135151
}
136152

137153
// Read plugin.json
@@ -154,26 +170,38 @@ func (p *PluginLoader) LoadPlugins() ([]types.Plugin, error) {
154170
}
155171

156172
pluginID := pluginDef.ID
157-
fmt.Printf("Registering plugin from filesystem: %s\n", pluginID)
158-
159-
// Create a wrapper execution function that dynamically imports and executes the plugin
160-
p.pluginExecuteFuncs[pluginID] = func(params map[string]interface{}) (interface{}, error) {
161-
// Try to build and load the plugin dynamically
162-
pluginInstance, err := p.loadPlugin(pluginDir, pluginID)
163-
if err != nil {
164-
return nil, fmt.Errorf("failed to load plugin %s: %v", pluginID, err)
165-
}
173+
if isPrebuilt {
174+
fmt.Printf("Registering pre-built plugin from filesystem: %s\n", pluginID)
175+
} else {
176+
fmt.Printf("Registering plugin from filesystem: %s\n", pluginID)
177+
}
166178

167-
// Execute the plugin
168-
return pluginInstance.Execute(params)
179+
// Create execution function based on plugin type
180+
if isPrebuilt {
181+
// For pre-built plugins, execute the binary directly
182+
binaryPath := filepath.Join(pluginDir, pluginID)
183+
p.pluginExecuteFuncs[pluginID] = createPrebuiltExecuteFunc(binaryPath, pluginID)
184+
} else {
185+
// For source plugins, dynamically build and load
186+
capturedPluginDir := pluginDir
187+
capturedPluginID := pluginID
188+
p.pluginExecuteFuncs[pluginID] = func(params map[string]interface{}) (interface{}, error) {
189+
// Try to build and load the plugin dynamically
190+
pluginInstance, err := p.loadPlugin(capturedPluginDir, capturedPluginID)
191+
if err != nil {
192+
return nil, fmt.Errorf("failed to load plugin %s: %v", capturedPluginID, err)
193+
}
194+
195+
// Execute the plugin
196+
return pluginInstance.Execute(params)
197+
}
169198
}
170199

171200
// Register with the registry
172201
registry.RegisterPluginFunc(pluginID, p.pluginExecuteFuncs[pluginID])
173202

174-
// Also register the plugin execution functions from the helper
175-
// Skip override for plugins that have proper standalone implementations
176-
if pluginID != "dns_lookup" {
203+
// For source plugins, also try to register the helper functions
204+
if !isPrebuilt && pluginID != "dns_lookup" {
177205
if helperFunc, err := LoadPluginFunc(pluginDir, pluginID); err == nil {
178206
// Override with the helper function if available
179207
registry.RegisterPluginFunc(pluginID, helperFunc)
@@ -184,6 +212,36 @@ func (p *PluginLoader) LoadPlugins() ([]types.Plugin, error) {
184212
return p.plugins, nil
185213
}
186214

215+
// createPrebuiltExecuteFunc creates an execution function for a pre-built binary plugin
216+
func createPrebuiltExecuteFunc(binaryPath, pluginID string) func(map[string]interface{}) (interface{}, error) {
217+
return func(params map[string]interface{}) (interface{}, error) {
218+
// Convert parameters to JSON
219+
paramsJSON, err := json.Marshal(params)
220+
if err != nil {
221+
return nil, fmt.Errorf("failed to marshal parameters: %v", err)
222+
}
223+
224+
// Execute the binary with the --execute flag
225+
cmd := exec.Command(binaryPath, "--execute="+string(paramsJSON))
226+
output, err := cmd.CombinedOutput()
227+
if err != nil {
228+
return nil, fmt.Errorf("failed to execute plugin %s: %v\nOutput: %s", pluginID, err, string(output))
229+
}
230+
231+
// Try to parse the output as JSON
232+
var result interface{}
233+
if err := json.Unmarshal(output, &result); err != nil {
234+
// If not valid JSON, return as string
235+
return map[string]interface{}{
236+
"result": string(output),
237+
"params": params,
238+
}, nil
239+
}
240+
241+
return result, nil
242+
}
243+
}
244+
187245
// loadPlugin loads a plugin from the given directory
188246
func (p *PluginLoader) loadPlugin(pluginDir string, pluginID string) (types.Plugin, error) {
189247
// Try to build the plugin

app/plugins/plugin_installer.go

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,10 +1709,17 @@ type BulkInstallResponse struct {
17091709
SuccessCount int `json:"successCount"`
17101710
FailureCount int `json:"failureCount"`
17111711
OverallSuccess bool `json:"overallSuccess"`
1712+
Channel string `json:"channel,omitempty"`
17121713
}
17131714

17141715
// BulkInstallPlugins installs multiple plugins from a list of repositories
1716+
// Deprecated: Use BulkInstallPluginsWithChannel instead
17151717
func (pi *PluginInstaller) BulkInstallPlugins(repositories []string) BulkInstallResponse {
1718+
return pi.BulkInstallPluginsWithChannel(repositories, ReleaseChannelStable)
1719+
}
1720+
1721+
// BulkInstallPluginsWithChannel installs multiple plugins from a list of repositories using the specified channel
1722+
func (pi *PluginInstaller) BulkInstallPluginsWithChannel(repositories []string, channel ReleaseChannel) BulkInstallResponse {
17161723
results := make([]BulkInstallResult, 0, len(repositories))
17171724
successCount := 0
17181725
failureCount := 0
@@ -1722,7 +1729,32 @@ func (pi *PluginInstaller) BulkInstallPlugins(repositories []string) BulkInstall
17221729
PluginID: extractPluginIDFromRepo(repo),
17231730
}
17241731

1725-
plugin, err := pi.InstallPlugin(repo)
1732+
// Extract org and repo name from the URL
1733+
parts := strings.Split(repo, "/")
1734+
var org, repoName string
1735+
for i, part := range parts {
1736+
if part == "github.com" && i+2 < len(parts) {
1737+
org = parts[i+1]
1738+
repoName = parts[i+2]
1739+
break
1740+
}
1741+
}
1742+
1743+
if org == "" || repoName == "" {
1744+
// Fallback: use last two parts
1745+
if len(parts) >= 2 {
1746+
org = parts[len(parts)-2]
1747+
repoName = parts[len(parts)-1]
1748+
}
1749+
}
1750+
1751+
// Remove .git suffix if present
1752+
if strings.HasSuffix(repoName, ".git") {
1753+
repoName = repoName[:len(repoName)-4]
1754+
}
1755+
1756+
// Install using the channel-aware method
1757+
plugin, err := pi.InstallFromGitHubWithChannel(org, repoName, channel)
17261758
if err != nil {
17271759
result.Success = false
17281760
result.Error = err.Error()
@@ -1742,6 +1774,7 @@ func (pi *PluginInstaller) BulkInstallPlugins(repositories []string) BulkInstall
17421774
SuccessCount: successCount,
17431775
FailureCount: failureCount,
17441776
OverallSuccess: failureCount == 0,
1777+
Channel: string(channel),
17451778
}
17461779
}
17471780

@@ -2003,8 +2036,30 @@ func (pi *PluginInstaller) extractTarGz(reader io.Reader, destDir string) error
20032036
return nil
20042037
}
20052038

2039+
// ReleaseChannel represents the type of release to download
2040+
type ReleaseChannel string
2041+
2042+
const (
2043+
// ReleaseChannelStable is for stable releases (tagged versions)
2044+
ReleaseChannelStable ReleaseChannel = "stable"
2045+
// ReleaseChannelBeta is for beta releases (pre-release builds)
2046+
ReleaseChannelBeta ReleaseChannel = "beta"
2047+
// ReleaseChannelSource installs from source code
2048+
ReleaseChannelSource ReleaseChannel = "source"
2049+
)
2050+
20062051
// InstallFromGitHubWithBinary installs a plugin, preferring pre-built binaries
2052+
// Deprecated: Use InstallFromGitHubWithChannel instead
20072053
func (pi *PluginInstaller) InstallFromGitHubWithBinary(org string, repo string, useBeta bool) (PluginMetadata, error) {
2054+
channel := ReleaseChannelStable
2055+
if useBeta {
2056+
channel = ReleaseChannelBeta
2057+
}
2058+
return pi.InstallFromGitHubWithChannel(org, repo, channel)
2059+
}
2060+
2061+
// InstallFromGitHubWithChannel installs a plugin using the specified release channel
2062+
func (pi *PluginInstaller) InstallFromGitHubWithChannel(org string, repo string, channel ReleaseChannel) (PluginMetadata, error) {
20082063
// Extract plugin ID from repo name
20092064
pluginID := repo
20102065
if strings.HasPrefix(repo, "Plugin_") {
@@ -2017,18 +2072,26 @@ func (pi *PluginInstaller) InstallFromGitHubWithBinary(org string, repo string,
20172072
return PluginMetadata{}, fmt.Errorf("plugin with ID %s already exists", pluginID)
20182073
}
20192074

2075+
// Source channel always clones from GitHub
2076+
if channel == ReleaseChannelSource {
2077+
return pi.InstallFromGitHub(org, repo, "main")
2078+
}
2079+
20202080
// Try to download pre-built binary first
2081+
useBeta := channel == ReleaseChannelBeta
20212082
err := pi.downloadPrebuiltBinary(org, repo, pluginID, useBeta)
20222083
if err != nil {
2023-
log.Printf("Pre-built binary not available: %v", err)
2084+
log.Printf("Pre-built binary not available for %s channel: %v", channel, err)
20242085
log.Printf("Falling back to source installation...")
20252086

20262087
// Fall back to cloning from GitHub
2027-
branch := "main"
2028-
if useBeta {
2029-
branch = "main" // Beta still uses main branch, just different release
2030-
}
2031-
return pi.InstallFromGitHub(org, repo, branch)
2088+
return pi.InstallFromGitHub(org, repo, "main")
2089+
}
2090+
2091+
// Mark plugin as pre-built (no plugin.go file)
2092+
prebuiltMarker := filepath.Join(pluginDir, ".prebuilt")
2093+
if err := os.WriteFile(prebuiltMarker, []byte(string(channel)), 0644); err != nil {
2094+
log.Printf("Warning: Failed to create prebuilt marker: %v", err)
20322095
}
20332096

20342097
// Read plugin metadata

app/plugins/plugin_manager.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package plugins
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"os"
@@ -165,6 +166,12 @@ func (pm *PluginManager) RefreshPlugins() error {
165166

166167
pluginDir := filepath.Join("app/plugins/plugins", entry.Name())
167168
pluginID := entry.Name()
169+
pluginJSONPath := filepath.Join(pluginDir, "plugin.json")
170+
171+
// Plugin.json must exist for all plugins
172+
if _, err := os.Stat(pluginJSONPath); os.IsNotExist(err) {
173+
continue
174+
}
168175

169176
// Get the plugin execution function from the registry
170177
executeFunc, err := registry.GetPluginFunc(pluginID)
@@ -173,15 +180,39 @@ func (pm *PluginManager) RefreshPlugins() error {
173180
continue
174181
}
175182

176-
// Try to get plugin definition
177-
plugin, err := loader.loadPlugin(pluginDir, pluginID)
178-
if err != nil {
179-
fmt.Printf("Warning: Failed to load plugin %s: %v\n", pluginID, err)
183+
// Check if this is a pre-built plugin
184+
prebuiltMarker := filepath.Join(pluginDir, ".prebuilt")
185+
pluginGoPath := filepath.Join(pluginDir, "plugin.go")
186+
isPrebuilt := false
187+
if _, err := os.Stat(prebuiltMarker); !os.IsNotExist(err) {
188+
isPrebuilt = true
189+
} else if _, err := os.Stat(pluginGoPath); os.IsNotExist(err) {
190+
// No plugin.go - check if there's a binary
191+
binaryPath := filepath.Join(pluginDir, pluginID)
192+
if _, err := os.Stat(binaryPath); !os.IsNotExist(err) {
193+
isPrebuilt = true
194+
}
195+
}
196+
197+
// Read plugin definition from plugin.json
198+
var definition types.PluginDefinition
199+
if jsonData, err := os.ReadFile(pluginJSONPath); err == nil {
200+
if err := json.Unmarshal(jsonData, &definition); err != nil {
201+
fmt.Printf("Warning: Failed to parse plugin.json for %s: %v\n", pluginID, err)
202+
continue
203+
}
204+
} else {
205+
fmt.Printf("Warning: Failed to read plugin.json for %s: %v\n", pluginID, err)
180206
continue
181207
}
182208

183-
// Get plugin definition
184-
definition := plugin.GetDefinition()
209+
// For source plugins, try to get dynamic definition
210+
if !isPrebuilt {
211+
plugin, err := loader.loadPlugin(pluginDir, pluginID)
212+
if err == nil {
213+
definition = plugin.GetDefinition()
214+
}
215+
}
185216

186217
// Register the plugin
187218
pm.plugins[pluginID] = &Plugin{
@@ -196,7 +227,11 @@ func (pm *PluginManager) RefreshPlugins() error {
196227
Execute: executeFunc,
197228
}
198229

199-
fmt.Printf("Registered plugin: %s (%s)\n", definition.Name, definition.ID)
230+
if isPrebuilt {
231+
fmt.Printf("Registered pre-built plugin: %s (%s)\n", definition.Name, definition.ID)
232+
} else {
233+
fmt.Printf("Registered plugin: %s (%s)\n", definition.Name, definition.ID)
234+
}
200235
}
201236

202237
// Register hardcoded plugins if they don't already exist

frontend/src/api/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,12 @@ export const pluginManagerApi = {
124124
// Refresh plugin catalog from GitHub
125125
refreshCatalog: () => api.post('/plugins/manage/refresh-catalog'),
126126

127-
// Install plugin from repository
128-
install: (repository) => api.post('/plugins/manage/install', { repository }),
127+
// Install plugin from repository with channel selection
128+
// channel: 'stable' (default), 'beta', or 'source'
129+
install: (repository, channel = 'stable') => api.post('/plugins/manage/install', { repository, channel }),
129130

130-
// Bulk install multiple plugins
131-
bulkInstall: (repositories) => api.post('/plugins/manage/bulk-install', { repositories }),
131+
// Bulk install multiple plugins with channel selection
132+
bulkInstall: (repositories, channel = 'stable') => api.post('/plugins/manage/bulk-install', { repositories, channel }),
132133

133134
// Update a specific plugin
134135
update: (id) => api.post(`/plugins/manage/update/${id}`),

0 commit comments

Comments
 (0)