From 73b30b51870ddc3efd766db505a631b44fb1e132 Mon Sep 17 00:00:00 2001 From: Daniel Spangenberger Date: Fri, 12 Jun 2026 11:46:26 -0400 Subject: [PATCH 1/2] feat: add flags to `omni config init` for non-interactive setup --name, --endpoint, --auth, and --api-key each skip their interactive prompt, so OAuth profiles can be created with a single command: omni config init --name prod --endpoint https://myorg.omni.co --auth oauth --api-key alone implies --auth api-key. Invalid --auth values and the contradictory --auth oauth + --api-key combination are rejected. Co-Authored-By: Claude Fable 5 --- cmd/omni/config_commands.go | 78 +++++++++++++++++----- cmd/omni/config_commands_test.go | 107 +++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 17 deletions(-) diff --git a/cmd/omni/config_commands.go b/cmd/omni/config_commands.go index 9993713..e297770 100644 --- a/cmd/omni/config_commands.go +++ b/cmd/omni/config_commands.go @@ -40,29 +40,64 @@ func addConfigCommands(root *cobra.Command) { } func configInitCmd() *cobra.Command { - return &cobra.Command{ + var ( + name string + endpoint string + authMethod string + apiKey string + ) + + cmd := &cobra.Command{ Use: "init", Short: "Create a new configuration profile", + Long: `Create a new configuration profile. + +Prompts interactively for any value not supplied via flags. With --name, +--endpoint, and --auth all set, no prompts are shown.`, + Example: ` # Interactive setup + omni config init + + # Non-interactive OAuth (opens browser for login) + omni config init --name prod --endpoint https://myorg.omni.co --auth oauth + + # Non-interactive API key + omni config init --name prod --endpoint https://myorg.omni.co --api-key "$OMNI_API_TOKEN"`, RunE: func(cmd *cobra.Command, args []string) error { reader := bufio.NewReader(os.Stdin) - fmt.Print("Profile name: ") - name, _ := reader.ReadString('\n') + if !cmd.Flags().Changed("name") { + fmt.Print("Profile name: ") + name, _ = reader.ReadString('\n') + } name = strings.TrimSpace(name) if name == "" { name = "default" } - fmt.Print("API endpoint (e.g., https://myorg.omni.co): ") - endpoint, _ := reader.ReadString('\n') + if !cmd.Flags().Changed("endpoint") { + fmt.Print("API endpoint (e.g., https://myorg.omni.co): ") + endpoint, _ = reader.ReadString('\n') + } endpoint = strings.TrimSpace(endpoint) - fmt.Println("Authentication method:") - fmt.Println(" 1) API key") - fmt.Println(" 2) OAuth (browser login)") - fmt.Print("Choose [1/2]: ") - choice, _ := reader.ReadString('\n') - choice = strings.TrimSpace(strings.ToLower(choice)) + choice := strings.TrimSpace(strings.ToLower(authMethod)) + switch { + case choice == "" && apiKey != "": + choice = "api-key" + case choice == "": + fmt.Println("Authentication method:") + fmt.Println(" 1) API key") + fmt.Println(" 2) OAuth (browser login)") + fmt.Print("Choose [1/2]: ") + choice, _ = reader.ReadString('\n') + choice = strings.TrimSpace(strings.ToLower(choice)) + case choice != "oauth" && choice != "api-key": + return fmt.Errorf("invalid --auth %q — must be %q or %q", authMethod, "api-key", "oauth") + } + + if choice == "oauth" && apiKey != "" { + return fmt.Errorf("--api-key cannot be combined with --auth oauth") + } cfg, err := config.Load() if err != nil { @@ -87,13 +122,15 @@ func configInitCmd() *cobra.Command { cfg.Profiles[name] = p default: // "1", "a", "api-key", or empty - fmt.Print("API key: ") - apiKeyBytes, err := term.ReadPassword(int(os.Stdin.Fd())) - fmt.Println() - if err != nil { - return fmt.Errorf("reading API key: %w", err) + if apiKey == "" { + fmt.Print("API key: ") + apiKeyBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + return fmt.Errorf("reading API key: %w", err) + } + apiKey = strings.TrimSpace(string(apiKeyBytes)) } - apiKey := strings.TrimSpace(string(apiKeyBytes)) cfg.Profiles[name] = config.Profile{ APIEndpoint: endpoint, @@ -114,6 +151,13 @@ func configInitCmd() *cobra.Command { return nil }, } + + cmd.Flags().StringVar(&name, "name", "", "profile name (skips prompt)") + cmd.Flags().StringVar(&endpoint, "endpoint", "", "API endpoint, e.g. https://myorg.omni.co (skips prompt)") + cmd.Flags().StringVar(&authMethod, "auth", "", `authentication method: "api-key" or "oauth" (skips prompt)`) + cmd.Flags().StringVar(&apiKey, "api-key", "", "API key (skips prompt; implies --auth api-key)") + + return cmd } func configShowCmd() *cobra.Command { diff --git a/cmd/omni/config_commands_test.go b/cmd/omni/config_commands_test.go index fbf83ad..53ec6fe 100644 --- a/cmd/omni/config_commands_test.go +++ b/cmd/omni/config_commands_test.go @@ -81,6 +81,113 @@ func TestApplyOAuthToken_CopiesAllFields(t *testing.T) { } } +// --- config init (non-interactive flags) --- + +// With --name, --endpoint, and --api-key all supplied, init must not prompt +// for anything and should write the profile straight to disk. +func TestConfigInit_NonInteractiveAPIKey(t *testing.T) { + withConfig(t, nil) + + cmd := configInitCmd() + cmd.SetArgs([]string{ + "--name", "prod", + "--endpoint", "https://myorg.omniapp.co", + "--api-key", "sk-test-1234", + }) + out := captureStdout(t, func() { + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute: %v", err) + } + }) + if strings.Contains(out, "Profile name:") || strings.Contains(out, "Choose [1/2]:") { + t.Errorf("expected no interactive prompts, got:\n%s", out) + } + + cfg, err := config.Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + p := cfg.Profiles["prod"] + if p.APIEndpoint != "https://myorg.omniapp.co" { + t.Errorf("APIEndpoint = %q", p.APIEndpoint) + } + // --api-key alone implies --auth api-key. + if p.AuthMethod != "api-key" { + t.Errorf("AuthMethod = %q, want %q", p.AuthMethod, "api-key") + } + if p.APIKey != "sk-test-1234" { + t.Errorf("APIKey = %q", p.APIKey) + } + // First profile becomes the default. + if cfg.DefaultProfile != "prod" { + t.Errorf("DefaultProfile = %q, want %q", cfg.DefaultProfile, "prod") + } +} + +// --auth oauth must validate the endpoint BEFORE launching the browser flow, +// so a bad endpoint fails fast instead of opening a browser against it. +func TestConfigInit_OAuthRejectsInvalidEndpoint(t *testing.T) { + withConfig(t, nil) + t.Setenv("OMNI_CLI_DANGEROUSLY_ALLOW_INSECURE_REQUESTS", "") + + cmd := configInitCmd() + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{ + "--name", "prod", + "--endpoint", "http://insecure.omniapp.co", + "--auth", "oauth", + }) + err := cmd.Execute() + if err == nil { + t.Fatal("expected endpoint validation error, got nil") + } + if !strings.Contains(err.Error(), "HTTPS") { + t.Errorf("error = %q, want HTTPS validation failure", err.Error()) + } +} + +func TestConfigInit_InvalidAuthFlag(t *testing.T) { + withConfig(t, nil) + + cmd := configInitCmd() + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{ + "--name", "prod", + "--endpoint", "https://myorg.omniapp.co", + "--auth", "magic", + }) + err := cmd.Execute() + if err == nil { + t.Fatal("expected error for invalid --auth value, got nil") + } + if !strings.Contains(err.Error(), `invalid --auth "magic"`) { + t.Errorf("error = %q, want it to name the invalid value", err.Error()) + } +} + +func TestConfigInit_APIKeyConflictsWithOAuth(t *testing.T) { + withConfig(t, nil) + + cmd := configInitCmd() + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{ + "--name", "prod", + "--endpoint", "https://myorg.omniapp.co", + "--auth", "oauth", + "--api-key", "sk-test-1234", + }) + err := cmd.Execute() + if err == nil { + t.Fatal("expected conflict error, got nil") + } + if !strings.Contains(err.Error(), "--api-key cannot be combined") { + t.Errorf("error = %q, want api-key/oauth conflict message", err.Error()) + } +} + // --- config logout --- func TestConfigLogout_ClearsTokensForNamedProfile(t *testing.T) { From 42610d7f595270ae587b410c21b506a83600a1f2 Mon Sep 17 00:00:00 2001 From: Daniel Spangenberger Date: Fri, 12 Jun 2026 11:54:33 -0400 Subject: [PATCH 2/2] docs: use real omniapp.co domain in example endpoint URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Omni app URLs are https://.omniapp.co, and that domain is on the endpoint allowlist — so copied examples now pass validation. Co-Authored-By: Claude Fable 5 --- cmd/omni/config_commands.go | 8 ++++---- internal/auth/auth_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/omni/config_commands.go b/cmd/omni/config_commands.go index e297770..269a67d 100644 --- a/cmd/omni/config_commands.go +++ b/cmd/omni/config_commands.go @@ -58,10 +58,10 @@ Prompts interactively for any value not supplied via flags. With --name, omni config init # Non-interactive OAuth (opens browser for login) - omni config init --name prod --endpoint https://myorg.omni.co --auth oauth + omni config init --name prod --endpoint https://myorg.omniapp.co --auth oauth # Non-interactive API key - omni config init --name prod --endpoint https://myorg.omni.co --api-key "$OMNI_API_TOKEN"`, + omni config init --name prod --endpoint https://myorg.omniapp.co --api-key "$OMNI_API_TOKEN"`, RunE: func(cmd *cobra.Command, args []string) error { reader := bufio.NewReader(os.Stdin) @@ -75,7 +75,7 @@ Prompts interactively for any value not supplied via flags. With --name, } if !cmd.Flags().Changed("endpoint") { - fmt.Print("API endpoint (e.g., https://myorg.omni.co): ") + fmt.Print("API endpoint (e.g., https://myorg.omniapp.co): ") endpoint, _ = reader.ReadString('\n') } endpoint = strings.TrimSpace(endpoint) @@ -153,7 +153,7 @@ Prompts interactively for any value not supplied via flags. With --name, } cmd.Flags().StringVar(&name, "name", "", "profile name (skips prompt)") - cmd.Flags().StringVar(&endpoint, "endpoint", "", "API endpoint, e.g. https://myorg.omni.co (skips prompt)") + cmd.Flags().StringVar(&endpoint, "endpoint", "", "API endpoint, e.g. https://myorg.omniapp.co (skips prompt)") cmd.Flags().StringVar(&authMethod, "auth", "", `authentication method: "api-key" or "oauth" (skips prompt)`) cmd.Flags().StringVar(&apiKey, "api-key", "", "API key (skips prompt; implies --auth api-key)") diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 8ebe57d..9a02a5b 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -150,8 +150,8 @@ func TestDo_NilBody(t *testing.T) { } // If the user's config has a trailing slash on the base URL (like -// "https://myorg.omni.co/"), we shouldn't end up with a double slash -// in the final URL ("https://myorg.omni.co//api/v1/models"). +// "https://myorg.omniapp.co/"), we shouldn't end up with a double slash +// in the final URL ("https://myorg.omniapp.co//api/v1/models"). func TestDo_BaseURLTrailingSlash(t *testing.T) { var gotPath string