Add version tracking and validation to prevent downgrade issues

Store the last pluginctl version used in plugin.json props and validate
before running commands. Prevents issues when using older pluginctl
versions on plugins modified by newer versions.

- Add Version field to PluginCtlConfig struct
- Implement version validation before command execution
- Add WritePluginManifest and SavePluginCtlConfig helper functions
- Add comprehensive tests for version comparison logic
- Skip validation for 'version' command and when no plugin.json exists

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Felipe M 2025-07-31 11:46:42 +02:00
parent 9d3c6b357f
commit 353cc9efc7
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
5 changed files with 286 additions and 51 deletions

143
plugin.go
View file

@ -1,143 +0,0 @@
package pluginctl
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/mattermost/mattermost/server/public/model"
)
const PluginManifestName = "plugin.json"
// PluginCtlConfig represents the configuration for pluginctl stored in the manifest props.
type PluginCtlConfig struct {
IgnoreAssets []string `json:"ignore_assets,omitempty"`
}
// LoadPluginManifest loads and parses the plugin.json file from the current directory.
func LoadPluginManifest() (*model.Manifest, error) {
return LoadPluginManifestFromPath(".")
}
// LoadPluginManifestFromPath loads and parses the plugin.json file from the specified directory.
func LoadPluginManifestFromPath(dir string) (*model.Manifest, error) {
manifestPath := filepath.Join(dir, PluginManifestName)
// Check if plugin.json exists
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
return nil, fmt.Errorf("plugin.json not found in directory %s", dir)
}
// Read the file
data, err := os.ReadFile(manifestPath)
if err != nil {
return nil, fmt.Errorf("failed to read plugin.json: %w", err)
}
// Parse JSON into Manifest struct
var manifest model.Manifest
if err := json.Unmarshal(data, &manifest); err != nil {
return nil, fmt.Errorf("failed to parse plugin.json: %w", err)
}
return &manifest, nil
}
// HasServerCode checks if the plugin contains server-side code.
func HasServerCode(manifest *model.Manifest) bool {
return manifest.Server != nil && len(manifest.Server.Executables) > 0
}
// HasWebappCode checks if the plugin contains webapp code.
func HasWebappCode(manifest *model.Manifest) bool {
return manifest.Webapp != nil && manifest.Webapp.BundlePath != ""
}
// IsValidPluginDirectory checks if the current directory contains a valid plugin.
func IsValidPluginDirectory() error {
_, err := LoadPluginManifest()
return err
}
// GetEffectivePluginPath determines the plugin path from flag, environment variable, or current directory.
func GetEffectivePluginPath(flagPath string) string {
const EnvPluginPath = "PLUGINCTL_PLUGIN_PATH"
// Priority: 1. Command line flag, 2. Environment variable, 3. Current directory
if flagPath != "" {
return flagPath
}
if envPath := os.Getenv(EnvPluginPath); envPath != "" {
return envPath
}
// Default to current directory
cwd, err := os.Getwd()
if err != nil {
return "."
}
return cwd
}
// ParsePluginCtlConfig extracts and parses the pluginctl configuration from the manifest props.
func ParsePluginCtlConfig(manifest *model.Manifest) (*PluginCtlConfig, error) {
// Default configuration
config := &PluginCtlConfig{
IgnoreAssets: []string{},
}
// Check if props exist
if manifest.Props == nil {
return config, nil
}
// Check if pluginctl config exists in props
pluginctlData, exists := manifest.Props["pluginctl"]
if !exists {
return config, nil
}
// Convert to JSON and parse
jsonData, err := json.Marshal(pluginctlData)
if err != nil {
return nil, fmt.Errorf("failed to marshal pluginctl config: %w", err)
}
if err := json.Unmarshal(jsonData, config); err != nil {
return nil, fmt.Errorf("failed to parse pluginctl config: %w", err)
}
return config, nil
}
const (
HelpFlagLong = "--help"
HelpFlagShort = "-h"
)
// CheckForHelpFlag checks if --help is in the arguments and shows help if found.
// Returns true if help was shown, false otherwise.
func CheckForHelpFlag(args []string, helpText string) bool {
for _, arg := range args {
if arg == HelpFlagLong || arg == HelpFlagShort {
Logger.Info(helpText)
return true
}
}
return false
}
// ShowErrorWithHelp displays an error message followed by command help.
func ShowErrorWithHelp(errorMsg, helpText string) error {
Logger.Error(errorMsg)
Logger.Info(helpText)
return fmt.Errorf("%s", errorMsg)
}