diff --git a/README.md b/README.md index 13fd0f56..eb684883 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ cd gogcli make ``` +Windows (PowerShell, no Make/bash required): + +```powershell +go build -o .\bin\gog.exe .\cmd\gog +``` + Run: ```bash @@ -107,6 +113,12 @@ Before adding an account, create OAuth2 credentials from Google Cloud Console: gog auth credentials ~/Downloads/client_secret_....json ``` +Windows PowerShell: + +```powershell +.\bin\gog.exe auth credentials "C:\path\to\client_secret_....json" +``` + For multiple OAuth clients/projects: ```bash @@ -120,6 +132,12 @@ gog auth credentials list gog auth add you@gmail.com ``` +Windows PowerShell: + +```powershell +.\bin\gog.exe auth add you@gmail.com +``` + This will open a browser window for OAuth authorization. The refresh token is stored securely in your system keychain. Headless / remote server flows (no browser on the server): @@ -175,6 +193,13 @@ export GOG_ACCOUNT=you@gmail.com gog gmail labels list ``` +Windows PowerShell: + +```powershell +$env:GOG_ACCOUNT = "you@gmail.com" +.\bin\gog.exe gmail labels list +``` + ## Authentication & Secrets ### Accounts and tokens @@ -557,6 +582,7 @@ Options: - **Never commit OAuth client credentials** to version control - Store client credentials outside your project directory - Use different OAuth clients for development and production +- Rotate the OAuth client secret immediately if it is ever shared or exposed - Re-authorize with `--force-consent` if you suspect token compromise - Remove unused accounts with `gog auth remove ` @@ -1614,6 +1640,13 @@ After cloning, install tools: make tools ``` +Windows (PowerShell) note: the `Makefile` uses POSIX shell commands. If you are not running Git Bash/WSL, use direct Go commands instead: + +```powershell +go build -o .\bin\gog.exe .\cmd\gog +go test ./... +``` + Pinned tools (installed into `.tools/`): - Format: `make fmt` (goimports + gofumpt) @@ -1645,6 +1678,18 @@ scripts/live-test.sh --account you@gmail.com --skip groups,keep,calendar-enterpr scripts/live-test.sh --client work --account you@company.com ``` +Windows note: use the PowerShell wrapper (it invokes Git Bash when available): + +```powershell +.\scripts\live-test.ps1 --fast +``` + +If you prefer WSL directly: + +```powershell +wsl bash scripts/live-test.sh --fast +``` + Script toggles: - `--auth all,groups` to re-auth before running @@ -1658,6 +1703,13 @@ Go test wrapper (opt-in): GOG_LIVE=1 go test -tags=integration ./internal/integration -run Live ``` +Windows PowerShell: + +```powershell +$env:GOG_LIVE = "1" +go test -tags=integration ./internal/integration -run Live +``` + Optional env: - `GOG_LIVE_FAST=1` - `GOG_LIVE_SKIP=groups,keep` diff --git a/docs/refactor/README.md b/docs/refactor/README.md index 504ac0b8..0169d370 100644 --- a/docs/refactor/README.md +++ b/docs/refactor/README.md @@ -12,6 +12,11 @@ Shipped (today) - `exports.md`: Drive-backed export command pattern (`docs|slides|sheets`). - `output.md`: shared table + paging helpers. - `templates.md`: googleauth HTML templates via `//go:embed`. +- Windows compatibility pass: + - `config.ExpandPath` handles both `~/...` and `~\\...`. + - integration live tests support Windows via `scripts/live-test.ps1` wrapper. + - README now documents Windows-native build/auth/live-test flows. + - full `go test ./...` and `govulncheck` validation completed on Windows. Backlog / next wins diff --git a/internal/config/paths.go b/internal/config/paths.go index 5b6636ad..2c4d55b6 100644 --- a/internal/config/paths.go +++ b/internal/config/paths.go @@ -262,13 +262,19 @@ func ExpandPath(path string) (string, error) { return home, nil } - if strings.HasPrefix(path, "~/") { + if strings.HasPrefix(path, "~/") || strings.HasPrefix(path, "~\\") { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("expand home dir: %w", err) } - return filepath.Join(home, path[2:]), nil + rel := strings.TrimPrefix(path[2:], string(os.PathSeparator)) + rel = strings.TrimLeft(rel, `/\\`) + if rel == "" { + return home, nil + } + + return filepath.Join(home, rel), nil } return path, nil diff --git a/internal/config/paths_test.go b/internal/config/paths_test.go index 77429fe0..d8e464e9 100644 --- a/internal/config/paths_test.go +++ b/internal/config/paths_test.go @@ -74,6 +74,11 @@ func TestPaths_CreateDirs(t *testing.T) { func TestExpandPath(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + if vol := filepath.VolumeName(home); vol != "" { + t.Setenv("HOMEDRIVE", vol) + t.Setenv("HOMEPATH", strings.TrimPrefix(home, vol)) + } tests := []struct { name string @@ -96,6 +101,11 @@ func TestExpandPath(t *testing.T) { input: "~/Downloads/file.txt", want: filepath.Join(home, "Downloads/file.txt"), }, + { + name: "tilde with windows-style subpath", + input: "~\\Downloads\\file.txt", + want: filepath.Join(home, "Downloads", "file.txt"), + }, { name: "absolute path unchanged", input: "/usr/local/bin", diff --git a/internal/integration/live_test.go b/internal/integration/live_test.go index 9f688116..f677a6f1 100644 --- a/internal/integration/live_test.go +++ b/internal/integration/live_test.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "testing" "time" ) @@ -18,6 +19,16 @@ func TestLiveScript(t *testing.T) { root := findRepoRoot(t) script := filepath.Join(root, "scripts", "live-test.sh") + cmdName := script + cmdArgs := []string{} + if runtime.GOOS == "windows" { + script = filepath.Join(root, "scripts", "live-test.ps1") + if _, err := os.Stat(script); err != nil { + t.Skipf("windows live test wrapper not found at %s", script) + } + cmdName = "powershell" + cmdArgs = []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script} + } args := []string{} if os.Getenv("GOG_LIVE_FAST") != "" { @@ -42,7 +53,7 @@ func TestLiveScript(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) defer cancel() - cmd := exec.CommandContext(ctx, script, args...) + cmd := exec.CommandContext(ctx, cmdName, append(cmdArgs, args...)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = os.Environ() diff --git a/scripts/live-test.ps1 b/scripts/live-test.ps1 new file mode 100644 index 00000000..ba7c9625 --- /dev/null +++ b/scripts/live-test.ps1 @@ -0,0 +1,39 @@ +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Args +) + +$ErrorActionPreference = "Stop" + +$scriptPath = Join-Path $PSScriptRoot "live-test.sh" +if (-not (Test-Path $scriptPath)) { + Write-Error "Could not find live-test.sh at $scriptPath" + exit 1 +} + +$bashCandidates = @() + +$bashCommand = Get-Command bash -ErrorAction SilentlyContinue +if ($bashCommand) { + $bashCandidates += $bashCommand.Source +} + +$gitBash = "C:\Program Files\Git\bin\bash.exe" +if (Test-Path $gitBash) { + $bashCandidates += $gitBash +} + +$bashCandidates = $bashCandidates | Select-Object -Unique + +if ($bashCandidates.Count -eq 0) { + Write-Error "No bash executable found. Install Git for Windows (Git Bash) or run scripts/live-test.sh in WSL." + exit 1 +} + +$bash = $bashCandidates[0] +& $bash $scriptPath @Args +$exitCode = $LASTEXITCODE +if ($null -eq $exitCode) { + $exitCode = 0 +} +exit $exitCode