diff --git a/internal/pcurl/add.go b/internal/pcurl/add.go index e63c9e7..f864756 100644 --- a/internal/pcurl/add.go +++ b/internal/pcurl/add.go @@ -17,14 +17,10 @@ type AddOptions struct { } func confirmExisting(profileName, host string, c *Config, prompt *Prompter, force bool) (existing *Profile, declined bool, err error) { - existing = c.FindProfile(profileName) - if existing == nil { - if name := c.FindProfileByHost(host); name != "" { - existing = c.FindProfile(name) + if existing = c.FindProfile(profileName); existing != nil { + if force { + return existing, false, nil } - } - - if existing != nil && !force { ok, promptErr := prompt.ConfirmUpdate(profileName) if promptErr != nil { return nil, false, promptErr @@ -32,9 +28,31 @@ func confirmExisting(profileName, host string, c *Config, prompt *Prompter, forc if !ok { return existing, true, nil } + return existing, false, nil + } + + // No profile named profileName: check whether another profile already + // covers this host. (otherName == profileName is unreachable here — such a + // profile would have matched FindProfile above — but kept for clarity.) + otherName := c.FindProfileByHost(host) + if otherName == "" || otherName == profileName { + return nil, false, nil } - return existing, false, nil + // --force creates the new profile without prompting; the existing + // same-host profile is left untouched. + if force { + return nil, false, nil + } + + ok, promptErr := prompt.ConfirmHostConflict(profileName, otherName, host) + if promptErr != nil { + return nil, false, promptErr + } + if !ok { + return nil, true, nil + } + return nil, false, nil } func pickHeaders(parsed *curlparse.Result, opts AddOptions, prompt *Prompter) error { diff --git a/internal/pcurl/add_test.go b/internal/pcurl/add_test.go index 4ca0d69..0b6e279 100644 --- a/internal/pcurl/add_test.go +++ b/internal/pcurl/add_test.go @@ -138,6 +138,46 @@ func TestAdd_UpdateExisting_Decline(t *testing.T) { assert.Equal(t, []string{"Accept: old"}, p.Headers, "headers should be unchanged") } +func TestAdd_HostConflict_Confirm(t *testing.T) { + ex, _ := newTestExecuter(t) + c := &Config{Profiles: map[string]*Profile{ + "example.com": {MatchHosts: []string{"example.com"}, Headers: []string{"Accept: old"}}, + }} + require.NoError(t, ex.CM.Save(c)) + var out bytes.Buffer + + err := ex.Add( + []string{"https://example.com", "-H", "Authorization: Bearer new"}, + AddOptions{Name: "alt-account"}, + &out, strings.NewReader("y\nk\n"), false, + ) + require.NoError(t, err) + + c, _ = ex.CM.Load() + assert.NotNil(t, c.FindProfile("example.com"), "original profile should remain") + assert.NotNil(t, c.FindProfile("alt-account"), "new profile should be created") + assert.Contains(t, out.String(), "already used by profile") +} + +func TestAdd_HostConflict_Decline(t *testing.T) { + ex, _ := newTestExecuter(t) + c := &Config{Profiles: map[string]*Profile{ + "example.com": {MatchHosts: []string{"example.com"}, Headers: []string{"Accept: old"}}, + }} + require.NoError(t, ex.CM.Save(c)) + var out bytes.Buffer + + err := ex.Add( + []string{"https://example.com", "-H", "Authorization: Bearer new"}, + AddOptions{Name: "alt-account"}, + &out, strings.NewReader("n\n"), false, + ) + require.NoError(t, err) + + c, _ = ex.CM.Load() + assert.Nil(t, c.FindProfile("alt-account"), "new profile should not be created") +} + func TestAdd_WithCookies_Force(t *testing.T) { ex, kc := newTestExecuter(t) var out bytes.Buffer diff --git a/internal/pcurl/prompt.go b/internal/pcurl/prompt.go index b6b2889..ff7eb3c 100644 --- a/internal/pcurl/prompt.go +++ b/internal/pcurl/prompt.go @@ -156,6 +156,37 @@ func (p *Prompter) confirmUpdateText(profileName string) (bool, error) { return strings.ToLower(strings.TrimSpace(answer)) != "n", nil } +// ConfirmHostConflict asks whether to create a new profile when the host +// is already covered by a differently-named profile. +func (p *Prompter) ConfirmHostConflict(newName, existingName, host string) (bool, error) { + if p.Interactive { + return p.confirmHostConflictHuh(newName, existingName, host) + } + return p.confirmHostConflictText(newName, existingName, host) +} + +func (p *Prompter) confirmHostConflictHuh(newName, existingName, host string) (bool, error) { + var confirm bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Host %q is already used by profile %q. Create new profile %q anyway?", host, existingName, newName)). + Affirmative("Yes"). + Negative("No"). + Value(&confirm). + Run() + return confirm, err +} + +func (p *Prompter) confirmHostConflictText(newName, existingName, host string) (bool, error) { + fmt.Fprintf(p.Out, "Host %q is already used by profile %q. Create new profile %q anyway? [y/N]: ", host, existingName, newName) + + answer, err := p.readLine() + if err != nil { + return false, err + } + + return strings.ToLower(strings.TrimSpace(answer)) == "y", nil +} + // PickHeaders shows non-secret headers and lets the user toggle selection. func (p *Prompter) PickHeaders(headers []curlparse.Header) error { if p.Interactive {