Skip to content

Commit 0930c16

Browse files
authored
Merge pull request #75 from salmonumbrella/feat/calendar-propose-time
Add calendar propose-time, tasks repeat, auth alias, and more
2 parents c28a360 + d683a48 commit 0930c16

44 files changed

Lines changed: 2748 additions & 130 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
- Gmail: show attachment info (incl. humanized sizes) for `gmail get` full/metadata output, with JSON `sizeHuman`. (#83) — thanks @jeanregisser.
8+
- CLI: calendar propose-time, event types, tasks repeat + get, auth aliases, time now, enable-commands, and day-of-week JSON fields. (#75) — thanks @salmonumbrella.
89

910
### Fixed
1011

@@ -15,6 +16,7 @@
1516
- Classroom: scan pages when filtering coursework/materials by topic. (#73) — thanks @salmonumbrella.
1617
- CLI: enable shell completions and stop flag suggestions after `--`. (#77) — thanks @salmonumbrella.
1718
- Timezone: honor `--timezone local` and allow env/config defaults for Gmail + Calendar output. (#79) — thanks @salmonumbrella.
19+
- Calendar/Tasks: propose-time decline sends updates and repeat-until keeps due time. (#75) — thanks @salmonumbrella.
1820

1921
### Build
2022

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,11 @@ gog keep get <noteId> --account you@yourdomain.com
303303

304304
### Environment Variables
305305

306-
- `GOG_ACCOUNT` - Default account email to use (avoids repeating `--account`; otherwise uses keyring default or a single stored token)
306+
- `GOG_ACCOUNT` - Default account email or alias to use (avoids repeating `--account`; otherwise uses keyring default or a single stored token)
307307
- `GOG_JSON` - Default JSON output
308308
- `GOG_PLAIN` - Default plain output
309309
- `GOG_COLOR` - Color mode: `auto` (default), `always`, or `never`
310+
- `GOG_ENABLE_COMMANDS` - Comma-separated allowlist of top-level commands (e.g., `calendar,tasks`)
310311

311312
### Config File (JSON5)
312313

@@ -324,8 +325,21 @@ Example (JSON5 supports comments and trailing commas):
324325
{
325326
// Avoid macOS Keychain prompts
326327
keyring_backend: "file",
328+
// Optional account aliases
329+
account_aliases: {
330+
work: "work@company.com",
331+
personal: "me@gmail.com",
332+
},
327333
}
328334
```
335+
336+
### Account Aliases
337+
338+
```bash
339+
gog auth alias set work work@company.com
340+
gog auth alias list
341+
gog auth alias unset work
342+
```
329343

330344
## Security
331345

@@ -538,6 +552,25 @@ gog calendar update <calendarId> <eventId> \
538552
--from 2025-01-15T11:00:00Z \
539553
--to 2025-01-15T12:00:00Z
540554

555+
# Special event types via --event-type (focus-time/out-of-office/working-location)
556+
gog calendar create primary \
557+
--event-type focus-time \
558+
--from 2025-01-15T13:00:00Z \
559+
--to 2025-01-15T14:00:00Z
560+
561+
gog calendar create primary \
562+
--event-type out-of-office \
563+
--from 2025-01-20 \
564+
--to 2025-01-21 \
565+
--all-day
566+
567+
gog calendar create primary \
568+
--event-type working-location \
569+
--working-location-type office \
570+
--working-office-label "HQ" \
571+
--from 2025-01-22 \
572+
--to 2025-01-23
573+
541574
# Add attendees without replacing existing attendees/RSVP state
542575
gog calendar update <calendarId> <eventId> \
543576
--add-attendee "alice@example.com,bob@example.com"
@@ -558,6 +591,13 @@ gog calendar conflicts --calendars "primary,work@example.com" \
558591
--today # Today's conflicts
559592
```
560593

594+
### Time
595+
596+
```bash
597+
gog time now
598+
gog time now --timezone UTC
599+
```
600+
561601
### Drive
562602

563603
```bash
@@ -655,12 +695,16 @@ gog tasks lists create <title>
655695

656696
# Tasks in a list
657697
gog tasks list <tasklistId> --max 50
698+
gog tasks get <tasklistId> <taskId>
658699
gog tasks add <tasklistId> --title "Task title"
700+
gog tasks add <tasklistId> --title "Weekly sync" --due 2025-02-01 --repeat weekly --repeat-count 4
659701
gog tasks update <tasklistId> <taskId> --title "New title"
660702
gog tasks done <tasklistId> <taskId>
661703
gog tasks undo <tasklistId> <taskId>
662704
gog tasks delete <tasklistId> <taskId>
663705
gog tasks clear <tasklistId>
706+
707+
# Note: Google Tasks treats due dates as date-only; time components may be ignored.
664708
```
665709

666710
### Sheets
@@ -880,7 +924,8 @@ gog --verbose gmail search 'newer_than:7d'
880924

881925
All commands support these flags:
882926

883-
- `--account <email>` - Account to use (overrides GOG_ACCOUNT)
927+
- `--account <email|alias|auto>` - Account to use (overrides GOG_ACCOUNT)
928+
- `--enable-commands <csv>` - Allowlist top-level commands (e.g., `calendar,tasks`)
884929
- `--json` - Output JSON to stdout (best for scripting)
885930
- `--plain` - Output stable, parseable text to stdout (TSV; no colors)
886931
- `--color <mode>` - Color mode: `auto`, `always`, or `never` (default: auto)

docs/spec.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,14 @@ We intentionally avoid storing refresh tokens in plain JSON on disk.
121121

122122
Environment:
123123

124-
- `GOG_ACCOUNT=you@gmail.com` (used when `--account` is not set; otherwise uses keyring default or a single stored token)
124+
- `GOG_ACCOUNT=you@gmail.com` (email or alias; used when `--account` is not set; otherwise uses keyring default or a single stored token)
125125
- `GOG_KEYRING_PASSWORD=...` (used when keyring falls back to encrypted file backend in non-interactive environments)
126126
- `GOG_KEYRING_BACKEND={auto|keychain|file}` (force backend; use `file` to avoid Keychain prompts and pair with `GOG_KEYRING_PASSWORD` for non-interactive)
127127
- `GOG_TIMEZONE=America/New_York` (default output timezone; IANA name or `UTC`; `local` forces local timezone)
128+
- `GOG_ENABLE_COMMANDS=calendar,tasks` (optional allowlist of top-level commands)
128129
- `config.json` can also set `keyring_backend` (JSON5; env vars take precedence)
129130
- `config.json` can also set `default_timezone` (IANA name or `UTC`)
131+
- `config.json` can also set `account_aliases` for `gog auth alias` (JSON5)
130132

131133
Flag aliases:
132134
- `--out` also accepts `--output`.
@@ -141,6 +143,9 @@ Flag aliases:
141143
- `gog auth services [--markdown]`
142144
- `gog auth keep <email> --key <service-account.json>` (Google Keep; Workspace only)
143145
- `gog auth list`
146+
- `gog auth alias list`
147+
- `gog auth alias set <alias> <email>`
148+
- `gog auth alias unset <alias>`
144149
- `gog auth status`
145150
- `gog auth remove <email>`
146151
- `gog auth tokens list`
@@ -169,11 +174,12 @@ Flag aliases:
169174
- `gog calendar acl <calendarId>`
170175
- `gog calendar events <calendarId> [--from RFC3339] [--to RFC3339] [--max N] [--page TOKEN] [--query Q]`
171176
- `gog calendar event <calendarId> <eventId>`
172-
- `gog calendar create <calendarId> --summary S --from DT --to DT [--description D] [--location L] [--attendees a@b.com,c@d.com] [--all-day]`
173-
- `gog calendar update <calendarId> <eventId> [--summary S] [--from DT] [--to DT] [--description D] [--location L] [--attendees ...] [--add-attendee ...] [--all-day]`
177+
- `gog calendar create <calendarId> --summary S --from DT --to DT [--description D] [--location L] [--attendees a@b.com,c@d.com] [--all-day] [--event-type TYPE]`
178+
- `gog calendar update <calendarId> <eventId> [--summary S] [--from DT] [--to DT] [--description D] [--location L] [--attendees ...] [--add-attendee ...] [--all-day] [--event-type TYPE]`
174179
- `gog calendar delete <calendarId> <eventId>`
175180
- `gog calendar freebusy <calendarIds> --from RFC3339 --to RFC3339`
176181
- `gog calendar respond <calendarId> <eventId> --status accepted|declined|tentative [--send-updates all|none|externalOnly]`
182+
- `gog time now [--timezone TZ]`
177183
- `gog classroom courses [--state ...] [--max N] [--page TOKEN]`
178184
- `gog classroom courses get <courseId>`
179185
- `gog classroom courses create --name NAME [--owner me] [--state ACTIVE|...]`
@@ -255,8 +261,9 @@ Flag aliases:
255261
- `gog tasks lists [--max N] [--page TOKEN]`
256262
- `gog tasks lists create <title>`
257263
- `gog tasks list <tasklistId> [--max N] [--page TOKEN]`
258-
- `gog tasks add <tasklistId> --title T [--notes N] [--due RFC3339] [--parent ID] [--previous ID]`
259-
- `gog tasks update <tasklistId> <taskId> [--title T] [--notes N] [--due RFC3339] [--status needsAction|completed]`
264+
- `gog tasks get <tasklistId> <taskId>`
265+
- `gog tasks add <tasklistId> --title T [--notes N] [--due RFC3339|YYYY-MM-DD] [--repeat daily|weekly|monthly|yearly] [--repeat-count N] [--repeat-until DT] [--parent ID] [--previous ID]`
266+
- `gog tasks update <tasklistId> <taskId> [--title T] [--notes N] [--due RFC3339|YYYY-MM-DD] [--status needsAction|completed]`
260267
- `gog tasks done <tasklistId> <taskId>`
261268
- `gog tasks undo <tasklistId> <taskId>`
262269
- `gog tasks delete <tasklistId> <taskId>`

internal/cmd/account.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,38 @@ import (
44
"os"
55
"strings"
66

7+
"github.com/steipete/gogcli/internal/config"
78
"github.com/steipete/gogcli/internal/secrets"
89
)
910

1011
var openSecretsStoreForAccount = secrets.OpenDefault
1112

1213
func requireAccount(flags *RootFlags) (string, error) {
1314
if v := strings.TrimSpace(flags.Account); v != "" {
14-
return v, nil
15+
if resolved, ok, err := resolveAccountAlias(v); err != nil {
16+
return "", err
17+
} else if ok {
18+
return resolved, nil
19+
}
20+
if shouldAutoSelectAccount(v) {
21+
v = ""
22+
}
23+
if v != "" {
24+
return v, nil
25+
}
1526
}
1627
if v := strings.TrimSpace(os.Getenv("GOG_ACCOUNT")); v != "" {
17-
return v, nil
28+
if resolved, ok, err := resolveAccountAlias(v); err != nil {
29+
return "", err
30+
} else if ok {
31+
return resolved, nil
32+
}
33+
if shouldAutoSelectAccount(v) {
34+
v = ""
35+
}
36+
if v != "" {
37+
return v, nil
38+
}
1839
}
1940

2041
if store, err := openSecretsStoreForAccount(); err == nil {
@@ -35,3 +56,20 @@ func requireAccount(flags *RootFlags) (string, error) {
3556

3657
return "", usage("missing --account (or set GOG_ACCOUNT, set default via `gog auth manage`, or store exactly one token)")
3758
}
59+
60+
func resolveAccountAlias(value string) (string, bool, error) {
61+
value = strings.TrimSpace(value)
62+
if value == "" || strings.Contains(value, "@") || shouldAutoSelectAccount(value) {
63+
return "", false, nil
64+
}
65+
return config.ResolveAccountAlias(value)
66+
}
67+
68+
func shouldAutoSelectAccount(value string) bool {
69+
switch strings.ToLower(strings.TrimSpace(value)) {
70+
case "auto", "default":
71+
return true
72+
default:
73+
return false
74+
}
75+
}

internal/cmd/account_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package cmd
22

33
import (
44
"errors"
5+
"path/filepath"
56
"testing"
67

8+
"github.com/steipete/gogcli/internal/config"
79
"github.com/steipete/gogcli/internal/secrets"
810
)
911

@@ -51,6 +53,66 @@ func TestRequireAccount_UsesEnv(t *testing.T) {
5153
}
5254
}
5355

56+
func TestRequireAccount_ResolvesAliasFlag(t *testing.T) {
57+
home := t.TempDir()
58+
t.Setenv("HOME", home)
59+
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, "xdg-config"))
60+
if err := config.WriteConfig(config.File{
61+
AccountAliases: map[string]string{"work": "alias@example.com"},
62+
}); err != nil {
63+
t.Fatalf("write config: %v", err)
64+
}
65+
66+
flags := &RootFlags{Account: "work"}
67+
got, err := requireAccount(flags)
68+
if err != nil {
69+
t.Fatalf("err: %v", err)
70+
}
71+
if got != "alias@example.com" {
72+
t.Fatalf("got %q", got)
73+
}
74+
}
75+
76+
func TestRequireAccount_ResolvesAliasEnv(t *testing.T) {
77+
home := t.TempDir()
78+
t.Setenv("HOME", home)
79+
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, "xdg-config"))
80+
if err := config.WriteConfig(config.File{
81+
AccountAliases: map[string]string{"work": "alias@example.com"},
82+
}); err != nil {
83+
t.Fatalf("write config: %v", err)
84+
}
85+
86+
t.Setenv("GOG_ACCOUNT", "work")
87+
flags := &RootFlags{}
88+
got, err := requireAccount(flags)
89+
if err != nil {
90+
t.Fatalf("err: %v", err)
91+
}
92+
if got != "alias@example.com" {
93+
t.Fatalf("got %q", got)
94+
}
95+
}
96+
97+
func TestRequireAccount_AutoUsesDefault(t *testing.T) {
98+
t.Setenv("GOG_ACCOUNT", "")
99+
flags := &RootFlags{Account: "auto"}
100+
101+
prev := openSecretsStoreForAccount
102+
t.Cleanup(func() { openSecretsStoreForAccount = prev })
103+
openSecretsStoreForAccount = func() (secrets.Store, error) {
104+
return &fakeSecretsStore{defaultAccount: "default@example.com"}, nil
105+
}
106+
107+
got, err := requireAccount(flags)
108+
if err != nil {
109+
t.Fatalf("err: %v", err)
110+
}
111+
if got != "default@example.com" {
112+
t.Fatalf("got %q", got)
113+
}
114+
}
115+
54116
func TestRequireAccount_Missing(t *testing.T) {
55117
t.Setenv("GOG_ACCOUNT", "")
56118
flags := &RootFlags{}

internal/cmd/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type AuthCmd struct {
5353
Add AuthAddCmd `cmd:"" name:"add" help:"Authorize and store a refresh token"`
5454
Services AuthServicesCmd `cmd:"" name:"services" help:"List supported auth services and scopes"`
5555
List AuthListCmd `cmd:"" name:"list" help:"List stored accounts"`
56+
Aliases AuthAliasCmd `cmd:"" name:"alias" help:"Manage account aliases"`
5657
Status AuthStatusCmd `cmd:"" name:"status" help:"Show auth configuration and keyring backend"`
5758
Keyring AuthKeyringCmd `cmd:"" name:"keyring" help:"Configure keyring backend"`
5859
Remove AuthRemoveCmd `cmd:"" name:"remove" help:"Remove a stored refresh token"`

0 commit comments

Comments
 (0)