From 59191af31824b1b36457556f3c979d520b1dfb4c Mon Sep 17 00:00:00 2001 From: kroexov Date: Thu, 14 May 2026 15:37:22 +0300 Subject: [PATCH 1/2] Distinguish host-conflict from name-collision in `pcurl add` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before, `pcurl add --name X ` for a host already covered by a differently-named profile prompted "Profile \"X\" already exists. Update?" — but X did not exist. The misleading message came from collapsing two cases in `confirmExisting`: 1. A profile named X already exists. 2. No profile named X, but another profile already covers the host. Case 2 still proceeds to create a new profile under X (resulting in two profiles for the same host), which is intentional and useful for multi-account setups — only the prompt was wrong. Split the logic so each case has its own prompt: * Name collision → existing `ConfirmUpdate` (default Yes), updates X. * Host conflict → new `ConfirmHostConflict` (default No), creates X as a separate profile and leaves the original untouched. Adds `TestAdd_HostConflict_Confirm` / `TestAdd_HostConflict_Decline`. --- internal/pcurl/add.go | 29 +++++++++++++++++++-------- internal/pcurl/add_test.go | 40 ++++++++++++++++++++++++++++++++++++++ internal/pcurl/prompt.go | 31 +++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/internal/pcurl/add.go b/internal/pcurl/add.go index e63c9e7..bb4d1b2 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,26 @@ func confirmExisting(profileName, host string, c *Config, prompt *Prompter, forc if !ok { return existing, true, nil } + return existing, false, nil + } + + otherName := c.FindProfileByHost(host) + if otherName == "" || otherName == profileName { + return nil, false, nil } - return existing, false, nil + 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..feb6864 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: "fgagsarasg"}, + &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("fgagsarasg"), "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: "fgagsarasg"}, + &out, strings.NewReader("n\n"), false, + ) + require.NoError(t, err) + + c, _ = ex.CM.Load() + assert.Nil(t, c.FindProfile("fgagsarasg"), "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 { From 7f8edcad19de5ed7fd7b34c0529d8a8943129c99 Mon Sep 17 00:00:00 2001 From: Sergey Bykov Date: Mon, 15 Jun 2026 15:44:32 +0300 Subject: [PATCH 2/2] Clarify confirmExisting and rename test profile - Rename the placeholder profile name in the host-conflict tests to "alt-account" for readability. - Document that --force creates the new profile without touching the existing same-host profile, and note the unreachable defensive guard in confirmExisting. --- internal/pcurl/add.go | 5 +++++ internal/pcurl/add_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/pcurl/add.go b/internal/pcurl/add.go index bb4d1b2..f864756 100644 --- a/internal/pcurl/add.go +++ b/internal/pcurl/add.go @@ -31,11 +31,16 @@ func confirmExisting(profileName, host string, c *Config, prompt *Prompter, forc 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 } + // --force creates the new profile without prompting; the existing + // same-host profile is left untouched. if force { return nil, false, nil } diff --git a/internal/pcurl/add_test.go b/internal/pcurl/add_test.go index feb6864..0b6e279 100644 --- a/internal/pcurl/add_test.go +++ b/internal/pcurl/add_test.go @@ -148,14 +148,14 @@ func TestAdd_HostConflict_Confirm(t *testing.T) { err := ex.Add( []string{"https://example.com", "-H", "Authorization: Bearer new"}, - AddOptions{Name: "fgagsarasg"}, + 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("fgagsarasg"), "new profile should be created") + assert.NotNil(t, c.FindProfile("alt-account"), "new profile should be created") assert.Contains(t, out.String(), "already used by profile") } @@ -169,13 +169,13 @@ func TestAdd_HostConflict_Decline(t *testing.T) { err := ex.Add( []string{"https://example.com", "-H", "Authorization: Bearer new"}, - AddOptions{Name: "fgagsarasg"}, + AddOptions{Name: "alt-account"}, &out, strings.NewReader("n\n"), false, ) require.NoError(t, err) c, _ = ex.CM.Load() - assert.Nil(t, c.FindProfile("fgagsarasg"), "new profile should not be created") + assert.Nil(t, c.FindProfile("alt-account"), "new profile should not be created") } func TestAdd_WithCookies_Force(t *testing.T) {