|
@@ -0,0 +1,166 @@
|
|
|
|
|
+package dialog
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+
|
|
|
|
|
+ tea "github.com/charmbracelet/bubbletea"
|
|
|
|
|
+ "github.com/opencode-ai/opencode/internal/config"
|
|
|
|
|
+ "github.com/opencode-ai/opencode/internal/tui/util"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// Command prefix constants
|
|
|
|
|
+const (
|
|
|
|
|
+ UserCommandPrefix = "user:"
|
|
|
|
|
+ ProjectCommandPrefix = "project:"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
|
|
|
|
|
+func LoadCustomCommands() ([]Command, error) {
|
|
|
|
|
+ cfg := config.Get()
|
|
|
|
|
+ if cfg == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("config not loaded")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var commands []Command
|
|
|
|
|
+
|
|
|
|
|
+ // Load user commands from XDG_CONFIG_HOME/opencode/commands
|
|
|
|
|
+ xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
|
|
|
|
|
+ if xdgConfigHome == "" {
|
|
|
|
|
+ // Default to ~/.config if XDG_CONFIG_HOME is not set
|
|
|
|
|
+ home, err := os.UserHomeDir()
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ xdgConfigHome = filepath.Join(home, ".config")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if xdgConfigHome != "" {
|
|
|
|
|
+ userCommandsDir := filepath.Join(xdgConfigHome, "opencode", "commands")
|
|
|
|
|
+ userCommands, err := loadCommandsFromDir(userCommandsDir, UserCommandPrefix)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ // Log error but continue - we'll still try to load other commands
|
|
|
|
|
+ fmt.Printf("Warning: failed to load user commands from XDG_CONFIG_HOME: %v\n", err)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ commands = append(commands, userCommands...)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Load commands from $HOME/.opencode/commands
|
|
|
|
|
+ home, err := os.UserHomeDir()
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ homeCommandsDir := filepath.Join(home, ".opencode", "commands")
|
|
|
|
|
+ homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ // Log error but continue - we'll still try to load other commands
|
|
|
|
|
+ fmt.Printf("Warning: failed to load home commands: %v\n", err)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ commands = append(commands, homeCommands...)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Load project commands from data directory
|
|
|
|
|
+ projectCommandsDir := filepath.Join(cfg.Data.Directory, "commands")
|
|
|
|
|
+ projectCommands, err := loadCommandsFromDir(projectCommandsDir, ProjectCommandPrefix)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ // Log error but return what we have so far
|
|
|
|
|
+ fmt.Printf("Warning: failed to load project commands: %v\n", err)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ commands = append(commands, projectCommands...)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return commands, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// loadCommandsFromDir loads commands from a specific directory with the given prefix
|
|
|
|
|
+func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
|
|
|
|
|
+ // Check if the commands directory exists
|
|
|
|
|
+ if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
|
|
|
|
|
+ // Create the commands directory if it doesn't exist
|
|
|
|
|
+ if err := os.MkdirAll(commandsDir, 0755); err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("failed to create commands directory %s: %w", commandsDir, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Return empty list since we just created the directory
|
|
|
|
|
+ return []Command{}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var commands []Command
|
|
|
|
|
+
|
|
|
|
|
+ // Walk through the commands directory and load all .md files
|
|
|
|
|
+ err := filepath.Walk(commandsDir, func(path string, info os.FileInfo, err error) error {
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Skip directories
|
|
|
|
|
+ if info.IsDir() {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Only process markdown files
|
|
|
|
|
+ if !strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Read the file content
|
|
|
|
|
+ content, err := os.ReadFile(path)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Errorf("failed to read command file %s: %w", path, err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get the command ID from the file name without the .md extension
|
|
|
|
|
+ commandID := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
|
|
|
|
|
+
|
|
|
|
|
+ // Get relative path from commands directory
|
|
|
|
|
+ relPath, err := filepath.Rel(commandsDir, path)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Errorf("failed to get relative path for %s: %w", path, err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create the command ID from the relative path
|
|
|
|
|
+ // Replace directory separators with colons
|
|
|
|
|
+ commandIDPath := strings.ReplaceAll(filepath.Dir(relPath), string(filepath.Separator), ":")
|
|
|
|
|
+ if commandIDPath != "." {
|
|
|
|
|
+ commandID = commandIDPath + ":" + commandID
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create a command
|
|
|
|
|
+ command := Command{
|
|
|
|
|
+ ID: prefix + commandID,
|
|
|
|
|
+ Title: prefix + commandID,
|
|
|
|
|
+ Description: fmt.Sprintf("Custom command from %s", relPath),
|
|
|
|
|
+ Handler: func(cmd Command) tea.Cmd {
|
|
|
|
|
+ commandContent := string(content)
|
|
|
|
|
+
|
|
|
|
|
+ // Check if the command contains $ARGUMENTS placeholder
|
|
|
|
|
+ if strings.Contains(commandContent, "$ARGUMENTS") {
|
|
|
|
|
+ // Show arguments dialog
|
|
|
|
|
+ return util.CmdHandler(ShowArgumentsDialogMsg{
|
|
|
|
|
+ CommandID: cmd.ID,
|
|
|
|
|
+ Content: commandContent,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // No arguments needed, run command directly
|
|
|
|
|
+ return util.CmdHandler(CommandRunCustomMsg{
|
|
|
|
|
+ Content: commandContent,
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ commands = append(commands, command)
|
|
|
|
|
+ return nil
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("failed to load custom commands from %s: %w", commandsDir, err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return commands, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// CommandRunCustomMsg is sent when a custom command is executed
|
|
|
|
|
+type CommandRunCustomMsg struct {
|
|
|
|
|
+ Content string
|
|
|
|
|
+}
|