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>
237 lines
6.8 KiB
Go
237 lines
6.8 KiB
Go
package pluginctl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"text/template"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
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.
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
var manifest *model.Manifest
|
|
|
|
const manifestStr = ` + "`%s`" + `
|
|
|
|
func init() {
|
|
_ = json.NewDecoder(strings.NewReader(manifestStr)).Decode(&manifest)
|
|
}
|
|
`
|
|
|
|
const pluginIDJSFileTemplate = `// This file is automatically generated. Do not modify it manually.
|
|
|
|
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)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal manifest: %w", err)
|
|
}
|
|
manifestStr := string(manifestBytes)
|
|
|
|
// Generate server manifest file if server exists
|
|
if HasServerCode(manifest) {
|
|
serverDir := filepath.Join(pluginPath, "server")
|
|
if err := os.MkdirAll(serverDir, manifestDirPerm); err != nil {
|
|
return fmt.Errorf("failed to create server directory: %w", err)
|
|
}
|
|
|
|
serverManifestPath := filepath.Join(serverDir, "manifest.go")
|
|
serverContent := fmt.Sprintf(pluginIDGoFileTemplate, manifestStr)
|
|
|
|
if err := os.WriteFile(serverManifestPath, []byte(serverContent), manifestFilePerm); err != nil {
|
|
return fmt.Errorf("failed to write server manifest: %w", err)
|
|
}
|
|
|
|
Logger.Info("Generated server manifest", "path", serverManifestPath)
|
|
}
|
|
|
|
// Generate webapp manifest file if webapp exists
|
|
if HasWebappCode(manifest) {
|
|
webappDir := filepath.Join(pluginPath, "webapp", "src")
|
|
if err := os.MkdirAll(webappDir, manifestDirPerm); err != nil {
|
|
return fmt.Errorf("failed to create webapp directory: %w", err)
|
|
}
|
|
|
|
webappManifestPath := filepath.Join(webappDir, "manifest.ts")
|
|
webappContent := fmt.Sprintf(pluginIDJSFileTemplate, manifestStr)
|
|
|
|
if err := os.WriteFile(webappManifestPath, []byte(webappContent), manifestFilePerm); err != nil {
|
|
return fmt.Errorf("failed to write webapp manifest: %w", err)
|
|
}
|
|
|
|
Logger.Info("Generated webapp manifest", "path", webappManifestPath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RunManifestCommand implements the 'manifest' command functionality with subcommands.
|
|
func RunManifestCommand(args []string, pluginPath string) error {
|
|
helpText := `Manage plugin manifest files
|
|
|
|
Usage:
|
|
pluginctl manifest <subcommand> [options]
|
|
|
|
Subcommands:
|
|
get TEMPLATE Get manifest field using template syntax (e.g., {{.id}}, {{.version}})
|
|
apply Generate manifest files for server/webapp
|
|
check Validate plugin manifest
|
|
|
|
Options:
|
|
--help, -h Show this help message
|
|
|
|
Examples:
|
|
pluginctl manifest get '{{.Id}}' # Get plugin ID
|
|
pluginctl manifest get '{{.Version}}' # Get plugin version
|
|
pluginctl manifest apply # Generate server/webapp manifest files
|
|
pluginctl manifest check # Validate manifest`
|
|
|
|
// Check for help flag
|
|
if CheckForHelpFlag(args, helpText) {
|
|
return nil
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
return ShowErrorWithHelp("manifest command requires a subcommand", helpText)
|
|
}
|
|
|
|
// Convert to absolute path
|
|
absPath, err := filepath.Abs(pluginPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve path: %w", err)
|
|
}
|
|
|
|
// Load plugin manifest
|
|
manifest, err := LoadPluginManifestFromPath(absPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
|
}
|
|
|
|
subcommand := args[0]
|
|
switch subcommand {
|
|
case "get":
|
|
if len(args) < 2 {
|
|
return ShowErrorWithHelp("get subcommand requires a template expression", helpText)
|
|
}
|
|
templateStr := args[1]
|
|
|
|
// Parse and execute template with manifest as context
|
|
tmpl, err := template.New("manifest").Parse(templateStr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse template: %w", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, *manifest); err != nil {
|
|
return fmt.Errorf("failed to execute template: %w", err)
|
|
}
|
|
|
|
fmt.Print(buf.String())
|
|
case "apply":
|
|
if err := applyManifest(manifest, absPath); err != nil {
|
|
return fmt.Errorf("failed to apply manifest: %w", err)
|
|
}
|
|
case "check":
|
|
if err := manifest.IsValid(); err != nil {
|
|
Logger.Error("Plugin manifest validation failed", "error", err)
|
|
|
|
return err
|
|
}
|
|
Logger.Info("Plugin manifest is valid")
|
|
default:
|
|
return ShowErrorWithHelp(fmt.Sprintf("unknown subcommand: %s", subcommand), helpText)
|
|
}
|
|
|
|
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
|
|
}
|