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; ` // 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 [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 }