mysticals 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +753 -143
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { createElement } from "react";
5
5
  import { render } from "ink";
6
6
 
7
7
  // package.json
8
- var version = "0.3.0";
8
+ var version = "0.4.0";
9
9
 
10
10
  // src/client.ts
11
11
  import { spawn } from "child_process";
@@ -53,6 +53,7 @@ var methodSet = {
53
53
  "events.update": true,
54
54
  "events.delete": true,
55
55
  "events.respond": true,
56
+ "events.recurrence": true,
56
57
  "sync.now": true
57
58
  };
58
59
  var METHODS = Object.keys(methodSet);
@@ -474,6 +475,15 @@ var color = z.string().regex(/^#[0-9a-fA-F]{6}$/);
474
475
  var text = (max2) => z.string().max(max2);
475
476
  var ordered = (v) => Date.parse(v.end) >= Date.parse(v.start);
476
477
  var Range = z.object({ start: isoDateOrTime, end: isoDateOrTime }).refine(ordered, "end before start");
478
+ var Recurrence = z.object({
479
+ freq: z.enum(["daily", "weekly", "monthly", "yearly"]),
480
+ interval: z.number().int().min(1).max(999).optional(),
481
+ byDay: z.array(z.enum(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])).max(7).optional(),
482
+ until: z.iso.date().optional(),
483
+ count: z.number().int().min(1).max(999).optional(),
484
+ // An RRULE value kept from the provider: one line of plain RRULE parts only.
485
+ rule: z.string().max(1e3).regex(/^[A-Z0-9=;,+-]+$/i).optional()
486
+ }).strict();
477
487
  var NewEvent = z.object({
478
488
  accountId: id,
479
489
  calendarId: id,
@@ -483,7 +493,8 @@ var NewEvent = z.object({
483
493
  allDay: z.boolean(),
484
494
  location: text(1e3).optional(),
485
495
  description: text(2e4).optional(),
486
- attendees: z.array(z.email()).max(500).optional()
496
+ attendees: z.array(z.email()).max(500).optional(),
497
+ recurrence: Recurrence.optional()
487
498
  }).refine(ordered, "end before start");
488
499
  var Attendee = z.object({
489
500
  email: text(320).min(1),
@@ -500,7 +511,9 @@ var EventEdit = EventRef.extend({
500
511
  allDay: z.boolean(),
501
512
  location: text(1e3).optional(),
502
513
  description: text(2e4).optional(),
503
- attendees: z.array(Attendee).max(500)
514
+ attendees: z.array(Attendee).max(500),
515
+ // Absent keeps the series' rule; null stops repeating.
516
+ recurrence: Recurrence.nullish()
504
517
  }).refine(ordered, "end before start");
505
518
  var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
506
519
  var CaldavInput = z.object({
@@ -516,7 +529,7 @@ var CaldavInput = z.object({
516
529
  });
517
530
  var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
518
531
  var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
519
- var Scope = z.enum(["one", "following", "all"]).default("one");
532
+ var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
520
533
  function createApi(store, sync, deps) {
521
534
  const account = (accountId) => {
522
535
  const a = store.get(id.parse(accountId));
@@ -612,12 +625,19 @@ function createApi(store, sync, deps) {
612
625
  syncOne(a.id);
613
626
  return ev;
614
627
  },
615
- update: async (raw) => {
628
+ update: async (raw, scope) => {
616
629
  const edit = EventEdit.parse(raw);
617
630
  const cached2 = writable(cachedEvent(edit));
618
- const { title: title2, start, end, allDay, location, description, attendees } = edit;
619
- const ev = await store.getProvider(cached2.accountId).updateEvent({ ...cached2, title: title2, start, end, allDay, location, description, attendees });
620
- upsert(ev, cached2.accountId, cached2.calendarId);
631
+ const how = cached2.recurringEventId ? Scope.parse(scope) : "one";
632
+ const { title: title2, start, end, allDay, location, description, attendees, recurrence } = edit;
633
+ if (recurrence !== void 0 && cached2.recurringEventId && how === "one")
634
+ throw new Error("The repeat rule belongs to the series: choose \u201CThis and following\u201D or \u201CAll events\u201D");
635
+ const ev = await store.getProvider(cached2.accountId).updateEvent({ ...cached2, title: title2, start, end, allDay, location, description, attendees, ...recurrence !== void 0 ? { recurrence } : {} }, how);
636
+ if (how === "one") upsert(ev, cached2.accountId, cached2.calendarId);
637
+ else {
638
+ const own = { ...ev, accountId: cached2.accountId, calendarId: cached2.calendarId };
639
+ applyLocal(cached2.accountId, (events) => [...events.filter((e) => e.id !== own.id && !deletedBy(cached2, how, e)), own]);
640
+ }
621
641
  syncOne(cached2.accountId);
622
642
  return ev;
623
643
  },
@@ -628,6 +648,10 @@ function createApi(store, sync, deps) {
628
648
  applyLocal(cached2.accountId, (events) => events.filter((e) => !deletedBy(cached2, how, e)));
629
649
  syncOne(cached2.accountId);
630
650
  },
651
+ recurrence: async (raw) => {
652
+ const cached2 = cachedEvent(EventRef.parse(raw));
653
+ return store.getProvider(cached2.accountId).getRecurrence(cached2);
654
+ },
631
655
  respond: async (raw, status) => {
632
656
  const cached2 = cachedEvent(EventRef.parse(raw));
633
657
  const ev = await store.getProvider(cached2.accountId).respond(cached2, RsvpStatus.parse(status));
@@ -829,11 +853,171 @@ var AccountStore = class {
829
853
  };
830
854
 
831
855
  // ../../packages/core/src/mock/mockApi.ts
832
- import { addDays, format as format2 } from "date-fns";
856
+ import { addDays as addDays2, format as format4 } from "date-fns";
833
857
 
834
858
  // ../../packages/core/src/mock/MockProvider.ts
835
859
  import { randomUUID as randomUUID3 } from "crypto";
836
- import { parseISO as parseISO3 } from "date-fns";
860
+ import { addDays, differenceInCalendarDays, differenceInCalendarMonths, format as format3, parseISO as parseISO4, startOfWeek } from "date-fns";
861
+
862
+ // ../../packages/core/src/logic/recurrence.ts
863
+ import { format as format2, isValid, parseISO as parseISO3 } from "date-fns";
864
+ var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
865
+ var JS_DAY = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
866
+ var SHORT = { MO: "Mon", TU: "Tue", WE: "Wed", TH: "Thu", FR: "Fri", SA: "Sat", SU: "Sun" };
867
+ var WORKWEEK = ["MO", "TU", "WE", "TH", "FR"];
868
+ var FREQS = ["daily", "weekly", "monthly", "yearly"];
869
+ var UNIT = { daily: "day", weekly: "week", monthly: "month", yearly: "year" };
870
+ var asDate = (start) => start.length === 10 ? parseISO3(start) : new Date(start);
871
+ var weekdayOf = (start) => JS_DAY[asDate(start).getDay()];
872
+ var shortDay = (d) => SHORT[d];
873
+ function normalize(r, start) {
874
+ if (r.rule) return { freq: r.freq, rule: r.rule };
875
+ const out = { freq: r.freq };
876
+ if (r.interval && r.interval > 1) out.interval = r.interval;
877
+ if (r.freq === "weekly" && r.byDay?.length) {
878
+ const days = WEEKDAYS.filter((d) => r.byDay.includes(d));
879
+ if (!(days.length === 1 && days[0] === weekdayOf(start))) out.byDay = days;
880
+ }
881
+ if (r.until) out.until = r.until;
882
+ else if (r.count) out.count = r.count;
883
+ return out;
884
+ }
885
+ function sameRecurrence(a, b, start) {
886
+ if (!a || !b) return a === b;
887
+ return JSON.stringify(normalize(a, start)) === JSON.stringify(normalize(b, start));
888
+ }
889
+ function toRRule(r, allDay) {
890
+ if (r.rule) return r.rule;
891
+ const parts = [`FREQ=${r.freq.toUpperCase()}`];
892
+ if (r.interval && r.interval > 1) parts.push(`INTERVAL=${r.interval}`);
893
+ if (r.freq === "weekly" && r.byDay?.length) parts.push(`BYDAY=${WEEKDAYS.filter((d) => r.byDay.includes(d)).join(",")}`);
894
+ if (r.until) {
895
+ const until = allDay ? r.until.replace(/-/g, "") : (/* @__PURE__ */ new Date(`${r.until}T23:59:59`)).toISOString().replace(/[-:]|\.\d{3}/g, "");
896
+ parts.push(`UNTIL=${until}`);
897
+ } else if (r.count) parts.push(`COUNT=${r.count}`);
898
+ return parts.join(";");
899
+ }
900
+ var EDITABLE = /* @__PURE__ */ new Set(["FREQ", "INTERVAL", "BYDAY", "UNTIL", "COUNT", "WKST"]);
901
+ function fromRRule(text2, start) {
902
+ const rule = text2.replace(/^RRULE:/i, "").trim();
903
+ const kv = new Map(rule.split(";").filter(Boolean).map((p) => {
904
+ const i = p.indexOf("=");
905
+ return [p.slice(0, i).toUpperCase(), p.slice(i + 1).toUpperCase()];
906
+ }));
907
+ const freq = (kv.get("FREQ") ?? "").toLowerCase();
908
+ const custom = () => ({ freq: FREQS.includes(freq) ? freq : "daily", rule });
909
+ if (!FREQS.includes(freq) || [...kv.keys()].some((k) => !EDITABLE.has(k))) return custom();
910
+ const out = { freq };
911
+ const interval = Number(kv.get("INTERVAL") ?? 1);
912
+ if (!Number.isInteger(interval) || interval < 1) return custom();
913
+ if (interval > 1) out.interval = interval;
914
+ if (kv.has("BYDAY")) {
915
+ const days = kv.get("BYDAY").split(",");
916
+ if (freq !== "weekly" || days.some((d) => !WEEKDAYS.includes(d))) return custom();
917
+ out.byDay = WEEKDAYS.filter((d) => days.includes(d));
918
+ }
919
+ const until = kv.get("UNTIL");
920
+ if (until) {
921
+ const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(until);
922
+ if (!m) return custom();
923
+ if (!m[4]) out.until = `${m[1]}-${m[2]}-${m[3]}`;
924
+ else {
925
+ const at3 = m[7] ? new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])) : new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
926
+ const s = start && start.length > 10 ? asDate(start) : void 0;
927
+ const minutes = (d) => d.getHours() * 60 + d.getMinutes();
928
+ if (s && minutes(at3) < minutes(s)) at3.setDate(at3.getDate() - 1);
929
+ out.until = format2(at3, "yyyy-MM-dd");
930
+ }
931
+ } else if (kv.has("COUNT")) {
932
+ const count = Number(kv.get("COUNT"));
933
+ if (!Number.isInteger(count) || count < 1) return custom();
934
+ out.count = count;
935
+ }
936
+ return out;
937
+ }
938
+ function presets(start) {
939
+ const d = asDate(start);
940
+ return [
941
+ { id: "none", label: "Does not repeat", recurrence: null },
942
+ { id: "daily", label: "Every day", recurrence: { freq: "daily" } },
943
+ { id: "weekly", label: `Every week on ${format2(d, "EEEE")}`, recurrence: { freq: "weekly" } },
944
+ { id: "monthly", label: `Every month on day ${d.getDate()}`, recurrence: { freq: "monthly" } },
945
+ { id: "yearly", label: `Every year on ${format2(d, "d MMMM")}`, recurrence: { freq: "yearly" } },
946
+ { id: "weekdays", label: "Every weekday (Mon\u2013Fri)", recurrence: { freq: "weekly", byDay: WORKWEEK } }
947
+ ];
948
+ }
949
+ function presetOf(r, start) {
950
+ if (!r) return "none";
951
+ if (r.rule) return "custom";
952
+ const { until: _u, count: _c, ...base } = normalize(r, start);
953
+ const hit = presets(start).find((p) => p.recurrence && JSON.stringify(normalize(p.recurrence, start)) === JSON.stringify(base));
954
+ return hit?.id ?? "custom";
955
+ }
956
+ var withEnd = (r, from) => from?.until ? { ...r, until: from.until, count: void 0 } : from?.count ? { ...r, count: from.count, until: void 0 } : r;
957
+ function describe(r, start) {
958
+ if (!r) return "Does not repeat";
959
+ if (r.rule) return `Custom rule (${r.rule})`;
960
+ const n = normalize(r, start);
961
+ const d = asDate(start);
962
+ const every = n.interval ? `Every ${n.interval} ${UNIT[n.freq]}s` : `Every ${UNIT[n.freq]}`;
963
+ const on = n.freq === "weekly" ? !n.interval && n.byDay?.join() === WORKWEEK.join() ? "" : ` on ${(n.byDay ?? [weekdayOf(start)]).map(shortDay).join(", ")}` : n.freq === "monthly" ? ` on day ${d.getDate()}` : n.freq === "yearly" ? ` on ${format2(d, "d MMMM")}` : "";
964
+ const head = n.freq === "weekly" && !n.interval && n.byDay?.join() === WORKWEEK.join() ? "Every weekday (Mon\u2013Fri)" : `${every}${on}`;
965
+ const end = n.until ? ` \xB7 until ${format2(parseISO3(n.until), "d MMM yyyy")}` : n.count ? ` \xB7 ${n.count} time${n.count > 1 ? "s" : ""}` : "";
966
+ return head + end;
967
+ }
968
+ var everyText = (r) => `${r.interval ?? 1} ${UNIT[r.freq]}${(r.interval ?? 1) > 1 ? "s" : ""}`;
969
+ function parseEvery(text2) {
970
+ const m = /^\s*(\d+)?\s*(day|week|month|year)s?\s*$/i.exec(text2);
971
+ const n = Number(m?.[1] ?? 1);
972
+ if (!m || n < 1 || n > 999) throw new Error('Repeat every: use e.g. "2 weeks" (day, week, month or year)');
973
+ const freq = FREQS.find((f) => UNIT[f] === m[2].toLowerCase());
974
+ return { freq, interval: n > 1 ? n : void 0 };
975
+ }
976
+ var daysText = (days) => days.map((d) => d.toLowerCase()).join(" ");
977
+ function parseDays(text2) {
978
+ const words = text2.split(/[\s,;]+/).filter(Boolean).map((w) => w.slice(0, 2).toUpperCase());
979
+ const bad = words.filter((w) => !WEEKDAYS.includes(w));
980
+ if (!words.length || bad.length) throw new Error('Repeat on: use days like "mo th"');
981
+ return WEEKDAYS.filter((d) => words.includes(d));
982
+ }
983
+ var endsText = (r) => r.until ? r.until : r.count ? `${r.count} times` : "never";
984
+ function parseEnds(text2) {
985
+ const t = text2.trim().toLowerCase();
986
+ if (!t || t === "never") return {};
987
+ if (/^\d{4}-\d{2}-\d{2}$/.test(t) && isValid(parseISO3(t)) && format2(parseISO3(t), "yyyy-MM-dd") === t) return { until: t };
988
+ const m = /^(\d+)(\s*times?)?$/.exec(t);
989
+ if (m && +m[1] >= 1 && +m[1] <= 999) return { count: +m[1] };
990
+ throw new Error('Ends: use "never", a date (YYYY-MM-DD) or "10 times"');
991
+ }
992
+
993
+ // ../../packages/core/src/mock/MockProvider.ts
994
+ var MAX_INSTANCES = 60;
995
+ function expand(first, r, series) {
996
+ const start = parseISO4(first.start);
997
+ const length = parseISO4(first.end).getTime() - start.getTime();
998
+ const every = r.interval ?? 1;
999
+ const days = r.byDay?.length ? r.byDay : [weekdayOf(first.start)];
1000
+ const until = r.until ? parseISO4(r.until) : void 0;
1001
+ const hits = (d) => {
1002
+ const n = differenceInCalendarDays(d, start);
1003
+ if (r.freq === "daily") return n % every === 0;
1004
+ if (r.freq === "weekly")
1005
+ return days.includes(weekdayOf(format3(d, "yyyy-MM-dd"))) && differenceInCalendarDays(startOfWeek(d, { weekStartsOn: 1 }), startOfWeek(start, { weekStartsOn: 1 })) / 7 % every === 0;
1006
+ const months = differenceInCalendarMonths(d, start);
1007
+ if (d.getDate() !== start.getDate()) return false;
1008
+ return r.freq === "monthly" ? months % every === 0 : months % (12 * every) === 0;
1009
+ };
1010
+ const out = [];
1011
+ for (let n = 0; n < 3 * 366 && out.length < (r.count ?? MAX_INSTANCES) && out.length < MAX_INSTANCES; n++) {
1012
+ const d = addDays(start, n);
1013
+ if (until && differenceInCalendarDays(d, until) > 0) break;
1014
+ if (!hits(d)) continue;
1015
+ const s = first.allDay ? format3(d, "yyyy-MM-dd") : d.toISOString();
1016
+ const e = first.allDay ? format3(addDays(parseISO4(first.end), n), "yyyy-MM-dd") : new Date(d.getTime() + length).toISOString();
1017
+ out.push({ ...first, id: `${series}@${s}`, start: s, end: e, recurringEventId: series, recurrence: void 0 });
1018
+ }
1019
+ return out;
1020
+ }
837
1021
  var MockProvider = class {
838
1022
  constructor(accountId, email, calendars) {
839
1023
  this.accountId = accountId;
@@ -844,11 +1028,13 @@ var MockProvider = class {
844
1028
  email;
845
1029
  calendars;
846
1030
  events = [];
1031
+ /** Series id -> its rule. */
1032
+ rules = /* @__PURE__ */ new Map();
847
1033
  async listCalendars() {
848
1034
  return structuredClone(this.calendars);
849
1035
  }
850
1036
  async listEvents(calendarId, range) {
851
- const t = (iso2) => parseISO3(iso2).getTime();
1037
+ const t = (iso2) => parseISO4(iso2).getTime();
852
1038
  return structuredClone(
853
1039
  this.events.filter((e) => e.calendarId === calendarId && t(e.end) > t(range.start) && t(e.start) < t(range.end))
854
1040
  );
@@ -867,12 +1053,45 @@ var MockProvider = class {
867
1053
  organizer: { email: this.email },
868
1054
  attendees: (input.attendees ?? []).map((email) => ({ email, status: "needsAction" }))
869
1055
  };
1056
+ if (input.recurrence) return this.startSeries(ev, input.recurrence, ev.id);
870
1057
  this.events.push(ev);
871
1058
  return structuredClone(ev);
872
1059
  }
873
- async updateEvent(event) {
1060
+ /** Add the series `first` starts under `r` (or `first` alone for null); returns its first event. */
1061
+ startSeries(first, r, series) {
1062
+ const evs = r ? expand(first, r, series) : [{ ...first, recurringEventId: void 0, recurrence: void 0 }];
1063
+ if (r) this.rules.set(series, r);
1064
+ this.events.push(...evs);
1065
+ return structuredClone(evs[0]);
1066
+ }
1067
+ async getRecurrence(event) {
1068
+ return event.recurringEventId && this.rules.get(event.recurringEventId) || null;
1069
+ }
1070
+ async updateEvent(event, scope = "one") {
874
1071
  const i = this.events.findIndex((e) => e.id === event.id);
875
1072
  if (i < 0) throw new Error("not found");
1073
+ const old = this.events[i];
1074
+ const series = old.recurringEventId;
1075
+ if (event.recurrence !== void 0 && (!series || scope !== "one")) {
1076
+ const own = (e) => e.id === old.id || !!series && e.recurringEventId === series && (scope === "all" || e.start >= old.start);
1077
+ const firstOld = series && scope === "all" ? this.events.filter(own).sort((a, b) => a.start.localeCompare(b.start))[0] : old;
1078
+ this.events = this.events.filter((e) => !own(e));
1079
+ const shift = Date.parse(event.start) - Date.parse(old.start);
1080
+ const anchor = event.recurrence && firstOld !== old && !event.allDay ? { start: new Date(Date.parse(firstOld.start) + shift).toISOString(), end: new Date(Date.parse(firstOld.start) + shift + Date.parse(event.end) - Date.parse(event.start)).toISOString() } : {};
1081
+ const id2 = scope === "all" && series ? series : randomUUID3();
1082
+ return this.startSeries({ ...event, ...anchor, accountId: this.accountId }, event.recurrence, id2);
1083
+ }
1084
+ if (series && scope !== "one") {
1085
+ const shift = Date.parse(event.start) - Date.parse(old.start);
1086
+ const length = Date.parse(event.end) - Date.parse(event.start);
1087
+ const at3 = (ms) => new Date(ms).toISOString();
1088
+ const { title: title2, location, description, attendees } = event;
1089
+ this.events = this.events.map((e) => {
1090
+ if (e.id === event.id || e.recurringEventId !== series || scope === "following" && e.start < old.start) return e;
1091
+ const moved = old.allDay || event.allDay ? {} : { start: at3(Date.parse(e.start) + shift), end: at3(Date.parse(e.start) + shift + length) };
1092
+ return { ...e, title: title2, location, description, attendees, ...moved };
1093
+ });
1094
+ }
876
1095
  this.events[i] = { ...event, accountId: this.accountId };
877
1096
  return structuredClone(this.events[i]);
878
1097
  }
@@ -942,8 +1161,9 @@ function createMockApi(onChanged) {
942
1161
  for (let d = -2; d <= 4; d++) {
943
1162
  seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
944
1163
  }
1164
+ personal.rules.set("run-series", { freq: "daily", count: 7 });
945
1165
  seed(personal, "p-main", { title: "Dinner with friends", start: iso(2, 20), end: iso(2, 22), location: "Kyiv" });
946
- const day = (offset) => format2(addDays(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
1166
+ const day = (offset) => format4(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
947
1167
  seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
948
1168
  const prov = (id2) => {
949
1169
  const p = providers.get(id2);
@@ -999,8 +1219,8 @@ function createMockApi(onChanged) {
999
1219
  onChanged(input.accountId);
1000
1220
  return ev;
1001
1221
  },
1002
- update: async (ev) => {
1003
- const r = await own(ev.accountId, ev.calendarId).updateEvent(ev);
1222
+ update: async (ev, scope) => {
1223
+ const r = await own(ev.accountId, ev.calendarId).updateEvent(ev, scope);
1004
1224
  onChanged(ev.accountId);
1005
1225
  return r;
1006
1226
  },
@@ -1008,6 +1228,7 @@ function createMockApi(onChanged) {
1008
1228
  await own(ev.accountId, ev.calendarId).deleteEvent(ev, scope);
1009
1229
  onChanged(ev.accountId);
1010
1230
  },
1231
+ recurrence: async (ev) => own(ev.accountId, ev.calendarId).getRecurrence(ev),
1011
1232
  respond: async (ev, status) => {
1012
1233
  const r = await own(ev.accountId, ev.calendarId).respond(ev, status);
1013
1234
  onChanged(ev.accountId);
@@ -1034,7 +1255,7 @@ import {
1034
1255
 
1035
1256
  // ../../packages/core/src/providers/caldav/ics.ts
1036
1257
  import ICAL from "ical.js";
1037
- var MAX_INSTANCES = 1e5;
1258
+ var MAX_INSTANCES2 = 1e5;
1038
1259
  var PARTSTAT_IN = {
1039
1260
  ACCEPTED: "accepted",
1040
1261
  DECLINED: "declined",
@@ -1132,7 +1353,7 @@ function parseEvents(ics, href, etag, ctx, range) {
1132
1353
  -Infinity,
1133
1354
  ...vevents.filter((v) => v !== master).map((v) => v.getFirstPropertyValue("recurrence-id").toJSDate().getTime())
1134
1355
  );
1135
- for (let i = 0, next = it.next(); next && i < MAX_INSTANCES; i++, next = it.next()) {
1356
+ for (let i = 0, next = it.next(); next && i < MAX_INSTANCES2; i++, next = it.next()) {
1136
1357
  const ms = next.toJSDate().getTime();
1137
1358
  if (ms >= rangeEnd && ms > lastOverride) break;
1138
1359
  const d = event.getOccurrenceDetails(next);
@@ -1155,9 +1376,40 @@ function buildIcs(uid, input, email) {
1155
1376
  setText(v, "location", input.location);
1156
1377
  setText(v, "description", input.description);
1157
1378
  setAttendees(v, input.attendees ?? [], email);
1379
+ if (input.recurrence) setRule(v, input.recurrence, input.allDay);
1158
1380
  root.addSubcomponent(v);
1159
1381
  return root.toString();
1160
1382
  }
1383
+ function setRule(v, r, allDay) {
1384
+ v.removeAllProperties("rrule");
1385
+ v.addPropertyWithValue("rrule", ICAL.Recur.fromString(toRRule(r, allDay)));
1386
+ }
1387
+ function recurrenceOf(raw) {
1388
+ const master = findMaster(parse(raw.ics));
1389
+ const rule = master?.getFirstPropertyValue("rrule");
1390
+ return master && rule ? fromRRule(rule.toString(), toIso(dtstart(master))) : null;
1391
+ }
1392
+ function applyRule(root, master, event) {
1393
+ if (event.recurrence === void 0) return;
1394
+ const overrides = root.getAllSubcomponents("vevent").filter((v) => v !== master);
1395
+ if (event.recurrence === null) {
1396
+ for (const n of ["rrule", "rdate", "exdate"]) master.removeAllProperties(n);
1397
+ for (const o of overrides) root.removeSubcomponent(o);
1398
+ master.addProperty(replaceTime(master, "dtstart", fromIso(event.start, event.allDay)));
1399
+ master.addProperty(replaceTime(master, "dtend", fromIso(event.end, event.allDay)));
1400
+ return;
1401
+ }
1402
+ setRule(master, event.recurrence, event.allDay);
1403
+ const ms = (t) => t.toJSDate().getTime();
1404
+ const rid = (o) => o.getFirstPropertyValue("recurrence-id");
1405
+ const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t) => t instanceof ICAL.Time);
1406
+ const last = Math.max(-Infinity, ...overrides.map((o) => ms(rid(o))), ...exdates.map(ms));
1407
+ const slots2 = /* @__PURE__ */ new Set();
1408
+ const it = master.getFirstPropertyValue("rrule").iterator(dtstart(master));
1409
+ for (let t = it.next(), n = 0; t && ms(t) <= last && n < MAX_INSTANCES2; t = it.next(), n++) slots2.add(ms(t));
1410
+ for (const o of overrides) if (!slots2.has(ms(rid(o)))) root.removeSubcomponent(o);
1411
+ mapDates(master, "exdate", (t) => slots2.has(ms(t)) ? t : null);
1412
+ }
1161
1413
  function setAttendees(v, emails, email) {
1162
1414
  const wanted = [...new Set(emails.map(cleanEmail).filter(Boolean))];
1163
1415
  const existing = new Map(v.getAllProperties("attendee").map((p) => [cleanEmail(p.getFirstValue()), p]));
@@ -1217,9 +1469,14 @@ function applyUpdate(event, email) {
1217
1469
  const raw = event.raw;
1218
1470
  const root = parse(raw.ics);
1219
1471
  const v = targetVevent(root, raw);
1220
- setTime(v, "dtstart", fromIso(event.start, event.allDay));
1472
+ writeEdit(v, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
1473
+ if (event.recurrence && !raw.recurrenceId && !v.hasProperty("rrule")) setRule(v, event.recurrence, event.allDay);
1474
+ return root.toString();
1475
+ }
1476
+ function writeEdit(v, event, email, start, end) {
1477
+ v.addProperty(replaceTime(v, "dtstart", start));
1221
1478
  v.removeAllProperties("duration");
1222
- setTime(v, "dtend", fromIso(event.end, event.allDay));
1479
+ v.addProperty(replaceTime(v, "dtend", end));
1223
1480
  setText(v, "summary", event.title);
1224
1481
  setText(v, "location", event.location);
1225
1482
  setText(v, "description", event.description);
@@ -1228,10 +1485,156 @@ function applyUpdate(event, email) {
1228
1485
  event.attendees.map((a) => a.email),
1229
1486
  email
1230
1487
  );
1488
+ bump(v);
1489
+ }
1490
+ function bump(v) {
1231
1491
  v.updatePropertyWithValue("sequence", Number(v.getFirstPropertyValue("sequence") ?? 0) + 1);
1232
1492
  touch(v);
1493
+ }
1494
+ function timeProp(name, t) {
1495
+ const p = new ICAL.Property(name);
1496
+ p.setValue(t);
1497
+ const tzid = t.zone?.tzid;
1498
+ if (!t.isDate && tzid && tzid !== "UTC" && tzid !== "floating") p.setParameter("tzid", tzid);
1499
+ return p;
1500
+ }
1501
+ function replaceTime(v, name, t) {
1502
+ v.removeAllProperties(name);
1503
+ return timeProp(name, t);
1504
+ }
1505
+ var dtstart = (v) => v.getFirstPropertyValue("dtstart");
1506
+ var dateOf = (t) => ICAL.Time.fromData({ year: t.year, month: t.month, day: t.day, isDate: true });
1507
+ var daysBetween = (a, b) => Math.round(dateOf(b).subtractDate(dateOf(a)).toSeconds() / 86400);
1508
+ function addDays3(t, days) {
1509
+ const x = t.clone();
1510
+ x.adjust(days, 0, 0, 0);
1511
+ return x;
1512
+ }
1513
+ function spanFrom(start, event) {
1514
+ if (event.allDay) return [start, addDays3(start, daysBetween(fromIso(event.start, true), fromIso(event.end, true)))];
1515
+ const end = start.clone();
1516
+ end.adjust(0, 0, 0, (Date.parse(event.end) - Date.parse(event.start)) / 1e3);
1517
+ return [start, end];
1518
+ }
1519
+ function slotShift(master, before, event) {
1520
+ const anchor = dtstart(master);
1521
+ const zone = anchor.isDate ? void 0 : anchor.zone;
1522
+ const local = (t) => zone && !t.isDate ? t.convertToZone(zone) : t.clone();
1523
+ const after = fromIso(event.start, event.allDay);
1524
+ if (event.allDay) {
1525
+ const days2 = daysBetween(local(before), after);
1526
+ return (t) => addDays3(dateOf(local(t)), days2);
1527
+ }
1528
+ if (!anchor.isDate) {
1529
+ const seconds = (zone ? after.convertToZone(zone) : after).subtractDate(local(before)).toSeconds();
1530
+ return (t) => {
1531
+ const x = local(t);
1532
+ x.adjust(0, 0, 0, seconds);
1533
+ return x;
1534
+ };
1535
+ }
1536
+ const days = daysBetween(before, after);
1537
+ return (t) => {
1538
+ const d = addDays3(dateOf(t), days);
1539
+ return ICAL.Time.fromData({ year: d.year, month: d.month, day: d.day, hour: after.hour, minute: after.minute, second: after.second }, ICAL.Timezone.utcTimezone);
1540
+ };
1541
+ }
1542
+ function mapDates(v, name, map) {
1543
+ const props = v.getAllProperties(name);
1544
+ if (!props.length) return;
1545
+ const out = [];
1546
+ for (const p of props) {
1547
+ for (const value of p.getValues()) {
1548
+ if (!(value instanceof ICAL.Time)) {
1549
+ const keep = new ICAL.Property(name);
1550
+ keep.setValue(value);
1551
+ out.push(keep);
1552
+ continue;
1553
+ }
1554
+ const t = map(value);
1555
+ if (t) out.push(timeProp(name, t));
1556
+ }
1557
+ }
1558
+ v.removeAllProperties(name);
1559
+ for (const p of out) v.addProperty(p);
1560
+ }
1561
+ var attendeeKey = (v) => v.getAllProperties("attendee").map((p) => cleanEmail(p.getFirstValue())).sort().join(",");
1562
+ function rewriteSeries(root, master, edited, event, email, shift, anchor) {
1563
+ const shared = ["summary", "location", "description"].map((n) => [n, master.getFirstPropertyValue(n)]);
1564
+ const people = attendeeKey(master);
1565
+ const retyped = dtstart(master).isDate !== event.allDay;
1566
+ const [start, end] = spanFrom(shift(anchor), event);
1567
+ writeEdit(master, event, email, start, end);
1568
+ mapDates(master, "exdate", shift);
1569
+ mapDates(master, "rdate", shift);
1570
+ for (const o of root.getAllSubcomponents("vevent")) {
1571
+ const rid = o.getFirstPropertyValue("recurrence-id");
1572
+ if (!rid) continue;
1573
+ o.addProperty(replaceTime(o, "recurrence-id", shift(rid)));
1574
+ if (o === edited) {
1575
+ writeEdit(o, event, email, fromIso(event.start, event.allDay), fromIso(event.end, event.allDay));
1576
+ continue;
1577
+ }
1578
+ const [s, e] = retyped ? spanFrom(shift(dtstart(o)), event) : [shift(dtstart(o)), shift(new ICAL.Event(o).endDate)];
1579
+ o.addProperty(replaceTime(o, "dtstart", s));
1580
+ o.removeAllProperties("duration");
1581
+ o.addProperty(replaceTime(o, "dtend", e));
1582
+ for (const [name, old] of shared) {
1583
+ if (String(o.getFirstPropertyValue(name) ?? "") === String(old ?? "")) setText(o, name, event[name === "summary" ? "title" : name]);
1584
+ }
1585
+ if (attendeeKey(o) === people) setAttendees(o, event.attendees.map((a) => a.email), email);
1586
+ bump(o);
1587
+ }
1588
+ }
1589
+ var instanceStart = (master, override, rid) => override ? dtstart(override) : ridTime(master, rid).time;
1590
+ function applyUpdateSeries(event, email) {
1591
+ const raw = event.raw;
1592
+ const root = parse(raw.ics);
1593
+ const master = findMaster(root);
1594
+ if (!raw.recurrenceId || !master) return applyUpdate(event, email);
1595
+ const edited = findOverride(root, raw.recurrenceId);
1596
+ const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
1597
+ rewriteSeries(root, master, edited, event, email, shift, dtstart(master));
1598
+ applyRule(root, master, event);
1233
1599
  return root.toString();
1234
1600
  }
1601
+ function applySplitFollowing(event, email, uid) {
1602
+ const raw = event.raw;
1603
+ if (!raw.recurrenceId) throw new Error("Not a recurring instance");
1604
+ const rest = applyDeleteFollowing(raw);
1605
+ if (!rest) return null;
1606
+ const root = parse(raw.ics);
1607
+ const master = masterOf(root);
1608
+ const slot = ridTime(master, raw.recurrenceId).time;
1609
+ const cut = slot.toJSDate().getTime();
1610
+ const edited = findOverride(root, raw.recurrenceId);
1611
+ const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
1612
+ for (const v of root.getAllSubcomponents("vevent")) {
1613
+ const t = v.getFirstPropertyValue("recurrence-id");
1614
+ if (t && t.toJSDate().getTime() < cut) root.removeSubcomponent(v);
1615
+ }
1616
+ const later = (t) => t.toJSDate().getTime() >= cut ? t : null;
1617
+ mapDates(master, "exdate", later);
1618
+ mapDates(master, "rdate", later);
1619
+ const first = dtstart(master);
1620
+ for (const p of event.recurrence === void 0 ? master.getAllProperties("rrule") : []) {
1621
+ const r = p.getFirstValue().clone();
1622
+ if (!r.count) continue;
1623
+ const it = r.iterator(first);
1624
+ let passed = 0;
1625
+ for (let t = it.next(); t && t.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t = it.next()) passed++;
1626
+ r.count = Math.max(1, r.count - passed);
1627
+ p.setValue(r);
1628
+ }
1629
+ for (const v of root.getAllSubcomponents("vevent")) {
1630
+ v.updatePropertyWithValue("uid", uid);
1631
+ v.updatePropertyWithValue("sequence", -1);
1632
+ v.updatePropertyWithValue("created", ICAL.Time.now());
1633
+ }
1634
+ rewriteSeries(root, master, edited, event, email, shift, slot);
1635
+ applyRule(root, master, event);
1636
+ return { rest, next: root.toString() };
1637
+ }
1235
1638
  function applyDeleteInstance(raw) {
1236
1639
  if (!raw.recurrenceId) throw new Error("Not a recurring instance");
1237
1640
  const root = parse(raw.ics);
@@ -1378,7 +1781,8 @@ var createCaldavProvider = (ctx) => {
1378
1781
  if (!obj?.data) throw new Error("Saved event could not be read back from the server");
1379
1782
  const range = { start: new Date(new Date(around.start).getTime() - DAY).toISOString(), end: new Date(new Date(around.end).getTime() + DAY).toISOString() };
1380
1783
  const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
1381
- const ev = id2 === void 0 ? events[0] : events.find((e) => e.id === id2);
1784
+ const at3 = Date.parse(around.start);
1785
+ const ev = id2 === void 0 ? events.find((e) => Date.parse(e.start) === at3) ?? events[0] : events.find((e) => e.id === id2);
1382
1786
  if (!ev) throw new Error("Saved event not found on the server");
1383
1787
  return ev;
1384
1788
  }
@@ -1406,9 +1810,24 @@ var createCaldavProvider = (ctx) => {
1406
1810
  const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
1407
1811
  return reload(calendarId, href, void 0, input);
1408
1812
  },
1409
- async updateEvent(event) {
1410
- await put(event, applyUpdate(event, ctx.email), "Update event");
1411
- return reload(event.calendarId, event.raw.href, event.id, event);
1813
+ async updateEvent(event, scope = "one") {
1814
+ const raw = event.raw;
1815
+ if (!raw.recurrenceId || scope === "one") {
1816
+ await put(event, applyUpdate(event, ctx.email), "Update event");
1817
+ return reload(event.calendarId, raw.href, event.recurrence ? void 0 : event.id, event);
1818
+ }
1819
+ const uid = randomUUID4();
1820
+ const split = scope === "following" ? applySplitFollowing(event, ctx.email, uid) : null;
1821
+ if (!split) {
1822
+ await put(event, applyUpdateSeries(event, ctx.email), "Update event");
1823
+ return reload(event.calendarId, raw.href, void 0, event);
1824
+ }
1825
+ await put(event, split.rest, "Update event");
1826
+ const { headers } = await getConn();
1827
+ const filename = `${uid}.ics`;
1828
+ check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: timedFetch }), "Update event");
1829
+ const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
1830
+ return reload(event.calendarId, href, void 0, event);
1412
1831
  },
1413
1832
  async deleteEvent(event, scope = "one") {
1414
1833
  const raw = event.raw;
@@ -1417,6 +1836,9 @@ var createCaldavProvider = (ctx) => {
1417
1836
  const { headers } = await getConn();
1418
1837
  check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: timedFetch }), "Delete event");
1419
1838
  },
1839
+ async getRecurrence(event) {
1840
+ return recurrenceOf(event.raw);
1841
+ },
1420
1842
  async respond(event, status) {
1421
1843
  await put(event, applyRespond(event.raw, ctx.email, status), "Respond");
1422
1844
  return reload(event.calendarId, event.raw.href, event.id, event);
@@ -1440,7 +1862,7 @@ import { createHash as createHash2, randomBytes } from "crypto";
1440
1862
  import { createServer } from "http";
1441
1863
  var AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
1442
1864
  var TOKEN_URL = "https://oauth2.googleapis.com/token";
1443
- var SCOPES = "openid email https://www.googleapis.com/auth/calendar";
1865
+ var SCOPES = "openid email https://www.googleapis.com/auth/calendar.calendarlist.readonly https://www.googleapis.com/auth/calendar.events";
1444
1866
  var TIMEOUT_MS = 5 * 60 * 1e3;
1445
1867
  var clientConfig = {};
1446
1868
  function setClientConfig(cfg) {
@@ -1607,7 +2029,29 @@ function truncateRecurrence(recurrence, cutoff) {
1607
2029
  return `RRULE:${[...parts, `UNTIL=${until}`].join(";")}`;
1608
2030
  });
1609
2031
  }
2032
+ var DAY2 = 864e5;
2033
+ var plusDays = (date, days) => new Date(Date.parse(date.slice(0, 10)) + days * DAY2).toISOString().slice(0, 10);
2034
+ var daysBetween2 = (a, b) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / DAY2);
2035
+ var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
2036
+ function shiftAnchor(anchor, before, start, allDay, timeZone) {
2037
+ if (allDay) return { date: plusDays(anchor.date ?? anchor.dateTime, daysBetween2(before.date ?? before.dateTime, start)) };
2038
+ if (anchor.dateTime && before.dateTime) {
2039
+ return { dateTime: new Date(Date.parse(anchor.dateTime) + Date.parse(start) - Date.parse(before.dateTime)).toISOString(), timeZone };
2040
+ }
2041
+ const at3 = new Date(start);
2042
+ const day = /* @__PURE__ */ new Date(`${plusDays(anchor.date, daysBetween2(before.date, localDate(at3)))}T00:00:00`);
2043
+ day.setHours(at3.getHours(), at3.getMinutes(), at3.getSeconds());
2044
+ return { dateTime: day.toISOString(), timeZone };
2045
+ }
2046
+ function endFrom(from, event, timeZone) {
2047
+ if (event.allDay) return { date: plusDays(from.date, daysBetween2(event.start, event.end)) };
2048
+ return { dateTime: new Date(Date.parse(from.dateTime) + Date.parse(event.end) - Date.parse(event.start)).toISOString(), timeZone };
2049
+ }
1610
2050
  var toGTime = (iso2, allDay) => allDay ? { date: iso2.slice(0, 10), dateTime: null, timeZone: null } : { dateTime: iso2, date: null, timeZone: null };
2051
+ var zoned = (iso2, allDay, timeZone) => allDay ? { date: iso2.slice(0, 10) } : { dateTime: iso2, timeZone };
2052
+ var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
2053
+ var rruleLines = (r, allDay) => [`RRULE:${toRRule(r, allDay)}`];
2054
+ var nulled = (t) => ({ date: null, dateTime: null, timeZone: null, ...t });
1611
2055
  function createGoogleProviderImpl(ctx) {
1612
2056
  if (ctx.credentials.kind !== "google") throw new Error("Google provider needs Google credentials");
1613
2057
  let creds = ctx.credentials;
@@ -1645,6 +2089,35 @@ function createGoogleProviderImpl(ctx) {
1645
2089
  return !e.organizer || e.organizer.email.toLowerCase() === ctx.email.toLowerCase();
1646
2090
  };
1647
2091
  const map = (g, calendarId) => mapEvent(g, ctx.accountId, calendarId);
2092
+ const fields = (event, prevAttendees, organizer) => {
2093
+ const body = { summary: event.title, location: event.location ?? null, description: event.description ?? null };
2094
+ if (organizer) {
2095
+ const prev = new Map((prevAttendees ?? []).map((a) => [a.email.toLowerCase(), a]));
2096
+ body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
2097
+ }
2098
+ return body;
2099
+ };
2100
+ async function remainingRecurrence(calendarId, series, recurrence, slot) {
2101
+ const rules = recurrence.filter((l) => l.startsWith("RRULE:"));
2102
+ if (!rules.some((l) => /;COUNT=|:COUNT=/i.test(l))) return rules;
2103
+ const cut = Date.parse(slot.dateTime ?? slot.date);
2104
+ let passed = 0;
2105
+ let pageToken;
2106
+ do {
2107
+ const page = await api("GET", `${eventPath(calendarId, series)}/instances`, {
2108
+ showDeleted: "true",
2109
+ timeMax: new Date(cut).toISOString(),
2110
+ maxResults: "2500",
2111
+ ...pageToken ? { pageToken } : {}
2112
+ });
2113
+ for (const g of page.items ?? []) {
2114
+ const t = g.originalStartTime ?? g.start;
2115
+ if (Date.parse(t.dateTime ?? t.date) < cut) passed++;
2116
+ }
2117
+ pageToken = page.nextPageToken;
2118
+ } while (pageToken);
2119
+ return rules.map((l) => l.replace(/COUNT=(\d+)/i, (_, n) => `COUNT=${Math.max(1, Number(n) - passed)}`));
2120
+ }
1648
2121
  return {
1649
2122
  async listCalendars() {
1650
2123
  const out = [];
@@ -1685,36 +2158,67 @@ function createGoogleProviderImpl(ctx) {
1685
2158
  },
1686
2159
  async createEvent(calendarId, input) {
1687
2160
  const attendees = (input.attendees ?? []).map((email) => ({ email }));
2161
+ const rule = input.recurrence;
1688
2162
  const g = await api(
1689
2163
  "POST",
1690
2164
  eventPath(calendarId),
1691
2165
  { sendUpdates: attendees.length ? "all" : "none" },
1692
2166
  {
1693
2167
  summary: input.title,
1694
- start: toGTime(input.start, input.allDay),
1695
- end: toGTime(input.end, input.allDay),
2168
+ start: rule ? zoned(input.start, input.allDay, localZone()) : toGTime(input.start, input.allDay),
2169
+ end: rule ? zoned(input.end, input.allDay, localZone()) : toGTime(input.end, input.allDay),
1696
2170
  location: input.location,
1697
2171
  description: input.description,
1698
- attendees
2172
+ attendees,
2173
+ ...rule ? { recurrence: rruleLines(rule, input.allDay) } : {}
1699
2174
  }
1700
2175
  );
1701
2176
  return map(g, calendarId);
1702
2177
  },
1703
- async updateEvent(event) {
2178
+ async updateEvent(event, scope = "one") {
1704
2179
  const organizer = isOrganizer(event);
1705
- const body = {
1706
- summary: event.title,
1707
- start: toGTime(event.start, event.allDay),
1708
- end: toGTime(event.end, event.allDay),
1709
- location: event.location ?? null,
1710
- description: event.description ?? null
1711
- };
1712
- if (organizer) {
1713
- const prev = new Map((rawOf(event)?.attendees ?? []).map((a) => [a.email.toLowerCase(), a]));
1714
- body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
2180
+ const q = { sendUpdates: organizer ? "all" : "none" };
2181
+ const series = event.recurringEventId;
2182
+ const before = rawOf(event)?.start;
2183
+ if (!series || scope === "one" || !before) {
2184
+ const rule2 = !series ? event.recurrence : void 0;
2185
+ const time = (iso2) => rule2 ? nulled(zoned(iso2, event.allDay, localZone())) : toGTime(iso2, event.allDay);
2186
+ const body = {
2187
+ ...fields(event, rawOf(event)?.attendees, organizer),
2188
+ start: time(event.start),
2189
+ end: time(event.end),
2190
+ ...rule2 ? { recurrence: rruleLines(rule2, event.allDay) } : {}
2191
+ };
2192
+ return map(await api("PATCH", eventPath(event.calendarId, event.id), q, body), event.calendarId);
1715
2193
  }
1716
- const g = await api("PATCH", eventPath(event.calendarId, event.id), { sendUpdates: organizer ? "all" : "none" }, body);
1717
- return map(g, event.calendarId);
2194
+ const master = await api("GET", eventPath(event.calendarId, series));
2195
+ const timeZone = master.start.timeZone ?? localZone();
2196
+ const rule = event.recurrence;
2197
+ const slot = rawOf(event)?.originalStartTime ?? before;
2198
+ const first = master.start.dateTime ?? master.start.date;
2199
+ const at3 = slot.dateTime ?? slot.date;
2200
+ if (scope === "all" || Date.parse(at3) <= Date.parse(first)) {
2201
+ const start2 = rule === null ? zoned(event.start, event.allDay, timeZone) : shiftAnchor(master.start, before, event.start, event.allDay, timeZone);
2202
+ const body = {
2203
+ ...fields(event, master.attendees, organizer),
2204
+ start: nulled(start2),
2205
+ end: nulled(endFrom(start2, event, timeZone)),
2206
+ // A new RRULE replaces the old ones; EXDATE/RDATE lines stay.
2207
+ ...rule === void 0 ? {} : { recurrence: rule ? [...rruleLines(rule, event.allDay), ...(master.recurrence ?? []).filter((l) => !l.startsWith("RRULE:"))] : [] }
2208
+ };
2209
+ await api("PATCH", eventPath(event.calendarId, series), q, body);
2210
+ return event;
2211
+ }
2212
+ const recurrence = rule === void 0 ? await remainingRecurrence(event.calendarId, series, master.recurrence ?? [], slot) : rule ? rruleLines(rule, event.allDay) : void 0;
2213
+ await api("PATCH", eventPath(event.calendarId, series), q, { recurrence: truncateRecurrence(master.recurrence ?? [], slot) });
2214
+ const start = shiftAnchor(slot, before, event.start, event.allDay, timeZone);
2215
+ const created = await api("POST", eventPath(event.calendarId), q, {
2216
+ ...fields(event, master.attendees, organizer),
2217
+ start,
2218
+ end: endFrom(start, event, timeZone),
2219
+ ...recurrence ? { recurrence } : {}
2220
+ });
2221
+ return { ...map(created, event.calendarId), start: event.start, end: event.end, recurringEventId: recurrence ? created.id : void 0 };
1718
2222
  },
1719
2223
  async deleteEvent(event, scope = "one") {
1720
2224
  const q = { sendUpdates: isOrganizer(event) ? "all" : "none" };
@@ -1732,6 +2236,12 @@ function createGoogleProviderImpl(ctx) {
1732
2236
  }
1733
2237
  await api("DELETE", eventPath(event.calendarId, series), q);
1734
2238
  },
2239
+ async getRecurrence(event) {
2240
+ if (!event.recurringEventId) return null;
2241
+ const master = await api("GET", eventPath(event.calendarId, event.recurringEventId));
2242
+ const line = master.recurrence?.find((l) => l.startsWith("RRULE:"));
2243
+ return line ? fromRRule(line, master.start.dateTime ?? master.start.date) : null;
2244
+ },
1735
2245
  async respond(event, status) {
1736
2246
  const attendees = rawOf(event)?.attendees ?? event.attendees.map((a) => ({ email: a.email, displayName: a.name, responseStatus: a.status, self: a.self, organizer: a.organizer }));
1737
2247
  if (!attendees.some((a) => a.self)) throw new Error("This account is not an attendee of the event");
@@ -2070,32 +2580,32 @@ async function runDaemon() {
2070
2580
  // src/ui/hooks.ts
2071
2581
  import { createContext, useCallback, useContext, useEffect, useState } from "react";
2072
2582
  import { useStdout } from "ink";
2073
- import { addDays as addDays3, addWeeks as addWeeks2, startOfDay as startOfDay2 } from "date-fns";
2583
+ import { addDays as addDays5, addWeeks as addWeeks2, startOfDay as startOfDay2 } from "date-fns";
2074
2584
 
2075
2585
  // ../../packages/core/src/logic/layout.ts
2076
2586
  import {
2077
- addDays as addDays2,
2587
+ addDays as addDays4,
2078
2588
  addMonths as addMonths2,
2079
2589
  addWeeks,
2080
- format as format3,
2590
+ format as format5,
2081
2591
  isSameDay as isSameDay2,
2082
- parseISO as parseISO4,
2592
+ parseISO as parseISO5,
2083
2593
  startOfDay,
2084
2594
  startOfMonth,
2085
- startOfWeek
2595
+ startOfWeek as startOfWeek2
2086
2596
  } from "date-fns";
2087
2597
  var WEEK_STARTS_ON = 1;
2088
2598
  function eventBounds(e) {
2089
- const start = parseISO4(e.start);
2090
- let end = parseISO4(e.end);
2091
- if (end <= start) end = e.allDay ? addDays2(start, 1) : new Date(start.getTime() + 6e4);
2599
+ const start = parseISO5(e.start);
2600
+ let end = parseISO5(e.end);
2601
+ if (end <= start) end = e.allDay ? addDays4(start, 1) : new Date(start.getTime() + 6e4);
2092
2602
  return { start, end };
2093
2603
  }
2094
2604
  var isPast = (e, now) => eventBounds(e).end.getTime() <= +now;
2095
2605
  function overlapsDay(e, day) {
2096
2606
  const d0 = startOfDay(day);
2097
2607
  const { start, end } = eventBounds(e);
2098
- return start < addDays2(d0, 1) && end > d0;
2608
+ return start < addDays4(d0, 1) && end > d0;
2099
2609
  }
2100
2610
  function eventsOnDay(events, day) {
2101
2611
  return events.filter((e) => overlapsDay(e, day)).sort((a, b) => {
@@ -2142,31 +2652,31 @@ function layoutDay(events, day, minDur = 0) {
2142
2652
  return packColumns(items, minDur);
2143
2653
  }
2144
2654
  function monthGrid(date) {
2145
- const first = startOfWeek(startOfMonth(date), { weekStartsOn: WEEK_STARTS_ON });
2146
- return Array.from({ length: 42 }, (_, i) => addDays2(first, i));
2655
+ const first = startOfWeek2(startOfMonth(date), { weekStartsOn: WEEK_STARTS_ON });
2656
+ return Array.from({ length: 42 }, (_, i) => addDays4(first, i));
2147
2657
  }
2148
2658
  function viewDays(view, date) {
2149
2659
  if (view === "month") return monthGrid(date);
2150
2660
  if (view === "day") return [startOfDay(date)];
2151
- if (view === "3day") return [0, 1, 2].map((i) => addDays2(startOfDay(date), i));
2152
- const first = startOfWeek(date, { weekStartsOn: WEEK_STARTS_ON });
2153
- return Array.from({ length: 7 }, (_, i) => addDays2(first, i));
2661
+ if (view === "3day") return [0, 1, 2].map((i) => addDays4(startOfDay(date), i));
2662
+ const first = startOfWeek2(date, { weekStartsOn: WEEK_STARTS_ON });
2663
+ return Array.from({ length: 7 }, (_, i) => addDays4(first, i));
2154
2664
  }
2155
2665
  function viewRange(view, date) {
2156
2666
  const days = viewDays(view, date);
2157
- return { start: days[0].toISOString(), end: addDays2(days[days.length - 1], 1).toISOString() };
2667
+ return { start: days[0].toISOString(), end: addDays4(days[days.length - 1], 1).toISOString() };
2158
2668
  }
2159
2669
  function shiftDate(view, date, dir) {
2160
- if (view === "day") return addDays2(date, dir);
2161
- if (view === "3day") return addDays2(date, 3 * dir);
2670
+ if (view === "day") return addDays4(date, dir);
2671
+ if (view === "3day") return addDays4(date, 3 * dir);
2162
2672
  if (view === "week") return addWeeks(date, dir);
2163
2673
  return addMonths2(date, dir);
2164
2674
  }
2165
2675
  function rangeLabel(a, b) {
2166
2676
  const head = a.getFullYear() !== b.getFullYear() ? "d MMM yyyy" : a.getMonth() !== b.getMonth() ? "d MMM" : "d";
2167
- return `${format3(a, head)} \u2013 ${format3(b, "d MMM yyyy")}`;
2677
+ return `${format5(a, head)} \u2013 ${format5(b, "d MMM yyyy")}`;
2168
2678
  }
2169
- var ymd = (d) => format3(d, "yyyy-MM-dd");
2679
+ var ymd = (d) => format5(d, "yyyy-MM-dd");
2170
2680
 
2171
2681
  // ../../packages/core/src/logic/visible.ts
2172
2682
  var key2 = (accountId, calendarId) => `${accountId}/${calendarId}`;
@@ -2186,7 +2696,7 @@ var eventKey = (e) => `${e.accountId}/${e.calendarId}/${e.id}`;
2186
2696
  function useLive(load, deps) {
2187
2697
  const api = useApi();
2188
2698
  const [state, setState] = useState({});
2189
- const [bump, setBump] = useState(0);
2699
+ const [bump2, setBump] = useState(0);
2190
2700
  useEffect(() => {
2191
2701
  let seq = 0;
2192
2702
  let alive2 = true;
@@ -2203,7 +2713,7 @@ function useLive(load, deps) {
2203
2713
  alive2 = false;
2204
2714
  off();
2205
2715
  };
2206
- }, [api, bump, ...deps]);
2716
+ }, [api, bump2, ...deps]);
2207
2717
  return { ...state, reload: useCallback(() => setBump((b) => b + 1), []) };
2208
2718
  }
2209
2719
  function useDirectory() {
@@ -2224,13 +2734,13 @@ var AGENDA_DAYS = 14;
2224
2734
  function navRange({ view, date }) {
2225
2735
  if (view === "agenda" || view === "2day") {
2226
2736
  const start = startOfDay2(date);
2227
- return { start: start.toISOString(), end: addDays3(start, view === "agenda" ? AGENDA_DAYS : 2).toISOString() };
2737
+ return { start: start.toISOString(), end: addDays5(start, view === "agenda" ? AGENDA_DAYS : 2).toISOString() };
2228
2738
  }
2229
2739
  return viewRange(view, date);
2230
2740
  }
2231
2741
  var shiftNav = ({ view, date }, dir) => ({
2232
2742
  view,
2233
- date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ? addDays3(date, 2 * dir) : shiftDate(view, date, dir)
2743
+ date: view === "agenda" ? addWeeks2(date, dir) : view === "2day" ? addDays5(date, 2 * dir) : shiftDate(view, date, dir)
2234
2744
  });
2235
2745
  function useNav(initial = { view: "agenda", date: /* @__PURE__ */ new Date() }) {
2236
2746
  const [nav, setNav] = useState(initial);
@@ -2265,12 +2775,12 @@ function useTermSize() {
2265
2775
  // src/ui/App.tsx
2266
2776
  import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
2267
2777
  import { Box as Box11, Text as Text11, useApp, useStdin as useStdin2 } from "ink";
2268
- import { addDays as addDays9, differenceInMinutes as differenceInMinutes3, format as format11, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
2778
+ import { addDays as addDays11, differenceInMinutes as differenceInMinutes3, format as format13, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
2269
2779
 
2270
2780
  // ../../packages/core/src/logic/editor.ts
2271
- import { addDays as addDays4, addHours, format as format4, parseISO as parseISO5, startOfHour } from "date-fns";
2781
+ import { addDays as addDays6, addHours, format as format6, parseISO as parseISO6, startOfHour } from "date-fns";
2272
2782
  var LOCAL = "yyyy-MM-dd'T'HH:mm";
2273
- var toLocalInput = (iso2) => format4(iso2.length === 10 ? parseISO5(iso2) : new Date(iso2), LOCAL);
2783
+ var toLocalInput = (iso2) => format6(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
2274
2784
  var fromLocalInput = (local) => new Date(local).toISOString();
2275
2785
  var writableCalendars = (calendars, accountId) => calendars.filter((c) => c.accountId === accountId && !c.readOnly);
2276
2786
  var writableAccounts = (accounts, calendars) => accounts.filter((a) => writableCalendars(calendars, a.id).length > 0);
@@ -2280,8 +2790,8 @@ var splitEmails = (text2) => text2.split(/[\s,;]+/).map((s) => s.trim()).filter(
2280
2790
  var isEmail = (s) => EMAIL.test(s);
2281
2791
  function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new Date()) {
2282
2792
  const accountId = soleId(writableAccounts(accounts, calendars));
2283
- const start = prefill.start ? toLocalInput(prefill.start) : format4(startOfHour(addHours(now, 1)), LOCAL);
2284
- let end = prefill.end ? toLocalInput(prefill.end) : format4(addHours(new Date(start), 1), LOCAL);
2793
+ const start = prefill.start ? toLocalInput(prefill.start) : format6(startOfHour(addHours(now, 1)), LOCAL);
2794
+ let end = prefill.end ? toLocalInput(prefill.end) : format6(addHours(new Date(start), 1), LOCAL);
2285
2795
  if (prefill.allDay && prefill.end) end = inclusiveEnd(start, end);
2286
2796
  return {
2287
2797
  accountId,
@@ -2292,11 +2802,13 @@ function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new
2292
2802
  allDay: !!prefill.allDay,
2293
2803
  location: "",
2294
2804
  description: "",
2295
- attendees: []
2805
+ attendees: [],
2806
+ repeat: null,
2807
+ repeatWas: null
2296
2808
  };
2297
2809
  }
2298
2810
  var inclusiveEnd = (startLocal, endLocal) => {
2299
- const d = format4(addDays4(new Date(endLocal), -1), LOCAL);
2811
+ const d = format6(addDays6(new Date(endLocal), -1), LOCAL);
2300
2812
  return d.slice(0, 10) < startLocal.slice(0, 10) ? startLocal : d;
2301
2813
  };
2302
2814
  function formFromEvent(e) {
@@ -2311,23 +2823,28 @@ function formFromEvent(e) {
2311
2823
  allDay: e.allDay,
2312
2824
  location: e.location ?? "",
2313
2825
  description: e.description ?? "",
2314
- attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email)
2826
+ attendees: e.attendees.filter((a) => !a.self && !a.organizer).map((a) => a.email),
2827
+ repeat: null,
2828
+ // A series' rule isn't cached: the editor loads it (events.recurrence) and sets both fields.
2829
+ repeatWas: e.recurringEventId ? void 0 : null
2315
2830
  };
2316
2831
  }
2832
+ var withLoadedRepeat = (f, rule) => ({ ...f, repeat: rule, repeatWas: rule });
2833
+ var repeatChanged = (f) => f.repeatWas !== void 0 && !sameRecurrence(f.repeat, f.repeatWas, f.start);
2317
2834
  function setAllDay(f, allDay) {
2318
2835
  if (allDay || f.end > f.start) return { ...f, allDay };
2319
- return { ...f, allDay, end: format4(addHours(new Date(f.start), 1), LOCAL) };
2836
+ return { ...f, allDay, end: format6(addHours(new Date(f.start), 1), LOCAL) };
2320
2837
  }
2321
2838
  function moveStart(f, start) {
2322
2839
  const end = new Date(new Date(f.end).getTime() + new Date(start).getTime() - new Date(f.start).getTime());
2323
- return { ...f, start, end: format4(end, LOCAL) };
2840
+ return { ...f, start, end: format6(end, LOCAL) };
2324
2841
  }
2325
2842
  function times(f) {
2326
2843
  if (f.allDay) {
2327
2844
  const start2 = f.start.slice(0, 10);
2328
2845
  const endIncl = f.end.slice(0, 10);
2329
2846
  if (endIncl < start2) throw new Error("End date is before start date");
2330
- return { allDay: true, start: start2, end: format4(addDays4(parseISO5(endIncl), 1), "yyyy-MM-dd") };
2847
+ return { allDay: true, start: start2, end: format6(addDays6(parseISO6(endIncl), 1), "yyyy-MM-dd") };
2331
2848
  }
2332
2849
  const start = fromLocalInput(f.start);
2333
2850
  const end = fromLocalInput(f.end);
@@ -2350,7 +2867,8 @@ function formToInput(f) {
2350
2867
  ...times(f),
2351
2868
  location: f.location.trim() || void 0,
2352
2869
  description: f.description.trim() || void 0,
2353
- ...attendees.length ? { attendees } : {}
2870
+ ...attendees.length ? { attendees } : {},
2871
+ ...f.repeat ? { recurrence: f.repeat } : {}
2354
2872
  };
2355
2873
  }
2356
2874
  function applyForm(e, f) {
@@ -2364,7 +2882,9 @@ function applyForm(e, f) {
2364
2882
  ...times(f),
2365
2883
  location: f.location.trim() || void 0,
2366
2884
  description: f.description.trim() || void 0,
2367
- attendees: [...keep, ...invited]
2885
+ attendees: [...keep, ...invited],
2886
+ // Only a changed rule is sent; absent keeps the series' rule.
2887
+ ...repeatChanged(f) ? { recurrence: f.repeat } : {}
2368
2888
  };
2369
2889
  }
2370
2890
  var errorText = (e) => String(e instanceof Error ? e.message : e).replace(/^Error invoking remote method '[^']+': (\w*Error: )?/, "");
@@ -2483,10 +3003,10 @@ function Button({ k, label, onPress, color: color2 = C.accent }) {
2483
3003
  // src/ui/views/Agenda.tsx
2484
3004
  import { useMemo, useReducer, useRef as useRef2 } from "react";
2485
3005
  import { Box as Box3, Text as Text3 } from "ink";
2486
- import { addDays as addDays6, differenceInCalendarDays, differenceInMinutes as differenceInMinutes2, format as format7, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
3006
+ import { addDays as addDays8, differenceInCalendarDays as differenceInCalendarDays2, differenceInMinutes as differenceInMinutes2, format as format9, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
2487
3007
 
2488
3008
  // ../../packages/core/src/logic/details.ts
2489
- import { addDays as addDays5, format as format5, isSameDay as isSameDay3, parseISO as parseISO6 } from "date-fns";
3009
+ import { addDays as addDays7, format as format7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
2490
3010
  var same = (a, b) => !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
2491
3011
  function ownerLine(calendar, label, email) {
2492
3012
  return { calendar: same(calendar, label) || same(calendar, email) ? void 0 : calendar, label, email: same(email, label) ? void 0 : email };
@@ -2496,17 +3016,17 @@ function canEdit(e, account, calendar) {
2496
3016
  if (e.attendees.length === 0) return true;
2497
3017
  return same(e.organizer?.email, account.email) || e.attendees.some((a) => a.self && a.organizer);
2498
3018
  }
2499
- var at2 = (iso2, allDay) => (allDay ? parseISO6(iso2) : new Date(iso2)).getTime();
3019
+ var at2 = (iso2, allDay) => (allDay ? parseISO7(iso2) : new Date(iso2)).getTime();
2500
3020
  var pendingInvites = (events, now = /* @__PURE__ */ new Date()) => events.filter((e) => e.myStatus === "needsAction" && at2(e.end, e.allDay) > now.getTime()).sort((a, b) => at2(a.start, a.allDay) - at2(b.start, b.allDay));
2501
3021
  function formatWhen(e) {
2502
3022
  if (e.allDay) {
2503
- const s2 = parseISO6(e.start);
2504
- const last = addDays5(parseISO6(e.end), -1);
2505
- return last > s2 ? `${format5(s2, "EEE, d MMM")} \u2013 ${format5(last, "EEE, d MMM")} \xB7 all day` : `${format5(s2, "EEE, d MMM")} \xB7 all day`;
3023
+ const s2 = parseISO7(e.start);
3024
+ const last = addDays7(parseISO7(e.end), -1);
3025
+ return last > s2 ? `${format7(s2, "EEE, d MMM")} \u2013 ${format7(last, "EEE, d MMM")} \xB7 all day` : `${format7(s2, "EEE, d MMM")} \xB7 all day`;
2506
3026
  }
2507
3027
  const s = new Date(e.start);
2508
3028
  const t = new Date(e.end);
2509
- return isSameDay3(s, t) ? `${format5(s, "EEE, d MMM")} \xB7 ${format5(s, "HH:mm")} \u2013 ${format5(t, "HH:mm")}` : `${format5(s, "EEE, d MMM HH:mm")} \u2013 ${format5(t, "EEE, d MMM HH:mm")}`;
3029
+ return isSameDay3(s, t) ? `${format7(s, "EEE, d MMM")} \xB7 ${format7(s, "HH:mm")} \u2013 ${format7(t, "HH:mm")}` : `${format7(s, "EEE, d MMM HH:mm")} \u2013 ${format7(t, "EEE, d MMM HH:mm")}`;
2510
3030
  }
2511
3031
  var STATUS_ICON = {
2512
3032
  accepted: "\u2713",
@@ -2527,7 +3047,7 @@ function place(location = "") {
2527
3047
  }
2528
3048
 
2529
3049
  // ../../packages/core/src/logic/status.ts
2530
- import { differenceInMinutes, format as format6 } from "date-fns";
3050
+ import { differenceInMinutes, format as format8 } from "date-fns";
2531
3051
  function pickNowNext(events, now) {
2532
3052
  const t = now.getTime();
2533
3053
  const timed = events.filter((e) => !e.allDay && e.myStatus !== "declined").sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
@@ -2539,7 +3059,7 @@ function pickNowNext(events, now) {
2539
3059
  function startsLabel(start, now) {
2540
3060
  const s = new Date(start);
2541
3061
  const m = differenceInMinutes(s, now, { roundingMethod: "ceil" });
2542
- return m <= 60 ? `in ${m}m` : `at ${format6(s, "HH:mm")}`;
3062
+ return m <= 60 ? `in ${m}m` : `at ${format8(s, "HH:mm")}`;
2543
3063
  }
2544
3064
 
2545
3065
  // src/ui/screens/EventDetails.tsx
@@ -2753,7 +3273,7 @@ var span = (m) => {
2753
3273
  };
2754
3274
  function busyMinutes(list, day) {
2755
3275
  const from = +startOfDay3(day);
2756
- const to = +addDays6(day, 1);
3276
+ const to = +addDays8(day, 1);
2757
3277
  const iv = list.filter((e) => !e.allDay && e.myStatus !== "declined").map((e) => [Math.max(+eventBounds(e).start, from), Math.min(+eventBounds(e).end, to)]).sort((a, b) => a[0] - b[0]);
2758
3278
  let total = 0;
2759
3279
  let end = -Infinity;
@@ -2768,7 +3288,7 @@ var overlaps = (e, list) => !e.allDay && e.myStatus !== "declined" && list.some(
2768
3288
  (o) => o !== e && !o.allDay && o.myStatus !== "declined" && eventBounds(o).start < eventBounds(e).end && eventBounds(e).start < eventBounds(o).end
2769
3289
  );
2770
3290
  function relativeDay(day, now) {
2771
- const d = differenceInCalendarDays(day, now);
3291
+ const d = differenceInCalendarDays2(day, now);
2772
3292
  return d === 0 ? "today" : d === 1 ? "tomorrow" : d === -1 ? "yesterday" : d > 0 ? `in ${d} days` : `${-d} days ago`;
2773
3293
  }
2774
3294
  function fitParts(parts, w) {
@@ -2797,7 +3317,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2797
3317
  const rows = useMemo(() => {
2798
3318
  const out = [];
2799
3319
  for (let i = 0; i < AGENDA_DAYS; i++) {
2800
- const day = addDays6(startOfDay3(date), i);
3320
+ const day = addDays8(startOfDay3(date), i);
2801
3321
  const list = eventsOnDay(events, day);
2802
3322
  if (!list.length) {
2803
3323
  const last = out.at(-1);
@@ -2838,7 +3358,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2838
3358
  const key3 = "free" in r ? `free${ymd(r.free[0])}` : "e" in r ? `${ymd(r.day)}/${eventKey(r.e)}` : "nowLine" in r ? `${ymd(r.day)}/now` : "divider" in r ? `${ymd(r.day)}/d${i}` : ymd(r.day);
2839
3359
  if ("free" in r) {
2840
3360
  const [a, b] = r.free;
2841
- const label = isSameDay4(a, b) ? format7(a, "EEE d MMM") : `${format7(a, "EEE d MMM")} \u2013 ${format7(b, "EEE d MMM")}`;
3361
+ const label = isSameDay4(a, b) ? format9(a, "EEE d MMM") : `${format9(a, "EEE d MMM")} \u2013 ${format9(b, "EEE d MMM")}`;
2842
3362
  lines.push(
2843
3363
  /* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.muted, wrap: "truncate", children: ` ${label} free` }) }, key3)
2844
3364
  );
@@ -2852,7 +3372,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2852
3372
  }
2853
3373
  if ("nowLine" in r) {
2854
3374
  lines.push(
2855
- /* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.now, bold: true, wrap: "truncate", children: ` ${format7(now, "HH:mm")} ${"\u2500".repeat(width)}` }) }, key3)
3375
+ /* @__PURE__ */ jsx3(Box3, { width, height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx3(Text3, { color: C.now, bold: true, wrap: "truncate", children: ` ${format9(now, "HH:mm")} ${"\u2500".repeat(width)}` }) }, key3)
2856
3376
  );
2857
3377
  return;
2858
3378
  }
@@ -2860,7 +3380,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2860
3380
  const today = isSameDay4(r.day, now);
2861
3381
  const list = eventsOnDay(events, r.day);
2862
3382
  const busy = busyMinutes(list, r.day);
2863
- const left = ` ${format7(r.day, "EEE d MMM")} `;
3383
+ const left = ` ${format9(r.day, "EEE d MMM")} `;
2864
3384
  const rel = relativeDay(r.day, now);
2865
3385
  const right = ` ${list.length} ${list.length === 1 ? "event" : "events"}${busy ? ` \xB7 ${span(busy)} busy` : ""} `;
2866
3386
  const rule = "\u2500".repeat(Math.max(width - left.length - rel.length - right.length - 1, 1));
@@ -2878,7 +3398,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2878
3398
  const { start, end } = eventBounds(e);
2879
3399
  const running = !e.allDay && start <= now && now < end;
2880
3400
  const past = isPast(e, now) || e.myStatus === "declined";
2881
- const clockOf = (d, edge) => isSameDay4(d, r.day) ? format7(d, "HH:mm") : +d === +addDays6(r.day, 1) ? "24:00" : edge;
3401
+ const clockOf = (d, edge) => isSameDay4(d, r.day) ? format9(d, "HH:mm") : +d === +addDays8(r.day, 1) ? "24:00" : edge;
2882
3402
  const badges = [];
2883
3403
  if (running) badges.push({ text: ` \u25CF now \xB7 ends in ${span(Math.max(differenceInMinutes2(end, now, { roundingMethod: "ceil" }), 1))}`, color: C.green, bold: true });
2884
3404
  else if (e === upNext) badges.push({ text: ` ${startsLabel(e.start, now)}`, color: C.yellow });
@@ -2916,16 +3436,16 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
2916
3436
 
2917
3437
  // src/ui/views/Day.tsx
2918
3438
  import { useMemo as useMemo3 } from "react";
2919
- import { addDays as addDays7, startOfDay as startOfDay5 } from "date-fns";
3439
+ import { addDays as addDays9, startOfDay as startOfDay5 } from "date-fns";
2920
3440
 
2921
3441
  // src/ui/views/TimeGrid.tsx
2922
3442
  import { useMemo as useMemo2 } from "react";
2923
3443
  import { Box as Box5, Text as Text5 } from "ink";
2924
- import { format as format9, isSameDay as isSameDay6 } from "date-fns";
3444
+ import { format as format11, isSameDay as isSameDay6 } from "date-fns";
2925
3445
 
2926
3446
  // src/ui/views/Month.tsx
2927
3447
  import { Box as Box4, Text as Text4 } from "ink";
2928
- import { format as format8, isSameDay as isSameDay5, isSameMonth, startOfDay as startOfDay4 } from "date-fns";
3448
+ import { format as format10, isSameDay as isSameDay5, isSameMonth, startOfDay as startOfDay4 } from "date-fns";
2929
3449
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2930
3450
  function fit(items, rows, selected) {
2931
3451
  if (items.length <= rows) return { shown: items, more: 0 };
@@ -2936,7 +3456,7 @@ function fit(items, rows, selected) {
2936
3456
  }
2937
3457
  function EventLine({ e, day, now, selected, time, onSelect, onOpen }) {
2938
3458
  const { start, end } = eventBounds(e);
2939
- const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${format8(start, "HH:mm")} `;
3459
+ const prefix = !time || e.allDay ? "" : start < startOfDay4(day) ? "\u2026 " : `${format10(start, "HH:mm")} `;
2940
3460
  const live = !e.allDay && start <= now && now < end;
2941
3461
  return /* @__PURE__ */ jsx4(Clickable, { height: 1, onClick: clickEvent(e, selected, { onSelect, onOpen }), children: /* @__PURE__ */ jsxs4(
2942
3462
  Text4,
@@ -2960,7 +3480,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
2960
3480
  const cellHeight = Math.max(Math.floor((height - 1) / 6), 1);
2961
3481
  const weeks = [0, 1, 2, 3, 4, 5].map((w) => days.slice(w * 7, w * 7 + 7));
2962
3482
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
2963
- /* @__PURE__ */ jsx4(Box4, { children: weeks[0].map((d) => /* @__PURE__ */ jsx4(Box4, { width: colWidth, children: /* @__PURE__ */ jsx4(Text4, { color: C.accent, bold: true, children: format8(d, "EEE") }) }, d.getDay())) }),
3483
+ /* @__PURE__ */ jsx4(Box4, { children: weeks[0].map((d) => /* @__PURE__ */ jsx4(Box4, { width: colWidth, children: /* @__PURE__ */ jsx4(Text4, { color: C.accent, bold: true, children: format10(d, "EEE") }) }, d.getDay())) }),
2964
3484
  weeks.map((week) => /* @__PURE__ */ jsx4(Box4, { height: cellHeight, children: week.map((day) => {
2965
3485
  const items = eventsOnDay(events, day);
2966
3486
  const { shown, more } = fit(items, cellHeight - 1, items.findIndex((e) => eventKey(e) === selectedKey));
@@ -2974,7 +3494,7 @@ function Month({ events, date, now, cursor, selectedKey, width, height, onSelect
2974
3494
  inverse: here,
2975
3495
  color: today ? C.now : here ? C.accent : isSameMonth(day, date) ? void 0 : C.muted,
2976
3496
  children: [
2977
- here ? ` ${format8(day, "d")} ` : format8(day, "d"),
3497
+ here ? ` ${format10(day, "d")} ` : format10(day, "d"),
2978
3498
  cellHeight === 1 && items.length > 0 && /* @__PURE__ */ jsxs4(Text4, { color: C.muted, children: [
2979
3499
  " \xB7",
2980
3500
  items.length
@@ -3064,10 +3584,10 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3064
3584
  const selected = k === selectedKey;
3065
3585
  const first = p.start / 60;
3066
3586
  const { start, end } = eventBounds(e);
3067
- const from = isSameDay6(start, day) ? format9(start, "HH:mm") : "\u2026";
3587
+ const from = isSameDay6(start, day) ? format11(start, "HH:mm") : "\u2026";
3068
3588
  const title2 = `${rsvpMark(e)}${e.title || "(no title)"}`;
3069
3589
  const where = place(e.location);
3070
- const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${format9(start, "mm")} ${title2}`, `${from}\u2013${isSameDay6(end, day) ? format9(end, "HH:mm") : "\u2026"}`, where] : [`${from} ${title2}`, [where, duration(e)].filter(Boolean).join(" \xB7 ")];
3590
+ const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${format11(start, "mm")} ${title2}`, `${from}\u2013${isSameDay6(end, day) ? format11(end, "HH:mm") : "\u2026"}`, where] : [`${from} ${title2}`, [where, duration(e)].filter(Boolean).join(" \xB7 ")];
3071
3591
  const text2 = lines[h - first] ?? "";
3072
3592
  const inner = p.col < p.cols - 1 && w > 1 ? w - 1 : w;
3073
3593
  const past = isPast(e, now) || e.myStatus === "declined";
@@ -3095,7 +3615,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3095
3615
  const isToday = isSameDay6(c.day, now);
3096
3616
  const here = !!cursor && isSameDay6(c.day, cursor) && days.length > 1;
3097
3617
  return /* @__PURE__ */ jsx5(Clickable, { height: 1, width: colW, onClick: onPickDay && (() => onPickDay(c.day)), children: /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
3098
- /* @__PURE__ */ jsx5(Text5, { bold: true, inverse: here, color: isToday ? C.now : C.accent, children: ` ${format9(c.day, "EEE d")} ` }),
3618
+ /* @__PURE__ */ jsx5(Text5, { bold: true, inverse: here, color: isToday ? C.now : C.accent, children: ` ${format11(c.day, "EEE d")} ` }),
3099
3619
  c.count > 0 && /* @__PURE__ */ jsxs5(Text5, { color: C.muted, children: [
3100
3620
  "\xB7",
3101
3621
  c.count
@@ -3125,7 +3645,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3125
3645
  /* @__PURE__ */ jsx5(Clickable, { flexDirection: "column", width, onWheel: (dir) => scrollBy(dir), children: Array.from({ length: Math.min(gridH, 24) }, (_, i) => {
3126
3646
  const h = top + i;
3127
3647
  const current = today && h === now.getHours();
3128
- const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? `${format9(now, "HH:mm")}\u2500` : `${String(h).padStart(2, "0")}:00` }) });
3648
+ const gutter = /* @__PURE__ */ jsx5(Box5, { width: GUTTER, flexShrink: 0, children: /* @__PURE__ */ jsx5(Text5, { color: current ? C.now : C.muted, bold: current, children: current ? `${format11(now, "HH:mm")}\u2500` : `${String(h).padStart(2, "0")}:00` }) });
3129
3649
  return row(
3130
3650
  `h${h}`,
3131
3651
  gutter,
@@ -3140,7 +3660,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3140
3660
  import { jsx as jsx6 } from "react/jsx-runtime";
3141
3661
  function useDays(date, n) {
3142
3662
  const t = startOfDay5(date).getTime();
3143
- return useMemo3(() => Array.from({ length: n }, (_, i) => addDays7(t, i)), [t, n]);
3663
+ return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t, i)), [t, n]);
3144
3664
  }
3145
3665
  var Day = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 1) });
3146
3666
  var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 2) });
@@ -3157,7 +3677,7 @@ function Week(p) {
3157
3677
  // src/ui/screens/EventEditor.tsx
3158
3678
  import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
3159
3679
  import { Box as Box6, Text as Text6 } from "ink";
3160
- import { format as format10, isValid, parse as parse2 } from "date-fns";
3680
+ import { format as format12, isValid as isValid2, parse as parse2 } from "date-fns";
3161
3681
  import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
3162
3682
  var LABELS = {
3163
3683
  title: "Title",
@@ -3170,20 +3690,29 @@ var LABELS = {
3170
3690
  endTime: "End time",
3171
3691
  location: "Location",
3172
3692
  description: "Notes",
3173
- attendees: "Invitees"
3693
+ attendees: "Invitees",
3694
+ repeat: "Repeat",
3695
+ repeatEvery: "Every",
3696
+ repeatDays: "On days",
3697
+ repeatEnds: "Stops"
3174
3698
  };
3699
+ var SCOPE_KEYS2 = { "1": "one", "2": "following", "3": "all" };
3175
3700
  var HINTS = {
3176
3701
  startDate: "YYYY-MM-DD",
3177
3702
  startTime: "HH:mm",
3178
3703
  endDate: "YYYY-MM-DD",
3179
3704
  endTime: "HH:mm",
3180
3705
  attendees: "a@x.com, b@y.com",
3181
- title: "New Event"
3706
+ title: "New Event",
3707
+ repeatEvery: "2 weeks",
3708
+ repeatDays: "mo th",
3709
+ repeatEnds: "never \xB7 2026-12-31 \xB7 10 times"
3182
3710
  };
3711
+ var PICKERS = /* @__PURE__ */ new Set(["account", "calendar", "allDay", "repeat"]);
3183
3712
  function toLocal(date, time) {
3184
3713
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time)) return;
3185
3714
  const d = parse2(`${date}T${time}`, "yyyy-MM-dd'T'HH:mm", /* @__PURE__ */ new Date());
3186
- return isValid(d) && format10(d, "yyyy-MM-dd'T'HH:mm") === `${date}T${time}` ? `${date}T${time}` : void 0;
3715
+ return isValid2(d) && format12(d, "yyyy-MM-dd'T'HH:mm") === `${date}T${time}` ? `${date}T${time}` : void 0;
3187
3716
  }
3188
3717
  var timeTexts = (f) => ({
3189
3718
  startDate: f.start.slice(0, 10),
@@ -3191,25 +3720,48 @@ var timeTexts = (f) => ({
3191
3720
  endDate: f.end.slice(0, 10),
3192
3721
  endTime: f.end.slice(11)
3193
3722
  });
3723
+ var repeatTexts = (r, start) => ({
3724
+ repeatEvery: r ? everyText(r) : "",
3725
+ repeatDays: daysText(r?.byDay?.length ? r.byDay : [weekdayOf(start)]),
3726
+ repeatEnds: r ? endsText(r) : "never"
3727
+ });
3194
3728
  var textsFrom = (f) => ({
3195
3729
  title: f.title,
3196
3730
  location: f.location,
3197
3731
  description: f.description,
3198
3732
  attendees: f.attendees.join(", "),
3199
- ...timeTexts(f)
3733
+ ...timeTexts(f),
3734
+ ...repeatTexts(f.repeat, f.start)
3200
3735
  });
3201
- var fieldsFor = (form, editing) => [
3202
- "title",
3203
- ...editing ? [] : ["account", "calendar"],
3204
- "allDay",
3205
- "startDate",
3206
- ...form.allDay ? [] : ["startTime"],
3207
- "endDate",
3208
- ...form.allDay ? [] : ["endTime"],
3209
- "location",
3210
- "description",
3211
- "attendees"
3212
- ];
3736
+ var isCustom = (s) => !!s.form.repeat && !s.form.repeat.rule && (s.custom || presetOf(s.form.repeat, s.form.start) === "custom");
3737
+ var fieldsFor = (s, editing) => {
3738
+ const { form } = s;
3739
+ const custom = isCustom(s);
3740
+ return [
3741
+ "title",
3742
+ ...editing ? [] : ["account", "calendar"],
3743
+ "allDay",
3744
+ "startDate",
3745
+ ...form.allDay ? [] : ["startTime"],
3746
+ "endDate",
3747
+ ...form.allDay ? [] : ["endTime"],
3748
+ "repeat",
3749
+ ...custom ? ["repeatEvery"] : [],
3750
+ ...custom && form.repeat?.freq === "weekly" ? ["repeatDays"] : [],
3751
+ ...form.repeat && !form.repeat.rule ? ["repeatEnds"] : [],
3752
+ "location",
3753
+ "description",
3754
+ "attendees"
3755
+ ];
3756
+ };
3757
+ function readRepeat(s) {
3758
+ const r = s.form.repeat;
3759
+ if (!r || r.rule) return r;
3760
+ const { until: _u, count: _c, ...base } = r;
3761
+ const rule = isCustom(s) ? { ...parseEvery(s.texts.repeatEvery) } : base;
3762
+ if (isCustom(s) && rule.freq === "weekly") rule.byDay = parseDays(s.texts.repeatDays);
3763
+ return { ...rule, ...parseEnds(s.texts.repeatEnds) };
3764
+ }
3213
3765
  function EventEditor({ event, initialStart, onClose }) {
3214
3766
  const api = useApi();
3215
3767
  const { accounts, calendars, loaded } = useDirectory();
@@ -3223,18 +3775,41 @@ function EventEditor({ event, initialStart, onClose }) {
3223
3775
  const [error, setError] = useState3("");
3224
3776
  const [saving, setSaving] = useState3(false);
3225
3777
  const busy = useRef3(false);
3778
+ const [askScope, setAskScopeState] = useState3(false);
3779
+ const asking = useRef3(false);
3780
+ const setAskScope = (on) => {
3781
+ asking.current = on;
3782
+ setAskScopeState(on);
3783
+ };
3784
+ const [repeatFailed, setRepeatFailed] = useState3(false);
3226
3785
  const alive2 = useRef3(true);
3227
3786
  useEffect3(() => () => void (alive2.current = false), []);
3228
3787
  useEffect3(() => {
3229
3788
  if (latest.current || !loaded) return;
3230
3789
  const form2 = event ? formFromEvent(event) : emptyForm(accounts, calendars, {}, initialStart);
3231
- latest.current = { form: form2, texts: textsFrom(form2), focus: 0 };
3790
+ latest.current = { form: form2, texts: textsFrom(form2), focus: 0, custom: false };
3232
3791
  setState(latest.current);
3792
+ if (event?.recurringEventId) {
3793
+ api.events.recurrence(event).then(
3794
+ (rule) => {
3795
+ if (!alive2.current || !latest.current) return;
3796
+ const f = withLoadedRepeat(latest.current.form, rule);
3797
+ update({ form: f, texts: { ...latest.current.texts, ...repeatTexts(rule, f.start) } });
3798
+ },
3799
+ () => alive2.current && setRepeatFailed(true)
3800
+ );
3801
+ }
3233
3802
  }, [loaded]);
3234
3803
  const choices = writableAccounts(accounts, calendars);
3235
- const save = async () => {
3804
+ const save = async (scope) => {
3236
3805
  if (!latest.current || busy.current) return;
3237
3806
  const { form: form2, texts: texts2 } = latest.current;
3807
+ let repeat;
3808
+ try {
3809
+ repeat = readRepeat(latest.current);
3810
+ } catch (e) {
3811
+ return setError(errorText(e));
3812
+ }
3238
3813
  setError("");
3239
3814
  const start = toLocal(texts2.startDate, form2.allDay ? form2.start.slice(11) : texts2.startTime);
3240
3815
  const end = toLocal(texts2.endDate, form2.allDay ? form2.end.slice(11) : texts2.endTime);
@@ -3247,14 +3822,18 @@ function EventEditor({ event, initialStart, onClose }) {
3247
3822
  description: texts2.description,
3248
3823
  attendees: splitEmails(texts2.attendees),
3249
3824
  start,
3250
- end
3825
+ end,
3826
+ repeat
3251
3827
  };
3252
3828
  let request;
3253
3829
  try {
3254
- request = event ? api.events.update(applyForm(event, f)) : api.events.create(formToInput(f));
3830
+ const edited = event && applyForm(event, f);
3831
+ if (edited && event.recurringEventId && !scope) return setAskScope(repeatChanged(f) ? "rule" : "any");
3832
+ request = edited ? api.events.update(edited, scope) : api.events.create(formToInput(f));
3255
3833
  } catch (e) {
3256
3834
  return setError(errorText(e));
3257
3835
  }
3836
+ setAskScope(false);
3258
3837
  busy.current = true;
3259
3838
  setSaving(true);
3260
3839
  try {
@@ -3273,7 +3852,8 @@ function EventEditor({ event, initialStart, onClose }) {
3273
3852
  const i = items.findIndex((x) => x.id === id2);
3274
3853
  return items[i < 0 ? dir > 0 ? 0 : items.length - 1 : (i + dir + items.length) % items.length].id;
3275
3854
  };
3276
- const pick = ({ form: form2, texts: texts2 }, field, dir) => {
3855
+ const pick = (st, field, dir) => {
3856
+ const { form: form2, texts: texts2 } = st;
3277
3857
  if (field === "account") {
3278
3858
  const accountId = cycle2(choices, form2.accountId, dir);
3279
3859
  update({ form: { ...form2, accountId, calendarId: soleId(writableCalendars(calendars, accountId)) } });
@@ -3282,6 +3862,19 @@ function EventEditor({ event, initialStart, onClose }) {
3282
3862
  } else if (field === "allDay") {
3283
3863
  const f = setAllDay(form2, !form2.allDay);
3284
3864
  update({ form: f, texts: { ...texts2, ...timeTexts(f) } });
3865
+ } else if (field === "repeat") {
3866
+ if (form2.repeatWas === void 0) return;
3867
+ const ids = [...presets(form2.start).map((p) => p.id), "custom"];
3868
+ const at3 = ids.indexOf(isCustom(st) ? "custom" : presetOf(form2.repeat, form2.start));
3869
+ const next = ids[(at3 + dir + ids.length) % ids.length];
3870
+ let prev = form2.repeat;
3871
+ try {
3872
+ prev = readRepeat(st);
3873
+ } catch {
3874
+ }
3875
+ const preset = presets(form2.start).find((p) => p.id === next)?.recurrence;
3876
+ const repeat = next === "custom" ? prev && !prev.rule ? prev : withEnd({ freq: "weekly" }, prev) : preset ? withEnd(preset, prev) : null;
3877
+ update({ form: { ...form2, repeat }, custom: next === "custom", texts: { ...texts2, ...repeatTexts(repeat, form2.start) } });
3285
3878
  }
3286
3879
  };
3287
3880
  const edit = ({ form: form2, texts: texts2 }, field, value2) => {
@@ -3301,17 +3894,22 @@ function EventEditor({ event, initialStart, onClose }) {
3301
3894
  const clickField = (f) => {
3302
3895
  const s = latest.current;
3303
3896
  if (!s || busy.current) return;
3304
- const i = fieldsFor(s.form, !!event).indexOf(f);
3897
+ const i = fieldsFor(s, !!event).indexOf(f);
3305
3898
  if (i < 0) return;
3306
- if (i === s.focus && (f === "account" || f === "calendar" || f === "allDay")) return pick(s, f, 1);
3899
+ if (i === s.focus && PICKERS.has(f)) return pick(s, f, 1);
3307
3900
  update({ focus: i });
3308
3901
  };
3309
3902
  useKeys((input, key3) => {
3903
+ if (key3.escape && asking.current && !busy.current) return setAskScope(false);
3310
3904
  if (key3.escape) return onClose();
3311
3905
  const s = latest.current;
3312
3906
  if (!s || busy.current) return;
3907
+ if (asking.current) {
3908
+ if (!Object.hasOwn(SCOPE_KEYS2, input) || asking.current === "rule" && SCOPE_KEYS2[input] === "one") return;
3909
+ return void save(SCOPE_KEYS2[input]);
3910
+ }
3313
3911
  if (key3.ctrl && input === "s") return void save();
3314
- const fields2 = fieldsFor(s.form, !!event);
3912
+ const fields2 = fieldsFor(s, !!event);
3315
3913
  const i = Math.min(s.focus, fields2.length - 1);
3316
3914
  const field = fields2[i];
3317
3915
  const move = (dir) => update({ focus: (i + dir + fields2.length) % fields2.length });
@@ -3319,19 +3917,20 @@ function EventEditor({ event, initialStart, onClose }) {
3319
3917
  if (key3.downArrow) return move(1);
3320
3918
  if (key3.upArrow) return move(-1);
3321
3919
  if (key3.return) return i === fields2.length - 1 ? void save() : move(1);
3322
- if (field === "account" || field === "calendar" || field === "allDay") {
3920
+ if (PICKERS.has(field)) {
3323
3921
  if (key3.leftArrow) pick(s, field, -1);
3324
3922
  else if (key3.rightArrow || input === " ") pick(s, field, 1);
3325
3923
  return;
3326
3924
  }
3327
- if (key3.backspace || key3.delete) return edit(s, field, s.texts[field].slice(0, -1));
3925
+ const text2 = field;
3926
+ if (key3.backspace || key3.delete) return edit(s, text2, s.texts[text2].slice(0, -1));
3328
3927
  if (key3.ctrl || key3.meta || !input) return;
3329
3928
  const printable = input.replace(/[\u0000-\u001f\u007f]/g, "");
3330
- if (printable) edit(s, field, s.texts[field] + printable);
3929
+ if (printable) edit(s, text2, s.texts[text2] + printable);
3331
3930
  });
3332
3931
  if (!state) return /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "Loading\u2026" });
3333
3932
  const { form, texts } = state;
3334
- const fields = fieldsFor(form, !!event);
3933
+ const fields = fieldsFor(state, !!event);
3335
3934
  const current = fields[Math.min(state.focus, fields.length - 1)];
3336
3935
  const account = accounts.find((a) => a.id === form.accountId);
3337
3936
  const calendar = calendars.find((c) => c.accountId === form.accountId && c.id === form.calendarId);
@@ -3346,6 +3945,11 @@ function EventEditor({ event, initialStart, onClose }) {
3346
3945
  return calendar ? { text: calendar.name } : { text: "\u2039 choose calendar \u203A", dim: true };
3347
3946
  case "allDay":
3348
3947
  return { text: form.allDay ? "[x]" : "[ ]" };
3948
+ case "repeat":
3949
+ if (form.repeatWas === void 0) return { text: repeatFailed ? "couldn\u2019t load; stays as it is" : "loading\u2026", dim: true };
3950
+ if (form.repeat?.rule) return { text: describe(form.repeat, form.start) };
3951
+ if (isCustom(state)) return { text: "Custom" };
3952
+ return { text: presets(form.start).find((p) => p.id === presetOf(form.repeat, form.start)).label };
3349
3953
  default:
3350
3954
  return texts[f] ? { text: texts[f] } : { text: HINTS[f] ?? "", dim: true };
3351
3955
  }
@@ -3356,7 +3960,7 @@ function EventEditor({ event, initialStart, onClose }) {
3356
3960
  rows.map((f) => {
3357
3961
  const on = f === current;
3358
3962
  const v = value(f);
3359
- const picker = !event && (f === "account" || f === "calendar");
3963
+ const picker = !event && (f === "account" || f === "calendar") || f === "repeat";
3360
3964
  return /* @__PURE__ */ jsxs6(Clickable, { onClick: () => clickField(f), children: [
3361
3965
  /* @__PURE__ */ jsxs6(Text6, { color: on ? C.cyan : void 0, children: [
3362
3966
  on ? "\u203A " : " ",
@@ -3368,7 +3972,13 @@ function EventEditor({ event, initialStart, onClose }) {
3368
3972
  }),
3369
3973
  /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: account ? `Organizer: ${account.email}` : "Choose which account this event belongs to." }) }),
3370
3974
  error && /* @__PURE__ */ jsx8(Text6, { color: C.red, children: error }),
3371
- saving ? /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: "Saving\u2026" }) : /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
3975
+ saving ? /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: "Saving\u2026" }) : askScope ? /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
3976
+ /* @__PURE__ */ jsx8(Text6, { color: C.yellow, children: askScope === "rule" ? "Change the repeat rule for: " : "Save recurring event: " }),
3977
+ askScope !== "rule" && /* @__PURE__ */ jsx8(Button, { k: "1", label: "this event", color: C.green, onPress: () => void save("one") }),
3978
+ /* @__PURE__ */ jsx8(Button, { k: "2", label: "this and following", color: C.green, onPress: () => void save("following") }),
3979
+ /* @__PURE__ */ jsx8(Button, { k: "3", label: "all events", color: C.green, onPress: () => void save("all") }),
3980
+ /* @__PURE__ */ jsx8(Button, { k: "esc", label: "back", onPress: () => setAskScope(false) })
3981
+ ] }) : /* @__PURE__ */ jsxs6(Box6, { flexWrap: "wrap", children: [
3372
3982
  /* @__PURE__ */ jsx8(Box6, { marginRight: 2, children: /* @__PURE__ */ jsx8(Text6, { color: C.muted, children: "tab/\u2191\u2193 move \xB7 \u2190/\u2192/space pick" }) }),
3373
3983
  /* @__PURE__ */ jsx8(Button, { k: "ctrl+s", label: "save", color: C.green, onPress: () => void save() }),
3374
3984
  /* @__PURE__ */ jsx8(Button, { k: "esc", label: "cancel", onPress: onClose })
@@ -3391,12 +4001,12 @@ import { Box as Box8, Text as Text8 } from "ink";
3391
4001
  // src/ui/screens/Invites.tsx
3392
4002
  import { useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
3393
4003
  import { Box as Box7, Text as Text7 } from "ink";
3394
- import { addDays as addDays8, startOfDay as startOfDay6 } from "date-fns";
4004
+ import { addDays as addDays10, startOfDay as startOfDay6 } from "date-fns";
3395
4005
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3396
4006
  var INVITE_DAYS = 60;
3397
4007
  function useUpcoming(now) {
3398
4008
  const day = startOfDay6(now).getTime();
3399
- const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays8(day, INVITE_DAYS).toISOString() }), [day]);
4009
+ const range = useMemo5(() => ({ start: new Date(day).toISOString(), end: addDays10(day, INVITE_DAYS).toISOString() }), [day]);
3400
4010
  return useEvents(range);
3401
4011
  }
3402
4012
  var REPLY = { y: "accepted", n: "declined", m: "tentative" };
@@ -3574,12 +4184,12 @@ var NAMES = {
3574
4184
  };
3575
4185
  function useKeyState(init) {
3576
4186
  const ref = useRef5(init);
3577
- const [, bump] = useState6(0);
4187
+ const [, bump2] = useState6(0);
3578
4188
  return [
3579
4189
  () => ref.current,
3580
4190
  (next) => {
3581
4191
  ref.current = next;
3582
- bump((n) => n + 1);
4192
+ bump2((n) => n + 1);
3583
4193
  }
3584
4194
  ];
3585
4195
  }
@@ -3996,9 +4606,9 @@ function FirstSyncCheck({ width, height, children }) {
3996
4606
  ] }) });
3997
4607
  }
3998
4608
  function title({ view, date }) {
3999
- if (view === "month") return format11(date, "MMMM yyyy");
4000
- if (view === "day") return format11(date, "EEE d MMM yyyy");
4001
- const days = view === "agenda" ? [date, addDays9(date, AGENDA_DAYS - 1)] : view === "2day" ? [date, addDays9(date, 1)] : viewDays(view, date);
4609
+ if (view === "month") return format13(date, "MMMM yyyy");
4610
+ if (view === "day") return format13(date, "EEE d MMM yyyy");
4611
+ const days = view === "agenda" ? [date, addDays11(date, AGENDA_DAYS - 1)] : view === "2day" ? [date, addDays11(date, 1)] : viewDays(view, date);
4002
4612
  return rangeLabel(days[0], days[days.length - 1]);
4003
4613
  }
4004
4614
  var chronological = (a, b) => eventBounds(a).start.getTime() - eventBounds(b).start.getTime() || Number(b.allDay) - Number(a.allDay);
@@ -4076,8 +4686,8 @@ function Shell({ initialNav, updateCheck, notice }) {
4076
4686
  const { cursor: from, view, date, key: key3 } = live.current;
4077
4687
  const sel = ordered2.find((e) => eventKey(e) === key3);
4078
4688
  const minute = sel ? sel.allDay ? void 0 : Math.max(differenceInMinutes3(eventBounds(sel).start, from), 0) : live.current.minute;
4079
- const day = addDays9(from, n);
4080
- const nextDate = onPageOf({ view, date }, day) ? date : view === "2day" && n > 0 ? addDays9(day, -1) : day;
4689
+ const day = addDays11(from, n);
4690
+ const nextDate = onPageOf({ view, date }, day) ? date : view === "2day" && n > 0 ? addDays11(day, -1) : day;
4081
4691
  Object.assign(live.current, { cursor: day, date: nextDate, minute });
4082
4692
  setCursor(day);
4083
4693
  if (+nextDate !== +date) setDate(nextDate);