@bobfrankston/gcal 0.1.62 → 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 +333 -95
- package/gcal.js.map +1 -1
- package/gcal.ts +344 -88
- package/glib/gutils.d.ts.map +1 -1
- package/glib/gutils.js +13 -0
- package/glib/gutils.js.map +1 -1
- package/glib/gutils.ts +13 -0
- package/gtask.js +12 -3
- package/gtask.js.map +1 -1
- package/gtask.ts +12 -3
- package/package.json +1 -1
package/gcal.ts
CHANGED
|
@@ -12,7 +12,7 @@ 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,
|
|
@@ -290,6 +290,7 @@ Commands:
|
|
|
290
290
|
show Show full details for an event (-json for JSON)
|
|
291
291
|
open Open event in browser
|
|
292
292
|
add Add event (explicit, AI, or interactive)
|
|
293
|
+
update | edit | set Change an event's time/title/location/busy/...
|
|
293
294
|
del | delete Delete event(s) by ID
|
|
294
295
|
remind Add reminder(s) to existing event
|
|
295
296
|
resched Reschedule event
|
|
@@ -338,20 +339,55 @@ const USAGE: Record<string, string> = {
|
|
|
338
339
|
Examples:
|
|
339
340
|
gcal open abc12345
|
|
340
341
|
`,
|
|
341
|
-
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)
|
|
342
344
|
gcal add "<free text>" AI-parsed single arg
|
|
343
345
|
gcal add -clip AI-parsed from clipboard
|
|
344
346
|
gcal add Interactive (type description)
|
|
345
347
|
Add a calendar event. Default duration 1h. Use -r <dur> to add reminder(s).
|
|
346
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
|
+
|
|
347
355
|
Examples:
|
|
348
356
|
gcal add "Dentist" "Friday 3pm" "1h"
|
|
349
357
|
gcal add "Lunch" "1/14/2026 12:00" "1h"
|
|
350
358
|
gcal add "Meeting" "tomorrow 10:00"
|
|
351
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)
|
|
352
363
|
gcal add "Dentist appointment Friday 3pm for 1 hour"
|
|
353
364
|
gcal add -clip
|
|
354
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)
|
|
355
391
|
`,
|
|
356
392
|
del: `gcal del <id> [id2...] [-all] [-b]
|
|
357
393
|
gcal delete <id> [id2...]
|
|
@@ -411,7 +447,9 @@ const USAGE: Record<string, string> = {
|
|
|
411
447
|
const HELP_ALIASES: Record<string, string> = {
|
|
412
448
|
'listc': 'calendars',
|
|
413
449
|
'list-calendars': 'calendars',
|
|
414
|
-
'list-recurring': 'listr'
|
|
450
|
+
'list-recurring': 'listr',
|
|
451
|
+
'set': 'update',
|
|
452
|
+
'edit': 'update'
|
|
415
453
|
};
|
|
416
454
|
|
|
417
455
|
function showUsage(cmd?: string): void {
|
|
@@ -439,6 +477,16 @@ interface ParsedArgs {
|
|
|
439
477
|
clip: boolean;
|
|
440
478
|
all: boolean;
|
|
441
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 */
|
|
442
490
|
reminders: number[];
|
|
443
491
|
rrule: string; /** RRULE body, e.g. "FREQ=DAILY". RRULE: prefix added automatically. */
|
|
444
492
|
since?: Date;
|
|
@@ -460,6 +508,11 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
460
508
|
clip: false,
|
|
461
509
|
all: false,
|
|
462
510
|
json: false,
|
|
511
|
+
allDay: false,
|
|
512
|
+
transparency: '',
|
|
513
|
+
removeReminders: [],
|
|
514
|
+
clearReminders: false,
|
|
515
|
+
open: false,
|
|
463
516
|
reminders: [],
|
|
464
517
|
rrule: '',
|
|
465
518
|
helpCmd: ''
|
|
@@ -506,6 +559,57 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
506
559
|
case '--json':
|
|
507
560
|
result.json = true;
|
|
508
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;
|
|
509
613
|
case '-r':
|
|
510
614
|
case '-reminder':
|
|
511
615
|
case '--reminder': {
|
|
@@ -592,6 +696,102 @@ function buildReminders(minutes: number[]): GoogleEvent['reminders'] | undefined
|
|
|
592
696
|
};
|
|
593
697
|
}
|
|
594
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
|
+
|
|
595
795
|
/** Match events by ID prefix and dedup recurring instances to the earliest.
|
|
596
796
|
* `events` must be ordered by startTime (as returned by listEvents). */
|
|
597
797
|
function findByPrefix(events: GoogleEvent[], prefix: string, includeBirthdays: boolean): GoogleEvent[] {
|
|
@@ -863,33 +1063,49 @@ async function main(): Promise<void> {
|
|
|
863
1063
|
case 'add': {
|
|
864
1064
|
// Explicit mode: gcal add "title" "when" [duration]
|
|
865
1065
|
if (parsed.args.length >= 2 && !parsed.clip) {
|
|
866
|
-
const [title, when,
|
|
1066
|
+
const [title, when, third] = parsed.args;
|
|
867
1067
|
const startTime = parseDateTime(when);
|
|
868
|
-
const durationMins = parseDuration(duration);
|
|
869
|
-
const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
|
|
870
1068
|
|
|
871
1069
|
const event: GoogleEvent = {
|
|
872
1070
|
summary: title,
|
|
873
|
-
start: {
|
|
874
|
-
|
|
875
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
876
|
-
},
|
|
877
|
-
end: {
|
|
878
|
-
dateTime: endTime.toISOString(),
|
|
879
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
880
|
-
},
|
|
1071
|
+
start: {},
|
|
1072
|
+
end: {},
|
|
881
1073
|
reminders: buildReminders(parsed.reminders)
|
|
882
1074
|
};
|
|
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;
|
|
883
1094
|
if (parsed.rrule) event.recurrence = [`RRULE:${parsed.rrule}`];
|
|
884
1095
|
|
|
885
1096
|
const token = await getAccessToken(user, true);
|
|
886
|
-
|
|
1097
|
+
if (!parsed.allDay) {
|
|
1098
|
+
await checkProximity(token, parsed.calendar,
|
|
1099
|
+
new Date(event.start.dateTime!), new Date(event.end.dateTime!));
|
|
1100
|
+
}
|
|
887
1101
|
const created = await createEvent(token, event, parsed.calendar);
|
|
888
1102
|
console.log(`\nEvent created: ${created.summary}`);
|
|
889
1103
|
console.log(` When: ${formatDateTime(created.start)} - ${formatDateTime(created.end)}`);
|
|
1104
|
+
if (created.transparency === 'transparent') console.log(` Free (not busy)`);
|
|
890
1105
|
if (created.htmlLink) {
|
|
891
1106
|
console.log(` Link: ${created.htmlLink}`);
|
|
892
1107
|
}
|
|
1108
|
+
if (parsed.open && created.htmlLink) openUrl(created.htmlLink);
|
|
893
1109
|
break;
|
|
894
1110
|
}
|
|
895
1111
|
|
|
@@ -949,6 +1165,7 @@ async function main(): Promise<void> {
|
|
|
949
1165
|
description: extracted.description,
|
|
950
1166
|
reminders: buildReminders(parsed.reminders)
|
|
951
1167
|
};
|
|
1168
|
+
if (parsed.transparency) event.transparency = parsed.transparency;
|
|
952
1169
|
events.push(event);
|
|
953
1170
|
|
|
954
1171
|
console.log(`\n Event: ${extracted.summary}`);
|
|
@@ -992,6 +1209,7 @@ async function main(): Promise<void> {
|
|
|
992
1209
|
if (created.htmlLink) {
|
|
993
1210
|
console.log(` Link: ${created.htmlLink}`);
|
|
994
1211
|
}
|
|
1212
|
+
if (parsed.open && created.htmlLink) openUrl(created.htmlLink);
|
|
995
1213
|
}
|
|
996
1214
|
break;
|
|
997
1215
|
}
|
|
@@ -1128,6 +1346,111 @@ async function main(): Promise<void> {
|
|
|
1128
1346
|
break;
|
|
1129
1347
|
}
|
|
1130
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
|
+
|
|
1131
1454
|
case 'resched':
|
|
1132
1455
|
case 'reschedule':
|
|
1133
1456
|
case 'snooze': {
|
|
@@ -1177,73 +1500,10 @@ async function main(): Promise<void> {
|
|
|
1177
1500
|
}
|
|
1178
1501
|
}
|
|
1179
1502
|
|
|
1180
|
-
const
|
|
1181
|
-
let patch: Partial<GoogleEvent>;
|
|
1182
|
-
let newStartDisplay: { date?: string; dateTime?: string; timeZone?: string };
|
|
1183
|
-
let newEndDisplay: { date?: string; dateTime?: string; timeZone?: string };
|
|
1184
|
-
|
|
1185
|
-
if (origIsAllDay) {
|
|
1186
|
-
const origStart = parseAllDay(event.start!.date!);
|
|
1187
|
-
const origEnd = parseAllDay(event.end!.date!);
|
|
1188
|
-
const origDurDays = Math.max(1, Math.round((origEnd.getTime() - origStart.getTime()) / 86400_000));
|
|
1189
|
-
|
|
1190
|
-
let newStart: Date;
|
|
1191
|
-
const adv = whenArg.match(/^\+(\d+)([dw])$/i);
|
|
1192
|
-
if (adv) {
|
|
1193
|
-
const [, n, unit] = adv;
|
|
1194
|
-
const amt = parseInt(n);
|
|
1195
|
-
newStart = new Date(origStart);
|
|
1196
|
-
newStart.setDate(newStart.getDate() + (unit.toLowerCase() === 'w' ? amt * 7 : amt));
|
|
1197
|
-
} else {
|
|
1198
|
-
newStart = parseDateTime(whenArg);
|
|
1199
|
-
newStart.setHours(0, 0, 0, 0);
|
|
1200
|
-
}
|
|
1201
|
-
const newEnd = new Date(newStart);
|
|
1202
|
-
newEnd.setDate(newEnd.getDate() + origDurDays);
|
|
1203
|
-
|
|
1204
|
-
patch = {
|
|
1205
|
-
start: { date: formatYMD(newStart) },
|
|
1206
|
-
end: { date: formatYMD(newEnd) }
|
|
1207
|
-
};
|
|
1208
|
-
newStartDisplay = { date: formatYMD(newStart) };
|
|
1209
|
-
newEndDisplay = { date: formatYMD(newEnd) };
|
|
1210
|
-
} else {
|
|
1211
|
-
const origStart = new Date(event.start!.dateTime!);
|
|
1212
|
-
const origEnd = new Date(event.end!.dateTime!);
|
|
1213
|
-
const origDurMs = origEnd.getTime() - origStart.getTime();
|
|
1214
|
-
|
|
1215
|
-
let newStart: Date;
|
|
1216
|
-
const adv = whenArg.match(/^\+(\d+)([dwhm])$/i);
|
|
1217
|
-
if (adv) {
|
|
1218
|
-
const [, n, unit] = adv;
|
|
1219
|
-
const amt = parseInt(n);
|
|
1220
|
-
newStart = new Date(origStart);
|
|
1221
|
-
switch (unit.toLowerCase()) {
|
|
1222
|
-
case 'd': newStart.setDate(newStart.getDate() + amt); break;
|
|
1223
|
-
case 'w': newStart.setDate(newStart.getDate() + amt * 7); break;
|
|
1224
|
-
case 'h': newStart.setHours(newStart.getHours() + amt); break;
|
|
1225
|
-
case 'm': newStart.setMinutes(newStart.getMinutes() + amt); break;
|
|
1226
|
-
}
|
|
1227
|
-
} else {
|
|
1228
|
-
newStart = parseDateTime(whenArg);
|
|
1229
|
-
if (!hasTimeComponent(whenArg)) {
|
|
1230
|
-
newStart.setHours(origStart.getHours(), origStart.getMinutes(), 0, 0);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
const durMs = durationArg ? parseDuration(durationArg) * 60_000 : origDurMs;
|
|
1234
|
-
const newEnd = new Date(newStart.getTime() + durMs);
|
|
1235
|
-
|
|
1236
|
-
const tz = event.start!.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1237
|
-
patch = {
|
|
1238
|
-
start: { dateTime: newStart.toISOString(), timeZone: tz },
|
|
1239
|
-
end: { dateTime: newEnd.toISOString(), timeZone: tz }
|
|
1240
|
-
};
|
|
1241
|
-
newStartDisplay = patch.start!;
|
|
1242
|
-
newEndDisplay = patch.end!;
|
|
1243
|
-
}
|
|
1503
|
+
const { patch, startDisplay, endDisplay } = reschedulePatch(event, whenArg, durationArg);
|
|
1244
1504
|
|
|
1245
1505
|
// Proximity check for timed events (skip all-day)
|
|
1246
|
-
if (
|
|
1506
|
+
if (patch.start?.dateTime && patch.end?.dateTime) {
|
|
1247
1507
|
await checkProximity(
|
|
1248
1508
|
token,
|
|
1249
1509
|
parsed.calendar,
|
|
@@ -1256,7 +1516,8 @@ async function main(): Promise<void> {
|
|
|
1256
1516
|
const updated = await patchEvent(token, event.id!, patch, parsed.calendar);
|
|
1257
1517
|
console.log(`Rescheduled: ${updated.summary}`);
|
|
1258
1518
|
console.log(` From: ${formatDateTime(event.start!)} - ${formatDateTime(event.end!)}`);
|
|
1259
|
-
console.log(` To: ${formatDateTime(
|
|
1519
|
+
console.log(` To: ${formatDateTime(startDisplay)} - ${formatDateTime(endDisplay)}`);
|
|
1520
|
+
if (parsed.open && updated.htmlLink) openUrl(updated.htmlLink);
|
|
1260
1521
|
break;
|
|
1261
1522
|
}
|
|
1262
1523
|
|
|
@@ -1344,6 +1605,7 @@ async function main(): Promise<void> {
|
|
|
1344
1605
|
if (event.hangoutLink) console.log(` Meet: ${event.hangoutLink}`);
|
|
1345
1606
|
if (event.htmlLink) console.log(` Link: ${event.htmlLink}`);
|
|
1346
1607
|
console.log(` Status: ${event.status || 'confirmed'}`);
|
|
1608
|
+
console.log(` Shows as: ${event.transparency === 'transparent' ? 'free' : 'busy'}`);
|
|
1347
1609
|
if (event.created) console.log(` Created: ${formatDateTime({ dateTime: event.created })}`);
|
|
1348
1610
|
if (event.updated) console.log(` Updated: ${formatDateTime({ dateTime: event.updated })}`);
|
|
1349
1611
|
console.log(` ID: ${event.id}`);
|
|
@@ -1389,13 +1651,7 @@ async function main(): Promise<void> {
|
|
|
1389
1651
|
console.log(`Opening: ${event.summary || '(no title)'}`);
|
|
1390
1652
|
console.log(` ${event.htmlLink}`);
|
|
1391
1653
|
|
|
1392
|
-
|
|
1393
|
-
execSync(`start "" "${event.htmlLink}"`, { stdio: 'ignore', shell: 'cmd.exe' });
|
|
1394
|
-
} else if (process.platform === 'darwin') {
|
|
1395
|
-
execSync(`open "${event.htmlLink}"`, { stdio: 'ignore' });
|
|
1396
|
-
} else {
|
|
1397
|
-
execSync(`xdg-open "${event.htmlLink}"`, { stdio: 'ignore' });
|
|
1398
|
-
}
|
|
1654
|
+
openUrl(event.htmlLink);
|
|
1399
1655
|
break;
|
|
1400
1656
|
}
|
|
1401
1657
|
|
package/glib/gutils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gutils.d.ts","sourceRoot":"","sources":["gutils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,UAAU;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,OAAO,QAAoC,CAAC;AAEzD,+DAA+D;AAC/D,wBAAgB,SAAS,IAAI,MAAM,CAKlC;AAED,kFAAkF;AAClF,wBAAgB,UAAU,IAAI,MAAM,CAMnC;AAED,eAAO,MAAM,QAAQ,QAAe,CAAC;AACrC,eAAO,MAAM,WAAW,QAAwC,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAA8C,CAAC;AAE5E,wBAAgB,UAAU,IAAI,UAAU,CASvC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAMnD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAUpD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQhD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAStC;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAIpD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,MAAM,CAsDzE;AAID,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAgBlG;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmB7H;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"gutils.d.ts","sourceRoot":"","sources":["gutils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,UAAU;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,OAAO,QAAoC,CAAC;AAEzD,+DAA+D;AAC/D,wBAAgB,SAAS,IAAI,MAAM,CAKlC;AAED,kFAAkF;AAClF,wBAAgB,UAAU,IAAI,MAAM,CAMnC;AAED,eAAO,MAAM,QAAQ,QAAe,CAAC;AACrC,eAAO,MAAM,WAAW,QAAwC,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAA8C,CAAC;AAE5E,wBAAgB,UAAU,IAAI,UAAU,CASvC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAMnD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAUpD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQhD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAStC;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAIpD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,MAAM,CAsDzE;AAID,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAgBlG;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmB7H;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CA+KjD;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGjD;AAED,kDAAkD;AAClD,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAKzC;AAED,4BAA4B;AAC5B,wBAAgB,EAAE,IAAI,MAAM,CAG3B"}
|
package/glib/gutils.js
CHANGED
|
@@ -306,6 +306,19 @@ export function parseDateTime(input) {
|
|
|
306
306
|
d.setHours(h, parseInt(min || '0'), 0, 0);
|
|
307
307
|
return d;
|
|
308
308
|
}
|
|
309
|
+
// Handle weekday name alone: "wed", "next wed", "friday" -> that day at midnight
|
|
310
|
+
const dayOnlyMatch = lower.match(/^(next\s+)?(sun|mon|tue|wed|thu|fri|sat)[a-z]*$/i);
|
|
311
|
+
if (dayOnlyMatch) {
|
|
312
|
+
const [, next, day] = dayOnlyMatch;
|
|
313
|
+
const targetDay = weekdayAbbr.indexOf(day.toLowerCase().slice(0, 3));
|
|
314
|
+
const d = new Date(now);
|
|
315
|
+
let daysUntil = targetDay - d.getDay();
|
|
316
|
+
if (daysUntil <= 0 || next)
|
|
317
|
+
daysUntil += 7;
|
|
318
|
+
d.setDate(d.getDate() + daysUntil);
|
|
319
|
+
d.setHours(0, 0, 0, 0);
|
|
320
|
+
return d;
|
|
321
|
+
}
|
|
309
322
|
// Handle month names: "jan 15", "jan 15 2026", "jan 15 3pm", "january 15 2026 3pm"
|
|
310
323
|
const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
311
324
|
const monthMatch = lower.match(/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+(\d{1,2})(?:\s+(\d{4}))?(?:\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/i);
|