mysticals 0.4.0 → 0.6.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 (2) hide show
  1. package/dist/cli.js +147 -41
  2. 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.4.0";
8
+ var version = "0.6.0";
9
9
 
10
10
  // src/client.ts
11
11
  import { spawn } from "child_process";
@@ -46,6 +46,7 @@ var methodSet = {
46
46
  "accounts.addCaldav": true,
47
47
  "accounts.update": true,
48
48
  "accounts.remove": true,
49
+ "accounts.reauth": true,
49
50
  "calendars.list": true,
50
51
  "calendars.setVisible": true,
51
52
  "events.list": true,
@@ -239,6 +240,21 @@ function when(e, now) {
239
240
  return `${isSameDay(s, now) ? "Today" : format(s, "EEE, d MMM")} \xB7 ${format(s, "HH:mm")}`;
240
241
  }
241
242
 
243
+ // ../../packages/core/src/providers/http.ts
244
+ var http = { timeoutMs: 3e4 };
245
+ var timedFetch = async (input, init) => {
246
+ const timeout = AbortSignal.timeout(http.timeoutMs);
247
+ try {
248
+ return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
249
+ } catch (e) {
250
+ if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
251
+ throw e;
252
+ }
253
+ };
254
+ var AuthError = class extends Error {
255
+ name = "AuthError";
256
+ };
257
+
242
258
  // ../../packages/core/src/sync/engine.ts
243
259
  var MIN = 6e4;
244
260
  var SyncEngine = class {
@@ -261,6 +277,8 @@ var SyncEngine = class {
261
277
  queued = /* @__PURE__ */ new Map();
262
278
  /** Bumped by `edited`: a pass that started before a local edit must not overwrite it. */
263
279
  edits = /* @__PURE__ */ new Map();
280
+ /** Accounts whose last pass listed no calendars while the cache had some (see syncAccount). */
281
+ emptied = /* @__PURE__ */ new Set();
264
282
  running = false;
265
283
  unsubscribe;
266
284
  start() {
@@ -280,6 +298,7 @@ var SyncEngine = class {
280
298
  /**
281
299
  * Sync one account, or all accounts independently when id omitted.
282
300
  * A single-account call rejects with that account's error; the all-accounts call never rejects.
301
+ * The all-accounts call skips accounts whose credentials were rejected (`authError`): retrying can't help.
283
302
  */
284
303
  /**
285
304
  * `quiet`: the change came from this app (own edit/RSVP), so don't notify about it.
@@ -288,7 +307,7 @@ var SyncEngine = class {
288
307
  */
289
308
  async syncNow(accountId, opts = {}) {
290
309
  if (accountId === void 0) {
291
- await Promise.allSettled(this.store.list().map((a) => this.syncNow(a.id)));
310
+ await Promise.allSettled(this.store.list().filter((a) => !a.authError).map((a) => this.syncNow(a.id)));
292
311
  return;
293
312
  }
294
313
  const existing = this.inflight.get(accountId);
@@ -326,7 +345,8 @@ var SyncEngine = class {
326
345
  schedule(id2) {
327
346
  clearTimeout(this.timers.get(id2));
328
347
  this.timers.delete(id2);
329
- if (!this.running || !this.store.list().some((a) => a.id === id2)) {
348
+ const acc = this.store.list().find((a) => a.id === id2);
349
+ if (!this.running || !acc || acc.authError) {
330
350
  this.failures.delete(id2);
331
351
  return;
332
352
  }
@@ -340,7 +360,8 @@ var SyncEngine = class {
340
360
  }
341
361
  // ponytail: quiet silences the whole sync, so an external change landing in the same pass is not announced.
342
362
  async syncAccount(id2, quiet) {
343
- const hadError = !!this.store.list().find((a) => a.id === id2)?.error;
363
+ const acc = this.store.list().find((a) => a.id === id2);
364
+ const hadError = !!acc?.error;
344
365
  const edits = this.edits.get(id2) ?? 0;
345
366
  try {
346
367
  const provider = this.store.getProvider(id2);
@@ -363,10 +384,15 @@ var SyncEngine = class {
363
384
  } catch {
364
385
  prev = void 0;
365
386
  }
387
+ if (!calendars.length && prev?.calendars.length && !this.emptied.has(id2)) {
388
+ this.emptied.add(id2);
389
+ throw new Error("The server returned no calendars; showing the last synced data");
390
+ }
391
+ this.emptied.delete(id2);
366
392
  const changed = !prev || JSON.stringify([prev.calendars, prev.events]) !== JSON.stringify([next.calendars, next.events]);
367
393
  await this.store.writeCache(id2, next);
368
394
  this.failures.delete(id2);
369
- if (hadError) await this.store.update(id2, { error: void 0 });
395
+ if (hadError) await this.store.update(id2, { error: void 0, authError: false });
370
396
  if (changed || hadError) this.onChanged(id2);
371
397
  if (changed && !quiet && prev?.syncedAt && this.opts.onEvents) {
372
398
  const notes = diffEvents(prev.events, next.events, now);
@@ -375,9 +401,10 @@ var SyncEngine = class {
375
401
  } catch (e) {
376
402
  this.failures.set(id2, (this.failures.get(id2) ?? 0) + 1);
377
403
  const message = e instanceof Error ? e.message : String(e);
378
- await this.store.update(id2, { error: message }).catch(() => {
404
+ const auth = e instanceof AuthError;
405
+ await this.store.update(id2, { error: message, ...auth ? { authError: true } : {} }).catch(() => {
379
406
  });
380
- if (!hadError) this.onChanged(id2);
407
+ if (!hadError || auth && !acc?.authError) this.onChanged(id2);
381
408
  throw e;
382
409
  }
383
410
  }
@@ -527,6 +554,7 @@ var CaldavInput = z.object({
527
554
  password: text(2e3).min(1),
528
555
  color: color.optional()
529
556
  });
557
+ var ReauthInput = z.object({ password: text(2e3).min(1) }).strict();
530
558
  var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
531
559
  var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
532
560
  var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
@@ -592,6 +620,26 @@ function createApi(store, sync, deps) {
592
620
  deps.onChanged?.(a.id);
593
621
  return a;
594
622
  },
623
+ reauth: async (accountId, raw) => {
624
+ const a = account(accountId);
625
+ let credentials;
626
+ if (a.kind === "google") {
627
+ const signed = await deps.googleSignIn();
628
+ if (signed.email.toLowerCase() !== a.email.toLowerCase())
629
+ throw new Error(`Signed in as ${signed.email}; sign in as ${a.email} to reconnect this account`);
630
+ credentials = signed.credentials;
631
+ } else {
632
+ const { password } = ReauthInput.parse(raw);
633
+ const old = store.readCreds(a.id);
634
+ if (old.kind !== "caldav") throw new Error("Credentials kind does not match account kind");
635
+ await deps.verifyCaldav({ label: a.label, serverUrl: old.serverUrl, username: old.username, password });
636
+ credentials = { ...old, password };
637
+ }
638
+ const updated = await store.update(a.id, { credentials, error: void 0, authError: false });
639
+ deps.onChanged?.(a.id);
640
+ syncOne(a.id);
641
+ return updated;
642
+ },
595
643
  remove: async (accountId) => {
596
644
  const a = account(accountId);
597
645
  await store.remove(a.id);
@@ -740,6 +788,7 @@ var AccountStore = class {
740
788
  await mkdir(dir, { recursive: true });
741
789
  await writeAtomic(join4(dir, "creds.bin"), data);
742
790
  }
791
+ /** Decrypted credentials; main/daemon only (accounts.reauth keeps the server and username), never to the renderer. */
743
792
  readCreds(id2) {
744
793
  return JSON.parse(this.crypto.decrypt(readFileSync2(join4(this.accountDir(id2), "creds.bin"))));
745
794
  }
@@ -775,6 +824,10 @@ var AccountStore = class {
775
824
  if (patch.error) acc.error = patch.error;
776
825
  else delete acc.error;
777
826
  }
827
+ if ("authError" in patch) {
828
+ if (patch.authError) acc.authError = true;
829
+ else delete acc.authError;
830
+ }
778
831
  await this.saveRegistry();
779
832
  return { ...acc };
780
833
  });
@@ -1118,7 +1171,15 @@ var iso = (dayOffset, h, m = 0) => {
1118
1171
  };
1119
1172
  function createMockApi(onChanged) {
1120
1173
  const accounts = [
1121
- { id: "work", kind: "caldav", label: "Work", email: "me@work.example", color: "#8be9fd" },
1174
+ {
1175
+ id: "work",
1176
+ kind: "caldav",
1177
+ label: "Work",
1178
+ email: "me@work.example",
1179
+ color: "#8be9fd",
1180
+ // e2e: a revoked app password; accounts.reauth with any password fixes it.
1181
+ ...process.env.MYSTICALS_MOCK_AUTH_ERROR ? { error: "CalDAV login failed: the password was changed or revoked", authError: true } : {}
1182
+ },
1122
1183
  { id: "personal", kind: "google", label: "Personal", email: "me@gmail.example", color: "#50fa7b" }
1123
1184
  ];
1124
1185
  const providers = /* @__PURE__ */ new Map([
@@ -1157,6 +1218,12 @@ function createMockApi(onChanged) {
1157
1218
  myStatus: "needsAction",
1158
1219
  attendees: [{ email: "pm@work.example", status: "accepted", organizer: true }, { email: "me@work.example", status: "needsAction", self: true }]
1159
1220
  });
1221
+ seed(work, "work-main", {
1222
+ title: "Quarterly roadmap sync with design",
1223
+ start: iso(0, 11, 30),
1224
+ end: iso(0, 12),
1225
+ location: "Zoom https://zoom.us/j/1234567890?pwd=abc"
1226
+ });
1160
1227
  seed(personal, "p-main", { title: "Gym", start: iso(0, 19), end: iso(0, 20) });
1161
1228
  for (let d = -2; d <= 4; d++) {
1162
1229
  seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
@@ -1165,6 +1232,8 @@ function createMockApi(onChanged) {
1165
1232
  seed(personal, "p-main", { title: "Dinner with friends", start: iso(2, 20), end: iso(2, 22), location: "Kyiv" });
1166
1233
  const day = (offset) => format4(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
1167
1234
  seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
1235
+ for (const title2 of ["Conference", "Team offsite", "Release freeze", "Birthday"])
1236
+ seed(personal, "p-main", { title: title2, start: day(3), end: day(4), allDay: true });
1168
1237
  const prov = (id2) => {
1169
1238
  const p = providers.get(id2);
1170
1239
  if (!p) throw new Error(`unknown account ${id2}`);
@@ -1191,6 +1260,14 @@ function createMockApi(onChanged) {
1191
1260
  onChanged(id2);
1192
1261
  return structuredClone(a);
1193
1262
  },
1263
+ reauth: async (id2) => {
1264
+ const a = accounts.find((x) => x.id === id2);
1265
+ if (!a) throw new Error("unknown account");
1266
+ delete a.error;
1267
+ delete a.authError;
1268
+ onChanged(id2);
1269
+ return structuredClone(a);
1270
+ },
1194
1271
  remove: async (id2) => {
1195
1272
  const i = accounts.findIndex((a) => a.id === id2);
1196
1273
  if (i >= 0) accounts.splice(i, 1);
@@ -1692,18 +1769,6 @@ function applyRespond(raw, email, status) {
1692
1769
  return root.toString();
1693
1770
  }
1694
1771
 
1695
- // ../../packages/core/src/providers/http.ts
1696
- var http = { timeoutMs: 3e4 };
1697
- var timedFetch = async (input, init) => {
1698
- const timeout = AbortSignal.timeout(http.timeoutMs);
1699
- try {
1700
- return await fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
1701
- } catch (e) {
1702
- if (timeout.aborted) throw new Error(`Request to ${new URL(String(input instanceof Request ? input.url : input)).host} timed out`);
1703
- throw e;
1704
- }
1705
- };
1706
-
1707
1772
  // ../../packages/core/src/providers/caldav/index.ts
1708
1773
  var FALLBACK_COLOR = "#8e8e93";
1709
1774
  var DAY = 864e5;
@@ -1728,7 +1793,7 @@ async function connect2(serverUrl, username, password) {
1728
1793
  });
1729
1794
  return { account, headers };
1730
1795
  } catch (e) {
1731
- if (unauthorized) throw new Error("CalDAV login failed: wrong username or password");
1796
+ if (unauthorized) throw new AuthError("CalDAV login failed: wrong username or password");
1732
1797
  throw new Error(`Could not find a CalDAV service at ${serverUrl}: ${e.message}`);
1733
1798
  }
1734
1799
  }
@@ -1754,10 +1819,15 @@ async function listVeventCalendars(conn) {
1754
1819
  headers: conn.headers,
1755
1820
  props: CALENDAR_PROPS,
1756
1821
  projectedProps: { currentUserPrivilegeSet: true },
1757
- fetch: timedFetch
1822
+ fetch: davFetch
1758
1823
  });
1759
1824
  return cals.filter((c) => !c.components?.length || c.components.includes("VEVENT"));
1760
1825
  }
1826
+ var davFetch = async (input, init) => {
1827
+ const res = await timedFetch(input, init);
1828
+ if (res.status === 401) throw new AuthError("CalDAV login failed: the password was changed or revoked");
1829
+ return res;
1830
+ };
1761
1831
  function check(res, what) {
1762
1832
  if (res.ok) return;
1763
1833
  if (res.status === 412) throw new Error(`${what} failed: the event was changed on the server, refresh and try again`);
@@ -1777,7 +1847,7 @@ var createCaldavProvider = (ctx) => {
1777
1847
  const mapCtx = (calendarId) => ({ accountId: ctx.accountId, calendarId, email: ctx.email });
1778
1848
  async function reload(calendarId, href, id2, around) {
1779
1849
  const { headers } = await getConn();
1780
- const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: timedFetch });
1850
+ const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: davFetch });
1781
1851
  if (!obj?.data) throw new Error("Saved event could not be read back from the server");
1782
1852
  const range = { start: new Date(new Date(around.start).getTime() - DAY).toISOString(), end: new Date(new Date(around.end).getTime() + DAY).toISOString() };
1783
1853
  const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
@@ -1789,7 +1859,7 @@ var createCaldavProvider = (ctx) => {
1789
1859
  const put = async (event, ics, what) => {
1790
1860
  const { headers } = await getConn();
1791
1861
  const raw = event.raw;
1792
- check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: timedFetch }), what);
1862
+ check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: davFetch }), what);
1793
1863
  };
1794
1864
  const provider = {
1795
1865
  async listCalendars() {
@@ -1798,7 +1868,7 @@ var createCaldavProvider = (ctx) => {
1798
1868
  },
1799
1869
  async listEvents(calendarId, range) {
1800
1870
  const { headers } = await getConn();
1801
- const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: timedFetch });
1871
+ const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: davFetch });
1802
1872
  return objs.flatMap((o) => o.data ? parseEvents(o.data, o.url, o.etag, mapCtx(calendarId), range) : []);
1803
1873
  },
1804
1874
  async createEvent(calendarId, input) {
@@ -1806,7 +1876,7 @@ var createCaldavProvider = (ctx) => {
1806
1876
  const uid = randomUUID4();
1807
1877
  const filename = `${uid}.ics`;
1808
1878
  const ics = buildIcs(uid, input, ctx.email);
1809
- check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: timedFetch }), "Create event");
1879
+ check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: davFetch }), "Create event");
1810
1880
  const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
1811
1881
  return reload(calendarId, href, void 0, input);
1812
1882
  },
@@ -1825,7 +1895,7 @@ var createCaldavProvider = (ctx) => {
1825
1895
  await put(event, split.rest, "Update event");
1826
1896
  const { headers } = await getConn();
1827
1897
  const filename = `${uid}.ics`;
1828
- check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: timedFetch }), "Update event");
1898
+ check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: davFetch }), "Update event");
1829
1899
  const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
1830
1900
  return reload(event.calendarId, href, void 0, event);
1831
1901
  },
@@ -1834,7 +1904,7 @@ var createCaldavProvider = (ctx) => {
1834
1904
  const rest = !raw.recurrenceId || scope === "all" ? null : scope === "following" ? applyDeleteFollowing(raw) : applyDeleteInstance(raw);
1835
1905
  if (rest) return put(event, rest, "Delete event");
1836
1906
  const { headers } = await getConn();
1837
- check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: timedFetch }), "Delete event");
1907
+ check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: davFetch }), "Delete event");
1838
1908
  },
1839
1909
  async getRecurrence(event) {
1840
1910
  return recurrenceOf(event.raw);
@@ -1849,7 +1919,7 @@ var createCaldavProvider = (ctx) => {
1849
1919
  async function verifyCaldav(input) {
1850
1920
  const c = await connect2(input.serverUrl.trim(), input.username, input.password);
1851
1921
  await listVeventCalendars(c);
1852
- const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: timedFetch }).catch(() => []);
1922
+ const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: davFetch }).catch(() => []);
1853
1923
  const mails = addresses.filter((a) => /^mailto:/i.test(a)).map(cleanEmail);
1854
1924
  const login = cleanEmail(input.username);
1855
1925
  const email = mails.find((m) => m === login) ?? mails[0] ?? (login.includes("@") ? login : void 0);
@@ -1902,7 +1972,10 @@ function emailFromIdToken(idToken) {
1902
1972
  }
1903
1973
  function parseTokenResponse(json, now = Date.now()) {
1904
1974
  const j = json ?? {};
1905
- if (typeof j.error === "string") throw new Error(`Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`);
1975
+ if (typeof j.error === "string") {
1976
+ const msg = `Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`;
1977
+ throw j.error === "invalid_grant" ? new AuthError(msg) : new Error(msg);
1978
+ }
1906
1979
  if (typeof j.access_token !== "string") throw new Error("Google token response has no access_token");
1907
1980
  const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 3600;
1908
1981
  return {
@@ -2074,6 +2147,7 @@ function createGoogleProviderImpl(ctx) {
2074
2147
  });
2075
2148
  let res = await send(await token());
2076
2149
  if (res.status === 401) res = await send(await refresh());
2150
+ if (res.status === 401) throw new AuthError("Google rejected the sign-in; sign in again");
2077
2151
  if (res.status === 204 || method === "DELETE" && res.status === 410) return void 0;
2078
2152
  if (!res.ok) {
2079
2153
  const err = await res.json().catch(() => ({}));
@@ -3041,9 +3115,8 @@ function meetingUrl(location) {
3041
3115
  return location?.match(/https?:\/\/[^\s<>"]+/i)?.[0].replace(/[.,;:!?)\]}'>]+$/, "");
3042
3116
  }
3043
3117
  function place(location = "") {
3044
- const url = meetingUrl(location);
3045
- const rest = (url ? location.replace(url, "") : location).replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
3046
- return rest || (url ? "video call" : "");
3118
+ const rest = location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
3119
+ return rest || (meetingUrl(location) ? "video call" : "");
3047
3120
  }
3048
3121
 
3049
3122
  // ../../packages/core/src/logic/status.ts
@@ -4123,9 +4196,16 @@ function StatusLine({ now, message, update, width, onOpen, onInvites, onAccounts
4123
4196
  invites === 1 ? "1 invite" : `${invites} invites`,
4124
4197
  " (i)"
4125
4198
  ] }), onInvites]);
4126
- if (failed.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
4199
+ const off = failed.filter((a) => a.authError);
4200
+ const broken = failed.filter((a) => !a.authError);
4201
+ if (off.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
4127
4202
  "\u26A0 ",
4128
- failed.map((a) => a.label).join(", "),
4203
+ off.map((a) => a.label).join(", "),
4204
+ " disconnected (s)"
4205
+ ] }), onAccounts]);
4206
+ if (broken.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
4207
+ "\u26A0 ",
4208
+ broken.map((a) => a.label).join(", "),
4129
4209
  " sync failed (s)"
4130
4210
  ] }), onAccounts]);
4131
4211
  if (message) parts.push([message]);
@@ -4331,13 +4411,13 @@ function Accounts({ onClose }) {
4331
4411
  (e) => say(errorText(e), true)
4332
4412
  );
4333
4413
  };
4334
- const addGoogle = () => {
4414
+ const googleSignIn2 = (fn, done) => {
4335
4415
  const s = ++googleSession.current;
4336
4416
  setMode({ kind: "google" });
4337
4417
  setMessage(void 0);
4338
4418
  setAuthUrl(void 0);
4339
- api.accounts.addGoogle().then(
4340
- (a) => s === googleSession.current && (setMode({ kind: "list" }), say(`Added ${a.label}`)),
4419
+ fn().then(
4420
+ (a) => s === googleSession.current && (setMode({ kind: "list" }), say(done(a))),
4341
4421
  (e) => s === googleSession.current && (setMode({ kind: "list" }), say(errorText(e), true))
4342
4422
  );
4343
4423
  };
@@ -4345,7 +4425,7 @@ function Accounts({ onClose }) {
4345
4425
  const act = (input) => {
4346
4426
  const { row: row2 } = current();
4347
4427
  if (input === "R") return run("Syncing all\u2026", "Synced all", () => api.sync.now());
4348
- if (input === "g") return addGoogle();
4428
+ if (input === "g") return googleSignIn2(() => api.accounts.addGoogle(), (a2) => `Added ${a2.label}`);
4349
4429
  if (input === "a") return setMode({ kind: "caldav" });
4350
4430
  if (!row2) return;
4351
4431
  const a = row2.account;
@@ -4364,6 +4444,11 @@ function Accounts({ onClose }) {
4364
4444
  return run("Saving\u2026", `${a.label} colour changed`, () => api.accounts.update(a.id, { color: color2 }));
4365
4445
  }
4366
4446
  if (input === "x") return setMode({ kind: "confirm", account: a });
4447
+ if (input === "p" && a.authError) {
4448
+ if (a.kind === "google") return googleSignIn2(() => api.accounts.reauth(a.id), () => `${a.label} reconnected`);
4449
+ setMessage(void 0);
4450
+ return setMode({ kind: "reauth", account: a, value: "" });
4451
+ }
4367
4452
  };
4368
4453
  const confirmRemove = (yes) => {
4369
4454
  const mode2 = getMode();
@@ -4389,6 +4474,19 @@ function Accounts({ onClose }) {
4389
4474
  return;
4390
4475
  }
4391
4476
  if (mode2.kind === "confirm") return confirmRemove(input.toLowerCase() === "y");
4477
+ if (mode2.kind === "reauth") {
4478
+ if (key3.escape) return setMode({ kind: "list" });
4479
+ if (key3.return) {
4480
+ if (!mode2.value) return;
4481
+ const { id: id2, label } = mode2.account;
4482
+ const password = mode2.value;
4483
+ run("Reconnecting\u2026", `${label} reconnected`, () => api.accounts.reauth(id2, { password }));
4484
+ return setMode({ kind: "list" });
4485
+ }
4486
+ const value = editText(mode2.value, input, key3);
4487
+ if (value !== void 0) setMode({ ...mode2, value });
4488
+ return;
4489
+ }
4392
4490
  if (mode2.kind === "rename") {
4393
4491
  if (key3.escape) return setMode({ kind: "list" });
4394
4492
  if (key3.return) {
@@ -4467,7 +4565,14 @@ function Accounts({ onClose }) {
4467
4565
  /* @__PURE__ */ jsx12(Spinner, {}),
4468
4566
  " Loading calendars\u2026"
4469
4567
  ] }),
4470
- a.error && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Last sync failed: ${a.error}` })
4568
+ a.authError ? /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Disconnected: ${a.error ?? "credentials rejected"} \u2014 p to reconnect` }) : a.error && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Last sync failed: ${a.error}` }),
4569
+ mode.kind === "reauth" && mode.account.id === a.id && /* @__PURE__ */ jsxs10(Text10, { children: [
4570
+ " New app password: ",
4571
+ /* @__PURE__ */ jsxs10(Text10, { color: C.cyan, children: [
4572
+ "\u2022".repeat(mode.value.length),
4573
+ "\u258F"
4574
+ ] })
4575
+ ] })
4471
4576
  ] }, a.id);
4472
4577
  }),
4473
4578
  mode.kind === "confirm" && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginTop: 1, children: [
@@ -4496,7 +4601,7 @@ function Accounts({ onClose }) {
4496
4601
  /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: `Over SSH, forward its port first: ssh -L ${loopbackPort(authUrl)}:127.0.0.1:${loopbackPort(authUrl)} <host>` })
4497
4602
  ] })
4498
4603
  ] }),
4499
- mode.kind === "rename" && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "enter save esc cancel" }),
4604
+ (mode.kind === "rename" || mode.kind === "reauth") && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "enter save esc cancel" }),
4500
4605
  loadError && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: loadError }),
4501
4606
  message && /* @__PURE__ */ jsx12(Text10, { color: message.error ? C.red : C.green, children: message.text }),
4502
4607
  mode.kind === "list" && /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexWrap: "wrap", children: [
@@ -4507,6 +4612,7 @@ function Accounts({ onClose }) {
4507
4612
  row && button("e", "rename"),
4508
4613
  row && button("c", "colour"),
4509
4614
  row && button("x", "remove"),
4615
+ row?.account.authError && button("p", "reconnect"),
4510
4616
  button("g", "add Google"),
4511
4617
  button("a", "add CalDAV"),
4512
4618
  /* @__PURE__ */ jsx12(Button, { k: "esc", label: "close", onPress: onClose })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysticals",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Isolated multi-account calendar for the terminal on macOS, Linux and Windows",
5
5
  "keywords": [
6
6
  "calendar",
@@ -47,7 +47,7 @@
47
47
  "tsup": "^8.5.1",
48
48
  "typescript": "^5.9.3",
49
49
  "vitest": "^3.2.7",
50
- "@mysticals/core": "0.4.0"
50
+ "@mysticals/core": "0.6.0"
51
51
  },
52
52
  "scripts": {
53
53
  "dev": "tsup --watch",