Skip to content

feat(contacts): add group management and starring operations #101

@rianjs

Description

@rianjs

Context

Following the Gmail organizational operations (#98), this adds non-destructive organizational operations for Google Contacts: managing contact group memberships and starring contacts. Reuses the internal/bulk/ package and --ids pattern established in #98.

Depends on #98 for: internal/bulk/ package, architecture test allowlist, scope migration infrastructure, --ids flag pattern.


Scope Change

internal/auth/auth.go: Replace people.ContactsReadonlyScope with people.ContactsScope (full contacts read/write). The architecture test allowlist (from #98) already permits this scope.

New API Client Methods

internal/contacts/client.go:

func (c *Client) AddToGroup(ctx context.Context, groupResourceName string, contactResourceNames []string) error
func (c *Client) RemoveFromGroup(ctx context.Context, groupResourceName string, contactResourceNames []string) error
func (c *Client) ResolveGroupName(ctx context.Context, name string) (string, error)
func (c *Client) SearchContactIDs(ctx context.Context, query string, pageSize int64) ([]string, error)

Group membership uses c.service.ContactGroups.Members.Modify().

Starring contacts: Implemented by adding to/removing from the system group contactGroups/starred — so star/unstar reuse AddToGroup/RemoveFromGroup with that resource name.

ResolveGroupName maps user-friendly group names to resource names (e.g., "Friends" → contactGroups/abc123).

New Interface Methods

Add to ContactsClient in internal/cmd/contacts/output.go:

AddToGroup(ctx context.Context, groupResourceName string, contactResourceNames []string) error
RemoveFromGroup(ctx context.Context, groupResourceName string, contactResourceNames []string) error
ResolveGroupName(ctx context.Context, name string) (string, error)
SearchContactIDs(ctx context.Context, query string, pageSize int64) ([]string, error)

New Commands

Command Description
gro contacts add-to-group <group-name> <contact-ids...> Add contacts to a group
gro contacts remove-from-group <group-name> <contact-ids...> Remove contacts from a group
gro contacts star <contact-ids...> Star contacts (add to system "Starred" group)
gro contacts unstar <contact-ids...> Unstar contacts

All support bulk input modes (--stdin, --query, positional args), --json, --dry-run. Reuses internal/bulk/.

Also add --ids flag to gro contacts list and gro contacts search for piping:

gro contacts search "John" --ids | gro contacts add-to-group "Friends" --stdin
gro contacts list --max 10 --ids | gro contacts star --stdin --dry-run

Files to Create

File Purpose
internal/cmd/contacts/group_manage.go Add-to-group / remove-from-group commands
internal/cmd/contacts/group_manage_test.go Tests
internal/cmd/contacts/star.go Star/unstar commands
internal/cmd/contacts/star_test.go Tests

Files to Modify

File Change
internal/auth/auth.go Replace contacts readonly scope with contacts scope
internal/auth/auth_test.go Update scope count/assertions
internal/architecture/architecture_test.go Update allowlist (already present from #98)
internal/contacts/client.go Add AddToGroup, RemoveFromGroup, ResolveGroupName, SearchContactIDs methods
internal/cmd/contacts/output.go Extend ContactsClient interface
internal/cmd/contacts/mock_test.go Add mock function fields
internal/cmd/contacts/contacts.go Register new subcommands
internal/cmd/contacts/list.go Add --ids flag
internal/cmd/contacts/search.go Add --ids flag
integration-tests.md Add contacts organizational test section
README.md Add contacts organizational operations examples

Integration Tests

Group Management

Test Case Command Expected
Add to group CONTACT=$(gro ppl list --max 1 --ids); gro contacts add-to-group "Friends" "$CONTACT" "Added 1 contact(s) to group 'Friends'."
Add via pipe gro ppl search "John" --ids | gro contacts add-to-group "Work" --stdin "Added N contact(s) to group 'Work'."
Remove from group gro contacts remove-from-group "Friends" "$CONTACT" "Removed 1 contact(s) from group 'Friends'."
Dry run gro contacts add-to-group "Friends" --query "John" --dry-run "[dry-run] Would add N contact(s) to group 'Friends'."
Invalid group gro contacts add-to-group "NonexistentGroup12345" "$CONTACT" Error: group not found
JSON output gro contacts add-to-group "Friends" "$CONTACT" --json Valid JSON result

Starring

Test Case Command Expected
Star contact CONTACT=$(gro ppl list --max 1 --ids); gro contacts star "$CONTACT" "Starred 1 contact(s)."
Star via pipe gro ppl search "John" --ids | gro contacts star --stdin "Starred N contact(s)."
Unstar gro contacts unstar "$CONTACT" "Unstarred 1 contact(s)."
Dry run gro contacts star --query "John" --dry-run "[dry-run] Would star N contact(s)."

--ids Flag

Test Case Command Expected
List --ids gro ppl list --max 3 --ids 3 bare resource names, one per line
Search --ids gro ppl search "test" --ids Bare resource names, one per line
--ids and --json exclusive gro ppl list --ids --json Error: mutually exclusive

Key Risks

  • Contacts starring mechanism — verify that contactGroups/starred is the correct system group resource name for starring contacts via the People API.
  • Group member modification API — the People API's group member modification may have batch limits similar to Gmail's BatchModify. Check and implement chunking if needed.
  • people.ContactsScope breadth — this is a broad scope (full read/write). The architecture test allowlist and forbidden-methods test provide defense-in-depth against accidental destructive operations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions