@@ -383,7 +383,9 @@ func (c *CalendarUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *
383383 return usage ("--original-start required when --scope=single" )
384384 }
385385 case scopeFuture :
386- return fmt .Errorf ("scope=future is not supported yet" )
386+ if strings .TrimSpace (c .OriginalStartTime ) == "" {
387+ return usage ("--original-start required when --scope=future" )
388+ }
387389 case scopeAll :
388390 default :
389391 return fmt .Errorf ("invalid scope: %q (must be single, future, or all)" , scope )
@@ -481,7 +483,19 @@ func (c *CalendarUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *
481483 }
482484
483485 targetEventID := eventID
484- if scope == scopeSingle {
486+ var parentRecurrence []string
487+ if scope == scopeFuture {
488+ parent , getErr := svc .Events .Get (calendarID , eventID ).Context (ctx ).Do ()
489+ if getErr != nil {
490+ return getErr
491+ }
492+ if len (parent .Recurrence ) == 0 {
493+ return fmt .Errorf ("event %s is not a recurring event" , eventID )
494+ }
495+ parentRecurrence = parent .Recurrence
496+ patch .Recurrence = parentRecurrence
497+ }
498+ if scope == scopeSingle || scope == scopeFuture {
485499 instanceID , resolveErr := resolveRecurringInstanceID (ctx , svc , calendarID , eventID , c .OriginalStartTime )
486500 if resolveErr != nil {
487501 return resolveErr
@@ -493,6 +507,16 @@ func (c *CalendarUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *
493507 if err != nil {
494508 return err
495509 }
510+ if scope == scopeFuture {
511+ truncated , truncateErr := truncateRecurrence (parentRecurrence , c .OriginalStartTime )
512+ if truncateErr != nil {
513+ return truncateErr
514+ }
515+ _ , patchErr := svc .Events .Patch (calendarID , eventID , & calendar.Event {Recurrence : truncated }).Context (ctx ).Do ()
516+ if patchErr != nil {
517+ return patchErr
518+ }
519+ }
496520 if outfmt .IsJSON (ctx ) {
497521 return outfmt .WriteJSON (os .Stdout , map [string ]any {"event" : updated })
498522 }
@@ -532,7 +556,9 @@ func (c *CalendarDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
532556 return usage ("--original-start required when --scope=single" )
533557 }
534558 case scopeFuture :
535- return fmt .Errorf ("scope=future is not supported yet" )
559+ if strings .TrimSpace (c .OriginalStartTime ) == "" {
560+ return usage ("--original-start required when --scope=future" )
561+ }
536562 case scopeAll :
537563 default :
538564 return fmt .Errorf ("invalid scope: %q (must be single, future, or all)" , scope )
@@ -542,6 +568,9 @@ func (c *CalendarDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
542568 if scope == scopeSingle {
543569 confirmMessage = fmt .Sprintf ("delete event %s (instance start %s) from calendar %s" , eventID , c .OriginalStartTime , calendarID )
544570 }
571+ if scope == scopeFuture {
572+ confirmMessage = fmt .Sprintf ("delete event %s (instance start %s) and all following from calendar %s" , eventID , c .OriginalStartTime , calendarID )
573+ }
545574 if confirmErr := confirmDestructive (ctx , flags , confirmMessage ); confirmErr != nil {
546575 return confirmErr
547576 }
@@ -552,7 +581,18 @@ func (c *CalendarDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
552581 }
553582
554583 targetEventID := eventID
555- if scope == scopeSingle {
584+ var parentRecurrence []string
585+ if scope == scopeFuture {
586+ parent , getErr := svc .Events .Get (calendarID , eventID ).Context (ctx ).Do ()
587+ if getErr != nil {
588+ return getErr
589+ }
590+ if len (parent .Recurrence ) == 0 {
591+ return fmt .Errorf ("event %s is not a recurring event" , eventID )
592+ }
593+ parentRecurrence = parent .Recurrence
594+ }
595+ if scope == scopeSingle || scope == scopeFuture {
556596 instanceID , resolveErr := resolveRecurringInstanceID (ctx , svc , calendarID , eventID , c .OriginalStartTime )
557597 if resolveErr != nil {
558598 return resolveErr
@@ -563,6 +603,16 @@ func (c *CalendarDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
563603 if err := svc .Events .Delete (calendarID , targetEventID ).Do (); err != nil {
564604 return err
565605 }
606+ if scope == scopeFuture {
607+ truncated , truncateErr := truncateRecurrence (parentRecurrence , c .OriginalStartTime )
608+ if truncateErr != nil {
609+ return truncateErr
610+ }
611+ _ , patchErr := svc .Events .Patch (calendarID , eventID , & calendar.Event {Recurrence : truncated }).Context (ctx ).Do ()
612+ if patchErr != nil {
613+ return patchErr
614+ }
615+ }
566616 if outfmt .IsJSON (ctx ) {
567617 return outfmt .WriteJSON (os .Stdout , map [string ]any {
568618 "deleted" : true ,
@@ -1010,6 +1060,76 @@ func originalStartRange(originalStart string) (string, string, error) {
10101060 return parsed .Format (time .RFC3339 ), parsed .Add (24 * time .Hour ).Format (time .RFC3339 ), nil
10111061}
10121062
1063+ func truncateRecurrence (rules []string , originalStart string ) ([]string , error ) {
1064+ if len (rules ) == 0 {
1065+ return nil , fmt .Errorf ("recurrence rules missing" )
1066+ }
1067+ untilValue , err := recurrenceUntil (originalStart )
1068+ if err != nil {
1069+ return nil , err
1070+ }
1071+
1072+ updated := make ([]string , 0 , len (rules ))
1073+ foundRule := false
1074+ for _ , rule := range rules {
1075+ trimmed := strings .TrimSpace (rule )
1076+ upper := strings .ToUpper (trimmed )
1077+ if ! strings .HasPrefix (upper , "RRULE" ) {
1078+ updated = append (updated , trimmed )
1079+ continue
1080+ }
1081+ foundRule = true
1082+ body := strings .TrimPrefix (trimmed , "RRULE:" )
1083+ if body == trimmed {
1084+ body = strings .TrimPrefix (trimmed , "RRULE" )
1085+ body = strings .TrimPrefix (body , ":" )
1086+ }
1087+ parts := strings .Split (body , ";" )
1088+ filtered := make ([]string , 0 , len (parts )+ 1 )
1089+ for _ , part := range parts {
1090+ part = strings .TrimSpace (part )
1091+ if part == "" {
1092+ continue
1093+ }
1094+ upperPart := strings .ToUpper (part )
1095+ if strings .HasPrefix (upperPart , "UNTIL=" ) || strings .HasPrefix (upperPart , "COUNT=" ) {
1096+ continue
1097+ }
1098+ filtered = append (filtered , part )
1099+ }
1100+ filtered = append (filtered , "UNTIL=" + untilValue )
1101+ updated = append (updated , "RRULE:" + strings .Join (filtered , ";" ))
1102+ }
1103+ if ! foundRule {
1104+ return nil , fmt .Errorf ("recurrence has no RRULE" )
1105+ }
1106+ return updated , nil
1107+ }
1108+
1109+ func recurrenceUntil (originalStart string ) (string , error ) {
1110+ originalStart = strings .TrimSpace (originalStart )
1111+ if originalStart == "" {
1112+ return "" , fmt .Errorf ("original start time required" )
1113+ }
1114+ if strings .Contains (originalStart , "T" ) {
1115+ parsed , err := time .Parse (time .RFC3339 , originalStart )
1116+ if err != nil {
1117+ parsed , err = time .Parse (time .RFC3339Nano , originalStart )
1118+ }
1119+ if err != nil {
1120+ return "" , fmt .Errorf ("invalid original start time %q" , originalStart )
1121+ }
1122+ until := parsed .Add (- time .Second ).UTC ()
1123+ return until .Format ("20060102T150405Z" ), nil
1124+ }
1125+ parsed , err := time .Parse ("2006-01-02" , originalStart )
1126+ if err != nil {
1127+ return "" , fmt .Errorf ("invalid original start date %q" , originalStart )
1128+ }
1129+ until := parsed .AddDate (0 , 0 , - 1 )
1130+ return until .Format ("20060102" ), nil
1131+ }
1132+
10131133func buildAttendees (csv string ) []* calendar.EventAttendee {
10141134 addrs := splitCSV (csv )
10151135 if len (addrs ) == 0 {
0 commit comments