diff --git a/COMMANDS.md b/COMMANDS.md index d8e7684..df4437b 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -21,11 +21,11 @@ MateCommit es simple de usar. La idea es que te ayude a hacer commits más copad ### Instalación Básica ```bash -# Configurar el idioma a español -matecommit config set-lang --lang es +# Configuración interactiva completa (recomendado) +matecommit config init -# Configurar tu API key de Gemini -matecommit config set-api-key --key tu-api-key +# O si solo querés ver la configuración actual +matecommit config show ``` ## Comandos Principales @@ -50,7 +50,18 @@ matecommit s --no-emoji ### Configuración Básica -#### Ver toda la configuración +#### Configuración interactiva completa +```bash +matecommit config init +``` + +Este comando te guía paso a paso para configurar: +- 🌍 **Idioma**: Español o inglés +- 🤖 **IA**: API key de Gemini y modelo +- 🔧 **VCS**: Token de GitHub para resúmenes de PR +- 🎫 **Tickets**: Integración con Jira (opcional) + +#### Ver configuración actual ```bash matecommit config show ``` @@ -67,58 +78,74 @@ Modelos de IA configurados: - gemini: gemini-1.5-pro ``` -#### Configurar API Key +#### Editar configuración manualmente ```bash -# Configurar la API key de Gemini -matecommit config set-api-key --key tu-api-key +matecommit config edit ``` +Abre el archivo de configuración en tu editor preferido para editarlo manualmente. + ### Configuración de IA -Ahora podés elegir entre diferentes IAs y modelos: +La configuración de IA se hace a través del comando `config init`: ```bash -# Ver las IAs disponibles -matecommit config set-ai-active +# Configuración interactiva que incluye IA +matecommit config init +``` -# Activar Gemini -matecommit config set-ai-active gemini +Durante el proceso te va a preguntar: +- 🤖 **API Key de Gemini**: Tu clave para usar Gemini +- 🧠 **Modelo**: Qué modelo usar (gemini-1.5-flash, gemini-1.5-pro, etc.) -# Configurar el modelo de Gemini -matecommit config set-ai-model gemini gemini-1.5-pro +**Nota**: Actualmente solo soporta Gemini, pero próximamente vamos a agregar OpenAI y Claude. -# O si preferís OpenAI -matecommit config set-ai-active openai -matecommit config set-ai-model openai gpt-4 +### Integración con Jira + +La configuración de Jira también se hace con `config init`: + +```bash +# Configuración interactiva que incluye Jira +matecommit config init ``` -### Integración con Jira +Durante el proceso te va a preguntar si querés habilitar Jira y te pedirá: +- 🌐 **Base URL**: La URL de tu instancia de Jira +- 📧 **Email**: Tu email de Jira +- 🔑 **API Token**: Tu token de API de Jira -Si laburás con Jira, tenés estas opciones: +### Configuración de VCS + +La configuración de VCS se hace con `config init`: ```bash -# Configurar las credenciales -matecommit config jira \ - --base-url https://tu-empresa.atlassian.net \ - --api-key tu-api-key \ - --email tu@email.com +# Configuración interactiva que incluye VCS +matecommit config init +``` -# Activar la integración -matecommit config ticket enable +Durante el proceso te va a preguntar si querés habilitar VCS y te pedirá: +- 🔑 **Token de GitHub**: Tu Personal Access Token (recomendamos classic tokens) -# Desactivar la integración -matecommit config ticket disable +**Importante para repositorios de organizaciones**: +- Usá **Personal access tokens (classic)** en lugar de fine-grained tokens +- Los classic tokens funcionan mejor con organizaciones sin necesidad de aprobación + +Una vez configurado, podés usar: +```bash +# Resumir un Pull Request +matecommit summarize-pr --pr-number 42 +matecommit spr -n 42 # alias corto ``` ### Idiomas ```bash -# Cambiar el idioma default -matecommit config set-lang --lang es # español -matecommit config set-lang --lang en # inglés +# Configurar idioma default (se hace en config init) +matecommit config init # te pregunta el idioma # O usar otro idioma solo para una sugerencia matecommit s -l en # sugerencia en inglés +matecommit s -l es # sugerencia en español ``` ## Ejemplos con Salidas @@ -174,56 +201,47 @@ matecommit s 💡 feat(PROJ-123): implementa nuevo endpoint de usuarios ``` -### Configuración de VCS - -Configura proveedores de control de versiones (GitHub, GitLab, etc.): -```bash -+# Configurar un proveedor VCS (ej: GitHub) -+matecommit config set-vcs \ - --provider github \ - --token tu-token \ - --owner tu-usuario \ - --repo tu-repositorio - -# Establecer el proveedor VCS activo -matecommit config set-active-vcs --provider github -# Resumir un Pull Request (requiere VCS configurado) -matecommit summarize-pr --pr-number 42 -matecommit spr -n 42 # alias corto -``` - ### Ejemplo 4: Resumen de PR con VCS ```bash matecommit spr -n 42 -✅ PR #42 actulizado: Implementacion de repository +✅ PR #42 actualizado: Implementación de repository ``` ## Tips y Trucos 1. **Alias Rápidos**: - Usá `s` en lugar de `suggest` + - Usá `spr` en lugar de `summarize-pr` - `config show` te muestra todo de una 2. **Mejores Prácticas**: - Siempre hacé `git add` antes de usar MateCommit - Si no te convence ninguna sugerencia, apretá 0 y pedí más + - Usá classic tokens para GitHub en lugar de fine-grained tokens 3. **Personalización**: - - Probá diferentes IAs hasta encontrar la que mejor te funcione - - Podés tener diferentes modelos configurados para cada IA + - Probá diferentes modelos de Gemini hasta encontrar el que mejor te funcione - Los emojis son opcionales pero le dan más onda 😎 + - Podés tener un idioma default y usar otro para commits específicos 4. **Integración con Jira**: - Activala solo si trabajás con tickets - Te agrega automáticamente el número de ticket en los commits -5. **Idiomas**: - - Podés tener un idioma default y usar otro para commits específicos - - El análisis técnico se adapta al idioma elegido +5. **Configuración**: + - Usá `config init` para configurar todo de una vez + - Si algo no te gusta, podés editarlo con `config edit` + - Siempre podés volver a ejecutar `config init` para cambiar algo + +6. **Repositorios de Organización**: + - Para repos de organizaciones, usá Personal Access Tokens (classic) + - Los fine-grained tokens requieren aprobación de la organización ¿Necesitás más ayuda? Siempre podés usar: ```bash matecommit --help # o para un comando específico matecommit config --help +matecommit suggest --help +matecommit summarize-pr --help ``` \ No newline at end of file diff --git a/README.md b/README.md index 95de1d5..61d45a7 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ 4. **Configuración inicial**: ```bash - # Configura tu API key de Gemini - matecommit config set-api-key --key + # Configuración interactiva completa + matecommit config init - # Establece tu idioma preferido - matecommit config set-lang --lang es # o en para inglés + # O si solo querés ver la configuración actual + matecommit config show ``` ### Desde el código fuente diff --git a/cmd/main.go b/cmd/main.go index b5e78dc..8d63c16 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,10 @@ package main import ( "context" "fmt" + "log" + "net/http" + "os" + "github.com/Tomas-vilte/MateCommit/internal/cli/command/config" "github.com/Tomas-vilte/MateCommit/internal/cli/command/handler" "github.com/Tomas-vilte/MateCommit/internal/cli/command/pr" @@ -16,9 +20,6 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/infrastructure/tickets/jira" "github.com/Tomas-vilte/MateCommit/internal/services" "github.com/urfave/cli/v3" - "log" - "net/http" - "os" ) func main() { @@ -56,12 +57,16 @@ func initializeApp() (*cli.Command, error) { gitService := git.NewGitService() aiProvider, err := gemini.NewGeminiService(context.Background(), cfgApp, translations) if err != nil { - log.Fatalf("Error initializing AI service: %v", err) + log.Printf("Warning: %v", err) + log.Println("La IA no está configurada. Podés configurarla con 'matecommit config init'") + aiProvider = nil } aiSummarizer, err := gemini.NewGeminiPRSummarizer(context.Background(), cfgApp, translations) if err != nil { - log.Fatalf("Error al crear el servicio: %v", err) + log.Printf("Warning: %v", err) + log.Println("El resumidor de PRs está deshabilitado hasta configurar la IA (Gemini).") + aiSummarizer = nil } ticketService := jira.NewJiraService(cfgApp, &http.Client{}) @@ -73,13 +78,8 @@ func initializeApp() (*cli.Command, error) { registerCommand := registry.NewRegistry(cfgApp, translations) prServiceFactory := factory.NewPrServiceFactory(cfgApp, translations, aiSummarizer, gitService) - prService, err := prServiceFactory.CreatePRService() - if err != nil { - log.Printf("Warning: %v", err) - log.Println("Algunos comandos estan desactivados, configura el vcs") - } - prCommand := pr.NewSummarizeCommand(prService) + prCommand := pr.NewSummarizeCommand(prServiceFactory) if err := registerCommand.Register("suggest", suggest.NewSuggestCommandFactory(commitService, commitHandler)); err != nil { log.Fatalf("Error al registrar el comando 'suggest': %v", err) @@ -96,7 +96,7 @@ func initializeApp() (*cli.Command, error) { return &cli.Command{ Name: "mate-commit", Usage: translations.GetMessage("app_usage", 0, nil), - Version: "1.2.0", + Version: "1.3.0", Description: translations.GetMessage("app_description", 0, nil), Commands: registerCommand.CreateCommands(), }, nil diff --git a/internal/cli/command/config/config.go b/internal/cli/command/config/config.go index bfccc5f..26a671e 100644 --- a/internal/cli/command/config/config.go +++ b/internal/cli/command/config/config.go @@ -19,15 +19,9 @@ func (c *ConfigCommandFactory) CreateCommand(t *i18n.Translations, cfg *config.C Aliases: []string{"c"}, Usage: t.GetMessage("config_command_usage", 0, nil), Commands: []*cli.Command{ - c.newSetLangCommand(t, cfg), c.newShowCommand(t, cfg), - c.newSetAPIKeyCommand(t, cfg), - c.newSetJiraConfigCommand(t, cfg), - c.newSetTicketCommand(t, cfg), - c.newSetAIActiveCommand(t, cfg), - c.newSetAIModelCommand(t, cfg), - c.newSetActiveVCSCommand(t, cfg), - c.newSetVCSConfigCommand(t, cfg), + c.newInitCommand(t, cfg), + c.newEditCommand(t, cfg), }, } } diff --git a/internal/cli/command/config/config_test.go b/internal/cli/command/config/config_test.go index e472625..4738f81 100644 --- a/internal/cli/command/config/config_test.go +++ b/internal/cli/command/config/config_test.go @@ -1,12 +1,13 @@ package config import ( - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/stretchr/testify/assert" "os" "path/filepath" "testing" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/stretchr/testify/assert" ) func setupConfigTest(t *testing.T) (*config.Config, *i18n.Translations, string, func()) { diff --git a/internal/cli/command/config/edit.go b/internal/cli/command/config/edit.go new file mode 100644 index 0000000..b4d966c --- /dev/null +++ b/internal/cli/command/config/edit.go @@ -0,0 +1,46 @@ +package config + +import ( + "context" + "fmt" + "os" + "os/exec" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/urfave/cli/v3" +) + +func (c *ConfigCommandFactory) newEditCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "edit", + Usage: t.GetMessage("config_edit_usage", 0, nil), + Action: editConfigAction(cfg), + } +} + +func editConfigAction(cfg *config.Config) cli.ActionFunc { + return func(ctx context.Context, command *cli.Command) error { + editor := os.Getenv("EDITOR") + if editor == "" { + if _, err := exec.LookPath("nano"); err == nil { + editor = "nano" + } else if _, err := exec.LookPath("vim"); err == nil { + editor = "vim" + } else { + return fmt.Errorf("ningun editor de texto definido. Por favor, configure la variable de entorno $EDITOR") + } + } + + cmd := exec.Command(editor, cfg.PathFile) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("error al abrir el editor: %w", err) + } + + return nil + } +} diff --git a/internal/cli/command/config/edit_test.go b/internal/cli/command/config/edit_test.go new file mode 100644 index 0000000..aa7941a --- /dev/null +++ b/internal/cli/command/config/edit_test.go @@ -0,0 +1,171 @@ +package config + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupEditTest(t *testing.T) (*config.Config, *i18n.Translations, func()) { + tempDir := t.TempDir() + fakeConfigPath := filepath.Join(tempDir, "config.yaml") + + err := os.WriteFile(fakeConfigPath, []byte("language: es"), 0600) + require.NoError(t, err) + + cfg := &config.Config{ + PathFile: fakeConfigPath, + } + + translations, err := i18n.NewTranslations("es", "../../../i18n/locales") + require.NoError(t, err) + + cleanup := func() { + } + + return cfg, translations, cleanup +} + +func createFakeExecutable(t *testing.T, name string, exitCode int) (dir string, logFile string) { + dir = t.TempDir() + logFile = filepath.Join(dir, "log.txt") + + scriptContent := fmt.Sprintf(`#!/bin/sh + echo "$@" > '%s' + exit %d + `, logFile, exitCode) + + execPath := filepath.Join(dir, name) + err := os.WriteFile(execPath, []byte(scriptContent), 0755) + require.NoError(t, err) + + return dir, logFile +} + +func TestEditCommand(t *testing.T) { + factory := &ConfigCommandFactory{} + + t.Run("should create command with correct name and usage", func(t *testing.T) { + // Arrange + cfg, translations, cleanup := setupEditTest(t) + defer cleanup() + + // Act + cmd := factory.newEditCommand(translations, cfg) + + // Assert + assert.Equal(t, "edit", cmd.Name) + assert.Equal(t, translations.GetMessage("config_edit_usage", 0, nil), cmd.Usage) + assert.NotNil(t, cmd.Action) + }) + + t.Run("should use editor from $EDITOR environment variable", func(t *testing.T) { + // Arrange + cfg, translations, cleanup := setupEditTest(t) + defer cleanup() + + fakeEditorDir, logFile := createFakeExecutable(t, "my-test-editor", 0) + originalPath := os.Getenv("PATH") + t.Setenv("PATH", fakeEditorDir+string(filepath.ListSeparator)+originalPath) + t.Setenv("EDITOR", "my-test-editor") + + cmd := factory.newEditCommand(translations, cfg) + + // Act + err := cmd.Run(context.Background(), []string{"edit"}) + + // Assert + assert.NoError(t, err) + + logBytes, err := os.ReadFile(logFile) + require.NoError(t, err) + assert.Equal(t, cfg.PathFile, strings.TrimSpace(string(logBytes))) + }) + + t.Run("should fallback to nano if $EDITOR is not set", func(t *testing.T) { + // Arrange + cfg, translations, cleanup := setupEditTest(t) + defer cleanup() + + fakeNanoDir, logFile := createFakeExecutable(t, "nano", 0) + t.Setenv("PATH", fakeNanoDir) + t.Setenv("EDITOR", "") + + cmd := factory.newEditCommand(translations, cfg) + + // Act + err := cmd.Run(context.Background(), []string{"edit"}) + + // Assert + assert.NoError(t, err) + logBytes, err := os.ReadFile(logFile) + require.NoError(t, err) + assert.Equal(t, cfg.PathFile, strings.TrimSpace(string(logBytes))) + }) + + t.Run("should fallback to vim if $EDITOR and nano are not available", func(t *testing.T) { + // Arrange + cfg, translations, cleanup := setupEditTest(t) + defer cleanup() + + fakeVimDir, logFile := createFakeExecutable(t, "vim", 0) + t.Setenv("PATH", fakeVimDir) + t.Setenv("EDITOR", "") + + cmd := factory.newEditCommand(translations, cfg) + + // Act + err := cmd.Run(context.Background(), []string{"edit"}) + + // Assert + assert.NoError(t, err) + logBytes, err := os.ReadFile(logFile) + require.NoError(t, err) + assert.Equal(t, cfg.PathFile, strings.TrimSpace(string(logBytes))) + }) + + t.Run("should return error if no editor is found", func(t *testing.T) { + // Arrange + cfg, translations, cleanup := setupEditTest(t) + defer cleanup() + + t.Setenv("PATH", t.TempDir()) + t.Setenv("EDITOR", "") + + cmd := factory.newEditCommand(translations, cfg) + + // Act + err := cmd.Run(context.Background(), []string{"edit"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "ningun editor de texto definido") + }) + + t.Run("should return error if editor fails to run", func(t *testing.T) { + // Arrange + cfg, translations, cleanup := setupEditTest(t) + defer cleanup() + + fakeEditorDir, _ := createFakeExecutable(t, "failing-editor", 1) + t.Setenv("PATH", fakeEditorDir) + t.Setenv("EDITOR", "failing-editor") + + cmd := factory.newEditCommand(translations, cfg) + + // Act + err := cmd.Run(context.Background(), []string{"edit"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "error al abrir el editor") + }) +} diff --git a/internal/cli/command/config/init.go b/internal/cli/command/config/init.go new file mode 100644 index 0000000..06caa8f --- /dev/null +++ b/internal/cli/command/config/init.go @@ -0,0 +1,328 @@ +package config + +import ( + "bufio" + "context" + "fmt" + "net/url" + "os" + "strings" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/urfave/cli/v3" +) + +func (c *ConfigCommandFactory) newInitCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "init", + Usage: t.GetMessage("config_init_usage", 0, nil), + Action: initConfigAction(cfg, t), + } +} + +func initConfigAction(cfg *config.Config, t *i18n.Translations) cli.ActionFunc { + return func(ctx context.Context, command *cli.Command) error { + reader := bufio.NewReader(os.Stdin) + return runInitProcess(ctx, command, reader, cfg, t) + } +} + +func runInitProcess(ctx context.Context, command *cli.Command, reader *bufio.Reader, cfg *config.Config, t *i18n.Translations) error { + if err := configureWelcome(reader, cfg, t); err != nil { + return err + } + if err := configureLanguage(reader, cfg, t); err != nil { + return err + } + if err := configureVCS(reader, cfg, t); err != nil { + return err + } + if err := configureTickets(reader, cfg, t); err != nil { + return err + } + if err := config.SaveConfig(cfg); err != nil { + fmt.Println(t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{"Error": err.Error()})) + return fmt.Errorf("error saving configuration: %w", err) + } + + printConfigSummary(cfg, t) + + fmt.Println() + fmt.Print(t.GetMessage("init.prompt_run_again", 0, nil)) + runAgain, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + + if isYes(runAgain) { + return runInitProcess(ctx, command, reader, cfg, t) + } + + return nil +} + +func configureWelcome(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations) error { + aiProviders := config.SupportedAIs() + aiProvidersStr := strings.Join(toStrings(aiProviders), ", ") + + geminiModels := config.ModelsForAI(config.AIGemini) + geminiModelsStr := strings.Join(toStrings(geminiModels), ", ") + geminiDefault := string(config.DefaultModelForAI(config.AIGemini)) + + printSection(t.GetMessage("init.section_welcome", 0, nil)) + fmt.Println(t.GetMessage("init.welcome", 0, nil)) + fmt.Println(t.GetMessage("init.ai_intro", 0, map[string]interface{}{"Providers": aiProvidersStr})) + + fmt.Print(t.GetMessage("init.prompt_gemini_api_key", 0, nil)) + apiKey, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading API key: %w", err) + } + apiKey = strings.TrimSpace(apiKey) + + if apiKey != "" && !isValidAPIKey(apiKey) { + fmt.Println(t.GetMessage("init.warning_invalid_api_key", 0, nil)) + } + cfg.GeminiAPIKey = apiKey + + fmt.Println(t.GetMessage("init.model_hint_supported", 0, map[string]interface{}{"Models": geminiModelsStr})) + fmt.Print(t.GetMessage("init.prompt_model_with_default", 0, map[string]interface{}{"Default": geminiDefault})) + modelInput, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading model: %w", err) + } + modelInput = strings.TrimSpace(modelInput) + + cfg.AIConfig.ActiveAI = config.AIGemini + if cfg.AIConfig.Models == nil { + cfg.AIConfig.Models = make(map[config.AI]config.Model) + } + if modelInput == "" { + cfg.AIConfig.Models[config.AIGemini] = config.Model(geminiDefault) + } else { + cfg.AIConfig.Models[config.AIGemini] = config.Model(modelInput) + } + + return nil +} + +func configureLanguage(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations) error { + printSection(t.GetMessage("init.section_language", 0, nil)) + fmt.Println(t.GetMessage("init.language_supported_with_current", 0, map[string]interface{}{"Current": cfg.Language})) + fmt.Print(t.GetMessage("init.prompt_language_blank_keeps", 0, nil)) + + lang, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading language: %w", err) + } + lang = strings.TrimSpace(strings.ToLower(lang)) + + if lang != "" { + if isValidLanguage(lang) { + cfg.Language = lang + } else { + fmt.Println(t.GetMessage("init.error_invalid_language", 0, nil)) + } + } + + return nil +} + +func configureVCS(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations) error { + vcsProviders := config.SupportedVCSProviders() + vcsProvidersStr := strings.Join(vcsProviders, ", ") + + printSection(t.GetMessage("init.section_vcs", 0, nil)) + fmt.Print(t.GetMessage("init.prompt_vcs_enable_blank_no", 0, map[string]interface{}{"Providers": vcsProvidersStr})) + + ansVCS, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading VCS answer: %w", err) + } + ansVCS = strings.TrimSpace(strings.ToLower(ansVCS)) + + if isYes(ansVCS) { + provider := "github" + fmt.Print(t.GetMessage("init.prompt_github_token_blank_skip", 0, nil)) + token, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading GitHub token: %w", err) + } + token = strings.TrimSpace(token) + + if token != "" { + if cfg.VCSConfigs == nil { + cfg.VCSConfigs = make(map[string]config.VCSConfig) + } + cfg.VCSConfigs[provider] = config.VCSConfig{ + Provider: provider, + Token: token, + } + cfg.ActiveVCSProvider = provider + } else { + fmt.Println(t.GetMessage("init.info_vcs_skipped", 0, nil)) + } + } + + return nil +} + +func configureTickets(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations) error { + ticketProviders := config.SupportedTicketServices() + ticketProvidersStr := strings.Join(ticketProviders, ", ") + printSection(t.GetMessage("init.section_tickets", 0, nil)) + fmt.Print(t.GetMessage("init.prompt_ticket_enable_blank_no", 0, map[string]interface{}{"Providers": ticketProvidersStr})) + + ansJira, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading Jira answer: %w", err) + } + ansJira = strings.TrimSpace(strings.ToLower(ansJira)) + + if !isYes(ansJira) { + disableTickets(cfg) + return nil + } + + cfg.UseTicket = true + cfg.ActiveTicketService = "jira" + + fmt.Print(t.GetMessage("init.prompt_jira_base_url_blank_cancel", 0, nil)) + jiraURL, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading Jira URL: %w", err) + } + jiraURL = strings.TrimSpace(jiraURL) + + if jiraURL == "" { + fmt.Println(t.GetMessage("init.info_jira_canceled", 0, nil)) + disableTickets(cfg) + return nil + } + + if !isValidURL(jiraURL) { + fmt.Println(t.GetMessage("init.warning_invalid_url", 0, nil)) + } + cfg.JiraConfig.BaseURL = jiraURL + + fmt.Print(t.GetMessage("init.prompt_jira_email_blank_cancel", 0, nil)) + jiraEmail, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading Jira email: %w", err) + } + jiraEmail = strings.TrimSpace(jiraEmail) + + if jiraEmail == "" { + fmt.Println(t.GetMessage("init.info_jira_canceled", 0, nil)) + disableTickets(cfg) + return nil + } + + if !isValidEmail(jiraEmail) { + fmt.Println(t.GetMessage("init.warning_invalid_email", 0, nil)) + } + cfg.JiraConfig.Email = jiraEmail + + fmt.Print(t.GetMessage("init.prompt_jira_api_token_blank_cancel", 0, nil)) + jiraToken, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("error reading Jira token: %w", err) + } + jiraToken = strings.TrimSpace(jiraToken) + + if jiraToken == "" { + fmt.Println(t.GetMessage("init.info_jira_canceled", 0, nil)) + disableTickets(cfg) + return nil + } + cfg.JiraConfig.APIKey = jiraToken + + return nil +} + +func printConfigSummary(cfg *config.Config, t *i18n.Translations) { + printSection(t.GetMessage("init.section_finish", 0, nil)) + fmt.Println(t.GetMessage("init.saved_ok", 0, nil)) + fmt.Println() + fmt.Println(t.GetMessage("init.summary_header", 0, nil)) + + langLabel := t.GetMessage("language_label", 0, map[string]interface{}{"Lang": cfg.Language}) + activeAI := string(cfg.AIConfig.ActiveAI) + fmt.Println(t.GetMessage("config_models.active_ai_label", 0, map[string]interface{}{"IA": activeAI})) + + if m, ok := cfg.AIConfig.Models[config.AIGemini]; ok && m != "" { + fmt.Println(t.GetMessage("init.summary_model", 0, map[string]interface{}{"AI": "gemini", "Model": string(m)})) + } else { + fmt.Println(t.GetMessage("init.summary_model_none", 0, map[string]interface{}{"AI": "gemini"})) + } + + apiMask := "❌" + if cfg.GeminiAPIKey != "" { + apiMask = "✅" + } + fmt.Println(t.GetMessage("init.summary_api", 0, map[string]interface{}{"AI": "gemini", "Configured": apiMask})) + + if cfg.ActiveVCSProvider != "" { + fmt.Println(t.GetMessage("vcs_summary.config_active_vcs_updated", 0, map[string]interface{}{"Provider": cfg.ActiveVCSProvider})) + } else { + fmt.Println(t.GetMessage("init.summary_vcs_none", 0, nil)) + } + + if cfg.UseTicket && cfg.ActiveTicketService == "jira" { + fmt.Println(t.GetMessage("config_models.ticket_service_enabled", 0, map[string]interface{}{"Service": "jira"})) + fmt.Println(t.GetMessage("config_models.jira_config_label", 0, map[string]interface{}{"BaseURL": cfg.JiraConfig.BaseURL, "Email": cfg.JiraConfig.Email})) + } else { + fmt.Println(t.GetMessage("config_models.ticket_service_disabled", 0, nil)) + } + + fmt.Println(langLabel) +} + +func disableTickets(cfg *config.Config) { + cfg.UseTicket = false + cfg.ActiveTicketService = "" +} + +func isYes(s string) bool { + switch strings.ToLower(strings.TrimSpace(s)) { + case "y", "yes", "s", "si", "sí": + return true + default: + return false + } +} + +func isValidLanguage(lang string) bool { + validLangs := map[string]bool{ + "en": true, + "es": true, + } + return validLangs[strings.ToLower(lang)] +} + +func isValidAPIKey(key string) bool { + return len(key) > 10 && !strings.Contains(key, " ") +} + +func isValidURL(rawURL string) bool { + u, err := url.Parse(rawURL) + return err == nil && u.Scheme != "" && u.Host != "" +} + +func isValidEmail(email string) bool { + return strings.Contains(email, "@") && strings.Contains(email, ".") +} + +func printSection(title string) { + fmt.Println() + fmt.Println(title) +} + +func toStrings[T ~string](vals []T) []string { + out := make([]string, 0, len(vals)) + for _, v := range vals { + out = append(out, string(v)) + } + return out +} diff --git a/internal/cli/command/config/init_test.go b/internal/cli/command/config/init_test.go new file mode 100644 index 0000000..c657fd1 --- /dev/null +++ b/internal/cli/command/config/init_test.go @@ -0,0 +1,130 @@ +package config + +import ( + "bytes" + "context" + "io" + "os" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" +) + +func runInitCommandTest(t *testing.T, userInput string) (output string, finalCfg *config.Config) { + tempDir := t.TempDir() + fakeConfigPath := filepath.Join(tempDir, "config.yaml") + cfg := &config.Config{ + PathFile: fakeConfigPath, + Language: "es", + } + + translations, err := i18n.NewTranslations("es", "../../../i18n/locales") + require.NoError(t, err) + + originalStdin := os.Stdin + originalStdout := os.Stdout + defer func() { + os.Stdin = originalStdin + os.Stdout = originalStdout + }() + + rIn, wIn, err := os.Pipe() + require.NoError(t, err) + os.Stdin = rIn + + rOut, wOut, err := os.Pipe() + require.NoError(t, err) + os.Stdout = wOut + + go func() { + defer func() { + _ = wIn.Close() + }() + _, err := wIn.Write([]byte(userInput)) + require.NoError(t, err) + }() + + var outputBuffer bytes.Buffer + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _, _ = io.Copy(&outputBuffer, rOut) + }() + + factory := &ConfigCommandFactory{} + cmd := factory.newInitCommand(translations, cfg) + + actionErr := cmd.Action(context.Background(), &cli.Command{}) + + _ = wOut.Close() + wg.Wait() + + if actionErr != nil && !strings.Contains(actionErr.Error(), "pipe") { + require.NoError(t, actionErr) + } + + return outputBuffer.String(), cfg +} + +func TestInitCommand(t *testing.T) { + t.Run("should configure all options successfully", func(t *testing.T) { + userInput := strings.Join([]string{ + "my-gemini-api-key", "gemini-1.5-flash", "en", "yes", "my-github-token", "yes", + "https://myjira.atlassian.net", "user@example.com", "my-jira-token", "no", + }, "\n") + "\n" + output, finalCfg := runInitCommandTest(t, userInput) + assert.Contains(t, output, "Introduce tu API Key de Gemini") + assert.Contains(t, output, "Introduce tu token de acceso de GitHub") + assert.Contains(t, output, "URL base de tu instancia de Jira") + assert.Contains(t, output, "Resumen de configuración") + assert.Equal(t, "my-gemini-api-key", finalCfg.GeminiAPIKey) + assert.Equal(t, "en", finalCfg.Language) + assert.True(t, finalCfg.UseTicket) + }) + + t.Run("should skip optional VCS and Tickets sections", func(t *testing.T) { + userInput := strings.Join([]string{ + "test-api-key", "", "", "no", "n", "no", + }, "\n") + "\n" + output, finalCfg := runInitCommandTest(t, userInput) + + assert.Contains(t, output, "Servicio de tickets deshabilitado") + assert.NotContains(t, output, "Introduce tu token de acceso de GitHub") + + assert.Equal(t, "test-api-key", finalCfg.GeminiAPIKey) + assert.Equal(t, config.Model("gemini-2.5-pro"), finalCfg.AIConfig.Models[config.AIGemini]) + assert.Equal(t, "", finalCfg.ActiveVCSProvider) + assert.False(t, finalCfg.UseTicket) + }) + + t.Run("should handle invalid language and keep original", func(t *testing.T) { + userInput := strings.Join([]string{ + "", "", "fr", "no", "no", "no", + }, "\n") + "\n" + output, finalCfg := runInitCommandTest(t, userInput) + assert.Contains(t, output, "Idioma inválido. Por favor ingresa 'en' o 'es'.") + assert.Equal(t, "es", finalCfg.Language) + }) + + t.Run("should run configuration again if user enters yes", func(t *testing.T) { + userInput := strings.Join([]string{ + "first-run-key", "", "es", "no", "no", "yes", + "second-run-key", "gemini-pro", "en", "no", "no", "no", + }, "\n") + "\n" + output, finalCfg := runInitCommandTest(t, userInput) + + assert.Equal(t, 2, strings.Count(output, "Introduce tu API Key de Gemini")) + assert.Equal(t, 2, strings.Count(output, "Resumen de configuración")) + assert.Equal(t, "second-run-key", finalCfg.GeminiAPIKey) + assert.Equal(t, "en", finalCfg.Language) + assert.Equal(t, config.Model("gemini-pro"), finalCfg.AIConfig.Models[config.AIGemini]) + }) +} diff --git a/internal/cli/command/config/set_ai_active.go b/internal/cli/command/config/set_ai_active.go deleted file mode 100644 index 63e4bec..0000000 --- a/internal/cli/command/config/set_ai_active.go +++ /dev/null @@ -1,54 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetAIActiveCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "set-ai-active", - Usage: t.GetMessage("config_models.config_set_ai_active_usage", 0, nil), - Action: func(ctx context.Context, command *cli.Command) error { - ai := command.Args().First() - if ai == "" { - fmt.Println(t.GetMessage("config_models.config_available_ais", 0, nil)) - for _, validAI := range []config.AI{config.AIGemini, config.AIOpenAI} { - fmt.Printf("- %s\n", validAI) - } - msg := t.GetMessage("config_models.error_missing_ai", 0, nil) - return fmt.Errorf("%s", msg) - } - - var newActiveAI config.AI - switch config.AI(ai) { - case config.AIGemini, config.AIOpenAI: - newActiveAI = config.AI(ai) - default: - msg := t.GetMessage("config_models.error_invalid_ai", 0, map[string]interface{}{ - "AI": ai, - }) - return fmt.Errorf("%s", msg) - } - - cfgCopy := *cfg - cfgCopy.AIConfig.ActiveAI = newActiveAI - - if err := config.SaveConfig(cfg); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - fmt.Println(t.GetMessage("config_models.config_set_ai_active_success", 0, map[string]interface{}{ - "AI": ai, - })) - - cfg.AIConfig.ActiveAI = newActiveAI - return nil - }, - } -} diff --git a/internal/cli/command/config/set_ai_active_test.go b/internal/cli/command/config/set_ai_active_test.go deleted file mode 100644 index 059bde1..0000000 --- a/internal/cli/command/config/set_ai_active_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package config - -import ( - "bytes" - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" - "io" - "os" - "testing" -) - -func TestSetAIActiveCommand(t *testing.T) { - t.Run("should successfully set active AI to gemini", func(t *testing.T) { - // arrange - cfg, translations, _, cleanup := setupConfigTest(t) - defer cleanup() - cfg.AIConfig = config.AIConfig{ - ActiveAI: config.AIOpenAI, - } - cmd := NewConfigCommandFactory().newSetAIActiveCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - err := app.Run(ctx, []string{"config", "set-ai-active", "gemini"}) - - // Assert - assert.NoError(t, err) - assert.Equal(t, config.AIGemini, cfg.AIConfig.ActiveAI) - }) - - t.Run("should fail with invalid AI", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - defer cleanup() - originalAI := config.AIOpenAI - cfg.AIConfig = config.AIConfig{ - ActiveAI: originalAI, - } - cmd := NewConfigCommandFactory().newSetAIActiveCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-active", "invalid-ai"}) - - // Assert - assert.Error(t, err) - assert.Equal(t, originalAI, cfg.AIConfig.ActiveAI) - }) - - t.Run("should show available AIs when no AI is provided", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - defer cleanup() - cfg.AIConfig = config.AIConfig{ - ActiveAI: config.AIOpenAI, - } - cmd := NewConfigCommandFactory().newSetAIActiveCommand(translations, cfg) - - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-active"}) - - if err := w.Close(); err != nil { - assert.NoError(t, err) - } - os.Stdout = oldStdout - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - assert.NoError(t, err) - } - output := buf.String() - - // Assert - assert.Error(t, err) - assert.Contains(t, output, "gemini") - assert.Contains(t, output, "openai") - // Verificar que el AI activo no cambió - assert.Equal(t, config.AIOpenAI, cfg.AIConfig.ActiveAI) - }) - - t.Run("save config error", func(t *testing.T) { - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cfg.AIConfig.ActiveAI = config.AIOpenAI - - err := os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - factory := NewConfigCommandFactory() - cmd := factory.newSetAIActiveCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err = app.Run(ctx, []string{"config", "set-ai-active", "gemini"}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al guardar la configuración") - assert.Equal(t, config.AIOpenAI, cfg.AIConfig.ActiveAI) - }) -} diff --git a/internal/cli/command/config/set_ai_model.go b/internal/cli/command/config/set_ai_model.go deleted file mode 100644 index 3ef19cc..0000000 --- a/internal/cli/command/config/set_ai_model.go +++ /dev/null @@ -1,108 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetAIModelCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "set-ai-model", - Usage: t.GetMessage("config_models.config_set_ai_model_usage", 0, nil), - Action: func(ctx context.Context, cmd *cli.Command) error { - args := cmd.Args() - if args.Len() < 1 { - // Mostrar un listado de IAs disponibles - fmt.Println(t.GetMessage("config_models.config_available_ais", 0, nil)) - for _, validAI := range []config.AI{config.AIGemini, config.AIOpenAI} { - fmt.Printf("- %s\n", validAI) - } - msg := t.GetMessage("config_models.error_missing_ai", 0, nil) - return fmt.Errorf("%s", msg) - } - - ai := args.Get(0) - model := args.Get(1) - - // Validar que la IA sea válida usando el enum - var validModels []config.Model - switch config.AI(ai) { - case config.AIGemini: - validModels = []config.Model{ - config.ModelGeminiV15Flash, - config.ModelGeminiV15Pro, - config.ModelGeminiV20Flash, - } - case config.AIOpenAI: - validModels = []config.Model{ - config.ModelGPTV4o, - config.ModelGPTV4oMini, - } - default: - msg := t.GetMessage("config_models.error_invalid_ai", 0, map[string]interface{}{ - "AI": ai, - }) - return fmt.Errorf("%s", msg) - } - - if model == "" { - currentModel := cfg.AIConfig.Models[config.AI(model)] - if currentModel == "" { - fmt.Println(t.GetMessage("config_models.config_no_model_selected_for_ai", 0, map[string]interface{}{ - "AI": ai, - })) - } else { - fmt.Println(t.GetMessage("config_models.config_current_model_for_ai", 0, map[string]interface{}{ - "AI": ai, - "Model": currentModel, - })) - } - // Mostrar un listado de modelos disponibles para la IA seleccionada - fmt.Println(t.GetMessage("config_models.config_set_ai_model_usage", 0, map[string]interface{}{ - "AI": ai, - })) - for _, validModel := range validModels { - fmt.Printf("- %s\n", validModel) - } - msg := t.GetMessage("config_models.error_missing_model", 0, nil) - return fmt.Errorf("%s", msg) - } - - // Validar que el modelo sea válido - valid := false - for _, validModel := range validModels { - if config.Model(model) == validModel { - valid = true - break - } - } - if !valid { - msg := t.GetMessage("config_models.error_invalid_model", 0, map[string]interface{}{ - "Model": model, - }) - return fmt.Errorf("%s", msg) - } - - if cfg.AIConfig.Models == nil { - cfg.AIConfig.Models = make(map[config.AI]config.Model) - } - - // Guardar el modelo seleccionado - cfg.AIConfig.Models[config.AI(ai)] = config.Model(model) - if err := config.SaveConfig(cfg); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - fmt.Println(t.GetMessage("config_models.config_set_ai_model_success", 0, map[string]interface{}{ - "AI": ai, - "Model": model, - })) - return nil - }, - } -} diff --git a/internal/cli/command/config/set_ai_model_test.go b/internal/cli/command/config/set_ai_model_test.go deleted file mode 100644 index b631ece..0000000 --- a/internal/cli/command/config/set_ai_model_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package config - -import ( - "bytes" - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" - "io" - "os" - "testing" -) - -func TestSetAIModelCommand(t *testing.T) { - t.Run("should successfully set model for Gemini", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: make(map[config.AI]config.Model), - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "gemini", "gemini-1.5-flash"}) - - // Assert - assert.NoError(t, err) - assert.Equal(t, config.ModelGeminiV15Flash, cfg.AIConfig.Models[config.AIGemini]) - }) - - t.Run("should fail with invalid AI", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: make(map[config.AI]config.Model), - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - // Simular argumentos de la línea de comandos - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "invalid-ai", "gpt-v4o"}) - - // Assert - assert.Error(t, err) - }) - - t.Run("should handle empty AI config", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - assert.NoError(t, config.SaveConfig(cfg)) - defer cleanup() - - ctx := context.Background() - - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - app := &cli.Command{Commands: []*cli.Command{cmd}} - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "gemini", "gemini-1.5-flash"}) - - // Assert - assert.NoError(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.Equal(t, config.ModelGeminiV15Flash, loadedCfg.AIConfig.Models[config.AIGemini]) - }) - - t.Run("should show available models for OpenAI when no model provided", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: make(map[config.AI]config.Model), - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - // Capturar stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "openai"}) - - if err := w.Close(); err != nil { - assert.NoError(t, err) - } - os.Stdout = oldStdout - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - assert.NoError(t, err) - } - output := buf.String() - - // Assert - assert.Error(t, err) - assert.Contains(t, output, string(config.ModelGPTV4o)) - assert.Contains(t, output, string(config.ModelGPTV4oMini)) - }) - - t.Run("should show available AIs list when no arguments provided", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: make(map[config.AI]config.Model), - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - // Capturar stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model"}) - - // Restaurar stdout y capturar salida - if err := w.Close(); err != nil { - assert.NoError(t, err) - } - os.Stdout = oldStdout - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - assert.NoError(t, err) - } - output := buf.String() - - // Assert - assert.Error(t, err) - assert.Contains(t, output, "- gemini") - assert.Contains(t, output, "- openai") - - expectedMsg := translations.GetMessage("config_models.error_missing_ai", 0, nil) - assert.Contains(t, err.Error(), expectedMsg) - }) - - t.Run("should show current model when AI has model configured", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: map[config.AI]config.Model{ - config.AIOpenAI: config.ModelGPTV4o, - }, - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "openai"}) - - if err := w.Close(); err != nil { - assert.NoError(t, err) - } - os.Stdout = oldStdout - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - assert.NoError(t, err) - } - output := buf.String() - - // Assert - assert.Error(t, err) - expectedNoModelMsg := translations.GetMessage("config_models.config_no_model_selected_for_ai", 0, map[string]interface{}{ - "AI": "openai", - }) - assert.Contains(t, output, expectedNoModelMsg) - assert.Contains(t, output, "- gpt-4o") - assert.Contains(t, output, "- gpt-4o-mini") - }) - - t.Run("should show current model and available models when AI has model set", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: map[config.AI]config.Model{ - config.AIOpenAI: config.ModelGPTV4o, - }, - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "openai"}) - - if err := w.Close(); err != nil { - assert.NoError(t, err) - } - os.Stdout = oldStdout - var buf bytes.Buffer - if _, err := io.Copy(&buf, r); err != nil { - assert.NoError(t, err) - } - output := buf.String() - - // Assert - assert.Error(t, err) - assert.Contains(t, output, string(config.ModelGPTV4o)) - assert.Contains(t, output, string(config.ModelGPTV4oMini)) - }) - - t.Run("should fail with invalid model", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: make(map[config.AI]config.Model), - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "openai", "invalid-model"}) - - // Assert - assert.Error(t, err) - assert.Empty(t, cfg.AIConfig.Models[config.AIOpenAI]) - }) - - t.Run("should successfully set OpenAI model", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.AIConfig = config.AIConfig{ - Models: make(map[config.AI]config.Model), - } - defer cleanup() - cmd := NewConfigCommandFactory().newSetAIModelCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-ai-model", "openai", "gpt-4o"}) - - // Assert - assert.NoError(t, err) - assert.Equal(t, config.ModelGPTV4o, cfg.AIConfig.Models[config.AIOpenAI]) - }) -} diff --git a/internal/cli/command/config/set_api_key.go b/internal/cli/command/config/set_api_key.go deleted file mode 100644 index a64ae58..0000000 --- a/internal/cli/command/config/set_api_key.go +++ /dev/null @@ -1,45 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetAPIKeyCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "set-api-key", - Usage: t.GetMessage("commands.set_api_key_usage", 0, nil), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "key", - Aliases: []string{"k"}, - Usage: t.GetMessage("flags.gemini_api_key", 0, nil), - Required: true, - }, - }, - Action: func(ctx context.Context, command *cli.Command) error { - apiKey := command.String("key") - if len(apiKey) < 10 { - msg := t.GetMessage("api.invalid_key", 0, nil) - return fmt.Errorf("%s", msg) - } - - oldAPIKey := cfg.GeminiAPIKey - cfg.GeminiAPIKey = apiKey - if err := config.SaveConfig(cfg); err != nil { - cfg.GeminiAPIKey = oldAPIKey - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - - fmt.Printf("%s\n", t.GetMessage("api.key_configured", 0, nil)) - fmt.Printf("%s\n", t.GetMessage("api.key_configuration_help", 0, nil)) - return nil - }, - } -} diff --git a/internal/cli/command/config/set_api_key_test.go b/internal/cli/command/config/set_api_key_test.go deleted file mode 100644 index ee8f64b..0000000 --- a/internal/cli/command/config/set_api_key_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package config - -import ( - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" - "os" - "testing" -) - -func TestSetAPIKeyCommand(t *testing.T) { - t.Run("should fail with invalid API key length", func(t *testing.T) { - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cmd := NewConfigCommandFactory().newSetAPIKeyCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-api-key", "--key", "short"}) - - // Assert - assert.Error(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.Empty(t, loadedCfg.GeminiAPIKey) - }) - - t.Run("should successfully update existing API key", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - cfg.GeminiAPIKey = "old_api_key-12345" - assert.NoError(t, config.SaveConfig(cfg)) - defer cleanup() - - cmd := NewConfigCommandFactory().newSetAPIKeyCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - newKey := "new-api-key-67890" - - // Act - err := app.Run(ctx, []string{"config", "set-api-key", "--key", newKey}) - - // Assert - assert.NoError(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.Equal(t, newKey, loadedCfg.GeminiAPIKey) - }) - - t.Run("save config error", func(t *testing.T) { - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - err := os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - factory := NewConfigCommandFactory() - cmd := factory.newSetAPIKeyCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err = app.Run(ctx, []string{"config", "set-api-key", "--key", "test-api-key-123456"}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al guardar la configuración") - assert.Empty(t, cfg.GeminiAPIKey) - }) -} diff --git a/internal/cli/command/config/set_jira_config.go b/internal/cli/command/config/set_jira_config.go deleted file mode 100644 index ff16940..0000000 --- a/internal/cli/command/config/set_jira_config.go +++ /dev/null @@ -1,56 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetJiraConfigCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "jira", - Usage: t.GetMessage("jira_config_command_usage.jira_config_usage", 0, nil), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "base-url", - Aliases: []string{"u"}, - Usage: t.GetMessage("jira_config_command_usage.jira_config_base_url_usage", 0, nil), - }, - &cli.StringFlag{ - Name: "api-key", - Usage: t.GetMessage("jira_config_command_usage.jira_config_api_key_usage", 0, nil), - }, - &cli.StringFlag{ - Name: "email", - Usage: t.GetMessage("jira_config_command_usage.jira_config_email_usage", 0, nil), - }, - }, - Action: func(ctx context.Context, command *cli.Command) error { - baseURL := command.String("base-url") - apiKey := command.String("api-key") - email := command.String("email") - - if baseURL == "" || apiKey == "" || email == "" { - msg := t.GetMessage("jira_config_command_usage.jira_config_missing_fields", 0, nil) - return fmt.Errorf("%s", msg) - } - - cfg.JiraConfig.BaseURL = baseURL - cfg.JiraConfig.APIKey = apiKey - cfg.JiraConfig.Email = email - - if err := config.SaveConfig(cfg); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - - fmt.Println(t.GetMessage("jira_config_command_usage.jira_config_success", 0, nil)) - return nil - }, - } - -} diff --git a/internal/cli/command/config/set_jira_config_test.go b/internal/cli/command/config/set_jira_config_test.go deleted file mode 100644 index f730f3b..0000000 --- a/internal/cli/command/config/set_jira_config_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package config - -import ( - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" - "os" - "testing" -) - -func TestSetJiraConfigCommand(t *testing.T) { - t.Run("should successfully set Jira configuration", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - factory := NewConfigCommandFactory() - cmd := factory.newSetJiraConfigCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - baseURL := "https://example.atlassian.net" - apiKey := "test-api-key" - email := "user@example.com" - - // Act - err := app.Run(ctx, []string{"config", "jira", "--base-url", baseURL, "--api-key", apiKey, "--email", email}) - - // Assert - assert.NoError(t, err) - - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.Equal(t, baseURL, loadedCfg.JiraConfig.BaseURL) - assert.Equal(t, apiKey, loadedCfg.JiraConfig.APIKey) - assert.Equal(t, email, loadedCfg.JiraConfig.Email) - }) - - t.Run("should fail when missing base URL", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - defer cleanup() - - factory := NewConfigCommandFactory() - cmd := factory.newSetJiraConfigCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - apiKey := "test-api-key" - email := "user@example.com" - - // Act - err := app.Run(ctx, []string{"config", "jira", "--api-key", apiKey, "--email", email}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Todos los campos son requeridos") - }) - - t.Run("should fail when missing API key", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - defer cleanup() - - factory := NewConfigCommandFactory() - cmd := factory.newSetJiraConfigCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - baseURL := "https://example.atlassian.net" - email := "user@example.com" - - // Act - err := app.Run(ctx, []string{"config", "jira", "--base-url", baseURL, "--email", email}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Todos los campos son requeridos") - }) - - t.Run("should fail when missing email", func(t *testing.T) { - // Arrange - cfg, translations, _, cleanup := setupConfigTest(t) - defer cleanup() - - factory := NewConfigCommandFactory() - cmd := factory.newSetJiraConfigCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - baseURL := "https://example.atlassian.net" - apiKey := "test-api-key" - - // Act - err := app.Run(ctx, []string{"config", "jira", "--base-url", baseURL, "--api-key", apiKey}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Todos los campos son requeridos") - }) - - t.Run("should handle error when saving configuration fails", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - // Hacer que la ruta de configuración sea un directorio para forzar un error de guardado - err := os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - factory := NewConfigCommandFactory() - cmd := factory.newSetJiraConfigCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - baseURL := "https://example.atlassian.net" - apiKey := "test-api-key" - email := "user@example.com" - - // Act - err = app.Run(ctx, []string{"config", "jira", "--base-url", baseURL, "--api-key", apiKey, "--email", email}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al guardar la configuración") - }) -} diff --git a/internal/cli/command/config/set_lang.go b/internal/cli/command/config/set_lang.go deleted file mode 100644 index 20998e9..0000000 --- a/internal/cli/command/config/set_lang.go +++ /dev/null @@ -1,59 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetLangCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "set-lang", - Usage: t.GetMessage("config_set_lang_usage", 0, nil), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Aliases: []string{"l"}, - Usage: t.GetMessage("config_set_lang_flag_usage", 0, nil), - Required: true, - }, - }, - Action: func(ctx context.Context, command *cli.Command) error { - lang := command.String("lang") - - supportedLanguages := []string{"en", "es"} - var validLang bool - for _, supportedLang := range supportedLanguages { - if lang == supportedLang { - validLang = true - break - } - } - - if !validLang { - msg := t.GetMessage("config_models.error_invalid_language", 0, map[string]interface{}{ - "Language": lang, - }) - return fmt.Errorf("%s", msg) - } - - cfgCopy := *cfg - cfgCopy.Language = lang - - cfg.Language = lang - if err := config.SaveConfig(&cfgCopy); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - - cfg.Language = lang - - fmt.Printf("%s\n", t.GetMessage("language_configured", 0, map[string]interface{}{"Lang": lang})) - return nil - }, - } -} diff --git a/internal/cli/command/config/set_lang_test.go b/internal/cli/command/config/set_lang_test.go deleted file mode 100644 index 0019265..0000000 --- a/internal/cli/command/config/set_lang_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package config - -import ( - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" - "os" - "testing" -) - -func TestSetLangCommand(t *testing.T) { - t.Run("should successfully set valid language to English", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cfg.Language = "es" - assert.NoError(t, config.SaveConfig(cfg)) - - factory := NewConfigCommandFactory() - cmd := factory.newSetLangCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-lang", "-lang", "en"}) - - // Assert - assert.NoError(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - - assert.Equal(t, "en", loadedCfg.Language) - }) - - t.Run("should fail with unsupported language", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - assert.NoError(t, config.SaveConfig(cfg)) - - factory := NewConfigCommandFactory() - cmd := factory.newSetLangCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"config", "set-lang", "--lang", "fr"}) - - // Assert - assert.Error(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.Equal(t, "es", loadedCfg.Language) - }) - - t.Run("config save error", func(t *testing.T) { - // arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - err := os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - factory := NewConfigCommandFactory() - cmd := factory.newSetLangCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - err = app.Run(ctx, []string{"config", "set-lang", "--lang", "en"}) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al guardar la configuración") - assert.Equal(t, "en", cfg.Language) - }) -} diff --git a/internal/cli/command/config/set_ticket.go b/internal/cli/command/config/set_ticket.go deleted file mode 100644 index 31ea925..0000000 --- a/internal/cli/command/config/set_ticket.go +++ /dev/null @@ -1,56 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetTicketCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "ticket", - Aliases: []string{"t"}, - Usage: t.GetMessage("jira_config_command_usage.ticket_command_usage", 0, nil), - Commands: []*cli.Command{ - { - Name: "disable", - Aliases: []string{"d"}, - Usage: t.GetMessage("jira_config_command_usage.disable_ticket_command_usage", 0, nil), - Action: func(ctx context.Context, command *cli.Command) error { - cfg.UseTicket = false - cfg.ActiveTicketService = "" - - if err := config.SaveConfig(cfg); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - - fmt.Println(t.GetMessage("jira_config_command_usage.ticket_disabled_success", 0, nil)) - return nil - }, - }, - { - Name: "enable", - Aliases: []string{"e"}, - Usage: t.GetMessage("jira_config_command_usage.enable_ticket_command_usage", 0, nil), - Action: func(ctx context.Context, command *cli.Command) error { - cfg.UseTicket = true - - if err := config.SaveConfig(cfg); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - - fmt.Println(t.GetMessage("jira_config_command_usage.ticket_enabled_success", 0, nil)) - return nil - }, - }, - }, - } -} diff --git a/internal/cli/command/config/set_ticket_test.go b/internal/cli/command/config/set_ticket_test.go deleted file mode 100644 index 6d337a5..0000000 --- a/internal/cli/command/config/set_ticket_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package config - -import ( - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" - "os" - "path/filepath" - "testing" -) - -func TestSetTicketCommand(t *testing.T) { - t.Run("should successfully enable ticket", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cmd := NewConfigCommandFactory().newSetTicketCommand(translations, cfg) - - // Act - err := cmd.Commands[1].Action(context.Background(), &cli.Command{}) - - // Assert - assert.NoError(t, err) - assert.True(t, cfg.UseTicket) - - // Verificar que la configuración se guardó correctamente - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.True(t, loadedCfg.UseTicket) - }) - - t.Run("should successfully disable ticket", func(t *testing.T) { - // arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - cfg.UseTicket = true - defer cleanup() - - cmd := NewConfigCommandFactory().newSetTicketCommand(translations, cfg) - - // act - err := cmd.Commands[0].Action(context.Background(), &cli.Command{}) - - // assert - assert.NoError(t, err) - assert.False(t, cfg.UseTicket) - - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.False(t, loadedCfg.UseTicket) - }) - - t.Run("save error config with enable", func(t *testing.T) { - // arrange - tempDir, err := os.MkdirTemp("", "matecommit-test-*") - assert.NoError(t, err) - defer func() { - if err := os.RemoveAll(tempDir); err != nil { - return - } - }() - - tmpConfigPath := filepath.Join(tempDir, "config.json") - err = os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - // config inicial - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.PathFile = tmpConfigPath - defer cleanup() - - factory := NewConfigCommandFactory() - cmd := factory.newSetTicketCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // act - err = app.Run(ctx, []string{"config", "ticket", "disable"}) - - // assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al guardar la configuración") - - }) - - t.Run("save error config with disable", func(t *testing.T) { - // arrange - tempDir, err := os.MkdirTemp("", "matecommit-test-*") - assert.NoError(t, err) - defer func() { - if err := os.RemoveAll(tempDir); err != nil { - return - } - }() - - tmpConfigPath := filepath.Join(tempDir, "config.json") - err = os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - // config inicial - cfg, translations, _, cleanup := setupConfigTest(t) - cfg.PathFile = tmpConfigPath - defer cleanup() - - factory := NewConfigCommandFactory() - cmd := factory.newSetTicketCommand(translations, cfg) - - app := &cli.Command{Commands: []*cli.Command{cmd}} - ctx := context.Background() - - // act - err = app.Run(ctx, []string{"config", "ticket", "enable"}) - - // assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al guardar la configuración") - - }) -} diff --git a/internal/cli/command/config/set_vcs_active.go b/internal/cli/command/config/set_vcs_active.go deleted file mode 100644 index 5220eaf..0000000 --- a/internal/cli/command/config/set_vcs_active.go +++ /dev/null @@ -1,48 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetActiveVCSCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "set-active-vcs", - Usage: t.GetMessage("vcs_summary.config_set_active_vcs_usage", 0, nil), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "provider", - Aliases: []string{"p"}, - Usage: t.GetMessage("vcs_summary.config_set_active_vcs_provider_usage", 0, nil), - Required: true, - }, - }, - Action: func(ctx context.Context, command *cli.Command) error { - provider := command.String("provider") - - if _, exists := cfg.VCSConfigs[provider]; !exists { - msg := t.GetMessage("error.vcs_provider_not_configured", 0, map[string]interface{}{ - "Provider": provider, - }) - return fmt.Errorf("%s", msg) - } - - cfg.ActiveVCSProvider = provider - - if err := config.SaveConfig(cfg); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - - fmt.Println(t.GetMessage("vcs_summary.config_active_vcs_updated", 0, map[string]interface{}{ - "Provider": provider, - })) - return nil - }, - } -} diff --git a/internal/cli/command/config/set_vcs_active_test.go b/internal/cli/command/config/set_vcs_active_test.go deleted file mode 100644 index 3301273..0000000 --- a/internal/cli/command/config/set_vcs_active_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package config - -import ( - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestSetActiveVCSCommand(t *testing.T) { - t.Run("should successfully set active VCS provider", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cfg.VCSConfigs = map[string]config.VCSConfig{ - "github": {Provider: "github"}, - "gitlab": {Provider: "gitlab"}, - } - cfg.ActiveVCSProvider = "gitlab" - assert.NoError(t, config.SaveConfig(cfg)) - - factory := NewConfigCommandFactory() - cmd := factory.newSetActiveVCSCommand(translations, cfg) - - app := cmd - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"set-active-vcs", "--provider", "github"}) - - // Assert - assert.NoError(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.Equal(t, "github", loadedCfg.ActiveVCSProvider) - }) - - t.Run("should fail with non-existent provider", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cfg.VCSConfigs = map[string]config.VCSConfig{ - "github": {Provider: "github"}, - } - cfg.ActiveVCSProvider = "github" - assert.NoError(t, config.SaveConfig(cfg)) - - factory := NewConfigCommandFactory() - cmd := factory.newSetActiveVCSCommand(translations, cfg) - - app := cmd - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"set-active-vcs", "--provider", "bitbucket"}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), translations.GetMessage("error.vcs_provider_not_configured", 0, map[string]interface{}{ - "Provider": "bitbucket", - })) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - assert.Equal(t, "github", loadedCfg.ActiveVCSProvider) - }) - - t.Run("config save error", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - err := os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - cfg.VCSConfigs = map[string]config.VCSConfig{ - "github": {Provider: "github"}, - } - - factory := NewConfigCommandFactory() - cmd := factory.newSetActiveVCSCommand(translations, cfg) - - app := cmd - ctx := context.Background() - - // Act - err = app.Run(ctx, []string{"set-active-vcs", "--provider", "github"}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "error al guardar la configuración") - assert.Equal(t, "github", cfg.ActiveVCSProvider) - }) -} diff --git a/internal/cli/command/config/set_vcs_config.go b/internal/cli/command/config/set_vcs_config.go deleted file mode 100644 index aa4d516..0000000 --- a/internal/cli/command/config/set_vcs_config.go +++ /dev/null @@ -1,78 +0,0 @@ -package config - -import ( - "context" - "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -func (c *ConfigCommandFactory) newSetVCSConfigCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "set-vcs", - Usage: t.GetMessage("vcs_summary.config_set_vcs_usage", 0, nil), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "provider", - Aliases: []string{"p"}, - Usage: t.GetMessage("vcs_summary.config_set_vcs_provider_usage", 0, nil), - Required: true, - }, - &cli.StringFlag{ - Name: "token", - Aliases: []string{"t"}, - Usage: t.GetMessage("vcs_summary.config_set_vcs_token_usage", 0, nil), - Required: false, - }, - &cli.StringFlag{ - Name: "owner", - Aliases: []string{"o"}, - Usage: t.GetMessage("vcs_summary.config_set_vcs_owner_usage", 0, nil), - Required: false, - }, - &cli.StringFlag{ - Name: "repo", - Aliases: []string{"r"}, - Usage: t.GetMessage("vcs_summary.config_set_vcs_repo_usage", 0, nil), - Required: false, - }, - }, - Action: func(ctx context.Context, command *cli.Command) error { - provider := command.String("provider") - - if cfg.VCSConfigs == nil { - cfg.VCSConfigs = make(map[string]config.VCSConfig) - } - - vcsConfig, exists := cfg.VCSConfigs[provider] - if !exists { - vcsConfig = config.VCSConfig{Provider: provider} - } - - if token := command.String("token"); token != "" { - vcsConfig.Token = token - } - if owner := command.String("owner"); owner != "" { - vcsConfig.Owner = owner - } - if repo := command.String("repo"); repo != "" { - vcsConfig.Repo = repo - } - - cfg.VCSConfigs[provider] = vcsConfig - - if err := config.SaveConfig(cfg); err != nil { - msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return fmt.Errorf("%s", msg) - } - - fmt.Println(t.GetMessage("vcs_summary.config_vcs_updated", 0, map[string]interface{}{ - "Provider": provider, - })) - return nil - }, - } -} diff --git a/internal/cli/command/config/set_vcs_config_test.go b/internal/cli/command/config/set_vcs_config_test.go deleted file mode 100644 index 556e15c..0000000 --- a/internal/cli/command/config/set_vcs_config_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package config - -import ( - "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestSetVCSConfigCommand(t *testing.T) { - t.Run("should successfully set new VCS config", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - assert.NoError(t, config.SaveConfig(cfg)) - - factory := NewConfigCommandFactory() - cmd := factory.newSetVCSConfigCommand(translations, cfg) - - app := cmd - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"set-vcs", "--provider", "github", "--token", "abc123", "--owner", "testuser", "--repo", "testrepo"}) - - // Assert - assert.NoError(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - - vcsConfig, exists := loadedCfg.VCSConfigs["github"] - assert.True(t, exists) - assert.Equal(t, "github", vcsConfig.Provider) - assert.Equal(t, "abc123", vcsConfig.Token) - assert.Equal(t, "testuser", vcsConfig.Owner) - assert.Equal(t, "testrepo", vcsConfig.Repo) - }) - - t.Run("should update existing VCS config", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cfg.VCSConfigs = map[string]config.VCSConfig{ - "github": { - Provider: "github", - Token: "old-token", - Owner: "old-owner", - Repo: "old-repo", - }, - } - assert.NoError(t, config.SaveConfig(cfg)) - - factory := NewConfigCommandFactory() - cmd := factory.newSetVCSConfigCommand(translations, cfg) - - app := cmd - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"set-vcs", "--provider", "github", "--owner", "new-owner"}) - - // Assert - assert.NoError(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - - vcsConfig, exists := loadedCfg.VCSConfigs["github"] - assert.True(t, exists) - assert.Equal(t, "github", vcsConfig.Provider) - assert.Equal(t, "old-token", vcsConfig.Token) - assert.Equal(t, "new-owner", vcsConfig.Owner) - assert.Equal(t, "old-repo", vcsConfig.Repo) - }) - - t.Run("should create VCSConfigs map if nil", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - cfg.VCSConfigs = nil - assert.NoError(t, config.SaveConfig(cfg)) - - factory := NewConfigCommandFactory() - cmd := factory.newSetVCSConfigCommand(translations, cfg) - - app := cmd - ctx := context.Background() - - // Act - err := app.Run(ctx, []string{"set-vcs", "--provider", "github", "--token", "abc123"}) - - // Assert - assert.NoError(t, err) - loadedCfg, err := config.LoadConfig(tmpConfigPath) - assert.NoError(t, err) - - vcsConfig, exists := loadedCfg.VCSConfigs["github"] - assert.True(t, exists) - assert.Equal(t, "github", vcsConfig.Provider) - assert.Equal(t, "abc123", vcsConfig.Token) - }) - - t.Run("config save error", func(t *testing.T) { - // Arrange - cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) - defer cleanup() - - err := os.Mkdir(tmpConfigPath, 0755) - assert.NoError(t, err) - - factory := NewConfigCommandFactory() - cmd := factory.newSetVCSConfigCommand(translations, cfg) - - app := cmd - ctx := context.Background() - - // Act - err = app.Run(ctx, []string{"set-vcs", "--provider", "github", "--token", "abc123"}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al guardar la configuración") - - if cfg.VCSConfigs != nil { - vcsConfig, exists := cfg.VCSConfigs["github"] - assert.True(t, exists) - assert.Equal(t, "github", vcsConfig.Provider) - assert.Equal(t, "abc123", vcsConfig.Token) - } - }) -} diff --git a/internal/cli/command/config/show.go b/internal/cli/command/config/show.go index 8f7530a..2375abe 100644 --- a/internal/cli/command/config/show.go +++ b/internal/cli/command/config/show.go @@ -3,6 +3,7 @@ package config import ( "context" "fmt" + "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/urfave/cli/v3" diff --git a/internal/cli/command/config/show_test.go b/internal/cli/command/config/show_test.go index 4513841..3199362 100644 --- a/internal/cli/command/config/show_test.go +++ b/internal/cli/command/config/show_test.go @@ -3,12 +3,13 @@ package config import ( "bytes" "context" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" "io" "os" "testing" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" ) func TestShowCommand(t *testing.T) { @@ -65,7 +66,7 @@ func TestShowCommand(t *testing.T) { cfg.AIConfig = config.AIConfig{ ActiveAI: config.AIGemini, Models: map[config.AI]config.Model{ - config.AIGemini: config.ModelGeminiV15Flash, + config.AIGemini: config.ModelGeminiV25Flash, config.AIOpenAI: config.ModelGPTV4o, }, } @@ -101,7 +102,7 @@ func TestShowCommand(t *testing.T) { assert.Contains(t, output, "Configuración de Jira - BaseURL: https://example.atlassian.net, Email: user@example.com") // Check AI models - assert.Contains(t, output, "gemini: gemini-1.5-flash") + assert.Contains(t, output, "gemini: gemini-2.5-flash") assert.Contains(t, output, "openai: gpt-4o") }) } diff --git a/internal/cli/command/pr/summarize.go b/internal/cli/command/pr/summarize.go index 6e9ab95..99feb3d 100644 --- a/internal/cli/command/pr/summarize.go +++ b/internal/cli/command/pr/summarize.go @@ -5,18 +5,19 @@ import ( "fmt" cfg "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/factory" + "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/urfave/cli/v3" ) type SummarizeCommand struct { - prService ports.PRService + prFactory factory.PRServiceFactoryInterface } -func NewSummarizeCommand(prService ports.PRService) *SummarizeCommand { +func NewSummarizeCommand(prFactory factory.PRServiceFactoryInterface) *SummarizeCommand { return &SummarizeCommand{ - prService: prService, + prFactory: prFactory, } } @@ -34,19 +35,13 @@ func (c *SummarizeCommand) CreateCommand(t *i18n.Translations, cfg *cfg.Config) }, }, Action: func(ctx context.Context, command *cli.Command) error { - activeVCS := cfg.VCSConfigs[cfg.ActiveVCSProvider] - - if cfg.ActiveVCSProvider == "" || cfg.VCSConfigs == nil || activeVCS.Owner == "" { - return fmt.Errorf("%s", t.GetMessage("error.no_repo_configured", 0, nil)) - } - - if activeVCS.Repo == "" { - return fmt.Errorf("%s", t.GetMessage("error.invalid_repo_format", 0, nil)) + prService, err := c.prFactory.CreatePRService() + if err != nil { + return fmt.Errorf(t.GetMessage("error.pr_service_creation_error", 0, nil)+": %w", err) } - prNumber := command.Int("pr-number") - summary, err := c.prService.SummarizePR(ctx, int(prNumber)) + summary, err := prService.SummarizePR(ctx, int(prNumber)) if err != nil { return fmt.Errorf(t.GetMessage("error.pr_summary_error", 0, nil)+": %w", err) } diff --git a/internal/cli/command/pr/summarize_test.go b/internal/cli/command/pr/summarize_test.go index ddf7631..2a76955 100644 --- a/internal/cli/command/pr/summarize_test.go +++ b/internal/cli/command/pr/summarize_test.go @@ -7,6 +7,7 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -22,107 +23,94 @@ func (m *MockPRService) SummarizePR(ctx context.Context, prNumber int) (models.P return args.Get(0).(models.PRSummary), args.Error(1) } -func setupSummarizeTest(t *testing.T) (*MockPRService, *i18n.Translations, *config.Config) { - mockPRService := new(MockPRService) +type MockPRServiceFactory struct { + mock.Mock +} - cfg := &config.Config{ - ActiveVCSProvider: "github", - VCSConfigs: map[string]config.VCSConfig{ - "github": { - Owner: "testowner", - Repo: "testrepo", - }, - }, +func (m *MockPRServiceFactory) CreatePRService() (ports.PRService, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) } + return args.Get(0).(ports.PRService), args.Error(1) +} + +func setupSummarizeTest(t *testing.T) (*MockPRService, *MockPRServiceFactory, *i18n.Translations, *config.Config) { + mockPRService := new(MockPRService) + mockFactory := new(MockPRServiceFactory) + + cfg := &config.Config{} translations, err := i18n.NewTranslations("es", "../../../i18n/locales") require.NoError(t, err) - return mockPRService, translations, cfg + return mockPRService, mockFactory, translations, cfg } func TestSummarizeCommand(t *testing.T) { t.Run("should successfully summarize PR", func(t *testing.T) { // Arrange - mockPRService, translations, cfg := setupSummarizeTest(t) + mockPRService, mockFactory, translations, cfg := setupSummarizeTest(t) prNumber := 123 summary := models.PRSummary{ Title: "Test PR", } + mockFactory.On("CreatePRService").Return(mockPRService, nil) mockPRService.On("SummarizePR", mock.Anything, prNumber).Return(summary, nil) - cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) - app := cmd - - ctx := context.Background() + prCommand := NewSummarizeCommand(mockFactory) + cmd := prCommand.CreateCommand(translations, cfg) - err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123"}) + // Act + err := cmd.Run(context.Background(), []string{"summarize-pr", "--pr-number", "123"}) // Assert assert.NoError(t, err) + mockFactory.AssertExpectations(t) mockPRService.AssertExpectations(t) }) - t.Run("should fail when repo is not configured", func(t *testing.T) { + t.Run("should fail when factory returns error", func(t *testing.T) { // Arrange - mockPRService, translations, cfg := setupSummarizeTest(t) - cfg.ActiveVCSProvider = "" - cfg.VCSConfigs = nil - - cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) - app := cmd - ctx := context.Background() + _, mockFactory, translations, cfg := setupSummarizeTest(t) - // Act - err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123"}) - - // Assert - assert.Error(t, err) - assert.Contains(t, err.Error(), translations.GetMessage("error.no_repo_configured", 0, nil)) - }) - - t.Run("should fail with invalid repo format", func(t *testing.T) { - // Arrange - mockPRService, translations, cfg := setupSummarizeTest(t) - - cfg.VCSConfigs["github"] = config.VCSConfig{ - Owner: "testowner", - Repo: "", - } + mockError := fmt.Errorf("factory error") + mockFactory.On("CreatePRService").Return(nil, mockError) - cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) - app := cmd - ctx := context.Background() + prCommand := NewSummarizeCommand(mockFactory) + cmd := prCommand.CreateCommand(translations, cfg) // Act - err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123"}) + err := cmd.Run(context.Background(), []string{"summarize-pr", "--pr-number", "123"}) // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), translations.GetMessage("error.invalid_repo_format", 0, nil)) + assert.Contains(t, err.Error(), translations.GetMessage("error.pr_service_creation_error", 0, nil)) + mockFactory.AssertExpectations(t) }) t.Run("should fail when PR service returns error", func(t *testing.T) { // Arrange - mockPRService, translations, cfg := setupSummarizeTest(t) + mockPRService, mockFactory, translations, cfg := setupSummarizeTest(t) prNumber := 123 mockError := fmt.Errorf("service error") + mockFactory.On("CreatePRService").Return(mockPRService, nil) mockPRService.On("SummarizePR", mock.Anything, prNumber).Return(models.PRSummary{}, mockError) - cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) - app := cmd - ctx := context.Background() + prCommand := NewSummarizeCommand(mockFactory) + cmd := prCommand.CreateCommand(translations, cfg) // Act - err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123"}) + err := cmd.Run(context.Background(), []string{"summarize-pr", "--pr-number", "123"}) // Assert assert.Error(t, err) assert.Contains(t, err.Error(), translations.GetMessage("error.pr_summary_error", 0, nil)) + mockFactory.AssertExpectations(t) mockPRService.AssertExpectations(t) }) } diff --git a/internal/config/ai.go b/internal/config/ai.go index 63750e4..bd88d4a 100644 --- a/internal/config/ai.go +++ b/internal/config/ai.go @@ -10,11 +10,38 @@ const ( type Model string const ( - ModelGeminiV15Flash Model = "gemini-1.5-flash" - ModelGeminiV15Pro Model = "gemini-1.5-pro" - ModelGeminiV20Flash Model = "gemini-2.0-flash" + ModelGeminiV25Pro Model = "gemini-2.5-pro" + ModelGeminiV25Flash Model = "gemini-2.5-flash" + ModelGeminiV25FlashLite Model = "gemini-2.5-flash-lite" // TODO: Agregar mas modelos para openai o otros... ModelGPTV4o Model = "gpt-4o" ModelGPTV4oMini Model = "gpt-4o-mini" ) + +func SupportedAIs() []AI { + return []AI{ + AIGemini, + } +} + +func ModelsForAI(ai AI) []Model { + switch ai { + case AIGemini: + return []Model{ + ModelGeminiV25Pro, + ModelGeminiV25Flash, + ModelGeminiV25FlashLite, + } + default: + return []Model{} + } +} + +func DefaultModelForAI(ai AI) Model { + models := ModelsForAI(ai) + if len(models) == 0 { + return "" + } + return models[0] +} diff --git a/internal/config/providers.go b/internal/config/providers.go new file mode 100644 index 0000000..cbd78db --- /dev/null +++ b/internal/config/providers.go @@ -0,0 +1,9 @@ +package config + +func SupportedVCSProviders() []string { + return []string{"github"} +} + +func SupportedTicketServices() []string { + return []string{"jira"} +} diff --git a/internal/i18n/locales/active.en.toml b/internal/i18n/locales/active.en.toml index 5c360e9..5ffdaac 100644 --- a/internal/i18n/locales/active.en.toml +++ b/internal/i18n/locales/active.en.toml @@ -43,6 +43,15 @@ other = "❌ Error generating suggestions: {{.Error}}" [config_command_usage] other = "⚙️ Manage configuration" +[config_init_usage] +other = "🚀 Initialize configuration wizard" + +[config_edit_usage] +other = "✏️ Open configuration file in editor" + +[work_in_progress] +other = "🚧 This feature is under development." + [config_set_lang_usage] other = "🌍 Set default language" @@ -249,4 +258,45 @@ error_get_name_from_branch = "Error getting branch name: {{.Error}}" [pr_service] error_get_pr = "Error getting the PR: {{.Error}}" error_create_summary_pr = "Error creating PR summary: {{.Error}}" -error_update_pr = "Error updating PR: {{.Error}}" \ No newline at end of file +error_update_pr = "Error updating PR: {{.Error}}" + +[init] +section_welcome = "1. Welcome and AI" +welcome = "👋 Welcome to the MateCommit setup assistant!" +ai_intro = "First, let’s configure the AI. (Supported providers: {{.Providers}})" +prompt_gemini_api_key = "> Enter your Gemini API Key (Enter to skip): " +model_hint_supported = "> You can specify a model (supported: {{.Models}})." +prompt_model_with_default = "> Model to use (default: {{.Default}}): " + +section_language = "2. Language" +language_supported_with_current = "Now, the language. (Supported: en, es) Current: {{.Current}}" +prompt_language_blank_keeps = "> Choose your preferred language (en/es). Enter keeps current: " +error_invalid_language = "Invalid language. Please enter 'en' or 'es'." + +section_vcs = "3. Version Control (VCS)" +prompt_vcs_enable_blank_no = "Configure VCS for features like PR summaries? (Supported: {{.Providers}}) (y/n, Enter = no): " +prompt_github_token_blank_skip = "> Enter your GitHub Personal Access Token (Enter to skip): " +info_vcs_skipped = "VCS configuration skipped." + +section_tickets = "4. Ticket Manager" +prompt_ticket_enable_blank_no = "Integrate a ticket manager? (Supported: {{.Providers}}) (y/n, Enter = no): " +prompt_jira_base_url_blank_cancel = "> Base URL of your Jira instance (Enter to cancel): " +prompt_jira_email_blank_cancel = "> Your Jira email (Enter to cancel): " +prompt_jira_api_token_blank_cancel = "> Your Jira API Token (Enter to cancel): " +info_jira_canceled = "Jira configuration was canceled." + +section_finish = "5. Finish" +saved_ok = "✅ Configuration saved successfully!" +summary_header = "📋 Configuration summary" +summary_model = "Configured model for {{.AI}}: {{.Model}}" +summary_model_none = "No model configured for {{.AI}}" +summary_api = "🔑 API {{.AI}}: {{.Configured}}" +summary_vcs_none = "No VCS provider configured" +prompt_run_again = "Do you want to run the wizard again? (y/n): " + +[ai_missing] +for_suggest = "❌ AI is not configured. Set your Gemini API Key to use 'suggest'.\n 👉 matecommit config init" +for_pr = "❌ AI is not configured. Set your Gemini API Key to use 'summarize-pr'.\n 👉 matecommit config init" + +ai_missing_for_suggest = "❌ AI is not configured. Set your Gemini API Key to use 'suggest'.\n 👉 matecommit config init" +ai_missing_for_pr = "❌ AI is not configured. Set your Gemini API Key to use 'summarize-pr'.\n 👉 matecommit config init" \ No newline at end of file diff --git a/internal/i18n/locales/active.es.toml b/internal/i18n/locales/active.es.toml index 458418e..0f018bd 100644 --- a/internal/i18n/locales/active.es.toml +++ b/internal/i18n/locales/active.es.toml @@ -61,6 +61,15 @@ other = "❌ Error al generar sugerencias: {{.Error}}" [config_command_usage] other = "⚙️ Administrar configuración" +[config_init_usage] +other = "🚀 Iniciar asistente de configuración" + +[config_edit_usage] +other = "✏️ Abrir archivo de configuración en el editor" + +[work_in_progress] +other = "🚧 Esta funcionalidad está en desarrollo." + [config_set_lang_usage] other = "🌍 Configurar idioma por defecto" @@ -256,4 +265,45 @@ error_get_name_from_branch = "Error al obtener el nombre de la branch: {{.Error} [pr_service] error_get_pr = "Error al obtener el PR: {{.Error}}" error_create_summary_pr = "Error al crear el resumen del PR: {{.Error}}" -error_update_pr = "Error al actualizar el PR: {{.Error}}" \ No newline at end of file +error_update_pr = "Error al actualizar el PR: {{.Error}}" + +[init] +section_welcome = "1. Bienvenida e IA" +welcome = "👋 ¡Bienvenido al asistente de configuración de MateCommit!" +ai_intro = "Primero, configuremos la IA. (Proveedores soportados: {{.Providers}})" +prompt_gemini_api_key = "> Introduce tu API Key de Gemini (Enter para omitir): " +model_hint_supported = "> Puedes especificar un modelo (soportados: {{.Models}})." +prompt_model_with_default = "> Modelo a usar (por defecto: {{.Default}}): " + +section_language = "2. Idioma" +language_supported_with_current = "Ahora, el idioma. (Soportados: en, es) Actual: {{.Current}}" +prompt_language_blank_keeps = "> Elige tu idioma preferido (en/es). Enter mantiene el actual: " +error_invalid_language = "Idioma inválido. Por favor ingresa 'en' o 'es'." + +section_vcs = "3. Control de Versiones (VCS)" +prompt_vcs_enable_blank_no = "¿Configurar VCS para funciones como resúmenes de PRs? (Soportado: {{.Providers}}) (s/n, Enter = no): " +prompt_github_token_blank_skip = "> Introduce tu token de acceso de GitHub (Enter para omitir): " +info_vcs_skipped = "Se omitió la configuración de VCS." + +section_tickets = "4. Gestor de Tickets" +prompt_ticket_enable_blank_no = "¿Integrar un gestor de tickets? (Soportado: {{.Providers}}) (s/n, Enter = no): " +prompt_jira_base_url_blank_cancel = "> URL base de tu instancia de Jira (Enter para cancelar): " +prompt_jira_email_blank_cancel = "> Tu email de Jira (Enter para cancelar): " +prompt_jira_api_token_blank_cancel = "> Tu API Token de Jira (Enter para cancelar): " +info_jira_canceled = "Se canceló la configuración de Jira." + +section_finish = "5. Finalización" +saved_ok = "✅ ¡Configuración guardada con éxito!" +summary_header = "📋 Resumen de configuración" +summary_model = "Modelo configurado para {{.AI}}: {{.Model}}" +summary_model_none = "Ningún modelo configurado para {{.AI}}" +summary_api = "🔑 API {{.AI}}: {{.Configured}}" +summary_vcs_none = "Proveedor VCS no configurado" +prompt_run_again = "¿Querés volver a ejecutar el asistente? (s/n): " + +[ai_missing] +for_suggest = "❌ La IA no está configurada. Configurá tu API Key de Gemini para usar 'suggest'.\n 👉 matecommit config init" +for_pr = "❌ La IA no está configurada. Configurá tu API Key de Gemini para usar 'summarize-pr'.\n 👉 matecommit config init" + +ai_missing_for_suggest = "❌ La IA no está configurada. Configurá tu API Key de Gemini para usar 'suggest'.\n 👉 matecommit config init" +ai_missing_for_pr = "❌ La IA no está configurada. Configurá tu API Key de Gemini para usar 'summarize-pr'.\n 👉 matecommit config init" \ No newline at end of file diff --git a/internal/infrastructure/factory/pr_service_factory.go b/internal/infrastructure/factory/pr_service_factory.go index 1c93548..ea3e448 100644 --- a/internal/infrastructure/factory/pr_service_factory.go +++ b/internal/infrastructure/factory/pr_service_factory.go @@ -10,15 +10,19 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/services" ) -type PRServiceFactory struct { +type PRServiceFactoryInterface interface { + CreatePRService() (ports.PRService, error) +} + +type prServiceFactory struct { config *config.Config aiService ports.PRSummarizer trans *i18n.Translations gitService ports.GitService } -func NewPrServiceFactory(cfg *config.Config, trans *i18n.Translations, aiService ports.PRSummarizer, gitService ports.GitService) *PRServiceFactory { - return &PRServiceFactory{ +func NewPrServiceFactory(cfg *config.Config, trans *i18n.Translations, aiService ports.PRSummarizer, gitService ports.GitService) PRServiceFactoryInterface { + return &prServiceFactory{ config: cfg, trans: trans, aiService: aiService, @@ -26,7 +30,7 @@ func NewPrServiceFactory(cfg *config.Config, trans *i18n.Translations, aiService } } -func (f *PRServiceFactory) CreatePRService() (ports.PRService, error) { +func (f *prServiceFactory) CreatePRService() (ports.PRService, error) { owner, repo, provider, err := f.gitService.GetRepoInfo() if err != nil { return nil, fmt.Errorf("error al obtener la informacion del repositorio: %w", err) @@ -41,7 +45,7 @@ func (f *PRServiceFactory) CreatePRService() (ports.PRService, error) { } provider = f.config.ActiveVCSProvider } else { - return nil, fmt.Errorf("proveedor de VCS '%s' detectado automáticamente pero no configurado. Use 'matecommit config set-vcs --provider %s --token ' para configurarlo", provider, provider) + return nil, fmt.Errorf("proveedor de VCS '%s' detectado automáticamente pero no configurado", provider) } } diff --git a/internal/services/commit_service.go b/internal/services/commit_service.go index 901894d..0eeb226 100644 --- a/internal/services/commit_service.go +++ b/internal/services/commit_service.go @@ -32,6 +32,11 @@ func NewCommitService(git ports.GitService, ai ports.CommitSummarizer, ticketMan func (s *CommitService) GenerateSuggestions(ctx context.Context, count int) ([]models.CommitSuggestion, error) { var commitInfo models.CommitInfo + if s.ai == nil { + msg := s.trans.GetMessage("ai_missing_for_suggest", 0, nil) + return nil, fmt.Errorf("%s", msg) + } + changes, err := s.git.GetChangedFiles() if err != nil { return nil, err diff --git a/internal/services/pr_service.go b/internal/services/pr_service.go index efb755f..eb18bc3 100644 --- a/internal/services/pr_service.go +++ b/internal/services/pr_service.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" @@ -23,6 +24,10 @@ func NewPRService(vcsClient ports.VCSClient, aiService ports.PRSummarizer, trans } func (s *PRService) SummarizePR(ctx context.Context, prNumber int) (models.PRSummary, error) { + if s.aiService == nil { + msg := s.trans.GetMessage("ai_missing_for_pr", 0, nil) + return models.PRSummary{}, fmt.Errorf("%s", msg) + } prData, err := s.vcsClient.GetPR(ctx, prNumber) if err != nil { msg := s.trans.GetMessage("pr_service.error_get_pr", 0, map[string]interface{}{ diff --git a/internal/services/pr_service_test.go b/internal/services/pr_service_test.go index 7e09b7a..0cfbf4b 100644 --- a/internal/services/pr_service_test.go +++ b/internal/services/pr_service_test.go @@ -263,7 +263,7 @@ func setupServices(t *testing.T, testConfig TestConfig) (*PRService, error) { Language: "es", AIConfig: config.AIConfig{ Models: map[config.AI]config.Model{ - config.AIGemini: config.ModelGeminiV15Flash, + config.AIGemini: config.ModelGeminiV25Flash, }, }, }