Skip to content

Commit 3de9412

Browse files
author
Peter Steinberger
committed
chore: merge main into PR branch (openclaw#187)
2 parents f2b86d5 + e3cb940 commit 3de9412

12 files changed

Lines changed: 451 additions & 76 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- Gmail: add `--exclude-labels` to `watch serve` (defaults: `SPAM,TRASH`). (#194) — thanks @salmonumbrella.
8+
- Drive: share files with an entire Workspace domain via `drive share --to domain`. (#192) — thanks @Danielkweber.
9+
510
### Fixed
611

712
- Auth: improve remote/server-friendly manual OAuth flow (`auth add --remote`). (#187) — thanks @salmonumbrella.

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# 🧭 gogcli — Google in your terminal.
22

3+
![GitHub Repo Banner](https://ghrb.waren.build/banner?header=gogcli%F0%9F%A7%AD&subheader=Google+in+your+terminal&bg=f3f4f6&color=1f2937&support=true)
4+
<!-- Created with GitHub Repo Banner by Waren Gonzaga: https://ghrb.waren.build -->
5+
36
Fast, script-friendly CLI for Gmail, Calendar, Chat, Classroom, Drive, Docs, Slides, Sheets, Contacts, Tasks, People, Groups (Workspace), and Keep (Workspace-only). JSON-first output, multiple accounts, and least-privilege auth built in.
47

58
## Features
@@ -789,11 +792,11 @@ gog drive rename <fileId> "New Name"
789792
gog drive move <fileId> --parent <destinationFolderId>
790793
gog drive delete <fileId> # Move to trash
791794

792-
# Permissions
793-
gog drive permissions <fileId>
794-
gog drive share <fileId> --email user@example.com --role reader
795-
gog drive share <fileId> --email user@example.com --role writer
796-
gog drive unshare <fileId> --permission-id <permissionId>
795+
# Permissions
796+
gog drive permissions <fileId>
797+
gog drive share <fileId> --to user --email user@example.com --role reader
798+
gog drive share <fileId> --to user --email user@example.com --role writer
799+
gog drive unshare <fileId> --permission-id <permissionId>
797800

798801
# Shared drives (Team Drives)
799802
gog drive drives --max 100

docs/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ Flag aliases:
178178
- `gog drive delete <fileId>`
179179
- `gog drive move <fileId> --parent ID`
180180
- `gog drive rename <fileId> <newName>`
181-
- `gog drive share <fileId> [--anyone | --email addr] [--role reader|writer] [--discoverable]`
181+
- `gog drive share <fileId> --to anyone|user|domain [--email addr] [--domain example.com] [--role reader|writer] [--discoverable]`
182182
- `gog drive permissions <fileId> [--max N] [--page TOKEN]`
183183
- `gog drive unshare <fileId> <permissionId>`
184184
- `gog drive url <fileIds...>`

internal/cmd/drive.go

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ const (
4040
extPptx = ".pptx"
4141
extPNG = ".png"
4242
extTXT = ".txt"
43+
44+
driveShareToAnyone = "anyone"
45+
driveShareToUser = "user"
46+
driveShareToDomain = "domain"
47+
48+
drivePermRoleReader = "reader"
49+
drivePermRoleWriter = "writer"
4350
)
4451

4552
type DriveCmd struct {
@@ -572,8 +579,10 @@ func (c *DriveRenameCmd) Run(ctx context.Context, flags *RootFlags) error {
572579

573580
type DriveShareCmd struct {
574581
FileID string `arg:"" name:"fileId" help:"File ID"`
575-
Anyone bool `name:"anyone" help:"Make publicly accessible"`
576-
Email string `name:"email" help:"Share with specific user"`
582+
To string `name:"to" help:"Share target: anyone|user|domain"`
583+
Anyone bool `name:"anyone" hidden:"" help:"(deprecated) Use --to=anyone"`
584+
Email string `name:"email" help:"User email (for --to=user)"`
585+
Domain string `name:"domain" help:"Domain (for --to=domain; e.g. example.com)"`
577586
Role string `name:"role" help:"Permission: reader|writer" default:"reader"`
578587
Discoverable bool `name:"discoverable" help:"Allow file discovery in search (anyone/domain only)"`
579588
}
@@ -589,14 +598,58 @@ func (c *DriveShareCmd) Run(ctx context.Context, flags *RootFlags) error {
589598
return usage("empty fileId")
590599
}
591600

592-
if !c.Anyone && strings.TrimSpace(c.Email) == "" {
593-
return usage("must specify --anyone or --email")
601+
to := strings.TrimSpace(c.To)
602+
email := strings.TrimSpace(c.Email)
603+
domain := strings.TrimSpace(c.Domain)
604+
605+
// Back-compat: allow legacy target flags without --to, but keep it unambiguous.
606+
// New UX: prefer explicit --to + matching parameter.
607+
if to == "" {
608+
switch {
609+
case c.Anyone && email == "" && domain == "":
610+
to = driveShareToAnyone
611+
case !c.Anyone && email != "" && domain == "":
612+
to = driveShareToUser
613+
case !c.Anyone && email == "" && domain != "":
614+
to = driveShareToDomain
615+
case !c.Anyone && email == "" && domain == "":
616+
return usage("must specify --to (anyone|user|domain)")
617+
default:
618+
return usage("ambiguous share target (use --to=anyone|user|domain)")
619+
}
620+
}
621+
622+
switch to {
623+
case driveShareToAnyone:
624+
if email != "" || domain != "" {
625+
return usage("--to=anyone cannot be combined with --email or --domain")
626+
}
627+
case driveShareToUser:
628+
if email == "" {
629+
return usage("missing --email for --to=user")
630+
}
631+
if domain != "" || c.Anyone {
632+
return usage("--to=user cannot be combined with --anyone or --domain")
633+
}
634+
if c.Discoverable {
635+
return usage("--discoverable is only valid for --to=anyone or --to=domain")
636+
}
637+
case driveShareToDomain:
638+
if domain == "" {
639+
return usage("missing --domain for --to=domain")
640+
}
641+
if email != "" || c.Anyone {
642+
return usage("--to=domain cannot be combined with --anyone or --email")
643+
}
644+
default:
645+
// Should be guarded by enum, but keep a friendly message for future changes.
646+
return usage("invalid --to (expected anyone|user|domain)")
594647
}
595648
role := strings.TrimSpace(c.Role)
596649
if role == "" {
597-
role = "reader"
650+
role = drivePermRoleReader
598651
}
599-
if role != "reader" && role != "writer" {
652+
if role != drivePermRoleReader && role != drivePermRoleWriter {
600653
return usage("invalid --role (expected reader|writer)")
601654
}
602655

@@ -606,18 +659,23 @@ func (c *DriveShareCmd) Run(ctx context.Context, flags *RootFlags) error {
606659
}
607660

608661
perm := &drive.Permission{Role: role}
609-
if c.Anyone {
662+
switch to {
663+
case driveShareToAnyone:
610664
perm.Type = "anyone"
611665
perm.AllowFileDiscovery = c.Discoverable
612-
} else {
666+
case driveShareToDomain:
667+
perm.Type = "domain"
668+
perm.Domain = domain
669+
perm.AllowFileDiscovery = c.Discoverable
670+
default:
613671
perm.Type = "user"
614-
perm.EmailAddress = strings.TrimSpace(c.Email)
672+
perm.EmailAddress = email
615673
}
616674

617675
created, err := svc.Permissions.Create(fileID, perm).
618676
SupportsAllDrives(true).
619677
SendNotificationEmail(false).
620-
Fields("id, type, role, emailAddress").
678+
Fields("id, type, role, emailAddress, domain, allowFileDiscovery").
621679
Context(ctx).
622680
Do()
623681
if err != nil {
@@ -713,7 +771,7 @@ func (c *DrivePermissionsCmd) Run(ctx context.Context, flags *RootFlags) error {
713771

714772
call := svc.Permissions.List(fileID).
715773
SupportsAllDrives(true).
716-
Fields("nextPageToken, permissions(id, type, role, emailAddress)").
774+
Fields("nextPageToken, permissions(id, type, role, emailAddress, domain)").
717775
Context(ctx)
718776
if c.Max > 0 {
719777
call = call.PageSize(c.Max)
@@ -744,6 +802,9 @@ func (c *DrivePermissionsCmd) Run(ctx context.Context, flags *RootFlags) error {
744802
fmt.Fprintln(w, "ID\tTYPE\tROLE\tEMAIL")
745803
for _, p := range resp.Permissions {
746804
email := p.EmailAddress
805+
if email == "" && p.Domain != "" {
806+
email = p.Domain
807+
}
747808
if email == "" {
748809
email = "-"
749810
}

internal/cmd/drive_commands_more_test.go

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,63 @@ func TestDriveCommands_MoreCoverage(t *testing.T) {
107107
w.WriteHeader(http.StatusNoContent)
108108
return
109109
case r.Method == http.MethodPost && strings.HasSuffix(path, "/permissions"):
110-
_ = json.NewEncoder(w).Encode(map[string]any{
111-
"id": "perm1",
112-
"type": "user",
113-
"role": "reader",
114-
"emailAddress": "share@example.com",
115-
})
116-
return
110+
var req map[string]any
111+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
112+
http.Error(w, "bad json", http.StatusBadRequest)
113+
return
114+
}
115+
typ, _ := req["type"].(string)
116+
role, _ := req["role"].(string)
117+
if role == "" {
118+
role = "reader"
119+
}
120+
121+
switch typ {
122+
case "user":
123+
email, _ := req["emailAddress"].(string)
124+
if email == "" {
125+
http.Error(w, "missing emailAddress", http.StatusBadRequest)
126+
return
127+
}
128+
_ = json.NewEncoder(w).Encode(map[string]any{
129+
"id": "perm1",
130+
"type": "user",
131+
"role": role,
132+
"emailAddress": email,
133+
})
134+
return
135+
case "domain":
136+
domain, _ := req["domain"].(string)
137+
if domain == "" {
138+
http.Error(w, "missing domain", http.StatusBadRequest)
139+
return
140+
}
141+
resp := map[string]any{
142+
"id": "perm1",
143+
"type": "domain",
144+
"role": role,
145+
"domain": domain,
146+
}
147+
if afd, ok := req["allowFileDiscovery"].(bool); ok {
148+
resp["allowFileDiscovery"] = afd
149+
}
150+
_ = json.NewEncoder(w).Encode(resp)
151+
return
152+
case "anyone":
153+
resp := map[string]any{
154+
"id": "perm1",
155+
"type": "anyone",
156+
"role": role,
157+
}
158+
if afd, ok := req["allowFileDiscovery"].(bool); ok {
159+
resp["allowFileDiscovery"] = afd
160+
}
161+
_ = json.NewEncoder(w).Encode(resp)
162+
return
163+
default:
164+
http.Error(w, "invalid type", http.StatusBadRequest)
165+
return
166+
}
117167
case r.Method == http.MethodDelete && strings.Contains(path, "/permissions/"):
118168
w.WriteHeader(http.StatusNoContent)
119169
return
@@ -191,11 +241,16 @@ func TestDriveCommands_MoreCoverage(t *testing.T) {
191241
t.Fatalf("unexpected rename output: %q", out)
192242
}
193243

194-
out = run("--json", "--account", "a@b.com", "drive", "share", "file1", "--email", "share@example.com")
244+
out = run("--json", "--account", "a@b.com", "drive", "share", "file1", "--to", "user", "--email", "share@example.com")
195245
if !strings.Contains(out, "\"permissionId\"") {
196246
t.Fatalf("unexpected share json: %q", out)
197247
}
198248

249+
out = run("--json", "--account", "a@b.com", "drive", "share", "file1", "--to", "domain", "--domain", "example.com", "--role", "writer")
250+
if !strings.Contains(out, "\"permissionId\"") {
251+
t.Fatalf("unexpected domain share json: %q", out)
252+
}
253+
199254
out = run("--force", "--account", "a@b.com", "drive", "unshare", "file1", "perm1")
200255
if !strings.Contains(out, "removed") {
201256
t.Fatalf("unexpected unshare output: %q", out)

internal/cmd/drive_errors_test.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,39 @@ func TestDriveCommand_ValidationErrors(t *testing.T) {
1515
}
1616

1717
shareCmd := &DriveShareCmd{}
18-
if err := runKong(t, shareCmd, []string{"file1"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "must specify") {
19-
t.Fatalf("expected share validation error, got %v", err)
18+
if err := runKong(t, shareCmd, []string{"file1"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "must specify --to") {
19+
t.Fatalf("expected share target error, got %v", err)
2020
}
2121

2222
shareCmd = &DriveShareCmd{}
23-
if err := runKong(t, shareCmd, []string{"file1", "--anyone", "--role", "owner"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "invalid --role") {
23+
if err := runKong(t, shareCmd, []string{"file1", "--to", "domain", "--domain", "example.com", "--role", "owner"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "invalid --role") {
24+
t.Fatalf("expected role error for domain share, got %v", err)
25+
}
26+
27+
shareCmd = &DriveShareCmd{}
28+
if err := runKong(t, shareCmd, []string{"file1", "--to", "anyone", "--role", "owner"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "invalid --role") {
2429
t.Fatalf("expected role error, got %v", err)
2530
}
31+
32+
shareCmd = &DriveShareCmd{}
33+
if err := runKong(t, shareCmd, []string{"file1", "--to", "user"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "missing --email") {
34+
t.Fatalf("expected missing email error, got %v", err)
35+
}
36+
37+
shareCmd = &DriveShareCmd{}
38+
if err := runKong(t, shareCmd, []string{"file1", "--to", "domain"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "missing --domain") {
39+
t.Fatalf("expected missing domain error, got %v", err)
40+
}
41+
42+
shareCmd = &DriveShareCmd{}
43+
if err := runKong(t, shareCmd, []string{"file1", "--to", "user", "--email", "a@b.com", "--discoverable"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "discoverable") {
44+
t.Fatalf("expected discoverable error, got %v", err)
45+
}
46+
47+
shareCmd = &DriveShareCmd{}
48+
if err := runKong(t, shareCmd, []string{"file1", "--email", "a@b.com", "--domain", "example.com"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "ambiguous") {
49+
t.Fatalf("expected ambiguous target error, got %v", err)
50+
}
2651
}
2752

2853
func TestDriveDeleteUnshare_NoInput(t *testing.T) {

0 commit comments

Comments
 (0)