package external import ( "bytes" "context" "fmt" "os" "os/exec" "time" ) // Tool represents an external command-line tool type Tool interface { Name() string BinaryName() string IsInstalled() bool Version() (string, error) Execute(ctx context.Context, args ...string) ([]byte, error) ExecuteWithInput(ctx context.Context, input string, args ...string) ([]byte, error) } // BaseTool provides common functionality for external tools type BaseTool struct { name string binaryName string binaryPath string timeout time.Duration } // NewBaseTool creates a new base tool func NewBaseTool(name, binaryName string) *BaseTool { return &BaseTool{ name: name, binaryName: binaryName, timeout: 5 * time.Minute, // Default timeout } } // Name returns the tool name func (t *BaseTool) Name() string { return t.name } // BinaryName returns the binary name func (t *BaseTool) BinaryName() string { return t.binaryName } // IsInstalled checks if the tool is available in PATH func (t *BaseTool) IsInstalled() bool { if t.binaryPath == "" { path, err := exec.LookPath(t.binaryName) if err != nil { return false } t.binaryPath = path } return true } // Version returns the tool version func (t *BaseTool) Version() (string, error) { if !t.IsInstalled() { return "", fmt.Errorf("tool %s not installed", t.name) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() output, err := t.Execute(ctx, "--version") if err != nil { // Try alternative version flags output, err = t.Execute(ctx, "version") if err != nil { output, err = t.Execute(ctx, "-v") if err != nil { return "", fmt.Errorf("getting version: %w", err) } } } return string(output), nil } // Execute runs the tool with given arguments func (t *BaseTool) Execute(ctx context.Context, args ...string) ([]byte, error) { return t.ExecuteWithInput(ctx, "", args...) } // ExecuteWithInput runs the tool with stdin input func (t *BaseTool) ExecuteWithInput(ctx context.Context, input string, args ...string) ([]byte, error) { if !t.IsInstalled() { return nil, fmt.Errorf("tool %s not installed", t.name) } // Create command with timeout ctx, cancel := context.WithTimeout(ctx, t.timeout) defer cancel() cmd := exec.CommandContext(ctx, t.binaryPath, args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if input != "" { cmd.Stdin = bytes.NewBufferString(input) } // Set environment cmd.Env = os.Environ() err := cmd.Run() if err != nil { return nil, fmt.Errorf("executing %s %v: %w\nstderr: %s", t.name, args, err, stderr.String()) } return stdout.Bytes(), nil } // SetTimeout sets the execution timeout func (t *BaseTool) SetTimeout(timeout time.Duration) { t.timeout = timeout } // SetBinaryPath explicitly sets the binary path (useful for testing) func (t *BaseTool) SetBinaryPath(path string) { t.binaryPath = path }