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

View file

@ -15,6 +15,9 @@ const (
// File permissions for generated directories and files.
manifestDirPerm = 0o750
manifestFilePerm = 0o600
// Name of the plugin manifest file.
PluginManifestName = "plugin.json"
)
const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually.
@ -44,6 +47,51 @@ const manifest = JSON.parse(` + "`%s`" + `);
export default manifest;
`
// PluginCtlConfig represents the configuration for pluginctl stored in the manifest props.
type PluginCtlConfig struct {
Version string `json:"version,omitempty"`
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 != ""
}
// applyManifest generates manifest files for server and webapp components.
func applyManifest(manifest *model.Manifest, pluginPath string) error {
manifestBytes, err := json.Marshal(manifest)
@ -168,3 +216,22 @@ Examples:
return nil
}
// WritePluginManifest saves the manifest to plugin.json in the specified path.
func WritePluginManifest(manifest *model.Manifest, path string) error {
manifestPath := filepath.Join(path, PluginManifestName)
// Marshal manifest to JSON with proper indentation
data, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal manifest: %w", err)
}
// Write to file
const fileMode = 0600
if err := os.WriteFile(manifestPath, data, fileMode); err != nil {
return fmt.Errorf("failed to write plugin.json: %w", err)
}
return nil
}