@bobfrankston/gcal 0.1.26 → 0.1.30

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.ts CHANGED
@@ -4,19 +4,21 @@
4
4
  * Manage Google Calendar events with ICS import support
5
5
  *
6
6
  * Can be associated with .ics files for direct import:
7
- * gcal assoc
7
+ * assoc .ics=icsfile
8
+ * ftype icsfile=gcal "%1"
8
9
  */
9
10
 
10
11
  import fs from 'fs';
11
12
  import path from 'path';
12
- import { execSync } from 'child_process';
13
13
  import { authenticateOAuth } from '@bobfrankston/oauthsupport';
14
- import type { GoogleEvent, EventReminder, EventsListResponse, CalendarListEntry, CalendarListResponse } from './glib/types.ts';
14
+ import type { GoogleEvent, EventsListResponse, CalendarListEntry, CalendarListResponse } from './glib/types.ts';
15
15
  import {
16
16
  CREDENTIALS_FILE, loadConfig, saveConfig, getUserPaths,
17
17
  ensureUserDir, formatDateTime, formatDuration, parseDuration, parseDateTime, ts, normalizeUser
18
18
  } from './glib/gutils.js';
19
19
 
20
+ import pkg from './package.json' with { type: 'json' };
21
+ const VERSION: string = pkg.version;
20
22
  const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
21
23
  const CALENDAR_SCOPE_READ = 'https://www.googleapis.com/auth/calendar.readonly';
22
24
  const CALENDAR_SCOPE_WRITE = 'https://www.googleapis.com/auth/calendar';
@@ -25,9 +27,15 @@ let abortController: AbortController = null;
25
27
 
26
28
  function setupAbortHandler(): void {
27
29
  abortController = new AbortController();
30
+ let ctrlCCount = 0;
28
31
  process.on('SIGINT', () => {
32
+ ctrlCCount++;
29
33
  abortController?.abort();
30
- console.log('\n\nCtrl+C pressed - aborting...');
34
+ if (ctrlCCount >= 2) {
35
+ console.log('\n\nForce exit.');
36
+ process.exit(1);
37
+ }
38
+ console.log('\n\nCtrl+C pressed - aborting... (press again to force exit)');
31
39
  });
32
40
  }
33
41
 
@@ -56,7 +64,7 @@ async function getAccessToken(user: string, writeAccess = false, forceRefresh =
56
64
  scope,
57
65
  tokenDirectory: paths.userDir,
58
66
  tokenFileName,
59
- credentialsKey: 'web',
67
+ credentialsKey: 'installed',
60
68
  signal: abortController?.signal
61
69
  });
62
70
 
@@ -90,8 +98,7 @@ async function listEvents(
90
98
  accessToken: string,
91
99
  calendarId = 'primary',
92
100
  maxResults = 10,
93
- timeMin?: string,
94
- timeMax?: string
101
+ timeMin?: string
95
102
  ): Promise<GoogleEvent[]> {
96
103
  const params = new URLSearchParams({
97
104
  maxResults: maxResults.toString(),
@@ -99,8 +106,6 @@ async function listEvents(
99
106
  orderBy: 'startTime',
100
107
  timeMin: timeMin || new Date().toISOString()
101
108
  });
102
- if (timeMax)
103
- params.set('timeMax', timeMax);
104
109
 
105
110
  const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
106
111
  const res = await apiFetch(url, accessToken);
@@ -141,24 +146,6 @@ async function deleteEvent(
141
146
  }
142
147
  }
143
148
 
144
- async function updateEvent(
145
- accessToken: string,
146
- eventId: string,
147
- body: Partial<GoogleEvent>,
148
- calendarId = 'primary'
149
- ): Promise<GoogleEvent> {
150
- const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`;
151
- const res = await apiFetch(url, accessToken, {
152
- method: 'PATCH',
153
- body: JSON.stringify(body)
154
- });
155
- if (!res.ok) {
156
- const errText = await res.text();
157
- throw new Error(`Failed to update event: ${res.status} ${errText}`);
158
- }
159
- return await res.json() as GoogleEvent;
160
- }
161
-
162
149
  async function importIcsFile(
163
150
  filePath: string,
164
151
  accessToken: string,
@@ -239,56 +226,9 @@ async function importIcsFile(
239
226
  return result;
240
227
  }
241
228
 
242
- function setupFileAssociation(): void {
243
- if (process.platform !== 'win32') {
244
- console.error('File association is only supported on Windows.');
245
- process.exit(1);
246
- }
247
-
248
- const classKey = 'HKCU\\Software\\Classes';
249
- const command = `cmd.exe /c gcal "%1"`;
250
-
251
- try {
252
- execSync(`reg add "${classKey}\\.ics" /ve /d "gcalFile" /f`, { stdio: 'pipe' });
253
- execSync(`reg add "${classKey}\\gcalFile\\shell\\open\\command" /ve /d "${command}" /f`, { stdio: 'pipe' });
254
- console.log('.ics file association set — double-click any .ics to import via gcal');
255
- } catch (e: any) {
256
- console.error(`Failed to set file association: ${e.message}`);
257
- process.exit(1);
258
- }
259
- }
260
-
261
- /** Format reminders compactly: "30m", "1h:email", "30m,1h:email", "def", "" */
262
- function formatReminders(reminders?: { useDefault?: boolean; overrides?: EventReminder[] }): string {
263
- if (!reminders) return '';
264
- if (reminders.useDefault) return 'def';
265
- if (!reminders.overrides || reminders.overrides.length === 0) return '';
266
- return reminders.overrides.map(r => {
267
- const mins = r.minutes || 0;
268
- const label = mins >= 1440 ? `${mins / 1440}d` : mins >= 60 ? `${mins / 60}h` : `${mins}m`;
269
- return `${label}${r.method === 'email' ? ':email' : ''}`;
270
- }).join(',');
271
- }
272
-
273
- /** Clean up URLs — replace https URLs with [sitename] labels */
274
- function cleanUrls(text: string): string {
275
- if (!text) return text;
276
- return text.replace(/https?:\/\/([^\/\s]+)\S*/gi, (_match, host: string) => {
277
- // Extract meaningful site name from hostname
278
- const parts = host.split('.');
279
- // Drop common prefixes (www, us02web, events, etc.) and TLD suffixes
280
- // Keep the main domain name: "us02web.zoom.us" → "Zoom", "events.vtools.ieee.org" → "ieee"
281
- if (parts.length >= 2) {
282
- const domain = parts.length > 2 ? parts[parts.length - 2] : parts[0];
283
- return `[${domain.charAt(0).toUpperCase() + domain.slice(1)}]`;
284
- }
285
- return '[link]';
286
- });
287
- }
288
-
289
229
  function showUsage(): void {
290
230
  console.log(`
291
- gcal - Google Calendar CLI
231
+ gcal v${VERSION} - Google Calendar CLI
292
232
 
293
233
  Usage:
294
234
  gcal <file.ics> Import ICS file (file association)
@@ -298,10 +238,8 @@ Commands:
298
238
  list [n] List upcoming n events (default: 10)
299
239
  add <title> <when> [duration] Add event
300
240
  del|delete <id> [id2...] Delete event(s) by ID (prefix match)
301
- update <id> [flags] Update event by ID (prefix match)
302
241
  import <file.ics> Import events from ICS file
303
242
  calendars List available calendars
304
- assoc Associate .ics files with gcal (Windows)
305
243
  help Show this help
306
244
 
307
245
  Options:
@@ -309,35 +247,21 @@ Options:
309
247
  -defaultUser <email> Set default user for future use
310
248
  -c, -calendar <id> Calendar ID (default: primary)
311
249
  -n <count> Number of events to list
312
- -limit <span> Time horizon for list: #d, #w, #m, #y (default: 3m)
313
250
  -v, -verbose Show event IDs and links
314
251
  -b, -birthdays Include birthday events (hidden by default)
315
- -title <text> New title (update command)
316
- -loc <text> New location (update command)
317
- -start <when> New start time (update command)
318
- -dur <duration> New duration (update command)
319
- -r <spec> Reminders: #m, #h, #d with optional :email/:popup
320
- -r 0 No reminders
321
- -r 30m 30 min popup (default kind)
322
- -r 1h:email 1 hour email
323
- -r 15m,1h Multiple: comma-separated
324
252
 
325
253
  Examples:
326
254
  gcal meeting.ics Import ICS file
327
255
  gcal list List next 10 events
328
- gcal add "Dentist" "fri 3pm" "1h"
256
+ gcal add "Dentist" "Friday 3pm" "1h"
329
257
  gcal add "Lunch" "1/14/2026 12:00" "1h"
330
258
  gcal add "Meeting" "tomorrow 10:00"
331
259
  gcal add "Appointment" "jan 15 2pm"
332
- gcal add "Lunch" "tomorrow noon" "1h"
333
- gcal add "Call" "tomorrow 3pm" "30m" -r 15m,1h:email
334
- gcal update abc1 -title "New Title" -loc "Room 5"
335
- gcal update abc1 -start "friday 3pm" -dur 2h
336
- gcal update abc1 -r 15m,1h:email
337
260
  gcal -defaultUser bob@gmail.com Set default user
338
261
 
339
262
  File Association (Windows):
340
- gcal assoc Set up .ics file association
263
+ assoc .ics=icsfile
264
+ ftype icsfile=gcal "%1"
341
265
  `);
342
266
  }
343
267
 
@@ -352,56 +276,6 @@ interface ParsedArgs {
352
276
  verbose: boolean;
353
277
  icsFile: string; /** Direct .ics file path */
354
278
  birthdays: boolean;
355
- reminders: EventReminder[]; /** Reminder overrides, empty = use defaults, null entry = no reminders */
356
- noReminders: boolean; /** -r 0 means no reminders at all */
357
- timeLimit: string; /** Max time horizon for list, e.g. "3m", "1y", "2w" — default "3m" */
358
- title: string; /** New title for update command */
359
- location: string; /** New location for update command */
360
- startTime: string; /** New start time for update command */
361
- duration: string; /** New duration for update command */
362
- }
363
-
364
- /** Parse a reminder spec like "30m", "1h", "2d", "30m:email", "1h:popup" */
365
- function parseReminder(spec: string): EventReminder {
366
- // Split on colon for method: "30m:email" or just "30m"
367
- const [timePart, methodPart] = spec.split(':');
368
- const method: 'email' | 'popup' = methodPart === 'email' ? 'email' : 'popup';
369
-
370
- const match = timePart.match(/^(\d+)\s*([mhd]?)$/i);
371
- if (!match) {
372
- console.error(`Invalid reminder: "${spec}" — use #m, #h, or #d (e.g. 30m, 1h, 2d)`);
373
- process.exit(1);
374
- }
375
-
376
- const num = parseInt(match[1]);
377
- const unit = (match[2] || 'm').toLowerCase();
378
- let minutes: number;
379
- switch (unit) {
380
- case 'h': minutes = num * 60; break;
381
- case 'd': minutes = num * 60 * 24; break;
382
- default: minutes = num; break;
383
- }
384
-
385
- return { method, minutes };
386
- }
387
-
388
- /** Parse a time limit spec like "3m", "1y", "2w", "90d" into a future Date */
389
- function parseTimeLimit(spec: string): Date {
390
- const match = spec.match(/^(\d+)\s*([dwmy]?)$/i);
391
- if (!match) {
392
- console.error(`Invalid time limit: "${spec}" — use #d, #w, #m, or #y (e.g. 3m, 90d, 1y)`);
393
- process.exit(1);
394
- }
395
- const num = parseInt(match[1]);
396
- const unit = (match[2] || 'm').toLowerCase();
397
- const now = new Date();
398
- switch (unit) {
399
- case 'd': now.setDate(now.getDate() + num); break;
400
- case 'w': now.setDate(now.getDate() + num * 7); break;
401
- case 'y': now.setFullYear(now.getFullYear() + num); break;
402
- default: now.setMonth(now.getMonth() + num); break; // 'm' = months
403
- }
404
- return now;
405
279
  }
406
280
 
407
281
  function parseArgs(argv: string[]): ParsedArgs {
@@ -415,14 +289,7 @@ function parseArgs(argv: string[]): ParsedArgs {
415
289
  help: false,
416
290
  verbose: false,
417
291
  icsFile: '',
418
- birthdays: false,
419
- reminders: [],
420
- noReminders: false,
421
- timeLimit: '3m',
422
- title: '',
423
- location: '',
424
- startTime: '',
425
- duration: ''
292
+ birthdays: false
426
293
  };
427
294
 
428
295
  const unknown: string[] = [];
@@ -457,49 +324,17 @@ function parseArgs(argv: string[]): ParsedArgs {
457
324
  case '--birthdays':
458
325
  result.birthdays = true;
459
326
  break;
460
- case '-limit':
461
- case '--limit':
462
- result.timeLimit = argv[++i] || '3m';
463
- break;
464
- case '-title':
465
- case '--title':
466
- result.title = argv[++i] || '';
467
- break;
468
- case '-loc':
469
- case '-location':
470
- case '--location':
471
- result.location = argv[++i] || '';
472
- break;
473
- case '-start':
474
- case '--start':
475
- result.startTime = argv[++i] || '';
476
- break;
477
- case '-dur':
478
- case '-duration':
479
- case '--duration':
480
- result.duration = argv[++i] || '';
481
- break;
482
- case '-r':
483
- case '-reminder':
484
- case '--reminder': {
485
- const rval = argv[++i] || '';
486
- if (rval === '0' || rval === 'none') {
487
- result.noReminders = true;
488
- } else {
489
- for (const part of rval.split(',')) {
490
- const reminder = parseReminder(part.trim());
491
- if (reminder)
492
- result.reminders.push(reminder);
493
- }
494
- }
495
- break;
496
- }
497
327
  case '-h':
498
328
  case '-help':
499
329
  case '--help':
500
330
  case 'help':
501
331
  result.help = true;
502
332
  break;
333
+ case '-V':
334
+ case '-version':
335
+ case '--version':
336
+ console.log(`gcal v${VERSION}`);
337
+ process.exit(0);
503
338
  default:
504
339
  if (arg.startsWith('-')) {
505
340
  unknown.push(arg);
@@ -519,8 +354,7 @@ function parseArgs(argv: string[]): ParsedArgs {
519
354
  }
520
355
 
521
356
  if (unknown.length > 0) {
522
- console.error(`Unknown options: ${unknown.join(', ')}\n`);
523
- showUsage();
357
+ console.error(`Unknown options: ${unknown.join(', ')}`);
524
358
  process.exit(1);
525
359
  }
526
360
 
@@ -575,12 +409,6 @@ async function main(): Promise<void> {
575
409
  process.exit(1);
576
410
  }
577
411
 
578
- // Commands that don't need a user
579
- if (parsed.command === 'assoc') {
580
- setupFileAssociation();
581
- process.exit(0);
582
- }
583
-
584
412
  // Resolve user
585
413
  const user = resolveUser(parsed.user, false);
586
414
  if (!user) {
@@ -620,9 +448,8 @@ async function main(): Promise<void> {
620
448
 
621
449
  case 'list': {
622
450
  const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
623
- const timeMax = parseTimeLimit(parsed.timeLimit).toISOString();
624
451
  const token = await getAccessToken(user, false);
625
- let events = await listEvents(token, parsed.calendar, count, undefined, timeMax);
452
+ let events = await listEvents(token, parsed.calendar, count);
626
453
  const birthdayCount = events.filter(e => e.eventType === 'birthday').length;
627
454
  if (!parsed.birthdays) {
628
455
  events = events.filter(e => e.eventType !== 'birthday');
@@ -639,33 +466,31 @@ async function main(): Promise<void> {
639
466
  const shortId = (event.id || '').slice(0, 8);
640
467
  const start = event.start ? formatDateTime(event.start) : '?';
641
468
  const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
642
- const summary = cleanUrls(event.summary || '(no title)') + (event.eventType === 'birthday' ? ' [from contact]' : '');
643
- const loc = cleanUrls(event.location || '');
644
- const rem = formatReminders(event.reminders);
469
+ const summary = (event.summary || '(no title)') + (event.eventType === 'birthday' ? ' [from contact]' : '');
470
+ const loc = event.location || '';
645
471
  if (parsed.verbose) {
646
- rows.push([shortId, start, duration, summary, rem, loc, event.htmlLink || '']);
472
+ rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
647
473
  } else {
648
- rows.push([shortId, start, duration, summary, rem, loc]);
474
+ rows.push([shortId, start, duration, summary, loc]);
649
475
  }
650
476
  }
651
477
 
652
478
  // Calculate column widths
653
479
  const headers = parsed.verbose
654
- ? ['ID', 'When', 'Dur', 'Event', 'Rem', 'Location', 'Link']
655
- : ['ID', 'When', 'Dur', 'Event', 'Rem', 'Location'];
480
+ ? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
481
+ : ['ID', 'When', 'Dur', 'Event', 'Location'];
656
482
  const colWidths = headers.map((h, i) =>
657
483
  Math.max(h.length, ...rows.map(r => (r[i] || '').length))
658
484
  );
659
485
 
660
486
  // Print header
661
- const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
487
+ const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
662
488
  console.log(headerLine);
663
- console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
489
+ console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
664
490
 
665
- // Print rows — last column not padded to avoid trailing whitespace
491
+ // Print rows
666
492
  for (const row of rows) {
667
- const lastIdx = row.length - 1;
668
- const line = row.map((cell, i) => i < lastIdx ? (cell || '').padEnd(colWidths[i]) : (cell || '')).join(' ');
493
+ const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
669
494
  console.log(line);
670
495
  }
671
496
  }
@@ -699,26 +524,10 @@ async function main(): Promise<void> {
699
524
  }
700
525
  };
701
526
 
702
- if (parsed.noReminders) {
703
- event.reminders = { useDefault: false, overrides: [] };
704
- } else if (parsed.reminders.length > 0) {
705
- event.reminders = { useDefault: false, overrides: parsed.reminders };
706
- }
707
-
708
527
  const token = await getAccessToken(user, true);
709
528
  const created = await createEvent(token, event, parsed.calendar);
710
529
  console.log(`\nEvent created: ${created.summary}`);
711
530
  console.log(` When: ${formatDateTime(created.start)} - ${formatDateTime(created.end)}`);
712
- if (parsed.noReminders) {
713
- console.log(` Reminders: none`);
714
- } else if (parsed.reminders.length > 0) {
715
- const rlist = parsed.reminders.map(r => {
716
- const mins = r.minutes;
717
- const label = mins >= 1440 ? `${mins / 1440}d` : mins >= 60 ? `${mins / 60}h` : `${mins}m`;
718
- return `${label}${r.method === 'email' ? ':email' : ''}`;
719
- }).join(', ');
720
- console.log(` Reminders: ${rlist}`);
721
- }
722
531
  if (created.htmlLink) {
723
532
  console.log(` Link: ${created.htmlLink}`);
724
533
  }
@@ -746,12 +555,7 @@ async function main(): Promise<void> {
746
555
  if (matches.length > 1) {
747
556
  console.error(`${idPrefix}: ambiguous (${matches.length} matches)`);
748
557
  for (const e of matches) {
749
- // Show enough ID to distinguish recurring instances (base_date)
750
- const displayId = (e.id || '').length > 12 ? e.id!.slice(0, 16) : e.id?.slice(0, 8);
751
- const cleaned = cleanUrls(e.summary || '');
752
- const summary = cleaned.length > 60 ? cleaned.slice(0, 57) + '...' : cleaned;
753
- const when = e.start ? formatDateTime(e.start) : '';
754
- console.error(` ${displayId} ${when} ${summary}`);
558
+ console.error(` ${e.id?.slice(0, 8)} - ${e.summary}`);
755
559
  }
756
560
  continue;
757
561
  }
@@ -762,7 +566,7 @@ async function main(): Promise<void> {
762
566
  continue;
763
567
  }
764
568
  await deleteEvent(token, event.id!, parsed.calendar);
765
- console.log(`Deleted: ${cleanUrls(event.summary || '')}`);
569
+ console.log(`Deleted: ${event.summary}`);
766
570
  }
767
571
  break;
768
572
  }
@@ -781,104 +585,6 @@ async function main(): Promise<void> {
781
585
  break;
782
586
  }
783
587
 
784
- case 'update': {
785
- if (parsed.args.length === 0) {
786
- console.error('Usage: gcal update <id> [-title "..."] [-loc "..."] [-start "..."] [-dur "..."] [-r ...]');
787
- console.error('Use "gcal list -v" to see event IDs');
788
- process.exit(1);
789
- }
790
-
791
- const idPrefix = parsed.args[0];
792
- const token = await getAccessToken(user, true);
793
- const events = await listEvents(token, parsed.calendar, 50);
794
- const matches = events.filter(e => e.id?.startsWith(idPrefix));
795
-
796
- if (matches.length === 0) {
797
- console.error(`${idPrefix}: not found`);
798
- process.exit(1);
799
- }
800
- if (matches.length > 1) {
801
- console.error(`${idPrefix}: ambiguous (${matches.length} matches)`);
802
- for (const e of matches) {
803
- const displayId = (e.id || '').length > 12 ? e.id.slice(0, 16) : e.id?.slice(0, 8);
804
- const cleaned = cleanUrls(e.summary || '');
805
- const summary = cleaned.length > 60 ? cleaned.slice(0, 57) + '...' : cleaned;
806
- const when = e.start ? formatDateTime(e.start) : '';
807
- console.error(` ${displayId} ${when} ${summary}`);
808
- }
809
- process.exit(1);
810
- }
811
-
812
- const target = matches[0];
813
- const body: Partial<GoogleEvent> = {};
814
- const changes: string[] = [];
815
-
816
- if (parsed.title) {
817
- body.summary = parsed.title;
818
- changes.push(`title → "${parsed.title}"`);
819
- }
820
- if (parsed.location) {
821
- body.location = parsed.location;
822
- changes.push(`location → "${parsed.location}"`);
823
- }
824
- if (parsed.startTime) {
825
- const newStart = parseDateTime(parsed.startTime);
826
- const durationMs = parsed.duration
827
- ? parseDuration(parsed.duration) * 60 * 1000
828
- : (target.end?.dateTime && target.start?.dateTime)
829
- ? new Date(target.end.dateTime).getTime() - new Date(target.start.dateTime).getTime()
830
- : 60 * 60 * 1000;
831
- const newEnd = new Date(newStart.getTime() + durationMs);
832
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
833
- body.start = { dateTime: newStart.toISOString(), timeZone: tz };
834
- body.end = { dateTime: newEnd.toISOString(), timeZone: tz };
835
- changes.push(`start → ${formatDateTime(body.start)}`);
836
- if (parsed.duration) changes.push(`duration → ${parsed.duration}`);
837
- } else if (parsed.duration) {
838
- // Duration change without start change — shift end time
839
- if (target.start?.dateTime) {
840
- const startMs = new Date(target.start.dateTime).getTime();
841
- const newEnd = new Date(startMs + parseDuration(parsed.duration) * 60 * 1000);
842
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
843
- body.end = { dateTime: newEnd.toISOString(), timeZone: tz };
844
- changes.push(`duration → ${parsed.duration}`);
845
- } else {
846
- console.error('Cannot change duration of all-day event');
847
- process.exit(1);
848
- }
849
- }
850
-
851
- if (parsed.noReminders) {
852
- body.reminders = { useDefault: false, overrides: [] };
853
- changes.push('reminders → none');
854
- } else if (parsed.reminders.length > 0) {
855
- body.reminders = { useDefault: false, overrides: parsed.reminders };
856
- const rlist = parsed.reminders.map(r => {
857
- const mins = r.minutes;
858
- const label = mins >= 1440 ? `${mins / 1440}d` : mins >= 60 ? `${mins / 60}h` : `${mins}m`;
859
- return `${label}${r.method === 'email' ? ':email' : ''}`;
860
- }).join(', ');
861
- changes.push(`reminders → ${rlist}`);
862
- }
863
-
864
- if (changes.length === 0) {
865
- console.error('No update flags provided. Use -title, -loc, -start, -dur, or -r');
866
- process.exit(1);
867
- }
868
-
869
- const updated = await updateEvent(token, target.id, body, parsed.calendar);
870
- console.log(`\nUpdated: ${cleanUrls(updated.summary || '')}`);
871
- for (const c of changes) {
872
- console.log(` ${c}`);
873
- }
874
- break;
875
- }
876
-
877
- case 'assoc': {
878
- setupFileAssociation();
879
- break;
880
- }
881
-
882
588
  default:
883
589
  console.error(`Unknown command: ${parsed.command}`);
884
590
  showUsage();
@@ -887,10 +593,8 @@ async function main(): Promise<void> {
887
593
  }
888
594
 
889
595
  if (import.meta.main) {
890
- main().then(() => {
891
- process.exitCode = 0;
892
- }).catch(e => {
596
+ main().catch(e => {
893
597
  console.error(`Error: ${e.message}`);
894
- process.exitCode = 1;
598
+ process.exit(1);
895
599
  });
896
600
  }
package/glib/gutils.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare function getAppDir(): string;
20
20
  export declare function getDataDir(): string;
21
21
  export declare const DATA_DIR: string;
22
22
  export declare const CONFIG_FILE: string;
23
- /** Use credentials from gcards (shared OAuth app) */
23
+ /** OAuth credentials shipped with the package */
24
24
  export declare const CREDENTIALS_FILE: string;
25
25
  export declare function loadConfig(): GcalConfig;
26
26
  export declare function saveConfig(config: GcalConfig): void;
@@ -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;CAC5B;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,qDAAqD;AACrD,eAAO,MAAM,gBAAgB,QAAiE,CAAC;AAE/F,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;AAED,4DAA4D;AAC5D,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,CAclG;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,CAkB7H;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAoIjD;AAED,4BAA4B;AAC5B,wBAAgB,EAAE,IAAI,MAAM,CAG3B"}
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;CAC5B;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;AAED,4DAA4D;AAC5D,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,CAclG;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,CAkB7H;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CA+GjD;AAED,4BAA4B;AAC5B,wBAAgB,EAAE,IAAI,MAAM,CAG3B"}