diff --git a/README.md b/README.md index 3ac4dcb..1ca785d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ A powerful command-line utility for debugging, monitoring, and inspecting A2A se ## 🚀 Features - **Server Connectivity**: Test connections to A2A servers and retrieve agent information +- **Interactive Mode**: Chat interface with streaming and background modes using Bubble Tea UI - **Task Management**: List, filter, and inspect tasks with detailed status information - **Real-time Streaming**: Submit streaming tasks and monitor real-time agent responses - **Streaming Summaries**: Summaries with Task IDs, durations, and event counts @@ -98,6 +99,12 @@ List all tasks: a2a tasks list ``` +Start interactive chat mode: + +```bash +a2a interactive +``` + Get specific task details: ```bash @@ -132,6 +139,12 @@ a2a tasks submit # Submit a task and get response a2a tasks submit-streaming # Submit streaming task with real-time responses and summary ``` +#### Interactive Mode + +```bash +a2a interactive # Start interactive chat mode with A2A server +``` + #### Server Commands ```bash @@ -171,6 +184,45 @@ insecure: false - `--history-length`: Number of history messages to include +### Interactive Mode + +The interactive mode provides a chat-like interface for communicating with A2A agents in real-time: + +```bash +a2a interactive +``` + +**Features:** +- **Dual Modes**: Switch between Streaming (realtime) and Background (long running tasks) modes +- **Live Chat**: Type messages and get real-time responses +- **Bubble Tea UI**: Clean terminal interface with message history and status updates +- **Mode Switching**: Press Tab to switch between streaming and background modes +- **Auto-scrolling**: Message history automatically scrolls to show latest messages +- **Error Handling**: Clear error messages and connection status + +**Controls:** +- Type your message and press **Enter** to send +- Press **Tab** to switch between streaming/background modes +- Press **Ctrl+C** to quit + +**Example Session:** +``` +🤖 A2A Interactive Chat - Streaming Mode + +Context ID: ctx-1725670123 + +â„šī¸ 🚀 Interactive A2A Chat Session Started +â„šī¸ 📡 Mode: Streaming (realtime) +â„šī¸ đŸ’Ŧ Type your message and press Enter to send +â„šī¸ âŒ¨ī¸ Press Tab to switch modes, Ctrl+C to quit +👤 You: Hello, can you help me with my project? +🤖 Agent: Hello! I'd be happy to help you with your project. What kind of project are you working on? + +Interactive Mode - Press Tab to switch modes, Ctrl+C to quit + +đŸ’Ŧ Hello, can you help me debug this code? +``` + ### Examples #### Configuration Management diff --git a/cli/interactive.go b/cli/interactive.go new file mode 100644 index 0000000..2a8c912 --- /dev/null +++ b/cli/interactive.go @@ -0,0 +1,589 @@ +package cli + +import ( + "context" + "encoding/json" + "fmt" + "log" + "strings" + "time" + + a2a "github.com/inference-gateway/a2a-debugger/a2a" + client "github.com/inference-gateway/adk/client" + adk "github.com/inference-gateway/adk/types" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + cobra "github.com/spf13/cobra" +) + +// Interactive mode types +type sessionMode int + +const ( + StreamingMode sessionMode = iota + BackgroundMode +) + +type chatModel struct { + // UI State + input string + messages []chatMessage + viewport viewport + mode sessionMode + ready bool + quitting bool + + // A2A State + contextID string + taskID string + client client.A2AClient + + // Status + statusLine string + isWaiting bool + lastResponse time.Time + + // Config + width int + height int +} + +type chatMessage struct { + content string + role string + timestamp time.Time + isError bool +} + +type viewport struct { + content []string + offset int +} + +// Bubble Tea messages +type tickMsg time.Time +type responseMsg struct { + message string + error error +} +type streamEventMsg struct { + event interface{} + error error +} + +var interactiveCmd = &cobra.Command{ + Use: "interactive", + Short: "Start interactive chat mode with A2A server", + Long: `Start an interactive chat session with the A2A server. +Supports both streaming (realtime) and background (long running tasks) modes. + +Use Ctrl+C to quit, Tab to switch between streaming/background modes.`, + RunE: func(cmd *cobra.Command, args []string) error { + ensureA2AClient() + + // Test connection first + ctx := context.Background() + _, err := a2aClient.GetAgentCard(ctx) + if err != nil { + return fmt.Errorf("failed to connect to A2A server: %w", err) + } + + // Initialize model + m := initialModel() + m.client = a2aClient + + // Start Bubble Tea program + p := tea.NewProgram(m, tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + log.Fatal(err) + } + + return nil + }, +} + +func initialModel() chatModel { + return chatModel{ + input: "", + messages: []chatMessage{}, + viewport: viewport{content: []string{}, offset: 0}, + mode: StreamingMode, + ready: false, + quitting: false, + contextID: fmt.Sprintf("ctx-%d", time.Now().Unix()), + taskID: "", + statusLine: "Interactive Mode - Press Tab to switch modes, Ctrl+C to quit", + isWaiting: false, + width: 80, + height: 24, + } +} + +func (m chatModel) Init() tea.Cmd { + m.addSystemMessage("🚀 Interactive A2A Chat Session Started") + m.addSystemMessage("📡 Mode: Streaming (realtime)") + m.addSystemMessage("đŸ’Ŧ Type your message and press Enter to send") + m.addSystemMessage("âŒ¨ī¸ Press Tab to switch modes, Ctrl+C to quit") + return tea.Tick(time.Second, func(t time.Time) tea.Msg { + return tickMsg(t) + }) +} + +func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + m.ready = true + return m, nil + + case tea.KeyMsg: + if m.quitting { + return m, tea.Quit + } + + switch msg.String() { + case "ctrl+c": + m.quitting = true + return m, tea.Quit + + case "tab": + if m.mode == StreamingMode { + m.mode = BackgroundMode + m.addSystemMessage("🔄 Switched to Background Mode (long running tasks)") + } else { + m.mode = StreamingMode + m.addSystemMessage("🔄 Switched to Streaming Mode (realtime)") + } + return m, nil + + case "enter": + if m.input != "" && !m.isWaiting { + return m.sendMessage() + } + return m, nil + + case "backspace": + if len(m.input) > 0 { + m.input = m.input[:len(m.input)-1] + } + return m, nil + + default: + if !m.isWaiting { + m.input += msg.String() + } + return m, nil + } + + case responseMsg: + m.isWaiting = false + m.lastResponse = time.Now() + if msg.error != nil { + m.addErrorMessage(fmt.Sprintf("Error: %v", msg.error)) + } else { + m.addAssistantMessage(msg.message) + } + return m, nil + + case streamEventMsg: + if msg.error != nil { + m.isWaiting = false + m.addErrorMessage(fmt.Sprintf("Stream error: %v", msg.error)) + } else { + m.handleStreamEvent(msg.event) + } + return m, nil + + case tickMsg: + return m, tea.Tick(time.Second, func(t time.Time) tea.Msg { + return tickMsg(t) + }) + } + + return m, nil +} + +func (m chatModel) View() string { + if !m.ready { + return "Initializing interactive chat..." + } + + var b strings.Builder + + // Header + modeStr := "Streaming" + if m.mode == BackgroundMode { + modeStr = "Background" + } + + headerStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("99")). + Background(lipgloss.Color("235")). + Padding(0, 1) + + header := headerStyle.Render(fmt.Sprintf("🤖 A2A Interactive Chat - %s Mode", modeStr)) + b.WriteString(header + "\n") + + // Context info + contextStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + Margin(1, 0) + + contextInfo := contextStyle.Render(fmt.Sprintf("Context ID: %s", m.contextID)) + if m.taskID != "" { + contextInfo += contextStyle.Render(fmt.Sprintf(" | Task ID: %s", m.taskID)) + } + b.WriteString(contextInfo + "\n") + + // Messages area + messagesHeight := m.height - 8 // Reserve space for header, input, etc. + messages := m.renderMessages(messagesHeight) + b.WriteString(messages) + + // Status line + statusStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("243")). + Italic(true). + Margin(1, 0) + + status := m.statusLine + if m.isWaiting { + status = "âŗ Waiting for response..." + } + b.WriteString(statusStyle.Render(status) + "\n") + + // Input area + inputStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("99")). + Padding(0, 1) + + prompt := "đŸ’Ŧ " + if m.isWaiting { + prompt = "âŗ " + } + + input := inputStyle.Render(fmt.Sprintf("%s%s", prompt, m.input)) + b.WriteString(input) + + return b.String() +} + +func (m *chatModel) sendMessage() (tea.Model, tea.Cmd) { + message := strings.TrimSpace(m.input) + if message == "" { + return *m, nil + } + + m.addUserMessage(message) + m.input = "" + m.isWaiting = true + + messageID := fmt.Sprintf("msg-%d", time.Now().Unix()) + + params := adk.MessageSendParams{ + Message: adk.Message{ + Kind: "message", + MessageID: messageID, + Role: "user", + Parts: []adk.Part{ + map[string]interface{}{ + "kind": "text", + "text": message, + }, + }, + }, + } + + params.Message.ContextID = &m.contextID + if m.taskID != "" { + params.Message.TaskID = &m.taskID + } + + if m.mode == StreamingMode { + return *m, m.sendStreamingMessage(params) + } else { + return *m, m.sendBackgroundMessage(params) + } +} + +func (m *chatModel) sendBackgroundMessage(params adk.MessageSendParams) tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + resp, err := m.client.SendTask(ctx, params) + if err != nil { + return responseMsg{message: "", error: handleA2AError(err, "message/send")} + } + + resultBytes, err := json.Marshal(resp.Result) + if err != nil { + return responseMsg{message: "", error: fmt.Errorf("failed to marshal response: %w", err)} + } + + var task adk.Task + if err := json.Unmarshal(resultBytes, &task); err != nil { + return responseMsg{message: "", error: fmt.Errorf("failed to unmarshal task: %w", err)} + } + + // Update task ID for future messages + m.taskID = task.ID + + // Extract response message + if task.Status.Message != nil && len(task.Status.Message.Parts) > 0 { + var responseText strings.Builder + for _, part := range task.Status.Message.Parts { + if partMap, ok := part.(map[string]interface{}); ok { + if kind, ok := partMap["kind"].(string); ok && kind == "text" { + if text, ok := partMap["text"].(string); ok { + responseText.WriteString(text) + } + } + } + } + return responseMsg{message: responseText.String(), error: nil} + } + + return responseMsg{message: fmt.Sprintf("Task submitted (Status: %s)", task.Status.State), error: nil} + } +} + +func (m *chatModel) sendStreamingMessage(params adk.MessageSendParams) tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + eventChan := make(chan interface{}, 100) + + go func() { + defer close(eventChan) + err := m.client.SendTaskStreaming(ctx, params, eventChan) + if err != nil { + eventChan <- streamEventMsg{event: nil, error: handleA2AError(err, "message/send")} + } + }() + + // Process first event + select { + case event := <-eventChan: + if event == nil { + return streamEventMsg{event: nil, error: fmt.Errorf("no response received")} + } + return streamEventMsg{event: event, error: nil} + case <-time.After(30 * time.Second): + return streamEventMsg{event: nil, error: fmt.Errorf("timeout waiting for response")} + } + } +} + +func (m *chatModel) handleStreamEvent(event interface{}) { + eventJSON, err := json.Marshal(event) + if err != nil { + m.addErrorMessage(fmt.Sprintf("Failed to parse stream event: %v", err)) + return + } + + var genericEvent map[string]interface{} + if err := json.Unmarshal(eventJSON, &genericEvent); err != nil { + m.addErrorMessage(fmt.Sprintf("Failed to unmarshal stream event: %v", err)) + return + } + + kind, ok := genericEvent["kind"].(string) + if !ok { + m.addErrorMessage("Stream event missing kind field") + return + } + + switch kind { + case "status-update": + var statusEvent a2a.TaskStatusUpdateEvent + if err := json.Unmarshal(eventJSON, &statusEvent); err != nil { + m.addErrorMessage(fmt.Sprintf("Failed to parse status event: %v", err)) + return + } + + // Update task ID + if m.taskID == "" { + m.taskID = statusEvent.TaskID + } + + // Process message parts + if statusEvent.Status.Message != nil && len(statusEvent.Status.Message.Parts) > 0 { + var responseText strings.Builder + for _, part := range statusEvent.Status.Message.Parts { + if partMap, ok := part.(map[string]interface{}); ok { + if kind, ok := partMap["kind"].(string); ok && kind == "text" { + if text, ok := partMap["text"].(string); ok { + responseText.WriteString(text) + } + } + } + } + if responseText.Len() > 0 { + m.addAssistantMessage(responseText.String()) + } + } + + if statusEvent.Final { + m.isWaiting = false + } + + case "artifact-update": + var artifactEvent a2a.TaskArtifactUpdateEvent + if err := json.Unmarshal(eventJSON, &artifactEvent); err != nil { + m.addErrorMessage(fmt.Sprintf("Failed to parse artifact event: %v", err)) + return + } + + artifactInfo := fmt.Sprintf("📄 Artifact: %s", artifactEvent.Artifact.ArtifactID) + if artifactEvent.Artifact.Name != nil { + artifactInfo += fmt.Sprintf(" (%s)", *artifactEvent.Artifact.Name) + } + m.addSystemMessage(artifactInfo) + + // Update task ID + if m.taskID == "" { + m.taskID = artifactEvent.TaskID + } + } +} + +func (m *chatModel) addUserMessage(content string) { + m.addMessage(chatMessage{ + content: content, + role: "user", + timestamp: time.Now(), + isError: false, + }) +} + +func (m *chatModel) addAssistantMessage(content string) { + m.addMessage(chatMessage{ + content: content, + role: "assistant", + timestamp: time.Now(), + isError: false, + }) +} + +func (m *chatModel) addSystemMessage(content string) { + m.addMessage(chatMessage{ + content: content, + role: "system", + timestamp: time.Now(), + isError: false, + }) +} + +func (m *chatModel) addErrorMessage(content string) { + m.addMessage(chatMessage{ + content: content, + role: "error", + timestamp: time.Now(), + isError: true, + }) +} + +func (m *chatModel) addMessage(msg chatMessage) { + m.messages = append(m.messages, msg) +} + +func (m *chatModel) renderMessages(height int) string { + if len(m.messages) == 0 { + return "" + } + + var lines []string + for _, msg := range m.messages { + lines = append(lines, m.renderMessage(msg)...) + } + + // Handle viewport scrolling + start := 0 + if len(lines) > height { + start = len(lines) - height + } + + var result strings.Builder + for i := start; i < len(lines) && i < start+height; i++ { + result.WriteString(lines[i] + "\n") + } + + return result.String() +} + +func (m *chatModel) renderMessage(msg chatMessage) []string { + var style lipgloss.Style + var prefix string + + switch msg.role { + case "user": + style = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) + prefix = "👤 You: " + case "assistant": + style = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) + prefix = "🤖 Agent: " + case "system": + style = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) + prefix = "â„šī¸ " + case "error": + style = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) + prefix = "❌ " + } + + if msg.isError { + style = style.Foreground(lipgloss.Color("9")) + } + + // Wrap long messages + maxWidth := m.width - 10 + if maxWidth < 50 { + maxWidth = 50 + } + + lines := wrapText(msg.content, maxWidth) + var styledLines []string + + for i, line := range lines { + if i == 0 { + styledLines = append(styledLines, style.Render(prefix+line)) + } else { + styledLines = append(styledLines, style.Render(" "+line)) + } + } + + return styledLines +} + +// Simple text wrapping function +func wrapText(text string, width int) []string { + if len(text) <= width { + return []string{text} + } + + words := strings.Fields(text) + var lines []string + var currentLine strings.Builder + + for _, word := range words { + if currentLine.Len()+len(word)+1 > width && currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + currentLine.Reset() + } + if currentLine.Len() > 0 { + currentLine.WriteString(" ") + } + currentLine.WriteString(word) + } + + if currentLine.Len() > 0 { + lines = append(lines, currentLine.String()) + } + + return lines +} + +func init() { + rootCmd.AddCommand(interactiveCmd) +} \ No newline at end of file diff --git a/go.mod b/go.mod index 611142f..e814483 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/inference-gateway/a2a-debugger go 1.24 require ( + github.com/charmbracelet/bubbletea v1.3.7 + github.com/charmbracelet/lipgloss v1.1.0 github.com/inference-gateway/adk v0.7.4 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 @@ -10,13 +12,27 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -25,9 +41,10 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.26.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 8281f74..755d0d8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,23 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.7 h1:FNaEEFEenOEPnZsY9MI64thl2c84MI66+1QaQbxGOl4= +github.com/charmbracelet/bubbletea v1.3.7/go.mod h1:PEOcbQCNzJ2BYUd484kHPO5g3kLO28IffOdFeI2EWus= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -21,14 +37,31 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -52,6 +85,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -60,8 +95,10 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=