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