From a6fe7e673d900745d6c78014d905d1c17d5b74e4 Mon Sep 17 00:00:00 2001 From: "inference-gateway-maintainer[bot]" <246577062+inference-gateway-maintainer[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:13:22 +0000 Subject: [PATCH 1/7] feat: add /env shortcut for creating .env.example with provider API keys --- cmd/env.go | 136 +++++++++++++++++++++ cmd/env_test.go | 236 +++++++++++++++++++++++++++++++++++++ cmd/init.go | 15 +++ docs/commands-reference.md | 32 +++++ docs/shortcuts-guide.md | 1 + 5 files changed, 420 insertions(+) create mode 100644 cmd/env.go create mode 100644 cmd/env_test.go diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 00000000..1de72956 --- /dev/null +++ b/cmd/env.go @@ -0,0 +1,136 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + cobra "github.com/spf13/cobra" + + config "github.com/inference-gateway/cli/config" + icons "github.com/inference-gateway/cli/internal/ui/styles/icons" +) + +// envExampleFileName is the name of the .env.example file +const envExampleFileName = ".env.example" + +// envVars is the list of all provider API environment variables +var envVars = []string{ + "GOOGLE_SEARCH_API_KEY", + "GOOGLE_SEARCH_ENGINE_ID", + "", + "DUCKDUCKGO_SEARCH_API_KEY", + "", + "ANTHROPIC_API_KEY", + "CLOUDFLARE_API_KEY", + "COHERE_API_KEY", + "GROQ_API_KEY", + "OLLAMA_API_KEY", + "OLLAMA_CLOUD_API_KEY", + "OPENAI_API_KEY", + "DEEPSEEK_API_KEY", + "GOOGLE_API_KEY", + "MISTRAL_API_KEY", + "MINIMAX_API_KEY", + "MOONSHOT_API_KEY", +} + +// envExampleContent generates the content for .env.example +func envExampleContent() string { + var sb strings.Builder + sb.WriteString("# Inference Gateway Environment Variables\n") + sb.WriteString("# Copy this file to .env and fill in your API keys\n") + sb.WriteString("#\n") + sb.WriteString("# cp .env.example .env\n") + sb.WriteString("\n") + + for _, v := range envVars { + if v == "" { + sb.WriteString("\n") + } else { + sb.WriteString(fmt.Sprintf("%s=\n", v)) + } + } + + return sb.String() +} + +var envCmd = &cobra.Command{ + Use: "env", + Short: "Generate a .env.example file with provider API keys", + Long: `Generate a .env.example file in the current directory with all the +different provider API environment variables needed by the Inference Gateway. + +This is a convenient shortcut so you don't need to remember which providers +are available or what environment variables to set. + +After the file is created, copy it to .env and fill in your API keys: + + cp .env.example .env + +If .env.example already exists, this command will error. Use --overwrite to +replace it.`, + RunE: func(cmd *cobra.Command, args []string) error { + return createEnvExample(cmd) + }, +} + +func init() { + envCmd.Flags().Bool("overwrite", false, "Overwrite .env.example if it already exists") + rootCmd.AddCommand(envCmd) +} + +// createEnvExample creates the .env.example file with provider API keys +func createEnvExample(cmd *cobra.Command) error { + overwrite, _ := cmd.Flags().GetBool("overwrite") + + envExamplePath := envExampleFileName + + // Check if .env.example already exists + if !overwrite { + if _, err := os.Stat(envExamplePath); err == nil { + return fmt.Errorf("%s %s already exists (use --overwrite to replace)", envExampleFileName, envExamplePath) + } + } + + // Create .env.example + content := envExampleContent() + if err := os.WriteFile(envExamplePath, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to create %s file: %w", envExampleFileName, err) + } + + fmt.Printf("%s Successfully created %s\n", icons.CheckMarkStyle.Render(icons.CheckMark), envExamplePath) + fmt.Println("") + fmt.Println("Next steps:") + fmt.Println(" cp .env.example .env") + fmt.Println(" Edit .env and add your API keys") + fmt.Println("") + + // Check if .gitignore exists, if not create one with .env entry + gitignorePath := filepath.Join(config.ConfigDirName, config.GitignoreFileName) + if _, err := os.Stat(gitignorePath); os.IsNotExist(err) { + // Check if there's a root .gitignore + rootGitignore := config.GitignoreFileName + if _, err := os.Stat(rootGitignore); os.IsNotExist(err) { + gitignoreContent := "# Inference Gateway\n.env\n" + if err := os.WriteFile(rootGitignore, []byte(gitignoreContent), 0644); err != nil { + return fmt.Errorf("failed to create .gitignore file: %w", err) + } + fmt.Printf("%s Created .gitignore with .env entry\n", icons.CheckMarkStyle.Render(icons.CheckMark)) + } else if err == nil { + // .gitignore exists, check if .env is already in it + data, err := os.ReadFile(rootGitignore) + if err == nil && !strings.Contains(string(data), ".env") { + f, err := os.OpenFile(rootGitignore, os.O_APPEND|os.O_WRONLY, 0644) + if err == nil { + _, _ = f.WriteString("\n# Inference Gateway\n.env\n") + f.Close() + fmt.Printf("%s Added .env to .gitignore\n", icons.CheckMarkStyle.Render(icons.CheckMark)) + } + } + } + } + + return nil +} diff --git a/cmd/env_test.go b/cmd/env_test.go new file mode 100644 index 00000000..f3f44065 --- /dev/null +++ b/cmd/env_test.go @@ -0,0 +1,236 @@ +package cmd + +import ( + "os" + "strings" + "testing" + + cobra "github.com/spf13/cobra" +) + +func TestEnvExampleContent(t *testing.T) { + content := envExampleContent() + + // Check that it contains the header + if !strings.Contains(content, "# Inference Gateway Environment Variables") { + t.Errorf("envExampleContent() should contain header") + } + + // Check that it contains the cp hint + if !strings.Contains(content, "cp .env.example .env") { + t.Errorf("envExampleContent() should contain cp hint") + } + + // Check that it contains all expected provider API keys + expectedVars := []string{ + "ANTHROPIC_API_KEY=", + "OPENAI_API_KEY=", + "DEEPSEEK_API_KEY=", + "GOOGLE_API_KEY=", + "GROQ_API_KEY=", + "MISTRAL_API_KEY=", + "CLOUDFLARE_API_KEY=", + "COHERE_API_KEY=", + "OLLAMA_API_KEY=", + "OLLAMA_CLOUD_API_KEY=", + "GOOGLE_SEARCH_API_KEY=", + "GOOGLE_SEARCH_ENGINE_ID=", + "DUCKDUCKGO_SEARCH_API_KEY=", + "MINIMAX_API_KEY=", + "MOONSHOT_API_KEY=", + } + + for _, v := range expectedVars { + if !strings.Contains(content, v) { + t.Errorf("envExampleContent() should contain %s", v) + } + } +} + +func TestCreateEnvExample(t *testing.T) { + tests := []struct { + name string + setupFiles map[string]string // files to create before test + overwrite bool + wantErr bool + errContains string + wantFileExists bool + }{ + { + name: "creates .env.example when it doesn't exist", + setupFiles: nil, + overwrite: false, + wantErr: false, + wantFileExists: true, + }, + { + name: "fails when .env.example already exists", + setupFiles: map[string]string{ + ".env.example": "existing content", + }, + overwrite: false, + wantErr: true, + errContains: "already exists", + wantFileExists: true, + }, + { + name: "overwrites when --overwrite is set", + setupFiles: map[string]string{ + ".env.example": "existing content", + }, + overwrite: true, + wantErr: false, + wantFileExists: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "infer-env-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + defer func() { _ = os.Chdir(oldWd) }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp dir: %v", err) + } + + // Setup pre-existing files + for path, content := range tt.setupFiles { + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("failed to create setup file %s: %v", path, err) + } + } + + cmd := &cobra.Command{} + cmd.Flags().Bool("overwrite", tt.overwrite, "") + _ = cmd.Flag("overwrite").Value.Set(func() string { + if tt.overwrite { + return "true" + } + return "false" + }()) + + err = createEnvExample(cmd) + + if (err != nil) != tt.wantErr { + t.Errorf("createEnvExample() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && tt.errContains != "" && err != nil { + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("createEnvExample() error should contain %q, got %q", tt.errContains, err.Error()) + } + } + + if tt.wantFileExists { + if _, err := os.Stat(".env.example"); os.IsNotExist(err) { + t.Errorf("expected .env.example to exist, but it doesn't") + } + } + }) + } +} + +func TestCreateEnvExampleCreatesGitignore(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "infer-env-gitignore-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + defer func() { _ = os.Chdir(oldWd) }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp dir: %v", err) + } + + cmd := &cobra.Command{} + cmd.Flags().Bool("overwrite", false, "") + _ = cmd.Flag("overwrite").Value.Set("false") + + err = createEnvExample(cmd) + if err != nil { + t.Fatalf("createEnvExample() error = %v", err) + } + + // Check that .gitignore was created with .env entry + gitignoreContent, err := os.ReadFile(".gitignore") + if err != nil { + t.Fatalf("expected .gitignore to be created, but got error: %v", err) + } + + if !strings.Contains(string(gitignoreContent), ".env") { + t.Errorf(".gitignore should contain .env entry, got: %s", string(gitignoreContent)) + } +} + +func TestCreateEnvExampleWithExistingGitignore(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "infer-env-existing-gitignore-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + defer func() { _ = os.Chdir(oldWd) }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp dir: %v", err) + } + + // Create a .gitignore without .env + if err := os.WriteFile(".gitignore", []byte("*.log\n"), 0644); err != nil { + t.Fatalf("failed to create .gitignore: %v", err) + } + + cmd := &cobra.Command{} + cmd.Flags().Bool("overwrite", false, "") + _ = cmd.Flag("overwrite").Value.Set("false") + + err = createEnvExample(cmd) + if err != nil { + t.Fatalf("createEnvExample() error = %v", err) + } + + // Check that .env was added to .gitignore + gitignoreContent, err := os.ReadFile(".gitignore") + if err != nil { + t.Fatalf("expected .gitignore to exist, but got error: %v", err) + } + + if !strings.Contains(string(gitignoreContent), ".env") { + t.Errorf(".gitignore should contain .env entry, got: %s", string(gitignoreContent)) + } +} + +func TestEnvExampleContentFormat(t *testing.T) { + content := envExampleContent() + + // Each line should either be empty, a comment, or a KEY= format + lines := strings.Split(content, "\n") + for i, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if !strings.Contains(line, "=") { + t.Errorf("line %d should be in KEY= format, got: %q", i+1, line) + } + } +} diff --git a/cmd/init.go b/cmd/init.go index 03471036..b4f4f57d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -173,6 +173,18 @@ plans/ return fmt.Errorf("failed to create skills directory: %w", err) } + // Create .env.example with provider API keys (non-fatal if it already exists) + envExampleCreated := false + envExamplePath := envExampleFileName + if _, err := os.Stat(envExamplePath); os.IsNotExist(err) { + content := envExampleContent() + if err := os.WriteFile(envExamplePath, []byte(content), 0644); err != nil { + fmt.Printf("%s Warning: failed to create %s: %v\n", icons.CrossMarkStyle.Render(icons.CrossMark), envExampleFileName, err) + } else { + envExampleCreated = true + } + } + var scopeDesc string if userspace { scopeDesc = "userspace" @@ -198,6 +210,9 @@ plans/ fmt.Printf(" Created: %s\n", computerUsePath) fmt.Printf(" Created: %s\n", agentsPath) fmt.Printf(" Created: %s/\n", skillsDirPath) + if envExampleCreated { + fmt.Printf(" Created: %s\n", envExamplePath) + } if migrated { fmt.Printf("\n%s Migrated legacy `channels:` block from config.yaml into %s.\n", icons.CheckMarkStyle.Render(icons.CheckMark), channelsPath) fmt.Printf(" You can now remove the `channels:` block from %s.\n", configPath) diff --git a/docs/commands-reference.md b/docs/commands-reference.md index 59f56d4c..18998632 100644 --- a/docs/commands-reference.md +++ b/docs/commands-reference.md @@ -23,6 +23,7 @@ Initialize a new project with Inference Gateway CLI. This creates: - `.infer/` directory with: - `config.yaml` - Main configuration file for the project - `.gitignore` - Ensures sensitive files are not committed to version control +- `.env.example` - Template with all provider API environment variables (if not already exists) This is the recommended command to start working with Inference Gateway CLI in a new project. @@ -42,6 +43,37 @@ infer init --overwrite infer init --userspace ``` +### `infer env` + +Generate a `.env.example` file in the current directory with all the different provider API +environment variables needed by the Inference Gateway. This is a convenient shortcut so you +don't need to remember which providers are available or what environment variables to set. + +If `.env.example` already exists, the command will error. Use `--overwrite` to replace it. + +If no `.gitignore` exists in the project root, one is created with `.env` added to it. + +**Options:** + +- `--overwrite`: Overwrite `.env.example` if it already exists + +**Examples:** + +```bash +# Create .env.example with all provider API keys +infer env + +# Overwrite existing .env.example +infer env --overwrite +``` + +**Next steps after creation:** + +```bash +cp .env.example .env +# Edit .env and add your API keys +``` + --- ## Configuration Management diff --git a/docs/shortcuts-guide.md b/docs/shortcuts-guide.md index 2ea27f8e..5884d116 100644 --- a/docs/shortcuts-guide.md +++ b/docs/shortcuts-guide.md @@ -50,6 +50,7 @@ These shortcuts are available out of the box: - `/copy [format]` - Copy the current conversation to the system clipboard (formats: `text`, `markdown`, `json`; default `text`) - `/export [format]` - Export conversation to markdown - `/init` - Set input with project analysis prompt for AGENTS.md generation +- `/env` - Generate a `.env.example` file with all provider API environment variables - `/voice [seconds]` - Record from the microphone and transcribe to the input field using Whisper (only available when `speech_to_text.enabled` is `true`) - `/release-notes [version]` - Show GitHub release notes for a version or the latest (requires the `gh` CLI installed and authenticated) From de86b80a6077b95c5f88b219d67f39d59c7d060b Mon Sep 17 00:00:00 2001 From: "inference-gateway-maintainer[bot]" <246577062+inference-gateway-maintainer[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:46:31 +0000 Subject: [PATCH 2/7] fix: simplify .gitignore handling in /env shortcut --- cmd/env.go | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/cmd/env.go b/cmd/env.go index 1de72956..6719b982 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" "os" - "path/filepath" "strings" cobra "github.com/spf13/cobra" @@ -90,7 +89,7 @@ func createEnvExample(cmd *cobra.Command) error { // Check if .env.example already exists if !overwrite { if _, err := os.Stat(envExamplePath); err == nil { - return fmt.Errorf("%s %s already exists (use --overwrite to replace)", envExampleFileName, envExamplePath) + return fmt.Errorf("%s already exists (use --overwrite to replace)", envExamplePath) } } @@ -107,27 +106,23 @@ func createEnvExample(cmd *cobra.Command) error { fmt.Println(" Edit .env and add your API keys") fmt.Println("") - // Check if .gitignore exists, if not create one with .env entry - gitignorePath := filepath.Join(config.ConfigDirName, config.GitignoreFileName) + // Check if root .gitignore exists, if not create one with .env entry + gitignorePath := config.GitignoreFileName if _, err := os.Stat(gitignorePath); os.IsNotExist(err) { - // Check if there's a root .gitignore - rootGitignore := config.GitignoreFileName - if _, err := os.Stat(rootGitignore); os.IsNotExist(err) { - gitignoreContent := "# Inference Gateway\n.env\n" - if err := os.WriteFile(rootGitignore, []byte(gitignoreContent), 0644); err != nil { - return fmt.Errorf("failed to create .gitignore file: %w", err) - } - fmt.Printf("%s Created .gitignore with .env entry\n", icons.CheckMarkStyle.Render(icons.CheckMark)) - } else if err == nil { - // .gitignore exists, check if .env is already in it - data, err := os.ReadFile(rootGitignore) - if err == nil && !strings.Contains(string(data), ".env") { - f, err := os.OpenFile(rootGitignore, os.O_APPEND|os.O_WRONLY, 0644) - if err == nil { - _, _ = f.WriteString("\n# Inference Gateway\n.env\n") - f.Close() - fmt.Printf("%s Added .env to .gitignore\n", icons.CheckMarkStyle.Render(icons.CheckMark)) - } + gitignoreContent := "# Inference Gateway\n.env\n" + if err := os.WriteFile(gitignorePath, []byte(gitignoreContent), 0644); err != nil { + return fmt.Errorf("failed to create .gitignore file: %w", err) + } + fmt.Printf("%s Created .gitignore with .env entry\n", icons.CheckMarkStyle.Render(icons.CheckMark)) + } else if err == nil { + // .gitignore exists, check if .env is already in it + data, err := os.ReadFile(gitignorePath) + if err == nil && !strings.Contains(string(data), ".env") { + f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_WRONLY, 0644) + if err == nil { + _, _ = f.WriteString("\n# Inference Gateway\n.env\n") + f.Close() + fmt.Printf("%s Added .env to .gitignore\n", icons.CheckMarkStyle.Render(icons.CheckMark)) } } } From 0daf2115efb6334d11e34c1a4cecdc37863b8ddf Mon Sep 17 00:00:00 2001 From: "inference-gateway-maintainer[bot]" <246577062+inference-gateway-maintainer[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:48:21 +0000 Subject: [PATCH 3/7] fix: correct indentation in init.go for CI compliance --- cmd/init.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index b4f4f57d..f4d0c98f 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -179,9 +179,9 @@ plans/ if _, err := os.Stat(envExamplePath); os.IsNotExist(err) { content := envExampleContent() if err := os.WriteFile(envExamplePath, []byte(content), 0644); err != nil { - fmt.Printf("%s Warning: failed to create %s: %v\n", icons.CrossMarkStyle.Render(icons.CrossMark), envExampleFileName, err) + fmt.Printf("%s Warning: failed to create %s: %v\n", icons.CrossMarkStyle.Render(icons.CrossMark), envExampleFileName, err) } else { - envExampleCreated = true + envExampleCreated = true } } @@ -211,7 +211,7 @@ plans/ fmt.Printf(" Created: %s\n", agentsPath) fmt.Printf(" Created: %s/\n", skillsDirPath) if envExampleCreated { - fmt.Printf(" Created: %s\n", envExamplePath) + fmt.Printf(" Created: %s\n", envExamplePath) } if migrated { fmt.Printf("\n%s Migrated legacy `channels:` block from config.yaml into %s.\n", icons.CheckMarkStyle.Render(icons.CheckMark), channelsPath) From f777d0dcbb6c3191c6ebef44acd69cc9080faad6 Mon Sep 17 00:00:00 2001 From: "inference-gateway-maintainer[bot]" <246577062+inference-gateway-maintainer[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:50:54 +0000 Subject: [PATCH 4/7] fix: resolve lint issues in env.go --- cmd/env.go | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/cmd/env.go b/cmd/env.go index 6719b982..73162af4 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -48,7 +48,7 @@ func envExampleContent() string { if v == "" { sb.WriteString("\n") } else { - sb.WriteString(fmt.Sprintf("%s=\n", v)) + fmt.Fprintf(&sb, "%s=\n", v) } } @@ -106,7 +106,18 @@ func createEnvExample(cmd *cobra.Command) error { fmt.Println(" Edit .env and add your API keys") fmt.Println("") - // Check if root .gitignore exists, if not create one with .env entry + // Ensure .env is in .gitignore + if err := ensureEnvInGitignore(); err != nil { + return err + } + + return nil +} + +// ensureEnvInGitignore ensures that .env is listed in the root .gitignore file. +// If .gitignore doesn't exist, it creates one with .env entry. +// If .gitignore exists but doesn't contain .env, it appends it. +func ensureEnvInGitignore() error { gitignorePath := config.GitignoreFileName if _, err := os.Stat(gitignorePath); os.IsNotExist(err) { gitignoreContent := "# Inference Gateway\n.env\n" @@ -114,18 +125,27 @@ func createEnvExample(cmd *cobra.Command) error { return fmt.Errorf("failed to create .gitignore file: %w", err) } fmt.Printf("%s Created .gitignore with .env entry\n", icons.CheckMarkStyle.Render(icons.CheckMark)) - } else if err == nil { - // .gitignore exists, check if .env is already in it - data, err := os.ReadFile(gitignorePath) - if err == nil && !strings.Contains(string(data), ".env") { - f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_WRONLY, 0644) - if err == nil { - _, _ = f.WriteString("\n# Inference Gateway\n.env\n") - f.Close() - fmt.Printf("%s Added .env to .gitignore\n", icons.CheckMarkStyle.Render(icons.CheckMark)) - } - } + return nil } + // .gitignore exists, check if .env is already in it + data, err := os.ReadFile(gitignorePath) + if err != nil { + return fmt.Errorf("failed to read .gitignore: %w", err) + } + if strings.Contains(string(data), ".env") { + return nil + } + + f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open .gitignore: %w", err) + } + defer f.Close() + + if _, err := f.WriteString("\n# Inference Gateway\n.env\n"); err != nil { + return fmt.Errorf("failed to write to .gitignore: %w", err) + } + fmt.Printf("%s Added .env to .gitignore\n", icons.CheckMarkStyle.Render(icons.CheckMark)) return nil } From 790b7ac059809edeed57d0c23005dc5518b6c6c4 Mon Sep 17 00:00:00 2001 From: "inference-gateway-maintainer[bot]" <246577062+inference-gateway-maintainer[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:53:52 +0000 Subject: [PATCH 5/7] fix: resolve errcheck lint for f.Close() in env.go --- cmd/env.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/env.go b/cmd/env.go index 73162af4..6430c477 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -141,7 +141,9 @@ func ensureEnvInGitignore() error { if err != nil { return fmt.Errorf("failed to open .gitignore: %w", err) } - defer f.Close() + defer func() { + _ = f.Close() + }() if _, err := f.WriteString("\n# Inference Gateway\n.env\n"); err != nil { return fmt.Errorf("failed to write to .gitignore: %w", err) From 75a4ba1740ad0a63cb5c377317c246fadf54285d Mon Sep 17 00:00:00 2001 From: "inference-gateway-maintainer[bot]" <246577062+inference-gateway-maintainer[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:14:02 +0000 Subject: [PATCH 6/7] fix: address code review feedback for /env shortcut --- cmd/env.go | 15 ++++++++++----- cmd/init.go | 46 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/cmd/env.go b/cmd/env.go index 6430c477..970100da 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -14,13 +14,15 @@ import ( // envExampleFileName is the name of the .env.example file const envExampleFileName = ".env.example" -// envVars is the list of all provider API environment variables +// envVars is the list of all provider API environment variables. +// Empty-string entries produce blank-line separators in the output, +// grouping search-related keys separately from provider API keys. var envVars = []string{ "GOOGLE_SEARCH_API_KEY", "GOOGLE_SEARCH_ENGINE_ID", - "", + "", // --- search providers --- "DUCKDUCKGO_SEARCH_API_KEY", - "", + "", // --- LLM providers --- "ANTHROPIC_API_KEY", "CLOUDFLARE_API_KEY", "COHERE_API_KEY", @@ -133,8 +135,11 @@ func ensureEnvInGitignore() error { if err != nil { return fmt.Errorf("failed to read .gitignore: %w", err) } - if strings.Contains(string(data), ".env") { - return nil + // Check if .env is already listed (exact line match, trimmed) + for _, line := range strings.Split(string(data), "\n") { + if strings.TrimSpace(line) == ".env" { + return nil + } } f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_WRONLY, 0644) diff --git a/cmd/init.go b/cmd/init.go index f4d0c98f..4f4a84b5 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -41,7 +41,7 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen,gocyclo,cyclo var configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, - a2aShortcutsPath, skillsShortcutsPath, mcpPath, keybindingsPath, promptsPath, + envShortcutsPath, a2aShortcutsPath, skillsShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, heartbeatPath, computerUsePath, agentsPath, skillsDirPath string if userspace { @@ -56,6 +56,7 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen,gocyclo,cyclo mcpShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "mcp.yaml") shellsShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "shells.yaml") exportShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "export.yaml") + envShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "env.yaml") a2aShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "a2a.yaml") skillsShortcutsPath = filepath.Join(homeDir, config.ConfigDirName, "shortcuts", "skills.yaml") mcpPath = filepath.Join(homeDir, config.ConfigDirName, config.MCPFileName) @@ -74,6 +75,7 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen,gocyclo,cyclo mcpShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "mcp.yaml") shellsShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "shells.yaml") exportShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "export.yaml") + envShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "env.yaml") a2aShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "a2a.yaml") skillsShortcutsPath = filepath.Join(config.ConfigDirName, "shortcuts", "skills.yaml") mcpPath = filepath.Join(config.ConfigDirName, config.MCPFileName) @@ -87,7 +89,7 @@ func initializeProject(cmd *cobra.Command) error { //nolint:funlen,gocyclo,cyclo } if !overwrite { - if err := validateFilesNotExist(configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, skillsShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, heartbeatPath, computerUsePath, agentsPath); err != nil { + if err := validateFilesNotExist(configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, envShortcutsPath, a2aShortcutsPath, skillsShortcutsPath, mcpPath, keybindingsPath, promptsPath, channelsPath, heartbeatPath, computerUsePath, agentsPath); err != nil { return err } } @@ -131,6 +133,10 @@ plans/ return fmt.Errorf("failed to create Export shortcuts file: %w", err) } + if err := createEnvShortcutsFile(envShortcutsPath); err != nil { + return fmt.Errorf("failed to create Env shortcuts file: %w", err) + } + if err := createA2AShortcutsFile(a2aShortcutsPath); err != nil { return fmt.Errorf("failed to create A2A shortcuts file: %w", err) } @@ -173,7 +179,9 @@ plans/ return fmt.Errorf("failed to create skills directory: %w", err) } - // Create .env.example with provider API keys (non-fatal if it already exists) + // Create .env.example with provider API keys (non-fatal if it already exists). + // Note: --overwrite does NOT affect .env.example; it is always created only + // when absent, to avoid silently overwriting a user's customized file. envExampleCreated := false envExamplePath := envExampleFileName if _, err := os.Stat(envExamplePath); os.IsNotExist(err) { @@ -185,6 +193,13 @@ plans/ } } + // Ensure .env is in the root .gitignore (same safety as `infer env`) + if envExampleCreated { + if err := ensureEnvInGitignore(); err != nil { + fmt.Printf("%s Warning: failed to add .env to .gitignore: %v\n", icons.CrossMarkStyle.Render(icons.CrossMark), err) + } + } + var scopeDesc string if userspace { scopeDesc = "userspace" @@ -669,6 +684,31 @@ shortcuts: return os.WriteFile(path, []byte(exportShortcutsContent), 0644) } +// createEnvShortcutsFile creates the Env shortcuts YAML file that wraps `infer env`, +// so typing `/env` in chat mode runs the env command. +func createEnvShortcutsFile(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return fmt.Errorf("failed to create shortcuts directory: %w", err) + } + + envShortcutsContent := `--- +# Env Shortcuts +# Generate a .env.example file with all provider API environment variables +# +# Usage: +# - /env - Generate a .env.example file with provider API keys + +shortcuts: + - name: env + description: "Generate a .env.example file with provider API keys" + command: infer + args: + - env +` + + return os.WriteFile(path, []byte(envShortcutsContent), 0644) +} + // createA2AShortcutsFile creates the A2A shortcuts YAML file func createA2AShortcutsFile(path string) error { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { From 36a1725199b094b6dcd2aaeb10348c709d345c82 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Fri, 26 Jun 2026 23:18:51 +0200 Subject: [PATCH 7/7] chore: remove noisy comments Co-authored-by: Eden Reich --- cmd/env.go | 4 ---- cmd/env_test.go | 10 +--------- cmd/init.go | 4 ---- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/cmd/env.go b/cmd/env.go index 970100da..81980943 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -88,14 +88,12 @@ func createEnvExample(cmd *cobra.Command) error { envExamplePath := envExampleFileName - // Check if .env.example already exists if !overwrite { if _, err := os.Stat(envExamplePath); err == nil { return fmt.Errorf("%s already exists (use --overwrite to replace)", envExamplePath) } } - // Create .env.example content := envExampleContent() if err := os.WriteFile(envExamplePath, []byte(content), 0644); err != nil { return fmt.Errorf("failed to create %s file: %w", envExampleFileName, err) @@ -130,12 +128,10 @@ func ensureEnvInGitignore() error { return nil } - // .gitignore exists, check if .env is already in it data, err := os.ReadFile(gitignorePath) if err != nil { return fmt.Errorf("failed to read .gitignore: %w", err) } - // Check if .env is already listed (exact line match, trimmed) for _, line := range strings.Split(string(data), "\n") { if strings.TrimSpace(line) == ".env" { return nil diff --git a/cmd/env_test.go b/cmd/env_test.go index f3f44065..9035865f 100644 --- a/cmd/env_test.go +++ b/cmd/env_test.go @@ -11,17 +11,14 @@ import ( func TestEnvExampleContent(t *testing.T) { content := envExampleContent() - // Check that it contains the header if !strings.Contains(content, "# Inference Gateway Environment Variables") { t.Errorf("envExampleContent() should contain header") } - // Check that it contains the cp hint if !strings.Contains(content, "cp .env.example .env") { t.Errorf("envExampleContent() should contain cp hint") } - // Check that it contains all expected provider API keys expectedVars := []string{ "ANTHROPIC_API_KEY=", "OPENAI_API_KEY=", @@ -50,7 +47,7 @@ func TestEnvExampleContent(t *testing.T) { func TestCreateEnvExample(t *testing.T) { tests := []struct { name string - setupFiles map[string]string // files to create before test + setupFiles map[string]string overwrite bool wantErr bool errContains string @@ -102,7 +99,6 @@ func TestCreateEnvExample(t *testing.T) { t.Fatalf("failed to change to temp dir: %v", err) } - // Setup pre-existing files for path, content := range tt.setupFiles { if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("failed to create setup file %s: %v", path, err) @@ -166,7 +162,6 @@ func TestCreateEnvExampleCreatesGitignore(t *testing.T) { t.Fatalf("createEnvExample() error = %v", err) } - // Check that .gitignore was created with .env entry gitignoreContent, err := os.ReadFile(".gitignore") if err != nil { t.Fatalf("expected .gitignore to be created, but got error: %v", err) @@ -194,7 +189,6 @@ func TestCreateEnvExampleWithExistingGitignore(t *testing.T) { t.Fatalf("failed to change to temp dir: %v", err) } - // Create a .gitignore without .env if err := os.WriteFile(".gitignore", []byte("*.log\n"), 0644); err != nil { t.Fatalf("failed to create .gitignore: %v", err) } @@ -208,7 +202,6 @@ func TestCreateEnvExampleWithExistingGitignore(t *testing.T) { t.Fatalf("createEnvExample() error = %v", err) } - // Check that .env was added to .gitignore gitignoreContent, err := os.ReadFile(".gitignore") if err != nil { t.Fatalf("expected .gitignore to exist, but got error: %v", err) @@ -222,7 +215,6 @@ func TestCreateEnvExampleWithExistingGitignore(t *testing.T) { func TestEnvExampleContentFormat(t *testing.T) { content := envExampleContent() - // Each line should either be empty, a comment, or a KEY= format lines := strings.Split(content, "\n") for i, line := range lines { line = strings.TrimSpace(line) diff --git a/cmd/init.go b/cmd/init.go index 4f4a84b5..c53f9e26 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -179,9 +179,6 @@ plans/ return fmt.Errorf("failed to create skills directory: %w", err) } - // Create .env.example with provider API keys (non-fatal if it already exists). - // Note: --overwrite does NOT affect .env.example; it is always created only - // when absent, to avoid silently overwriting a user's customized file. envExampleCreated := false envExamplePath := envExampleFileName if _, err := os.Stat(envExamplePath); os.IsNotExist(err) { @@ -193,7 +190,6 @@ plans/ } } - // Ensure .env is in the root .gitignore (same safety as `infer env`) if envExampleCreated { if err := ensureEnvInGitignore(); err != nil { fmt.Printf("%s Warning: failed to add .env to .gitignore: %v\n", icons.CrossMarkStyle.Render(icons.CrossMark), err)