mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +00:00
chore: Clean up unused code and call commands endpoint for commands
This commit is contained in:
parent
f70e06a283
commit
ae629b159a
5 changed files with 42 additions and 407 deletions
|
@ -110,8 +110,12 @@ func New(
|
||||||
|
|
||||||
slog.Debug("Loaded config", "config", configInfo)
|
slog.Debug("Loaded config", "config", configInfo)
|
||||||
|
|
||||||
// Create commands client - assuming server runs on same host with port 3000
|
// Create commands client using the same base URL as the HTTP client
|
||||||
commandsClient := commands.NewCommandsClient("http://localhost:3000")
|
baseURL := os.Getenv("OPENCODE_SERVER")
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = "http://localhost:4096" // Default fallback
|
||||||
|
}
|
||||||
|
commandsClient := commands.NewCommandsClient(baseURL)
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
Info: appInfo,
|
Info: appInfo,
|
||||||
|
|
|
@ -56,7 +56,7 @@ func NewCommandsClient(baseURL string) *CommandsClient {
|
||||||
|
|
||||||
// ListCustomCommands fetches all available custom commands from the server
|
// ListCustomCommands fetches all available custom commands from the server
|
||||||
func (c *CommandsClient) ListCustomCommands(ctx context.Context) ([]CustomCommand, error) {
|
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)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != 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
|
// GetCustomCommand fetches a specific custom command from the server
|
||||||
func (c *CommandsClient) GetCustomCommand(ctx context.Context, name string) (*CustomCommand, error) {
|
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)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != 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
|
// ExecuteCustomCommand executes a custom command on the server
|
||||||
func (c *CommandsClient) ExecuteCustomCommand(ctx context.Context, name string, arguments *string) (*ExecuteCommandResponse, error) {
|
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{
|
reqBody := ExecuteCommandRequest{
|
||||||
Arguments: arguments,
|
Arguments: arguments,
|
||||||
|
|
|
@ -314,9 +314,9 @@ func IsValidCustomCommand(commandName string, configPath string) bool {
|
||||||
return false
|
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 {
|
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 {
|
if client != nil {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
exists, err := client.CustomCommandExists(ctx, commandName)
|
exists, err := client.CustomCommandExists(ctx, commandName)
|
||||||
|
@ -325,8 +325,8 @@ func IsValidCustomCommandWithClient(commandName string, configPath string, clien
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to local filesystem check
|
// Return false if server is not available or returns an error
|
||||||
return IsValidCustomCommand(commandName, configPath)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package completions
|
package completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"io/fs"
|
"fmt"
|
||||||
"os"
|
"log/slog"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"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) {
|
func (c *CommandCompletionProvider) getCustomCommands() ([]CustomCommandFile, error) {
|
||||||
// Try to get commands from server first
|
// Get commands from server endpoint
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
serverCommands, err := c.app.CommandsClient.ListCustomCommands(ctx)
|
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 {
|
if err != nil {
|
||||||
// Log error but continue with project commands
|
return nil, fmt.Errorf("failed to get commands from server: %w", err)
|
||||||
globalCommands = []CustomCommandFile{}
|
|
||||||
}
|
}
|
||||||
commands = append(commands, globalCommands...)
|
|
||||||
|
|
||||||
// Get project-level commands from $PWD/.opencode/commands
|
slog.Debug("Server commands:" + strconv.Itoa(len(serverCommands)))
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err == nil {
|
// Convert server commands to local format
|
||||||
projectCommandsDir := filepath.Join(cwd, ".opencode", "commands")
|
var commands []CustomCommandFile
|
||||||
projectCommands, err := c.scanCommandsDirectory(projectCommandsDir, "")
|
for _, cmd := range serverCommands {
|
||||||
if err == nil {
|
description := ""
|
||||||
commands = append(commands, projectCommands...)
|
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
|
// Sort commands alphabetically
|
||||||
|
@ -168,57 +98,6 @@ func (c *CommandCompletionProvider) getCustomCommands() ([]CustomCommandFile, er
|
||||||
return commands, nil
|
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) {
|
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
commands := c.app.Commands
|
commands := c.app.Commands
|
||||||
|
@ -226,7 +105,7 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
||||||
// Get custom commands
|
// Get custom commands
|
||||||
customCommands, err := c.getCustomCommands()
|
customCommands, err := c.getCustomCommands()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log error but continue with built-in commands
|
// If server is not available, return only built-in commands
|
||||||
customCommands = []CustomCommandFile{}
|
customCommands = []CustomCommandFile{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -36,14 +32,6 @@ import (
|
||||||
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
|
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
|
||||||
type InterruptDebounceTimeoutMsg struct{}
|
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
|
// InterruptKeyState tracks the state of interrupt key presses for debouncing
|
||||||
type InterruptKeyState int
|
type InterruptKeyState int
|
||||||
|
|
||||||
|
@ -1018,16 +1006,15 @@ func (a appModel) parseCustomCommand(input string) (commandName, arguments strin
|
||||||
arguments = parts[1]
|
arguments = parts[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this command exists as a custom command
|
// Check if this command exists as a custom command via server
|
||||||
customCommands, err := a.getAvailableCustomCommands()
|
ctx := context.Background()
|
||||||
|
exists, err := a.app.CommandsClient.CustomCommandExists(ctx, commandName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range customCommands {
|
if exists {
|
||||||
if cmd.Name == commandName {
|
return commandName, arguments, true
|
||||||
return commandName, arguments, true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", "", false
|
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) {
|
func (a appModel) executeCustomCommandWithArgs(commandName, arguments string) (tea.Model, tea.Cmd) {
|
||||||
slog.Debug("Executing custom command with arguments", "command", commandName, "arguments", arguments)
|
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()
|
ctx := context.Background()
|
||||||
var args *string
|
var args *string
|
||||||
if arguments != "" {
|
if arguments != "" {
|
||||||
|
@ -1053,244 +1040,9 @@ func (a appModel) executeCustomCommandWithArgs(commandName, arguments string) (t
|
||||||
return a, cmd
|
return a, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to local execution if server is not available
|
// Server execution failed
|
||||||
slog.Warn("Server execution failed, falling back to local execution", "command", commandName, "error", err)
|
slog.Error("Failed to execute custom command via server", "command", commandName, "error", err)
|
||||||
|
return a, toast.NewErrorToast("Failed to execute custom command: " + commandName)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(app *app.App) tea.Model {
|
func NewModel(app *app.App) tea.Model {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue