@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.d.ts +2 -1
- package/gcal.d.ts.map +1 -1
- package/gcal.js +39 -326
- package/gcal.js.map +1 -1
- package/gcal.ts +40 -336
- package/glib/gutils.d.ts +1 -1
- package/glib/gutils.d.ts.map +1 -1
- package/glib/gutils.js +26 -49
- package/glib/gutils.js.map +1 -1
- package/glib/gutils.ts +22 -43
- package/package.json +4 -4
- package/tsconfig.json +1 -0
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
|
-
*
|
|
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,
|
|
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
|
-
|
|
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: '
|
|
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" "
|
|
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
|
-
|
|
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(', ')}
|
|
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
|
|
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 =
|
|
643
|
-
const loc =
|
|
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,
|
|
472
|
+
rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
|
|
647
473
|
} else {
|
|
648
|
-
rows.push([shortId, start, duration, summary,
|
|
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', '
|
|
655
|
-
: ['ID', 'When', 'Dur', 'Event', '
|
|
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
|
|
491
|
+
// Print rows
|
|
666
492
|
for (const row of rows) {
|
|
667
|
-
const
|
|
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
|
-
|
|
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: ${
|
|
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().
|
|
891
|
-
process.exitCode = 0;
|
|
892
|
-
}).catch(e => {
|
|
596
|
+
main().catch(e => {
|
|
893
597
|
console.error(`Error: ${e.message}`);
|
|
894
|
-
process.
|
|
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
|
-
/**
|
|
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;
|
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;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,
|
|
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"}
|