← back to Cli Printing Press
fix(cli): make cookie-auth login --chrome work on Windows (#1404)
fdca9f7aa3bd697e3153e4852d7905752cbf5d47 · 2026-05-14 12:38:32 -0700 · Trevin Chow
* fix(cli): make cookie-auth login --chrome work on Windows
The generated `auth login --chrome` flow had two stacked Windows blockers:
1. `detectCookieTool` probed pycookiecheat with a hardcoded `python3`, which
does not exist on Windows installs that ship `python.exe` or the `py`
launcher. The probe failed, the user was told "no cookie tool found",
and the install hint advised `pip install pycookiecheat` even though the
user already had it installed.
2. pycookiecheat itself raises `OSError("This script only works on MacOS
or Linux.")` on Windows, so even with a working Python binary the
extraction crashed once it reached `chrome_cookies()`.
Resolve both:
- New `resolvePythonBinary` helper tries `python3`, then `python`, then
`py -3`, and plumbs the resolved binary into both the import probe and
the extraction call.
- `detectCookieTool` now short-circuits the pycookiecheat probe on
`runtime.GOOS == "windows"` and falls through to `cookies` /
`cookie-scoop`. When nothing is available on Windows it surfaces a
Windows-specific error pointing at `auth login --browser` (live Chrome
via CDP) and cookie-scoop-cli.
- The "Install one of" user-facing hint is now platform-aware: Windows
users get the working workaround instead of advice to install a tool
that does not support their platform.
Regression test asserts the generated auth.go contains the resolver, the
Windows skip, and the Windows-specific fallback prose.
Closes #1101
* fix(cli): thread resolved Python binary through cookieTool struct
Greptile review on PR #1404:
- P1 (test): `assert.Contains(t, content, "\"python\"")` was vacuously
satisfied as a substring of `"python3"`. Anchor the assertions to the
struct-literal forms `{"python3", nil}`, `{"python", nil}`, and
`{"py", []string{"-3"}}` so an accidental removal of any candidate
fails the test.
- P2 (runtime): `detectCookieTool` and `extractViaPycookiecheat` both
called `resolvePythonBinary` independently. If PATH mutated between
detection and extraction, the probe could succeed against python3 while
extraction silently fell through to a different interpreter. Thread the
resolved `(bin, args)` pair on a new `cookieTool` struct so the same
Python is used end to end. `cookieToolSupportsProfiles` now takes
`tool.name`; the other branches (cookies, cookie-scoop) carry empty
pyBin/pyArgs.
Files touched
M internal/generator/generator_test.goM internal/generator/templates/auth_browser.go.tmpl
Diff
commit fdca9f7aa3bd697e3153e4852d7905752cbf5d47
Author: Trevin Chow <trevin@trevinchow.com>
Date: Thu May 14 12:38:32 2026 -0700
fix(cli): make cookie-auth login --chrome work on Windows (#1404)
* fix(cli): make cookie-auth login --chrome work on Windows
The generated `auth login --chrome` flow had two stacked Windows blockers:
1. `detectCookieTool` probed pycookiecheat with a hardcoded `python3`, which
does not exist on Windows installs that ship `python.exe` or the `py`
launcher. The probe failed, the user was told "no cookie tool found",
and the install hint advised `pip install pycookiecheat` even though the
user already had it installed.
2. pycookiecheat itself raises `OSError("This script only works on MacOS
or Linux.")` on Windows, so even with a working Python binary the
extraction crashed once it reached `chrome_cookies()`.
Resolve both:
- New `resolvePythonBinary` helper tries `python3`, then `python`, then
`py -3`, and plumbs the resolved binary into both the import probe and
the extraction call.
- `detectCookieTool` now short-circuits the pycookiecheat probe on
`runtime.GOOS == "windows"` and falls through to `cookies` /
`cookie-scoop`. When nothing is available on Windows it surfaces a
Windows-specific error pointing at `auth login --browser` (live Chrome
via CDP) and cookie-scoop-cli.
- The "Install one of" user-facing hint is now platform-aware: Windows
users get the working workaround instead of advice to install a tool
that does not support their platform.
Regression test asserts the generated auth.go contains the resolver, the
Windows skip, and the Windows-specific fallback prose.
Closes #1101
* fix(cli): thread resolved Python binary through cookieTool struct
Greptile review on PR #1404:
- P1 (test): `assert.Contains(t, content, "\"python\"")` was vacuously
satisfied as a substring of `"python3"`. Anchor the assertions to the
struct-literal forms `{"python3", nil}`, `{"python", nil}`, and
`{"py", []string{"-3"}}` so an accidental removal of any candidate
fails the test.
- P2 (runtime): `detectCookieTool` and `extractViaPycookiecheat` both
called `resolvePythonBinary` independently. If PATH mutated between
detection and extraction, the probe could succeed against python3 while
extraction silently fell through to a different interpreter. Thread the
resolved `(bin, args)` pair on a new `cookieTool` struct so the same
Python is used end to end. `cookieToolSupportsProfiles` now takes
`tool.name`; the other branches (cookies, cookie-scoop) carry empty
pyBin/pyArgs.
---
internal/generator/generator_test.go | 65 ++++++++++++++++
internal/generator/templates/auth_browser.go.tmpl | 94 +++++++++++++++++------
2 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 11b7325c..66072459 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -6326,6 +6326,71 @@ func TestGenerate_CookieAuthUsesBrowserTemplate(t *testing.T) {
runGoCommand(t, outputDir, "build", "./...")
}
+// TestGenerate_CookieAuthWindowsCompatibility asserts the cookie-auth template
+// emits a portable Python resolver and a Windows-aware fallback for the
+// `auth login --chrome` flow. Regression for issue #1101: hardcoded `python3`
+// and a non-Windows-friendly install hint made every cookie-auth CLI unusable
+// on Windows.
+func TestGenerate_CookieAuthWindowsCompatibility(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := &spec.APISpec{
+ Name: "winapp",
+ Version: "0.1.0",
+ BaseURL: "https://app.example.com",
+ Auth: spec.AuthConfig{
+ Type: "cookie",
+ Header: "Cookie",
+ In: "cookie",
+ CookieDomain: ".example.com",
+ EnvVars: []string{"WINAPP_COOKIES"},
+ },
+ Config: spec.ConfigSpec{
+ Format: "toml",
+ Path: "~/.config/winapp-pp-cli/config.toml",
+ },
+ Resources: map[string]spec.Resource{
+ "items": {
+ Description: "Manage items",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {Method: "GET", Path: "/api/items", Description: "List items"},
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "winapp-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ auth, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
+ require.NoError(t, err)
+ content := string(auth)
+
+ // Python resolver tries all three common binary names rather than only `python3`.
+ // Match the struct-literal needles so `"python"` isn't vacuously satisfied as
+ // a substring of `"python3"`.
+ assert.Contains(t, content, "resolvePythonBinary")
+ assert.Contains(t, content, `{"python3", nil}`)
+ assert.Contains(t, content, `{"python", nil}`)
+ assert.Contains(t, content, `{"py", []string{"-3"}}`)
+
+ // pycookiecheat is skipped on Windows because it raises OSError there.
+ assert.Contains(t, content, `runtime.GOOS != "windows"`)
+ assert.Contains(t, content, "pycookiecheat does not support Windows")
+
+ // The resolved (bin, args) pair is carried on cookieTool so detection and
+ // extraction cannot disagree about which interpreter to invoke.
+ assert.Contains(t, content, "type cookieTool struct")
+ assert.Contains(t, content, "pyBin")
+ assert.Contains(t, content, "pyArgs")
+ assert.NotContains(t, content, `exec.Command("python3", "-c", script)`)
+ assert.Contains(t, content, `exec.Command(tool.pyBin,`)
+
+ // Windows users get a workable next step instead of the Unix install hint.
+ assert.Contains(t, content, "auth login --browser")
+ assert.Contains(t, content, "cookie-scoop-cli")
+}
+
func TestGenerate_UserAgentOverrideGatedByBrowserTransport(t *testing.T) {
t.Parallel()
diff --git a/internal/generator/templates/auth_browser.go.tmpl b/internal/generator/templates/auth_browser.go.tmpl
index de2dda92..4fea558d 100644
--- a/internal/generator/templates/auth_browser.go.tmpl
+++ b/internal/generator/templates/auth_browser.go.tmpl
@@ -118,20 +118,27 @@ profile by name when the installed backend supports it.`,
if err != nil {
fmt.Fprintln(w, red("No cookie extraction tool found."))
fmt.Fprintln(w, "")
- fmt.Fprintln(w, "Install one of:")
- fmt.Fprintln(w, " pip install pycookiecheat # Python (recommended)")
- fmt.Fprintln(w, " brew install barnardb/cookies/cookies # Homebrew")
- fmt.Fprintln(w, " cargo install cookie-scoop-cli # Rust")
+ if runtime.GOOS == "windows" {
+ fmt.Fprintln(w, "pycookiecheat does not support Windows. Read cookies from a live Chrome session instead:")
+ fmt.Fprintf(w, "\n {{.Name}}-pp-cli auth login --browser\n\n")
+ fmt.Fprintln(w, "Or install a cross-platform reader:")
+ fmt.Fprintln(w, " cargo install cookie-scoop-cli # Rust")
+ } else {
+ fmt.Fprintln(w, "Install one of:")
+ fmt.Fprintln(w, " pip install pycookiecheat # Python (recommended)")
+ fmt.Fprintln(w, " brew install barnardb/cookies/cookies # Homebrew")
+ fmt.Fprintln(w, " cargo install cookie-scoop-cli # Rust")
+ }
return authErr(fmt.Errorf("no cookie tool found"))
}
- if profileFlag != "" && !cookieToolSupportsProfiles(tool) {
- return authErr(fmt.Errorf("%s does not support --profile; install pycookiecheat or cookie-scoop-cli", tool))
+ if profileFlag != "" && !cookieToolSupportsProfiles(tool.name) {
+ return authErr(fmt.Errorf("%s does not support --profile; install pycookiecheat or cookie-scoop-cli", tool.name))
}
// Step 2: Resolve which Chrome profile to use when the backend can honor it
profileDir := ""
- if cookieToolSupportsProfiles(tool) {
+ if cookieToolSupportsProfiles(tool.name) {
profileDir, err = resolveChromeProfile(w, cmd.InOrStdin(), domain, profileFlag)
if err != nil {
loginURL := "https://" + strings.TrimPrefix(domain, ".")
@@ -1135,41 +1142,78 @@ func resolveProfileByName(name string) (string, error) {
// --- Cookie extraction tools ---
-// detectCookieTool checks for available cookie extraction tools in preference order.
-func detectCookieTool() (string, error) {
- tools := []struct {
- name string
- check []string
+// resolvePythonBinary finds a usable Python 3 interpreter, returning the
+// executable name and any leading args (e.g. `py -3` on Windows). pycookiecheat
+// is the only tool the CLI shells into Python for, and Windows ships
+// `python.exe` or the `py` launcher rather than `python3`.
+func resolvePythonBinary() (string, []string, bool) {
+ candidates := []struct {
+ bin string
+ args []string
}{
- {"pycookiecheat", []string{"python3", "-c", "import pycookiecheat"}},
- {"cookies", []string{"cookies", "--help"}},
- {"cookie-scoop", []string{"cookie-scoop", "--help"}},
+ {"python3", nil},
+ {"python", nil},
+ {"py", []string{"-3"}},
}
+ for _, c := range candidates {
+ if _, err := exec.LookPath(c.bin); err == nil {
+ return c.bin, c.args, true
+ }
+ }
+ return "", nil, false
+}
+
+// cookieTool bundles the detected tool name with any auxiliary invocation
+// data the extraction call needs. For pycookiecheat we carry the Python
+// binary and its leading args so detection and extraction cannot disagree
+// if PATH mutates between calls.
+type cookieTool struct {
+ name string
+ pyBin string
+ pyArgs []string
+}
- for _, tool := range tools {
- if err := exec.Command(tool.check[0], tool.check[1:]...).Run(); err == nil {
- return tool.name, nil
+// detectCookieTool checks for available cookie extraction tools in preference order.
+// pycookiecheat is skipped on Windows because upstream raises
+// `OSError("This script only works on MacOS or Linux.")` regardless of whether
+// the import probe succeeds.
+func detectCookieTool() (cookieTool, error) {
+ if runtime.GOOS != "windows" {
+ if bin, args, ok := resolvePythonBinary(); ok {
+ probeArgs := append(append([]string{}, args...), "-c", "import pycookiecheat")
+ if err := exec.Command(bin, probeArgs...).Run(); err == nil {
+ return cookieTool{name: "pycookiecheat", pyBin: bin, pyArgs: args}, nil
+ }
}
}
- return "", fmt.Errorf("no cookie extraction tool found; install one: pip install pycookiecheat")
+ if err := exec.Command("cookies", "--help").Run(); err == nil {
+ return cookieTool{name: "cookies"}, nil
+ }
+ if err := exec.Command("cookie-scoop", "--help").Run(); err == nil {
+ return cookieTool{name: "cookie-scoop"}, nil
+ }
+ if runtime.GOOS == "windows" {
+ return cookieTool{}, fmt.Errorf("no cookie extraction tool found; pycookiecheat does not support Windows. Use `auth login --browser` (live Chrome via CDP) or install cookie-scoop-cli")
+ }
+ return cookieTool{}, fmt.Errorf("no cookie extraction tool found; install one: pip install pycookiecheat")
}
// extractCookies shells out to the detected tool and returns a Cookie header value string.
// profileDir selects which Chrome profile to read from (e.g. "Default", "Profile 1").
-func extractCookies(tool, domain, profileDir string) (string, error) {
- switch tool {
+func extractCookies(tool cookieTool, domain, profileDir string) (string, error) {
+ switch tool.name {
case "pycookiecheat":
- return extractViaPycookiecheat(domain, profileDir)
+ return extractViaPycookiecheat(tool, domain, profileDir)
case "cookies":
return extractViaCookiesCLI(domain)
case "cookie-scoop":
return extractViaCookieScoop(domain, profileDir)
default:
- return "", fmt.Errorf("unknown cookie tool: %s", tool)
+ return "", fmt.Errorf("unknown cookie tool: %s", tool.name)
}
}
-func extractViaPycookiecheat(domain, profileDir string) (string, error) {
+func extractViaPycookiecheat(tool cookieTool, domain, profileDir string) (string, error) {
cleanDomain := strings.TrimPrefix(domain, ".")
cookiePath := ""
if profileDir != "" && profileDir != "Default" {
@@ -1195,7 +1239,7 @@ func extractViaPycookiecheat(domain, profileDir string) (string, error) {
}
var out bytes.Buffer
- cmd := exec.Command("python3", "-c", script)
+ cmd := exec.Command(tool.pyBin, append(append([]string{}, tool.pyArgs...), "-c", script)...)
cmd.Stdout = &out
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
← 89f38c4c fix(cli): isolate typed-table upsert failures with per-item
·
back to Cli Printing Press
·
fix(cli): tighten phone-us PII regex to NANP-valid first dig 15964974 →