Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 61 additions & 17 deletions cmd/omni/config_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.omniapp.co --auth oauth

# Non-interactive API key
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)

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.omniapp.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 {
Expand All @@ -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,
Expand All @@ -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.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)")

return cmd
}

func configShowCmd() *cobra.Command {
Expand Down
107 changes: 107 additions & 0 deletions cmd/omni/config_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading