@bobfrankston/gcal 0.1.61 → 0.1.63
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.
- package/gcal.js +472 -105
- package/gcal.js.map +1 -1
- package/gcal.ts +487 -98
- package/glib/goauth.d.ts +1 -0
- package/glib/goauth.d.ts.map +1 -1
- package/glib/goauth.js +21 -2
- package/glib/goauth.js.map +1 -1
- package/glib/goauth.ts +21 -2
- package/glib/gutils.d.ts.map +1 -1
- package/glib/gutils.js +15 -0
- package/glib/gutils.js.map +1 -1
- package/glib/gutils.ts +14 -0
- package/gtask.js +19 -7
- package/gtask.js.map +1 -1
- package/gtask.ts +19 -7
- package/package.json +1 -1
package/gcal.ts
CHANGED
|
@@ -12,13 +12,13 @@ import fs from 'fs';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { execSync } from 'child_process';
|
|
14
14
|
import { createInterface } from 'readline/promises';
|
|
15
|
-
import type { GoogleEvent, EventsListResponse, CalendarListEntry, CalendarListResponse } from './glib/types.ts';
|
|
15
|
+
import type { GoogleEvent, EventDateTime, EventsListResponse, CalendarListEntry, CalendarListResponse } from './glib/types.ts';
|
|
16
16
|
import {
|
|
17
17
|
loadConfig, saveConfig,
|
|
18
18
|
formatDateTime, formatDuration, parseDuration, parseDateTime,
|
|
19
19
|
hasTimeComponent, parseAllDay, formatYMD, normalizeUser
|
|
20
20
|
} from './glib/gutils.js';
|
|
21
|
-
import { setupAbortHandler, getAccessToken, apiFetch } from './glib/goauth.js';
|
|
21
|
+
import { setupAbortHandler, teardownAbortHandler, getAccessToken, apiFetch } from './glib/goauth.js';
|
|
22
22
|
import { extractEventsFromText, readClipboard } from './glib/aihelper.js';
|
|
23
23
|
|
|
24
24
|
import pkg from './package.json' with { type: 'json' };
|
|
@@ -59,6 +59,38 @@ async function listEvents(
|
|
|
59
59
|
return data.items || [];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
async function listRecurringEvents(
|
|
63
|
+
accessToken: string,
|
|
64
|
+
calendarId = 'primary',
|
|
65
|
+
maxResults = 250
|
|
66
|
+
): Promise<GoogleEvent[]> {
|
|
67
|
+
// singleEvents=false returns the master records; orderBy=startTime is incompatible
|
|
68
|
+
// so we omit it and sort client-side. Pages until done or maxResults reached.
|
|
69
|
+
const out: GoogleEvent[] = [];
|
|
70
|
+
let pageToken: string | undefined;
|
|
71
|
+
do {
|
|
72
|
+
const params = new URLSearchParams({
|
|
73
|
+
maxResults: '250',
|
|
74
|
+
singleEvents: 'false'
|
|
75
|
+
});
|
|
76
|
+
if (pageToken) params.set('pageToken', pageToken);
|
|
77
|
+
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
|
|
78
|
+
const res = await apiFetch(url, accessToken);
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new Error(`Failed to list events: ${res.status} ${res.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
const data = await res.json() as EventsListResponse & { nextPageToken?: string };
|
|
83
|
+
for (const ev of data.items || []) {
|
|
84
|
+
if (ev.recurrence && ev.status !== 'cancelled') {
|
|
85
|
+
out.push(ev);
|
|
86
|
+
if (out.length >= maxResults) return out;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
pageToken = data.nextPageToken;
|
|
90
|
+
} while (pageToken);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
async function createEvent(
|
|
63
95
|
accessToken: string,
|
|
64
96
|
event: GoogleEvent,
|
|
@@ -258,12 +290,14 @@ Commands:
|
|
|
258
290
|
show Show full details for an event (-json for JSON)
|
|
259
291
|
open Open event in browser
|
|
260
292
|
add Add event (explicit, AI, or interactive)
|
|
293
|
+
update | edit | set Change an event's time/title/location/busy/...
|
|
261
294
|
del | delete Delete event(s) by ID
|
|
262
295
|
remind Add reminder(s) to existing event
|
|
263
296
|
resched Reschedule event
|
|
264
297
|
snooze Snooze event (default: +1d)
|
|
265
298
|
import Import events from ICS file
|
|
266
299
|
calendars | listc | list-calendars List available calendars
|
|
300
|
+
listr | list-recurring List recurring event masters (RRULE)
|
|
267
301
|
assoc Set up .ics file association (Windows)
|
|
268
302
|
help [command] Show help
|
|
269
303
|
|
|
@@ -305,20 +339,55 @@ const USAGE: Record<string, string> = {
|
|
|
305
339
|
Examples:
|
|
306
340
|
gcal open abc12345
|
|
307
341
|
`,
|
|
308
|
-
add: `gcal add <title> <when> [duration] Explicit
|
|
342
|
+
add: `gcal add <title> <when> [duration] Explicit (timed)
|
|
343
|
+
gcal add <title> <date> [days] -allday Explicit (all-day)
|
|
309
344
|
gcal add "<free text>" AI-parsed single arg
|
|
310
345
|
gcal add -clip AI-parsed from clipboard
|
|
311
346
|
gcal add Interactive (type description)
|
|
312
347
|
Add a calendar event. Default duration 1h. Use -r <dur> to add reminder(s).
|
|
313
348
|
|
|
349
|
+
-allday Create an all-day event. The third arg is a day count
|
|
350
|
+
(default 1); the event spans that many days.
|
|
351
|
+
-free Mark the event as Free (does not block time / not busy).
|
|
352
|
+
-busy Mark the event as Busy (the default).
|
|
353
|
+
-open Open the event in the browser after creating it.
|
|
354
|
+
|
|
314
355
|
Examples:
|
|
315
356
|
gcal add "Dentist" "Friday 3pm" "1h"
|
|
316
357
|
gcal add "Lunch" "1/14/2026 12:00" "1h"
|
|
317
358
|
gcal add "Meeting" "tomorrow 10:00"
|
|
318
359
|
gcal add "Appointment" "jan 15 2pm"
|
|
360
|
+
gcal add "Vacation" "jul 1" -allday (1 day, all-day)
|
|
361
|
+
gcal add "Conference" "jul 1" 3 -allday (3-day all-day event)
|
|
362
|
+
gcal add "Out of office" "jul 1" -allday -free (all-day, not busy)
|
|
319
363
|
gcal add "Dentist appointment Friday 3pm for 1 hour"
|
|
320
364
|
gcal add -clip
|
|
321
365
|
gcal add "Dentist" "Friday 3pm" -r 30m
|
|
366
|
+
`,
|
|
367
|
+
update: `gcal update <id> [options]
|
|
368
|
+
gcal edit <id> ... (aliases: edit, set)
|
|
369
|
+
Change settings on an existing event. Only the options you give are
|
|
370
|
+
changed. Searches up to 30 days back; widen with -since.
|
|
371
|
+
|
|
372
|
+
-when <when> New start time/date (accepts +Nd / +Nh advances too).
|
|
373
|
+
-dur <dur> New duration ("1h30m"); for all-day events, a day count.
|
|
374
|
+
-title <text> New event title.
|
|
375
|
+
-loc <text> New location ("" to clear).
|
|
376
|
+
-note <text> New description ("" to clear).
|
|
377
|
+
-free Mark as Free (not busy).
|
|
378
|
+
-busy Mark as Busy.
|
|
379
|
+
-r <dur> Add a popup reminder (repeatable).
|
|
380
|
+
-rx <dur> Remove a reminder matching that duration (repeatable).
|
|
381
|
+
-nr Remove all reminders.
|
|
382
|
+
-open Open the event in the browser afterward.
|
|
383
|
+
|
|
384
|
+
Examples:
|
|
385
|
+
gcal update abc12345 -free
|
|
386
|
+
gcal update abc12345 -busy -title "Team sync"
|
|
387
|
+
gcal update abc12345 -when "wed 2pm" -dur 90m
|
|
388
|
+
gcal update abc12345 -r 30m -r 1h (add two reminders)
|
|
389
|
+
gcal update abc12345 -rx 30m (remove the 30m reminder)
|
|
390
|
+
gcal update abc12345 -nr (clear all reminders)
|
|
322
391
|
`,
|
|
323
392
|
del: `gcal del <id> [id2...] [-all] [-b]
|
|
324
393
|
gcal delete <id> [id2...]
|
|
@@ -360,6 +429,12 @@ const USAGE: Record<string, string> = {
|
|
|
360
429
|
`,
|
|
361
430
|
calendars: `gcal calendars (aliases: listc, list-calendars)
|
|
362
431
|
List available calendars (id, name, access role).
|
|
432
|
+
`,
|
|
433
|
+
listr: `gcal listr (alias: list-recurring)
|
|
434
|
+
List recurring event masters in the current calendar, showing the
|
|
435
|
+
RRULE for each. Use this to confirm whether something like a daily
|
|
436
|
+
"Statin" reminder actually exists as a recurring event in Google
|
|
437
|
+
Calendar (vs. being defined only in another tool's local store).
|
|
363
438
|
`,
|
|
364
439
|
assoc: `gcal assoc
|
|
365
440
|
Set up Windows .ics file association so double-clicking imports to gcal.
|
|
@@ -371,7 +446,10 @@ const USAGE: Record<string, string> = {
|
|
|
371
446
|
|
|
372
447
|
const HELP_ALIASES: Record<string, string> = {
|
|
373
448
|
'listc': 'calendars',
|
|
374
|
-
'list-calendars': 'calendars'
|
|
449
|
+
'list-calendars': 'calendars',
|
|
450
|
+
'list-recurring': 'listr',
|
|
451
|
+
'set': 'update',
|
|
452
|
+
'edit': 'update'
|
|
375
453
|
};
|
|
376
454
|
|
|
377
455
|
function showUsage(cmd?: string): void {
|
|
@@ -399,7 +477,18 @@ interface ParsedArgs {
|
|
|
399
477
|
clip: boolean;
|
|
400
478
|
all: boolean;
|
|
401
479
|
json: boolean;
|
|
480
|
+
allDay: boolean;
|
|
481
|
+
transparency: string; /** 'opaque' (busy) | 'transparent' (free); '' = unset */
|
|
482
|
+
setTitle?: string; /** update: new summary (undefined = leave alone) */
|
|
483
|
+
setLoc?: string; /** update: new location */
|
|
484
|
+
setNote?: string; /** update: new description */
|
|
485
|
+
setWhen?: string; /** update: new start (date/time or +Nd advance) */
|
|
486
|
+
setDur?: string; /** update: new duration (timed) or day count (all-day) */
|
|
487
|
+
removeReminders: number[]; /** update: reminder minutes to drop */
|
|
488
|
+
clearReminders: boolean; /** update: drop all reminders */
|
|
489
|
+
open: boolean; /** open the event in the browser after add/update */
|
|
402
490
|
reminders: number[];
|
|
491
|
+
rrule: string; /** RRULE body, e.g. "FREQ=DAILY". RRULE: prefix added automatically. */
|
|
403
492
|
since?: Date;
|
|
404
493
|
till?: Date;
|
|
405
494
|
helpCmd: string; /** `gcal help <cmd>` */
|
|
@@ -419,7 +508,13 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
419
508
|
clip: false,
|
|
420
509
|
all: false,
|
|
421
510
|
json: false,
|
|
511
|
+
allDay: false,
|
|
512
|
+
transparency: '',
|
|
513
|
+
removeReminders: [],
|
|
514
|
+
clearReminders: false,
|
|
515
|
+
open: false,
|
|
422
516
|
reminders: [],
|
|
517
|
+
rrule: '',
|
|
423
518
|
helpCmd: ''
|
|
424
519
|
};
|
|
425
520
|
|
|
@@ -464,6 +559,57 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
464
559
|
case '--json':
|
|
465
560
|
result.json = true;
|
|
466
561
|
break;
|
|
562
|
+
case '-allday':
|
|
563
|
+
case '-all-day':
|
|
564
|
+
case '--allday':
|
|
565
|
+
result.allDay = true;
|
|
566
|
+
break;
|
|
567
|
+
case '-free':
|
|
568
|
+
case '--free':
|
|
569
|
+
result.transparency = 'transparent';
|
|
570
|
+
break;
|
|
571
|
+
case '-busy':
|
|
572
|
+
case '--busy':
|
|
573
|
+
result.transparency = 'opaque';
|
|
574
|
+
break;
|
|
575
|
+
case '-title':
|
|
576
|
+
case '--title':
|
|
577
|
+
result.setTitle = argv[++i] || '';
|
|
578
|
+
break;
|
|
579
|
+
case '-loc':
|
|
580
|
+
case '-location':
|
|
581
|
+
case '--location':
|
|
582
|
+
result.setLoc = argv[++i] || '';
|
|
583
|
+
break;
|
|
584
|
+
case '-note':
|
|
585
|
+
case '-desc':
|
|
586
|
+
case '-description':
|
|
587
|
+
case '--note':
|
|
588
|
+
result.setNote = argv[++i] || '';
|
|
589
|
+
break;
|
|
590
|
+
case '-when':
|
|
591
|
+
case '--when':
|
|
592
|
+
result.setWhen = argv[++i] || '';
|
|
593
|
+
break;
|
|
594
|
+
case '-dur':
|
|
595
|
+
case '-duration':
|
|
596
|
+
case '--duration':
|
|
597
|
+
result.setDur = argv[++i] || '';
|
|
598
|
+
break;
|
|
599
|
+
case '-rx':
|
|
600
|
+
case '-rmr':
|
|
601
|
+
case '--remove-reminder':
|
|
602
|
+
result.removeReminders.push(parseDuration(argv[++i] || ''));
|
|
603
|
+
break;
|
|
604
|
+
case '-nr':
|
|
605
|
+
case '-noreminders':
|
|
606
|
+
case '--no-reminders':
|
|
607
|
+
result.clearReminders = true;
|
|
608
|
+
break;
|
|
609
|
+
case '-open':
|
|
610
|
+
case '--open':
|
|
611
|
+
result.open = true;
|
|
612
|
+
break;
|
|
467
613
|
case '-r':
|
|
468
614
|
case '-reminder':
|
|
469
615
|
case '--reminder': {
|
|
@@ -472,6 +618,14 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
472
618
|
result.reminders.push(mins);
|
|
473
619
|
break;
|
|
474
620
|
}
|
|
621
|
+
case '-rule':
|
|
622
|
+
case '-rrule':
|
|
623
|
+
case '--rrule': {
|
|
624
|
+
let v = argv[++i] || '';
|
|
625
|
+
if (v.toUpperCase().startsWith('RRULE:')) v = v.slice(6);
|
|
626
|
+
result.rrule = v;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
475
629
|
case '-since':
|
|
476
630
|
case '--since': {
|
|
477
631
|
const val = argv[++i] || '';
|
|
@@ -542,6 +696,102 @@ function buildReminders(minutes: number[]): GoogleEvent['reminders'] | undefined
|
|
|
542
696
|
};
|
|
543
697
|
}
|
|
544
698
|
|
|
699
|
+
/** Compute a start/end patch for moving and/or resizing an event.
|
|
700
|
+
* whenArg undefined => keep original start; durationArg undefined => keep original length.
|
|
701
|
+
* For all-day events durationArg is a day count; for timed events a duration string. */
|
|
702
|
+
function reschedulePatch(
|
|
703
|
+
event: GoogleEvent,
|
|
704
|
+
whenArg: string | undefined,
|
|
705
|
+
durationArg: string | undefined
|
|
706
|
+
): { patch: Partial<GoogleEvent>; startDisplay: EventDateTime; endDisplay: EventDateTime } {
|
|
707
|
+
const origIsAllDay = !!event.start?.date;
|
|
708
|
+
|
|
709
|
+
if (origIsAllDay) {
|
|
710
|
+
const origStart = parseAllDay(event.start!.date!);
|
|
711
|
+
const origEnd = parseAllDay(event.end!.date!);
|
|
712
|
+
const origDurDays = Math.max(1, Math.round((origEnd.getTime() - origStart.getTime()) / 86400_000));
|
|
713
|
+
|
|
714
|
+
let newStart: Date;
|
|
715
|
+
if (whenArg) {
|
|
716
|
+
const adv = whenArg.match(/^\+(\d+)([dw])$/i);
|
|
717
|
+
if (adv) {
|
|
718
|
+
const [, n, unit] = adv;
|
|
719
|
+
const amt = parseInt(n);
|
|
720
|
+
newStart = new Date(origStart);
|
|
721
|
+
newStart.setDate(newStart.getDate() + (unit.toLowerCase() === 'w' ? amt * 7 : amt));
|
|
722
|
+
} else {
|
|
723
|
+
newStart = parseDateTime(whenArg);
|
|
724
|
+
newStart.setHours(0, 0, 0, 0);
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
newStart = new Date(origStart);
|
|
728
|
+
}
|
|
729
|
+
const durDays = durationArg ? (parseInt(durationArg, 10) || origDurDays) : origDurDays;
|
|
730
|
+
const newEnd = new Date(newStart);
|
|
731
|
+
newEnd.setDate(newEnd.getDate() + durDays);
|
|
732
|
+
|
|
733
|
+
const patch = { start: { date: formatYMD(newStart) }, end: { date: formatYMD(newEnd) } };
|
|
734
|
+
return { patch, startDisplay: patch.start, endDisplay: patch.end };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const origStart = new Date(event.start!.dateTime!);
|
|
738
|
+
const origEnd = new Date(event.end!.dateTime!);
|
|
739
|
+
const origDurMs = origEnd.getTime() - origStart.getTime();
|
|
740
|
+
|
|
741
|
+
let newStart: Date;
|
|
742
|
+
if (whenArg) {
|
|
743
|
+
const adv = whenArg.match(/^\+(\d+)([dwhm])$/i);
|
|
744
|
+
if (adv) {
|
|
745
|
+
const [, n, unit] = adv;
|
|
746
|
+
const amt = parseInt(n);
|
|
747
|
+
newStart = new Date(origStart);
|
|
748
|
+
switch (unit.toLowerCase()) {
|
|
749
|
+
case 'd': newStart.setDate(newStart.getDate() + amt); break;
|
|
750
|
+
case 'w': newStart.setDate(newStart.getDate() + amt * 7); break;
|
|
751
|
+
case 'h': newStart.setHours(newStart.getHours() + amt); break;
|
|
752
|
+
case 'm': newStart.setMinutes(newStart.getMinutes() + amt); break;
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
newStart = parseDateTime(whenArg);
|
|
756
|
+
if (!hasTimeComponent(whenArg)) {
|
|
757
|
+
newStart.setHours(origStart.getHours(), origStart.getMinutes(), 0, 0);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
newStart = new Date(origStart);
|
|
762
|
+
}
|
|
763
|
+
const durMs = durationArg ? parseDuration(durationArg) * 60_000 : origDurMs;
|
|
764
|
+
const newEnd = new Date(newStart.getTime() + durMs);
|
|
765
|
+
|
|
766
|
+
const tz = event.start!.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
767
|
+
const patch = {
|
|
768
|
+
start: { dateTime: newStart.toISOString(), timeZone: tz },
|
|
769
|
+
end: { dateTime: newEnd.toISOString(), timeZone: tz }
|
|
770
|
+
};
|
|
771
|
+
return { patch, startDisplay: patch.start, endDisplay: patch.end };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** Format a reminder override list for display, e.g. "10m, 1h" or "(none)". */
|
|
775
|
+
function formatReminders(overrides?: { minutes?: number }[]): string {
|
|
776
|
+
if (!overrides || overrides.length === 0) return '(none)';
|
|
777
|
+
return overrides
|
|
778
|
+
.map(r => r.minutes ?? 0)
|
|
779
|
+
.sort((a, b) => a - b)
|
|
780
|
+
.map(m => (m >= 60 && m % 60 === 0 ? `${m / 60}h` : `${m}m`))
|
|
781
|
+
.join(', ');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Open a URL in the platform's default browser. */
|
|
785
|
+
function openUrl(url: string): void {
|
|
786
|
+
if (process.platform === 'win32') {
|
|
787
|
+
execSync(`start "" "${url}"`, { stdio: 'ignore', shell: 'cmd.exe' });
|
|
788
|
+
} else if (process.platform === 'darwin') {
|
|
789
|
+
execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
790
|
+
} else {
|
|
791
|
+
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
545
795
|
/** Match events by ID prefix and dedup recurring instances to the earliest.
|
|
546
796
|
* `events` must be ordered by startTime (as returned by listEvents). */
|
|
547
797
|
function findByPrefix(events: GoogleEvent[], prefix: string, includeBirthdays: boolean): GoogleEvent[] {
|
|
@@ -677,6 +927,30 @@ async function main(): Promise<void> {
|
|
|
677
927
|
process.exit(1);
|
|
678
928
|
}
|
|
679
929
|
|
|
930
|
+
// Resolve partial calendar names against the user's calendar list.
|
|
931
|
+
// 'primary' and full IDs (containing '@') pass through unchanged.
|
|
932
|
+
if (parsed.calendar !== 'primary' && !parsed.calendar.includes('@')) {
|
|
933
|
+
const token = await getAccessToken(user, false);
|
|
934
|
+
const cals = await listCalendars(token);
|
|
935
|
+
const q = parsed.calendar.toLowerCase();
|
|
936
|
+
const exact = cals.filter(c => (c.summary || '').toLowerCase() === q);
|
|
937
|
+
const matches = exact.length > 0 ? exact : cals.filter(c =>
|
|
938
|
+
(c.summary || '').toLowerCase().includes(q) ||
|
|
939
|
+
(c.id || '').toLowerCase().includes(q)
|
|
940
|
+
);
|
|
941
|
+
if (matches.length === 0) {
|
|
942
|
+
console.error(`No calendar matches "${parsed.calendar}". Available:`);
|
|
943
|
+
for (const c of cals) console.error(` ${c.summary} -- ${c.id}`);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
if (matches.length > 1) {
|
|
947
|
+
console.error(`Ambiguous calendar "${parsed.calendar}":`);
|
|
948
|
+
for (const c of matches) console.error(` ${c.summary} -- ${c.id}`);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
parsed.calendar = matches[0].id!;
|
|
952
|
+
}
|
|
953
|
+
|
|
680
954
|
switch (parsed.command) {
|
|
681
955
|
case 'import': {
|
|
682
956
|
const filePath = parsed.icsFile || parsed.args[0];
|
|
@@ -717,7 +991,21 @@ async function main(): Promise<void> {
|
|
|
717
991
|
const token = await getAccessToken(user, false);
|
|
718
992
|
const timeMin = parsed.since ? parsed.since.toISOString() : undefined;
|
|
719
993
|
const timeMax = parsed.till ? parsed.till.toISOString() : undefined;
|
|
720
|
-
|
|
994
|
+
// Oversample when collapsing recurring series so we still get
|
|
995
|
+
// <count> distinct items after dedup.
|
|
996
|
+
const fetchCount = parsed.all ? count : Math.max(count * 5, 50);
|
|
997
|
+
let events = await listEvents(token, parsed.calendar, fetchCount, timeMin, timeMax);
|
|
998
|
+
if (!parsed.all) {
|
|
999
|
+
const seen = new Set<string>();
|
|
1000
|
+
events = events.filter(e => {
|
|
1001
|
+
const seriesKey = e.recurringEventId;
|
|
1002
|
+
if (!seriesKey) return true;
|
|
1003
|
+
if (seen.has(seriesKey)) return false;
|
|
1004
|
+
seen.add(seriesKey);
|
|
1005
|
+
return true;
|
|
1006
|
+
});
|
|
1007
|
+
events = events.slice(0, count);
|
|
1008
|
+
}
|
|
721
1009
|
const birthdayCount = events.filter(e => e.eventType === 'birthday').length;
|
|
722
1010
|
if (!parsed.birthdays) {
|
|
723
1011
|
events = events.filter(e => e.eventType !== 'birthday');
|
|
@@ -737,7 +1025,9 @@ async function main(): Promise<void> {
|
|
|
737
1025
|
const shortId = (event.id || '').slice(0, 8);
|
|
738
1026
|
const start = event.start ? formatDateTime(event.start) : '?';
|
|
739
1027
|
const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
|
|
740
|
-
const summary = (event.summary || '(no title)')
|
|
1028
|
+
const summary = (event.summary || '(no title)')
|
|
1029
|
+
+ (event.eventType === 'birthday' ? ' [from contact]' : '')
|
|
1030
|
+
+ (event.recurringEventId ? ' [recurring]' : '');
|
|
741
1031
|
const loc = event.location || '';
|
|
742
1032
|
if (parsed.verbose) {
|
|
743
1033
|
rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
|
|
@@ -773,32 +1063,49 @@ async function main(): Promise<void> {
|
|
|
773
1063
|
case 'add': {
|
|
774
1064
|
// Explicit mode: gcal add "title" "when" [duration]
|
|
775
1065
|
if (parsed.args.length >= 2 && !parsed.clip) {
|
|
776
|
-
const [title, when,
|
|
1066
|
+
const [title, when, third] = parsed.args;
|
|
777
1067
|
const startTime = parseDateTime(when);
|
|
778
|
-
const durationMins = parseDuration(duration);
|
|
779
|
-
const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
|
|
780
1068
|
|
|
781
1069
|
const event: GoogleEvent = {
|
|
782
1070
|
summary: title,
|
|
783
|
-
start: {
|
|
784
|
-
|
|
785
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
786
|
-
},
|
|
787
|
-
end: {
|
|
788
|
-
dateTime: endTime.toISOString(),
|
|
789
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
790
|
-
},
|
|
1071
|
+
start: {},
|
|
1072
|
+
end: {},
|
|
791
1073
|
reminders: buildReminders(parsed.reminders)
|
|
792
1074
|
};
|
|
793
1075
|
|
|
1076
|
+
if (parsed.allDay) {
|
|
1077
|
+
// All-day: [duration] is a day count (default 1). Google's
|
|
1078
|
+
// end.date is exclusive, so a 1-day event ends the next day.
|
|
1079
|
+
const days = third ? (parseInt(third, 10) || 1) : 1;
|
|
1080
|
+
const startD = new Date(startTime);
|
|
1081
|
+
startD.setHours(0, 0, 0, 0);
|
|
1082
|
+
const endD = new Date(startD);
|
|
1083
|
+
endD.setDate(endD.getDate() + days);
|
|
1084
|
+
event.start = { date: formatYMD(startD) };
|
|
1085
|
+
event.end = { date: formatYMD(endD) };
|
|
1086
|
+
} else {
|
|
1087
|
+
const durationMins = parseDuration(third || '1h');
|
|
1088
|
+
const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
|
|
1089
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1090
|
+
event.start = { dateTime: startTime.toISOString(), timeZone: tz };
|
|
1091
|
+
event.end = { dateTime: endTime.toISOString(), timeZone: tz };
|
|
1092
|
+
}
|
|
1093
|
+
if (parsed.transparency) event.transparency = parsed.transparency;
|
|
1094
|
+
if (parsed.rrule) event.recurrence = [`RRULE:${parsed.rrule}`];
|
|
1095
|
+
|
|
794
1096
|
const token = await getAccessToken(user, true);
|
|
795
|
-
|
|
1097
|
+
if (!parsed.allDay) {
|
|
1098
|
+
await checkProximity(token, parsed.calendar,
|
|
1099
|
+
new Date(event.start.dateTime!), new Date(event.end.dateTime!));
|
|
1100
|
+
}
|
|
796
1101
|
const created = await createEvent(token, event, parsed.calendar);
|
|
797
1102
|
console.log(`\nEvent created: ${created.summary}`);
|
|
798
1103
|
console.log(` When: ${formatDateTime(created.start)} - ${formatDateTime(created.end)}`);
|
|
1104
|
+
if (created.transparency === 'transparent') console.log(` Free (not busy)`);
|
|
799
1105
|
if (created.htmlLink) {
|
|
800
1106
|
console.log(` Link: ${created.htmlLink}`);
|
|
801
1107
|
}
|
|
1108
|
+
if (parsed.open && created.htmlLink) openUrl(created.htmlLink);
|
|
802
1109
|
break;
|
|
803
1110
|
}
|
|
804
1111
|
|
|
@@ -858,6 +1165,7 @@ async function main(): Promise<void> {
|
|
|
858
1165
|
description: extracted.description,
|
|
859
1166
|
reminders: buildReminders(parsed.reminders)
|
|
860
1167
|
};
|
|
1168
|
+
if (parsed.transparency) event.transparency = parsed.transparency;
|
|
861
1169
|
events.push(event);
|
|
862
1170
|
|
|
863
1171
|
console.log(`\n Event: ${extracted.summary}`);
|
|
@@ -877,13 +1185,17 @@ async function main(): Promise<void> {
|
|
|
877
1185
|
? '\nCreate this event? [Y/n] (auto-yes in 60s) '
|
|
878
1186
|
: `\nCreate ${events.length} events? [Y/n] (auto-yes in 60s) `;
|
|
879
1187
|
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
1188
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
880
1189
|
const confirm = await Promise.race([
|
|
881
1190
|
rl2.question(prompt).then(s => s.trim().toLowerCase()),
|
|
882
|
-
new Promise<string>(resolve =>
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1191
|
+
new Promise<string>(resolve => {
|
|
1192
|
+
timeoutId = setTimeout(() => {
|
|
1193
|
+
console.log('\nNo response — creating event(s).');
|
|
1194
|
+
resolve('');
|
|
1195
|
+
}, 60_000);
|
|
1196
|
+
})
|
|
886
1197
|
]);
|
|
1198
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
887
1199
|
rl2.close();
|
|
888
1200
|
if (confirm && confirm !== 'y' && confirm !== 'yes') {
|
|
889
1201
|
console.log('Cancelled.');
|
|
@@ -897,6 +1209,7 @@ async function main(): Promise<void> {
|
|
|
897
1209
|
if (created.htmlLink) {
|
|
898
1210
|
console.log(` Link: ${created.htmlLink}`);
|
|
899
1211
|
}
|
|
1212
|
+
if (parsed.open && created.htmlLink) openUrl(created.htmlLink);
|
|
900
1213
|
}
|
|
901
1214
|
break;
|
|
902
1215
|
}
|
|
@@ -957,6 +1270,38 @@ async function main(): Promise<void> {
|
|
|
957
1270
|
break;
|
|
958
1271
|
}
|
|
959
1272
|
|
|
1273
|
+
case 'listr':
|
|
1274
|
+
case 'list-recurring': {
|
|
1275
|
+
const token = await getAccessToken(user, false);
|
|
1276
|
+
const events = await listRecurringEvents(token, parsed.calendar, parsed.count || 250);
|
|
1277
|
+
|
|
1278
|
+
if (events.length === 0) {
|
|
1279
|
+
console.log('No recurring events found.');
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
console.log(`\nRecurring events (${events.length}):\n`);
|
|
1283
|
+
const rows: string[][] = [];
|
|
1284
|
+
for (const ev of events) {
|
|
1285
|
+
const shortId = (ev.id || '').slice(0, 8);
|
|
1286
|
+
const start = ev.start ? formatDateTime(ev.start) : '';
|
|
1287
|
+
const summary = ev.summary || '(no title)';
|
|
1288
|
+
const rule = (ev.recurrence || []).join('; ');
|
|
1289
|
+
rows.push([shortId, start, summary, rule]);
|
|
1290
|
+
}
|
|
1291
|
+
const headers = ['ID', 'Starts', 'Event', 'Recurrence'];
|
|
1292
|
+
const colWidths = headers.map((h, i) =>
|
|
1293
|
+
Math.max(h.length, ...rows.map(r => (r[i] || '').length))
|
|
1294
|
+
);
|
|
1295
|
+
const lastIdx = headers.length - 1;
|
|
1296
|
+
const padCell = (s: string, i: number) => i === lastIdx ? s : s.padEnd(colWidths[i]);
|
|
1297
|
+
console.log(headers.map(padCell).join(' '));
|
|
1298
|
+
console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
|
|
1299
|
+
for (const row of rows) {
|
|
1300
|
+
console.log(row.map((cell, i) => padCell(cell || '', i)).join(' '));
|
|
1301
|
+
}
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
960
1305
|
case 'remind': {
|
|
961
1306
|
if (parsed.args.length < 2) {
|
|
962
1307
|
console.error('Usage: gcal remind <id> <duration> [duration2...]');
|
|
@@ -1001,6 +1346,111 @@ async function main(): Promise<void> {
|
|
|
1001
1346
|
break;
|
|
1002
1347
|
}
|
|
1003
1348
|
|
|
1349
|
+
case 'update':
|
|
1350
|
+
case 'edit':
|
|
1351
|
+
case 'set': {
|
|
1352
|
+
if (parsed.args.length < 1) {
|
|
1353
|
+
console.error('Usage: gcal update <id> [-when <when>] [-dur <dur>] [-title <text>]');
|
|
1354
|
+
console.error(' [-loc <text>] [-note <text>] [-free|-busy]');
|
|
1355
|
+
console.error(' [-r <dur>] [-rx <dur>] [-nr] [-open]');
|
|
1356
|
+
console.error('Use "gcal list" to see event IDs');
|
|
1357
|
+
process.exit(1);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const patch: Partial<GoogleEvent> = {};
|
|
1361
|
+
if (parsed.setTitle !== undefined) patch.summary = parsed.setTitle;
|
|
1362
|
+
if (parsed.setLoc !== undefined) patch.location = parsed.setLoc;
|
|
1363
|
+
if (parsed.setNote !== undefined) patch.description = parsed.setNote;
|
|
1364
|
+
if (parsed.transparency) patch.transparency = parsed.transparency;
|
|
1365
|
+
|
|
1366
|
+
const changingTime = parsed.setWhen !== undefined || parsed.setDur !== undefined;
|
|
1367
|
+
const changingReminders = parsed.clearReminders
|
|
1368
|
+
|| parsed.reminders.length > 0 || parsed.removeReminders.length > 0;
|
|
1369
|
+
|
|
1370
|
+
if (Object.keys(patch).length === 0 && !changingTime && !changingReminders) {
|
|
1371
|
+
console.error('Nothing to change. Specify -when, -dur, -title, -loc, -note,');
|
|
1372
|
+
console.error(' -free, -busy, -r (add reminder), -rx (remove reminder), or -nr.');
|
|
1373
|
+
process.exit(1);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const idPrefix = parsed.args[0];
|
|
1377
|
+
const lookback = parsed.since
|
|
1378
|
+
? parsed.since.toISOString()
|
|
1379
|
+
: new Date(Date.now() - 30 * 86400_000).toISOString();
|
|
1380
|
+
const timeMax = parsed.till ? parsed.till.toISOString() : undefined;
|
|
1381
|
+
|
|
1382
|
+
const token = await getAccessToken(user, true);
|
|
1383
|
+
const events = await listEvents(token, parsed.calendar, 250, lookback, timeMax);
|
|
1384
|
+
const unique = findByPrefix(events, idPrefix, parsed.birthdays);
|
|
1385
|
+
|
|
1386
|
+
if (unique.length === 0) {
|
|
1387
|
+
console.error(`${idPrefix}: not found (searched from ${lookback.slice(0, 10)})`);
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
if (unique.length > 1) {
|
|
1391
|
+
console.error(`${idPrefix}: ambiguous (${unique.length} matches)`);
|
|
1392
|
+
for (const e of unique) {
|
|
1393
|
+
console.error(` ${e.id?.slice(0, 8)} - ${e.summary}`);
|
|
1394
|
+
}
|
|
1395
|
+
process.exit(1);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const event = unique[0];
|
|
1399
|
+
|
|
1400
|
+
// Time / duration change (reuses the resched logic)
|
|
1401
|
+
let timeFrom = '';
|
|
1402
|
+
let timeTo = '';
|
|
1403
|
+
if (changingTime) {
|
|
1404
|
+
const r = reschedulePatch(event, parsed.setWhen, parsed.setDur);
|
|
1405
|
+
patch.start = r.patch.start;
|
|
1406
|
+
patch.end = r.patch.end;
|
|
1407
|
+
timeFrom = `${formatDateTime(event.start!)} - ${formatDateTime(event.end!)}`;
|
|
1408
|
+
timeTo = `${formatDateTime(r.startDisplay)} - ${formatDateTime(r.endDisplay)}`;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Reminders: -nr clears all; otherwise merge existing with -r adds and -rx removes
|
|
1412
|
+
if (parsed.clearReminders) {
|
|
1413
|
+
patch.reminders = { useDefault: false, overrides: [] };
|
|
1414
|
+
} else if (changingReminders) {
|
|
1415
|
+
const mins = new Set<number>((event.reminders?.overrides ?? []).map(r => r.minutes ?? 0));
|
|
1416
|
+
for (const m of parsed.removeReminders) mins.delete(m);
|
|
1417
|
+
for (const m of parsed.reminders) mins.add(m);
|
|
1418
|
+
patch.reminders = {
|
|
1419
|
+
useDefault: false,
|
|
1420
|
+
overrides: [...mins].sort((a, b) => a - b).map(m => ({ method: 'popup' as const, minutes: m }))
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Proximity check when the timed slot moved
|
|
1425
|
+
if (patch.start?.dateTime && patch.end?.dateTime) {
|
|
1426
|
+
await checkProximity(
|
|
1427
|
+
token,
|
|
1428
|
+
parsed.calendar,
|
|
1429
|
+
new Date(patch.start.dateTime),
|
|
1430
|
+
new Date(patch.end.dateTime),
|
|
1431
|
+
(event.id || '').split('_')[0]
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const updated = await patchEvent(token, event.id!, patch, parsed.calendar);
|
|
1436
|
+
console.log(`Updated: ${updated.summary}`);
|
|
1437
|
+
if (patch.summary !== undefined) console.log(` Title: ${updated.summary}`);
|
|
1438
|
+
if (changingTime) {
|
|
1439
|
+
console.log(` When: ${timeFrom}`);
|
|
1440
|
+
console.log(` -> ${timeTo}`);
|
|
1441
|
+
}
|
|
1442
|
+
if (patch.location !== undefined) console.log(` Where: ${updated.location || '(cleared)'}`);
|
|
1443
|
+
if (patch.description !== undefined) console.log(` Note: ${updated.description || '(cleared)'}`);
|
|
1444
|
+
if (patch.transparency !== undefined) {
|
|
1445
|
+
console.log(` Shows as: ${updated.transparency === 'transparent' ? 'free' : 'busy'}`);
|
|
1446
|
+
}
|
|
1447
|
+
if (patch.reminders !== undefined) {
|
|
1448
|
+
console.log(` Reminders: ${formatReminders(updated.reminders?.overrides)}`);
|
|
1449
|
+
}
|
|
1450
|
+
if (parsed.open && updated.htmlLink) openUrl(updated.htmlLink);
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1004
1454
|
case 'resched':
|
|
1005
1455
|
case 'reschedule':
|
|
1006
1456
|
case 'snooze': {
|
|
@@ -1050,73 +1500,10 @@ async function main(): Promise<void> {
|
|
|
1050
1500
|
}
|
|
1051
1501
|
}
|
|
1052
1502
|
|
|
1053
|
-
const
|
|
1054
|
-
let patch: Partial<GoogleEvent>;
|
|
1055
|
-
let newStartDisplay: { date?: string; dateTime?: string; timeZone?: string };
|
|
1056
|
-
let newEndDisplay: { date?: string; dateTime?: string; timeZone?: string };
|
|
1057
|
-
|
|
1058
|
-
if (origIsAllDay) {
|
|
1059
|
-
const origStart = parseAllDay(event.start!.date!);
|
|
1060
|
-
const origEnd = parseAllDay(event.end!.date!);
|
|
1061
|
-
const origDurDays = Math.max(1, Math.round((origEnd.getTime() - origStart.getTime()) / 86400_000));
|
|
1062
|
-
|
|
1063
|
-
let newStart: Date;
|
|
1064
|
-
const adv = whenArg.match(/^\+(\d+)([dw])$/i);
|
|
1065
|
-
if (adv) {
|
|
1066
|
-
const [, n, unit] = adv;
|
|
1067
|
-
const amt = parseInt(n);
|
|
1068
|
-
newStart = new Date(origStart);
|
|
1069
|
-
newStart.setDate(newStart.getDate() + (unit.toLowerCase() === 'w' ? amt * 7 : amt));
|
|
1070
|
-
} else {
|
|
1071
|
-
newStart = parseDateTime(whenArg);
|
|
1072
|
-
newStart.setHours(0, 0, 0, 0);
|
|
1073
|
-
}
|
|
1074
|
-
const newEnd = new Date(newStart);
|
|
1075
|
-
newEnd.setDate(newEnd.getDate() + origDurDays);
|
|
1076
|
-
|
|
1077
|
-
patch = {
|
|
1078
|
-
start: { date: formatYMD(newStart) },
|
|
1079
|
-
end: { date: formatYMD(newEnd) }
|
|
1080
|
-
};
|
|
1081
|
-
newStartDisplay = { date: formatYMD(newStart) };
|
|
1082
|
-
newEndDisplay = { date: formatYMD(newEnd) };
|
|
1083
|
-
} else {
|
|
1084
|
-
const origStart = new Date(event.start!.dateTime!);
|
|
1085
|
-
const origEnd = new Date(event.end!.dateTime!);
|
|
1086
|
-
const origDurMs = origEnd.getTime() - origStart.getTime();
|
|
1087
|
-
|
|
1088
|
-
let newStart: Date;
|
|
1089
|
-
const adv = whenArg.match(/^\+(\d+)([dwhm])$/i);
|
|
1090
|
-
if (adv) {
|
|
1091
|
-
const [, n, unit] = adv;
|
|
1092
|
-
const amt = parseInt(n);
|
|
1093
|
-
newStart = new Date(origStart);
|
|
1094
|
-
switch (unit.toLowerCase()) {
|
|
1095
|
-
case 'd': newStart.setDate(newStart.getDate() + amt); break;
|
|
1096
|
-
case 'w': newStart.setDate(newStart.getDate() + amt * 7); break;
|
|
1097
|
-
case 'h': newStart.setHours(newStart.getHours() + amt); break;
|
|
1098
|
-
case 'm': newStart.setMinutes(newStart.getMinutes() + amt); break;
|
|
1099
|
-
}
|
|
1100
|
-
} else {
|
|
1101
|
-
newStart = parseDateTime(whenArg);
|
|
1102
|
-
if (!hasTimeComponent(whenArg)) {
|
|
1103
|
-
newStart.setHours(origStart.getHours(), origStart.getMinutes(), 0, 0);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
const durMs = durationArg ? parseDuration(durationArg) * 60_000 : origDurMs;
|
|
1107
|
-
const newEnd = new Date(newStart.getTime() + durMs);
|
|
1108
|
-
|
|
1109
|
-
const tz = event.start!.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1110
|
-
patch = {
|
|
1111
|
-
start: { dateTime: newStart.toISOString(), timeZone: tz },
|
|
1112
|
-
end: { dateTime: newEnd.toISOString(), timeZone: tz }
|
|
1113
|
-
};
|
|
1114
|
-
newStartDisplay = patch.start!;
|
|
1115
|
-
newEndDisplay = patch.end!;
|
|
1116
|
-
}
|
|
1503
|
+
const { patch, startDisplay, endDisplay } = reschedulePatch(event, whenArg, durationArg);
|
|
1117
1504
|
|
|
1118
1505
|
// Proximity check for timed events (skip all-day)
|
|
1119
|
-
if (
|
|
1506
|
+
if (patch.start?.dateTime && patch.end?.dateTime) {
|
|
1120
1507
|
await checkProximity(
|
|
1121
1508
|
token,
|
|
1122
1509
|
parsed.calendar,
|
|
@@ -1129,7 +1516,8 @@ async function main(): Promise<void> {
|
|
|
1129
1516
|
const updated = await patchEvent(token, event.id!, patch, parsed.calendar);
|
|
1130
1517
|
console.log(`Rescheduled: ${updated.summary}`);
|
|
1131
1518
|
console.log(` From: ${formatDateTime(event.start!)} - ${formatDateTime(event.end!)}`);
|
|
1132
|
-
console.log(` To: ${formatDateTime(
|
|
1519
|
+
console.log(` To: ${formatDateTime(startDisplay)} - ${formatDateTime(endDisplay)}`);
|
|
1520
|
+
if (parsed.open && updated.htmlLink) openUrl(updated.htmlLink);
|
|
1133
1521
|
break;
|
|
1134
1522
|
}
|
|
1135
1523
|
|
|
@@ -1217,6 +1605,7 @@ async function main(): Promise<void> {
|
|
|
1217
1605
|
if (event.hangoutLink) console.log(` Meet: ${event.hangoutLink}`);
|
|
1218
1606
|
if (event.htmlLink) console.log(` Link: ${event.htmlLink}`);
|
|
1219
1607
|
console.log(` Status: ${event.status || 'confirmed'}`);
|
|
1608
|
+
console.log(` Shows as: ${event.transparency === 'transparent' ? 'free' : 'busy'}`);
|
|
1220
1609
|
if (event.created) console.log(` Created: ${formatDateTime({ dateTime: event.created })}`);
|
|
1221
1610
|
if (event.updated) console.log(` Updated: ${formatDateTime({ dateTime: event.updated })}`);
|
|
1222
1611
|
console.log(` ID: ${event.id}`);
|
|
@@ -1262,13 +1651,7 @@ async function main(): Promise<void> {
|
|
|
1262
1651
|
console.log(`Opening: ${event.summary || '(no title)'}`);
|
|
1263
1652
|
console.log(` ${event.htmlLink}`);
|
|
1264
1653
|
|
|
1265
|
-
|
|
1266
|
-
execSync(`start "" "${event.htmlLink}"`, { stdio: 'ignore', shell: 'cmd.exe' });
|
|
1267
|
-
} else if (process.platform === 'darwin') {
|
|
1268
|
-
execSync(`open "${event.htmlLink}"`, { stdio: 'ignore' });
|
|
1269
|
-
} else {
|
|
1270
|
-
execSync(`xdg-open "${event.htmlLink}"`, { stdio: 'ignore' });
|
|
1271
|
-
}
|
|
1654
|
+
openUrl(event.htmlLink);
|
|
1272
1655
|
break;
|
|
1273
1656
|
}
|
|
1274
1657
|
|
|
@@ -1281,9 +1664,15 @@ async function main(): Promise<void> {
|
|
|
1281
1664
|
|
|
1282
1665
|
if (import.meta.main) {
|
|
1283
1666
|
main()
|
|
1284
|
-
.then(() =>
|
|
1285
|
-
|
|
1667
|
+
.then(async () => {
|
|
1668
|
+
await teardownAbortHandler();
|
|
1669
|
+
// Don't call process.exit explicitly — Node 25 on Windows asserts
|
|
1670
|
+
// in libuv (UV_HANDLE_CLOSING) when process.exit races with handle
|
|
1671
|
+
// teardown. Let the event loop drain naturally instead.
|
|
1672
|
+
})
|
|
1673
|
+
.catch(async e => {
|
|
1674
|
+
await teardownAbortHandler();
|
|
1286
1675
|
console.error(`Error: ${e.message}`);
|
|
1287
|
-
process.
|
|
1676
|
+
process.exitCode = 1;
|
|
1288
1677
|
});
|
|
1289
1678
|
}
|