Remove standalone fetcher, add setup tool with install/uninstall workflow
All checks were successful
Release / build (push) Successful in 1m45s
All checks were successful
Release / build (push) Successful in 1m45s
Drop claude-fetcher binary and cron job — the widget's built-in BackgroundFetcher is the sole fetcher now. Add cmd/setup with cross-platform install and uninstall (--uninstall): kills widget, removes binaries + autostart, cleans Claude Code statusline setting, optionally removes config dir. Also includes: browser-based login (chromedp), ICO wrapper for Windows tray icon, and reduced icon size (64px). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
102
internal/browser/login.go
Normal file
102
internal/browser/login.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/chromedp"
|
||||
|
||||
"git.davoryn.de/calic/claude-statusline/internal/config"
|
||||
)
|
||||
|
||||
// LoginAndGetSessionKey opens a browser window for the user to log in to
|
||||
// claude.ai and extracts the httpOnly sessionKey cookie via DevTools protocol.
|
||||
// The browser uses a persistent profile so the user only needs to log in once.
|
||||
// Returns the session key or an error (e.g. timeout after 2 minutes).
|
||||
func LoginAndGetSessionKey() (string, error) {
|
||||
execPath := findBrowserExec()
|
||||
|
||||
profileDir := filepath.Join(config.ConfigDir(), "browser-profile")
|
||||
if err := os.MkdirAll(profileDir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("create browser profile dir: %w", err)
|
||||
}
|
||||
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", false),
|
||||
chromedp.UserDataDir(profileDir),
|
||||
)
|
||||
if execPath != "" {
|
||||
opts = append(opts, chromedp.ExecPath(execPath))
|
||||
}
|
||||
|
||||
allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer allocCancel()
|
||||
|
||||
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||
defer cancel()
|
||||
|
||||
// Navigate to login page
|
||||
if err := chromedp.Run(ctx, chromedp.Navigate("https://claude.ai/login")); err != nil {
|
||||
return "", fmt.Errorf("navigate to login: %w", err)
|
||||
}
|
||||
|
||||
// Poll for the sessionKey cookie (httpOnly, so only accessible via DevTools)
|
||||
deadline := time.Now().Add(2 * time.Minute)
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if time.Now().After(deadline) {
|
||||
return "", fmt.Errorf("login timed out after 2 minutes")
|
||||
}
|
||||
|
||||
var cookies []*network.Cookie
|
||||
if err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var err error
|
||||
cookies, err = network.GetCookies().Do(ctx)
|
||||
return err
|
||||
})); err != nil {
|
||||
// Browser may have been closed by user
|
||||
return "", fmt.Errorf("get cookies: %w", err)
|
||||
}
|
||||
|
||||
for _, c := range cookies {
|
||||
if c.Name == "sessionKey" && (c.Domain == ".claude.ai" || c.Domain == "claude.ai") {
|
||||
key := c.Value
|
||||
if err := config.SetSessionKey(key); err != nil {
|
||||
return "", fmt.Errorf("save session key: %w", err)
|
||||
}
|
||||
// Use chromedp.Cancel to close gracefully (flushes cookies to profile)
|
||||
cancel()
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findBrowserExec returns the path to a Chromium-based browser, or "" to let
|
||||
// chromedp use its default detection (Chrome/Chromium on PATH).
|
||||
func findBrowserExec() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Prefer Edge (pre-installed on Windows 10+)
|
||||
candidates := []string{
|
||||
filepath.Join(os.Getenv("ProgramFiles(x86)"), "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
}
|
||||
for _, p := range candidates {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
}
|
||||
// On Linux/macOS, chromedp auto-detects Chrome/Chromium
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user