feat(cli)!: switch to --json/--plain
This commit is contained in:
parent
65b5ccb303
commit
18f2a9fd4f
7
Makefile
7
Makefile
@ -9,6 +9,11 @@ BIN_DIR := $(CURDIR)/bin
|
||||
BIN := $(BIN_DIR)/gog
|
||||
CMD := ./cmd/gog
|
||||
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
COMMIT := $(shell git rev-parse --short=12 HEAD 2>/dev/null || echo "")
|
||||
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
LDFLAGS := -X github.com/steipete/gogcli/internal/cmd.version=$(VERSION) -X github.com/steipete/gogcli/internal/cmd.commit=$(COMMIT) -X github.com/steipete/gogcli/internal/cmd.date=$(DATE)
|
||||
|
||||
TOOLS_DIR := $(CURDIR)/.tools
|
||||
GOFUMPT := $(TOOLS_DIR)/gofumpt
|
||||
GOIMPORTS := $(TOOLS_DIR)/goimports
|
||||
@ -16,7 +21,7 @@ GOLANGCI_LINT := $(TOOLS_DIR)/golangci-lint
|
||||
|
||||
build:
|
||||
@mkdir -p $(BIN_DIR)
|
||||
@go build -o $(BIN) $(CMD)
|
||||
@go build -ldflags "$(LDFLAGS)" -o $(BIN) $(CMD)
|
||||
|
||||
tools:
|
||||
@mkdir -p $(TOOLS_DIR)
|
||||
|
||||
@ -8,6 +8,6 @@ import (
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(os.Args[1:]); err != nil {
|
||||
os.Exit(1)
|
||||
os.Exit(cmd.ExitCode(err))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@ -13,5 +12,5 @@ func requireAccount(flags *rootFlags) (string, error) {
|
||||
if v := strings.TrimSpace(os.Getenv("GOG_ACCOUNT")); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
return "", errors.New("missing --account (or set GOG_ACCOUNT)")
|
||||
return "", usage("missing --account (or set GOG_ACCOUNT)")
|
||||
}
|
||||
|
||||
@ -2,7 +2,8 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -22,7 +23,7 @@ var (
|
||||
authorizeGoogle = googleauth.Authorize
|
||||
)
|
||||
|
||||
func newAuthCmd() *cobra.Command {
|
||||
func newAuthCmd(flags *rootFlags) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Authentication and accounts",
|
||||
@ -31,20 +32,26 @@ func newAuthCmd() *cobra.Command {
|
||||
cmd.AddCommand(newAuthCredentialsCmd())
|
||||
cmd.AddCommand(newAuthAddCmd())
|
||||
cmd.AddCommand(newAuthListCmd())
|
||||
cmd.AddCommand(newAuthRemoveCmd())
|
||||
cmd.AddCommand(newAuthTokensCmd())
|
||||
cmd.AddCommand(newAuthRemoveCmd(flags))
|
||||
cmd.AddCommand(newAuthTokensCmd(flags))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newAuthCredentialsCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "credentials <credentials.json>",
|
||||
Use: "credentials <credentials.json|->",
|
||||
Short: "Store OAuth client credentials",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
u := ui.FromContext(cmd.Context())
|
||||
inPath := args[0]
|
||||
b, err := os.ReadFile(inPath)
|
||||
var b []byte
|
||||
var err error
|
||||
if inPath == "-" {
|
||||
b, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
b, err = os.ReadFile(inPath)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -71,7 +78,7 @@ func newAuthCredentialsCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func newAuthTokensCmd() *cobra.Command {
|
||||
func newAuthTokensCmd(flags *rootFlags) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "tokens",
|
||||
Short: "Manage stored refresh tokens",
|
||||
@ -117,11 +124,19 @@ func newAuthTokensCmd() *cobra.Command {
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
u := ui.FromContext(cmd.Context())
|
||||
email := strings.TrimSpace(args[0])
|
||||
if email == "" {
|
||||
return usage("empty email")
|
||||
}
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("delete stored token for %s", email)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store, err := openSecretsStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
email := args[0]
|
||||
if err := store.DeleteToken(email); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -142,7 +157,7 @@ func newAuthTokensCmd() *cobra.Command {
|
||||
|
||||
func newAuthTokensExportCmd() *cobra.Command {
|
||||
var outPath string
|
||||
var force bool
|
||||
var overwrite bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "export <email>",
|
||||
@ -152,11 +167,11 @@ func newAuthTokensExportCmd() *cobra.Command {
|
||||
u := ui.FromContext(cmd.Context())
|
||||
email := strings.TrimSpace(args[0])
|
||||
if email == "" {
|
||||
return errors.New("empty email")
|
||||
return usage("empty email")
|
||||
}
|
||||
outPath = strings.TrimSpace(outPath)
|
||||
if outPath == "" {
|
||||
return errors.New("empty outPath")
|
||||
return usage("empty outPath")
|
||||
}
|
||||
|
||||
store, err := openSecretsStore()
|
||||
@ -173,7 +188,7 @@ func newAuthTokensExportCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
|
||||
if !force {
|
||||
if !overwrite {
|
||||
flags = os.O_WRONLY | os.O_CREATE | os.O_EXCL
|
||||
}
|
||||
f, openErr := os.OpenFile(outPath, flags, 0o600)
|
||||
@ -223,19 +238,25 @@ func newAuthTokensExportCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&outPath, "out", "", "Output file path (required)")
|
||||
cmd.Flags().BoolVar(&force, "force", false, "Overwrite output file if it exists")
|
||||
cmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite output file if it exists")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newAuthTokensImportCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "import <inPath>",
|
||||
Use: "import <inPath|->",
|
||||
Short: "Import a refresh token file into keyring (contains secrets)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
u := ui.FromContext(cmd.Context())
|
||||
inPath := args[0]
|
||||
b, err := os.ReadFile(inPath)
|
||||
var b []byte
|
||||
var err error
|
||||
if inPath == "-" {
|
||||
b, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
b, err = os.ReadFile(inPath)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -253,10 +274,10 @@ func newAuthTokensImportCmd() *cobra.Command {
|
||||
}
|
||||
ex.Email = strings.TrimSpace(ex.Email)
|
||||
if ex.Email == "" {
|
||||
return errors.New("missing email in token file")
|
||||
return usage("missing email in token file")
|
||||
}
|
||||
if strings.TrimSpace(ex.RefreshToken) == "" {
|
||||
return errors.New("missing refresh_token in token file")
|
||||
return usage("missing refresh_token in token file")
|
||||
}
|
||||
var createdAt time.Time
|
||||
if strings.TrimSpace(ex.CreatedAt) != "" {
|
||||
@ -438,14 +459,21 @@ func newAuthListCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func newAuthRemoveCmd() *cobra.Command {
|
||||
func newAuthRemoveCmd(flags *rootFlags) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "remove <email>",
|
||||
Short: "Remove a stored refresh token",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
u := ui.FromContext(cmd.Context())
|
||||
email := args[0]
|
||||
email := strings.TrimSpace(args[0])
|
||||
if email == "" {
|
||||
return usage("empty email")
|
||||
}
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("remove stored token for %s", email)); err != nil {
|
||||
return err
|
||||
}
|
||||
store, err := openSecretsStore()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -100,7 +100,7 @@ func TestAuthTokens_ExportImportRoundtrip_JSON(t *testing.T) {
|
||||
|
||||
stdout := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "tokens", "export", "a@b.com", "--out", outPath}); err != nil {
|
||||
if err := Execute([]string{"--json", "auth", "tokens", "export", "a@b.com", "--out", outPath}); err != nil {
|
||||
t.Fatalf("Execute export: %v", err)
|
||||
}
|
||||
})
|
||||
@ -132,7 +132,7 @@ func TestAuthTokens_ExportImportRoundtrip_JSON(t *testing.T) {
|
||||
|
||||
importOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "tokens", "import", outPath}); err != nil {
|
||||
if err := Execute([]string{"--json", "auth", "tokens", "import", outPath}); err != nil {
|
||||
t.Fatalf("Execute import: %v", err)
|
||||
}
|
||||
})
|
||||
@ -153,7 +153,7 @@ func TestAuthTokens_ExportImportRoundtrip_JSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthTokensExport_RequiresOut(t *testing.T) {
|
||||
err := Execute([]string{"--output", "json", "auth", "tokens", "export", "a@b.com"})
|
||||
err := Execute([]string{"--json", "auth", "tokens", "export", "a@b.com"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
@ -174,7 +174,7 @@ func TestAuthListRemoveTokensListDelete_JSON(t *testing.T) {
|
||||
|
||||
listOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "list"}); err != nil {
|
||||
if err := Execute([]string{"--json", "auth", "list"}); err != nil {
|
||||
t.Fatalf("Execute list: %v", err)
|
||||
}
|
||||
})
|
||||
@ -194,7 +194,7 @@ func TestAuthListRemoveTokensListDelete_JSON(t *testing.T) {
|
||||
// Tokens list (keys).
|
||||
keysOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "tokens", "list"}); err != nil {
|
||||
if err := Execute([]string{"--json", "auth", "tokens", "list"}); err != nil {
|
||||
t.Fatalf("Execute tokens list: %v", err)
|
||||
}
|
||||
})
|
||||
@ -212,7 +212,7 @@ func TestAuthListRemoveTokensListDelete_JSON(t *testing.T) {
|
||||
// Remove (auth remove)
|
||||
rmOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "remove", "b@b.com"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "auth", "remove", "b@b.com"}); err != nil {
|
||||
t.Fatalf("Execute remove: %v", err)
|
||||
}
|
||||
})
|
||||
@ -231,7 +231,7 @@ func TestAuthListRemoveTokensListDelete_JSON(t *testing.T) {
|
||||
// Tokens delete (auth tokens delete)
|
||||
delOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "tokens", "delete", "a@b.com"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "auth", "tokens", "delete", "a@b.com"}); err != nil {
|
||||
t.Fatalf("Execute tokens delete: %v", err)
|
||||
}
|
||||
})
|
||||
@ -250,7 +250,7 @@ func TestAuthListRemoveTokensListDelete_JSON(t *testing.T) {
|
||||
// Now empty.
|
||||
emptyKeysOut := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "tokens", "list"}); err != nil {
|
||||
if err := Execute([]string{"--json", "auth", "tokens", "list"}); err != nil {
|
||||
t.Fatalf("Execute tokens list: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -69,12 +69,19 @@ func newCalendarCalendarsCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tNAME\tROLE")
|
||||
for _, c := range resp.Items {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", c.Id, c.Summary, c.AccessRole)
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tNAME\tROLE")
|
||||
for _, c := range resp.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", c.Id, c.Summary, c.AccessRole)
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
}
|
||||
@ -123,8 +130,13 @@ func newCalendarAclCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "SCOPE_TYPE\tSCOPE_VALUE\tROLE")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "SCOPE_TYPE\tSCOPE_VALUE\tROLE")
|
||||
for _, rule := range resp.Items {
|
||||
scopeType := ""
|
||||
scopeValue := ""
|
||||
@ -132,9 +144,11 @@ func newCalendarAclCmd(flags *rootFlags) *cobra.Command {
|
||||
scopeType = rule.Scope.Type
|
||||
scopeValue = rule.Scope.Value
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", scopeType, scopeValue, rule.Role)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", scopeType, scopeValue, rule.Role)
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
}
|
||||
@ -206,12 +220,19 @@ func newCalendarEventsCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tSTART\tEND\tSUMMARY")
|
||||
for _, e := range resp.Items {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", e.Id, eventStart(e), eventEnd(e), e.Summary)
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tSTART\tEND\tSUMMARY")
|
||||
for _, e := range resp.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", e.Id, eventStart(e), eventEnd(e), e.Summary)
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -242,6 +263,10 @@ func newCalendarEventCmd(flags *rootFlags) *cobra.Command {
|
||||
calendarID := args[0]
|
||||
eventID := args[1]
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("delete calendar event %s/%s", calendarID, eventID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newCalendarService(cmd.Context(), account)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -309,7 +334,7 @@ func newCalendarCreateCmd(flags *rootFlags) *cobra.Command {
|
||||
calendarID := args[0]
|
||||
|
||||
if strings.TrimSpace(summary) == "" || strings.TrimSpace(from) == "" || strings.TrimSpace(to) == "" {
|
||||
return errors.New("required: --summary, --from, --to")
|
||||
return usage("required: --summary, --from, --to")
|
||||
}
|
||||
|
||||
svc, err := newCalendarService(cmd.Context(), account)
|
||||
@ -388,7 +413,7 @@ func newCalendarUpdateCmd(flags *rootFlags) *cobra.Command {
|
||||
targetAllDay = allDay
|
||||
// Converting between all-day and timed needs explicit start/end.
|
||||
if !cmd.Flags().Changed("from") || !cmd.Flags().Changed("to") {
|
||||
return errors.New("when changing --all-day, also provide --from and --to")
|
||||
return usage("when changing --all-day, also provide --from and --to")
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,7 +447,7 @@ func newCalendarUpdateCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return errors.New("no updates provided")
|
||||
return usage("no updates provided")
|
||||
}
|
||||
|
||||
updated, err := svc.Events.Update(calendarID, eventID, existing).Do()
|
||||
@ -503,10 +528,10 @@ func newCalendarFreeBusyCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
calendarIDs := splitCSV(args[0])
|
||||
if len(calendarIDs) == 0 {
|
||||
return errors.New("no calendar IDs provided")
|
||||
return usage("no calendar IDs provided")
|
||||
}
|
||||
if strings.TrimSpace(from) == "" || strings.TrimSpace(to) == "" {
|
||||
return errors.New("required: --from and --to")
|
||||
return usage("required: --from and --to")
|
||||
}
|
||||
|
||||
svc, err := newCalendarService(cmd.Context(), account)
|
||||
@ -535,14 +560,21 @@ func newCalendarFreeBusyCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "CALENDAR\tSTART\tEND")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "CALENDAR\tSTART\tEND")
|
||||
for id, data := range resp.Calendars {
|
||||
for _, b := range data.Busy {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", id, b.Start, b.End)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", id, b.Start, b.End)
|
||||
}
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
41
internal/cmd/confirm.go
Normal file
41
internal/cmd/confirm.go
Normal file
@ -0,0 +1,41 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func confirmDestructive(cmd *cobra.Command, flags *rootFlags, action string) error {
|
||||
if flags.Force {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Never prompt in non-interactive contexts.
|
||||
if flags.NoInput || !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return usagef("refusing to %s without --force (non-interactive)", action)
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf("Proceed to %s? [y/N]: ", action)
|
||||
if u := ui.FromContext(cmd.Context()); u != nil {
|
||||
u.Err().Println(prompt)
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(os.Stderr, prompt)
|
||||
}
|
||||
|
||||
line, readErr := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if readErr != nil && !errors.Is(readErr, os.ErrClosed) {
|
||||
return readErr
|
||||
}
|
||||
ans := strings.TrimSpace(strings.ToLower(line))
|
||||
if ans == "y" || ans == "yes" {
|
||||
return nil
|
||||
}
|
||||
return &ExitError{Code: 1, Err: errors.New("cancelled")}
|
||||
}
|
||||
36
internal/cmd/confirm_test.go
Normal file
36
internal/cmd/confirm_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
)
|
||||
|
||||
func TestConfirmDestructive_NonInteractiveRequiresForce(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
u, err := ui.New(ui.Options{Color: "never"})
|
||||
if err != nil {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
cmd.SetContext(ui.WithUI(context.Background(), u))
|
||||
|
||||
withStdin(t, "y\n", func() {
|
||||
flags := &rootFlags{Force: false, NoInput: false}
|
||||
err := confirmDestructive(cmd, flags, "delete something")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if ExitCode(err) != 2 {
|
||||
t.Fatalf("expected exit code 2, got %d (err=%v)", ExitCode(err), err)
|
||||
}
|
||||
})
|
||||
|
||||
withStdin(t, "", func() {
|
||||
flags := &rootFlags{Force: true, NoInput: false}
|
||||
if err := confirmDestructive(cmd, flags, "delete something"); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -83,15 +84,20 @@ func newContactsSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
for _, r := range resp.Results {
|
||||
p := r.Person
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(
|
||||
tw,
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\n",
|
||||
p.ResourceName,
|
||||
sanitizeTab(primaryName(p)),
|
||||
@ -99,7 +105,9 @@ func newContactsSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
sanitizeTab(primaryPhone(p)),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -72,20 +72,27 @@ func newContactsListCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
for _, p := range resp.Connections {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n",
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
|
||||
p.ResourceName,
|
||||
sanitizeTab(primaryName(p)),
|
||||
sanitizeTab(primaryEmail(p)),
|
||||
sanitizeTab(primaryPhone(p)),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -112,7 +119,7 @@ func newContactsGetCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
identifier := strings.TrimSpace(args[0])
|
||||
if identifier == "" {
|
||||
return errors.New("empty identifier")
|
||||
return usage("empty identifier")
|
||||
}
|
||||
|
||||
svc, err := newPeopleContactsService(cmd.Context(), account)
|
||||
@ -191,7 +198,7 @@ func newContactsCreateCmd(flags *rootFlags) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(given) == "" {
|
||||
return errors.New("required: --given")
|
||||
return usage("required: --given")
|
||||
}
|
||||
|
||||
svc, err := newPeopleContactsService(cmd.Context(), account)
|
||||
@ -249,7 +256,11 @@ func newContactsUpdateCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
resourceName := strings.TrimSpace(args[0])
|
||||
if !strings.HasPrefix(resourceName, "people/") {
|
||||
return errors.New("resourceName must start with people/")
|
||||
return usage("resourceName must start with people/")
|
||||
}
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("delete contact %s", resourceName)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newPeopleContactsService(cmd.Context(), account)
|
||||
@ -299,7 +310,7 @@ func newContactsUpdateCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
|
||||
if len(updateFields) == 0 {
|
||||
return errors.New("no updates provided")
|
||||
return usage("no updates provided")
|
||||
}
|
||||
|
||||
updated, err := svc.People.UpdateContact(resourceName, existing).
|
||||
@ -336,7 +347,7 @@ func newContactsDeleteCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
resourceName := strings.TrimSpace(args[0])
|
||||
if !strings.HasPrefix(resourceName, "people/") {
|
||||
return errors.New("resourceName must start with people/")
|
||||
return usage("resourceName must start with people/")
|
||||
}
|
||||
|
||||
svc, err := newPeopleContactsService(cmd.Context(), account)
|
||||
|
||||
@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -89,19 +90,26 @@ func newContactsDirectoryListCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "RESOURCE\tNAME\tEMAIL")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "RESOURCE\tNAME\tEMAIL")
|
||||
for _, p := range resp.People {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n",
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n",
|
||||
p.ResourceName,
|
||||
sanitizeTab(primaryName(p)),
|
||||
sanitizeTab(primaryEmail(p)),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -178,19 +186,26 @@ func newContactsDirectorySearchCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "RESOURCE\tNAME\tEMAIL")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "RESOURCE\tNAME\tEMAIL")
|
||||
for _, p := range resp.People {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n",
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n",
|
||||
p.ResourceName,
|
||||
sanitizeTab(primaryName(p)),
|
||||
sanitizeTab(primaryEmail(p)),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -272,20 +287,27 @@ func newContactsOtherListCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
for _, p := range resp.OtherContacts {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n",
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
|
||||
p.ResourceName,
|
||||
sanitizeTab(primaryName(p)),
|
||||
sanitizeTab(primaryEmail(p)),
|
||||
sanitizeTab(primaryPhone(p)),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -355,21 +377,28 @@ func newContactsOtherSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "RESOURCE\tNAME\tEMAIL\tPHONE")
|
||||
for _, r := range resp.Results {
|
||||
p := r.Person
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n",
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
|
||||
p.ResourceName,
|
||||
sanitizeTab(primaryName(p)),
|
||||
sanitizeTab(primaryEmail(p)),
|
||||
sanitizeTab(primaryPhone(p)),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@ -98,11 +98,16 @@ func newDriveLsCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tNAME\tTYPE\tSIZE\tMODIFIED")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tNAME\tTYPE\tSIZE\tMODIFIED")
|
||||
for _, f := range resp.Files {
|
||||
fmt.Fprintf(
|
||||
tw,
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%s\n",
|
||||
f.Id,
|
||||
f.Name,
|
||||
@ -111,7 +116,9 @@ func newDriveLsCmd(flags *rootFlags) *cobra.Command {
|
||||
formatDateTime(f.ModifiedTime),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -172,11 +179,16 @@ func newDriveSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tNAME\tTYPE\tSIZE\tMODIFIED")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tNAME\tTYPE\tSIZE\tMODIFIED")
|
||||
for _, f := range resp.Files {
|
||||
fmt.Fprintf(
|
||||
tw,
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%s\n",
|
||||
f.Id,
|
||||
f.Name,
|
||||
@ -185,7 +197,9 @@ func newDriveSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
formatDateTime(f.ModifiedTime),
|
||||
)
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -212,6 +226,10 @@ func newDriveGetCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
fileID := args[0]
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("delete drive file %s", fileID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newDriveService(cmd.Context(), account)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -573,13 +591,13 @@ func newDriveShareCmd(flags *rootFlags) *cobra.Command {
|
||||
fileID := args[0]
|
||||
|
||||
if !anyone && email == "" {
|
||||
return errors.New("must specify --anyone or --email")
|
||||
return usage("must specify --anyone or --email")
|
||||
}
|
||||
if role == "" {
|
||||
role = "reader"
|
||||
}
|
||||
if role != "reader" && role != "writer" {
|
||||
return errors.New("invalid --role (expected reader|writer)")
|
||||
return usage("invalid --role (expected reader|writer)")
|
||||
}
|
||||
|
||||
svc, err := newDriveService(cmd.Context(), account)
|
||||
@ -645,6 +663,10 @@ func newDriveUnshareCmd(flags *rootFlags) *cobra.Command {
|
||||
fileID := args[0]
|
||||
permissionID := args[1]
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("remove permission %s from drive file %s", permissionID, fileID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newDriveService(cmd.Context(), account)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -713,16 +735,23 @@ func newDrivePermissionsCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tTYPE\tROLE\tEMAIL")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tTYPE\tROLE\tEMAIL")
|
||||
for _, p := range resp.Permissions {
|
||||
email := p.EmailAddress
|
||||
if email == "" {
|
||||
email = "-"
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", p.Id, p.Type, p.Role, email)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", p.Id, p.Type, p.Role, email)
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
|
||||
@ -70,7 +70,7 @@ func TestDriveLsCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeText)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{})
|
||||
|
||||
textOut := captureStdout(t, func() {
|
||||
cmd := newDriveLsCmd(flags)
|
||||
@ -101,7 +101,7 @@ func TestDriveLsCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx2 := ui.WithUI(context.Background(), u2)
|
||||
ctx2 = outfmt.WithMode(ctx2, outfmt.ModeJSON)
|
||||
ctx2 = outfmt.WithMode(ctx2, outfmt.Mode{JSON: true})
|
||||
|
||||
jsonOut := captureStdout(t, func() {
|
||||
cmd := newDriveLsCmd(flags)
|
||||
@ -125,4 +125,25 @@ func TestDriveLsCmd_TextAndJSON(t *testing.T) {
|
||||
if parsed.NextPageToken != "npt" || len(parsed.Files) != 2 {
|
||||
t.Fatalf("unexpected json: %#v", parsed)
|
||||
}
|
||||
|
||||
// Plain mode: stable TSV (tabs preserved).
|
||||
var errBuf3 bytes.Buffer
|
||||
u3, err := ui.New(ui.Options{Stdout: io.Discard, Stderr: &errBuf3, Color: "never"})
|
||||
if err != nil {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx3 := ui.WithUI(context.Background(), u3)
|
||||
ctx3 = outfmt.WithMode(ctx3, outfmt.Mode{Plain: true})
|
||||
|
||||
plainOut := captureStdout(t, func() {
|
||||
cmd := newDriveLsCmd(flags)
|
||||
cmd.SetContext(ctx3)
|
||||
cmd.SetArgs([]string{})
|
||||
if execErr := cmd.Execute(); execErr != nil {
|
||||
t.Fatalf("execute: %v", execErr)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(plainOut, "ID\tNAME\tTYPE\tSIZE\tMODIFIED") {
|
||||
t.Fatalf("expected TSV header, got: %q", plainOut)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ func TestDrivePermissionsCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeText)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{})
|
||||
|
||||
textOut := captureStdout(t, func() {
|
||||
cmd := newDrivePermissionsCmd(flags)
|
||||
@ -90,7 +90,7 @@ func TestDrivePermissionsCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx2 := ui.WithUI(context.Background(), u2)
|
||||
ctx2 = outfmt.WithMode(ctx2, outfmt.ModeJSON)
|
||||
ctx2 = outfmt.WithMode(ctx2, outfmt.Mode{JSON: true})
|
||||
|
||||
jsonOut := captureStdout(t, func() {
|
||||
cmd := newDrivePermissionsCmd(flags)
|
||||
|
||||
@ -71,7 +71,7 @@ func TestDriveURLCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeText)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{})
|
||||
|
||||
cmd := newDriveURLCmd(flags)
|
||||
cmd.SetContext(ctx)
|
||||
@ -94,7 +94,7 @@ func TestDriveURLCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx2 := ui.WithUI(context.Background(), u2)
|
||||
ctx2 = outfmt.WithMode(ctx2, outfmt.ModeJSON)
|
||||
ctx2 = outfmt.WithMode(ctx2, outfmt.Mode{JSON: true})
|
||||
|
||||
cmd2 := newDriveURLCmd(flags)
|
||||
cmd2.SetContext(ctx2)
|
||||
|
||||
@ -30,7 +30,7 @@ func TestExecute_AuthAdd_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "add", "a@b.com", "--services", "calendar,gmail"}); err != nil {
|
||||
if err := Execute([]string{"--json", "auth", "add", "a@b.com", "--services", "calendar,gmail"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -20,7 +20,7 @@ func TestExecute_AuthCredentials_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "auth", "credentials", in}); err != nil {
|
||||
if err := Execute([]string{"--json", "auth", "credentials", in}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -47,3 +47,29 @@ func TestExecute_AuthCredentials_JSON(t *testing.T) {
|
||||
t.Fatalf("stat: %v size=%d", err, st.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_AuthCredentials_Stdin_JSON(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
withStdin(t, `{"installed":{"client_id":"id","client_secret":"sec"}}`, func() {
|
||||
if err := Execute([]string{"--json", "auth", "credentials", "-"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var parsed struct {
|
||||
Saved bool `json:"saved"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
||||
t.Fatalf("json parse: %v\nout=%q", err, out)
|
||||
}
|
||||
if !parsed.Saved || parsed.Path == "" {
|
||||
t.Fatalf("unexpected: %#v", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,37 +94,37 @@ func TestExecute_CalendarMoreCommands_JSON(t *testing.T) {
|
||||
|
||||
_ = captureStderr(t, func() {
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "acl", calendarID}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "calendar", "acl", calendarID}); err != nil {
|
||||
t.Fatalf("acl: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "events", calendarID, "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z", "--query", "hello"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "calendar", "events", calendarID, "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z", "--query", "hello"}); err != nil {
|
||||
t.Fatalf("events: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "event", calendarID, eventID}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "calendar", "event", calendarID, eventID}); err != nil {
|
||||
t.Fatalf("event: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "create", calendarID, "--summary", "S", "--from", "2025-12-17T10:00:00Z", "--to", "2025-12-17T11:00:00Z"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "calendar", "create", calendarID, "--summary", "S", "--from", "2025-12-17T10:00:00Z", "--to", "2025-12-17T11:00:00Z"}); err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "update", calendarID, eventID, "--summary", "S2"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "calendar", "update", calendarID, eventID, "--summary", "S2"}); err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "delete", calendarID, eventID}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "calendar", "delete", calendarID, eventID}); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "freebusy", "c1", "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "calendar", "freebusy", "c1", "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z"}); err != nil {
|
||||
t.Fatalf("freebusy: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -50,7 +50,7 @@ func TestExecute_CalendarCalendars_MaxAndPage_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"calendar", "calendars",
|
||||
"--max", "1",
|
||||
@ -113,7 +113,7 @@ func TestExecute_CalendarAcl_MaxAndPage_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"calendar", "acl", "c1",
|
||||
"--max", "2",
|
||||
|
||||
@ -73,7 +73,7 @@ func TestExecute_CalendarRespond_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"calendar", "respond",
|
||||
calendarID, eventID,
|
||||
|
||||
@ -43,7 +43,7 @@ func TestExecute_CalendarCalendars_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "calendar", "calendars"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "calendar", "calendars"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -51,7 +51,7 @@ func TestExecute_CalendarEvent_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "calendar", "event", "c1", "e1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "calendar", "event", "c1", "e1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -91,7 +91,7 @@ func TestExecute_CalendarAcl_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "calendar", "acl", "c1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "calendar", "acl", "c1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -132,7 +132,7 @@ func TestExecute_CalendarCalendars_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "calendar", "calendars"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "calendar", "calendars"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -49,7 +49,7 @@ func TestExecute_ContactsDirectoryList_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
errOut := captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "contacts", "directory", "list", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "contacts", "directory", "list", "--max", "1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -124,42 +124,42 @@ func TestExecute_ContactsMoreCommands_JSON(t *testing.T) {
|
||||
|
||||
_ = captureStderr(t, func() {
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "search", "Ada"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "search", "Ada"}); err != nil {
|
||||
t.Fatalf("search: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "create", "--given", "Ada", "--email", "ada@example.com", "--phone", "+1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "create", "--given", "Ada", "--email", "ada@example.com", "--phone", "+1"}); err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "update", "people/c1", "--given", "Ada", "--family", "Updated"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "update", "people/c1", "--given", "Ada", "--family", "Updated"}); err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "delete", "people/c1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "contacts", "delete", "people/c1"}); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "directory", "list", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "directory", "list", "--max", "1"}); err != nil {
|
||||
t.Fatalf("dir list: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "directory", "search", "Dir", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "directory", "search", "Dir", "--max", "1"}); err != nil {
|
||||
t.Fatalf("dir search: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "other", "list", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "other", "list", "--max", "1"}); err != nil {
|
||||
t.Fatalf("other list: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "other", "search", "Other"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "other", "search", "Other"}); err != nil {
|
||||
t.Fatalf("other search: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -49,7 +49,7 @@ func TestExecute_ContactsList_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "list", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "list", "--max", "1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -112,7 +112,7 @@ func TestExecute_ContactsGet_ByEmail_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "contacts", "get", "ada@example.com"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "contacts", "get", "ada@example.com"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -60,7 +60,7 @@ func TestExecute_DriveDownload_WithOutFile_JSON(t *testing.T) {
|
||||
stdout := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"drive", "download", "id1",
|
||||
"--out", outPath,
|
||||
@ -135,7 +135,7 @@ func TestExecute_DriveDownload_WithOutDir_JSON(t *testing.T) {
|
||||
stdout := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"drive", "download", "id1",
|
||||
"--out", outDir,
|
||||
|
||||
@ -109,47 +109,47 @@ func TestExecute_DriveMoreCommands_JSON(t *testing.T) {
|
||||
|
||||
_ = captureStderr(t, func() {
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "search", "hello"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "search", "hello"}); err != nil {
|
||||
t.Fatalf("search: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "upload", tmpFile, "--name", "upload.bin", "--folder", "np"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "upload", tmpFile, "--name", "upload.bin", "--folder", "np"}); err != nil {
|
||||
t.Fatalf("upload: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "mkdir", "Folder", "--parent", "np"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "mkdir", "Folder", "--parent", "np"}); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "rename", "id1", "New"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "rename", "id1", "New"}); err != nil {
|
||||
t.Fatalf("rename: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "move", "id1", "np"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "move", "id1", "np"}); err != nil {
|
||||
t.Fatalf("move: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "share", "id1", "--anyone", "--role", "reader"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "share", "id1", "--anyone", "--role", "reader"}); err != nil {
|
||||
t.Fatalf("share: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "permissions", "id1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "permissions", "id1"}); err != nil {
|
||||
t.Fatalf("permissions: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "unshare", "id1", "p1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "drive", "unshare", "id1", "p1"}); err != nil {
|
||||
t.Fatalf("unshare: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "delete", "id1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "drive", "delete", "id1"}); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -55,7 +55,7 @@ func TestExecute_DriveGet_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "get", "id1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "get", "id1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -123,7 +123,7 @@ func TestExecute_DriveDownload_JSON(t *testing.T) {
|
||||
dest := filepath.Join(t.TempDir(), "out.bin")
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "drive", "download", "id1", "--out", dest}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "drive", "download", "id1", "--out", dest}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -184,7 +184,7 @@ func TestDriveDownloadCmd_FileHasNoName(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeText)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{})
|
||||
|
||||
cmd := newDriveDownloadCmd(flags)
|
||||
cmd.SetContext(ctx)
|
||||
|
||||
@ -47,7 +47,7 @@ func TestExecute_DriveGet_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "drive", "get", "id1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "drive", "get", "id1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -83,7 +83,7 @@ func TestExecute_DrivePermissions_Text_NoPermissions(t *testing.T) {
|
||||
|
||||
errOut := captureStderr(t, func() {
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "drive", "permissions", "id1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "drive", "permissions", "id1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -124,7 +124,7 @@ func TestExecute_DrivePermissions_Text_WithPermissions(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "drive", "permissions", "id1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "drive", "permissions", "id1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -165,7 +165,7 @@ func TestExecute_DriveSearch_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
errOut := captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "drive", "search", "Doc", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "drive", "search", "Doc", "--max", "1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -75,7 +75,7 @@ func TestExecute_GmailAttachment_OutPath_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "attachment", "m1", "a1",
|
||||
"--out", outPath,
|
||||
@ -173,7 +173,7 @@ func TestExecute_GmailAttachment_NameOverride_ConfigDir_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "attachment", "m1", "a1",
|
||||
"--name", "override.bin",
|
||||
@ -241,7 +241,7 @@ func TestExecute_GmailAttachment_NotFound(t *testing.T) {
|
||||
outPath := filepath.Join(t.TempDir(), "a.bin")
|
||||
|
||||
err = Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "attachment", "m1", "a1",
|
||||
"--out", outPath,
|
||||
|
||||
@ -57,7 +57,7 @@ func TestExecute_GmailGet_Metadata_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "get", "m1",
|
||||
"--format", "metadata",
|
||||
@ -132,7 +132,7 @@ func TestExecute_GmailGet_Raw_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "get", "m1",
|
||||
"--format", "raw",
|
||||
@ -212,7 +212,6 @@ func TestExecute_GmailGet_Metadata_Text(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "text",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "get", "m1",
|
||||
"--format", "metadata",
|
||||
|
||||
@ -57,7 +57,7 @@ func TestExecute_GmailLabelsGet_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "gmail", "labels", "get", "INBOX"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "gmail", "labels", "get", "INBOX"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -129,7 +129,7 @@ func TestExecute_GmailThreadDraftsSend_JSON(t *testing.T) {
|
||||
|
||||
_ = captureStderr(t, func() {
|
||||
out := captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "thread", "t1", "--download"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "thread", "t1", "--download"}); err != nil {
|
||||
t.Fatalf("thread: %v", err)
|
||||
}
|
||||
})
|
||||
@ -148,32 +148,32 @@ func TestExecute_GmailThreadDraftsSend_JSON(t *testing.T) {
|
||||
}
|
||||
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "drafts", "list"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "drafts", "list"}); err != nil {
|
||||
t.Fatalf("drafts list: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "drafts", "get", "d1", "--download"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "drafts", "get", "d1", "--download"}); err != nil {
|
||||
t.Fatalf("drafts get: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "drafts", "create", "--to", "x@y.com", "--subject", "S", "--body", "B"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "drafts", "create", "--to", "x@y.com", "--subject", "S", "--body", "B"}); err != nil {
|
||||
t.Fatalf("drafts create: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "drafts", "send", "d1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "drafts", "send", "d1"}); err != nil {
|
||||
t.Fatalf("drafts send: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "drafts", "delete", "d1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "gmail", "drafts", "delete", "d1"}); err != nil {
|
||||
t.Fatalf("drafts delete: %v", err)
|
||||
}
|
||||
})
|
||||
_ = captureStdout(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "send", "--to", "x@y.com", "--subject", "S", "--body", "B"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "send", "--to", "x@y.com", "--subject", "S", "--body", "B"}); err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -59,7 +59,7 @@ func TestExecute_GmailSend_ReplyToHeader(t *testing.T) {
|
||||
_ = captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "send",
|
||||
"--to", "x@y.com",
|
||||
@ -141,7 +141,7 @@ func TestExecute_GmailSend_ReplyToMessageID(t *testing.T) {
|
||||
_ = captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "send",
|
||||
"--to", "x@y.com",
|
||||
@ -226,7 +226,7 @@ func TestExecute_GmailDraftsCreate_ReplyToMessageID(t *testing.T) {
|
||||
_ = captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--output", "json",
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "drafts", "create",
|
||||
"--to", "x@y.com",
|
||||
|
||||
@ -75,7 +75,7 @@ func TestExecute_GmailSearch_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "search", "newer_than:7d", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "search", "newer_than:7d", "--max", "1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -111,7 +111,7 @@ func TestExecute_GmailSearch_JSON(t *testing.T) {
|
||||
func TestExecute_GmailURL_JSON(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "gmail", "url", "t1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "gmail", "url", "t1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -78,7 +78,7 @@ func TestExecute_GmailThread_Text_Download(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "gmail", "thread", "t-thread-1", "--download"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "gmail", "thread", "t-thread-1", "--download"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -144,7 +144,7 @@ func TestExecute_GmailDraftsGet_Text_Download(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "gmail", "drafts", "get", "d1", "--download"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "gmail", "drafts", "get", "d1", "--download"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -50,7 +50,7 @@ func TestExecute_ContactsList_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
errOut := captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "contacts", "list", "--max", "1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "contacts", "list", "--max", "1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -96,7 +96,7 @@ func TestExecute_ContactsGet_ByResource_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "contacts", "get", "people/c1"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "contacts", "get", "people/c1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -138,7 +138,7 @@ func TestExecute_CalendarFreeBusy_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "calendar", "freebusy", "c1", "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "calendar", "freebusy", "c1", "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -44,7 +44,7 @@ func TestExecute_PeopleMe_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "people", "me"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "people", "me"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -98,7 +98,7 @@ func TestExecute_PeopleMe_Text(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "text", "--account", "a@b.com", "people", "me"}); err != nil {
|
||||
if err := Execute([]string{"--account", "a@b.com", "people", "me"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -43,7 +43,7 @@ func TestExecute_TasksLists_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "lists", "--max", "10"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "tasks", "lists", "--max", "10"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -101,7 +101,7 @@ func TestExecute_TasksListsCreate_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "lists", "create", "Teaching"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "tasks", "lists", "create", "Teaching"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -152,7 +152,7 @@ func TestExecute_TasksList_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "list", "l1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "tasks", "list", "l1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -212,7 +212,7 @@ func TestExecute_TasksAdd_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "add", "l1", "--title", "Hello"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "tasks", "add", "l1", "--title", "Hello"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -272,7 +272,7 @@ func TestExecute_TasksDone_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "done", "l1", "t1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "tasks", "done", "l1", "t1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -317,7 +317,7 @@ func TestExecute_TasksDelete_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "delete", "l1", "t1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "tasks", "delete", "l1", "t1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -374,7 +374,7 @@ func TestExecute_TasksUpdate_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "update", "l1", "t1", "--title", "New title"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "tasks", "update", "l1", "t1", "--title", "New title"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -433,7 +433,7 @@ func TestExecute_TasksUndo_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "undo", "l1", "t1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--account", "a@b.com", "tasks", "undo", "l1", "t1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
@ -479,7 +479,7 @@ func TestExecute_TasksClear_JSON(t *testing.T) {
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--output", "json", "--account", "a@b.com", "tasks", "clear", "l1"}); err != nil {
|
||||
if err := Execute([]string{"--json", "--force", "--account", "a@b.com", "tasks", "clear", "l1"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
76
internal/cmd/execute_version_exitcodes_test.go
Normal file
76
internal/cmd/execute_version_exitcodes_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExecute_VersionFlag(t *testing.T) {
|
||||
origV, origC, origD := version, commit, date
|
||||
t.Cleanup(func() {
|
||||
version = origV
|
||||
commit = origC
|
||||
date = origD
|
||||
})
|
||||
version = "1.2.3"
|
||||
commit = "abc123"
|
||||
date = ""
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--version"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
if !strings.Contains(out, "1.2.3") {
|
||||
t.Fatalf("unexpected out=%q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_VersionCommand_JSON(t *testing.T) {
|
||||
origV, origC, origD := version, commit, date
|
||||
t.Cleanup(func() {
|
||||
version = origV
|
||||
commit = origC
|
||||
date = origD
|
||||
})
|
||||
version = "1.2.3"
|
||||
commit = "abc123"
|
||||
date = "2025-12-26T00:00:00Z"
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--json", "version"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
||||
t.Fatalf("json parse: %v\nout=%q", err, out)
|
||||
}
|
||||
if parsed["version"] != "1.2.3" || parsed["commit"] != "abc123" || parsed["date"] != "2025-12-26T00:00:00Z" {
|
||||
t.Fatalf("unexpected json: %#v", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_ExitCodes(t *testing.T) {
|
||||
err := Execute([]string{"--nope"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if ExitCode(err) != 2 {
|
||||
t.Fatalf("expected exit code 2, got %d (err=%v)", ExitCode(err), err)
|
||||
}
|
||||
|
||||
err = Execute([]string{"drive", "get"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if ExitCode(err) != 2 {
|
||||
t.Fatalf("expected exit code 2, got %d (err=%v)", ExitCode(err), err)
|
||||
}
|
||||
}
|
||||
36
internal/cmd/exit.go
Normal file
36
internal/cmd/exit.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import "errors"
|
||||
|
||||
type ExitError struct {
|
||||
Code int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ExitError) Error() string {
|
||||
if e == nil || e.Err == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *ExitError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func ExitCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
var ee *ExitError
|
||||
if errors.As(err, &ee) && ee != nil {
|
||||
if ee.Code < 0 {
|
||||
return 1
|
||||
}
|
||||
return ee.Code
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strings"
|
||||
@ -79,7 +80,6 @@ func newGmailSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
items := make([]item, 0, len(resp.Threads))
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
for _, t := range resp.Threads {
|
||||
if t.Id == "" {
|
||||
continue
|
||||
@ -134,11 +134,20 @@ func newGmailSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintln(tw, "ID\tDATE\tFROM\tSUBJECT\tLABELS")
|
||||
for _, it := range items {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", it.ID, it.Date, it.From, it.Subject, strings.Join(it.Labels, ","))
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "ID\tDATE\tFROM\tSUBJECT\tLABELS")
|
||||
for _, it := range items {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", it.ID, it.Date, it.From, it.Subject, strings.Join(it.Labels, ","))
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
|
||||
@ -31,7 +31,7 @@ func newGmailAttachmentCmd(flags *rootFlags) *cobra.Command {
|
||||
messageID := strings.TrimSpace(args[0])
|
||||
attachmentID := strings.TrimSpace(args[1])
|
||||
if messageID == "" || attachmentID == "" {
|
||||
return errors.New("messageId/attachmentId required")
|
||||
return usage("messageId/attachmentId required")
|
||||
}
|
||||
|
||||
svc, err := newGmailService(cmd.Context(), account)
|
||||
|
||||
@ -2,8 +2,8 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -80,16 +80,23 @@ func newGmailDraftsListCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tMESSAGE_ID")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tMESSAGE_ID")
|
||||
for _, d := range resp.Drafts {
|
||||
msgID := ""
|
||||
if d.Message != nil {
|
||||
msgID = d.Message.Id
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\n", d.Id, msgID)
|
||||
fmt.Fprintf(w, "%s\t%s\n", d.Id, msgID)
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -118,6 +125,10 @@ func newGmailDraftsGetCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
draftID := args[0]
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("delete gmail draft %s", draftID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newGmailService(cmd.Context(), account)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -307,10 +318,10 @@ func newGmailDraftsCreateCmd(flags *rootFlags) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(to) == "" || strings.TrimSpace(subject) == "" {
|
||||
return errors.New("required: --to, --subject")
|
||||
return usage("required: --to, --subject")
|
||||
}
|
||||
if strings.TrimSpace(body) == "" && strings.TrimSpace(bodyHTML) == "" {
|
||||
return errors.New("required: --body or --body-html")
|
||||
return usage("required: --body or --body-html")
|
||||
}
|
||||
|
||||
svc, err := newGmailService(cmd.Context(), account)
|
||||
|
||||
@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@ -28,7 +27,7 @@ func newGmailGetCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
messageID := strings.TrimSpace(args[0])
|
||||
if messageID == "" {
|
||||
return errors.New("empty messageId")
|
||||
return usage("empty messageId")
|
||||
}
|
||||
|
||||
format = strings.TrimSpace(format)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@ -26,7 +25,7 @@ func newGmailHistoryCmd(flags *rootFlags) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(since) == "" {
|
||||
return errors.New("--since is required")
|
||||
return usage("--since is required")
|
||||
}
|
||||
startID, err := parseHistoryID(since)
|
||||
if err != nil {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -47,7 +47,7 @@ func newGmailLabelsGetCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
raw := strings.TrimSpace(args[0])
|
||||
if raw == "" {
|
||||
return errors.New("empty label")
|
||||
return usage("empty label")
|
||||
}
|
||||
id := raw
|
||||
if v, ok := idMap[strings.ToLower(raw)]; ok {
|
||||
@ -103,12 +103,19 @@ func newGmailLabelsListCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tNAME\tTYPE")
|
||||
for _, l := range resp.Labels {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\n", l.Id, l.Name, l.Type)
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tNAME\tTYPE")
|
||||
for _, l := range resp.Labels {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", l.Id, l.Name, l.Type)
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -132,7 +139,7 @@ func newGmailLabelsModifyCmd(flags *rootFlags) *cobra.Command {
|
||||
addLabels := splitCSV(add)
|
||||
removeLabels := splitCSV(remove)
|
||||
if len(addLabels) == 0 && len(removeLabels) == 0 {
|
||||
return errors.New("must specify --add and/or --remove")
|
||||
return usage("must specify --add and/or --remove")
|
||||
}
|
||||
|
||||
svc, err := newGmailService(cmd.Context(), account)
|
||||
|
||||
@ -78,7 +78,7 @@ func TestGmailLabelsGetCmd_JSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeJSON)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
|
||||
|
||||
cmd := newGmailLabelsGetCmd(flags)
|
||||
cmd.SetContext(ctx)
|
||||
@ -148,7 +148,7 @@ func TestGmailLabelsListCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeText)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{})
|
||||
|
||||
cmd := newGmailLabelsListCmd(flags)
|
||||
cmd.SetContext(ctx)
|
||||
@ -170,7 +170,7 @@ func TestGmailLabelsListCmd_TextAndJSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeJSON)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
|
||||
|
||||
cmd := newGmailLabelsListCmd(flags)
|
||||
cmd.SetContext(ctx)
|
||||
@ -261,7 +261,7 @@ func TestGmailLabelsModifyCmd_JSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeJSON)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
|
||||
|
||||
cmd := newGmailLabelsModifyCmd(flags)
|
||||
cmd.SetContext(ctx)
|
||||
|
||||
@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@ -34,10 +33,10 @@ func newGmailSendCmd(flags *rootFlags) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(to) == "" || strings.TrimSpace(subject) == "" {
|
||||
return errors.New("required: --to, --subject")
|
||||
return usage("required: --to, --subject")
|
||||
}
|
||||
if strings.TrimSpace(body) == "" && strings.TrimSpace(bodyHTML) == "" {
|
||||
return errors.New("required: --body or --body-html")
|
||||
return usage("required: --body or --body-html")
|
||||
}
|
||||
|
||||
svc, err := newGmailService(cmd.Context(), account)
|
||||
|
||||
@ -52,7 +52,7 @@ func newGmailWatchStartCmd(flags *rootFlags) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(topic) == "" {
|
||||
return errors.New("--topic is required")
|
||||
return usage("--topic is required")
|
||||
}
|
||||
ttl, err := parseDurationSeconds(ttlRaw)
|
||||
if err != nil {
|
||||
@ -194,6 +194,11 @@ func newGmailWatchStopCmd(flags *rootFlags) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := confirmDestructive(cmd, flags, "stop gmail watch and clear stored state"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newGmailService(cmd.Context(), account)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -239,19 +244,19 @@ func newGmailWatchServeCmd(flags *rootFlags) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return errors.New("--path must start with '/'")
|
||||
return usage("--path must start with '/'")
|
||||
}
|
||||
if port <= 0 {
|
||||
return errors.New("--port must be > 0")
|
||||
return usage("--port must be > 0")
|
||||
}
|
||||
if !verifyOIDC && sharedToken == "" && !isLoopbackHost(bind) {
|
||||
return errors.New("--verify-oidc or --token required when binding non-loopback")
|
||||
return usage("--verify-oidc or --token required when binding non-loopback")
|
||||
}
|
||||
if oidcEmail != "" && !verifyOIDC {
|
||||
return errors.New("--oidc-email requires --verify-oidc")
|
||||
return usage("--oidc-email requires --verify-oidc")
|
||||
}
|
||||
if oidcAudience != "" && !verifyOIDC {
|
||||
return errors.New("--oidc-audience requires --verify-oidc")
|
||||
return usage("--oidc-audience requires --verify-oidc")
|
||||
}
|
||||
|
||||
store, err := loadGmailWatchStore(account)
|
||||
@ -444,10 +449,10 @@ func requestGmailWatch(ctx context.Context, svc *gmail.Service, topic string, la
|
||||
func hookFromFlags(url, token string, includeBody bool, maxBytes int, maxBytesChanged bool, allowNoHook bool) (*gmailWatchHook, error) {
|
||||
if strings.TrimSpace(url) == "" {
|
||||
if token != "" {
|
||||
return nil, errors.New("--hook-url required when using --hook-token")
|
||||
return nil, usage("--hook-url required when using --hook-token")
|
||||
}
|
||||
if !allowNoHook && (includeBody || maxBytesChanged) {
|
||||
return nil, errors.New("--hook-url required when setting hook options")
|
||||
return nil, usage("--hook-url required when setting hook options")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@ -455,7 +460,7 @@ func hookFromFlags(url, token string, includeBody bool, maxBytes int, maxBytesCh
|
||||
if includeBody {
|
||||
maxBytes = defaultHookMaxBytes
|
||||
} else if maxBytesChanged {
|
||||
return nil, errors.New("--max-bytes must be > 0")
|
||||
return nil, usage("--max-bytes must be > 0")
|
||||
}
|
||||
}
|
||||
return &gmailWatchHook{
|
||||
|
||||
@ -74,7 +74,7 @@ func TestGmailWatchStartCmd_JSON(t *testing.T) {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.ModeJSON)
|
||||
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
|
||||
|
||||
cmd := newGmailWatchStartCmd(flags)
|
||||
cmd.SetContext(ctx)
|
||||
|
||||
@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -100,7 +99,7 @@ func (p *gmailPushPayload) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("historyId must be string or number")
|
||||
return usage("historyId must be string or number")
|
||||
}
|
||||
|
||||
type gmailHookMessage struct {
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/steipete/gogcli/internal/errfmt"
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
@ -14,12 +16,25 @@ import (
|
||||
type rootFlags struct {
|
||||
Color string
|
||||
Account string
|
||||
Output string
|
||||
JSON bool
|
||||
Plain bool
|
||||
Force bool
|
||||
NoInput bool
|
||||
}
|
||||
|
||||
func Execute(args []string) error {
|
||||
flags := rootFlags{Color: envOr("GOG_COLOR", "auto")}
|
||||
flags.Output = envOr("GOG_OUTPUT", "text")
|
||||
envMode := outfmt.FromEnv()
|
||||
flags.JSON = envMode.JSON
|
||||
flags.Plain = envMode.Plain
|
||||
|
||||
// Avoid dangerous prefix-matching for commands (future-proofing).
|
||||
cobra.EnablePrefixMatching = false
|
||||
|
||||
if hasExactArg(args, "--version") {
|
||||
fmt.Fprintln(os.Stdout, VersionString())
|
||||
return nil
|
||||
}
|
||||
|
||||
root := &cobra.Command{
|
||||
Use: "gog",
|
||||
@ -37,17 +52,17 @@ func Execute(args []string) error {
|
||||
# Avoid repeating --account
|
||||
export GOG_ACCOUNT=you@gmail.com
|
||||
|
||||
# Gmail
|
||||
gog gmail search 'newer_than:7d' --max 10
|
||||
gog gmail thread <threadId>
|
||||
gog gmail get <messageId> --format metadata
|
||||
gog gmail attachment <messageId> <attachmentId> --out ./attachment.bin
|
||||
gog gmail labels get INBOX --output=json
|
||||
# Gmail
|
||||
gog gmail search 'newer_than:7d' --max 10
|
||||
gog gmail thread <threadId>
|
||||
gog gmail get <messageId> --format metadata
|
||||
gog gmail attachment <messageId> <attachmentId> --out ./attachment.bin
|
||||
gog gmail labels get INBOX --json
|
||||
|
||||
# Calendar
|
||||
gog calendar calendars
|
||||
gog calendar events <calendarId> --from 2025-01-01T00:00:00Z --to 2025-01-08T00:00:00Z --max 50
|
||||
gog calendar respond <calendarId> <eventId> --status accepted
|
||||
# Calendar
|
||||
gog calendar calendars
|
||||
gog calendar events <calendarId> --from 2025-01-01T00:00:00Z --to 2025-01-08T00:00:00Z --max 50
|
||||
gog calendar respond <calendarId> <eventId> --status accepted
|
||||
|
||||
# Contacts
|
||||
gog contacts list --max 50
|
||||
@ -58,14 +73,14 @@ func Execute(args []string) error {
|
||||
gog tasks lists --max 50
|
||||
gog tasks list <tasklistId> --max 50
|
||||
|
||||
# People
|
||||
gog people me
|
||||
# People
|
||||
gog people me
|
||||
|
||||
# Parseable output
|
||||
gog --output=json drive ls --max 5 | jq .
|
||||
`),
|
||||
# Parseable output
|
||||
gog --json drive ls --max 5 | jq .
|
||||
`),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
mode, err := outfmt.Parse(flags.Output)
|
||||
mode, err := outfmt.FromFlags(flags.JSON, flags.Plain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -75,7 +90,7 @@ func Execute(args []string) error {
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
Color: func() string {
|
||||
if outfmt.IsJSON(cmd.Context()) {
|
||||
if outfmt.IsJSON(cmd.Context()) || outfmt.IsPlain(cmd.Context()) {
|
||||
return "never"
|
||||
}
|
||||
return flags.Color
|
||||
@ -92,20 +107,36 @@ func Execute(args []string) error {
|
||||
root.SetArgs(args)
|
||||
root.PersistentFlags().StringVar(&flags.Color, "color", flags.Color, "Color output: auto|always|never")
|
||||
root.PersistentFlags().StringVar(&flags.Account, "account", "", "Account email for API commands (gmail/calendar/drive/contacts/tasks/people)")
|
||||
root.PersistentFlags().StringVar(&flags.Output, "output", flags.Output, "Output format: text|json")
|
||||
root.PersistentFlags().BoolVar(&flags.JSON, "json", flags.JSON, "Output JSON to stdout (best for scripting)")
|
||||
root.PersistentFlags().BoolVar(&flags.Plain, "plain", flags.Plain, "Output stable, parseable text to stdout (TSV; no colors)")
|
||||
root.PersistentFlags().BoolVar(&flags.Force, "force", false, "Skip confirmations for destructive commands")
|
||||
root.PersistentFlags().BoolVar(&flags.NoInput, "no-input", false, "Never prompt; fail instead (useful for CI)")
|
||||
|
||||
root.AddCommand(newAuthCmd())
|
||||
root.AddCommand(newAuthCmd(&flags))
|
||||
root.AddCommand(newDriveCmd(&flags))
|
||||
root.AddCommand(newCalendarCmd(&flags))
|
||||
root.AddCommand(newGmailCmd(&flags))
|
||||
root.AddCommand(newContactsCmd(&flags))
|
||||
root.AddCommand(newTasksCmd(&flags))
|
||||
root.AddCommand(newPeopleCmd(&flags))
|
||||
root.AddCommand(newVersionCmd())
|
||||
|
||||
root.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||
// pflag already includes helpful context ("unknown flag", "invalid argument", ...).
|
||||
return newUsageError(err)
|
||||
})
|
||||
|
||||
err := root.Execute()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, pflag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ExitCode(err) == 1 && isUsageError(err) {
|
||||
err = &ExitError{Code: 2, Err: err}
|
||||
}
|
||||
|
||||
if u := ui.FromContext(root.Context()); u != nil {
|
||||
u.Err().Error(errfmt.Format(err))
|
||||
@ -121,3 +152,47 @@ func envOr(key, fallback string) string {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func hasExactArg(args []string, target string) bool {
|
||||
for _, a := range args {
|
||||
if a == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newUsageError wraps errors in a way main() can map to exit code 2.
|
||||
func newUsageError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// Preserve pflag.ErrHelp (should not be treated as failure).
|
||||
if errors.Is(err, pflag.ErrHelp) {
|
||||
return err
|
||||
}
|
||||
return &ExitError{Code: 2, Err: err}
|
||||
}
|
||||
|
||||
func isUsageError(err error) bool {
|
||||
var outErr *outfmt.ParseError
|
||||
if errors.As(err, &outErr) {
|
||||
return true
|
||||
}
|
||||
var uiErr *ui.ParseError
|
||||
if errors.As(err, &uiErr) {
|
||||
return true
|
||||
}
|
||||
msg := strings.TrimSpace(err.Error())
|
||||
switch {
|
||||
case strings.HasPrefix(msg, "accepts "),
|
||||
strings.HasPrefix(msg, "requires "),
|
||||
strings.HasPrefix(msg, "unknown command"),
|
||||
strings.HasPrefix(msg, "invalid argument"),
|
||||
strings.HasPrefix(msg, "unknown flag"),
|
||||
strings.HasPrefix(msg, "unknown shorthand flag"):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -38,7 +38,7 @@ func newTasksListCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
tasklistID := strings.TrimSpace(args[0])
|
||||
if tasklistID == "" {
|
||||
return errors.New("empty tasklistId")
|
||||
return usage("empty tasklistId")
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
@ -86,16 +86,23 @@ func newTasksListCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tTITLE\tSTATUS\tDUE\tUPDATED")
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tTITLE\tSTATUS\tDUE\tUPDATED")
|
||||
for _, t := range resp.Items {
|
||||
status := strings.TrimSpace(t.Status)
|
||||
if status == "" {
|
||||
status = "needsAction"
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", t.Id, t.Title, status, strings.TrimSpace(t.Due), strings.TrimSpace(t.Updated))
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", t.Id, t.Title, status, strings.TrimSpace(t.Due), strings.TrimSpace(t.Updated))
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
@ -140,10 +147,10 @@ func newTasksAddCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
tasklistID := strings.TrimSpace(args[0])
|
||||
if tasklistID == "" {
|
||||
return errors.New("empty tasklistId")
|
||||
return usage("empty tasklistId")
|
||||
}
|
||||
if strings.TrimSpace(title) == "" {
|
||||
return errors.New("required: --title")
|
||||
return usage("required: --title")
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
@ -213,10 +220,10 @@ func newTasksUpdateCmd(flags *rootFlags) *cobra.Command {
|
||||
tasklistID := strings.TrimSpace(args[0])
|
||||
taskID := strings.TrimSpace(args[1])
|
||||
if tasklistID == "" {
|
||||
return errors.New("empty tasklistId")
|
||||
return usage("empty tasklistId")
|
||||
}
|
||||
if taskID == "" {
|
||||
return errors.New("empty taskId")
|
||||
return usage("empty taskId")
|
||||
}
|
||||
|
||||
patch := &tasks.Task{}
|
||||
@ -238,11 +245,11 @@ func newTasksUpdateCmd(flags *rootFlags) *cobra.Command {
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return errors.New("no fields to update (set at least one of: --title, --notes, --due, --status)")
|
||||
return usage("no fields to update (set at least one of: --title, --notes, --due, --status)")
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("status") && patch.Status != "" && patch.Status != "needsAction" && patch.Status != "completed" {
|
||||
return errors.New("invalid --status (expected needsAction or completed)")
|
||||
return usage("invalid --status (expected needsAction or completed)")
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
@ -295,10 +302,14 @@ func newTasksDoneCmd(flags *rootFlags) *cobra.Command {
|
||||
tasklistID := strings.TrimSpace(args[0])
|
||||
taskID := strings.TrimSpace(args[1])
|
||||
if tasklistID == "" {
|
||||
return errors.New("empty tasklistId")
|
||||
return usage("empty tasklistId")
|
||||
}
|
||||
if taskID == "" {
|
||||
return errors.New("empty taskId")
|
||||
return usage("empty taskId")
|
||||
}
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("delete task %s from list %s", taskID, tasklistID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
@ -336,10 +347,10 @@ func newTasksUndoCmd(flags *rootFlags) *cobra.Command {
|
||||
tasklistID := strings.TrimSpace(args[0])
|
||||
taskID := strings.TrimSpace(args[1])
|
||||
if tasklistID == "" {
|
||||
return errors.New("empty tasklistId")
|
||||
return usage("empty tasklistId")
|
||||
}
|
||||
if taskID == "" {
|
||||
return errors.New("empty taskId")
|
||||
return usage("empty taskId")
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
@ -377,10 +388,10 @@ func newTasksDeleteCmd(flags *rootFlags) *cobra.Command {
|
||||
tasklistID := strings.TrimSpace(args[0])
|
||||
taskID := strings.TrimSpace(args[1])
|
||||
if tasklistID == "" {
|
||||
return errors.New("empty tasklistId")
|
||||
return usage("empty tasklistId")
|
||||
}
|
||||
if taskID == "" {
|
||||
return errors.New("empty taskId")
|
||||
return usage("empty taskId")
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
@ -418,7 +429,11 @@ func newTasksClearCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
tasklistID := strings.TrimSpace(args[0])
|
||||
if tasklistID == "" {
|
||||
return errors.New("empty tasklistId")
|
||||
return usage("empty tasklistId")
|
||||
}
|
||||
|
||||
if err := confirmDestructive(cmd, flags, fmt.Sprintf("clear completed tasks from list %s", tasklistID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@ -51,12 +51,19 @@ func newTasksListsCmd(flags *rootFlags) *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "ID\tTITLE")
|
||||
for _, tl := range resp.Items {
|
||||
fmt.Fprintf(tw, "%s\t%s\n", tl.Id, tl.Title)
|
||||
var w io.Writer = os.Stdout
|
||||
var tw *tabwriter.Writer
|
||||
if !outfmt.IsPlain(cmd.Context()) {
|
||||
tw = tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
w = tw
|
||||
}
|
||||
fmt.Fprintln(w, "ID\tTITLE")
|
||||
for _, tl := range resp.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\n", tl.Id, tl.Title)
|
||||
}
|
||||
if tw != nil {
|
||||
_ = tw.Flush()
|
||||
}
|
||||
_ = tw.Flush()
|
||||
if resp.NextPageToken != "" {
|
||||
u.Err().Printf("# Next page: --page %s", resp.NextPageToken)
|
||||
}
|
||||
@ -84,7 +91,7 @@ func newTasksListsCreateCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
title := strings.TrimSpace(strings.Join(args, " "))
|
||||
if title == "" {
|
||||
return errors.New("empty title")
|
||||
return usage("empty title")
|
||||
}
|
||||
|
||||
svc, err := newTasksService(cmd.Context(), account)
|
||||
|
||||
@ -43,3 +43,22 @@ func captureStderr(t *testing.T, fn func()) string {
|
||||
_ = r.Close()
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func withStdin(t *testing.T, input string, fn func()) {
|
||||
t.Helper()
|
||||
|
||||
orig := os.Stdin
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe: %v", err)
|
||||
}
|
||||
os.Stdin = r
|
||||
|
||||
_, _ = io.WriteString(w, input)
|
||||
_ = w.Close()
|
||||
|
||||
fn()
|
||||
|
||||
_ = r.Close()
|
||||
os.Stdin = orig
|
||||
}
|
||||
|
||||
14
internal/cmd/usage.go
Normal file
14
internal/cmd/usage.go
Normal file
@ -0,0 +1,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func usage(msg string) error {
|
||||
return &ExitError{Code: 2, Err: errors.New(msg)}
|
||||
}
|
||||
|
||||
func usagef(format string, args ...any) error {
|
||||
return &ExitError{Code: 2, Err: fmt.Errorf(format, args...)}
|
||||
}
|
||||
52
internal/cmd/version.go
Normal file
52
internal/cmd/version.go
Normal file
@ -0,0 +1,52 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = ""
|
||||
date = ""
|
||||
)
|
||||
|
||||
func VersionString() string {
|
||||
v := strings.TrimSpace(version)
|
||||
if v == "" {
|
||||
v = "dev"
|
||||
}
|
||||
if strings.TrimSpace(commit) == "" && strings.TrimSpace(date) == "" {
|
||||
return v
|
||||
}
|
||||
if strings.TrimSpace(commit) == "" {
|
||||
return fmt.Sprintf("%s (%s)", v, strings.TrimSpace(date))
|
||||
}
|
||||
if strings.TrimSpace(date) == "" {
|
||||
return fmt.Sprintf("%s (%s)", v, strings.TrimSpace(commit))
|
||||
}
|
||||
return fmt.Sprintf("%s (%s %s)", v, strings.TrimSpace(commit), strings.TrimSpace(date))
|
||||
}
|
||||
|
||||
func newVersionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if outfmt.IsJSON(cmd.Context()) {
|
||||
return outfmt.WriteJSON(os.Stdout, map[string]any{
|
||||
"version": strings.TrimSpace(version),
|
||||
"commit": strings.TrimSpace(commit),
|
||||
"date": strings.TrimSpace(date),
|
||||
})
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, VersionString())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -3,26 +3,31 @@ package outfmt
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Mode string
|
||||
type Mode struct {
|
||||
JSON bool
|
||||
Plain bool
|
||||
}
|
||||
|
||||
const (
|
||||
ModeText Mode = "text"
|
||||
ModeJSON Mode = "json"
|
||||
)
|
||||
type ParseError struct{ msg string }
|
||||
|
||||
func Parse(s string) (Mode, error) {
|
||||
switch Mode(strings.ToLower(strings.TrimSpace(s))) {
|
||||
case ModeText, "":
|
||||
return ModeText, nil
|
||||
case ModeJSON:
|
||||
return ModeJSON, nil
|
||||
default:
|
||||
return "", errors.New("invalid --output (expected text|json)")
|
||||
func (e *ParseError) Error() string { return e.msg }
|
||||
|
||||
func FromFlags(jsonOut bool, plainOut bool) (Mode, error) {
|
||||
if jsonOut && plainOut {
|
||||
return Mode{}, &ParseError{msg: "invalid output mode (cannot combine --json and --plain)"}
|
||||
}
|
||||
return Mode{JSON: jsonOut, Plain: plainOut}, nil
|
||||
}
|
||||
|
||||
func FromEnv() Mode {
|
||||
return Mode{
|
||||
JSON: envBool("GOG_JSON"),
|
||||
Plain: envBool("GOG_PLAIN"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,12 +43,11 @@ func FromContext(ctx context.Context) Mode {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return ModeText
|
||||
return Mode{}
|
||||
}
|
||||
|
||||
func IsJSON(ctx context.Context) bool {
|
||||
return FromContext(ctx) == ModeJSON
|
||||
}
|
||||
func IsJSON(ctx context.Context) bool { return FromContext(ctx).JSON }
|
||||
func IsPlain(ctx context.Context) bool { return FromContext(ctx).Plain }
|
||||
|
||||
func WriteJSON(w io.Writer, v any) error {
|
||||
enc := json.NewEncoder(w)
|
||||
@ -51,3 +55,13 @@ func WriteJSON(w io.Writer, v any) error {
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
func envBool(key string) bool {
|
||||
v := strings.TrimSpace(strings.ToLower(os.Getenv(key)))
|
||||
switch v {
|
||||
case "1", "true", "yes", "y", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,40 +6,27 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want Mode
|
||||
wantErr bool
|
||||
}{
|
||||
{"", ModeText, false},
|
||||
{"text", ModeText, false},
|
||||
{"json", ModeJSON, false},
|
||||
{" JSON ", ModeJSON, false},
|
||||
{"nope", "", true},
|
||||
func TestFromFlags(t *testing.T) {
|
||||
if _, err := FromFlags(true, true); err == nil {
|
||||
t.Fatalf("expected error when combining --json and --plain")
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := Parse(tt.in)
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatalf("Parse(%q): expected error", tt.in)
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("Parse(%q): %v", tt.in, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("Parse(%q)=%q want %q", tt.in, got, tt.want)
|
||||
}
|
||||
got, err := FromFlags(true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if !got.JSON || got.Plain {
|
||||
t.Fatalf("unexpected mode: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMode(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if FromContext(ctx) != ModeText {
|
||||
if IsJSON(ctx) || IsPlain(ctx) {
|
||||
t.Fatalf("expected default text")
|
||||
}
|
||||
ctx = WithMode(ctx, ModeJSON)
|
||||
if !IsJSON(ctx) {
|
||||
t.Fatalf("expected json")
|
||||
ctx = WithMode(ctx, Mode{JSON: true})
|
||||
if !IsJSON(ctx) || IsPlain(ctx) {
|
||||
t.Fatalf("expected json-only")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -22,6 +21,10 @@ type UI struct {
|
||||
err *Printer
|
||||
}
|
||||
|
||||
type ParseError struct{ msg string }
|
||||
|
||||
func (e *ParseError) Error() string { return e.msg }
|
||||
|
||||
func New(opts Options) (*UI, error) {
|
||||
if opts.Stdout == nil {
|
||||
opts.Stdout = os.Stdout
|
||||
@ -35,7 +38,7 @@ func New(opts Options) (*UI, error) {
|
||||
colorMode = "auto"
|
||||
}
|
||||
if colorMode != "auto" && colorMode != "always" && colorMode != "never" {
|
||||
return nil, errors.New("invalid --color (expected auto|always|never)")
|
||||
return nil, &ParseError{msg: "invalid --color (expected auto|always|never)"}
|
||||
}
|
||||
|
||||
out := termenv.NewOutput(opts.Stdout, termenv.WithProfile(termenv.EnvColorProfile()))
|
||||
|
||||
@ -11,6 +11,12 @@ function run(cmd, args) {
|
||||
return res.status;
|
||||
}
|
||||
|
||||
function runCapture(cmd, args) {
|
||||
const res = spawnSync(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
||||
if (res.error || res.status !== 0) return "";
|
||||
return String(res.stdout || "").trim();
|
||||
}
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const binDir = join(repoRoot, "bin");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
@ -18,7 +24,16 @@ mkdirSync(binDir, { recursive: true });
|
||||
const exe = process.platform === "win32" ? "gog.exe" : "gog";
|
||||
const binPath = join(binDir, exe);
|
||||
|
||||
run("go", ["build", "-o", binPath, "./cmd/gog"]);
|
||||
const version = runCapture("git", ["describe", "--tags", "--always", "--dirty"]) || "dev";
|
||||
const commit = runCapture("git", ["rev-parse", "--short=12", "HEAD"]) || "";
|
||||
const date = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
||||
const ldflags = [
|
||||
`-X github.com/steipete/gogcli/internal/cmd.version=${version}`,
|
||||
`-X github.com/steipete/gogcli/internal/cmd.commit=${commit}`,
|
||||
`-X github.com/steipete/gogcli/internal/cmd.date=${date}`,
|
||||
].join(" ");
|
||||
|
||||
run("go", ["build", "-ldflags", ldflags, "-o", binPath, "./cmd/gog"]);
|
||||
|
||||
const final = spawnSync(binPath, process.argv.slice(2), { stdio: "inherit" });
|
||||
if (final.error) throw final.error;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user