gogcli/internal/cmd/auth_keyring.go
salmonumbrella 07ffcb5d84
fix(paths): expand ~ in user-provided file paths (#56)
* fix(paths): expand ~ in user-provided file paths

When users specify paths with ~ (e.g., --out ~/Downloads/file.pdf) and
the path is quoted in the shell command, the tilde is not expanded by
the shell. This caused files to be written to a literal ~/Downloads
directory instead of the user's home directory.

Add config.ExpandPath() function that expands ~ at the beginning of
paths to the user's home directory. Apply this fix to all user-provided
file paths across:

- gmail attachment download (--out)
- drive download/export (--out)
- drive upload (localPath argument)
- auth token export (--out)
- auth credentials/import/keep (input paths)
- gmail thread attachments (--out-dir)
- gmail send/drafts (--attach)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lint): address wrapcheck and wsl issues

* fix(calendar): support ISO 8601 time format and add 'list' alias

- Add parsing for ISO 8601 datetime with numeric timezone without colon
  (e.g., 2026-01-09T16:38:41-0800), which is the format produced by
  macOS `date +%Y-%m-%dT%H:%M:%S%z`
- Add 'list' as an alias for 'events' subcommand for more intuitive CLI
  usage (gog calendar list instead of gog calendar events)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(changelog): note PR #56

* chore(lint): dedupe file string

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-10 02:19:30 +00:00

127 lines
3.1 KiB
Go

package cmd
import (
"context"
"os"
"strings"
"golang.org/x/term"
"github.com/steipete/gogcli/internal/config"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/secrets"
"github.com/steipete/gogcli/internal/ui"
)
type AuthKeyringCmd struct {
Backend string `arg:"" optional:"" name:"backend" help:"Keyring backend: auto|keychain|file"`
Backend2 string `arg:"" optional:"" name:"backend2" help:"(compat) Use: gog auth keyring set <backend>"`
}
func (c *AuthKeyringCmd) Run(ctx context.Context) error {
u := ui.FromContext(ctx)
const keyringPasswordEnv = "GOG_KEYRING_PASSWORD" //nolint:gosec // env var name, not a credential
backend := strings.ToLower(strings.TrimSpace(c.Backend))
backend2 := strings.ToLower(strings.TrimSpace(c.Backend2))
// Backwards compat for earlier suggestion: `gog auth keyring set <backend>`.
if backend == "set" {
backend = backend2
backend2 = ""
}
// No args: show current config.
if backend == "" {
path, _ := config.ConfigPath()
info, err := secrets.ResolveKeyringBackendInfo()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"keyring_backend": info.Value,
"source": info.Source,
"path": path,
})
}
if u == nil {
return nil
}
u.Out().Printf("path\t%s", path)
u.Out().Printf("keyring_backend\t%s", info.Value)
u.Out().Printf("source\t%s", info.Source)
u.Err().Println("Hint: gog auth keyring <auto|keychain|file>")
return nil
}
if backend2 != "" {
return usagef("too many args: %q %q", c.Backend, c.Backend2)
}
if backend == "default" {
backend = "auto"
}
allowed := map[string]struct{}{
"auto": {},
"keychain": {},
strFile: {},
}
if _, ok := allowed[backend]; !ok {
return usagef("invalid backend: %q (expected auto, keychain, or file)", c.Backend)
}
cfg, err := config.ReadConfig()
if err != nil {
return err
}
cfg.KeyringBackend = backend
if err := config.WriteConfig(cfg); err != nil {
return err
}
path, _ := config.ConfigPath()
// Env var wins; warn so it doesn't look "broken".
if v := strings.TrimSpace(os.Getenv("GOG_KEYRING_BACKEND")); v != "" &&
u != nil &&
!outfmt.IsJSON(ctx) &&
!outfmt.IsPlain(ctx) {
u.Err().Printf("NOTE: GOG_KEYRING_BACKEND=%s overrides config.json", v)
}
if backend == strFile &&
u != nil &&
!outfmt.IsJSON(ctx) &&
!outfmt.IsPlain(ctx) {
if v := strings.TrimSpace(os.Getenv(keyringPasswordEnv)); v != "" {
u.Err().Println("GOG_KEYRING_PASSWORD found in environment.")
} else if !term.IsTerminal(int(os.Stdin.Fd())) {
u.Err().Printf("NOTE: file keyring backend in non-interactive context requires %s", keyringPasswordEnv)
} else {
u.Err().Printf("Hint: set %s for non-interactive use (CI/ssh)", keyringPasswordEnv)
}
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"written": true,
"path": path,
"keyring_backend": backend,
})
}
if u == nil {
return nil
}
u.Out().Printf("written\ttrue")
u.Out().Printf("path\t%s", path)
u.Out().Printf("keyring_backend\t%s", backend)
return nil
}