Initial implementation of pluginctl CLI tool
- Add comprehensive info command with plugin manifest parsing - Implement global --plugin-path flag and PLUGINCTL_PLUGIN_PATH env var - Add full test suite with fixtures for various plugin configurations - Set up build system with Makefile, goreleaser, and golangci-lint - Include development tools with pinned versions for reproducible builds 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
fd6e4a4513
21 changed files with 4949 additions and 0 deletions
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Build outputs
|
||||
dist/
|
||||
/pluginctl
|
||||
|
||||
# Test outputs
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
# Go build cache
|
||||
.cache/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Local environment files
|
||||
.env
|
||||
.env.local
|
||||
.claude
|
373
.golangci.yml
Normal file
373
.golangci.yml
Normal file
|
@ -0,0 +1,373 @@
|
|||
# .golangci.yml
|
||||
# golangci-lint configuration for pluginctl
|
||||
|
||||
# Options for analysis running
|
||||
run:
|
||||
# Timeout for analysis
|
||||
timeout: 5m
|
||||
# Exit code when at least one issue was found
|
||||
issues-exit-code: 1
|
||||
# Include test files
|
||||
tests: true
|
||||
# Go version to target
|
||||
go: "1.24"
|
||||
|
||||
# Output configuration
|
||||
output:
|
||||
# Format of output
|
||||
formats:
|
||||
- format: colored-line-number
|
||||
# Print lines of code with issue
|
||||
print-issued-lines: true
|
||||
# Print linter name in the end of issue text
|
||||
print-linter-name: true
|
||||
# Sort results by: filepath, line and column
|
||||
sort-results: true
|
||||
|
||||
# Linters configuration
|
||||
linters:
|
||||
# Disable all linters by default
|
||||
disable-all: true
|
||||
# Enable specific linters
|
||||
enable:
|
||||
# Enabled by default
|
||||
- errcheck # Check for unchecked errors
|
||||
- gosimple # Simplify code
|
||||
- govet # Examine Go source code and reports suspicious constructs
|
||||
- ineffassign # Detect ineffectual assignments
|
||||
- staticcheck # Advanced Go linter
|
||||
- unused # Check for unused constants, variables, functions and types
|
||||
|
||||
# Additional linters
|
||||
- asciicheck # Check for non-ASCII characters
|
||||
- bodyclose # Check HTTP response body is closed
|
||||
- contextcheck # Check context.Context is propagated
|
||||
- cyclop # Check cyclomatic complexity
|
||||
- dupl # Check for duplicate code
|
||||
- durationcheck # Check for two durations multiplied together
|
||||
- errorlint # Check for error wrapping
|
||||
- exhaustive # Check exhaustiveness of enum switch statements
|
||||
- copyloopvar # Check for pointers to enclosing loop variables
|
||||
- forcetypeassert # Find forced type assertions
|
||||
- funlen # Check function length
|
||||
- gci # Control Go package import order
|
||||
- gocognit # Check cognitive complexity
|
||||
- goconst # Find repeated strings that could be constants
|
||||
- gocritic # Various checks
|
||||
- gocyclo # Check cyclomatic complexity
|
||||
- godot # Check if comments end in a period
|
||||
- gofmt # Check if the code was gofmt-ed
|
||||
- goimports # Check if imports are sorted
|
||||
- mnd # Check for magic numbers
|
||||
- gomoddirectives # Check for //go:build directives
|
||||
- gomodguard # Check for blocked module dependencies
|
||||
- goprintffuncname # Check printf-like function names
|
||||
- gosec # Security checker
|
||||
- lll # Check line length
|
||||
- makezero # Find slice declarations that are not initialized with zero length
|
||||
- misspell # Find commonly misspelled English words
|
||||
- nakedret # Check for naked returns
|
||||
- nilerr # Check for nil errors
|
||||
- nlreturn # Check for new line before return
|
||||
- noctx # Check for HTTP requests without context
|
||||
- prealloc # Find slice declarations that could be preallocated
|
||||
- predeclared # Check for predeclared identifiers
|
||||
- revive # Fast, configurable, extensible linter
|
||||
- rowserrcheck # Check SQL rows.Err
|
||||
- sqlclosecheck # Check SQL Close() calls
|
||||
- stylecheck # Stylecheck is a replacement for golint
|
||||
- thelper # Check test helpers
|
||||
- tparallel # Check test parallelization
|
||||
- unconvert # Check for unnecessary type conversions
|
||||
- unparam # Check for unused function parameters
|
||||
- wastedassign # Check for wasted assignment statements
|
||||
- whitespace # Check for unnecessary whitespace
|
||||
|
||||
# Linters settings
|
||||
linters-settings:
|
||||
# Settings for cyclop
|
||||
cyclop:
|
||||
max-complexity: 15
|
||||
package-average: 0.0
|
||||
skip-tests: true
|
||||
|
||||
# Settings for dupl
|
||||
dupl:
|
||||
threshold: 100
|
||||
|
||||
# Settings for errorlint
|
||||
errorlint:
|
||||
errorf: true
|
||||
asserts: true
|
||||
comparison: true
|
||||
|
||||
# Settings for exhaustive
|
||||
exhaustive:
|
||||
check-generated: false
|
||||
default-signifies-exhaustive: false
|
||||
|
||||
# Settings for funlen
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 50
|
||||
|
||||
# Settings for gci
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/mattermost/pluginctl)
|
||||
- blank
|
||||
- dot
|
||||
|
||||
# Settings for gocognit
|
||||
gocognit:
|
||||
min-complexity: 15
|
||||
|
||||
# Settings for goconst
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
|
||||
# Settings for gocritic
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- experimental
|
||||
- opinionated
|
||||
- performance
|
||||
- style
|
||||
disabled-checks:
|
||||
- dupImport
|
||||
- ifElseChain
|
||||
- octalLiteral
|
||||
- whyNoLint
|
||||
- wrapperFunc
|
||||
|
||||
# Settings for gocyclo
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
# Settings for godot
|
||||
godot:
|
||||
scope: declarations
|
||||
capital: false
|
||||
|
||||
# Settings for gofmt
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
||||
# Settings for goimports
|
||||
goimports:
|
||||
local-prefixes: github.com/mattermost/pluginctl
|
||||
|
||||
# Settings for mnd
|
||||
mnd:
|
||||
checks: argument,case,condition,operation,return,assign
|
||||
ignored-numbers: 0,1,2,3
|
||||
ignored-functions: strings.SplitN
|
||||
|
||||
# Settings for govet
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
|
||||
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
|
||||
|
||||
# Settings for lll
|
||||
lll:
|
||||
line-length: 120
|
||||
|
||||
# Settings for misspell
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
# Settings for nakedret
|
||||
nakedret:
|
||||
max-func-lines: 30
|
||||
|
||||
# Settings for prealloc
|
||||
prealloc:
|
||||
simple: true
|
||||
range-loops: true
|
||||
for-loops: false
|
||||
|
||||
# Settings for revive
|
||||
revive:
|
||||
min-confidence: 0
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: line-length-limit
|
||||
arguments: [120]
|
||||
- name: argument-limit
|
||||
arguments: [4]
|
||||
- name: cyclomatic
|
||||
arguments: [15]
|
||||
- name: max-public-structs
|
||||
arguments: [3]
|
||||
- name: file-header
|
||||
disabled: true
|
||||
|
||||
# Settings for staticcheck
|
||||
staticcheck:
|
||||
checks: ["all"]
|
||||
|
||||
# Settings for stylecheck
|
||||
stylecheck:
|
||||
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
|
||||
dot-import-whitelist:
|
||||
- fmt
|
||||
initialisms: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS"]
|
||||
http-status-code-whitelist: ["200", "400", "404", "500"]
|
||||
|
||||
# Settings for unparam
|
||||
unparam:
|
||||
check-exported: false
|
||||
|
||||
# Issues configuration
|
||||
issues:
|
||||
# List of regexps of issue texts to exclude
|
||||
exclude:
|
||||
# Exclude some linters from running on tests files
|
||||
- "G104:" # Errors unhandled (gosec)
|
||||
- "G204:" # Subprocess launched with variable (gosec)
|
||||
- "G304:" # File path provided as taint input (gosec)
|
||||
|
||||
# Skip files
|
||||
exclude-files:
|
||||
- ".*_test.go"
|
||||
- ".*\\.pb\\.go"
|
||||
|
||||
# Skip directories
|
||||
exclude-dirs:
|
||||
- vendor
|
||||
- node_modules
|
||||
- .git
|
||||
|
||||
# Make issues output unique by line
|
||||
uniq-by-line: true
|
||||
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- mnd
|
||||
- goconst
|
||||
- funlen
|
||||
- lll
|
||||
- dupl
|
||||
- gosec
|
||||
- gocritic
|
||||
- cyclop
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- maintidx
|
||||
- bodyclose
|
||||
- noctx
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tparallel
|
||||
- unparam
|
||||
- wastedassign
|
||||
- prealloc
|
||||
- nlreturn
|
||||
- wsl
|
||||
- gofumpt
|
||||
- whitespace
|
||||
- errorlint
|
||||
- contextcheck
|
||||
- thelper
|
||||
- forcetypeassert
|
||||
- copyloopvar
|
||||
- paralleltest
|
||||
- godot
|
||||
- godox
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nlreturn
|
||||
- noctx
|
||||
- predeclared
|
||||
- revive
|
||||
- stylecheck
|
||||
- tagliatelle
|
||||
- unconvert
|
||||
- wrapcheck
|
||||
- wsl
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- durationcheck
|
||||
- exhaustive
|
||||
- forbidigo
|
||||
- gci
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- godox
|
||||
- goheader
|
||||
- golint
|
||||
- mnd
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- ifshort
|
||||
- importas
|
||||
- interfacer
|
||||
- ireturn
|
||||
- maligned
|
||||
- makezero
|
||||
- misspell
|
||||
- nlreturn
|
||||
- nolintlint
|
||||
- paralleltest
|
||||
- prealloc
|
||||
- promlinter
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- sqlclosecheck
|
||||
- stylecheck
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- varcheck
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- wrapcheck
|
||||
- wsl
|
||||
|
||||
# Exclude lll issues for long lines with go:generate
|
||||
- linters:
|
||||
- lll
|
||||
source: "^//go:generate "
|
||||
|
||||
# Independently of option `exclude` we use default exclude patterns
|
||||
exclude-use-default: false
|
||||
|
||||
# Maximum issues count per one linter
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text
|
||||
max-same-issues: 0
|
||||
|
||||
# Show only new issues
|
||||
new: false
|
||||
|
||||
# Show only new issues created after git revision
|
||||
new-from-rev: ""
|
||||
|
||||
# Show only new issues created in git patch with set file path
|
||||
new-from-patch: ""
|
||||
|
||||
# Fix found issues (if it's supported by the linter)
|
||||
fix: false
|
129
.goreleaser.yml
Normal file
129
.goreleaser.yml
Normal file
|
@ -0,0 +1,129 @@
|
|||
# .goreleaser.yml
|
||||
# GoReleaser configuration for pluginctl
|
||||
version: 2
|
||||
|
||||
# Project information
|
||||
project_name: pluginctl
|
||||
|
||||
# Before hooks
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
|
||||
# Build configuration
|
||||
builds:
|
||||
- id: pluginctl
|
||||
binary: pluginctl
|
||||
main: ./cmd/pluginctl
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
ldflags:
|
||||
- -s -w
|
||||
flags:
|
||||
- -trimpath
|
||||
|
||||
# Archive configuration
|
||||
archives:
|
||||
- id: pluginctl
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
|
||||
# Checksums
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
algorithm: sha256
|
||||
|
||||
# Snapshot configuration
|
||||
snapshot:
|
||||
version_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
# Changelog configuration
|
||||
changelog:
|
||||
sort: asc
|
||||
use: github
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^ci:"
|
||||
- "^build:"
|
||||
- "^style:"
|
||||
- "^refactor:"
|
||||
- "^chore:"
|
||||
- "merge conflict"
|
||||
- "Merge pull request"
|
||||
- "Merge remote-tracking branch"
|
||||
- "Merge branch"
|
||||
groups:
|
||||
- title: "New Features"
|
||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||
order: 0
|
||||
- title: "Bug Fixes"
|
||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: "Documentation"
|
||||
regexp: "^.*docs[(\\w)]*:+.*$"
|
||||
order: 2
|
||||
- title: "Other Changes"
|
||||
order: 999
|
||||
|
||||
# Release configuration
|
||||
release:
|
||||
github:
|
||||
owner: mattermost
|
||||
name: pluginctl
|
||||
draft: false
|
||||
prerelease: auto
|
||||
name_template: "v{{ .Version }}"
|
||||
header: |
|
||||
## pluginctl {{ .Version }}
|
||||
|
||||
Welcome to this new release of pluginctl! 🎉
|
||||
|
||||
### What's new
|
||||
footer: |
|
||||
## Installation
|
||||
|
||||
### Binary Installation
|
||||
Download the appropriate binary for your platform from the assets below.
|
||||
|
||||
### Package Managers
|
||||
```bash
|
||||
# Using go install
|
||||
go install github.com/mattermost/pluginctl/cmd/pluginctl@{{ .Tag }}
|
||||
```
|
||||
|
||||
### From Source
|
||||
```bash
|
||||
git clone https://github.com/mattermost/pluginctl.git
|
||||
cd pluginctl
|
||||
make install
|
||||
```
|
||||
|
||||
**Full Changelog**: https://github.com/mattermost/pluginctl/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||
|
||||
# Announce configuration (optional)
|
||||
announce:
|
||||
slack:
|
||||
enabled: false
|
||||
discord:
|
||||
enabled: false
|
257
CLAUDE.md
Normal file
257
CLAUDE.md
Normal file
|
@ -0,0 +1,257 @@
|
|||
# pluginctl - Mattermost Plugin Development CLI
|
||||
|
||||
## Project Overview
|
||||
`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
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
pluginctl/
|
||||
├── cmd/pluginctl/main.go # CLI entrypoint with command routing
|
||||
├── 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
|
||||
|
||||
#### 1. **Separation of Concerns**
|
||||
- **CLI Framework**: `cmd/pluginctl/main.go` handles argument parsing, command routing, and error handling
|
||||
- **Command Implementation**: Each command gets its own file (e.g., `info.go`, `build.go`, `deploy.go`)
|
||||
- **Utility Functions**: Common plugin operations in `plugin.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 `main.go`
|
||||
- **Command functions**: Name pattern: `run[Command]Command(args []string) error`
|
||||
- **Error handling**: Return descriptive errors, let main.go handle exit codes
|
||||
|
||||
#### 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
|
||||
|
||||
#### Priority Order
|
||||
1. **Command-line flag**: `--plugin-path /path/to/plugin`
|
||||
2. **Environment variable**: `PLUGINCTL_PLUGIN_PATH=/path/to/plugin`
|
||||
3. **Current directory**: Default fallback
|
||||
|
||||
#### Implementation
|
||||
- `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
|
||||
|
||||
#### Required Checks
|
||||
- Plugin.json file must exist
|
||||
- Plugin.json must be valid JSON
|
||||
- Plugin.json must conform to Mattermost manifest schema
|
||||
|
||||
#### Utility Functions (plugin.go)
|
||||
- `LoadPluginManifest()` - Load from current directory
|
||||
- `LoadPluginManifestFromPath(path)` - Load from specific path
|
||||
- `HasServerCode(manifest)` - Check for server-side code
|
||||
- `HasWebappCode(manifest)` - Check for webapp code
|
||||
- `IsValidPluginDirectory()` - Validate current directory
|
||||
|
||||
### Future Command Ideas
|
||||
- `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
|
||||
- Current version: 0.1.0
|
||||
- Update version in `main.go` when releasing
|
||||
- Follow semantic versioning
|
||||
|
||||
### Documentation Maintenance
|
||||
- **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
|
||||
- Always maintain the separation between CLI framework and command implementation
|
||||
- Use the official Mattermost model types - never create custom manifest structs
|
||||
- Keep command implementations in separate files for maintainability
|
||||
- Always validate plugin.json before performing operations
|
||||
- Test new commands with the sample plugin.json file
|
||||
- Follow the established error handling patterns
|
||||
- Use the build system: `make check-changes` before any commits
|
||||
- Use pinned tool versions for reproducible development environments
|
201
LICENSE
Normal file
201
LICENSE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
159
Makefile
Normal file
159
Makefile
Normal file
|
@ -0,0 +1,159 @@
|
|||
# pluginctl Makefile
|
||||
# Based on common Go project patterns
|
||||
|
||||
# Build information
|
||||
VERSION ?= $(shell git describe --tags --always --dirty)
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Go build variables
|
||||
GO_VERSION ?= $(shell awk '/^go / {print $$2}' go.mod)
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
|
||||
# Tool versions
|
||||
GOLANGCI_LINT_VERSION ?= v1.62.2
|
||||
GORELEASER_VERSION ?= v2.10.2
|
||||
|
||||
# Project variables
|
||||
BINARY_NAME = pluginctl
|
||||
MAIN_PACKAGE = ./cmd/pluginctl
|
||||
DIST_DIR = dist
|
||||
|
||||
# Build flags
|
||||
LDFLAGS = -ldflags="-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(BUILD_DATE)"
|
||||
|
||||
# Default target
|
||||
.PHONY: all
|
||||
all: clean lint test build
|
||||
|
||||
# Help target
|
||||
.PHONY: help
|
||||
help: ## Show this help message
|
||||
@echo "Available targets:"
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
# Clean build artifacts
|
||||
.PHONY: clean
|
||||
clean: ## Clean build artifacts
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf $(DIST_DIR)
|
||||
@rm -f $(BINARY_NAME)
|
||||
@go clean -cache
|
||||
|
||||
# Install dependencies
|
||||
.PHONY: deps
|
||||
deps: ## Install/update dependencies
|
||||
@echo "Installing dependencies..."
|
||||
@go mod download
|
||||
@go mod tidy
|
||||
|
||||
# Run tests
|
||||
.PHONY: test
|
||||
test: ## Run tests
|
||||
@echo "Running tests..."
|
||||
@go test -v ./...
|
||||
|
||||
# Run tests with coverage
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## Run tests with coverage
|
||||
@echo "Running tests with coverage..."
|
||||
@go test -v -coverprofile=coverage.out ./...
|
||||
@go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "Coverage report generated: coverage.html"
|
||||
|
||||
# Lint code
|
||||
.PHONY: lint
|
||||
lint: ## Run linter
|
||||
@echo "Running linter..."
|
||||
@golangci-lint run
|
||||
|
||||
# Fix linting issues
|
||||
.PHONY: lint-fix
|
||||
lint-fix: ## Fix linting issues
|
||||
@echo "Fixing linting issues..."
|
||||
@golangci-lint run --fix
|
||||
|
||||
# Build for all platforms using goreleaser
|
||||
.PHONY: build-all
|
||||
build: clean ## Build for all platforms using goreleaser
|
||||
@echo "Building for all platforms using goreleaser..."
|
||||
@goreleaser build --clean
|
||||
|
||||
# Install binary
|
||||
.PHONY: install
|
||||
install:
|
||||
@echo "Installing $(BINARY_NAME)..."
|
||||
@go install $(LDFLAGS) $(MAIN_PACKAGE)
|
||||
|
||||
# Run the application
|
||||
.PHONY: run
|
||||
run: ## Run the application
|
||||
@go run $(MAIN_PACKAGE) $(ARGS)
|
||||
|
||||
# Format code
|
||||
.PHONY: fmt
|
||||
fmt: ## Format code
|
||||
@echo "Formatting code..."
|
||||
@go fmt ./...
|
||||
|
||||
# Generate code
|
||||
.PHONY: generate
|
||||
generate: ## Generate code
|
||||
@echo "Generating code..."
|
||||
@go generate ./...
|
||||
|
||||
# Check for updates
|
||||
.PHONY: check-updates
|
||||
check-updates: ## Check for dependency updates
|
||||
@echo "Checking for dependency updates..."
|
||||
@go list -u -m all
|
||||
|
||||
# Release (requires goreleaser)
|
||||
.PHONY: release
|
||||
release: ## Create a release
|
||||
@echo "Creating release..."
|
||||
@goreleaser release --clean
|
||||
|
||||
# Snapshot release (for testing)
|
||||
.PHONY: snapshot
|
||||
snapshot: ## Create a snapshot release
|
||||
@echo "Creating snapshot release..."
|
||||
@goreleaser release --snapshot --clean
|
||||
|
||||
# Development setup
|
||||
.PHONY: dev-setup
|
||||
dev-setup: ## Set up development environment
|
||||
@echo "Setting up development environment..."
|
||||
@go get -tool github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
@go get -tool github.com/goreleaser/goreleaser/v2@$(GORELEASER_VERSION)
|
||||
@echo "Development tools installed"
|
||||
|
||||
# Verify build
|
||||
.PHONY: verify
|
||||
verify: clean lint test build ## Verify build (clean, lint, test, build)
|
||||
@echo "Build verification complete"
|
||||
|
||||
# Quick development build
|
||||
.PHONY: dev
|
||||
dev: fmt lint build ## Quick development build (fmt, lint, build)
|
||||
|
||||
# Check changes target
|
||||
.PHONY: check-changes
|
||||
check-changes: lint test ## Check changes (lint, test)
|
||||
|
||||
# CI target
|
||||
.PHONY: ci
|
||||
ci: deps verify ## CI target (deps, verify)
|
||||
|
||||
# Print build info
|
||||
.PHONY: version
|
||||
version: ## Print version information
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Commit: $(COMMIT)"
|
||||
@echo "Build Date: $(BUILD_DATE)"
|
||||
@echo "Go Version: $(GO_VERSION)"
|
||||
@echo "OS/Arch: $(GOOS)/$(GOARCH)"
|
||||
@echo "Tool Versions:"
|
||||
@echo " golangci-lint: $(GOLANGCI_LINT_VERSION)"
|
||||
@echo " goreleaser: $(GORELEASER_VERSION)"
|
283
README.md
Normal file
283
README.md
Normal file
|
@ -0,0 +1,283 @@
|
|||
# pluginctl
|
||||
|
||||
A command-line interface tool for Mattermost plugin development. `pluginctl` provides utilities to manage, inspect, and work with Mattermost plugins from the command line.
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mattermost/pluginctl.git
|
||||
cd pluginctl
|
||||
make build
|
||||
```
|
||||
|
||||
### Binary Installation
|
||||
|
||||
Download the latest binary from the [releases page](https://github.com/mattermost/pluginctl/releases).
|
||||
|
||||
### Package Managers
|
||||
|
||||
```bash
|
||||
# Using go install
|
||||
go install github.com/mattermost/pluginctl/cmd/pluginctl@latest
|
||||
|
||||
# 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
|
||||
|
||||
### Basic Commands
|
||||
|
||||
```bash
|
||||
# Display plugin information
|
||||
pluginctl info
|
||||
|
||||
# Show help
|
||||
pluginctl --help
|
||||
|
||||
# Show version
|
||||
pluginctl --version
|
||||
```
|
||||
|
||||
### Plugin Path Configuration
|
||||
|
||||
`pluginctl` supports multiple ways to specify the plugin directory:
|
||||
|
||||
1. **Command-line flag** (highest priority):
|
||||
|
||||
```bash
|
||||
pluginctl --plugin-path /path/to/plugin info
|
||||
```
|
||||
|
||||
2. **Environment variable**:
|
||||
|
||||
```bash
|
||||
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
||||
pluginctl info
|
||||
```
|
||||
|
||||
3. **Current directory** (default):
|
||||
```bash
|
||||
cd /path/to/plugin
|
||||
pluginctl info
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
#### `info`
|
||||
|
||||
Display comprehensive information about a Mattermost plugin:
|
||||
|
||||
```bash
|
||||
pluginctl info
|
||||
```
|
||||
|
||||
**Output includes:**
|
||||
|
||||
- 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)
|
||||
make dev
|
||||
|
||||
# Check all changes before committing (lint, security, test)
|
||||
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
|
||||
|
||||
**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
|
||||
|
||||
1. Create a new command file (e.g., `build.go`)
|
||||
2. Implement the command following the patterns in `info.go`
|
||||
3. Register the command in `cmd/pluginctl/main.go`
|
||||
4. Update the help text and documentation
|
||||
|
||||
See [CLAUDE.md](CLAUDE.md) for detailed architecture guidelines.
|
||||
|
||||
### Code Style
|
||||
|
||||
- Follow Go best practices and conventions
|
||||
- Use the official Mattermost model types from `github.com/mattermost/mattermost/server/public/model`
|
||||
- Maintain separation between CLI framework and command implementation
|
||||
- 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
|
||||
|
||||
This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [Mattermost Plugin Developer Documentation](https://developers.mattermost.com/integrate/plugins/)
|
||||
- [Mattermost Plugin Starter Template](https://github.com/mattermost/mattermost-plugin-starter-template)
|
||||
- [Mattermost Server](https://github.com/mattermost/mattermost-server)
|
||||
|
||||
## Support
|
||||
|
||||
For questions, issues, or feature requests, please:
|
||||
|
||||
1. Check the [issues](https://github.com/mattermost/pluginctl/issues) page
|
||||
2. Create a new issue if your problem isn't already reported
|
||||
3. Join the [Mattermost Community](https://community.mattermost.com/) for general discussion
|
244
cmd/pluginctl/main.go
Normal file
244
cmd/pluginctl/main.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
ExitSuccess = 0
|
||||
ExitError = 1
|
||||
EnvPluginPath = "PLUGINCTL_PLUGIN_PATH"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var pluginPath string
|
||||
|
||||
flag.StringVar(&pluginPath, "plugin-path", "", "Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)")
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Error: No command specified\n\n")
|
||||
showUsage()
|
||||
os.Exit(ExitError)
|
||||
}
|
||||
|
||||
command := args[0]
|
||||
commandArgs := args[1:]
|
||||
|
||||
// Determine plugin path from flag, environment variable, or current directory
|
||||
effectivePluginPath := getEffectivePluginPath(pluginPath)
|
||||
|
||||
if err := runCommand(command, commandArgs, effectivePluginPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(ExitError)
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(command string, args []string, pluginPath string) error {
|
||||
switch command {
|
||||
case "info":
|
||||
return runInfoCommand(args, pluginPath)
|
||||
case "help":
|
||||
showUsage()
|
||||
|
||||
return nil
|
||||
case "version":
|
||||
return runVersionCommand(args)
|
||||
default:
|
||||
return fmt.Errorf("unknown command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
func runInfoCommand(args []string, pluginPath string) error {
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
return infoCommandWithPath(absPath)
|
||||
}
|
||||
|
||||
func runVersionCommand(args []string) error {
|
||||
version := getVersion()
|
||||
fmt.Printf("pluginctl version %s\n", version)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVersion returns the version information from build info.
|
||||
func getVersion() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// First try to get version from main module
|
||||
if info.Main.Version != "" && info.Main.Version != "(devel)" {
|
||||
return info.Main.Version
|
||||
}
|
||||
|
||||
// Look for version in build settings (set by goreleaser)
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" {
|
||||
// Return short commit hash if no version tag
|
||||
if len(setting.Value) >= 7 {
|
||||
return setting.Value[:7]
|
||||
}
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
|
||||
return "dev"
|
||||
}
|
||||
|
||||
// getEffectivePluginPath determines the plugin path from flag, environment variable, or current directory.
|
||||
func getEffectivePluginPath(flagPath string) string {
|
||||
// Priority: 1. Command line flag, 2. Environment variable, 3. Current directory
|
||||
if flagPath != "" {
|
||||
return flagPath
|
||||
}
|
||||
|
||||
if envPath := os.Getenv(EnvPluginPath); envPath != "" {
|
||||
return envPath
|
||||
}
|
||||
|
||||
// Default to current directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
|
||||
return cwd
|
||||
}
|
||||
|
||||
func infoCommandWithPath(path string) error {
|
||||
manifest, err := loadPluginManifestFromPath(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load plugin manifest from %s: %w", path, err)
|
||||
}
|
||||
|
||||
return printPluginInfo(manifest)
|
||||
}
|
||||
|
||||
func loadPluginManifestFromPath(dir string) (*model.Manifest, error) {
|
||||
manifestPath := filepath.Join(dir, "plugin.json")
|
||||
|
||||
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("plugin.json not found in directory %s", dir)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plugin.json: %w", err)
|
||||
}
|
||||
|
||||
var manifest model.Manifest
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse plugin.json: %w", err)
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
func printPluginInfo(manifest *model.Manifest) error {
|
||||
fmt.Printf("Plugin Information:\n")
|
||||
fmt.Printf("==================\n\n")
|
||||
|
||||
fmt.Printf("ID: %s\n", manifest.Id)
|
||||
fmt.Printf("Name: %s\n", manifest.Name)
|
||||
fmt.Printf("Version: %s\n", manifest.Version)
|
||||
|
||||
if manifest.MinServerVersion != "" {
|
||||
fmt.Printf("Min MM Version: %s\n", manifest.MinServerVersion)
|
||||
} else {
|
||||
fmt.Printf("Min MM Version: Not specified\n")
|
||||
}
|
||||
|
||||
if manifest.Description != "" {
|
||||
fmt.Printf("Description: %s\n", manifest.Description)
|
||||
}
|
||||
|
||||
fmt.Printf("\nCode Components:\n")
|
||||
fmt.Printf("================\n")
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if manifest.SettingsSchema != nil {
|
||||
fmt.Printf("Settings Schema: Yes\n")
|
||||
} else {
|
||||
fmt.Printf("Settings Schema: No\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasServerCode(manifest *model.Manifest) bool {
|
||||
return manifest.Server != nil && len(manifest.Server.Executables) > 0
|
||||
}
|
||||
|
||||
func hasWebappCode(manifest *model.Manifest) bool {
|
||||
return manifest.Webapp != nil && manifest.Webapp.BundlePath != ""
|
||||
}
|
||||
|
||||
func showUsage() {
|
||||
fmt.Printf(`pluginctl - Mattermost Plugin Development CLI
|
||||
|
||||
Usage:
|
||||
pluginctl [global options] <command> [command options] [arguments...]
|
||||
|
||||
Global Options:
|
||||
--plugin-path PATH Path to plugin directory (overrides PLUGINCTL_PLUGIN_PATH)
|
||||
|
||||
Commands:
|
||||
info Display plugin information
|
||||
help Show this help message
|
||||
version Show version information
|
||||
|
||||
Examples:
|
||||
pluginctl info # Show info for plugin in current directory
|
||||
pluginctl --plugin-path /path/to/plugin info # Show info for plugin at specific path
|
||||
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
||||
pluginctl info # Show info using environment variable
|
||||
pluginctl version # Show version information
|
||||
|
||||
Environment Variables:
|
||||
PLUGINCTL_PLUGIN_PATH Default plugin directory path
|
||||
|
||||
For more information about Mattermost plugin development, visit:
|
||||
https://developers.mattermost.com/integrate/plugins/
|
||||
`)
|
||||
}
|
512
go.mod
Normal file
512
go.mod
Normal file
|
@ -0,0 +1,512 @@
|
|||
module github.com/mattermost/pluginctl
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require github.com/mattermost/mattermost/server/public v0.1.15
|
||||
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.1 // indirect
|
||||
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||
cel.dev/expr v0.22.1 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
cloud.google.com/go/auth v0.15.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/iam v1.4.2 // indirect
|
||||
cloud.google.com/go/kms v1.21.1 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.6 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.1 // indirect
|
||||
cloud.google.com/go/storage v1.51.0 // indirect
|
||||
code.gitea.io/sdk/gitea v0.21.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/42wim/httpsig v1.2.2 // indirect
|
||||
github.com/4meepo/tagalign v1.3.4 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.3 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/Antonboom/errname v1.0.0 // indirect
|
||||
github.com/Antonboom/nilnil v1.0.0 // indirect
|
||||
github.com/Antonboom/testifylint v1.5.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.30 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.2 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/Crocmagnon/fatcontext v0.5.3 // indirect
|
||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
|
||||
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.2.0 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.5 // indirect
|
||||
github.com/alexkohler/prealloc v1.0.0 // indirect
|
||||
github.com/alingse/asasalint v0.0.11 // indirect
|
||||
github.com/anchore/bubbly v0.0.0-20241107060245-f2a5536f366a // indirect
|
||||
github.com/anchore/go-logger v0.0.0-20241005132348-65b4486fbb28 // indirect
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect
|
||||
github.com/anchore/quill v0.5.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/ashanbrown/forbidigo v1.6.0 // indirect
|
||||
github.com/ashanbrown/makezero v1.1.1 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.32.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.9.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||
github.com/blacktop/go-dwarf v1.0.10 // indirect
|
||||
github.com/blacktop/go-macho v1.1.238 // indirect
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||
github.com/bluesky-social/indigo v0.0.0-20240813042137-4006c0eca043 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.4.1 // indirect
|
||||
github.com/breml/bidichk v0.3.2 // indirect
|
||||
github.com/breml/errchkjson v0.4.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/butuzov/ireturn v0.3.0 // indirect
|
||||
github.com/butuzov/mirror v1.2.0 // indirect
|
||||
github.com/caarlos0/ctrlc v1.2.0 // indirect
|
||||
github.com/caarlos0/env/v11 v11.3.1 // indirect
|
||||
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
|
||||
github.com/caarlos0/go-shellwords v1.0.12 // indirect
|
||||
github.com/caarlos0/go-version v0.2.0 // indirect
|
||||
github.com/caarlos0/log v0.4.8 // indirect
|
||||
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
|
||||
github.com/catenacyber/perfsprint v0.7.1 // indirect
|
||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charithe/durationcheck v0.0.10 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/chavacava/garif v0.1.0 // indirect
|
||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect
|
||||
github.com/ckaznocha/intrange v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/curioswitch/go-reassign v0.2.0 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/daixiang0/gci v0.13.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||
github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dghubble/sling v1.4.0 // indirect
|
||||
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.1.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
|
||||
github.com/elliotchance/orderedmap/v2 v2.7.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.5 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.8 // indirect
|
||||
github.com/github/smimesign v0.2.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-critic/go-critic v0.11.5 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.14.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/loads v0.22.0 // indirect
|
||||
github.com/go-openapi/runtime v0.28.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.0 // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect
|
||||
github.com/golangci/golangci-lint v1.62.2 // indirect
|
||||
github.com/golangci/misspell v0.6.0 // indirect
|
||||
github.com/golangci/modinfo v0.3.4 // indirect
|
||||
github.com/golangci/plugin-module-register v0.1.1 // indirect
|
||||
github.com/golangci/revgrep v0.5.3 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.1 // indirect
|
||||
github.com/google/generative-ai-go v0.19.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.20.5 // indirect
|
||||
github.com/google/go-github/v72 v72.0.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/ko v0.18.0 // indirect
|
||||
github.com/google/rpmpack v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/google/wire v0.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.1.0 // indirect
|
||||
github.com/goreleaser/chglog v0.7.0 // indirect
|
||||
github.com/goreleaser/fileglob v1.3.0 // indirect
|
||||
github.com/goreleaser/goreleaser/v2 v2.10.2 // indirect
|
||||
github.com/goreleaser/nfpm/v2 v2.43.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.4.2 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.3 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/in-toto/attestation v1.1.1 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/ipfs/bbloom v0.0.4 // indirect
|
||||
github.com/ipfs/go-block-format v0.2.0 // indirect
|
||||
github.com/ipfs/go-cid v0.4.1 // indirect
|
||||
github.com/ipfs/go-datastore v0.6.0 // indirect
|
||||
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
|
||||
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
|
||||
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
|
||||
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
|
||||
github.com/ipfs/go-ipld-format v0.6.0 // indirect
|
||||
github.com/ipfs/go-log v1.0.5 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
||||
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect
|
||||
github.com/jgautheron/goconst v1.7.1 // indirect
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.2 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/julz/importas v0.1.0 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/kisielk/errcheck v1.8.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kulti/thelper v0.6.3 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.10 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/kyoh86/exportloopref v0.1.11 // indirect
|
||||
github.com/lasiar/canonicalheader v1.1.2 // indirect
|
||||
github.com/ldez/gomoddirectives v0.2.4 // indirect
|
||||
github.com/ldez/tagliatelle v0.5.0 // indirect
|
||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||
github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/macabu/inamedparam v0.1.3 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/maratori/testableexamples v1.0.0 // indirect
|
||||
github.com/maratori/testpackage v1.1.1 // indirect
|
||||
github.com/mark3labs/mcp-go v0.30.1 // indirect
|
||||
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.22 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||
github.com/mattn/go-mastodon v0.0.9 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mgechev/revive v1.5.1 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/mango v0.1.0 // indirect
|
||||
github.com/muesli/mango-cobra v1.2.0 // indirect
|
||||
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||
github.com/muesli/roff v0.1.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/nishanths/exhaustive v0.12.0 // indirect
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.18.3 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
|
||||
github.com/polyfloyd/go-errorlint v1.7.0 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/raeperd/recvcheck v0.1.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.3.5 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect
|
||||
github.com/sassoftware/relic v7.2.1+incompatible // indirect
|
||||
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.22.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sigstore/cosign/v2 v2.5.0 // indirect
|
||||
github.com/sigstore/protobuf-specs v0.4.1 // indirect
|
||||
github.com/sigstore/rekor v1.3.9 // indirect
|
||||
github.com/sigstore/sigstore v1.9.3 // indirect
|
||||
github.com/sigstore/sigstore-go v0.7.1 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.5 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sivchari/tenv v1.12.1 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/slack-go/slack v0.17.0 // indirect
|
||||
github.com/sonatard/noctx v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tdakkota/asciicheck v0.2.0 // indirect
|
||||
github.com/tetafro/godot v1.4.18 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.7.0 // indirect
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.2 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect
|
||||
github.com/timonwong/loggercheck v0.10.1 // indirect
|
||||
github.com/tinylib/msgp v1.2.5 // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/ultraware/funlen v0.1.0 // indirect
|
||||
github.com/ultraware/whitespace v0.1.1 // indirect
|
||||
github.com/uudashr/gocognit v1.1.3 // indirect
|
||||
github.com/uudashr/iface v1.2.1 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect
|
||||
github.com/wagoodman/go-progress v0.0.0-20220614130704-4b1c25a33c7c // indirect
|
||||
github.com/whyrusleeping/cbor-gen v0.1.3-0.20240731173018-74d74643234c // indirect
|
||||
github.com/wiggin77/merror v1.0.5 // indirect
|
||||
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xen0n/gosmopolitan v1.2.2 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yagipy/maintidx v1.0.0 // indirect
|
||||
github.com/yeya24/promlinter v0.3.0 // indirect
|
||||
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
||||
gitlab.com/gitlab-org/api/client-go v0.129.0 // indirect
|
||||
go-simpler.org/musttag v0.13.0 // indirect
|
||||
go-simpler.org/sloglint v0.7.2 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
gocloud.dev v0.41.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.228.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/grpc v1.72.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
honnef.co/go/tools v0.5.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
lukechampine.com/blake3 v1.2.1 // indirect
|
||||
mvdan.cc/gofumpt v0.7.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
|
||||
sigs.k8s.io/kind v0.27.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
github.com/goreleaser/goreleaser/v2
|
||||
github.com/securego/gosec/v2/cmd/gosec
|
||||
)
|
103
info.go
Normal file
103
info.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// InfoCommand implements the 'info' command functionality.
|
||||
func InfoCommand() error {
|
||||
manifest, err := LoadPluginManifest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
||||
}
|
||||
|
||||
return PrintPluginInfo(manifest)
|
||||
}
|
||||
|
||||
// PrintPluginInfo displays formatted plugin information.
|
||||
func PrintPluginInfo(manifest *model.Manifest) error {
|
||||
fmt.Printf("Plugin Information:\n")
|
||||
fmt.Printf("==================\n\n")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// InfoCommandWithPath implements the 'info' command with a custom path.
|
||||
func InfoCommandWithPath(path string) error {
|
||||
manifest, err := LoadPluginManifestFromPath(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load plugin manifest from %s: %w", path, err)
|
||||
}
|
||||
|
||||
return PrintPluginInfo(manifest)
|
||||
}
|
||||
|
||||
// RunInfoCommand implements the 'info' command functionality with plugin path.
|
||||
func RunInfoCommand(args []string, pluginPath string) error {
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
return InfoCommandWithPath(absPath)
|
||||
}
|
322
info_test.go
Normal file
322
info_test.go
Normal file
|
@ -0,0 +1,322 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestPrintPluginInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest *model.Manifest
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "Complete plugin with all features",
|
||||
manifest: &model.Manifest{
|
||||
Id: "com.example.testplugin",
|
||||
Name: "Test Plugin",
|
||||
Version: "1.0.0",
|
||||
MinServerVersion: "7.0.0",
|
||||
Description: "A test plugin for unit testing",
|
||||
Server: &model.ManifestServer{
|
||||
Executables: map[string]string{
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe",
|
||||
},
|
||||
},
|
||||
Webapp: &model.ManifestWebapp{
|
||||
BundlePath: "webapp/dist/main.js",
|
||||
},
|
||||
SettingsSchema: &model.PluginSettingsSchema{
|
||||
Header: "Test Settings",
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"Plugin Information:",
|
||||
"ID: com.example.testplugin",
|
||||
"Name: Test Plugin",
|
||||
"Version: 1.0.0",
|
||||
"Min MM Version: 7.0.0",
|
||||
"Description: A test plugin for unit testing",
|
||||
"Code Components:",
|
||||
"Server Code: Yes",
|
||||
"linux-amd64",
|
||||
"darwin-amd64",
|
||||
"windows-amd64",
|
||||
"Webapp Code: Yes",
|
||||
"Bundle Path: webapp/dist/main.js",
|
||||
"Settings Schema: Yes",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Minimal plugin with no optional fields",
|
||||
manifest: &model.Manifest{
|
||||
Id: "com.example.minimal",
|
||||
Name: "Minimal Plugin",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
expected: []string{
|
||||
"Plugin Information:",
|
||||
"ID: com.example.minimal",
|
||||
"Name: Minimal Plugin",
|
||||
"Version: 0.1.0",
|
||||
"Min MM Version: Not specified",
|
||||
"Code Components:",
|
||||
"Server Code: No",
|
||||
"Webapp Code: No",
|
||||
"Settings Schema: No",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Plugin with server code only",
|
||||
manifest: &model.Manifest{
|
||||
Id: "com.example.serveronly",
|
||||
Name: "Server Only Plugin",
|
||||
Version: "2.0.0",
|
||||
MinServerVersion: "8.0.0",
|
||||
Server: &model.ManifestServer{
|
||||
Executables: map[string]string{
|
||||
"linux-amd64": "server/plugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"Plugin Information:",
|
||||
"ID: com.example.serveronly",
|
||||
"Name: Server Only Plugin",
|
||||
"Version: 2.0.0",
|
||||
"Min MM Version: 8.0.0",
|
||||
"Server Code: Yes",
|
||||
"linux-amd64",
|
||||
"Webapp Code: No",
|
||||
"Settings Schema: No",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Plugin with webapp code only",
|
||||
manifest: &model.Manifest{
|
||||
Id: "com.example.webapponly",
|
||||
Name: "Webapp Only Plugin",
|
||||
Version: "1.5.0",
|
||||
Webapp: &model.ManifestWebapp{
|
||||
BundlePath: "dist/bundle.js",
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"Plugin Information:",
|
||||
"ID: com.example.webapponly",
|
||||
"Name: Webapp Only Plugin",
|
||||
"Version: 1.5.0",
|
||||
"Min MM Version: Not specified",
|
||||
"Server Code: No",
|
||||
"Webapp Code: Yes",
|
||||
"Bundle Path: dist/bundle.js",
|
||||
"Settings Schema: No",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Run the function
|
||||
err := PrintPluginInfo(tt.manifest)
|
||||
if err != nil {
|
||||
t.Fatalf("PrintPluginInfo returned error: %v", err)
|
||||
}
|
||||
|
||||
// Restore stdout and get output
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output, _ := io.ReadAll(r)
|
||||
outputStr := string(output)
|
||||
|
||||
// Check that all expected strings are present
|
||||
for _, expected := range tt.expected {
|
||||
if !strings.Contains(outputStr, expected) {
|
||||
t.Errorf("Expected output to contain %q, but it didn't.\nOutput:\n%s", expected, outputStr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasServerCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest *model.Manifest
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Plugin with server executables",
|
||||
manifest: &model.Manifest{
|
||||
Server: &model.ManifestServer{
|
||||
Executables: map[string]string{
|
||||
"linux-amd64": "server/plugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Plugin with empty server executables",
|
||||
manifest: &model.Manifest{
|
||||
Server: &model.ManifestServer{
|
||||
Executables: map[string]string{},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Plugin with nil server",
|
||||
manifest: &model.Manifest{
|
||||
Server: nil,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Plugin with no server field",
|
||||
manifest: &model.Manifest{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := HasServerCode(tt.manifest)
|
||||
if result != tt.expected {
|
||||
t.Errorf("hasServerCode() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasWebappCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest *model.Manifest
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Plugin with webapp bundle",
|
||||
manifest: &model.Manifest{
|
||||
Webapp: &model.ManifestWebapp{
|
||||
BundlePath: "webapp/dist/main.js",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Plugin with empty webapp bundle path",
|
||||
manifest: &model.Manifest{
|
||||
Webapp: &model.ManifestWebapp{
|
||||
BundlePath: "",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Plugin with nil webapp",
|
||||
manifest: &model.Manifest{
|
||||
Webapp: nil,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Plugin with no webapp field",
|
||||
manifest: &model.Manifest{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := HasWebappCode(tt.manifest)
|
||||
if result != tt.expected {
|
||||
t.Errorf("hasWebappCode() = %v, expected %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoCommandWithPath_InvalidPath(t *testing.T) {
|
||||
// Test with non-existent directory
|
||||
err := InfoCommandWithPath("/non/existent/path")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent path, but got nil")
|
||||
}
|
||||
|
||||
expectedErrMsg := "plugin.json not found"
|
||||
if !strings.Contains(err.Error(), expectedErrMsg) {
|
||||
t.Errorf("Expected error to contain %q, but got: %v", expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
// captureOutput captures stdout during function execution
|
||||
func captureOutput(fn func()) string {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
fn()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestPrintPluginInfo_OutputFormat(t *testing.T) {
|
||||
manifest := &model.Manifest{
|
||||
Id: "test.plugin",
|
||||
Name: "Test Plugin",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
output := captureOutput(func() {
|
||||
PrintPluginInfo(manifest)
|
||||
})
|
||||
|
||||
// Check for proper formatting
|
||||
|
||||
// Should have header separators
|
||||
if !strings.Contains(output, "==================") {
|
||||
t.Error("Output should contain header separators")
|
||||
}
|
||||
|
||||
// Should have proper sections
|
||||
if !strings.Contains(output, "Plugin Information:") {
|
||||
t.Error("Output should contain 'Plugin Information:' header")
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "Code Components:") {
|
||||
t.Error("Output should contain 'Code Components:' header")
|
||||
}
|
||||
|
||||
// Should have proper field formatting
|
||||
expectedFields := []string{
|
||||
"ID: test.plugin",
|
||||
"Name: Test Plugin",
|
||||
"Version: 1.0.0",
|
||||
}
|
||||
|
||||
for _, field := range expectedFields {
|
||||
if !strings.Contains(output, field) {
|
||||
t.Errorf("Output should contain field: %q", field)
|
||||
}
|
||||
}
|
||||
}
|
80
plugin.go
Normal file
80
plugin.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const PluginManifestName = "plugin.json"
|
||||
|
||||
// LoadPluginManifest loads and parses the plugin.json file from the current directory.
|
||||
func LoadPluginManifest() (*model.Manifest, error) {
|
||||
return LoadPluginManifestFromPath(".")
|
||||
}
|
||||
|
||||
// LoadPluginManifestFromPath loads and parses the plugin.json file from the specified directory.
|
||||
func LoadPluginManifestFromPath(dir string) (*model.Manifest, error) {
|
||||
manifestPath := filepath.Join(dir, PluginManifestName)
|
||||
|
||||
// Check if plugin.json exists
|
||||
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("plugin.json not found in directory %s", dir)
|
||||
}
|
||||
|
||||
// Read the file
|
||||
data, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plugin.json: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON into Manifest struct
|
||||
var manifest model.Manifest
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse plugin.json: %w", err)
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
// HasServerCode checks if the plugin contains server-side code.
|
||||
func HasServerCode(manifest *model.Manifest) bool {
|
||||
return manifest.Server != nil && len(manifest.Server.Executables) > 0
|
||||
}
|
||||
|
||||
// HasWebappCode checks if the plugin contains webapp code.
|
||||
func HasWebappCode(manifest *model.Manifest) bool {
|
||||
return manifest.Webapp != nil && manifest.Webapp.BundlePath != ""
|
||||
}
|
||||
|
||||
// IsValidPluginDirectory checks if the current directory contains a valid plugin.
|
||||
func IsValidPluginDirectory() error {
|
||||
_, err := LoadPluginManifest()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetEffectivePluginPath determines the plugin path from flag, environment variable, or current directory.
|
||||
func GetEffectivePluginPath(flagPath string) string {
|
||||
const EnvPluginPath = "PLUGINCTL_PLUGIN_PATH"
|
||||
|
||||
// Priority: 1. Command line flag, 2. Environment variable, 3. Current directory
|
||||
if flagPath != "" {
|
||||
return flagPath
|
||||
}
|
||||
|
||||
if envPath := os.Getenv(EnvPluginPath); envPath != "" {
|
||||
return envPath
|
||||
}
|
||||
|
||||
// Default to current directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
|
||||
return cwd
|
||||
}
|
404
plugin_test.go
Normal file
404
plugin_test.go
Normal file
|
@ -0,0 +1,404 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestLoadPluginManifestFromPath(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(string) error
|
||||
path string
|
||||
expectError bool
|
||||
expectedID string
|
||||
expectedName string
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "Valid plugin.json",
|
||||
setupFunc: func(dir string) error {
|
||||
manifest := map[string]interface{}{
|
||||
"id": "com.example.test",
|
||||
"name": "Test Plugin",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
data, _ := json.Marshal(manifest)
|
||||
return os.WriteFile(filepath.Join(dir, "plugin.json"), data, 0644)
|
||||
},
|
||||
path: tempDir,
|
||||
expectError: false,
|
||||
expectedID: "com.example.test",
|
||||
expectedName: "Test Plugin",
|
||||
},
|
||||
{
|
||||
name: "Missing plugin.json",
|
||||
setupFunc: func(dir string) error {
|
||||
return nil // Don't create any file
|
||||
},
|
||||
path: tempDir + "_missing",
|
||||
expectError: true,
|
||||
errorContains: "plugin.json not found",
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
setupFunc: func(dir string) error {
|
||||
invalidJSON := `{"id": "test", "name": "Test", invalid}`
|
||||
return os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(invalidJSON), 0644)
|
||||
},
|
||||
path: tempDir,
|
||||
expectError: true,
|
||||
errorContains: "failed to parse plugin.json",
|
||||
},
|
||||
{
|
||||
name: "Complex plugin with all fields",
|
||||
setupFunc: func(dir string) error {
|
||||
manifest := map[string]interface{}{
|
||||
"id": "com.example.complex",
|
||||
"name": "Complex Plugin",
|
||||
"version": "2.1.0",
|
||||
"min_server_version": "7.0.0",
|
||||
"description": "A complex test plugin",
|
||||
"server": map[string]interface{}{
|
||||
"executables": map[string]string{
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe",
|
||||
},
|
||||
},
|
||||
"webapp": map[string]interface{}{
|
||||
"bundle_path": "webapp/dist/main.js",
|
||||
},
|
||||
"settings_schema": map[string]interface{}{
|
||||
"header": "Complex Plugin Settings",
|
||||
"settings": []map[string]interface{}{
|
||||
{
|
||||
"key": "enable_feature",
|
||||
"display_name": "Enable Feature",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(manifest)
|
||||
return os.WriteFile(filepath.Join(dir, "plugin.json"), data, 0644)
|
||||
},
|
||||
path: tempDir,
|
||||
expectError: false,
|
||||
expectedID: "com.example.complex",
|
||||
expectedName: "Complex Plugin",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Setup test data
|
||||
if err := tt.setupFunc(tt.path); err != nil {
|
||||
t.Fatalf("Failed to setup test: %v", err)
|
||||
}
|
||||
|
||||
// Test the function
|
||||
manifest, err := LoadPluginManifestFromPath(tt.path)
|
||||
|
||||
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.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
t.Fatal("Expected manifest but got nil")
|
||||
}
|
||||
|
||||
if manifest.Id != tt.expectedID {
|
||||
t.Errorf("Expected ID %q, got %q", tt.expectedID, manifest.Id)
|
||||
}
|
||||
|
||||
if manifest.Name != tt.expectedName {
|
||||
t.Errorf("Expected Name %q, got %q", tt.expectedName, manifest.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginManifest(t *testing.T) {
|
||||
// Save current directory
|
||||
originalDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalDir)
|
||||
|
||||
// Create temporary directory and change to it
|
||||
tempDir := t.TempDir()
|
||||
if err := os.Chdir(tempDir); err != nil {
|
||||
t.Fatalf("Failed to change directory: %v", err)
|
||||
}
|
||||
|
||||
// Test with no plugin.json
|
||||
_, err = LoadPluginManifest()
|
||||
if err == nil {
|
||||
t.Error("Expected error when no plugin.json exists")
|
||||
}
|
||||
|
||||
// Create a valid plugin.json
|
||||
manifest := map[string]interface{}{
|
||||
"id": "com.example.current",
|
||||
"name": "Current Dir Plugin",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
data, _ := json.Marshal(manifest)
|
||||
if err := os.WriteFile("plugin.json", data, 0644); err != nil {
|
||||
t.Fatalf("Failed to create plugin.json: %v", err)
|
||||
}
|
||||
|
||||
// Test with valid plugin.json
|
||||
result, err := LoadPluginManifest()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Id != "com.example.current" {
|
||||
t.Errorf("Expected ID 'com.example.current', got %q", result.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEffectivePluginPath(t *testing.T) {
|
||||
// Save original environment
|
||||
originalEnv := os.Getenv("PLUGINCTL_PLUGIN_PATH")
|
||||
defer os.Setenv("PLUGINCTL_PLUGIN_PATH", originalEnv)
|
||||
|
||||
// Save original working directory
|
||||
originalDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
flagPath string
|
||||
envPath string
|
||||
expectedDir string
|
||||
}{
|
||||
{
|
||||
name: "Flag path takes priority",
|
||||
flagPath: "/path/from/flag",
|
||||
envPath: "/path/from/env",
|
||||
expectedDir: "/path/from/flag",
|
||||
},
|
||||
{
|
||||
name: "Environment variable when no flag",
|
||||
flagPath: "",
|
||||
envPath: "/path/from/env",
|
||||
expectedDir: "/path/from/env",
|
||||
},
|
||||
{
|
||||
name: "Current directory when no flag or env",
|
||||
flagPath: "",
|
||||
envPath: "",
|
||||
expectedDir: originalDir,
|
||||
},
|
||||
{
|
||||
name: "Empty flag falls back to env",
|
||||
flagPath: "",
|
||||
envPath: "/path/from/env",
|
||||
expectedDir: "/path/from/env",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Set environment variable
|
||||
os.Setenv("PLUGINCTL_PLUGIN_PATH", tt.envPath)
|
||||
|
||||
result := GetEffectivePluginPath(tt.flagPath)
|
||||
|
||||
if result != tt.expectedDir {
|
||||
t.Errorf("Expected path %q, got %q", tt.expectedDir, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginManifestValidation(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
manifestData map[string]interface{}
|
||||
expectValid bool
|
||||
}{
|
||||
{
|
||||
name: "Minimal valid manifest",
|
||||
manifestData: map[string]interface{}{
|
||||
"id": "com.example.minimal",
|
||||
"name": "Minimal Plugin",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
name: "Manifest with server executables",
|
||||
manifestData: map[string]interface{}{
|
||||
"id": "com.example.server",
|
||||
"name": "Server Plugin",
|
||||
"version": "1.0.0",
|
||||
"server": map[string]interface{}{
|
||||
"executables": map[string]string{
|
||||
"linux-amd64": "server/plugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
name: "Manifest with webapp bundle",
|
||||
manifestData: map[string]interface{}{
|
||||
"id": "com.example.webapp",
|
||||
"name": "Webapp Plugin",
|
||||
"version": "1.0.0",
|
||||
"webapp": map[string]interface{}{
|
||||
"bundle_path": "webapp/dist/main.js",
|
||||
},
|
||||
},
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
name: "Manifest with settings schema",
|
||||
manifestData: map[string]interface{}{
|
||||
"id": "com.example.settings",
|
||||
"name": "Settings Plugin",
|
||||
"version": "1.0.0",
|
||||
"settings_schema": map[string]interface{}{
|
||||
"header": "Plugin Settings",
|
||||
"settings": []map[string]interface{}{
|
||||
{
|
||||
"key": "test_setting",
|
||||
"type": "text",
|
||||
"default": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectValid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create plugin.json file
|
||||
data, err := json.Marshal(tt.manifestData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal test data: %v", err)
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(tempDir, "plugin.json")
|
||||
if err := os.WriteFile(pluginPath, data, 0644); err != nil {
|
||||
t.Fatalf("Failed to write plugin.json: %v", err)
|
||||
}
|
||||
|
||||
// Load and validate manifest
|
||||
manifest, err := LoadPluginManifestFromPath(tempDir)
|
||||
|
||||
if tt.expectValid {
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid manifest but got error: %v", err)
|
||||
}
|
||||
if manifest == nil {
|
||||
t.Error("Expected manifest but got nil")
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid manifest but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up for next test
|
||||
os.Remove(pluginPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasServerCodeAndWebappCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest *model.Manifest
|
||||
expectedServer bool
|
||||
expectedWebapp bool
|
||||
}{
|
||||
{
|
||||
name: "Plugin with both server and webapp",
|
||||
manifest: &model.Manifest{
|
||||
Server: &model.ManifestServer{
|
||||
Executables: map[string]string{
|
||||
"linux-amd64": "server/plugin",
|
||||
},
|
||||
},
|
||||
Webapp: &model.ManifestWebapp{
|
||||
BundlePath: "webapp/dist/main.js",
|
||||
},
|
||||
},
|
||||
expectedServer: true,
|
||||
expectedWebapp: true,
|
||||
},
|
||||
{
|
||||
name: "Plugin with server only",
|
||||
manifest: &model.Manifest{
|
||||
Server: &model.ManifestServer{
|
||||
Executables: map[string]string{
|
||||
"linux-amd64": "server/plugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedServer: true,
|
||||
expectedWebapp: false,
|
||||
},
|
||||
{
|
||||
name: "Plugin with webapp only",
|
||||
manifest: &model.Manifest{
|
||||
Webapp: &model.ManifestWebapp{
|
||||
BundlePath: "webapp/dist/main.js",
|
||||
},
|
||||
},
|
||||
expectedServer: false,
|
||||
expectedWebapp: true,
|
||||
},
|
||||
{
|
||||
name: "Plugin with neither",
|
||||
manifest: &model.Manifest{},
|
||||
expectedServer: false,
|
||||
expectedWebapp: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
serverResult := HasServerCode(tt.manifest)
|
||||
webappResult := HasWebappCode(tt.manifest)
|
||||
|
||||
if serverResult != tt.expectedServer {
|
||||
t.Errorf("hasServerCode() = %v, expected %v", serverResult, tt.expectedServer)
|
||||
}
|
||||
|
||||
if webappResult != tt.expectedWebapp {
|
||||
t.Errorf("hasWebappCode() = %v, expected %v", webappResult, tt.expectedWebapp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
37
testdata/complete_plugin.json
vendored
Normal file
37
testdata/complete_plugin.json
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"id": "com.example.complete",
|
||||
"name": "Complete Test Plugin",
|
||||
"description": "A complete plugin with all features for testing",
|
||||
"version": "2.1.0",
|
||||
"min_server_version": "7.0.0",
|
||||
"server": {
|
||||
"executables": {
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
|
||||
}
|
||||
},
|
||||
"webapp": {
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "Complete Plugin Settings",
|
||||
"footer": "Configure all settings for the complete plugin",
|
||||
"settings": [
|
||||
{
|
||||
"key": "enable_feature",
|
||||
"display_name": "Enable Main Feature",
|
||||
"type": "bool",
|
||||
"help_text": "Enable or disable the main plugin feature",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "api_endpoint",
|
||||
"display_name": "API Endpoint",
|
||||
"type": "text",
|
||||
"help_text": "The API endpoint URL",
|
||||
"default": "https://api.example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
6
testdata/invalid_plugin.json
vendored
Normal file
6
testdata/invalid_plugin.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"id": "com.example.invalid",
|
||||
"name": "Invalid Plugin",
|
||||
"version": "1.0.0",
|
||||
invalid_json_syntax: true
|
||||
}
|
5
testdata/minimal_plugin.json
vendored
Normal file
5
testdata/minimal_plugin.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "com.example.minimal",
|
||||
"name": "Minimal Test Plugin",
|
||||
"version": "1.0.0"
|
||||
}
|
22
testdata/plugin.json
vendored
Normal file
22
testdata/plugin.json
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "com.example.testplugin",
|
||||
"name": "Test Plugin",
|
||||
"description": "A test plugin for demonstrating pluginctl functionality",
|
||||
"version": "1.0.0",
|
||||
"min_server_version": "7.0.0",
|
||||
"server": {
|
||||
"executables": {
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
|
||||
}
|
||||
},
|
||||
"webapp": {
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "Configure the Test Plugin",
|
||||
"footer": "",
|
||||
"settings": []
|
||||
}
|
||||
}
|
13
testdata/server_only_plugin.json
vendored
Normal file
13
testdata/server_only_plugin.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "com.example.serveronly",
|
||||
"name": "Server Only Plugin",
|
||||
"description": "A plugin with only server-side code",
|
||||
"version": "1.5.0",
|
||||
"min_server_version": "8.0.0",
|
||||
"server": {
|
||||
"executables": {
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64"
|
||||
}
|
||||
}
|
||||
}
|
9
testdata/webapp_only_plugin.json
vendored
Normal file
9
testdata/webapp_only_plugin.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "com.example.webapponly",
|
||||
"name": "Webapp Only Plugin",
|
||||
"description": "A plugin with only client-side code",
|
||||
"version": "0.9.0",
|
||||
"webapp": {
|
||||
"bundle_path": "webapp/dist/bundle.js"
|
||||
}
|
||||
}
|
40
version.go
Normal file
40
version.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// RunVersionCommand implements the 'version' command functionality.
|
||||
func RunVersionCommand(args []string) error {
|
||||
version := getVersion()
|
||||
fmt.Printf("pluginctl version %s\n", version)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVersion returns the version information from build info.
|
||||
func getVersion() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// First try to get version from main module
|
||||
if info.Main.Version != "" && info.Main.Version != "(devel)" {
|
||||
return info.Main.Version
|
||||
}
|
||||
|
||||
// Look for version in build settings (set by goreleaser)
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" {
|
||||
// Return short commit hash if no version tag
|
||||
if len(setting.Value) >= 7 {
|
||||
return setting.Value[:7]
|
||||
}
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
|
||||
return "dev"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue