From d94ed27b66440fa3a254c9bc7ad039df43191019 Mon Sep 17 00:00:00 2001 From: takashiyamaguchi Date: Wed, 25 Mar 2026 04:03:04 +0900 Subject: [PATCH 1/3] feat: add `auth credentials delete` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new subcommand to delete stored OAuth client credentials. Previously there was no way to remove credentials via the CLI — only `set` and `list` were available. The command: - Deletes the credentials file for the specified client (or default) - Cleans up any domain mappings that reference the deleted client - Supports --dry-run, --force, --json flags (consistent with other destructive commands) - Prompts for confirmation before deletion Usage: gog auth credentials delete # delete default client gog --client work auth credentials delete # delete named client Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/cmd/auth_credentials.go | 51 ++++++++++++++++++++++++++++++-- internal/config/credentials.go | 17 +++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/internal/cmd/auth_credentials.go b/internal/cmd/auth_credentials.go index 61d52356..92ca1d8c 100644 --- a/internal/cmd/auth_credentials.go +++ b/internal/cmd/auth_credentials.go @@ -15,8 +15,9 @@ import ( ) type AuthCredentialsCmd struct { - Set AuthCredentialsSetCmd `cmd:"" default:"withargs" help:"Store OAuth client credentials"` - List AuthCredentialsListCmd `cmd:"" name:"list" help:"List stored OAuth client credentials"` + Set AuthCredentialsSetCmd `cmd:"" default:"withargs" help:"Store OAuth client credentials"` + List AuthCredentialsListCmd `cmd:"" name:"list" help:"List stored OAuth client credentials"` + Delete AuthCredentialsDeleteCmd `cmd:"" name:"delete" help:"Delete stored OAuth client credentials"` } type AuthCredentialsSetCmd struct { @@ -160,3 +161,49 @@ func (c *AuthCredentialsListCmd) Run(ctx context.Context, _ *RootFlags) error { } return nil } + +type AuthCredentialsDeleteCmd struct{} + +func (c *AuthCredentialsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error { + u := ui.FromContext(ctx) + client, err := normalizeClientForFlag(authclient.ClientOverrideFromContext(ctx)) + if err != nil { + return err + } + + if err := confirmDestructive(ctx, flags, fmt.Sprintf("delete OAuth credentials for client %q", client)); err != nil { + return err + } + + if err := config.DeleteClientCredentialsFor(client); err != nil { + return err + } + + // Remove domain mappings that reference this client. + cfg, err := config.ReadConfig() + if err != nil { + return err + } + var removed []string + for domain, mapped := range cfg.ClientDomains { + normalized, nerr := config.NormalizeClientNameOrDefault(mapped) + if nerr != nil { + continue + } + if normalized == client { + removed = append(removed, domain) + delete(cfg.ClientDomains, domain) + } + } + if len(removed) > 0 { + if err := config.WriteConfig(cfg); err != nil { + return err + } + } + + return writeResult(ctx, u, + kv("deleted", true), + kv("client", client), + kv("domains_removed", removed), + ) +} diff --git a/internal/config/credentials.go b/internal/config/credentials.go index 529ac8c5..67283f4d 100644 --- a/internal/config/credentials.go +++ b/internal/config/credentials.go @@ -114,6 +114,23 @@ func ReadClientCredentialsFor(client string) (ClientCredentials, error) { return c, nil } +func DeleteClientCredentialsFor(client string) error { + path, err := ClientCredentialsPathFor(client) + if err != nil { + return fmt.Errorf("resolve credentials path: %w", err) + } + + if err := os.Remove(path); err != nil { + if os.IsNotExist(err) { + return &CredentialsMissingError{Path: path, Cause: err} + } + + return fmt.Errorf("delete credentials: %w", err) + } + + return nil +} + func ClientCredentialsExists(client string) (bool, error) { path, err := ClientCredentialsPathFor(client) if err != nil { From 02c4abbe18f24bbcb1d04fa32f50735d89843ee0 Mon Sep 17 00:00:00 2001 From: takashiyamaguchi Date: Wed, 25 Mar 2026 04:08:33 +0900 Subject: [PATCH 2/3] refactor: rename to `remove`, add client arg and `all` support - Rename `delete` to `remove` for consistency with `auth remove` - Accept optional client name as positional argument (e.g. `gog auth credentials remove work`) - Support `all` to remove every stored credential at once - Falls back to --client flag or default when no arg is given Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/cmd/auth_credentials.go | 82 +++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/internal/cmd/auth_credentials.go b/internal/cmd/auth_credentials.go index 92ca1d8c..74c54fc7 100644 --- a/internal/cmd/auth_credentials.go +++ b/internal/cmd/auth_credentials.go @@ -17,7 +17,7 @@ import ( type AuthCredentialsCmd struct { Set AuthCredentialsSetCmd `cmd:"" default:"withargs" help:"Store OAuth client credentials"` List AuthCredentialsListCmd `cmd:"" name:"list" help:"List stored OAuth client credentials"` - Delete AuthCredentialsDeleteCmd `cmd:"" name:"delete" help:"Delete stored OAuth client credentials"` + Remove AuthCredentialsRemoveCmd `cmd:"" name:"remove" help:"Remove stored OAuth client credentials"` } type AuthCredentialsSetCmd struct { @@ -162,16 +162,33 @@ func (c *AuthCredentialsListCmd) Run(ctx context.Context, _ *RootFlags) error { return nil } -type AuthCredentialsDeleteCmd struct{} +type AuthCredentialsRemoveCmd struct { + Client string `arg:"" optional:"" name:"client" help:"Client name to remove (omit for default, or 'all' to remove every client)"` +} -func (c *AuthCredentialsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error { +func (c *AuthCredentialsRemoveCmd) Run(ctx context.Context, flags *RootFlags) error { u := ui.FromContext(ctx) - client, err := normalizeClientForFlag(authclient.ClientOverrideFromContext(ctx)) + + // Determine target client(s): explicit arg > --client flag > default. + target := strings.TrimSpace(c.Client) + if target == "" { + t, err := normalizeClientForFlag(authclient.ClientOverrideFromContext(ctx)) + if err != nil { + return err + } + target = t + } + + if strings.EqualFold(target, "all") { + return c.removeAll(ctx, flags, u) + } + + client, err := config.NormalizeClientNameOrDefault(target) if err != nil { return err } - if err := confirmDestructive(ctx, flags, fmt.Sprintf("delete OAuth credentials for client %q", client)); err != nil { + if err := confirmDestructive(ctx, flags, fmt.Sprintf("remove OAuth credentials for client %q", client)); err != nil { return err } @@ -179,11 +196,51 @@ func (c *AuthCredentialsDeleteCmd) Run(ctx context.Context, flags *RootFlags) er return err } - // Remove domain mappings that reference this client. - cfg, err := config.ReadConfig() + removed := removeDomainMappings(client) + + return writeResult(ctx, u, + kv("removed", true), + kv("client", client), + kv("domains_removed", removed), + ) +} + +func (c *AuthCredentialsRemoveCmd) removeAll(ctx context.Context, flags *RootFlags, u *ui.UI) error { + creds, err := config.ListClientCredentials() if err != nil { return err } + if len(creds) == 0 { + return writeResult(ctx, u, kv("removed", 0)) + } + + names := make([]string, 0, len(creds)) + for _, info := range creds { + names = append(names, info.Client) + } + if err := confirmDestructive(ctx, flags, fmt.Sprintf("remove all OAuth credentials (%s)", strings.Join(names, ", "))); err != nil { + return err + } + + for _, info := range creds { + if err := config.DeleteClientCredentialsFor(info.Client); err != nil { + return err + } + removeDomainMappings(info.Client) + } + + return writeResult(ctx, u, + kv("removed", len(creds)), + kv("clients", names), + ) +} + +// removeDomainMappings deletes config domain entries that point to the given client. +func removeDomainMappings(client string) []string { + cfg, err := config.ReadConfig() + if err != nil { + return nil + } var removed []string for domain, mapped := range cfg.ClientDomains { normalized, nerr := config.NormalizeClientNameOrDefault(mapped) @@ -196,14 +253,7 @@ func (c *AuthCredentialsDeleteCmd) Run(ctx context.Context, flags *RootFlags) er } } if len(removed) > 0 { - if err := config.WriteConfig(cfg); err != nil { - return err - } + _ = config.WriteConfig(cfg) } - - return writeResult(ctx, u, - kv("deleted", true), - kv("client", client), - kv("domains_removed", removed), - ) + return removed } From 502482082f9733f7bde1fd28418f264f3bbd0c8f Mon Sep 17 00:00:00 2001 From: takashiyamaguchi Date: Wed, 25 Mar 2026 04:11:31 +0900 Subject: [PATCH 3/3] feat: also remove associated tokens when removing credentials When removing OAuth client credentials, find and delete all refresh tokens stored under that client. The confirmation prompt now shows which accounts will be affected (e.g. "remove OAuth credentials for client "work" and 2 associated token(s) (alice@co.com, bob@co.com)"). Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/cmd/auth_credentials.go | 56 ++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/internal/cmd/auth_credentials.go b/internal/cmd/auth_credentials.go index 74c54fc7..97bc3ecd 100644 --- a/internal/cmd/auth_credentials.go +++ b/internal/cmd/auth_credentials.go @@ -188,7 +188,13 @@ func (c *AuthCredentialsRemoveCmd) Run(ctx context.Context, flags *RootFlags) er return err } - if err := confirmDestructive(ctx, flags, fmt.Sprintf("remove OAuth credentials for client %q", client)); err != nil { + accounts := findAccountsForClient(client) + + action := fmt.Sprintf("remove OAuth credentials for client %q", client) + if len(accounts) > 0 { + action += fmt.Sprintf(" and %d associated token(s) (%s)", len(accounts), strings.Join(accounts, ", ")) + } + if err := confirmDestructive(ctx, flags, action); err != nil { return err } @@ -196,12 +202,14 @@ func (c *AuthCredentialsRemoveCmd) Run(ctx context.Context, flags *RootFlags) er return err } - removed := removeDomainMappings(client) + tokensRemoved := removeTokensForClient(client, accounts) + domainsRemoved := removeDomainMappings(client) return writeResult(ctx, u, kv("removed", true), kv("client", client), - kv("domains_removed", removed), + kv("tokens_removed", tokensRemoved), + kv("domains_removed", domainsRemoved), ) } @@ -222,19 +230,61 @@ func (c *AuthCredentialsRemoveCmd) removeAll(ctx context.Context, flags *RootFla return err } + var allTokens []string for _, info := range creds { + accounts := findAccountsForClient(info.Client) if err := config.DeleteClientCredentialsFor(info.Client); err != nil { return err } + allTokens = append(allTokens, removeTokensForClient(info.Client, accounts)...) removeDomainMappings(info.Client) } return writeResult(ctx, u, kv("removed", len(creds)), kv("clients", names), + kv("tokens_removed", allTokens), ) } +// findAccountsForClient returns emails that have tokens stored under the given client. +func findAccountsForClient(client string) []string { + store, err := openSecretsStore() + if err != nil { + return nil + } + tokens, err := store.ListTokens() + if err != nil { + return nil + } + var emails []string + for _, tok := range tokens { + tokClient, _ := config.NormalizeClientNameOrDefault(tok.Client) + if tokClient == client { + emails = append(emails, tok.Email) + } + } + return emails +} + +// removeTokensForClient deletes tokens for the given accounts under the specified client. +func removeTokensForClient(client string, emails []string) []string { + if len(emails) == 0 { + return nil + } + store, err := openSecretsStore() + if err != nil { + return nil + } + var removed []string + for _, email := range emails { + if err := store.DeleteToken(client, email); err == nil { + removed = append(removed, email) + } + } + return removed +} + // removeDomainMappings deletes config domain entries that point to the given client. func removeDomainMappings(client string) []string { cfg, err := config.ReadConfig()