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:
Felipe M 2025-07-07 13:01:26 +02:00
commit fd6e4a4513
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
21 changed files with 4949 additions and 0 deletions

33
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
)

1717
go.sum Normal file

File diff suppressed because it is too large Load diff

103
info.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
{
"id": "com.example.minimal",
"name": "Minimal Test Plugin",
"version": "1.0.0"
}

22
testdata/plugin.json vendored Normal file
View 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
View 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
View 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
View 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"
}