From e273d0ec37c0bb923a48c4468c1d4b694e5cfc76 Mon Sep 17 00:00:00 2001 From: Mario <92220954+mario-gc@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:36:48 +0200 Subject: [PATCH 1/3] fix: update Go version to 1.26 to fix GO-2026-4602 (#4) * fix: update Go version to 1.26 to fix GO-2026-4602 - Fixes CVE-2026-27139 (FileInfo can escape from a Root in os) - Ensures continued support (Go 1.23 ended Feb 11, 2026) - Removed temporary workaround for GO-2026-4602 in security.yml - Updated CI/CD workflows to use Go 1.26 - Updated documentation to reflect Go 1.26+ requirement * fix: update golangci-lint to v2.11.4 for Go 1.26 compatibility - v1.64.8 was built with Go 1.24, incompatible with Go 1.26 - v2.11.4 is built with Go 1.26 and can lint Go 1.26 code * fix: update golangci-lint-action to v7 and migrate config to v2 - golangci-lint v2 requires golangci-lint-action v7 - Migrated .golangci.yml to v2 format - Moved gofmt/goimports to formatters section - Removed gosimple (merged into staticcheck in v2) - Updated CHANGELOG with migration details --- .github/workflows/ci.yml | 4 +- .github/workflows/lint.yml | 6 +-- .github/workflows/release.yml | 4 +- .github/workflows/security.yml | 19 +-------- .golangci.yml | 71 ++++++++++++++++++---------------- AGENTS.md | 2 +- CHANGELOG.md | 19 ++++++++- README.md | 4 +- go.mod | 2 +- 9 files changed, 69 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cd7c1c..509e995 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.23'] + go-version: ['1.26'] steps: - name: Checkout code @@ -50,7 +50,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.26' cache: true - name: Download dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 713ff12..f91e34a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,11 +24,11 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.26' cache: true - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v7 with: - version: v1.64.8 + version: v2.11.4 args: --timeout=5m \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ebe3ad..6717b23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.26' cache: true - name: Download dependencies @@ -44,7 +44,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.26' cache: true - name: Run GoReleaser diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 90358f2..ad76cc6 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -27,29 +27,14 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.26' cache: true - name: Install govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest - name: Run govulncheck - run: | - set +e - output=$(govulncheck ./... 2>&1) - exit_code=$? - echo "$output" - # Allow GO-2026-4602 (requires Go 1.25.8, not yet released) - if echo "$output" | grep -q "GO-2026-4602"; then - echo "::warning::GO-2026-4602 requires Go 1.25.8 (not yet released)" - fi - # Fail only if there are other vulnerabilities - if [ $exit_code -ne 0 ]; then - other_vulns=$(echo "$output" | grep "Vulnerability #" | grep -v "GO-2026-4602") - if [ -n "$other_vulns" ]; then - exit 1 - fi - fi + run: govulncheck ./... gosec: name: Gosec diff --git a/.golangci.yml b/.golangci.yml index 255a33f..f39f36d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,54 +1,59 @@ +version: "2" + linters: enable: - errcheck - - gosimple - govet - ineffassign - staticcheck - unused - - gofmt - - goimports - misspell - revive -linters-settings: - errcheck: - check-type-assertions: true - check-blank: true - - govet: - enable-all: true - disable: - - fieldalignment - - revive: + settings: + errcheck: + check-type-assertions: true + check-blank: true + + govet: + enable-all: true + disable: + - fieldalignment + + revive: + rules: + - name: exported + arguments: + - disableStutteringCheck + - name: var-naming + - name: package-comments + disabled: true + + misspell: + locale: US + + exclusions: rules: - - name: exported - arguments: - - disableStutteringCheck - - name: var-naming - - name: package-comments - disabled: true + - path: _test\.go + linters: + - errcheck - gofmt: - simplify: true +formatters: + enable: + - gofmt + - goimports - goimports: - local-prefixes: opencode-agent-switcher + settings: + gofmt: + simplify: true - misspell: - locale: US + goimports: + local-prefixes: + - opencode-agent-switcher issues: - exclude-rules: - - path: _test\.go - linters: - - errcheck - max-issues-per-linter: 50 max-same-issues: 3 - new: false run: - timeout: 5m modules-download-mode: readonly \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index dae583b..84351bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This document provides instructions and guidelines for AI agents operating withi `opencode-agent-switcher` is a Go CLI tool designed to manage and switch AI models and modes for various agents in the Opencode environment. It interacts with the `opencode` CLI and modifies agent configuration files. -- **Language:** Go 1.23+ +- **Language:** Go 1.26+ - **Entry Point:** `main.go` - **Module:** `opencode-agent-switcher` - **Dependencies:** diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f699d..8583422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.2] - 2026-04-02 + +### Security +- Updated Go version from 1.23 to 1.26 + - Fixes GO-2026-4602: FileInfo can escape from a Root in os + - CVE-2026-27139 (Medium severity) + - Ensures continued support (Go 1.23 ended Feb 11, 2026) +- Removed temporary workaround for GO-2026-4602 in security.yml + +### Changed +- Updated CI/CD workflows to use Go 1.26 +- Updated golangci-lint from v1.64.8 to v2.11.4 (built with Go 1.26) +- Migrated .golangci.yml to v2 format +- Updated golangci-lint-action from v6 to v7 +- Updated documentation to reflect Go 1.26+ requirement + ## [0.7.1] - 2026-04-02 ### Fixed @@ -127,7 +143,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - YAML frontmatter parsing for agent configuration files - MIT License -[Unreleased]: https://github.com/mario-gc/opencode-agent-switcher/compare/v0.7.1...HEAD +[Unreleased]: https://github.com/mario-gc/opencode-agent-switcher/compare/v0.7.2...HEAD +[0.7.2]: https://github.com/mario-gc/opencode-agent-switcher/compare/v0.7.1...v0.7.2 [0.7.1]: https://github.com/mario-gc/opencode-agent-switcher/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/mario-gc/opencode-agent-switcher/compare/v0.6.1...v0.7.0 [0.6.1]: https://github.com/mario-gc/opencode-agent-switcher/compare/v0.6.0...v0.6.1 diff --git a/README.md b/README.md index 96983d7..de1587a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/mario-gc/opencode-agent-switcher)](https://goreportcard.com/report/github.com/mario-gc/opencode-agent-switcher) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Go Version](https://img.shields.io/badge/Go-1.23+-00ADD8?logo=go)](https://golang.org/) +[![Go Version](https://img.shields.io/badge/Go-1.26+-00ADD8?logo=go)](https://golang.org/) A CLI tool for managing and switching AI models and modes for Opencode agents. @@ -28,7 +28,7 @@ A CLI tool for managing and switching AI models and modes for Opencode agents. ## Prerequisites -- **Go:** Version 1.23 or higher +- **Go:** Version 1.26 or higher - **Opencode:** The `opencode` CLI tool must be installed and configured ## Installation diff --git a/go.mod b/go.mod index f84598e..692afba 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mario-gc/opencode-agent-switcher -go 1.23 +go 1.26 require ( github.com/charmbracelet/huh v0.6.0 From 5fa850abcb5d8120974f67cc5a511033e4e16fd0 Mon Sep 17 00:00:00 2001 From: Mario <92220954+mario-gc@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:06:47 +0200 Subject: [PATCH 2/3] fix: add comments to exported functions and fix linting issues (#5) * fix: add comments to exported functions and fix staticcheck issues - Added comments to all exported functions across all packages - Fixed 3 staticcheck QF1012 issues in cli/prompt.go - Used fmt.Fprintf instead of WriteString(fmt.Sprintf(...)) - All 50 revive exported comment warnings resolved - All tests passing * fix: add comments to models and fix remaining staticcheck issues - Added comments to all exported constants, variables, and types in models/models.go - Added comment block for UI choice constants in cli/prompt.go - Fixed 3 remaining QF1012 staticcheck issues in cli/prompt.go - All 16 golangci-lint issues resolved (13 revive + 3 staticcheck) --- agents/agents.go | 17 +++++++++++++ cli/prompt.go | 56 +++++++++++++++++++++++++++++++++++++----- config/config.go | 16 ++++++++++++ models/models.go | 12 +++++++++ templates/templates.go | 17 +++++++++++++ 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/agents/agents.go b/agents/agents.go index aa8e9f7..f46bdbb 100644 --- a/agents/agents.go +++ b/agents/agents.go @@ -17,6 +17,8 @@ const maxFrontmatterSize = 64 * 1024 var validSegment = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_\-\.]*$`) +// ValidateModelID validates a model ID string format. +// Returns an error if the model ID is empty, exceeds 256 characters, or has invalid segments. func ValidateModelID(modelID string) error { if modelID == "" { return fmt.Errorf("model ID cannot be empty") @@ -58,6 +60,8 @@ func isPathWithinDir(path, dir string) (bool, error) { return strings.HasPrefix(absPath, absDir+string(filepath.Separator)), nil } +// LoadAllAgents loads agents from all available sources. +// Returns a deduplicated list with project configurations taking precedence over global ones. func LoadAllAgents() ([]models.Agent, error) { agentMap := make(map[string]models.Agent) @@ -114,10 +118,15 @@ func getProjectAgentsDir() string { return filepath.Join(".", ".opencode", "agents") } +// LoadAgents loads agents from a directory of markdown files. +// Deprecated: Use LoadAgentsFromDir for more control over source metadata. func LoadAgents(agentsDir string) ([]models.Agent, error) { return LoadAgentsFromDir(agentsDir, models.SourceGlobal, models.FormatMarkdown) } +// LoadAgentsFromDir loads agents from markdown files in a directory. +// Parameters location and format specify the source metadata for each agent. +// Returns a list of agents with valid model fields. func LoadAgentsFromDir(agentsDir, location, format string) ([]models.Agent, error) { absAgentsDir, err := filepath.Abs(agentsDir) if err != nil { @@ -185,6 +194,8 @@ func LoadAgentsFromDir(agentsDir, location, format string) ([]models.Agent, erro return agentList, nil } +// ParseFrontmatter extracts YAML frontmatter from markdown content. +// Returns a map of frontmatter fields or an error if parsing fails. func ParseFrontmatter(content string) (map[string]interface{}, error) { if !strings.HasPrefix(content, "---") { return nil, fmt.Errorf("no frontmatter found") @@ -208,6 +219,9 @@ func ParseFrontmatter(content string) (map[string]interface{}, error) { return frontmatter, nil } +// UpdateAgentModel updates the model field for an agent. +// Supports both markdown files and JSON configuration files. +// Returns an error if the model ID is invalid or the file cannot be updated. func UpdateAgentModel(agentPath, agentName, newModel string) error { if err := ValidateModelID(newModel); err != nil { return fmt.Errorf("invalid model ID: %w", err) @@ -231,6 +245,9 @@ func UpdateAgentModel(agentPath, agentName, newModel string) error { return updateAgentFieldInMarkdown(agentPath, "model", newModel) } +// UpdateAgentMode updates the mode field for an agent. +// Supports both markdown files and JSON configuration files. +// Returns an error if the mode is invalid (must be 'primary', 'subagent', or 'all'). func UpdateAgentMode(agentPath, agentName, newMode string) error { if !isValidMode(newMode) { return fmt.Errorf("invalid mode: must be 'primary', 'subagent', or 'all'") diff --git a/cli/prompt.go b/cli/prompt.go index 980c103..a603fb0 100644 --- a/cli/prompt.go +++ b/cli/prompt.go @@ -11,6 +11,7 @@ import ( "github.com/mario-gc/opencode-agent-switcher/models" ) +// UI choice constants for user selections. const ( ExitChoice = "__EXIT__" ContinueChoice = "__CONTINUE__" @@ -28,6 +29,8 @@ const ( TemplateInspect = "inspect" ) +// PromptAgentSelection prompts the user to select an agent from the list. +// Returns the selected agent name, a special choice (SortChoice, TemplatesChoice, ExitChoice), or an error. func PromptAgentSelection(agents []models.Agent, currentSort string, caseSensitive bool) (string, error) { var selectedName string @@ -79,6 +82,8 @@ func formatSourceTag(source models.AgentSource) string { return fmt.Sprintf("%s/%s", loc, fmtChar) } +// PromptActionSelection prompts the user to choose an action for a selected agent. +// Returns the selected action (ActionModel, ActionMode, or BackChoice) or an error. func PromptActionSelection(currentModel, currentMode string) (string, error) { var action string @@ -107,6 +112,8 @@ func PromptActionSelection(currentModel, currentMode string) (string, error) { return action, nil } +// PromptModeSelection prompts the user to select a mode for an agent. +// Returns the selected mode (primary, subagent, or all) or an error. func PromptModeSelection(currentMode string) (string, error) { var mode string @@ -136,6 +143,8 @@ func PromptModeSelection(currentMode string) (string, error) { return mode, nil } +// PromptAddModeField prompts the user whether to add an explicit mode field. +// Returns true if the user wants to add the field, false otherwise. func PromptAddModeField() (bool, error) { var choice string @@ -158,6 +167,8 @@ func PromptAddModeField() (bool, error) { return choice == "yes", nil } +// PromptModelSelection prompts the user to select a model from the list. +// Returns the selected model ID, CustomModelChoice, or an error. func PromptModelSelection(modelOptions []models.ModelOption) (string, error) { var selectedID string options := make([]huh.Option[string], len(modelOptions)+1) @@ -184,6 +195,8 @@ func PromptModelSelection(modelOptions []models.ModelOption) (string, error) { return selectedID, nil } +// PromptCustomModelInput prompts the user to enter a custom model ID. +// Returns the entered model ID or an error. func PromptCustomModelInput() (string, error) { var modelID string @@ -203,6 +216,8 @@ func PromptCustomModelInput() (string, error) { return modelID, nil } +// PromptConfirm prompts the user with a yes/no confirmation. +// Returns true if the user confirms, false otherwise. func PromptConfirm(message string) (bool, error) { var confirmed bool @@ -223,6 +238,8 @@ func PromptConfirm(message string) (bool, error) { return confirmed, nil } +// PromptContinueOrExit prompts the user to continue or exit the application. +// Returns true to continue, false to exit. func PromptContinueOrExit() (bool, error) { var choice string @@ -245,6 +262,8 @@ func PromptContinueOrExit() (bool, error) { return choice == ContinueChoice, nil } +// PromptUndo prompts the user whether to undo recent changes. +// Returns true if the user wants to undo, false to keep changes. func PromptUndo(message string) (bool, error) { var choice string @@ -288,6 +307,8 @@ func getSortDisplay(sortBy string, caseSensitive bool) string { return sortType } +// PromptSortSelection prompts the user to select a sorting method. +// Returns the selected sort option, updated case-sensitivity flag, or an error. func PromptSortSelection(currentSort string, caseSensitive bool) (string, bool, error) { var selected string @@ -327,6 +348,8 @@ func PromptSortSelection(currentSort string, caseSensitive bool) (string, bool, return selected, caseSensitive, nil } +// SortAgents sorts a list of agents by the specified criteria. +// Returns a new sorted slice without modifying the original. func SortAgents(agents []models.Agent, sortBy string, caseSensitive bool) []models.Agent { result := make([]models.Agent, len(agents)) copy(result, agents) @@ -364,6 +387,8 @@ func SortAgents(agents []models.Agent, sortBy string, caseSensitive bool) []mode return result } +// SortModels sorts a list of model options by the specified criteria. +// Returns a new sorted slice without modifying the original. func SortModels(modelOptions []models.ModelOption, sortBy string, caseSensitive bool) []models.ModelOption { result := make([]models.ModelOption, len(modelOptions)) copy(result, modelOptions) @@ -393,6 +418,8 @@ func SortModels(modelOptions []models.ModelOption, sortBy string, caseSensitive return result } +// PromptTemplateMenu prompts the user to choose a template operation. +// Returns the selected action (TemplateSave, TemplateShow, or BackChoice) or an error. func PromptTemplateMenu() (string, error) { var choice string @@ -416,6 +443,8 @@ func PromptTemplateMenu() (string, error) { return choice, nil } +// PromptTemplateName prompts the user to enter a name for a new template. +// Returns the entered name or an error. func PromptTemplateName() (string, error) { var name string @@ -435,6 +464,8 @@ func PromptTemplateName() (string, error) { return name, nil } +// PromptTemplateSelection prompts the user to select a template from the list. +// Returns the selected template name, BackChoice, or an error. func PromptTemplateSelection(templates []models.Template) (string, error) { if len(templates) == 0 { return "", nil @@ -477,6 +508,8 @@ func formatDate(timestamp string) string { return t.Format("2006-01-02") } +// PromptTemplateAction prompts the user to choose an action for a selected template. +// Returns the selected action (TemplateInspect, TemplateLoad, TemplateDelete, or BackChoice) or an error. func PromptTemplateAction(templateName string) (string, error) { var action string @@ -501,6 +534,8 @@ func PromptTemplateAction(templateName string) (string, error) { return action, nil } +// PromptTemplateOverwrite prompts the user whether to overwrite an existing template. +// Returns true if the user confirms overwrite, false otherwise. func PromptTemplateOverwrite(templateName string) (bool, error) { var choice string @@ -523,6 +558,9 @@ func PromptTemplateOverwrite(templateName string) (bool, error) { return choice == "yes", nil } +// PromptTemplateLoadConfirm prompts the user to confirm loading a template. +// Shows the number of matched and unmatched agents. +// Returns true if the user confirms, false otherwise. func PromptTemplateLoadConfirm(matchedCount, unmatchedCount int) (bool, error) { var choice string @@ -547,6 +585,8 @@ func PromptTemplateLoadConfirm(matchedCount, unmatchedCount int) (bool, error) { return choice == "yes", nil } +// PromptTemplateDeleteConfirm prompts the user to confirm deleting a template. +// Returns true if the user confirms, false otherwise. func PromptTemplateDeleteConfirm(templateName string) (bool, error) { var choice string @@ -569,6 +609,8 @@ func PromptTemplateDeleteConfirm(templateName string) (bool, error) { return choice == "yes", nil } +// PromptTemplateContinueOrExit prompts the user to continue or exit after template operations. +// Returns true to continue, false to exit. func PromptTemplateContinueOrExit() (bool, error) { var choice string @@ -591,12 +633,14 @@ func PromptTemplateContinueOrExit() (bool, error) { return choice == ContinueChoice, nil } +// FormatTemplateInspect formats a template for inspection display. +// Returns a formatted string showing template name, creation date, and all agent assignments. func FormatTemplateInspect(template models.Template) string { var builder strings.Builder - builder.WriteString(fmt.Sprintf("Template: %s\n", template.Name)) - builder.WriteString(fmt.Sprintf("Created: %s\n", formatDate(template.CreatedAt))) - builder.WriteString(fmt.Sprintf("Agents: %d\n\n", len(template.Agents))) + fmt.Fprintf(&builder, "Template: %s\n", template.Name) + fmt.Fprintf(&builder, "Created: %s\n", formatDate(template.CreatedAt)) + fmt.Fprintf(&builder, "Agents: %d\n\n", len(template.Agents)) agentNames := make([]string, 0, len(template.Agents)) for name := range template.Agents { @@ -611,9 +655,9 @@ func FormatTemplateInspect(template models.Template) string { if modeDisplay == "" { modeDisplay = "all (default)" } - builder.WriteString(fmt.Sprintf(" %s [%s]\n", name, sourceTag)) - builder.WriteString(fmt.Sprintf(" Model: %s\n", assignment.Model)) - builder.WriteString(fmt.Sprintf(" Mode: %s\n", modeDisplay)) + fmt.Fprintf(&builder, " %s [%s]\n", name, sourceTag) + fmt.Fprintf(&builder, " Model: %s\n", assignment.Model) + fmt.Fprintf(&builder, " Mode: %s\n", modeDisplay) } return builder.String() diff --git a/config/config.go b/config/config.go index de42325..1640c01 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,8 @@ func isValidModelID(modelID string) bool { return true } +// LoadGlobalConfig loads the global opencode configuration file. +// Returns the configuration or an error if the file cannot be read or parsed. func LoadGlobalConfig() (*models.OpencodeConfig, error) { home, err := os.UserHomeDir() if err != nil { @@ -43,6 +45,8 @@ func LoadGlobalConfig() (*models.OpencodeConfig, error) { return loadConfigFile(configPath) } +// LoadProjectConfig loads the project-level opencode configuration file. +// Returns nil if the file does not exist, or an error if it cannot be parsed. func LoadProjectConfig() (*models.OpencodeConfig, error) { configPath := filepath.Join(".", "opencode.json") if _, err := os.Stat(configPath); os.IsNotExist(err) { @@ -65,6 +69,9 @@ func loadConfigFile(configPath string) (*models.OpencodeConfig, error) { return &cfg, nil } +// GetAgentsFromConfig extracts agents from an opencode configuration. +// Parameters location and format specify the source metadata for each agent. +// Returns a list of agents defined in the configuration. func GetAgentsFromConfig(cfg *models.OpencodeConfig, location, format string) []models.Agent { if cfg == nil || cfg.Agent == nil { return nil @@ -87,6 +94,8 @@ func GetAgentsFromConfig(cfg *models.OpencodeConfig, location, format string) [] return agentList } +// GetAvailableModels extracts available models from an opencode configuration. +// Returns a list of model options with provider, ID, and display name. func GetAvailableModels(cfg *models.OpencodeConfig) []models.ModelOption { var options []models.ModelOption @@ -116,6 +125,8 @@ func GetAvailableModels(cfg *models.OpencodeConfig) []models.ModelOption { return options } +// GetModelsFromCLI retrieves available models by calling the opencode CLI. +// Returns a list of model options or an error if the CLI command fails. func GetModelsFromCLI() ([]models.ModelOption, error) { cmd := exec.Command("opencode", "models") output, err := cmd.Output() @@ -145,6 +156,8 @@ func GetModelsFromCLI() ([]models.ModelOption, error) { return options, nil } +// GetGlobalConfigPath returns the path to the global opencode configuration file. +// Returns an error if the user home directory cannot be determined. func GetGlobalConfigPath() (string, error) { home, err := os.UserHomeDir() if err != nil { @@ -153,10 +166,13 @@ func GetGlobalConfigPath() (string, error) { return filepath.Join(home, ".config", "opencode", "opencode.json"), nil } +// GetProjectConfigPath returns the path to the project-level opencode configuration file. func GetProjectConfigPath() string { return filepath.Join(".", "opencode.json") } +// UpdateAgentInJSON updates a field for an agent in a JSON configuration file. +// Returns an error if the agent does not exist or the file cannot be updated. func UpdateAgentInJSON(configPath, agentName, field, value string) error { data, err := os.ReadFile(configPath) if err != nil { diff --git a/models/models.go b/models/models.go index 8986cfd..6a7a908 100644 --- a/models/models.go +++ b/models/models.go @@ -1,5 +1,6 @@ package models +// Source constants for agent location and format. const ( SourceGlobal = "global" SourceProject = "project" @@ -7,6 +8,7 @@ const ( FormatJSON = "json" ) +// Sort constants for agent and model sorting options. const ( SortAgentAsc = "agent-asc" SortAgentDesc = "agent-desc" @@ -14,18 +16,22 @@ const ( SortModelDesc = "model-desc" ) +// DefaultSort is the default sorting method for agent lists. var DefaultSort = SortAgentAsc +// OpencodeConfig represents the opencode configuration file structure. type OpencodeConfig struct { Provider map[string]Provider `json:"provider"` Agent map[string]AgentConfig `json:"agent"` } +// Provider represents a model provider configuration. type Provider struct { Name string `json:"name"` Models map[string]Model `json:"models"` } +// Model represents a model configuration with limits and modalities. type Model struct { Name string `json:"name"` Limit map[string]int `json:"limit"` @@ -33,23 +39,27 @@ type Model struct { Variants map[string]interface{} `json:"variants"` } +// AgentConfig represents an agent configuration in opencode.json. type AgentConfig struct { Description string `json:"description"` Model string `json:"model"` Mode string `json:"mode"` } +// ModelOption represents a selectable model option. type ModelOption struct { ID string Display string Provider string } +// AgentSource represents the source location and format of an agent. type AgentSource struct { Location string Format string } +// Agent represents a loaded agent with its configuration. type Agent struct { Name string Path string @@ -59,12 +69,14 @@ type Agent struct { Source AgentSource } +// AgentAssignment represents a model and mode assignment for an agent. type AgentAssignment struct { Model string `json:"model"` Mode string `json:"mode"` Source AgentSource `json:"source"` } +// Template represents a saved agent configuration template. type Template struct { Name string `json:"name"` CreatedAt string `json:"created_at"` diff --git a/templates/templates.go b/templates/templates.go index d60e0b2..1ae5dfd 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -17,6 +17,8 @@ var validTemplateName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_\-]*$`) const templatesDirName = "opencode-agent-switcher" +// GetTemplatesDir returns the path to the templates directory. +// Creates the directory if it doesn't exist. Returns an error if creation fails. func GetTemplatesDir() (string, error) { home, err := os.UserHomeDir() if err != nil { @@ -29,6 +31,8 @@ func GetTemplatesDir() (string, error) { return dir, nil } +// ValidateTemplateName validates a template name format. +// Returns an error if the name is empty, exceeds 64 characters, or contains invalid characters. func ValidateTemplateName(name string) error { if name == "" { return fmt.Errorf("template name cannot be empty") @@ -42,6 +46,8 @@ func ValidateTemplateName(name string) error { return nil } +// TemplateExists checks if a template with the given name exists. +// Returns true if the template exists, false otherwise. func TemplateExists(name string) (bool, error) { dir, err := GetTemplatesDir() if err != nil { @@ -55,6 +61,9 @@ func TemplateExists(name string) (bool, error) { return err == nil, err } +// SaveTemplate saves the current agent configuration as a named template. +// Stores the template as a JSON file in the templates directory. +// Returns an error if the name is invalid or the file cannot be written. func SaveTemplate(name string, agents []models.Agent) error { if err := ValidateTemplateName(name); err != nil { return err @@ -89,6 +98,8 @@ func SaveTemplate(name string, agents []models.Agent) error { return os.WriteFile(path, data, 0600) } +// LoadTemplates loads all available templates from the templates directory. +// Returns a sorted list of templates or an error if the directory cannot be read. func LoadTemplates() ([]models.Template, error) { dir, err := GetTemplatesDir() if err != nil { @@ -130,6 +141,8 @@ func LoadTemplates() ([]models.Template, error) { return templates, nil } +// LoadTemplateByName loads a specific template by name. +// Returns the template or an error if it doesn't exist or cannot be parsed. func LoadTemplateByName(name string) (models.Template, error) { dir, err := GetTemplatesDir() if err != nil { @@ -150,6 +163,8 @@ func LoadTemplateByName(name string) (models.Template, error) { return template, nil } +// DeleteTemplate removes a template file from the templates directory. +// Returns an error if the template cannot be deleted. func DeleteTemplate(name string) error { dir, err := GetTemplatesDir() if err != nil { @@ -164,6 +179,8 @@ func DeleteTemplate(name string) error { return nil } +// MatchAgents matches template agents with current agents by name and source. +// Returns a list of matched agents with template assignments and a list of unmatched agent names. func MatchAgents(template models.Template, currentAgents []models.Agent) ([]models.Agent, []string) { var matched []models.Agent var unmatched []string From 6272c3dd46bd65542b4347298cbd9b78d20d9a1f Mon Sep 17 00:00:00 2001 From: Mario <92220954+mario-gc@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:12:48 +0000 Subject: [PATCH 3/3] docs: update version references to 0.7.2 --- AGENTS.md | 6 +++--- README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 84351bd..3f6d967 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -230,7 +230,7 @@ Templates allow saving and restoring agent configurations (model + mode assignme ### CD Pipeline (GoReleaser) The project uses GoReleaser for automated binary releases: -**Trigger:** Push a version tag (e.g., `v0.7.0`) +**Trigger:** Push a version tag (e.g., `v0.7.2`) **Process:** 1. Tag push triggers `.github/workflows/release.yml` @@ -243,8 +243,8 @@ The project uses GoReleaser for automated binary releases: **Release Flow:** ```bash # After merging to main and updating CHANGELOG.md -git tag -a v0.7.0 -m "Release v0.7.0" -git push origin v0.7.0 +git tag -a v0.7.2 -m "Release v0.7.2" +git push origin v0.7.2 # CD pipeline automatically creates the release ``` diff --git a/README.md b/README.md index de1587a..cb48056 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ Download the latest release for your platform from the [Releases page](https://g ```bash # Linux (amd64) -curl -sL https://github.com/mario-gc/opencode-agent-switcher/releases/latest/download/opencode-agent-switcher_0.7.0_linux_amd64.tar.gz | tar xz +curl -sL https://github.com/mario-gc/opencode-agent-switcher/releases/latest/download/opencode-agent-switcher_0.7.2_linux_amd64.tar.gz | tar xz # Linux (arm64) -curl -sL https://github.com/mario-gc/opencode-agent-switcher/releases/latest/download/opencode-agent-switcher_0.7.0_linux_arm64.tar.gz | tar xz +curl -sL https://github.com/mario-gc/opencode-agent-switcher/releases/latest/download/opencode-agent-switcher_0.7.2_linux_arm64.tar.gz | tar xz # Make executable chmod +x opencode-agent-switcher @@ -85,7 +85,7 @@ opencode-agent-switcher ```bash opencode-agent-switcher --version -# Output: opencode-agent-switcher 0.7.0 (commit: abc1234, built: 2026-04-02) +# Output: opencode-agent-switcher 0.7.2 (commit: abc1234, built: 2026-04-02) ``` ### Workflow