Add create-plugin command and improve asset management
- Add new create-plugin command to generate plugins from starter template - Add manifest name subcommand for template context - Replace hardcoded asset paths with explicit file patterns in embed directive - Refactor asset build system to use _setup.mk instead of setup.mk - Update versioning to use pluginctl manifest id for APP_NAME - Clean up Makefile by removing redundant bundle name definition 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d7ac783efc
commit
07f21c6812
7 changed files with 602 additions and 11 deletions
|
@ -26,13 +26,6 @@ default: all
|
||||||
# Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed.
|
# Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed.
|
||||||
include build/setup.mk
|
include build/setup.mk
|
||||||
|
|
||||||
BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
|
|
||||||
|
|
||||||
# Include custom makefile, if present
|
|
||||||
ifneq ($(wildcard build/custom.mk),)
|
|
||||||
include build/custom.mk
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifneq ($(MM_DEBUG),)
|
ifneq ($(MM_DEBUG),)
|
||||||
GO_BUILD_GCFLAGS = -gcflags "all=-N -l"
|
GO_BUILD_GCFLAGS = -gcflags "all=-N -l"
|
||||||
else
|
else
|
||||||
|
@ -40,4 +33,4 @@ else
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Include modular makefiles
|
# Include modular makefiles
|
||||||
include build/*.mk
|
include build/*.mk
|
||||||
|
|
|
@ -42,3 +42,5 @@ ifeq ($(NPM),)
|
||||||
$(error "npm is not available: see https://www.npmjs.com/get-npm")
|
$(error "npm is not available: see https://www.npmjs.com/get-npm")
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
# Used for semver bumping
|
# Used for semver bumping
|
||||||
PROTECTED_BRANCH := master
|
PROTECTED_BRANCH := master
|
||||||
APP_NAME := $(shell basename -s .git `git config --get remote.origin.url`)
|
APP_NAME := $(shell pluginctl manifest id)
|
||||||
CURRENT_VERSION := $(shell git describe --abbrev=0 --tags)
|
CURRENT_VERSION := $(shell git describe --abbrev=0 --tags)
|
||||||
VERSION_PARTS := $(subst ., ,$(subst v,,$(subst -rc, ,$(CURRENT_VERSION))))
|
VERSION_PARTS := $(subst ., ,$(subst v,,$(subst -rc, ,$(CURRENT_VERSION))))
|
||||||
MAJOR := $(word 1,$(VERSION_PARTS))
|
MAJOR := $(word 1,$(VERSION_PARTS))
|
||||||
|
@ -108,4 +108,4 @@ major-rc: ## to bump major release candidate version (semver)
|
||||||
@echo Bumping $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
@echo Bumping $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC) -m "Bumping $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)"
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC) -m "Bumping $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)"
|
||||||
git push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
@echo Bumped $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
@echo Bumped $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
|
|
@ -66,6 +66,8 @@ func runCommand(command string, args []string, pluginPath string) error {
|
||||||
return nil
|
return nil
|
||||||
case "version":
|
case "version":
|
||||||
return runVersionCommand(args)
|
return runVersionCommand(args)
|
||||||
|
case "create-plugin":
|
||||||
|
return runCreatePluginCommand(args, pluginPath)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown command: %s", command)
|
return fmt.Errorf("unknown command: %s", command)
|
||||||
}
|
}
|
||||||
|
@ -109,6 +111,10 @@ func runDeployCommand(args []string, pluginPath string) error {
|
||||||
return pluginctl.RunDeployCommand(args, pluginPath)
|
return pluginctl.RunDeployCommand(args, pluginPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runCreatePluginCommand(args []string, pluginPath string) error {
|
||||||
|
return pluginctl.RunCreatePluginCommand(args, pluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
func showUsage() {
|
func showUsage() {
|
||||||
usageText := `pluginctl - Mattermost Plugin Development CLI
|
usageText := `pluginctl - Mattermost Plugin Development CLI
|
||||||
|
|
||||||
|
@ -127,6 +133,7 @@ Commands:
|
||||||
updateassets Update plugin files from embedded assets
|
updateassets Update plugin files from embedded assets
|
||||||
manifest Get plugin manifest information (id, version, has_server, has_webapp, check)
|
manifest Get plugin manifest information (id, version, has_server, has_webapp, check)
|
||||||
logs View plugin logs (use --watch to follow logs in real-time)
|
logs View plugin logs (use --watch to follow logs in real-time)
|
||||||
|
create-plugin Create a new plugin from the starter template
|
||||||
help Show this help message
|
help Show this help message
|
||||||
version Show version information
|
version Show version information
|
||||||
|
|
||||||
|
@ -146,6 +153,7 @@ Examples:
|
||||||
pluginctl manifest check # Validate plugin manifest
|
pluginctl manifest check # Validate plugin manifest
|
||||||
pluginctl logs # View recent plugin logs
|
pluginctl logs # View recent plugin logs
|
||||||
pluginctl logs --watch # Watch plugin logs in real-time
|
pluginctl logs --watch # Watch plugin logs in real-time
|
||||||
|
pluginctl create-plugin # Create a new plugin from the starter template
|
||||||
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
||||||
pluginctl info # Show info using environment variable
|
pluginctl info # Show info using environment variable
|
||||||
pluginctl version # Show version information
|
pluginctl version # Show version information
|
||||||
|
|
586
create-plugin.go
Normal file
586
create-plugin.go
Normal file
|
@ -0,0 +1,586 @@
|
||||||
|
package pluginctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
starterTemplateURL = "https://github.com/mattermost/mattermost-plugin-starter-template.git"
|
||||||
|
starterTemplateWebURL = "https://github.com/mattermost/mattermost-plugin-starter-template"
|
||||||
|
pluginPrefix = "mattermost-plugin-"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
titleStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("205")).
|
||||||
|
Bold(true).
|
||||||
|
Margin(1, 0)
|
||||||
|
|
||||||
|
promptStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("36")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
inputStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("212")).
|
||||||
|
Background(lipgloss.Color("235")).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
errorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("196")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
successStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("46")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
infoStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("39"))
|
||||||
|
)
|
||||||
|
|
||||||
|
type inputModel struct {
|
||||||
|
input string
|
||||||
|
cursor int
|
||||||
|
placeholder string
|
||||||
|
prompt string
|
||||||
|
validation func(string) string
|
||||||
|
submitted bool
|
||||||
|
fixedPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
keyMsg, ok := msg.(tea.KeyMsg)
|
||||||
|
if !ok {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.handleKeyMsg(keyMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
//nolint:exhaustive
|
||||||
|
switch msg.Type {
|
||||||
|
case tea.KeyEsc, tea.KeyCtrlC:
|
||||||
|
return m, tea.Quit
|
||||||
|
case tea.KeyEnter:
|
||||||
|
if validationErr := m.validation(m.input); validationErr == "" {
|
||||||
|
m.submitted = true
|
||||||
|
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
case tea.KeyLeft:
|
||||||
|
m.handleCursorLeft()
|
||||||
|
case tea.KeyRight:
|
||||||
|
m.handleCursorRight()
|
||||||
|
case tea.KeyBackspace:
|
||||||
|
m.handleBackspace()
|
||||||
|
case tea.KeyDelete:
|
||||||
|
m.handleDelete()
|
||||||
|
case tea.KeyHome:
|
||||||
|
m.cursor = 0
|
||||||
|
case tea.KeyEnd:
|
||||||
|
m.cursor = len(m.input)
|
||||||
|
case tea.KeyRunes:
|
||||||
|
m.handleRunes(msg.Runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) handleCursorLeft() {
|
||||||
|
if m.cursor > 0 {
|
||||||
|
m.cursor--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) handleCursorRight() {
|
||||||
|
if m.cursor < len(m.input) {
|
||||||
|
m.cursor++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) handleBackspace() {
|
||||||
|
if m.cursor > 0 {
|
||||||
|
m.input = m.input[:m.cursor-1] + m.input[m.cursor:]
|
||||||
|
m.cursor--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) handleDelete() {
|
||||||
|
if m.cursor < len(m.input) {
|
||||||
|
m.input = m.input[:m.cursor] + m.input[m.cursor+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) handleRunes(runes []rune) {
|
||||||
|
m.input = m.input[:m.cursor] + string(runes) + m.input[m.cursor:]
|
||||||
|
m.cursor += len(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inputModel) View() string {
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
s.WriteString(titleStyle.Render("Create Mattermost Plugin"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
|
s.WriteString(promptStyle.Render(m.prompt))
|
||||||
|
s.WriteString("\n")
|
||||||
|
|
||||||
|
// Show input field with fixed prefix
|
||||||
|
displayValue := m.fixedPrefix + m.input
|
||||||
|
if m.input == "" {
|
||||||
|
displayValue = m.fixedPrefix + m.placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add cursor (adjust position for fixed prefix)
|
||||||
|
cursorPos := len(m.fixedPrefix) + m.cursor
|
||||||
|
if cursorPos <= len(displayValue) {
|
||||||
|
left := displayValue[:cursorPos]
|
||||||
|
right := displayValue[cursorPos:]
|
||||||
|
if cursorPos == len(displayValue) {
|
||||||
|
displayValue = left + "█"
|
||||||
|
} else {
|
||||||
|
displayValue = left + "█" + right[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString(inputStyle.Render(displayValue))
|
||||||
|
s.WriteString("\n")
|
||||||
|
|
||||||
|
// Show validation message
|
||||||
|
if m.input != "" {
|
||||||
|
if validationErr := m.validation(m.input); validationErr != "" {
|
||||||
|
s.WriteString(errorStyle.Render("✗ " + validationErr))
|
||||||
|
} else {
|
||||||
|
fullName := m.fixedPrefix + m.input
|
||||||
|
s.WriteString(successStyle.Render("✓ Valid plugin name: " + fullName))
|
||||||
|
}
|
||||||
|
s.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString(infoStyle.Render("\nPress Enter to continue, Ctrl+C to cancel"))
|
||||||
|
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePluginSuffix(suffix string) string {
|
||||||
|
if suffix == "" {
|
||||||
|
return "Plugin name cannot be empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid Go module name (simplified)
|
||||||
|
validName := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||||
|
if !validName.MatchString(suffix) {
|
||||||
|
return "Plugin name contains invalid characters"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for reasonable length
|
||||||
|
if len(suffix) < 2 {
|
||||||
|
return "Plugin name must be at least 2 characters long"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateModuleName(name string) string {
|
||||||
|
if name == "" {
|
||||||
|
return "Module name cannot be empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid Go module name
|
||||||
|
validModule := regexp.MustCompile(`^[a-zA-Z0-9._/-]+$`)
|
||||||
|
if !validModule.MatchString(name) {
|
||||||
|
return "Module name contains invalid characters"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should look like a valid repository path
|
||||||
|
if !strings.Contains(name, "/") {
|
||||||
|
return "Module name should be a repository path (e.g., github.com/user/repo)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTitleCase(s string) string {
|
||||||
|
words := strings.Fields(s)
|
||||||
|
for i, word := range words {
|
||||||
|
if word != "" {
|
||||||
|
words[i] = strings.ToUpper(word[:1]) + strings.ToLower(word[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(words, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptForPluginName() (string, error) {
|
||||||
|
model := &inputModel{
|
||||||
|
prompt: "Enter plugin name:",
|
||||||
|
placeholder: "example",
|
||||||
|
validation: validatePluginSuffix,
|
||||||
|
fixedPrefix: pluginPrefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
p := tea.NewProgram(model)
|
||||||
|
finalModel, err := p.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to run input prompt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := finalModel.(*inputModel)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected model type")
|
||||||
|
}
|
||||||
|
if !result.submitted {
|
||||||
|
return "", fmt.Errorf("operation canceled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the full plugin name (prefix + suffix)
|
||||||
|
return result.fixedPrefix + result.input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptForModuleName(pluginName string) (string, error) {
|
||||||
|
// Generate a sensible default based on the plugin name
|
||||||
|
defaultModule := fmt.Sprintf("github.com/user/%s", pluginName)
|
||||||
|
model := &inputModel{
|
||||||
|
prompt: "Enter Go module name (repository path):",
|
||||||
|
placeholder: defaultModule,
|
||||||
|
validation: validateModuleName,
|
||||||
|
}
|
||||||
|
|
||||||
|
p := tea.NewProgram(model)
|
||||||
|
finalModel, err := p.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to run input prompt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := finalModel.(*inputModel)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected model type")
|
||||||
|
}
|
||||||
|
if !result.submitted {
|
||||||
|
return "", fmt.Errorf("operation canceled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunCreatePluginCommand(args []string, pluginPath string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return fmt.Errorf("create-plugin command does not accept arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Starting plugin creation process")
|
||||||
|
|
||||||
|
// Prompt for plugin name
|
||||||
|
pluginName, err := promptForPluginName()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get plugin name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for module name
|
||||||
|
moduleName, err := promptForModuleName(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get module name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Creating plugin", "name", pluginName, "module", moduleName)
|
||||||
|
|
||||||
|
// Check if directory already exists
|
||||||
|
if _, err := os.Stat(pluginName); err == nil {
|
||||||
|
return fmt.Errorf("directory '%s' already exists", pluginName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the starter template
|
||||||
|
Logger.Info("Cloning starter template repository")
|
||||||
|
if err := cloneStarterTemplate(pluginName); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone starter template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the template
|
||||||
|
Logger.Info("Processing template files")
|
||||||
|
if err := processPluginTemplate(pluginName, moduleName); err != nil {
|
||||||
|
return fmt.Errorf("failed to process template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update assets
|
||||||
|
Logger.Info("Updating plugin assets")
|
||||||
|
if err := RunUpdateAssetsCommand([]string{}, pluginName); err != nil {
|
||||||
|
return fmt.Errorf("failed to update assets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run go mod tidy
|
||||||
|
Logger.Info("Running go mod tidy")
|
||||||
|
if err := runGoModTidy(pluginName); err != nil {
|
||||||
|
return fmt.Errorf("failed to run go mod tidy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize git and create initial commit
|
||||||
|
Logger.Info("Initializing git repository")
|
||||||
|
if err := initializeGitRepo(pluginName); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize git repository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Plugin created successfully!", "name", pluginName, "path", pluginName)
|
||||||
|
fmt.Printf("\n%s\n", successStyle.Render("✓ Plugin created successfully!"))
|
||||||
|
fmt.Printf("%s\n", infoStyle.Render(fmt.Sprintf("Plugin '%s' has been created in directory: %s",
|
||||||
|
pluginName, pluginName)))
|
||||||
|
fmt.Printf("%s\n", infoStyle.Render("Next steps:"))
|
||||||
|
fmt.Printf("%s\n", infoStyle.Render(" 1. cd "+pluginName))
|
||||||
|
fmt.Printf("%s\n", infoStyle.Render(" 2. Review and modify the plugin.json file"))
|
||||||
|
fmt.Printf("%s\n", infoStyle.Render(" 3. Start developing your plugin!"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneStarterTemplate(pluginName string) error {
|
||||||
|
cmd := exec.Command("git", "clone", "--depth", "1", starterTemplateURL, pluginName)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("git clone failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the .git directory from the template
|
||||||
|
gitDir := filepath.Join(pluginName, ".git")
|
||||||
|
if err := os.RemoveAll(gitDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove .git directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processPluginTemplate(pluginName, moduleName string) error {
|
||||||
|
// Update go.mod
|
||||||
|
if err := updateGoMod(pluginName, moduleName); err != nil {
|
||||||
|
return fmt.Errorf("failed to update go.mod: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update plugin.json
|
||||||
|
if err := updatePluginJSON(pluginName, moduleName); err != nil {
|
||||||
|
return fmt.Errorf("failed to update plugin.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Go code references
|
||||||
|
if err := updateGoCodeReferences(pluginName, moduleName); err != nil {
|
||||||
|
return fmt.Errorf("failed to update Go code references: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update README.md
|
||||||
|
if err := updateReadme(pluginName, moduleName); err != nil {
|
||||||
|
return fmt.Errorf("failed to update README.md: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hello.html
|
||||||
|
if err := updateHelloHTML(pluginName); err != nil {
|
||||||
|
return fmt.Errorf("failed to update hello.html: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateGoMod(pluginName, moduleName string) error {
|
||||||
|
goModPath := filepath.Join(pluginName, "go.mod")
|
||||||
|
content, err := os.ReadFile(goModPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read go.mod: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the module name
|
||||||
|
updated := strings.Replace(string(content),
|
||||||
|
"github.com/mattermost/mattermost-plugin-starter-template", moduleName, 1)
|
||||||
|
|
||||||
|
if err := os.WriteFile(goModPath, []byte(updated), filePermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to write go.mod: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePluginJSON(pluginName, moduleName string) error {
|
||||||
|
pluginJSONPath := filepath.Join(pluginName, "plugin.json")
|
||||||
|
|
||||||
|
// Load the manifest using the existing function
|
||||||
|
manifest, err := LoadPluginManifestFromPath(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the plugin ID (remove the prefix for the ID)
|
||||||
|
pluginID := strings.TrimPrefix(pluginName, pluginPrefix)
|
||||||
|
manifest.Id = "com.mattermost." + pluginID
|
||||||
|
|
||||||
|
// Update display name
|
||||||
|
displayName := strings.ReplaceAll(pluginID, "-", " ")
|
||||||
|
displayName = toTitleCase(displayName)
|
||||||
|
manifest.Name = displayName
|
||||||
|
|
||||||
|
// Update homepage_url and support_url if module is a GitHub repository
|
||||||
|
if strings.HasPrefix(moduleName, "github.com/") {
|
||||||
|
newURL := "https://" + moduleName
|
||||||
|
manifest.HomepageURL = newURL
|
||||||
|
manifest.SupportURL = newURL + "/issues"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update icon path
|
||||||
|
if manifest.IconPath == "assets/starter-template-icon.svg" {
|
||||||
|
manifest.IconPath = "assets/" + pluginID + "-icon.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the updated manifest back to JSON
|
||||||
|
manifestJSON, err := json.MarshalIndent(manifest, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal plugin manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(pluginJSONPath, manifestJSON, filePermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to write plugin.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateGoCodeReferences(pluginName, moduleName string) error {
|
||||||
|
// Walk through Go files and update import references
|
||||||
|
return filepath.Walk(pluginName, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(path, ".go") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace import references
|
||||||
|
updated := strings.Replace(string(content),
|
||||||
|
"github.com/mattermost/mattermost-plugin-starter-template", moduleName, -1)
|
||||||
|
|
||||||
|
// Update plugin ID in comments (like api.go)
|
||||||
|
pluginID := strings.TrimPrefix(pluginName, pluginPrefix)
|
||||||
|
updated = strings.Replace(updated, "com.mattermost.plugin-starter-template",
|
||||||
|
"com.mattermost."+pluginID, -1)
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, []byte(updated), filePermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGoModTidy(pluginName string) error {
|
||||||
|
cmd := exec.Command("go", "mod", "tidy")
|
||||||
|
cmd.Dir = pluginName
|
||||||
|
cmd.Stdout = io.Discard
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("go mod tidy failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeGitRepo(pluginName string) error {
|
||||||
|
// Initialize git repo
|
||||||
|
cmd := exec.Command("git", "init")
|
||||||
|
cmd.Dir = pluginName
|
||||||
|
cmd.Stdout = io.Discard
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("git init failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all files
|
||||||
|
cmd = exec.Command("git", "add", ".")
|
||||||
|
cmd.Dir = pluginName
|
||||||
|
cmd.Stdout = io.Discard
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("git add failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create initial commit
|
||||||
|
cmd = exec.Command("git", "commit", "-m", "Initial commit from mattermost-plugin-starter-template")
|
||||||
|
cmd.Dir = pluginName
|
||||||
|
cmd.Stdout = io.Discard
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("git commit failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateReadme(pluginName, moduleName string) error {
|
||||||
|
readmePath := filepath.Join(pluginName, "README.md")
|
||||||
|
content, err := os.ReadFile(readmePath)
|
||||||
|
if err != nil {
|
||||||
|
// README.md might not exist, which is fine
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to read README.md: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := string(content)
|
||||||
|
|
||||||
|
// Update all GitHub URLs to point to the new repository
|
||||||
|
if strings.HasPrefix(moduleName, "github.com/") {
|
||||||
|
newURL := "https://" + moduleName
|
||||||
|
updated = strings.Replace(updated, starterTemplateWebURL, newURL, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update plugin ID in comments and examples
|
||||||
|
pluginID := strings.TrimPrefix(pluginName, pluginPrefix)
|
||||||
|
updated = strings.Replace(updated, "com.mattermost.plugin-starter-template",
|
||||||
|
"com.mattermost."+pluginID, -1)
|
||||||
|
|
||||||
|
// Update clone command example
|
||||||
|
updated = strings.Replace(updated, "com.example.my-plugin", "com.example."+pluginID, -1)
|
||||||
|
|
||||||
|
if err := os.WriteFile(readmePath, []byte(updated), filePermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to write README.md: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateHelloHTML(pluginName string) error {
|
||||||
|
helloPath := filepath.Join(pluginName, "public", "hello.html")
|
||||||
|
content, err := os.ReadFile(helloPath)
|
||||||
|
if err != nil {
|
||||||
|
// hello.html might not exist, which is fine
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to read hello.html: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginID := strings.TrimPrefix(pluginName, pluginPrefix)
|
||||||
|
updated := strings.Replace(string(content), "com.mattermost.plugin-starter-template",
|
||||||
|
"com.mattermost."+pluginID, -1)
|
||||||
|
|
||||||
|
if err := os.WriteFile(helloPath, []byte(updated), filePermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to write hello.html: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -27,6 +27,8 @@ func RunManifestCommand(args []string, pluginPath string) error {
|
||||||
switch subcommand {
|
switch subcommand {
|
||||||
case "id":
|
case "id":
|
||||||
fmt.Println(manifest.Id)
|
fmt.Println(manifest.Id)
|
||||||
|
case "name":
|
||||||
|
fmt.Println(manifest.Name)
|
||||||
case "version":
|
case "version":
|
||||||
fmt.Println(manifest.Version)
|
fmt.Println(manifest.Version)
|
||||||
case "has_server":
|
case "has_server":
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets/*
|
//go:embed assets/.editorconfig assets/.gitattributes assets/.nvmrc assets/Makefile assets/*.yml assets/build/*.mk assets/.github/**/*.yml assets/webapp/.npmrc assets/webapp/*.config.js
|
||||||
var assetsFS embed.FS
|
var assetsFS embed.FS
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue