Compare commits
10 commits
b43e7ac3ec
...
03c521f237
Author | SHA1 | Date | |
---|---|---|---|
03c521f237 | |||
278958d1e4 | |||
6639dad2d6 | |||
873bf78c22 | |||
4d9c958fc9 | |||
71a7b0de11 | |||
c01c9c2843 | |||
3cbe5e4a9f | |||
4f0b94354c | |||
73149001eb |
36 changed files with 1501 additions and 566 deletions
287
CLAUDE.md
287
CLAUDE.md
|
@ -1,261 +1,58 @@
|
||||||
# pluginctl - Mattermost Plugin Development CLI
|
# pluginctl - Claude Memory
|
||||||
|
|
||||||
## Project Overview
|
## Critical Architecture Rules
|
||||||
`pluginctl` is a command-line interface tool for Mattermost plugin development. It provides utilities to manage, inspect, and work with Mattermost plugins from the command line.
|
|
||||||
|
|
||||||
## Architecture Guidelines
|
### Command Structure
|
||||||
|
|
||||||
### Project Structure
|
- **CRITICAL**: ALL command logic in separate files in ROOT folder (e.g., `info.go`, `enable.go`)
|
||||||
```
|
- **NEVER** put command implementation in `cmd/pluginctl/main.go` - only CLI framework code
|
||||||
pluginctl/
|
- **Command pattern**: `run[Command]Command(args []string, pluginPath string) error`
|
||||||
├── cmd/pluginctl/main.go # CLI entrypoint with command routing
|
- **Registration**: Add to `runCommand()` switch in main.go
|
||||||
├── plugin.go # Plugin manifest handling utilities
|
|
||||||
├── info.go # Info command implementation
|
|
||||||
├── [command].go # Additional command implementations
|
|
||||||
├── go.mod # Go module definition
|
|
||||||
├── go.sum # Go module dependencies
|
|
||||||
├── pluginctl # Built binary (gitignored)
|
|
||||||
└── CLAUDE.md # This architecture document
|
|
||||||
```
|
|
||||||
|
|
||||||
### Design Principles
|
### Dependencies & Types
|
||||||
|
|
||||||
#### 1. **Separation of Concerns**
|
- Use `github.com/mattermost/mattermost/server/public/model.Manifest`
|
||||||
- **CLI Framework**: `cmd/pluginctl/main.go` handles argument parsing, command routing, and error handling
|
- Commands in `pluginctl` package, main.go calls them
|
||||||
- **Command Implementation**: Each command gets its own file (e.g., `info.go`, `build.go`, `deploy.go`) **IN THE ROOT FOLDER, NOT IN cmd/pluginctl/**
|
|
||||||
- **Utility Functions**: Common plugin operations in `plugin.go`
|
|
||||||
|
|
||||||
**CRITICAL ARCHITECTURE RULE**: ALL COMMAND LOGIC MUST BE IN SEPARATE FILES IN THE ROOT FOLDER. The cmd/pluginctl/main.go file should ONLY contain CLI framework code (argument parsing, command routing, wrapper functions). Never put command implementation logic directly in main.go.
|
|
||||||
|
|
||||||
#### 2. **Plugin Manifest Handling**
|
|
||||||
- **Always use official Mattermost types**: Import `github.com/mattermost/mattermost/server/public/model` and use `model.Manifest`
|
|
||||||
- **Validation**: Always validate plugin.json existence and format before operations
|
|
||||||
- **Path handling**: Support both current directory and custom path operations
|
|
||||||
|
|
||||||
#### 3. **Command Structure**
|
|
||||||
- **Main command router**: Add new commands to the `runCommand()` function in `cmd/pluginctl/main.go`
|
|
||||||
- **Command functions**: Name pattern: `run[Command]Command(args []string, pluginPath string) error`
|
|
||||||
- **Error handling**: Return descriptive errors, let main.go handle exit codes
|
|
||||||
- **Command implementation**: Each command's logic goes in a separate file in the root folder (e.g., `enable.go`, `disable.go`, `reset.go`)
|
|
||||||
- **Command wrapper functions**: The main.go file contains simple wrapper functions that call the actual command implementations
|
|
||||||
|
|
||||||
#### 4. **Code Organization**
|
|
||||||
- **No inline implementations**: Keep command logic in separate files
|
|
||||||
- **Reusable utilities**: Common operations go in `plugin.go`
|
|
||||||
- **Self-contained**: Each command file should be importable and testable
|
|
||||||
|
|
||||||
### Current Commands
|
|
||||||
|
|
||||||
#### `info`
|
|
||||||
- **Purpose**: Display plugin manifest information
|
|
||||||
- **Implementation**: `info.go`
|
|
||||||
- **Usage**: `pluginctl info`
|
|
||||||
- **Features**:
|
|
||||||
- Shows plugin ID, name, version
|
|
||||||
- Displays minimum Mattermost version
|
|
||||||
- Indicates server/webapp code presence
|
|
||||||
- Shows settings schema availability
|
|
||||||
- **Path Resolution**: Uses global path logic (--plugin-path flag, environment variable, or current directory)
|
|
||||||
|
|
||||||
### Adding New Commands
|
|
||||||
|
|
||||||
#### Step 1: Create Command File
|
|
||||||
Create a new file named `[command].go` with the command implementation:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func run[Command]Command(args []string, pluginPath string) error {
|
|
||||||
// Command implementation here
|
|
||||||
// Use pluginPath to load plugin manifest
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Register in Main Router
|
|
||||||
Add the command to the `runCommand()` function in `cmd/pluginctl/main.go`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func runCommand(command string, args []string, pluginPath string) error {
|
|
||||||
switch command {
|
|
||||||
case "info":
|
|
||||||
return runInfoCommand(args, pluginPath)
|
|
||||||
case "new-command":
|
|
||||||
return runNewCommandCommand(args, pluginPath)
|
|
||||||
// ... other commands
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 3: Update Help Text
|
|
||||||
Add the command to the `showUsage()` function in `main.go`.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
#### Core Dependencies
|
|
||||||
- `github.com/mattermost/mattermost/server/public/model` - Official Mattermost plugin types
|
|
||||||
- Standard Go library for CLI operations
|
|
||||||
|
|
||||||
#### Dependency Management
|
|
||||||
- Use `go mod tidy` to manage dependencies
|
|
||||||
- Prefer standard library over external packages when possible
|
|
||||||
- Only add dependencies that provide significant value
|
|
||||||
|
|
||||||
### Build System and Development Tools
|
|
||||||
|
|
||||||
#### Tool Versions
|
|
||||||
The project uses pinned versions for reproducible builds:
|
|
||||||
- **golangci-lint**: v1.62.2
|
|
||||||
- **goreleaser**: v2.6.2
|
|
||||||
- **gosec**: v2.22.0
|
|
||||||
- **Go**: 1.24.3
|
|
||||||
|
|
||||||
#### Makefile Targets
|
|
||||||
|
|
||||||
**Development Workflow:**
|
|
||||||
- `make dev` - Quick development build (fmt, lint, build)
|
|
||||||
- `make check-changes` - Check changes (lint, security, test)
|
|
||||||
- `make verify` - Full verification (clean, lint, test, build)
|
|
||||||
|
|
||||||
**Building:**
|
|
||||||
- `make build` - Build binary for current platform
|
|
||||||
- `make build-all` - Build for all supported platforms
|
|
||||||
- `make install` - Install binary to GOPATH/bin
|
|
||||||
|
|
||||||
**Testing and Quality:**
|
|
||||||
- `make test` - Run tests
|
|
||||||
- `make test-coverage` - Run tests with coverage report
|
|
||||||
- `make lint` - Run linter
|
|
||||||
- `make lint-fix` - Fix linting issues automatically
|
|
||||||
- `make security` - Run security scan with gosec
|
|
||||||
|
|
||||||
**Development Setup:**
|
|
||||||
- `make dev-setup` - Install all development tools with pinned versions
|
|
||||||
- `make deps` - Install/update dependencies
|
|
||||||
- `make fmt` - Format code
|
|
||||||
|
|
||||||
**Release Management:**
|
|
||||||
- `make release` - Create production release (requires goreleaser)
|
|
||||||
- `make snapshot` - Create snapshot release for testing
|
|
||||||
|
|
||||||
**Utilities:**
|
|
||||||
- `make clean` - Clean build artifacts
|
|
||||||
- `make version` - Show version and tool information
|
|
||||||
- `make help` - Show all available targets
|
|
||||||
|
|
||||||
#### Configuration Files
|
|
||||||
|
|
||||||
**Makefile**
|
|
||||||
- Uses `go get -tool` for Go 1.24+ tool management
|
|
||||||
- Cross-platform build support (Linux, macOS, Windows)
|
|
||||||
- Git-based version information in binaries
|
|
||||||
|
|
||||||
**.goreleaser.yml**
|
|
||||||
- Multi-platform release automation
|
|
||||||
- GitHub releases with changelog generation
|
|
||||||
- Package manager integration (Homebrew, Scoop)
|
|
||||||
- Docker image building support
|
|
||||||
|
|
||||||
**.golangci.yml**
|
|
||||||
- 40+ enabled linters for comprehensive code quality
|
|
||||||
- Optimized for Go 1.24
|
|
||||||
- Security scanning integration
|
|
||||||
- Test file exclusions for appropriate linters
|
|
||||||
|
|
||||||
#### Development Workflow
|
|
||||||
|
|
||||||
1. **Setup**: `make dev-setup` (one-time)
|
|
||||||
2. **Development**: `make dev` (format, lint, build)
|
|
||||||
3. **Before commit**: `make check-changes` (lint, security, test)
|
|
||||||
4. **Full verification**: `make verify` (complete build verification)
|
|
||||||
|
|
||||||
#### Building
|
|
||||||
```bash
|
|
||||||
# Quick build
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Cross-platform builds
|
|
||||||
make build-all
|
|
||||||
|
|
||||||
# Development build with checks
|
|
||||||
make dev
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Testing
|
|
||||||
- Always test with a sample plugin.json file
|
|
||||||
- Test both current directory and custom path operations
|
|
||||||
- Verify help and version commands work correctly
|
|
||||||
- Use `make test-coverage` for coverage reports
|
|
||||||
|
|
||||||
### Error Handling Standards
|
|
||||||
|
|
||||||
#### Error Messages
|
|
||||||
- Use descriptive error messages that help users understand what went wrong
|
|
||||||
- Include file paths in error messages when relevant
|
|
||||||
- Wrap errors with context using `fmt.Errorf("operation failed: %w", err)`
|
|
||||||
|
|
||||||
#### Exit Codes
|
|
||||||
- `0`: Success
|
|
||||||
- `1`: General error
|
|
||||||
- Let main.go handle all exit codes - command functions should return errors
|
|
||||||
|
|
||||||
### Plugin Path Resolution
|
### Plugin Path Resolution
|
||||||
|
|
||||||
#### Priority Order
|
1. `--plugin-path` flag
|
||||||
1. **Command-line flag**: `--plugin-path /path/to/plugin`
|
2. `PLUGINCTL_PLUGIN_PATH` env var
|
||||||
2. **Environment variable**: `PLUGINCTL_PLUGIN_PATH=/path/to/plugin`
|
3. Current directory (default)
|
||||||
3. **Current directory**: Default fallback
|
|
||||||
|
|
||||||
#### Implementation
|
### Logging
|
||||||
- `getEffectivePluginPath(flagPath string) string` - Determines effective plugin path
|
|
||||||
- All commands receive the resolved plugin path as a parameter
|
|
||||||
- Path is resolved to absolute path before use
|
|
||||||
|
|
||||||
### Plugin Validation
|
- **CRITICAL**: Use `pluginctl.Logger` (global slog instance)
|
||||||
|
- **Error**: `pluginctl.Logger.Error("message", "error", err)`
|
||||||
|
- **Info**: `pluginctl.Logger.Info("message")`
|
||||||
|
- **NEVER** use `fmt.Print*` or `log.*`
|
||||||
|
|
||||||
#### Required Checks
|
### Build & Development
|
||||||
- Plugin.json file must exist
|
|
||||||
- Plugin.json must be valid JSON
|
|
||||||
- Plugin.json must conform to Mattermost manifest schema
|
|
||||||
|
|
||||||
#### Utility Functions (plugin.go)
|
- **CRITICAL**: Use `make dev` for testing builds, NOT `go build`
|
||||||
- `LoadPluginManifest()` - Load from current directory
|
- **Before commit**: `make check-changes`
|
||||||
- `LoadPluginManifestFromPath(path)` - Load from specific path
|
- **Dependencies**: `make deps && go mod tidy`
|
||||||
- `HasServerCode(manifest)` - Check for server-side code
|
|
||||||
- `HasWebappCode(manifest)` - Check for webapp code
|
|
||||||
- `IsValidPluginDirectory()` - Validate current directory
|
|
||||||
|
|
||||||
### Future Command Ideas
|
### Error Handling
|
||||||
- `init` - Initialize a new plugin project
|
|
||||||
- `build` - Build plugin for distribution
|
|
||||||
- `deploy` - Deploy plugin to Mattermost instance
|
|
||||||
- `validate` - Validate plugin structure and manifest
|
|
||||||
- `package` - Package plugin for distribution
|
|
||||||
- `test` - Run plugin tests
|
|
||||||
|
|
||||||
### Version Management
|
- Commands return errors, main.go handles exit codes
|
||||||
- Current version: 0.1.0
|
- Use `fmt.Errorf("context: %w", err)` for wrapping
|
||||||
- Update version in `main.go` when releasing
|
|
||||||
- Follow semantic versioning
|
|
||||||
|
|
||||||
### Documentation Maintenance
|
### Adding New Commands
|
||||||
- **CRITICAL**: Always keep README.md up to date with any changes
|
|
||||||
- When adding new commands, update both CLAUDE.md and README.md
|
|
||||||
- When changing build processes, update both architecture docs and user docs
|
|
||||||
- When adding new dependencies or tools, document them in both files
|
|
||||||
- README.md is the user-facing documentation - it must be comprehensive and current
|
|
||||||
|
|
||||||
### Notes for Claude Sessions
|
1. Create `[command].go` in root with `Run[Command]Command` function
|
||||||
- Always maintain the separation between CLI framework and command implementation
|
2. Add case to `runCommand()` switch in main.go
|
||||||
- Use the official Mattermost model types - never create custom manifest structs
|
3. Update `showUsage()` in main.go
|
||||||
- Keep command implementations in separate files for maintainability
|
|
||||||
- Always validate plugin.json before performing operations
|
### Key Patterns
|
||||||
- Test new commands with the sample plugin.json file
|
|
||||||
- Follow the established error handling patterns
|
- Always validate plugin.json exists before operations
|
||||||
- Use the build system: `make check-changes` before any commits
|
- Use structured logging with key-value pairs
|
||||||
- Use pinned tool versions for reproducible development environments
|
- Follow existing naming conventions
|
||||||
|
- Keep command files self-contained and testable
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **CRITICAL**: Keep README.md updated when adding new commands or changing functionality
|
||||||
|
- README.md should be user-facing - focus on usage, not implementation details
|
||||||
|
- Use `pluginctl --help` for command documentation, not hardcoded lists in README
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -136,11 +136,12 @@ verify: clean lint test build ## Verify build (clean, lint, test, build)
|
||||||
|
|
||||||
# Quick development build
|
# Quick development build
|
||||||
.PHONY: dev
|
.PHONY: dev
|
||||||
dev: fmt lint build ## Quick development build (fmt, lint, build)
|
dev: fmt lint snapshot ## Quick development build (fmt, lint, build)
|
||||||
|
|
||||||
# Check changes target
|
# Check changes target
|
||||||
.PHONY: check-changes
|
.PHONY: check-changes
|
||||||
check-changes: lint test ## Check changes (lint, test)
|
check-changes: lint test ## Check changes (lint, test)
|
||||||
|
@echo "All checks passed!"
|
||||||
|
|
||||||
# CI target
|
# CI target
|
||||||
.PHONY: ci
|
.PHONY: ci
|
||||||
|
|
206
README.md
206
README.md
|
@ -20,14 +20,7 @@ Download the latest binary from the [releases page](https://github.com/mattermos
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using go install
|
# Using go install
|
||||||
go install github.com/mattermost/pluginctl/cmd/pluginctl@latest
|
go install github.com/mattermost/pluginctl/cmd/pluginctl
|
||||||
|
|
||||||
# Using Homebrew (if available)
|
|
||||||
brew install mattermost/tap/pluginctl
|
|
||||||
|
|
||||||
# Using Scoop on Windows (if available)
|
|
||||||
scoop bucket add mattermost https://github.com/mattermost/scoop-bucket.git
|
|
||||||
scoop install pluginctl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
@ -38,11 +31,11 @@ scoop install pluginctl
|
||||||
# Display plugin information
|
# Display plugin information
|
||||||
pluginctl info
|
pluginctl info
|
||||||
|
|
||||||
# Show help
|
# Show help and available commands
|
||||||
pluginctl --help
|
pluginctl --help
|
||||||
|
|
||||||
# Show version
|
# Show version
|
||||||
pluginctl --version
|
pluginctl version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Plugin Path Configuration
|
### Plugin Path Configuration
|
||||||
|
@ -68,170 +61,43 @@ pluginctl --version
|
||||||
pluginctl info
|
pluginctl info
|
||||||
```
|
```
|
||||||
|
|
||||||
### Commands
|
### Available Commands
|
||||||
|
|
||||||
#### `info`
|
Run `pluginctl --help` to see all available commands and options.
|
||||||
|
|
||||||
Display comprehensive information about a Mattermost plugin:
|
## Development
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pluginctl info
|
# Set up development environment
|
||||||
```
|
make dev-setup
|
||||||
|
|
||||||
**Output includes:**
|
# Install dependencies
|
||||||
|
make deps
|
||||||
|
|
||||||
- Plugin ID, name, and version
|
|
||||||
- Minimum Mattermost server version required
|
|
||||||
- Description (if available)
|
|
||||||
- Server code presence and supported platforms
|
|
||||||
- Webapp code presence and bundle path
|
|
||||||
- Settings schema availability
|
|
||||||
|
|
||||||
**Example output:**
|
|
||||||
|
|
||||||
```
|
|
||||||
Plugin Information:
|
|
||||||
==================
|
|
||||||
|
|
||||||
ID: com.example.testplugin
|
|
||||||
Name: Test Plugin
|
|
||||||
Version: 1.0.0
|
|
||||||
Min MM Version: 7.0.0
|
|
||||||
Description: A test plugin for demonstrating pluginctl functionality
|
|
||||||
|
|
||||||
Code Components:
|
|
||||||
================
|
|
||||||
Server Code: Yes
|
|
||||||
Executables: linux-amd64, darwin-amd64, windows-amd64
|
|
||||||
Webapp Code: Yes
|
|
||||||
Bundle Path: webapp/dist/main.js
|
|
||||||
Settings Schema: Yes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Go 1.24.3 or later
|
|
||||||
- Valid Mattermost plugin directory with `plugin.json` manifest file
|
|
||||||
|
|
||||||
## Development Tools
|
|
||||||
|
|
||||||
The project uses the following tools for development and release automation:
|
|
||||||
|
|
||||||
- **golangci-lint** v1.62.2 - Code linting and quality checks
|
|
||||||
- **goreleaser** v2.6.2 - Automated releases and cross-platform builds
|
|
||||||
- **gosec** v2.22.0 - Security vulnerability scanning
|
|
||||||
|
|
||||||
## Plugin Directory Structure
|
|
||||||
|
|
||||||
`pluginctl` expects to work with standard Mattermost plugin directories containing a `plugin.json` file. For more information about Mattermost plugin structure, visit the [official documentation](https://developers.mattermost.com/integrate/plugins/).
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
| ----------------------- | ----------------------------- |
|
|
||||||
| `PLUGINCTL_PLUGIN_PATH` | Default plugin directory path |
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
We welcome contributions to `pluginctl`! Please see the [CLAUDE.md](CLAUDE.md) file for architecture guidelines and development instructions.
|
|
||||||
|
|
||||||
### Development Setup
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/mattermost/pluginctl.git
|
|
||||||
cd pluginctl
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Set up development environment (installs pinned tool versions):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make dev-setup
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make deps
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Build the project:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make build
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Test with a sample plugin:
|
|
||||||
```bash
|
|
||||||
./pluginctl info
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
|
|
||||||
Use these Make targets for efficient development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Quick development build (format, lint, build)
|
# Quick development build (format, lint, build)
|
||||||
make dev
|
make dev
|
||||||
|
|
||||||
# Check all changes before committing (lint, security, test)
|
# Run tests and checks before committing
|
||||||
make check-changes
|
make check-changes
|
||||||
|
|
||||||
# Full verification (clean, lint, test, build)
|
|
||||||
make verify
|
|
||||||
|
|
||||||
# Run tests with coverage
|
|
||||||
make test-coverage
|
|
||||||
|
|
||||||
# Build for all platforms
|
|
||||||
make build-all
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Make Targets
|
See `make help` for the complete list of available targets.
|
||||||
|
|
||||||
**Development:**
|
|
||||||
|
|
||||||
- `make dev` - Quick development build
|
|
||||||
- `make check-changes` - Validate changes (lint, security, test)
|
|
||||||
- `make verify` - Full build verification
|
|
||||||
- `make fmt` - Format code
|
|
||||||
- `make clean` - Clean build artifacts
|
|
||||||
|
|
||||||
**Testing:**
|
|
||||||
|
|
||||||
- `make test` - Run tests
|
|
||||||
- `make test-coverage` - Run tests with coverage report
|
|
||||||
- `make lint` - Run linter
|
|
||||||
- `make lint-fix` - Fix linting issues automatically
|
|
||||||
- `make security` - Run security scan
|
|
||||||
|
|
||||||
**Building:**
|
|
||||||
|
|
||||||
- `make build` - Build for current platform
|
|
||||||
- `make build-all` - Build for all platforms
|
|
||||||
- `make install` - Install to GOPATH/bin
|
|
||||||
|
|
||||||
**Release:**
|
|
||||||
|
|
||||||
- `make release` - Create production release
|
|
||||||
- `make snapshot` - Create snapshot release
|
|
||||||
|
|
||||||
**Utilities:**
|
|
||||||
|
|
||||||
- `make help` - Show all available targets
|
|
||||||
- `make version` - Show version information
|
|
||||||
- `make dev-setup` - Install development tools
|
|
||||||
|
|
||||||
### Adding New Commands
|
### Adding New Commands
|
||||||
|
|
||||||
1. Create a new command file (e.g., `build.go`)
|
1. Create a new command file (e.g., `build.go`) in the root directory
|
||||||
2. Implement the command following the patterns in `info.go`
|
2. Implement the command following existing patterns
|
||||||
3. Register the command in `cmd/pluginctl/main.go`
|
3. Register the command in `cmd/pluginctl/main.go`
|
||||||
4. Update the help text and documentation
|
4. Update the help text
|
||||||
|
|
||||||
See [CLAUDE.md](CLAUDE.md) for detailed architecture guidelines.
|
See [CLAUDE.md](CLAUDE.md) for detailed architecture guidelines.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions to `pluginctl`! Please see the [CLAUDE.md](CLAUDE.md) file for architecture guidelines and development patterns.
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
- Follow Go best practices and conventions
|
- Follow Go best practices and conventions
|
||||||
|
@ -239,31 +105,6 @@ See [CLAUDE.md](CLAUDE.md) for detailed architecture guidelines.
|
||||||
- Maintain separation between CLI framework and command implementation
|
- Maintain separation between CLI framework and command implementation
|
||||||
- Include comprehensive error handling with descriptive messages
|
- Include comprehensive error handling with descriptive messages
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
Test your changes with various plugin configurations:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Run tests with coverage
|
|
||||||
make test-coverage
|
|
||||||
|
|
||||||
# Test CLI functionality
|
|
||||||
./pluginctl info
|
|
||||||
|
|
||||||
# Test with command-line flag
|
|
||||||
./pluginctl --plugin-path /path/to/plugin info
|
|
||||||
|
|
||||||
# Test with environment variable
|
|
||||||
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
|
||||||
./pluginctl info
|
|
||||||
|
|
||||||
# Validate all changes before committing
|
|
||||||
make check-changes
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
|
This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
@ -278,6 +119,5 @@ This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENS
|
||||||
|
|
||||||
For questions, issues, or feature requests, please:
|
For questions, issues, or feature requests, please:
|
||||||
|
|
||||||
1. Check the [issues](https://github.com/mattermost/pluginctl/issues) page
|
- Check the [issues](https://github.com/mattermost/pluginctl/issues) or create a new issue if your problem isn't already reported
|
||||||
2. Create a new issue if your problem isn't already reported
|
- Join the [Mattermost Community](https://community.mattermost.com/) for general discussion
|
||||||
3. Join the [Mattermost Community](https://community.mattermost.com/) for general discussion
|
|
||||||
|
|
2
assets/.gitattributes
vendored
Normal file
2
assets/.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
server/manifest.go linguist-generated=true
|
||||||
|
webapp/src/manifest.js linguist-generated=true
|
1
assets/.gitpod.yml
Normal file
1
assets/.gitpod.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mainConfiguration: https://github.com/mattermost/mattermost-gitpod-config
|
1
assets/.nvmrc
Normal file
1
assets/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
20.11
|
43
assets/Makefile
Normal file
43
assets/Makefile
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
GO ?= $(shell command -v go 2> /dev/null)
|
||||||
|
NPM ?= $(shell command -v npm 2> /dev/null)
|
||||||
|
CURL ?= $(shell command -v curl 2> /dev/null)
|
||||||
|
MM_DEBUG ?=
|
||||||
|
GOPATH ?= $(shell go env GOPATH)
|
||||||
|
GO_TEST_FLAGS ?= -race
|
||||||
|
GO_BUILD_FLAGS ?=
|
||||||
|
MM_UTILITIES_DIR ?= ../mattermost-utilities
|
||||||
|
DLV_DEBUG_PORT := 2346
|
||||||
|
DEFAULT_GOOS := $(shell go env GOOS)
|
||||||
|
DEFAULT_GOARCH := $(shell go env GOARCH)
|
||||||
|
|
||||||
|
export GO111MODULE=on
|
||||||
|
|
||||||
|
# We need to export GOBIN to allow it to be set
|
||||||
|
# for processes spawned from the Makefile
|
||||||
|
export GOBIN ?= $(PWD)/bin
|
||||||
|
|
||||||
|
# You can include assets this directory into the bundle. This can be e.g. used to include profile pictures.
|
||||||
|
ASSETS_DIR ?= assets
|
||||||
|
|
||||||
|
## Define the default target (make all)
|
||||||
|
.PHONY: default
|
||||||
|
default: all
|
||||||
|
|
||||||
|
# Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed.
|
||||||
|
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),)
|
||||||
|
GO_BUILD_GCFLAGS = -gcflags "all=-N -l"
|
||||||
|
else
|
||||||
|
GO_BUILD_GCFLAGS =
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Include modular makefiles
|
||||||
|
include build/*.mk
|
83
assets/build/build.mk
Normal file
83
assets/build/build.mk
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Build Targets
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## Checks the code style, tests, builds and bundles the plugin.
|
||||||
|
.PHONY: all
|
||||||
|
all: check-style test dist
|
||||||
|
|
||||||
|
## Ensures the plugin manifest is valid
|
||||||
|
.PHONY: manifest-check
|
||||||
|
manifest-check:
|
||||||
|
pluginctl manifest check
|
||||||
|
|
||||||
|
|
||||||
|
## Builds the server, if it exists, for all supported architectures, unless MM_SERVICESETTINGS_ENABLEDEVELOPER is set.
|
||||||
|
.PHONY: server
|
||||||
|
server:
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
ifneq ($(MM_DEBUG),)
|
||||||
|
$(info DEBUG mode is on; to disable, unset MM_DEBUG)
|
||||||
|
endif
|
||||||
|
mkdir -p server/dist;
|
||||||
|
ifneq ($(MM_SERVICESETTINGS_ENABLEDEVELOPER),)
|
||||||
|
@echo Building plugin only for $(DEFAULT_GOOS)-$(DEFAULT_GOARCH) because MM_SERVICESETTINGS_ENABLEDEVELOPER is enabled
|
||||||
|
cd server && env CGO_ENABLED=0 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-$(DEFAULT_GOOS)-$(DEFAULT_GOARCH);
|
||||||
|
else
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-linux-amd64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-linux-arm64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-darwin-amd64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-darwin-arm64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-windows-amd64.exe;
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Ensures NPM dependencies are installed without having to run this all the time.
|
||||||
|
webapp/node_modules: $(wildcard webapp/package.json)
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
cd webapp && $(NPM) install
|
||||||
|
touch $@
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Builds the webapp, if it exists.
|
||||||
|
.PHONY: webapp
|
||||||
|
webapp: webapp/node_modules
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
ifeq ($(MM_DEBUG),)
|
||||||
|
cd webapp && $(NPM) run build;
|
||||||
|
else
|
||||||
|
cd webapp && $(NPM) run debug;
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Generates a tar bundle of the plugin for install.
|
||||||
|
.PHONY: bundle
|
||||||
|
bundle:
|
||||||
|
rm -rf dist/
|
||||||
|
mkdir -p dist/$(PLUGIN_ID)
|
||||||
|
cp plugin.json dist/$(PLUGIN_ID)/plugin.json
|
||||||
|
ifneq ($(wildcard $(ASSETS_DIR)/.),)
|
||||||
|
cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_PUBLIC),)
|
||||||
|
cp -r public dist/$(PLUGIN_ID)/
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
mkdir -p dist/$(PLUGIN_ID)/server
|
||||||
|
cp -r server/dist dist/$(PLUGIN_ID)/server/
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
mkdir -p dist/$(PLUGIN_ID)/webapp
|
||||||
|
cp -r webapp/dist dist/$(PLUGIN_ID)/webapp/
|
||||||
|
endif
|
||||||
|
ifeq ($(shell uname),Darwin)
|
||||||
|
cd dist && tar --disable-copyfile -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
|
||||||
|
else
|
||||||
|
cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
|
||||||
|
endif
|
||||||
|
|
||||||
|
@echo plugin built at: dist/$(BUNDLE_NAME)
|
||||||
|
|
||||||
|
## Builds and bundles the plugin.
|
||||||
|
.PHONY: dist
|
||||||
|
dist: server webapp bundle
|
47
assets/build/deploy.mk
Normal file
47
assets/build/deploy.mk
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Deployment and Plugin Management
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## Builds and installs the plugin to a server.
|
||||||
|
.PHONY: deploy
|
||||||
|
deploy: dist
|
||||||
|
pluginctl deploy --bundle-path dist/$(BUNDLE_NAME)
|
||||||
|
|
||||||
|
## Builds and installs the plugin to a server, updating the webapp automatically when changed.
|
||||||
|
.PHONY: watch
|
||||||
|
watch: server bundle
|
||||||
|
ifeq ($(MM_DEBUG),)
|
||||||
|
cd webapp && $(NPM) run build:watch
|
||||||
|
else
|
||||||
|
cd webapp && $(NPM) run debug:watch
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Installs a previous built plugin with updated webpack assets to a server.
|
||||||
|
.PHONY: deploy-from-watch
|
||||||
|
deploy-from-watch: bundle
|
||||||
|
pluginctl deploy --bundle-path dist/$(BUNDLE_NAME)
|
||||||
|
|
||||||
|
## Disable the plugin.
|
||||||
|
.PHONY: disable
|
||||||
|
disable: detach
|
||||||
|
pluginctl disable
|
||||||
|
|
||||||
|
## Enable the plugin.
|
||||||
|
.PHONY: enable
|
||||||
|
enable:
|
||||||
|
pluginctl enable
|
||||||
|
|
||||||
|
## Reset the plugin, effectively disabling and re-enabling it on the server.
|
||||||
|
.PHONY: reset
|
||||||
|
reset: detach
|
||||||
|
pluginctl reset
|
||||||
|
|
||||||
|
## View plugin logs.
|
||||||
|
.PHONY: logs
|
||||||
|
logs:
|
||||||
|
pluginctl logs
|
||||||
|
|
||||||
|
## Watch plugin logs.
|
||||||
|
.PHONY: logs-watch
|
||||||
|
logs-watch:
|
||||||
|
pluginctl logs --watch
|
53
assets/build/dev.mk
Normal file
53
assets/build/dev.mk
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Development and Debugging
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## Setup dlv for attaching, identifying the plugin PID for other targets.
|
||||||
|
.PHONY: setup-attach
|
||||||
|
setup-attach:
|
||||||
|
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
|
||||||
|
$(eval NUM_PID := $(shell echo -n ${PLUGIN_PID} | wc -w))
|
||||||
|
|
||||||
|
@if [ ${NUM_PID} -gt 2 ]; then \
|
||||||
|
echo "** There is more than 1 plugin process running. Run 'make kill reset' to restart just one."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
## Check if setup-attach succeeded.
|
||||||
|
.PHONY: check-attach
|
||||||
|
check-attach:
|
||||||
|
@if [ -z ${PLUGIN_PID} ]; then \
|
||||||
|
echo "Could not find plugin PID; the plugin is not running. Exiting."; \
|
||||||
|
exit 1; \
|
||||||
|
else \
|
||||||
|
echo "Located Plugin running with PID: ${PLUGIN_PID}"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
## Attach dlv to an existing plugin instance.
|
||||||
|
.PHONY: attach
|
||||||
|
attach: setup-attach check-attach
|
||||||
|
dlv attach ${PLUGIN_PID}
|
||||||
|
|
||||||
|
## Attach dlv to an existing plugin instance, exposing a headless instance on $DLV_DEBUG_PORT.
|
||||||
|
.PHONY: attach-headless
|
||||||
|
attach-headless: setup-attach check-attach
|
||||||
|
dlv attach ${PLUGIN_PID} --listen :$(DLV_DEBUG_PORT) --headless=true --api-version=2 --accept-multiclient
|
||||||
|
|
||||||
|
## Detach dlv from an existing plugin instance, if previously attached.
|
||||||
|
.PHONY: detach
|
||||||
|
detach: setup-attach
|
||||||
|
@DELVE_PID=$(shell ps aux | grep "dlv attach ${PLUGIN_PID}" | grep -v "grep" | awk -F " " '{print $$2}') && \
|
||||||
|
if [ "$$DELVE_PID" -gt 0 ] > /dev/null 2>&1 ; then \
|
||||||
|
echo "Located existing delve process running with PID: $$DELVE_PID. Killing." ; \
|
||||||
|
kill -9 $$DELVE_PID ; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
## Kill all instances of the plugin, detaching any existing dlv instance.
|
||||||
|
.PHONY: kill
|
||||||
|
kill: detach
|
||||||
|
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
|
||||||
|
|
||||||
|
@for PID in ${PLUGIN_PID}; do \
|
||||||
|
echo "Killing plugin pid $$PID"; \
|
||||||
|
kill -9 $$PID; \
|
||||||
|
done; \
|
44
assets/build/setup.mk
Normal file
44
assets/build/setup.mk
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Ensure that go is installed. Note that this is independent of whether or not a server is being
|
||||||
|
# built, since the build script itself uses go.
|
||||||
|
ifeq ($(GO),)
|
||||||
|
$(error "go is not available: see https://golang.org/doc/install")
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Gather build variables to inject into the manifest tool
|
||||||
|
BUILD_HASH_SHORT = $(shell git rev-parse --short HEAD)
|
||||||
|
BUILD_TAG_LATEST = $(shell git describe --tags --match 'v*' --abbrev=0 2>/dev/null)
|
||||||
|
BUILD_TAG_CURRENT = $(shell git tag --points-at HEAD)
|
||||||
|
|
||||||
|
# Extract the plugin id from the manifest.
|
||||||
|
PLUGIN_ID ?= $(shell pluginctl manifest id)
|
||||||
|
ifeq ($(PLUGIN_ID),)
|
||||||
|
$(error "Cannot parse id from $(MANIFEST_FILE)")
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Extract the plugin version from the manifest.
|
||||||
|
PLUGIN_VERSION ?= $(shell pluginctl manifest version)
|
||||||
|
ifeq ($(PLUGIN_VERSION),)
|
||||||
|
$(error "Cannot parse version from $(MANIFEST_FILE)")
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Determine if a server is defined in the manifest.
|
||||||
|
HAS_SERVER ?= $(shell pluginctl manifest has_server)
|
||||||
|
|
||||||
|
# Determine if a webapp is defined in the manifest.
|
||||||
|
HAS_WEBAPP ?= $(shell pluginctl manifest has_webapp)
|
||||||
|
|
||||||
|
# Determine if a /public folder is in use
|
||||||
|
HAS_PUBLIC ?= $(wildcard public/.)
|
||||||
|
|
||||||
|
# Determine if the mattermost-utilities repo is present
|
||||||
|
HAS_MM_UTILITIES ?= $(wildcard $(MM_UTILITIES_DIR)/.)
|
||||||
|
|
||||||
|
# Store the current path for later use
|
||||||
|
PWD ?= $(shell pwd)
|
||||||
|
|
||||||
|
# Ensure that npm (and thus node) is installed.
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
ifeq ($(NPM),)
|
||||||
|
$(error "npm is not available: see https://www.npmjs.com/get-npm")
|
||||||
|
endif
|
||||||
|
endif
|
57
assets/build/test.mk
Normal file
57
assets/build/test.mk
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Testing and Quality Assurance
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## Install go tools
|
||||||
|
install-go-tools:
|
||||||
|
@echo Installing go tools
|
||||||
|
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0
|
||||||
|
$(GO) install gotest.tools/gotestsum@v1.7.0
|
||||||
|
|
||||||
|
## Runs eslint and golangci-lint
|
||||||
|
.PHONY: check-style
|
||||||
|
check-style: manifest-check webapp/node_modules install-go-tools
|
||||||
|
@echo Checking for style guide compliance
|
||||||
|
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
cd webapp && npm run lint
|
||||||
|
cd webapp && npm run check-types
|
||||||
|
endif
|
||||||
|
|
||||||
|
# It's highly recommended to run go-vet first
|
||||||
|
# to find potential compile errors that could introduce
|
||||||
|
# weird reports at golangci-lint step
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
@echo Running golangci-lint
|
||||||
|
$(GO) vet ./...
|
||||||
|
$(GOBIN)/golangci-lint run ./...
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Runs any lints and unit tests defined for the server and webapp, if they exist.
|
||||||
|
.PHONY: test
|
||||||
|
test: webapp/node_modules install-go-tools
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
$(GOBIN)/gotestsum -- -v ./...
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
cd webapp && $(NPM) run test;
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Runs any lints and unit tests defined for the server and webapp, if they exist, optimized
|
||||||
|
## for a CI environment.
|
||||||
|
.PHONY: test-ci
|
||||||
|
test-ci: webapp/node_modules install-go-tools
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
$(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./...
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
cd webapp && $(NPM) run test;
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Creates a coverage report for the server code.
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage: webapp/node_modules
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
$(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt ./server/...
|
||||||
|
$(GO) tool cover -html=server/coverage.txt
|
||||||
|
endif
|
42
assets/build/utils.mk
Normal file
42
assets/build/utils.mk
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Utilities
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## Clean removes all build artifacts.
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -fr dist/
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
rm -fr server/coverage.txt
|
||||||
|
rm -fr server/dist
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
rm -fr webapp/junit.xml
|
||||||
|
rm -fr webapp/dist
|
||||||
|
rm -fr webapp/node_modules
|
||||||
|
endif
|
||||||
|
rm -fr build/bin/
|
||||||
|
|
||||||
|
## Extract strings for translation from the source code.
|
||||||
|
.PHONY: i18n-extract
|
||||||
|
i18n-extract:
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
ifeq ($(HAS_MM_UTILITIES),)
|
||||||
|
@echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
|
||||||
|
else
|
||||||
|
cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-webapp --webapp-dir $(PWD)/webapp
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Generate mocks for testing.
|
||||||
|
.PHONY: mock
|
||||||
|
mock:
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
go install github.com/golang/mock/mockgen@v1.6.0
|
||||||
|
mockgen -destination=server/command/mocks/mock_commands.go -package=mocks github.com/mattermost/mattermost-plugin-starter-template/server/command Command
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Show help documentation.
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//g" | sed -e "s/^## //g" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort
|
111
assets/build/versioning.mk
Normal file
111
assets/build/versioning.mk
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Semantic Versioning
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
# Used for semver bumping
|
||||||
|
PROTECTED_BRANCH := master
|
||||||
|
APP_NAME := $(shell basename -s .git `git config --get remote.origin.url`)
|
||||||
|
CURRENT_VERSION := $(shell git describe --abbrev=0 --tags)
|
||||||
|
VERSION_PARTS := $(subst ., ,$(subst v,,$(subst -rc, ,$(CURRENT_VERSION))))
|
||||||
|
MAJOR := $(word 1,$(VERSION_PARTS))
|
||||||
|
MINOR := $(word 2,$(VERSION_PARTS))
|
||||||
|
PATCH := $(word 3,$(VERSION_PARTS))
|
||||||
|
RC := $(shell echo $(CURRENT_VERSION) | grep -oE 'rc[0-9]+' | sed 's/rc//')
|
||||||
|
|
||||||
|
# Check if current branch is protected
|
||||||
|
define check_protected_branch
|
||||||
|
@current_branch=$$(git rev-parse --abbrev-ref HEAD); \
|
||||||
|
if ! echo "$(PROTECTED_BRANCH)" | grep -wq "$$current_branch" && ! echo "$$current_branch" | grep -q "^release"; then \
|
||||||
|
echo "Error: Tagging is only allowed from $(PROTECTED_BRANCH) or release branches. You are on $$current_branch branch."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
# Check if there are pending pulls
|
||||||
|
define check_pending_pulls
|
||||||
|
@git fetch; \
|
||||||
|
current_branch=$$(git rev-parse --abbrev-ref HEAD); \
|
||||||
|
if [ "$$(git rev-parse HEAD)" != "$$(git rev-parse origin/$$current_branch)" ]; then \
|
||||||
|
echo "Error: Your branch is not up to date with upstream. Please pull the latest changes before performing a release"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
# Prompt for approval
|
||||||
|
define prompt_approval
|
||||||
|
@read -p "About to bump $(APP_NAME) to version $(1), approve? (y/n) " userinput; \
|
||||||
|
if [ "$$userinput" != "y" ]; then \
|
||||||
|
echo "Bump aborted."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
.PHONY: patch minor major patch-rc minor-rc major-rc
|
||||||
|
|
||||||
|
patch: ## to bump patch version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval PATCH := $(shell echo $$(($(PATCH)+1))))
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH))
|
||||||
|
@echo Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
@echo Bumped $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
|
||||||
|
minor: ## to bump minor version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval MINOR := $(shell echo $$(($(MINOR)+1))))
|
||||||
|
@$(eval PATCH := 0)
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH))
|
||||||
|
@echo Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
@echo Bumped $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
|
||||||
|
major: ## to bump major version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
$(eval MAJOR := $(shell echo $$(($(MAJOR)+1))))
|
||||||
|
$(eval MINOR := 0)
|
||||||
|
$(eval PATCH := 0)
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH))
|
||||||
|
@echo Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
@echo Bumped $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
|
||||||
|
patch-rc: ## to bump patch release candidate version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval RC := $(shell echo $$(($(RC)+1))))
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH)-rc$(RC))
|
||||||
|
@echo Bumping $(APP_NAME) to Patch RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC) -m "Bumping $(APP_NAME) to Patch RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
@echo Bumped $(APP_NAME) to Patch RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
|
||||||
|
minor-rc: ## to bump minor release candidate version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval MINOR := $(shell echo $$(($(MINOR)+1))))
|
||||||
|
@$(eval PATCH := 0)
|
||||||
|
@$(eval RC := 1)
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH)-rc$(RC))
|
||||||
|
@echo Bumping $(APP_NAME) to Minor RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC) -m "Bumping $(APP_NAME) to Minor RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
@echo Bumped $(APP_NAME) to Minor RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
|
||||||
|
major-rc: ## to bump major release candidate version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval MAJOR := $(shell echo $$(($(MAJOR)+1))))
|
||||||
|
@$(eval MINOR := 0)
|
||||||
|
@$(eval PATCH := 0)
|
||||||
|
@$(eval RC := 1)
|
||||||
|
$(call prompt_approval,$(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 push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
@echo Bumped $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
11
client.go
11
client.go
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
@ -22,14 +21,14 @@ func getClient(ctx context.Context) (*model.Client4, error) {
|
||||||
|
|
||||||
client, connected := getUnixClient(socketPath)
|
client, connected := getUnixClient(socketPath)
|
||||||
if connected {
|
if connected {
|
||||||
log.Printf("Connecting using local mode over %s", socketPath)
|
Logger.Info("Connecting using local mode", "socket_path", socketPath)
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
|
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
|
||||||
log.Printf("No socket found at %s for local mode deployment. "+
|
Logger.Info("No socket found for local mode deployment. Attempting to authenticate with credentials.",
|
||||||
"Attempting to authenticate with credentials.", socketPath)
|
"socket_path", socketPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
|
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
|
||||||
|
@ -44,7 +43,7 @@ func getClient(ctx context.Context) (*model.Client4, error) {
|
||||||
client = model.NewAPIv4Client(siteURL)
|
client = model.NewAPIv4Client(siteURL)
|
||||||
|
|
||||||
if adminToken != "" {
|
if adminToken != "" {
|
||||||
log.Printf("Authenticating using token against %s.", siteURL)
|
Logger.Info("Authenticating using token", "site_url", siteURL)
|
||||||
client.SetToken(adminToken)
|
client.SetToken(adminToken)
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
|
@ -52,7 +51,7 @@ func getClient(ctx context.Context) (*model.Client4, error) {
|
||||||
|
|
||||||
if adminUsername != "" && adminPassword != "" {
|
if adminUsername != "" && adminPassword != "" {
|
||||||
client := model.NewAPIv4Client(siteURL)
|
client := model.NewAPIv4Client(siteURL)
|
||||||
log.Printf("Authenticating as %s against %s.", adminUsername, siteURL)
|
Logger.Info("Authenticating with credentials", "username", adminUsername, "site_url", siteURL)
|
||||||
_, _, err := client.Login(ctx, adminUsername, adminPassword)
|
_, _, err := client.Login(ctx, adminUsername, adminPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to login as %s: %w", adminUsername, err)
|
return nil, fmt.Errorf("failed to login as %s: %w", adminUsername, err)
|
||||||
|
|
|
@ -15,6 +15,9 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Initialize logger
|
||||||
|
pluginctl.InitLogger()
|
||||||
|
|
||||||
var pluginPath string
|
var pluginPath string
|
||||||
|
|
||||||
flag.StringVar(&pluginPath, "plugin-path", "", "Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)")
|
flag.StringVar(&pluginPath, "plugin-path", "", "Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)")
|
||||||
|
@ -22,7 +25,7 @@ func main() {
|
||||||
|
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
fmt.Fprintf(os.Stderr, "Error: No command specified\n\n")
|
pluginctl.Logger.Error("No command specified")
|
||||||
showUsage()
|
showUsage()
|
||||||
os.Exit(ExitError)
|
os.Exit(ExitError)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +37,7 @@ func main() {
|
||||||
effectivePluginPath := pluginctl.GetEffectivePluginPath(pluginPath)
|
effectivePluginPath := pluginctl.GetEffectivePluginPath(pluginPath)
|
||||||
|
|
||||||
if err := runCommand(command, commandArgs, effectivePluginPath); err != nil {
|
if err := runCommand(command, commandArgs, effectivePluginPath); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
pluginctl.Logger.Error("Command failed", "error", err)
|
||||||
os.Exit(ExitError)
|
os.Exit(ExitError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,8 +52,14 @@ func runCommand(command string, args []string, pluginPath string) error {
|
||||||
return runDisableCommand(args, pluginPath)
|
return runDisableCommand(args, pluginPath)
|
||||||
case "reset":
|
case "reset":
|
||||||
return runResetCommand(args, pluginPath)
|
return runResetCommand(args, pluginPath)
|
||||||
|
case "deploy":
|
||||||
|
return runDeployCommand(args, pluginPath)
|
||||||
case "updateassets":
|
case "updateassets":
|
||||||
return runUpdateAssetsCommand(args, pluginPath)
|
return runUpdateAssetsCommand(args, pluginPath)
|
||||||
|
case "manifest":
|
||||||
|
return runManifestCommand(args, pluginPath)
|
||||||
|
case "logs":
|
||||||
|
return runLogsCommand(args, pluginPath)
|
||||||
case "help":
|
case "help":
|
||||||
showUsage()
|
showUsage()
|
||||||
|
|
||||||
|
@ -68,7 +77,7 @@ func runInfoCommand(args []string, pluginPath string) error {
|
||||||
|
|
||||||
func runVersionCommand(_ []string) error {
|
func runVersionCommand(_ []string) error {
|
||||||
version := pluginctl.GetVersion()
|
version := pluginctl.GetVersion()
|
||||||
fmt.Printf("pluginctl version %s\n", version)
|
pluginctl.Logger.Info("pluginctl version", "version", version)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -88,8 +97,20 @@ func runUpdateAssetsCommand(args []string, pluginPath string) error {
|
||||||
return pluginctl.RunUpdateAssetsCommand(args, pluginPath)
|
return pluginctl.RunUpdateAssetsCommand(args, pluginPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runManifestCommand(args []string, pluginPath string) error {
|
||||||
|
return pluginctl.RunManifestCommand(args, pluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogsCommand(args []string, pluginPath string) error {
|
||||||
|
return pluginctl.RunLogsCommand(args, pluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDeployCommand(args []string, pluginPath string) error {
|
||||||
|
return pluginctl.RunDeployCommand(args, pluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
func showUsage() {
|
func showUsage() {
|
||||||
fmt.Printf(`pluginctl - Mattermost Plugin Development CLI
|
usageText := `pluginctl - Mattermost Plugin Development CLI
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
pluginctl [global options] <command> [command options] [arguments...]
|
pluginctl [global options] <command> [command options] [arguments...]
|
||||||
|
@ -102,7 +123,10 @@ Commands:
|
||||||
enable Enable plugin from current directory in Mattermost server
|
enable Enable plugin from current directory in Mattermost server
|
||||||
disable Disable plugin from current directory in Mattermost server
|
disable Disable plugin from current directory in Mattermost server
|
||||||
reset Reset plugin from current directory (disable then enable)
|
reset Reset plugin from current directory (disable then enable)
|
||||||
|
deploy Upload and enable plugin bundle to Mattermost server
|
||||||
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)
|
||||||
|
logs View plugin logs (use --watch to follow logs in real-time)
|
||||||
help Show this help message
|
help Show this help message
|
||||||
version Show version information
|
version Show version information
|
||||||
|
|
||||||
|
@ -112,7 +136,16 @@ Examples:
|
||||||
pluginctl enable # Enable plugin from current directory
|
pluginctl enable # Enable plugin from current directory
|
||||||
pluginctl disable # Disable plugin from current directory
|
pluginctl disable # Disable plugin from current directory
|
||||||
pluginctl reset # Reset plugin from current directory (disable then enable)
|
pluginctl reset # Reset plugin from current directory (disable then enable)
|
||||||
|
pluginctl deploy # Upload and enable plugin bundle from ./dist/
|
||||||
|
pluginctl deploy --bundle-path ./bundle.tar.gz # Deploy specific bundle file
|
||||||
pluginctl updateassets # Update plugin files from embedded assets
|
pluginctl updateassets # Update plugin files from embedded assets
|
||||||
|
pluginctl manifest id # Get plugin ID
|
||||||
|
pluginctl manifest version # Get plugin version
|
||||||
|
pluginctl manifest has_server # Check if plugin has server code
|
||||||
|
pluginctl manifest has_webapp # Check if plugin has webapp code
|
||||||
|
pluginctl manifest check # Validate plugin manifest
|
||||||
|
pluginctl logs # View recent plugin logs
|
||||||
|
pluginctl logs --watch # Watch plugin logs in real-time
|
||||||
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
|
||||||
|
@ -127,5 +160,6 @@ Environment Variables:
|
||||||
|
|
||||||
For more information about Mattermost plugin development, visit:
|
For more information about Mattermost plugin development, visit:
|
||||||
https://developers.mattermost.com/integrate/plugins/
|
https://developers.mattermost.com/integrate/plugins/
|
||||||
`)
|
`
|
||||||
|
pluginctl.Logger.Info(usageText)
|
||||||
}
|
}
|
||||||
|
|
98
deploy.go
Normal file
98
deploy.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package pluginctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunDeployCommand(args []string, pluginPath string) error {
|
||||||
|
var bundlePath string
|
||||||
|
|
||||||
|
// Parse flags
|
||||||
|
i := 0
|
||||||
|
for i < len(args) {
|
||||||
|
switch args[i] {
|
||||||
|
case "--bundle-path":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
return fmt.Errorf("--bundle-path flag requires a value")
|
||||||
|
}
|
||||||
|
bundlePath = args[i+1]
|
||||||
|
i += 2
|
||||||
|
default:
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no bundle path provided, auto-discover from dist folder
|
||||||
|
if bundlePath == "" {
|
||||||
|
manifest, err := LoadPluginManifestFromPath(pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedBundleName := fmt.Sprintf("%s-%s.tar.gz", manifest.Id, manifest.Version)
|
||||||
|
bundlePath = filepath.Join(pluginPath, "dist", expectedBundleName)
|
||||||
|
|
||||||
|
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("bundle not found at %s - run 'make bundle' to build the plugin first", bundlePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate bundle file exists
|
||||||
|
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("bundle file not found: %s", bundlePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load manifest to get plugin ID
|
||||||
|
manifest, err := LoadPluginManifestFromPath(pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginID := manifest.Id
|
||||||
|
if pluginID == "" {
|
||||||
|
return fmt.Errorf("plugin ID not found in manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), commandTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := getClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployPlugin(ctx, client, pluginID, bundlePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deployPlugin(ctx context.Context, client *model.Client4, pluginID, bundlePath string) error {
|
||||||
|
pluginBundle, err := os.Open(bundlePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open bundle file %s: %w", bundlePath, err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := pluginBundle.Close(); closeErr != nil {
|
||||||
|
Logger.Error("Failed to close plugin bundle", "error", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
Logger.Info("Uploading plugin bundle", "bundle_path", bundlePath)
|
||||||
|
_, _, err = client.UploadPluginForced(ctx, pluginBundle)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload plugin bundle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Enabling plugin", "plugin_id", pluginID)
|
||||||
|
_, err = client.EnablePlugin(ctx, pluginID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to enable plugin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Plugin deployed successfully", "plugin_id", pluginID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package pluginctl
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +12,7 @@ func RunDisableCommand(args []string, pluginPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func disablePlugin(ctx context.Context, client *model.Client4, pluginID string) error {
|
func disablePlugin(ctx context.Context, client *model.Client4, pluginID string) error {
|
||||||
log.Print("Disabling plugin.")
|
Logger.Info("Disabling plugin")
|
||||||
_, err := client.DisablePlugin(ctx, pluginID)
|
_, err := client.DisablePlugin(ctx, pluginID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to disable plugin: %w", err)
|
return fmt.Errorf("failed to disable plugin: %w", err)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package pluginctl
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +12,7 @@ func RunEnableCommand(args []string, pluginPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func enablePlugin(ctx context.Context, client *model.Client4, pluginID string) error {
|
func enablePlugin(ctx context.Context, client *model.Client4, pluginID string) error {
|
||||||
log.Print("Enabling plugin.")
|
Logger.Info("Enabling plugin")
|
||||||
_, err := client.EnablePlugin(ctx, pluginID)
|
_, err := client.EnablePlugin(ctx, pluginID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to enable plugin: %w", err)
|
return fmt.Errorf("failed to enable plugin: %w", err)
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -2,7 +2,10 @@ module github.com/mattermost/pluginctl
|
||||||
|
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require github.com/mattermost/mattermost/server/public v0.1.15
|
require (
|
||||||
|
github.com/lmittmann/tint v1.1.2
|
||||||
|
github.com/mattermost/mattermost/server/public v0.1.15
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -870,6 +870,8 @@ github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84Yrj
|
||||||
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
|
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
|
||||||
github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666 h1:ndfLOJNaxu0fX358UKxtq2bU8IMASWi87Hn0Nv/TIoY=
|
github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666 h1:ndfLOJNaxu0fX358UKxtq2bU8IMASWi87Hn0Nv/TIoY=
|
||||||
github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666/go.mod h1:WGXwLq/jKt0kng727wv6a0h0q7TVC+MwS2S75rcqL+4=
|
github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666/go.mod h1:WGXwLq/jKt0kng727wv6a0h0q7TVC+MwS2S75rcqL+4=
|
||||||
|
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
|
||||||
|
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||||
|
|
127
info.go
127
info.go
|
@ -19,68 +19,79 @@ func InfoCommand() error {
|
||||||
|
|
||||||
// PrintPluginInfo displays formatted plugin information.
|
// PrintPluginInfo displays formatted plugin information.
|
||||||
func PrintPluginInfo(manifest *model.Manifest) error {
|
func PrintPluginInfo(manifest *model.Manifest) error {
|
||||||
fmt.Printf("Plugin Information:\n")
|
printBasicInfo(manifest)
|
||||||
fmt.Printf("==================\n\n")
|
printCodeComponents(manifest)
|
||||||
|
printSettingsSchema(manifest)
|
||||||
// Basic plugin info
|
|
||||||
fmt.Printf("ID: %s\n", manifest.Id)
|
|
||||||
fmt.Printf("Name: %s\n", manifest.Name)
|
|
||||||
fmt.Printf("Version: %s\n", manifest.Version)
|
|
||||||
|
|
||||||
// Minimum Mattermost version
|
|
||||||
if manifest.MinServerVersion != "" {
|
|
||||||
fmt.Printf("Min MM Version: %s\n", manifest.MinServerVersion)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Min MM Version: Not specified\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description if available
|
|
||||||
if manifest.Description != "" {
|
|
||||||
fmt.Printf("Description: %s\n", manifest.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nCode Components:\n")
|
|
||||||
fmt.Printf("================\n")
|
|
||||||
|
|
||||||
// Server code presence
|
|
||||||
if HasServerCode(manifest) {
|
|
||||||
fmt.Printf("Server Code: Yes\n")
|
|
||||||
if manifest.Server != nil && len(manifest.Server.Executables) > 0 {
|
|
||||||
fmt.Printf(" Executables: ")
|
|
||||||
first := true
|
|
||||||
for platform := range manifest.Server.Executables {
|
|
||||||
if !first {
|
|
||||||
fmt.Printf(", ")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s", platform)
|
|
||||||
first = false
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Server Code: No\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webapp code presence
|
|
||||||
if HasWebappCode(manifest) {
|
|
||||||
fmt.Printf("Webapp Code: Yes\n")
|
|
||||||
if manifest.Webapp != nil && manifest.Webapp.BundlePath != "" {
|
|
||||||
fmt.Printf(" Bundle Path: %s\n", manifest.Webapp.BundlePath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Webapp Code: No\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings schema
|
|
||||||
if manifest.SettingsSchema != nil {
|
|
||||||
fmt.Printf("Settings Schema: Yes\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Settings Schema: No\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printBasicInfo prints basic plugin information.
|
||||||
|
func printBasicInfo(manifest *model.Manifest) {
|
||||||
|
Logger.Info("Plugin Information:")
|
||||||
|
Logger.Info("==================")
|
||||||
|
|
||||||
|
Logger.Info("ID:", "value", manifest.Id)
|
||||||
|
Logger.Info("Name:", "value", manifest.Name)
|
||||||
|
Logger.Info("Version:", "value", manifest.Version)
|
||||||
|
|
||||||
|
minVersion := manifest.MinServerVersion
|
||||||
|
if minVersion == "" {
|
||||||
|
minVersion = "Not specified"
|
||||||
|
}
|
||||||
|
Logger.Info("Min MM Version:", "value", minVersion)
|
||||||
|
|
||||||
|
if manifest.Description != "" {
|
||||||
|
Logger.Info("Description:", "value", manifest.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printCodeComponents prints information about server and webapp code.
|
||||||
|
func printCodeComponents(manifest *model.Manifest) {
|
||||||
|
Logger.Info("Code Components:")
|
||||||
|
Logger.Info("================")
|
||||||
|
|
||||||
|
printServerCodeInfo(manifest)
|
||||||
|
printWebappCodeInfo(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printServerCodeInfo prints server code information.
|
||||||
|
func printServerCodeInfo(manifest *model.Manifest) {
|
||||||
|
if HasServerCode(manifest) {
|
||||||
|
Logger.Info("Server Code:", "value", "Yes")
|
||||||
|
if manifest.Server != nil && len(manifest.Server.Executables) > 0 {
|
||||||
|
var executables []string
|
||||||
|
for platform := range manifest.Server.Executables {
|
||||||
|
executables = append(executables, platform)
|
||||||
|
}
|
||||||
|
Logger.Info("Executables:", "platforms", executables)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.Info("Server Code:", "value", "No")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printWebappCodeInfo prints webapp code information.
|
||||||
|
func printWebappCodeInfo(manifest *model.Manifest) {
|
||||||
|
if HasWebappCode(manifest) {
|
||||||
|
Logger.Info("Webapp Code:", "value", "Yes")
|
||||||
|
if manifest.Webapp != nil && manifest.Webapp.BundlePath != "" {
|
||||||
|
Logger.Info("Bundle Path:", "value", manifest.Webapp.BundlePath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.Info("Webapp Code:", "value", "No")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printSettingsSchema prints settings schema information.
|
||||||
|
func printSettingsSchema(manifest *model.Manifest) {
|
||||||
|
value := "No"
|
||||||
|
if manifest.SettingsSchema != nil {
|
||||||
|
value = "Yes"
|
||||||
|
}
|
||||||
|
Logger.Info("Settings Schema:", "value", value)
|
||||||
|
}
|
||||||
|
|
||||||
// InfoCommandWithPath implements the 'info' command with a custom path.
|
// InfoCommandWithPath implements the 'info' command with a custom path.
|
||||||
func InfoCommandWithPath(path string) error {
|
func InfoCommandWithPath(path string) error {
|
||||||
manifest, err := LoadPluginManifestFromPath(path)
|
manifest, err := LoadPluginManifestFromPath(path)
|
||||||
|
|
|
@ -125,6 +125,9 @@ func TestPrintPluginInfo(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Initialize logger for testing
|
||||||
|
InitLogger()
|
||||||
|
|
||||||
// Capture stdout
|
// Capture stdout
|
||||||
oldStdout := os.Stdout
|
oldStdout := os.Stdout
|
||||||
r, w, _ := os.Pipe()
|
r, w, _ := os.Pipe()
|
||||||
|
|
37
logger.go
Normal file
37
logger.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package pluginctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lmittmann/tint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is the global logger instance.
|
||||||
|
var Logger *slog.Logger
|
||||||
|
|
||||||
|
// InitLogger initializes the global logger.
|
||||||
|
func InitLogger() {
|
||||||
|
// Create a tint handler for colorized output
|
||||||
|
handler := tint.NewHandler(os.Stderr, &tint.Options{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
TimeFormat: time.Kitchen,
|
||||||
|
AddSource: false,
|
||||||
|
NoColor: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger = slog.New(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogLevel sets the minimum logging level.
|
||||||
|
func SetLogLevel(level slog.Level) {
|
||||||
|
handler := tint.NewHandler(os.Stderr, &tint.Options{
|
||||||
|
Level: level,
|
||||||
|
TimeFormat: time.Kitchen,
|
||||||
|
AddSource: false,
|
||||||
|
NoColor: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger = slog.New(handler)
|
||||||
|
}
|
215
logs.go
Normal file
215
logs.go
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
package pluginctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logsPerPage = 100
|
||||||
|
defaultLogsPerPage = 500
|
||||||
|
timeStampFormat = "2006-01-02 15:04:05.000 Z07:00"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunLogsCommand executes the logs command with optional --watch flag.
|
||||||
|
func RunLogsCommand(args []string, pluginPath string) error {
|
||||||
|
// Check for --watch flag
|
||||||
|
watch := false
|
||||||
|
if len(args) > 0 && args[0] == "--watch" {
|
||||||
|
watch = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if watch {
|
||||||
|
return runPluginCommand(args, pluginPath, watchPluginLogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runPluginCommand(args, pluginPath, getPluginLogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPluginLogs fetches the latest 500 log entries from Mattermost,
|
||||||
|
// and prints only the ones related to the plugin to stdout.
|
||||||
|
func getPluginLogs(ctx context.Context, client *model.Client4, pluginID string) error {
|
||||||
|
Logger.Info("Getting plugin logs", "plugin_id", pluginID)
|
||||||
|
|
||||||
|
err := checkJSONLogsSetting(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := fetchLogs(ctx, client, LogsRequest{
|
||||||
|
Page: 0,
|
||||||
|
PerPage: defaultLogsPerPage,
|
||||||
|
PluginID: pluginID,
|
||||||
|
Since: time.Unix(0, 0),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch log entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
printLogEntries(logs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchPluginLogs fetches log entries from Mattermost and prints them continuously.
|
||||||
|
// It will return without an error when ctx is canceled.
|
||||||
|
func watchPluginLogs(ctx context.Context, client *model.Client4, pluginID string) error {
|
||||||
|
Logger.Info("Watching plugin logs", "plugin_id", pluginID)
|
||||||
|
|
||||||
|
err := checkJSONLogsSetting(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
var oldestEntry string
|
||||||
|
|
||||||
|
// Use context.WithoutCancel to keep watching even if parent context times out
|
||||||
|
watchCtx := context.WithoutCancel(ctx)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-watchCtx.Done():
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
var page int
|
||||||
|
for {
|
||||||
|
logs, err := fetchLogs(watchCtx, client, LogsRequest{
|
||||||
|
Page: page,
|
||||||
|
PerPage: logsPerPage,
|
||||||
|
PluginID: pluginID,
|
||||||
|
Since: now,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch log entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allNew bool
|
||||||
|
logs, oldestEntry, allNew = checkOldestEntry(logs, oldestEntry)
|
||||||
|
|
||||||
|
printLogEntries(logs)
|
||||||
|
|
||||||
|
if !allNew {
|
||||||
|
// No more logs to fetch
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOldestEntry checks if logs contains new log entries.
|
||||||
|
// It returns the filtered slice of log entries, the new oldest entry and whether or not all entries were new.
|
||||||
|
func checkOldestEntry(logs []string, oldest string) (filteredLogs []string, newOldest string, allNew bool) {
|
||||||
|
if len(logs) == 0 {
|
||||||
|
return nil, oldest, false
|
||||||
|
}
|
||||||
|
|
||||||
|
newOldestEntry := logs[len(logs)-1]
|
||||||
|
|
||||||
|
i := slices.Index(logs, oldest)
|
||||||
|
switch i {
|
||||||
|
case -1:
|
||||||
|
// Every log entry is new
|
||||||
|
return logs, newOldestEntry, true
|
||||||
|
case len(logs) - 1:
|
||||||
|
// No new log entries
|
||||||
|
return nil, oldest, false
|
||||||
|
default:
|
||||||
|
// Filter out oldest log entry
|
||||||
|
return logs[i+1:], newOldestEntry, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogsRequest contains parameters for fetching logs.
|
||||||
|
type LogsRequest struct {
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
PluginID string
|
||||||
|
Since time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchLogs fetches log entries from Mattermost
|
||||||
|
// and filters them based on pluginID and timestamp.
|
||||||
|
func fetchLogs(ctx context.Context, client *model.Client4, req LogsRequest) ([]string, error) {
|
||||||
|
logs, _, err := client.GetLogs(ctx, req.Page, req.PerPage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get logs from Mattermost: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err = filterLogEntries(logs, req.PluginID, req.Since)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to filter log entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterLogEntries filters a given slice of log entries by pluginID.
|
||||||
|
// It also filters out any entries which timestamps are older than since.
|
||||||
|
func filterLogEntries(logs []string, pluginID string, since time.Time) ([]string, error) {
|
||||||
|
type logEntry struct {
|
||||||
|
PluginID string `json:"plugin_id"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]string, 0)
|
||||||
|
|
||||||
|
for _, e := range logs {
|
||||||
|
var le logEntry
|
||||||
|
err := json.Unmarshal([]byte(e), &le)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal log entry into JSON: %w", err)
|
||||||
|
}
|
||||||
|
if le.PluginID != pluginID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let, err := time.Parse(timeStampFormat, le.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unknown timestamp format: %w", err)
|
||||||
|
}
|
||||||
|
if let.Before(since) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log entries returned by the API have a newline as prefix.
|
||||||
|
// Remove that to make printing consistent.
|
||||||
|
e = strings.TrimPrefix(e, "\n")
|
||||||
|
|
||||||
|
ret = append(ret, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// printLogEntries prints a slice of log entries to stdout.
|
||||||
|
func printLogEntries(entries []string) {
|
||||||
|
for _, e := range entries {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkJSONLogsSetting(ctx context.Context, client *model.Client4) error {
|
||||||
|
cfg, _, err := client.GetConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch config: %w", err)
|
||||||
|
}
|
||||||
|
if cfg.LogSettings.FileJson == nil || !*cfg.LogSettings.FileJson {
|
||||||
|
return errors.New("JSON output for file logs are disabled. " +
|
||||||
|
"Please enable LogSettings.FileJson via the configuration in Mattermost")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
57
manifest.go
Normal file
57
manifest.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package pluginctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunManifestCommand implements the 'manifest' command functionality with subcommands.
|
||||||
|
func RunManifestCommand(args []string, pluginPath string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("manifest command requires a subcommand: id, version, has_server, has_webapp, check")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 "id":
|
||||||
|
fmt.Println(manifest.Id)
|
||||||
|
case "version":
|
||||||
|
fmt.Println(manifest.Version)
|
||||||
|
case "has_server":
|
||||||
|
if HasServerCode(manifest) {
|
||||||
|
fmt.Println("true")
|
||||||
|
} else {
|
||||||
|
fmt.Println("false")
|
||||||
|
}
|
||||||
|
case "has_webapp":
|
||||||
|
if HasWebappCode(manifest) {
|
||||||
|
fmt.Println("true")
|
||||||
|
} else {
|
||||||
|
fmt.Println("false")
|
||||||
|
}
|
||||||
|
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 fmt.Errorf("unknown subcommand: %s. Available subcommands: id, version, has_server, has_webapp, check",
|
||||||
|
subcommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
36
plugin.go
36
plugin.go
|
@ -11,6 +11,11 @@ import (
|
||||||
|
|
||||||
const PluginManifestName = "plugin.json"
|
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.
|
// LoadPluginManifest loads and parses the plugin.json file from the current directory.
|
||||||
func LoadPluginManifest() (*model.Manifest, error) {
|
func LoadPluginManifest() (*model.Manifest, error) {
|
||||||
return LoadPluginManifestFromPath(".")
|
return LoadPluginManifestFromPath(".")
|
||||||
|
@ -78,3 +83,34 @@ func GetEffectivePluginPath(flagPath string) string {
|
||||||
|
|
||||||
return cwd
|
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
|
||||||
|
}
|
||||||
|
|
204
plugin_test.go
204
plugin_test.go
|
@ -402,3 +402,207 @@ func TestHasServerCodeAndWebappCode(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParsePluginCtlConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
manifest *model.Manifest
|
||||||
|
expectedConfig *PluginCtlConfig
|
||||||
|
expectError bool
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Manifest with valid pluginctl config",
|
||||||
|
manifest: &model.Manifest{
|
||||||
|
Props: map[string]interface{}{
|
||||||
|
"pluginctl": map[string]interface{}{
|
||||||
|
"ignore_assets": []string{"*.test.js", "build/", "temp/**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedConfig: &PluginCtlConfig{
|
||||||
|
IgnoreAssets: []string{"*.test.js", "build/", "temp/**"},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manifest with no props",
|
||||||
|
manifest: &model.Manifest{
|
||||||
|
Props: nil,
|
||||||
|
},
|
||||||
|
expectedConfig: &PluginCtlConfig{
|
||||||
|
IgnoreAssets: []string{},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manifest with empty props",
|
||||||
|
manifest: &model.Manifest{
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
expectedConfig: &PluginCtlConfig{
|
||||||
|
IgnoreAssets: []string{},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manifest with no pluginctl config",
|
||||||
|
manifest: &model.Manifest{
|
||||||
|
Props: map[string]interface{}{
|
||||||
|
"other": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedConfig: &PluginCtlConfig{
|
||||||
|
IgnoreAssets: []string{},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manifest with empty pluginctl config",
|
||||||
|
manifest: &model.Manifest{
|
||||||
|
Props: map[string]interface{}{
|
||||||
|
"pluginctl": map[string]interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedConfig: &PluginCtlConfig{
|
||||||
|
IgnoreAssets: []string{},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Manifest with invalid pluginctl config",
|
||||||
|
manifest: &model.Manifest{
|
||||||
|
Props: map[string]interface{}{
|
||||||
|
"pluginctl": "invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedConfig: nil,
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "failed to parse pluginctl config",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
config, err := ParsePluginCtlConfig(tt.manifest)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error but got nil")
|
||||||
|
} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
|
||||||
|
t.Errorf("Expected error to contain %q but got: %v", tt.errorContains, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
t.Error("Expected config but got nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.IgnoreAssets) != len(tt.expectedConfig.IgnoreAssets) {
|
||||||
|
t.Errorf("Expected %d ignore assets but got %d", len(tt.expectedConfig.IgnoreAssets), len(config.IgnoreAssets))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, expected := range tt.expectedConfig.IgnoreAssets {
|
||||||
|
if config.IgnoreAssets[i] != expected {
|
||||||
|
t.Errorf("Expected ignore asset %d to be %q but got %q", i, expected, config.IgnoreAssets[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPathIgnored(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
relativePath string
|
||||||
|
ignorePatterns []string
|
||||||
|
expectedIgnore bool
|
||||||
|
expectedPattern string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No ignore patterns",
|
||||||
|
relativePath: "webapp/dist/main.js",
|
||||||
|
ignorePatterns: []string{},
|
||||||
|
expectedIgnore: false,
|
||||||
|
expectedPattern: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Direct file match",
|
||||||
|
relativePath: "test.js",
|
||||||
|
ignorePatterns: []string{"*.js"},
|
||||||
|
expectedIgnore: true,
|
||||||
|
expectedPattern: "*.js",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory pattern with slash",
|
||||||
|
relativePath: "build/output.js",
|
||||||
|
ignorePatterns: []string{"build/"},
|
||||||
|
expectedIgnore: true,
|
||||||
|
expectedPattern: "build/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory pattern without slash",
|
||||||
|
relativePath: "build/output.js",
|
||||||
|
ignorePatterns: []string{"build"},
|
||||||
|
expectedIgnore: true,
|
||||||
|
expectedPattern: "build",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nested directory match",
|
||||||
|
relativePath: "webapp/dist/main.js",
|
||||||
|
ignorePatterns: []string{"dist"},
|
||||||
|
expectedIgnore: true,
|
||||||
|
expectedPattern: "dist",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple patterns - first match",
|
||||||
|
relativePath: "test.js",
|
||||||
|
ignorePatterns: []string{"*.js", "*.css"},
|
||||||
|
expectedIgnore: true,
|
||||||
|
expectedPattern: "*.js",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple patterns - second match",
|
||||||
|
relativePath: "style.css",
|
||||||
|
ignorePatterns: []string{"*.js", "*.css"},
|
||||||
|
expectedIgnore: true,
|
||||||
|
expectedPattern: "*.css",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No match",
|
||||||
|
relativePath: "README.md",
|
||||||
|
ignorePatterns: []string{"*.js", "*.css"},
|
||||||
|
expectedIgnore: false,
|
||||||
|
expectedPattern: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Complex path with match",
|
||||||
|
relativePath: "webapp/node_modules/package/file.js",
|
||||||
|
ignorePatterns: []string{"node_modules"},
|
||||||
|
expectedIgnore: true,
|
||||||
|
expectedPattern: "node_modules",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ignored, pattern := isPathIgnored(tt.relativePath, tt.ignorePatterns)
|
||||||
|
|
||||||
|
if ignored != tt.expectedIgnore {
|
||||||
|
t.Errorf("Expected ignore result %v but got %v", tt.expectedIgnore, ignored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pattern != tt.expectedPattern {
|
||||||
|
t.Errorf("Expected pattern %q but got %q", tt.expectedPattern, pattern)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
239
updateassets.go
239
updateassets.go
|
@ -10,89 +10,206 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets/**/*
|
//go:embed assets/*
|
||||||
var assetsFS embed.FS
|
var assetsFS embed.FS
|
||||||
|
|
||||||
|
const (
|
||||||
|
assetsPrefix = "assets/"
|
||||||
|
assetsPrefixLen = 7
|
||||||
|
directoryPermissions = 0o750
|
||||||
|
filePermissions = 0o600
|
||||||
|
)
|
||||||
|
|
||||||
func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return fmt.Errorf("updateassets command does not accept arguments")
|
return fmt.Errorf("updateassets command does not accept arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Updating assets in plugin directory: %s\n", pluginPath)
|
Logger.Info("Updating assets in plugin directory", "path", pluginPath)
|
||||||
|
|
||||||
// Load plugin manifest to check for webapp code
|
|
||||||
manifest, err := LoadPluginManifestFromPath(pluginPath)
|
manifest, err := LoadPluginManifestFromPath(pluginPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the plugin has webapp code according to manifest
|
pluginCtlConfig, err := ParsePluginCtlConfig(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse pluginctl config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
hasWebapp := HasWebappCode(manifest)
|
hasWebapp := HasWebappCode(manifest)
|
||||||
|
updatedCount := 0
|
||||||
|
|
||||||
// Counter for updated files
|
config := AssetProcessorConfig{
|
||||||
var updatedCount int
|
pluginPath: pluginPath,
|
||||||
|
hasWebapp: hasWebapp,
|
||||||
|
updatedCount: &updatedCount,
|
||||||
|
pluginCtlConfig: pluginCtlConfig,
|
||||||
|
}
|
||||||
|
|
||||||
// Walk through the embedded assets
|
|
||||||
err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error {
|
err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
return processAssetEntry(path, d, err, config)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip the root assets directory
|
|
||||||
if path == "assets" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the "assets/" prefix to get the relative path
|
|
||||||
relativePath := path[7:] // len("assets/") = 7
|
|
||||||
|
|
||||||
// Skip webapp assets if plugin doesn't have webapp code
|
|
||||||
if !hasWebapp && strings.HasPrefix(relativePath, "webapp") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath := filepath.Join(pluginPath, relativePath)
|
|
||||||
|
|
||||||
if d.IsDir() {
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
if err := os.MkdirAll(targetPath, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Read file content from embedded FS
|
|
||||||
content, err := assetsFS.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read embedded file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if target file exists and compare content
|
|
||||||
existingContent, err := os.ReadFile(targetPath)
|
|
||||||
if err == nil && bytes.Equal(existingContent, content) {
|
|
||||||
// File exists and content is identical, skip update
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create parent directory if it doesn't exist
|
|
||||||
parentDir := filepath.Dir(targetPath)
|
|
||||||
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create parent directory %s: %w", parentDir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write file to target location
|
|
||||||
if err := os.WriteFile(targetPath, content, 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Updated file: %s\n", relativePath)
|
|
||||||
updatedCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update assets: %w", err)
|
return fmt.Errorf("failed to update assets: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Assets updated successfully! (%d files updated)\n", updatedCount)
|
Logger.Info("Assets updated successfully!", "files_updated", updatedCount)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPathIgnored checks if a path matches any of the ignore patterns.
|
||||||
|
func isPathIgnored(relativePath string, ignorePatterns []string) (ignored bool, matchedPattern string) {
|
||||||
|
for _, pattern := range ignorePatterns {
|
||||||
|
// Direct file or path match
|
||||||
|
if matched, err := filepath.Match(pattern, relativePath); err == nil && matched {
|
||||||
|
return true, pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path starts with the pattern (for directory patterns)
|
||||||
|
if strings.HasSuffix(pattern, "/") && strings.HasPrefix(relativePath, pattern) {
|
||||||
|
return true, pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any parent directory matches the pattern
|
||||||
|
if matchesParentDirectory(relativePath, pattern) {
|
||||||
|
return true, pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any directory component matches the pattern
|
||||||
|
if matchesDirectoryComponent(relativePath, pattern) {
|
||||||
|
return true, pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesParentDirectory checks if any parent directory matches the pattern.
|
||||||
|
func matchesParentDirectory(relativePath, pattern string) bool {
|
||||||
|
dir := filepath.Dir(relativePath)
|
||||||
|
for dir != "." && dir != "/" {
|
||||||
|
if matched, err := filepath.Match(pattern, dir); err == nil && matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Also check direct string match for directory names
|
||||||
|
if filepath.Base(dir) == pattern {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
dir = filepath.Dir(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesDirectoryComponent checks if any directory component matches the pattern.
|
||||||
|
func matchesDirectoryComponent(relativePath, pattern string) bool {
|
||||||
|
parts := strings.Split(relativePath, "/")
|
||||||
|
for _, part := range parts {
|
||||||
|
if matched, err := filepath.Match(pattern, part); err == nil && matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssetProcessorConfig struct {
|
||||||
|
pluginPath string
|
||||||
|
hasWebapp bool
|
||||||
|
updatedCount *int
|
||||||
|
pluginCtlConfig *PluginCtlConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProcessorConfig) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "assets" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relativePath := path[assetsPrefixLen:]
|
||||||
|
|
||||||
|
if !config.hasWebapp && strings.HasPrefix(relativePath, "webapp") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if path is ignored by pluginctl config
|
||||||
|
if ignored, pattern := isPathIgnored(relativePath, config.pluginCtlConfig.IgnoreAssets); ignored {
|
||||||
|
Logger.Info("Skipping asset due to ignore pattern", "path", relativePath, "pattern", pattern)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(config.pluginPath, relativePath)
|
||||||
|
|
||||||
|
if d.IsDir() {
|
||||||
|
return createDirectory(targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return processAssetFile(path, targetPath, relativePath, config.updatedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAssetFile(embeddedPath, targetPath, relativePath string, updatedCount *int) error {
|
||||||
|
shouldUpdate, err := shouldUpdateFile(embeddedPath, targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldUpdate {
|
||||||
|
err = updateFile(embeddedPath, targetPath, relativePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*updatedCount)++
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDirectory(targetPath string) error {
|
||||||
|
if err := os.MkdirAll(targetPath, directoryPermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUpdateFile(embeddedPath, targetPath string) (bool, error) {
|
||||||
|
content, err := assetsFS.ReadFile(embeddedPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingContent, err := os.ReadFile(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
// File doesn't exist or other error, should update
|
||||||
|
return true, nil //nolint:nilerr
|
||||||
|
}
|
||||||
|
|
||||||
|
return !bytes.Equal(existingContent, content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFile(embeddedPath, targetPath, relativePath string) error {
|
||||||
|
content, err := assetsFS.ReadFile(embeddedPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentDir := filepath.Dir(targetPath)
|
||||||
|
if err := os.MkdirAll(parentDir, directoryPermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to create parent directory %s: %w", parentDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(targetPath, content, filePermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Updated file", "path", relativePath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
package pluginctl
|
package pluginctl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunVersionCommand implements the 'version' command functionality.
|
// RunVersionCommand implements the 'version' command functionality.
|
||||||
func RunVersionCommand(args []string) error {
|
func RunVersionCommand(args []string) error {
|
||||||
version := GetVersion()
|
version := GetVersion()
|
||||||
fmt.Printf("pluginctl version %s\n", version)
|
Logger.Info("pluginctl version", "version", version)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue