chore: Clean up unused code and call commands endpoint for commands

This commit is contained in:
Liang-Shih Lin 2025-07-06 15:02:34 +08:00
parent f70e06a283
commit ae629b159a
5 changed files with 42 additions and 407 deletions

View file

@ -110,8 +110,12 @@ func New(
slog.Debug("Loaded config", "config", configInfo)
// Create commands client - assuming server runs on same host with port 3000
commandsClient := commands.NewCommandsClient("http://localhost:3000")
// Create commands client using the same base URL as the HTTP client
baseURL := os.Getenv("OPENCODE_SERVER")
if baseURL == "" {
baseURL = "http://localhost:4096" // Default fallback
}
commandsClient := commands.NewCommandsClient(baseURL)
app := &App{
Info: appInfo,

View file

@ -56,7 +56,7 @@ func NewCommandsClient(baseURL string) *CommandsClient {
// ListCustomCommands fetches all available custom commands from the server
func (c *CommandsClient) ListCustomCommands(ctx context.Context) ([]CustomCommand, error) {
url := fmt.Sprintf("%s/commands", c.baseURL)
url := fmt.Sprintf("%scommands", c.baseURL)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
@ -83,7 +83,7 @@ func (c *CommandsClient) ListCustomCommands(ctx context.Context) ([]CustomComman
// GetCustomCommand fetches a specific custom command from the server
func (c *CommandsClient) GetCustomCommand(ctx context.Context, name string) (*CustomCommand, error) {
url := fmt.Sprintf("%s/commands/%s", c.baseURL, name)
url := fmt.Sprintf("%scommands/%s", c.baseURL, name)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
@ -114,7 +114,7 @@ func (c *CommandsClient) GetCustomCommand(ctx context.Context, name string) (*Cu
// ExecuteCustomCommand executes a custom command on the server
func (c *CommandsClient) ExecuteCustomCommand(ctx context.Context, name string, arguments *string) (*ExecuteCommandResponse, error) {
url := fmt.Sprintf("%s/commands/%s/execute", c.baseURL, name)
url := fmt.Sprintf("%scommands/%s/execute", c.baseURL, name)
reqBody := ExecuteCommandRequest{
Arguments: arguments,

View file

@ -314,9 +314,9 @@ func IsValidCustomCommand(commandName string, configPath string) bool {
return false
}
// IsValidCustomCommandWithClient checks if a custom command exists via server or filesystem
// IsValidCustomCommandWithClient checks if a custom command exists via server
func IsValidCustomCommandWithClient(commandName string, configPath string, client *CommandsClient) bool {
// Try server first if client is available
// Use server endpoint if client is available
if client != nil {
ctx := context.Background()
exists, err := client.CustomCommandExists(ctx, commandName)
@ -325,8 +325,8 @@ func IsValidCustomCommandWithClient(commandName string, configPath string, clien
}
}
// Fallback to local filesystem check
return IsValidCustomCommand(commandName, configPath)
// Return false if server is not available or returns an error
return false
}
func LoadFromConfig(config *opencode.Config) CommandRegistry {

View file

@ -1,13 +1,12 @@
package completions
import (
"bufio"
"context"
"io/fs"
"os"
"fmt"
"log/slog"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"github.com/charmbracelet/lipgloss/v2"
@ -66,98 +65,29 @@ func getCustomCommandCompletionItem(cmd CustomCommandFile, space int, t theme.Th
})
}
// parseMarkdownMetadata extracts description from markdown frontmatter or first heading
func parseMarkdownMetadata(filePath string) string {
file, err := os.Open(filePath)
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
// Check for YAML frontmatter
if scanner.Scan() {
firstLine := strings.TrimSpace(scanner.Text())
if firstLine == "---" {
// Parse YAML frontmatter
descriptionRegex := regexp.MustCompile(`(?i)^description:\s*(.+)$`)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "---" {
break
}
if matches := descriptionRegex.FindStringSubmatch(line); len(matches) > 1 {
return strings.Trim(matches[1], `"'`)
}
}
} else {
// Check if first line is a heading and use it as description
if strings.HasPrefix(firstLine, "#") {
return strings.TrimSpace(strings.TrimLeft(firstLine, "#"))
}
}
}
// If no frontmatter, look for first heading
file.Seek(0, 0)
scanner = bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#") {
return strings.TrimSpace(strings.TrimLeft(line, "#"))
}
// Stop at first non-empty, non-comment line
if line != "" && !strings.HasPrefix(line, "<!--") {
break
}
}
return ""
}
func (c *CommandCompletionProvider) getCustomCommands() ([]CustomCommandFile, error) {
// Try to get commands from server first
// Get commands from server endpoint
ctx := context.Background()
serverCommands, err := c.app.CommandsClient.ListCustomCommands(ctx)
if err == nil {
// Convert server commands to local format
var commands []CustomCommandFile
for _, cmd := range serverCommands {
description := ""
if cmd.Description != nil {
description = *cmd.Description
}
commands = append(commands, CustomCommandFile{
Name: cmd.Name,
Description: description,
Filename: filepath.Base(cmd.FilePath),
Content: cmd.Content,
})
}
return commands, nil
}
// Fallback to local filesystem scanning if server is not available
var commands []CustomCommandFile
// Get global commands from ~/.config/opencode/commands
globalCommandsDir := filepath.Join(c.app.Info.Path.Config, "commands")
globalCommands, err := c.scanCommandsDirectory(globalCommandsDir, "")
if err != nil {
// Log error but continue with project commands
globalCommands = []CustomCommandFile{}
return nil, fmt.Errorf("failed to get commands from server: %w", err)
}
commands = append(commands, globalCommands...)
// Get project-level commands from $PWD/.opencode/commands
cwd, err := os.Getwd()
if err == nil {
projectCommandsDir := filepath.Join(cwd, ".opencode", "commands")
projectCommands, err := c.scanCommandsDirectory(projectCommandsDir, "")
if err == nil {
commands = append(commands, projectCommands...)
slog.Debug("Server commands:" + strconv.Itoa(len(serverCommands)))
// Convert server commands to local format
var commands []CustomCommandFile
for _, cmd := range serverCommands {
description := ""
if cmd.Description != nil {
description = *cmd.Description
}
commands = append(commands, CustomCommandFile{
Name: cmd.Name,
Description: description,
Filename: filepath.Base(cmd.FilePath),
Content: cmd.Content,
})
}
// Sort commands alphabetically
@ -168,57 +98,6 @@ func (c *CommandCompletionProvider) getCustomCommands() ([]CustomCommandFile, er
return commands, nil
}
// scanCommandsDirectory recursively scans a directory for markdown command files
func (c *CommandCompletionProvider) scanCommandsDirectory(baseDir, relativePath string) ([]CustomCommandFile, error) {
var commands []CustomCommandFile
currentDir := filepath.Join(baseDir, relativePath)
err := filepath.WalkDir(currentDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
// If the commands directory doesn't exist, just return empty list
if strings.Contains(err.Error(), "no such file or directory") {
return nil
}
return err
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(d.Name(), ".md") {
return nil
}
// Calculate relative path from base commands directory
relPath, err := filepath.Rel(baseDir, path)
if err != nil {
return err
}
// Convert file path to command name with colon notation
// e.g., "foo/bar.md" -> "foo:bar"
name := strings.TrimSuffix(relPath, ".md")
name = strings.ReplaceAll(name, string(filepath.Separator), ":")
description := parseMarkdownMetadata(path)
commands = append(commands, CustomCommandFile{
Name: name,
Filename: d.Name(),
Content: "", // We don't need content for completion
Description: description,
})
return nil
})
if err != nil {
return nil, err
}
return commands, nil
}
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
t := theme.CurrentTheme()
commands := c.app.Commands
@ -226,7 +105,7 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
// Get custom commands
customCommands, err := c.getCustomCommands()
if err != nil {
// Log error but continue with built-in commands
// If server is not available, return only built-in commands
customCommands = []CustomCommandFile{}
}

View file

@ -1,14 +1,10 @@
package tui
import (
"bytes"
"context"
"fmt"
"io/fs"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@ -36,14 +32,6 @@ import (
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
type InterruptDebounceTimeoutMsg struct{}
// CustomCommandFile represents a custom command file
type CustomCommandFile struct {
Name string `json:"name"`
Filename string `json:"filename"`
Content string `json:"content"`
Description string `json:"description"`
}
// InterruptKeyState tracks the state of interrupt key presses for debouncing
type InterruptKeyState int
@ -1018,16 +1006,15 @@ func (a appModel) parseCustomCommand(input string) (commandName, arguments strin
arguments = parts[1]
}
// Check if this command exists as a custom command
customCommands, err := a.getAvailableCustomCommands()
// Check if this command exists as a custom command via server
ctx := context.Background()
exists, err := a.app.CommandsClient.CustomCommandExists(ctx, commandName)
if err != nil {
return "", "", false
}
for _, cmd := range customCommands {
if cmd.Name == commandName {
return commandName, arguments, true
}
if exists {
return commandName, arguments, true
}
return "", "", false
@ -1037,7 +1024,7 @@ func (a appModel) parseCustomCommand(input string) (commandName, arguments strin
func (a appModel) executeCustomCommandWithArgs(commandName, arguments string) (tea.Model, tea.Cmd) {
slog.Debug("Executing custom command with arguments", "command", commandName, "arguments", arguments)
// Try to execute command via server first
// Execute command via server endpoint
ctx := context.Background()
var args *string
if arguments != "" {
@ -1053,244 +1040,9 @@ func (a appModel) executeCustomCommandWithArgs(commandName, arguments string) (t
return a, cmd
}
// Fallback to local execution if server is not available
slog.Warn("Server execution failed, falling back to local execution", "command", commandName, "error", err)
// Convert colon notation back to file path
filePath := strings.ReplaceAll(commandName, ":", string(filepath.Separator)) + ".md"
var commandFile string
var content []byte
var localErr error
// Try project-level commands first ($PWD/.opencode/commands)
if cwd, cwdErr := os.Getwd(); cwdErr == nil {
projectCommandsDir := filepath.Join(cwd, ".opencode", "commands")
projectCommandFile := filepath.Join(projectCommandsDir, filePath)
content, localErr = os.ReadFile(projectCommandFile)
if localErr == nil {
commandFile = projectCommandFile
}
}
// If not found in project, try global commands (~/.config/opencode/commands)
if localErr != nil {
globalCommandsDir := filepath.Join(a.app.Info.Path.Config, "commands")
globalCommandFile := filepath.Join(globalCommandsDir, filePath)
content, localErr = os.ReadFile(globalCommandFile)
if localErr == nil {
commandFile = globalCommandFile
}
}
if localErr != nil {
slog.Error("Failed to read custom command file", "command", commandName, "error", localErr)
return a, toast.NewErrorToast("Failed to read custom command: " + commandName)
}
slog.Debug("Executing custom command locally", "command", commandName, "file", commandFile, "arguments", arguments)
// Replace $ARGUMENTS placeholder with actual arguments
contentStr := string(content)
contentStr = strings.ReplaceAll(contentStr, "$ARGUMENTS", arguments)
// Process bash commands if any exist
processedContent, localErr := a.processBashCommands(contentStr)
if localErr != nil {
slog.Error("Failed to process bash commands", "error", localErr)
return a, toast.NewErrorToast("Failed to process bash commands: " + localErr.Error())
}
// Send the processed command content as a message to the LLM
a.app, cmd = a.app.SendChatMessage(context.Background(), processedContent, []opencode.FilePartParam{})
return a, cmd
}
// processBashCommands processes bash commands prefixed with ! in the content
func (a appModel) processBashCommands(content string) (string, error) {
lines := strings.Split(content, "\n")
var commands []string
var cleanLines []string
// Parse bash commands and clean content
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "!") {
command := strings.TrimSpace(trimmed[1:])
if command != "" {
commands = append(commands, command)
}
} else {
cleanLines = append(cleanLines, line)
}
}
// If no bash commands found, return original content
if len(commands) == 0 {
return content, nil
}
slog.Info("Processing bash commands", "count", len(commands))
// Execute bash commands and collect results
var contextSection strings.Builder
contextSection.WriteString("\n\n## Command Context\n\n")
contextSection.WriteString("The following bash commands were executed to gather context:\n\n")
for _, command := range commands {
result, err := a.executeBashCommand(command)
if err != nil {
slog.Error("Failed to execute bash command", "command", command, "error", err)
contextSection.WriteString(fmt.Sprintf("### Command: `%s`\n\n", command))
contextSection.WriteString(fmt.Sprintf("*Command failed: %s*\n\n", err.Error()))
continue
}
contextSection.WriteString(fmt.Sprintf("### Command: `%s`\n\n", command))
if result.ExitCode == 0 {
if strings.TrimSpace(result.Stdout) != "" {
contextSection.WriteString("```\n")
contextSection.WriteString(strings.TrimSpace(result.Stdout))
contextSection.WriteString("\n```\n\n")
} else {
contextSection.WriteString("*No output*\n\n")
}
} else {
contextSection.WriteString(fmt.Sprintf("*Command failed with exit code %d*\n\n", result.ExitCode))
if strings.TrimSpace(result.Stderr) != "" {
contextSection.WriteString("```\n")
contextSection.WriteString(strings.TrimSpace(result.Stderr))
contextSection.WriteString("\n```\n\n")
}
}
}
// Combine clean content with command context
cleanContent := strings.Join(cleanLines, "\n")
return cleanContent + contextSection.String(), nil
}
// BashCommandResult represents the result of executing a bash command
type BashCommandResult struct {
Command string
Stdout string
Stderr string
ExitCode int
}
// executeBashCommand executes a single bash command and returns the result
func (a appModel) executeBashCommand(command string) (*BashCommandResult, error) {
// List of banned commands for security
bannedCommands := []string{
"axel", "aria2c", "nc", "telnet", "lynx", "w3m", "links", "xh", "chrome", "firefox", "safari",
}
// Check if command is banned
for _, banned := range bannedCommands {
if strings.HasPrefix(command, banned) {
return nil, fmt.Errorf("command '%s' is not allowed", command)
}
}
// Execute the command
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "bash", "-c", command)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
exitCode := 0
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
exitCode = exitError.ExitCode()
} else {
return nil, fmt.Errorf("failed to execute command: %w", err)
}
}
return &BashCommandResult{
Command: command,
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: exitCode,
}, nil
}
// getAvailableCustomCommands returns a list of available custom commands
func (a appModel) getAvailableCustomCommands() ([]CustomCommandFile, error) {
var commands []CustomCommandFile
// Get global commands from ~/.config/opencode/commands
globalCommandsDir := filepath.Join(a.app.Info.Path.Config, "commands")
globalCommands, err := a.scanCommandsDirectory(globalCommandsDir, "")
if err != nil {
globalCommands = []CustomCommandFile{}
}
commands = append(commands, globalCommands...)
// Get project-level commands from $PWD/.opencode/commands
cwd, err := os.Getwd()
if err == nil {
projectCommandsDir := filepath.Join(cwd, ".opencode", "commands")
projectCommands, err := a.scanCommandsDirectory(projectCommandsDir, "")
if err == nil {
commands = append(commands, projectCommands...)
}
}
return commands, nil
}
// scanCommandsDirectory recursively scans a directory for markdown command files
func (a appModel) scanCommandsDirectory(baseDir, relativePath string) ([]CustomCommandFile, error) {
var commands []CustomCommandFile
currentDir := filepath.Join(baseDir, relativePath)
err := filepath.WalkDir(currentDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
if strings.Contains(err.Error(), "no such file or directory") {
return nil
}
return err
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(d.Name(), ".md") {
return nil
}
// Calculate relative path from base commands directory
relPath, err := filepath.Rel(baseDir, path)
if err != nil {
return err
}
// Convert file path to command name with colon notation
name := strings.TrimSuffix(relPath, ".md")
name = strings.ReplaceAll(name, string(filepath.Separator), ":")
commands = append(commands, CustomCommandFile{
Name: name,
Filename: d.Name(),
Content: "",
Description: "",
})
return nil
})
if err != nil {
return nil, err
}
return commands, nil
// Server execution failed
slog.Error("Failed to execute custom command via server", "command", commandName, "error", err)
return a, toast.NewErrorToast("Failed to execute custom command: " + commandName)
}
func NewModel(app *app.App) tea.Model {