Add tools command for direct binary downloads from GitHub releases

Introduces a new 'tools' command that installs development tools (golangci-lint, gotestsum)
by downloading pre-built binaries directly from GitHub releases instead of using 'go get -tool'.
This prevents modifications to plugin go.mod files and improves build reliability.

Features:
- Cross-platform support (Windows, macOS, Linux) with automatic architecture detection
- Version-specific binary naming with symlinks for easy access
- Configurable installation directory via --bin-dir flag
- Tar.gz archive extraction with binary validation
- Updated Makefile integration to use downloaded binaries

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Felipe M 2025-08-04 13:41:54 +02:00
parent f8e3266029
commit c1399f5107
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
9 changed files with 596 additions and 102 deletions

View file

@ -31,57 +31,57 @@ linters:
# Enable specific linters # Enable specific linters
enable: enable:
# Enabled by default # Enabled by default
- errcheck # Check for unchecked errors - errcheck # Check for unchecked errors
- gosimple # Simplify code - gosimple # Simplify code
- govet # Examine Go source code and reports suspicious constructs - govet # Examine Go source code and reports suspicious constructs
- ineffassign # Detect ineffectual assignments - ineffassign # Detect ineffectual assignments
- staticcheck # Advanced Go linter - staticcheck # Advanced Go linter
- unused # Check for unused constants, variables, functions and types - unused # Check for unused constants, variables, functions and types
# Additional linters # Additional linters
- asciicheck # Check for non-ASCII characters - asciicheck # Check for non-ASCII characters
- bodyclose # Check HTTP response body is closed - bodyclose # Check HTTP response body is closed
- contextcheck # Check context.Context is propagated - contextcheck # Check context.Context is propagated
- cyclop # Check cyclomatic complexity - cyclop # Check cyclomatic complexity
- dupl # Check for duplicate code - dupl # Check for duplicate code
- durationcheck # Check for two durations multiplied together - durationcheck # Check for two durations multiplied together
- errorlint # Check for error wrapping - errorlint # Check for error wrapping
- exhaustive # Check exhaustiveness of enum switch statements - exhaustive # Check exhaustiveness of enum switch statements
- copyloopvar # Check for pointers to enclosing loop variables - copyloopvar # Check for pointers to enclosing loop variables
- forcetypeassert # Find forced type assertions - forcetypeassert # Find forced type assertions
- funlen # Check function length - funlen # Check function length
- gci # Control Go package import order - gci # Control Go package import order
- gocognit # Check cognitive complexity - gocognit # Check cognitive complexity
- goconst # Find repeated strings that could be constants - goconst # Find repeated strings that could be constants
- gocritic # Various checks - gocritic # Various checks
- gocyclo # Check cyclomatic complexity - gocyclo # Check cyclomatic complexity
- godot # Check if comments end in a period - godot # Check if comments end in a period
- gofmt # Check if the code was gofmt-ed - gofmt # Check if the code was gofmt-ed
- goimports # Check if imports are sorted - goimports # Check if imports are sorted
- mnd # Check for magic numbers - mnd # Check for magic numbers
- gomoddirectives # Check for //go:build directives - gomoddirectives # Check for //go:build directives
- gomodguard # Check for blocked module dependencies - gomodguard # Check for blocked module dependencies
- goprintffuncname # Check printf-like function names - goprintffuncname # Check printf-like function names
- gosec # Security checker - gosec # Security checker
- lll # Check line length - lll # Check line length
- makezero # Find slice declarations that are not initialized with zero length - makezero # Find slice declarations that are not initialized with zero length
- misspell # Find commonly misspelled English words - misspell # Find commonly misspelled English words
- nakedret # Check for naked returns - nakedret # Check for naked returns
- nilerr # Check for nil errors - nilerr # Check for nil errors
- nlreturn # Check for new line before return - nlreturn # Check for new line before return
- noctx # Check for HTTP requests without context - noctx # Check for HTTP requests without context
- prealloc # Find slice declarations that could be preallocated - prealloc # Find slice declarations that could be preallocated
- predeclared # Check for predeclared identifiers - predeclared # Check for predeclared identifiers
- revive # Fast, configurable, extensible linter - revive # Fast, configurable, extensible linter
- rowserrcheck # Check SQL rows.Err - rowserrcheck # Check SQL rows.Err
- sqlclosecheck # Check SQL Close() calls - sqlclosecheck # Check SQL Close() calls
- stylecheck # Stylecheck is a replacement for golint - stylecheck # Stylecheck is a replacement for golint
- thelper # Check test helpers - thelper # Check test helpers
- tparallel # Check test parallelization - tparallel # Check test parallelization
- unconvert # Check for unnecessary type conversions - unconvert # Check for unnecessary type conversions
- unparam # Check for unused function parameters - unparam # Check for unused function parameters
- wastedassign # Check for wasted assignment statements - wastedassign # Check for wasted assignment statements
- whitespace # Check for unnecessary whitespace - whitespace # Check for unnecessary whitespace
# Linters settings # Linters settings
linters-settings: linters-settings:
@ -181,7 +181,7 @@ linters-settings:
# Settings for lll # Settings for lll
lll: lll:
line-length: 120 line-length: 140
# Settings for misspell # Settings for misspell
misspell: misspell:
@ -203,7 +203,7 @@ linters-settings:
rules: rules:
- name: atomic - name: atomic
- name: line-length-limit - name: line-length-limit
arguments: [120] arguments: [140]
- name: argument-limit - name: argument-limit
arguments: [4] arguments: [4]
- name: cyclomatic - name: cyclomatic
@ -219,10 +219,50 @@ linters-settings:
# Settings for stylecheck # Settings for stylecheck
stylecheck: stylecheck:
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] checks:
["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
dot-import-whitelist: dot-import-whitelist:
- fmt - 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"] 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"] http-status-code-whitelist: ["200", "400", "404", "500"]
# Settings for unparam # Settings for unparam
@ -234,24 +274,24 @@ issues:
# List of regexps of issue texts to exclude # List of regexps of issue texts to exclude
exclude: exclude:
# Exclude some linters from running on tests files # Exclude some linters from running on tests files
- "G104:" # Errors unhandled (gosec) - "G104:" # Errors unhandled (gosec)
- "G204:" # Subprocess launched with variable (gosec) - "G204:" # Subprocess launched with variable (gosec)
- "G304:" # File path provided as taint input (gosec) - "G304:" # File path provided as taint input (gosec)
# Skip files # Skip files
exclude-files: exclude-files:
- ".*_test.go" - ".*_test.go"
- ".*\\.pb\\.go" - ".*\\.pb\\.go"
# Skip directories # Skip directories
exclude-dirs: exclude-dirs:
- vendor - vendor
- node_modules - node_modules
- .git - .git
# Make issues output unique by line # Make issues output unique by line
uniq-by-line: true uniq-by-line: true
# Excluding configuration per-path, per-linter, per-text and per-source # Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules: exclude-rules:
# Exclude some linters from running on tests files # Exclude some linters from running on tests files
@ -370,4 +410,4 @@ issues:
new-from-patch: "" new-from-patch: ""
# Fix found issues (if it's supported by the linter) # Fix found issues (if it's supported by the linter)
fix: false fix: false

View file

@ -1,48 +1,88 @@
run: version: "2"
timeout: 5m
modules-download-mode: readonly
linters-settings:
gofmt:
simplify: true
goimports:
local-prefixes: {{.GoModule.Name}}
govet:
check-shadowing: true
enable-all: true
disable:
- fieldalignment
misspell:
locale: US
linters: linters:
disable-all: true
enable: enable:
- bodyclose - bodyclose
- errcheck - errcheck
- gocritic - gocritic
- gofmt
- goimports
- gosec - gosec
- gosimple
- govet
- ineffassign - ineffassign
- misspell - misspell
- nakedret - nakedret
- revive - revive
- staticcheck - staticcheck # Now includes gosimple and stylecheck
- stylecheck
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- whitespace - whitespace
- govet # Ensure this is included
settings:
errcheck:
# Add any errcheck settings here
exclude-functions:
- io.Copy(*bytes.Buffer)
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
gosec:
# Add gosec settings
excludes:
- G104 # Errors unhandled
staticcheck:
# Configure staticcheck (includes gosimple/stylecheck checks)
checks: ["all"]
revive:
# Add revive rules
rules:
- name: exported
disabled: false
exclusions:
presets:
- comments
- std-error-handling
- common-false-positives
rules:
- path: '_test\.go'
linters:
- errcheck
- gosec
formatters:
enable:
- gofmt
- goimports
settings:
gofmt:
simplify: true
goimports:
local-prefixes:
- {{.GoModule.Name}}
output:
formats:
text:
path: stdout
colors: true
print-linter-name: true
run:
timeout: 5m
tests: true
issues: issues:
exclude-rules: max-issues-per-linter: 0
- path: server/configuration.go max-same-issues: 0
linters: fix: false
- unused
- path: _test\.go
linters:
- bodyclose
- scopelint # https://github.com/kyoh86/scopelint/issues/4

View file

@ -2,11 +2,13 @@
# Testing and Quality Assurance # Testing and Quality Assurance
# ==================================================================================== # ====================================================================================
GOLANGCI_LINT_BINARY = ./build/bin/golangci-lint
GOTESTSUM_BINARY = ./build/bin/gotestsum
## Install go tools ## Install go tools
install-go-tools: install-go-tools:
@echo Installing go tools @echo "Installing development tools..."
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 @pluginctl tools install --bin-dir ./build/bin
$(GO) install gotest.tools/gotestsum@v1.7.0
## Runs eslint and golangci-lint ## Runs eslint and golangci-lint
.PHONY: check-style .PHONY: check-style
@ -24,14 +26,14 @@ endif
ifneq ($(HAS_SERVER),) ifneq ($(HAS_SERVER),)
@echo Running golangci-lint @echo Running golangci-lint
$(GO) vet ./... $(GO) vet ./...
$(GOBIN)/golangci-lint run ./... $(GOLANGCI_LINT_BINARY) run ./...
endif endif
## Runs any lints and unit tests defined for the server and webapp, if they exist. ## Runs any lints and unit tests defined for the server and webapp, if they exist.
.PHONY: test .PHONY: test
test: apply webapp/node_modules install-go-tools test: apply webapp/node_modules install-go-tools
ifneq ($(HAS_SERVER),) ifneq ($(HAS_SERVER),)
$(GOBIN)/gotestsum -- -v ./... $(GOTESTSUM_BINARY) -- -v ./...
endif endif
ifneq ($(HAS_WEBAPP),) ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) run test; cd webapp && $(NPM) run test;
@ -42,7 +44,7 @@ endif
.PHONY: test-ci .PHONY: test-ci
test-ci: apply webapp/node_modules install-go-tools test-ci: apply webapp/node_modules install-go-tools
ifneq ($(HAS_SERVER),) ifneq ($(HAS_SERVER),)
$(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./... $(GOTESTSUM_BINARY) --format standard-verbose --junitfile report.xml -- ./...
endif endif
ifneq ($(HAS_WEBAPP),) ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) run test; cd webapp && $(NPM) run test;

View file

@ -81,6 +81,8 @@ func runCommand(command string, args []string, pluginPath string) error {
return runVersionCommand(args) return runVersionCommand(args)
case "create-plugin": case "create-plugin":
return runCreatePluginCommand(args, pluginPath) return runCreatePluginCommand(args, pluginPath)
case "tools":
return runToolsCommand(args, pluginPath)
default: default:
return fmt.Errorf("unknown command: %s", command) return fmt.Errorf("unknown command: %s", command)
} }
@ -128,6 +130,10 @@ func runCreatePluginCommand(args []string, pluginPath string) error {
return pluginctl.RunCreatePluginCommand(args, pluginPath) return pluginctl.RunCreatePluginCommand(args, pluginPath)
} }
func runToolsCommand(args []string, pluginPath string) error {
return pluginctl.RunToolsCommand(args, pluginPath)
}
func showUsage() { func showUsage() {
usageText := `pluginctl - Mattermost Plugin Development CLI usageText := `pluginctl - Mattermost Plugin Development CLI
@ -148,6 +154,7 @@ Commands:
manifest Manage plugin manifest files manifest Manage plugin manifest files
logs View plugin logs logs View plugin logs
create-plugin Create a new plugin from template create-plugin Create a new plugin from template
tools Manage development tools (install golangci-lint, gotestsum)
version Show version information version Show version information
Environment Variables: Environment Variables:

5
go.mod
View file

@ -156,6 +156,7 @@ require (
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/dnephin/pflag v1.0.7 // indirect
github.com/docker/cli v28.1.1+incompatible // indirect github.com/docker/cli v28.1.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.1.1+incompatible // indirect github.com/docker/docker v28.1.1+incompatible // indirect
@ -239,6 +240,7 @@ require (
github.com/google/rpmpack v0.7.0 // indirect github.com/google/rpmpack v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962 // indirect github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/google/wire v0.6.0 // indirect github.com/google/wire v0.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
@ -288,6 +290,7 @@ require (
github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.2 // indirect github.com/jjti/go-spancheck v0.6.2 // indirect
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/julz/importas v0.1.0 // indirect github.com/julz/importas v0.1.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect
@ -498,6 +501,7 @@ require (
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/gotestsum v1.7.0 // indirect
honnef.co/go/tools v0.5.1 // indirect honnef.co/go/tools v0.5.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect
@ -512,4 +516,5 @@ tool (
github.com/golangci/golangci-lint/cmd/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint
github.com/goreleaser/goreleaser/v2 github.com/goreleaser/goreleaser/v2
github.com/securego/gosec/v2/cmd/gosec github.com/securego/gosec/v2/cmd/gosec
gotest.tools/gotestsum
) )

14
go.sum
View file

@ -394,6 +394,8 @@ github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
@ -437,6 +439,7 @@ github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
@ -454,6 +457,7 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
@ -615,6 +619,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -818,6 +823,9 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@ -898,6 +906,7 @@ github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r
github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s= github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s=
github.com/mattermost/mattermost/server/public v0.1.15 h1:8Kn5KzJJtrw1VaBlEH8ijhF20z0rGMge2ejpuJROfKc= github.com/mattermost/mattermost/server/public v0.1.15 h1:8Kn5KzJJtrw1VaBlEH8ijhF20z0rGMge2ejpuJROfKc=
github.com/mattermost/mattermost/server/public v0.1.15/go.mod h1:EwEPMkzc87/mZYkpi46K0R4fe08HXniPDcpYTB3gv5s= github.com/mattermost/mattermost/server/public v0.1.15/go.mod h1:EwEPMkzc87/mZYkpi46K0R4fe08HXniPDcpYTB3gv5s=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@ -1190,6 +1199,7 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@ -1517,6 +1527,7 @@ golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1590,6 +1601,7 @@ golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -1691,6 +1703,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/gotestsum v1.7.0 h1:RwpqwwFKBAa2h+F6pMEGpE707Edld0etUD3GhqqhDNc=
gotest.tools/gotestsum v1.7.0/go.mod h1:V1m4Jw3eBerhI/A6qCxUE07RnCg7ACkKj9BYcAm09V8=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=

View file

@ -47,12 +47,6 @@ const manifest = JSON.parse(` + "`%s`" + `);
export default manifest; export default manifest;
` `
// PluginCtlConfig represents the configuration for pluginctl stored in the manifest props.
type PluginCtlConfig struct {
Version string `json:"version,omitempty"`
IgnoreAssets []string `json:"ignore_assets,omitempty"`
}
// LoadPluginManifest loads and parses the plugin.json file from the current directory. // LoadPluginManifest loads and parses the plugin.json file from the current directory.
func LoadPluginManifest() (*model.Manifest, error) { func LoadPluginManifest() (*model.Manifest, error) {
return LoadPluginManifestFromPath(".") return LoadPluginManifestFromPath(".")

View file

@ -17,6 +17,12 @@ const (
UnknownVersion = "unknown" UnknownVersion = "unknown"
) )
// PluginCtlConfig represents the configuration for pluginctl stored in the manifest props.
type PluginCtlConfig struct {
Version string `json:"version,omitempty"`
IgnoreAssets []string `json:"ignore_assets,omitempty"`
}
// IsValidPluginDirectory checks if the current directory contains a valid plugin. // IsValidPluginDirectory checks if the current directory contains a valid plugin.
func IsValidPluginDirectory() error { func IsValidPluginDirectory() error {
_, err := LoadPluginManifest() _, err := LoadPluginManifest()

386
tools.go Normal file
View file

@ -0,0 +1,386 @@
// NOTE: We download tools directly from tarball/binary releases instead of using
// `go get -tool` to prevent modifications to plugin go.mod files on plugins.
package pluginctl
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
)
const (
defaultGolangciLintVersion = "v2.3.1"
defaultGotestsumVersion = "v1.7.0"
defaultBinDir = "./build/bin"
helpFlagLong = "--help"
helpFlagShort = "-h"
exeExtension = ".exe"
tempSuffix = "-temp"
// Platform constants.
platformDarwin = "darwin"
platformWindows = "windows"
platformLinux = "linux"
// Architecture constants.
archARM64 = "arm64"
archAMD64 = "amd64"
arch386 = "386"
// File permission constants.
dirPerm = 0750
filePerm = 0600
)
// ToolConfig represents configuration for downloading and installing a tool.
type ToolConfig struct {
Name string
Version string
GitHubRepo string
URLTemplate string
FilenameTemplate string
BinaryPath string // Path within archive (e.g., "bin/tool" or "tool")
}
var toolConfigs = map[string]ToolConfig{
"golangci-lint": {
Name: "golangci-lint",
Version: defaultGolangciLintVersion,
GitHubRepo: "golangci/golangci-lint",
URLTemplate: "https://github.com/{repo}/releases/download/{version}/" +
"golangci-lint-{version_no_v}-{os}-{arch}.tar.gz",
FilenameTemplate: "golangci-lint-{version_no_v}-{os}-{arch}.tar.gz",
BinaryPath: "golangci-lint-{version_no_v}-{os}-{arch}/golangci-lint",
},
"gotestsum": {
Name: "gotestsum",
Version: defaultGotestsumVersion,
GitHubRepo: "gotestyourself/gotestsum",
URLTemplate: "https://github.com/{repo}/releases/download/{version}/" +
"gotestsum_{version_no_v}_{os}_{arch}.tar.gz",
FilenameTemplate: "gotestsum_{version_no_v}_{os}_{arch}.tar.gz",
BinaryPath: "gotestsum",
},
}
func RunToolsCommand(args []string, pluginPath string) error {
if len(args) == 0 {
return showToolsUsage()
}
subcommand := args[0]
subcommandArgs := args[1:]
switch subcommand {
case "install":
return runToolsInstallCommand(subcommandArgs)
case "help", helpFlagLong, helpFlagShort:
return showToolsUsage()
default:
return fmt.Errorf("unknown tools subcommand: %s", subcommand)
}
}
func runToolsInstallCommand(args []string) error {
binDir := defaultBinDir
for i, arg := range args {
if arg == helpFlagLong || arg == helpFlagShort {
return showToolsInstallUsage()
}
if arg == "--bin-dir" && i+1 < len(args) {
binDir = args[i+1]
}
}
Logger.Info("Installing development tools...", "bin-dir", binDir)
if err := os.MkdirAll(binDir, dirPerm); err != nil {
return fmt.Errorf("failed to create bin directory: %w", err)
}
for toolName := range toolConfigs {
if err := installTool(toolName, binDir); err != nil {
return fmt.Errorf("failed to install %s: %w", toolName, err)
}
}
Logger.Info("All development tools installed successfully")
return nil
}
// getPlatform returns the platform string for tool downloads.
func getPlatform() string {
switch runtime.GOOS {
case platformDarwin:
return platformDarwin
case platformWindows:
return platformWindows
default:
return platformLinux
}
}
// getArchitecture returns the architecture string for tool downloads.
func getArchitecture() string {
switch runtime.GOARCH {
case archARM64:
return archARM64
case arch386:
return arch386
default:
return archAMD64
}
}
// expandTemplate replaces placeholders in template strings.
func expandTemplate(template string, config *ToolConfig, platform, arch string) string {
versionNoV := strings.TrimPrefix(config.Version, "v")
replacements := map[string]string{
"{repo}": config.GitHubRepo,
"{version}": config.Version,
"{version_no_v}": versionNoV,
"{os}": platform,
"{arch}": arch,
}
result := template
for placeholder, value := range replacements {
result = strings.ReplaceAll(result, placeholder, value)
}
return result
}
// downloadAndExtractTool downloads and extracts a tool from GitHub releases.
func downloadAndExtractTool(config *ToolConfig, binDir string) error {
platform := getPlatform()
arch := getArchitecture()
downloadURL := expandTemplate(config.URLTemplate, config, platform, arch)
binaryPathInArchive := expandTemplate(config.BinaryPath, config, platform, arch)
Logger.Info("Downloading tool", "tool", config.Name, "url", downloadURL)
resp, err := downloadToolFromURL(downloadURL, config.Name) //nolint:gosec // Trusted source
if err != nil {
return err
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
Logger.Error("Failed to close response body", "error", closeErr)
}
}()
return extractToolFromArchive(resp.Body, config, binDir, binaryPathInArchive)
}
// downloadToolFromURL downloads a tool from the specified URL.
func downloadToolFromURL(downloadURL, toolName string) (*http.Response, error) {
resp, err := http.Get(downloadURL) //nolint:gosec,noctx // URL from trusted configuration
if err != nil {
return nil, fmt.Errorf("failed to download %s: %w", toolName, err)
}
if resp.StatusCode != http.StatusOK {
if closeErr := resp.Body.Close(); closeErr != nil {
Logger.Error("Failed to close response body", "error", closeErr)
}
return nil, fmt.Errorf("failed to download %s: HTTP %d", toolName, resp.StatusCode)
}
return resp, nil
}
// extractToolFromArchive extracts the tool binary from a tar.gz archive.
func extractToolFromArchive(reader io.Reader, config *ToolConfig, binDir, binaryPathInArchive string) error {
gzr, err := gzip.NewReader(reader)
if err != nil {
return fmt.Errorf("failed to create gzip reader for %s: %w", config.Name, err)
}
defer func() {
if closeErr := gzr.Close(); closeErr != nil {
Logger.Error("Failed to close gzip reader", "error", closeErr)
}
}()
tr := tar.NewReader(gzr)
// Create final binary path
binaryName := config.Name
finalBinaryPath := filepath.Join(binDir, fmt.Sprintf("%s-%s", config.Name, config.Version))
if runtime.GOOS == platformWindows {
binaryName += exeExtension
finalBinaryPath += exeExtension
}
paths := binaryPaths{
pathInArchive: binaryPathInArchive,
binaryName: binaryName,
finalPath: finalBinaryPath,
}
return extractBinaryFromTar(tr, config, binDir, paths)
}
// binaryPaths holds path information for binary extraction.
type binaryPaths struct {
pathInArchive string
binaryName string
finalPath string
}
// extractBinaryFromTar searches and extracts the binary from a tar archive.
func extractBinaryFromTar(tr *tar.Reader, config *ToolConfig, binDir string, paths binaryPaths) error {
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("failed to read tar archive for %s: %w", config.Name, err)
}
if isBinaryFile(header.Name, paths.pathInArchive, paths.binaryName, config.Name) {
return saveBinaryFile(tr, config, binDir, paths.finalPath)
}
}
return fmt.Errorf("%s binary not found in archive", config.Name)
}
// isBinaryFile checks if the file matches the binary we're looking for.
func isBinaryFile(fileName, binaryPathInArchive, binaryName, configName string) bool {
return fileName == binaryPathInArchive ||
strings.HasSuffix(fileName, "/"+binaryName) ||
strings.HasSuffix(fileName, configName)
}
// saveBinaryFile saves the binary from tar reader to disk.
func saveBinaryFile(tr *tar.Reader, config *ToolConfig, binDir, finalBinaryPath string) error {
tempPath := filepath.Join(binDir, config.Name+tempSuffix)
if runtime.GOOS == platformWindows {
tempPath += exeExtension
}
file, err := os.OpenFile(tempPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePerm)
if err != nil {
return fmt.Errorf("failed to create temporary binary file for %s: %w", config.Name, err)
}
_, err = io.Copy(file, tr) //nolint:gosec // Archive from trusted source
if closeErr := file.Close(); closeErr != nil {
Logger.Error("Failed to close temp file", "error", closeErr)
}
if err != nil {
return fmt.Errorf("failed to write binary file for %s: %w", config.Name, err)
}
if err := os.Rename(tempPath, finalBinaryPath); err != nil {
return fmt.Errorf("failed to rename binary to final path for %s: %w", config.Name, err)
}
Logger.Info("Tool installed successfully", "tool", config.Name, "path", finalBinaryPath)
return nil
}
// installTool installs a single tool by name using its configuration.
func installTool(toolName, binDir string) error {
config, exists := toolConfigs[toolName]
if !exists {
return fmt.Errorf("unknown tool: %s", toolName)
}
binaryPath := filepath.Join(binDir, fmt.Sprintf("%s-%s", config.Name, config.Version))
symlinkPath := filepath.Join(binDir, config.Name)
if runtime.GOOS == platformWindows {
binaryPath += exeExtension
symlinkPath += exeExtension
}
if fileExists(binaryPath) {
return createSymlink(binaryPath, symlinkPath)
}
Logger.Info("Installing tool", "tool", config.Name, "version", config.Version)
if err := downloadAndExtractTool(&config, binDir); err != nil {
return err
}
return createSymlink(binaryPath, symlinkPath)
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func createSymlink(target, link string) error {
if fileExists(link) {
if err := os.Remove(link); err != nil {
return fmt.Errorf("failed to remove existing symlink: %w", err)
}
}
targetRel, err := filepath.Rel(filepath.Dir(link), target)
if err != nil {
return fmt.Errorf("failed to calculate relative path: %w", err)
}
if err := os.Symlink(targetRel, link); err != nil {
return fmt.Errorf("failed to create symlink: %w", err)
}
Logger.Info("Created symlink", "target", target, "link", link)
return nil
}
func showToolsUsage() error {
usageText := `Tools command - Manage development tools
Usage:
pluginctl tools <subcommand> [options]
Subcommands:
install Install development tools (golangci-lint, gotestsum)
Use 'pluginctl tools <subcommand> --help' for detailed information about a subcommand.
`
Logger.Info(usageText)
return nil
}
func showToolsInstallUsage() error {
usageText := `Install development tools
Usage:
pluginctl tools install
Description:
Installs the following development tools to ./bin/ directory:
- golangci-lint ` + defaultGolangciLintVersion + `
- gotestsum ` + defaultGotestsumVersion + `
Tools are downloaded with version-specific names (e.g., golangci-lint-v2.3.1)
to allow version tracking and prevent unnecessary re-downloads.
Options:
--help, -h Show this help message
`
Logger.Info(usageText)
return nil
}