@bobfrankston/gcal 0.1.26 → 0.1.31
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.d.ts
CHANGED
package/gcal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gcal.d.ts","sourceRoot":"","sources":["gcal.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"gcal.d.ts","sourceRoot":"","sources":["gcal.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG"}
|
package/gcal.js
CHANGED
|
@@ -4,22 +4,30 @@
|
|
|
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
|
import fs from 'fs';
|
|
10
11
|
import path from 'path';
|
|
11
|
-
import { execSync } from 'child_process';
|
|
12
12
|
import { authenticateOAuth } from '@bobfrankston/oauthsupport';
|
|
13
13
|
import { CREDENTIALS_FILE, loadConfig, saveConfig, getUserPaths, ensureUserDir, formatDateTime, formatDuration, parseDuration, parseDateTime, ts, normalizeUser } from './glib/gutils.js';
|
|
14
|
+
import pkg from './package.json' with { type: 'json' };
|
|
15
|
+
const VERSION = pkg.version;
|
|
14
16
|
const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
|
|
15
17
|
const CALENDAR_SCOPE_READ = 'https://www.googleapis.com/auth/calendar.readonly';
|
|
16
18
|
const CALENDAR_SCOPE_WRITE = 'https://www.googleapis.com/auth/calendar';
|
|
17
19
|
let abortController = null;
|
|
18
20
|
function setupAbortHandler() {
|
|
19
21
|
abortController = new AbortController();
|
|
22
|
+
let ctrlCCount = 0;
|
|
20
23
|
process.on('SIGINT', () => {
|
|
24
|
+
ctrlCCount++;
|
|
21
25
|
abortController?.abort();
|
|
22
|
-
|
|
26
|
+
if (ctrlCCount >= 2) {
|
|
27
|
+
console.log('\n\nForce exit.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
console.log('\n\nCtrl+C pressed - aborting... (press again to force exit)');
|
|
23
31
|
});
|
|
24
32
|
}
|
|
25
33
|
async function getAccessToken(user, writeAccess = false, forceRefresh = false) {
|
|
@@ -43,7 +51,7 @@ async function getAccessToken(user, writeAccess = false, forceRefresh = false) {
|
|
|
43
51
|
scope,
|
|
44
52
|
tokenDirectory: paths.userDir,
|
|
45
53
|
tokenFileName,
|
|
46
|
-
credentialsKey: '
|
|
54
|
+
credentialsKey: 'installed',
|
|
47
55
|
signal: abortController?.signal
|
|
48
56
|
});
|
|
49
57
|
if (!token) {
|
|
@@ -68,15 +76,13 @@ async function listCalendars(accessToken) {
|
|
|
68
76
|
const data = await res.json();
|
|
69
77
|
return data.items || [];
|
|
70
78
|
}
|
|
71
|
-
async function listEvents(accessToken, calendarId = 'primary', maxResults = 10, timeMin
|
|
79
|
+
async function listEvents(accessToken, calendarId = 'primary', maxResults = 10, timeMin) {
|
|
72
80
|
const params = new URLSearchParams({
|
|
73
81
|
maxResults: maxResults.toString(),
|
|
74
82
|
singleEvents: 'true',
|
|
75
83
|
orderBy: 'startTime',
|
|
76
84
|
timeMin: timeMin || new Date().toISOString()
|
|
77
85
|
});
|
|
78
|
-
if (timeMax)
|
|
79
|
-
params.set('timeMax', timeMax);
|
|
80
86
|
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
|
|
81
87
|
const res = await apiFetch(url, accessToken);
|
|
82
88
|
if (!res.ok) {
|
|
@@ -105,18 +111,6 @@ async function deleteEvent(accessToken, eventId, calendarId = 'primary') {
|
|
|
105
111
|
throw new Error(`Failed to delete event: ${res.status} ${errText}`);
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
|
-
async function updateEvent(accessToken, eventId, body, calendarId = 'primary') {
|
|
109
|
-
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`;
|
|
110
|
-
const res = await apiFetch(url, accessToken, {
|
|
111
|
-
method: 'PATCH',
|
|
112
|
-
body: JSON.stringify(body)
|
|
113
|
-
});
|
|
114
|
-
if (!res.ok) {
|
|
115
|
-
const errText = await res.text();
|
|
116
|
-
throw new Error(`Failed to update event: ${res.status} ${errText}`);
|
|
117
|
-
}
|
|
118
|
-
return await res.json();
|
|
119
|
-
}
|
|
120
114
|
async function importIcsFile(filePath, accessToken, calendarId = 'primary') {
|
|
121
115
|
const ICAL = await import('ical.js');
|
|
122
116
|
const result = { imported: 0, errors: [] };
|
|
@@ -186,56 +180,9 @@ async function importIcsFile(filePath, accessToken, calendarId = 'primary') {
|
|
|
186
180
|
}
|
|
187
181
|
return result;
|
|
188
182
|
}
|
|
189
|
-
function setupFileAssociation() {
|
|
190
|
-
if (process.platform !== 'win32') {
|
|
191
|
-
console.error('File association is only supported on Windows.');
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
const classKey = 'HKCU\\Software\\Classes';
|
|
195
|
-
const command = `cmd.exe /c gcal "%1"`;
|
|
196
|
-
try {
|
|
197
|
-
execSync(`reg add "${classKey}\\.ics" /ve /d "gcalFile" /f`, { stdio: 'pipe' });
|
|
198
|
-
execSync(`reg add "${classKey}\\gcalFile\\shell\\open\\command" /ve /d "${command}" /f`, { stdio: 'pipe' });
|
|
199
|
-
console.log('.ics file association set — double-click any .ics to import via gcal');
|
|
200
|
-
}
|
|
201
|
-
catch (e) {
|
|
202
|
-
console.error(`Failed to set file association: ${e.message}`);
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/** Format reminders compactly: "30m", "1h:email", "30m,1h:email", "def", "" */
|
|
207
|
-
function formatReminders(reminders) {
|
|
208
|
-
if (!reminders)
|
|
209
|
-
return '';
|
|
210
|
-
if (reminders.useDefault)
|
|
211
|
-
return 'def';
|
|
212
|
-
if (!reminders.overrides || reminders.overrides.length === 0)
|
|
213
|
-
return '';
|
|
214
|
-
return reminders.overrides.map(r => {
|
|
215
|
-
const mins = r.minutes || 0;
|
|
216
|
-
const label = mins >= 1440 ? `${mins / 1440}d` : mins >= 60 ? `${mins / 60}h` : `${mins}m`;
|
|
217
|
-
return `${label}${r.method === 'email' ? ':email' : ''}`;
|
|
218
|
-
}).join(',');
|
|
219
|
-
}
|
|
220
|
-
/** Clean up URLs — replace https URLs with [sitename] labels */
|
|
221
|
-
function cleanUrls(text) {
|
|
222
|
-
if (!text)
|
|
223
|
-
return text;
|
|
224
|
-
return text.replace(/https?:\/\/([^\/\s]+)\S*/gi, (_match, host) => {
|
|
225
|
-
// Extract meaningful site name from hostname
|
|
226
|
-
const parts = host.split('.');
|
|
227
|
-
// Drop common prefixes (www, us02web, events, etc.) and TLD suffixes
|
|
228
|
-
// Keep the main domain name: "us02web.zoom.us" → "Zoom", "events.vtools.ieee.org" → "ieee"
|
|
229
|
-
if (parts.length >= 2) {
|
|
230
|
-
const domain = parts.length > 2 ? parts[parts.length - 2] : parts[0];
|
|
231
|
-
return `[${domain.charAt(0).toUpperCase() + domain.slice(1)}]`;
|
|
232
|
-
}
|
|
233
|
-
return '[link]';
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
183
|
function showUsage() {
|
|
237
184
|
console.log(`
|
|
238
|
-
gcal - Google Calendar CLI
|
|
185
|
+
gcal v${VERSION} - Google Calendar CLI
|
|
239
186
|
|
|
240
187
|
Usage:
|
|
241
188
|
gcal <file.ics> Import ICS file (file association)
|
|
@@ -245,10 +192,8 @@ Commands:
|
|
|
245
192
|
list [n] List upcoming n events (default: 10)
|
|
246
193
|
add <title> <when> [duration] Add event
|
|
247
194
|
del|delete <id> [id2...] Delete event(s) by ID (prefix match)
|
|
248
|
-
update <id> [flags] Update event by ID (prefix match)
|
|
249
195
|
import <file.ics> Import events from ICS file
|
|
250
196
|
calendars List available calendars
|
|
251
|
-
assoc Associate .ics files with gcal (Windows)
|
|
252
197
|
help Show this help
|
|
253
198
|
|
|
254
199
|
Options:
|
|
@@ -256,89 +201,23 @@ Options:
|
|
|
256
201
|
-defaultUser <email> Set default user for future use
|
|
257
202
|
-c, -calendar <id> Calendar ID (default: primary)
|
|
258
203
|
-n <count> Number of events to list
|
|
259
|
-
-limit <span> Time horizon for list: #d, #w, #m, #y (default: 3m)
|
|
260
204
|
-v, -verbose Show event IDs and links
|
|
261
205
|
-b, -birthdays Include birthday events (hidden by default)
|
|
262
|
-
-title <text> New title (update command)
|
|
263
|
-
-loc <text> New location (update command)
|
|
264
|
-
-start <when> New start time (update command)
|
|
265
|
-
-dur <duration> New duration (update command)
|
|
266
|
-
-r <spec> Reminders: #m, #h, #d with optional :email/:popup
|
|
267
|
-
-r 0 No reminders
|
|
268
|
-
-r 30m 30 min popup (default kind)
|
|
269
|
-
-r 1h:email 1 hour email
|
|
270
|
-
-r 15m,1h Multiple: comma-separated
|
|
271
206
|
|
|
272
207
|
Examples:
|
|
273
208
|
gcal meeting.ics Import ICS file
|
|
274
209
|
gcal list List next 10 events
|
|
275
|
-
gcal add "Dentist" "
|
|
210
|
+
gcal add "Dentist" "Friday 3pm" "1h"
|
|
276
211
|
gcal add "Lunch" "1/14/2026 12:00" "1h"
|
|
277
212
|
gcal add "Meeting" "tomorrow 10:00"
|
|
278
213
|
gcal add "Appointment" "jan 15 2pm"
|
|
279
|
-
gcal add "Lunch" "tomorrow noon" "1h"
|
|
280
|
-
gcal add "Call" "tomorrow 3pm" "30m" -r 15m,1h:email
|
|
281
|
-
gcal update abc1 -title "New Title" -loc "Room 5"
|
|
282
|
-
gcal update abc1 -start "friday 3pm" -dur 2h
|
|
283
|
-
gcal update abc1 -r 15m,1h:email
|
|
284
214
|
gcal -defaultUser bob@gmail.com Set default user
|
|
285
215
|
|
|
286
216
|
File Association (Windows):
|
|
287
|
-
|
|
217
|
+
assoc .ics=icsfile
|
|
218
|
+
ftype icsfile=gcal "%1"
|
|
288
219
|
`);
|
|
289
220
|
}
|
|
290
|
-
/** Parse a reminder spec like "30m", "1h", "2d", "30m:email", "1h:popup" */
|
|
291
|
-
function parseReminder(spec) {
|
|
292
|
-
// Split on colon for method: "30m:email" or just "30m"
|
|
293
|
-
const [timePart, methodPart] = spec.split(':');
|
|
294
|
-
const method = methodPart === 'email' ? 'email' : 'popup';
|
|
295
|
-
const match = timePart.match(/^(\d+)\s*([mhd]?)$/i);
|
|
296
|
-
if (!match) {
|
|
297
|
-
console.error(`Invalid reminder: "${spec}" — use #m, #h, or #d (e.g. 30m, 1h, 2d)`);
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
const num = parseInt(match[1]);
|
|
301
|
-
const unit = (match[2] || 'm').toLowerCase();
|
|
302
|
-
let minutes;
|
|
303
|
-
switch (unit) {
|
|
304
|
-
case 'h':
|
|
305
|
-
minutes = num * 60;
|
|
306
|
-
break;
|
|
307
|
-
case 'd':
|
|
308
|
-
minutes = num * 60 * 24;
|
|
309
|
-
break;
|
|
310
|
-
default:
|
|
311
|
-
minutes = num;
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
return { method, minutes };
|
|
315
|
-
}
|
|
316
|
-
/** Parse a time limit spec like "3m", "1y", "2w", "90d" into a future Date */
|
|
317
|
-
function parseTimeLimit(spec) {
|
|
318
|
-
const match = spec.match(/^(\d+)\s*([dwmy]?)$/i);
|
|
319
|
-
if (!match) {
|
|
320
|
-
console.error(`Invalid time limit: "${spec}" — use #d, #w, #m, or #y (e.g. 3m, 90d, 1y)`);
|
|
321
|
-
process.exit(1);
|
|
322
|
-
}
|
|
323
|
-
const num = parseInt(match[1]);
|
|
324
|
-
const unit = (match[2] || 'm').toLowerCase();
|
|
325
|
-
const now = new Date();
|
|
326
|
-
switch (unit) {
|
|
327
|
-
case 'd':
|
|
328
|
-
now.setDate(now.getDate() + num);
|
|
329
|
-
break;
|
|
330
|
-
case 'w':
|
|
331
|
-
now.setDate(now.getDate() + num * 7);
|
|
332
|
-
break;
|
|
333
|
-
case 'y':
|
|
334
|
-
now.setFullYear(now.getFullYear() + num);
|
|
335
|
-
break;
|
|
336
|
-
default:
|
|
337
|
-
now.setMonth(now.getMonth() + num);
|
|
338
|
-
break; // 'm' = months
|
|
339
|
-
}
|
|
340
|
-
return now;
|
|
341
|
-
}
|
|
342
221
|
function parseArgs(argv) {
|
|
343
222
|
const result = {
|
|
344
223
|
command: '',
|
|
@@ -350,14 +229,7 @@ function parseArgs(argv) {
|
|
|
350
229
|
help: false,
|
|
351
230
|
verbose: false,
|
|
352
231
|
icsFile: '',
|
|
353
|
-
birthdays: false
|
|
354
|
-
reminders: [],
|
|
355
|
-
noReminders: false,
|
|
356
|
-
timeLimit: '3m',
|
|
357
|
-
title: '',
|
|
358
|
-
location: '',
|
|
359
|
-
startTime: '',
|
|
360
|
-
duration: ''
|
|
232
|
+
birthdays: false
|
|
361
233
|
};
|
|
362
234
|
const unknown = [];
|
|
363
235
|
let i = 0;
|
|
@@ -391,50 +263,17 @@ function parseArgs(argv) {
|
|
|
391
263
|
case '--birthdays':
|
|
392
264
|
result.birthdays = true;
|
|
393
265
|
break;
|
|
394
|
-
case '-limit':
|
|
395
|
-
case '--limit':
|
|
396
|
-
result.timeLimit = argv[++i] || '3m';
|
|
397
|
-
break;
|
|
398
|
-
case '-title':
|
|
399
|
-
case '--title':
|
|
400
|
-
result.title = argv[++i] || '';
|
|
401
|
-
break;
|
|
402
|
-
case '-loc':
|
|
403
|
-
case '-location':
|
|
404
|
-
case '--location':
|
|
405
|
-
result.location = argv[++i] || '';
|
|
406
|
-
break;
|
|
407
|
-
case '-start':
|
|
408
|
-
case '--start':
|
|
409
|
-
result.startTime = argv[++i] || '';
|
|
410
|
-
break;
|
|
411
|
-
case '-dur':
|
|
412
|
-
case '-duration':
|
|
413
|
-
case '--duration':
|
|
414
|
-
result.duration = argv[++i] || '';
|
|
415
|
-
break;
|
|
416
|
-
case '-r':
|
|
417
|
-
case '-reminder':
|
|
418
|
-
case '--reminder': {
|
|
419
|
-
const rval = argv[++i] || '';
|
|
420
|
-
if (rval === '0' || rval === 'none') {
|
|
421
|
-
result.noReminders = true;
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
for (const part of rval.split(',')) {
|
|
425
|
-
const reminder = parseReminder(part.trim());
|
|
426
|
-
if (reminder)
|
|
427
|
-
result.reminders.push(reminder);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
266
|
case '-h':
|
|
433
267
|
case '-help':
|
|
434
268
|
case '--help':
|
|
435
269
|
case 'help':
|
|
436
270
|
result.help = true;
|
|
437
271
|
break;
|
|
272
|
+
case '-V':
|
|
273
|
+
case '-version':
|
|
274
|
+
case '--version':
|
|
275
|
+
console.log(`gcal v${VERSION}`);
|
|
276
|
+
process.exit(0);
|
|
438
277
|
default:
|
|
439
278
|
if (arg.startsWith('-')) {
|
|
440
279
|
unknown.push(arg);
|
|
@@ -456,8 +295,7 @@ function parseArgs(argv) {
|
|
|
456
295
|
i++;
|
|
457
296
|
}
|
|
458
297
|
if (unknown.length > 0) {
|
|
459
|
-
console.error(`Unknown options: ${unknown.join(', ')}
|
|
460
|
-
showUsage();
|
|
298
|
+
console.error(`Unknown options: ${unknown.join(', ')}`);
|
|
461
299
|
process.exit(1);
|
|
462
300
|
}
|
|
463
301
|
return result;
|
|
@@ -501,11 +339,6 @@ async function main() {
|
|
|
501
339
|
showUsage();
|
|
502
340
|
process.exit(1);
|
|
503
341
|
}
|
|
504
|
-
// Commands that don't need a user
|
|
505
|
-
if (parsed.command === 'assoc') {
|
|
506
|
-
setupFileAssociation();
|
|
507
|
-
process.exit(0);
|
|
508
|
-
}
|
|
509
342
|
// Resolve user
|
|
510
343
|
const user = resolveUser(parsed.user, false);
|
|
511
344
|
if (!user) {
|
|
@@ -538,9 +371,8 @@ async function main() {
|
|
|
538
371
|
}
|
|
539
372
|
case 'list': {
|
|
540
373
|
const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
|
|
541
|
-
const timeMax = parseTimeLimit(parsed.timeLimit).toISOString();
|
|
542
374
|
const token = await getAccessToken(user, false);
|
|
543
|
-
let events = await listEvents(token, parsed.calendar, count
|
|
375
|
+
let events = await listEvents(token, parsed.calendar, count);
|
|
544
376
|
const birthdayCount = events.filter(e => e.eventType === 'birthday').length;
|
|
545
377
|
if (!parsed.birthdays) {
|
|
546
378
|
events = events.filter(e => e.eventType !== 'birthday');
|
|
@@ -556,29 +388,27 @@ async function main() {
|
|
|
556
388
|
const shortId = (event.id || '').slice(0, 8);
|
|
557
389
|
const start = event.start ? formatDateTime(event.start) : '?';
|
|
558
390
|
const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
|
|
559
|
-
const summary =
|
|
560
|
-
const loc =
|
|
561
|
-
const rem = formatReminders(event.reminders);
|
|
391
|
+
const summary = (event.summary || '(no title)') + (event.eventType === 'birthday' ? ' [from contact]' : '');
|
|
392
|
+
const loc = event.location || '';
|
|
562
393
|
if (parsed.verbose) {
|
|
563
|
-
rows.push([shortId, start, duration, summary,
|
|
394
|
+
rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
|
|
564
395
|
}
|
|
565
396
|
else {
|
|
566
|
-
rows.push([shortId, start, duration, summary,
|
|
397
|
+
rows.push([shortId, start, duration, summary, loc]);
|
|
567
398
|
}
|
|
568
399
|
}
|
|
569
400
|
// Calculate column widths
|
|
570
401
|
const headers = parsed.verbose
|
|
571
|
-
? ['ID', 'When', 'Dur', 'Event', '
|
|
572
|
-
: ['ID', 'When', 'Dur', 'Event', '
|
|
402
|
+
? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
|
|
403
|
+
: ['ID', 'When', 'Dur', 'Event', 'Location'];
|
|
573
404
|
const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] || '').length)));
|
|
574
405
|
// Print header
|
|
575
|
-
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join('
|
|
406
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
576
407
|
console.log(headerLine);
|
|
577
|
-
console.log(colWidths.map(w => '-'.repeat(w)).join('
|
|
578
|
-
// Print rows
|
|
408
|
+
console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
|
|
409
|
+
// Print rows
|
|
579
410
|
for (const row of rows) {
|
|
580
|
-
const
|
|
581
|
-
const line = row.map((cell, i) => i < lastIdx ? (cell || '').padEnd(colWidths[i]) : (cell || '')).join(' ');
|
|
411
|
+
const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
|
|
582
412
|
console.log(line);
|
|
583
413
|
}
|
|
584
414
|
}
|
|
@@ -608,27 +438,10 @@ async function main() {
|
|
|
608
438
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
609
439
|
}
|
|
610
440
|
};
|
|
611
|
-
if (parsed.noReminders) {
|
|
612
|
-
event.reminders = { useDefault: false, overrides: [] };
|
|
613
|
-
}
|
|
614
|
-
else if (parsed.reminders.length > 0) {
|
|
615
|
-
event.reminders = { useDefault: false, overrides: parsed.reminders };
|
|
616
|
-
}
|
|
617
441
|
const token = await getAccessToken(user, true);
|
|
618
442
|
const created = await createEvent(token, event, parsed.calendar);
|
|
619
443
|
console.log(`\nEvent created: ${created.summary}`);
|
|
620
444
|
console.log(` When: ${formatDateTime(created.start)} - ${formatDateTime(created.end)}`);
|
|
621
|
-
if (parsed.noReminders) {
|
|
622
|
-
console.log(` Reminders: none`);
|
|
623
|
-
}
|
|
624
|
-
else if (parsed.reminders.length > 0) {
|
|
625
|
-
const rlist = parsed.reminders.map(r => {
|
|
626
|
-
const mins = r.minutes;
|
|
627
|
-
const label = mins >= 1440 ? `${mins / 1440}d` : mins >= 60 ? `${mins / 60}h` : `${mins}m`;
|
|
628
|
-
return `${label}${r.method === 'email' ? ':email' : ''}`;
|
|
629
|
-
}).join(', ');
|
|
630
|
-
console.log(` Reminders: ${rlist}`);
|
|
631
|
-
}
|
|
632
445
|
if (created.htmlLink) {
|
|
633
446
|
console.log(` Link: ${created.htmlLink}`);
|
|
634
447
|
}
|
|
@@ -652,12 +465,7 @@ async function main() {
|
|
|
652
465
|
if (matches.length > 1) {
|
|
653
466
|
console.error(`${idPrefix}: ambiguous (${matches.length} matches)`);
|
|
654
467
|
for (const e of matches) {
|
|
655
|
-
|
|
656
|
-
const displayId = (e.id || '').length > 12 ? e.id.slice(0, 16) : e.id?.slice(0, 8);
|
|
657
|
-
const cleaned = cleanUrls(e.summary || '');
|
|
658
|
-
const summary = cleaned.length > 60 ? cleaned.slice(0, 57) + '...' : cleaned;
|
|
659
|
-
const when = e.start ? formatDateTime(e.start) : '';
|
|
660
|
-
console.error(` ${displayId} ${when} ${summary}`);
|
|
468
|
+
console.error(` ${e.id?.slice(0, 8)} - ${e.summary}`);
|
|
661
469
|
}
|
|
662
470
|
continue;
|
|
663
471
|
}
|
|
@@ -667,7 +475,7 @@ async function main() {
|
|
|
667
475
|
continue;
|
|
668
476
|
}
|
|
669
477
|
await deleteEvent(token, event.id, parsed.calendar);
|
|
670
|
-
console.log(`Deleted: ${
|
|
478
|
+
console.log(`Deleted: ${event.summary}`);
|
|
671
479
|
}
|
|
672
480
|
break;
|
|
673
481
|
}
|
|
@@ -683,99 +491,6 @@ async function main() {
|
|
|
683
491
|
}
|
|
684
492
|
break;
|
|
685
493
|
}
|
|
686
|
-
case 'update': {
|
|
687
|
-
if (parsed.args.length === 0) {
|
|
688
|
-
console.error('Usage: gcal update <id> [-title "..."] [-loc "..."] [-start "..."] [-dur "..."] [-r ...]');
|
|
689
|
-
console.error('Use "gcal list -v" to see event IDs');
|
|
690
|
-
process.exit(1);
|
|
691
|
-
}
|
|
692
|
-
const idPrefix = parsed.args[0];
|
|
693
|
-
const token = await getAccessToken(user, true);
|
|
694
|
-
const events = await listEvents(token, parsed.calendar, 50);
|
|
695
|
-
const matches = events.filter(e => e.id?.startsWith(idPrefix));
|
|
696
|
-
if (matches.length === 0) {
|
|
697
|
-
console.error(`${idPrefix}: not found`);
|
|
698
|
-
process.exit(1);
|
|
699
|
-
}
|
|
700
|
-
if (matches.length > 1) {
|
|
701
|
-
console.error(`${idPrefix}: ambiguous (${matches.length} matches)`);
|
|
702
|
-
for (const e of matches) {
|
|
703
|
-
const displayId = (e.id || '').length > 12 ? e.id.slice(0, 16) : e.id?.slice(0, 8);
|
|
704
|
-
const cleaned = cleanUrls(e.summary || '');
|
|
705
|
-
const summary = cleaned.length > 60 ? cleaned.slice(0, 57) + '...' : cleaned;
|
|
706
|
-
const when = e.start ? formatDateTime(e.start) : '';
|
|
707
|
-
console.error(` ${displayId} ${when} ${summary}`);
|
|
708
|
-
}
|
|
709
|
-
process.exit(1);
|
|
710
|
-
}
|
|
711
|
-
const target = matches[0];
|
|
712
|
-
const body = {};
|
|
713
|
-
const changes = [];
|
|
714
|
-
if (parsed.title) {
|
|
715
|
-
body.summary = parsed.title;
|
|
716
|
-
changes.push(`title → "${parsed.title}"`);
|
|
717
|
-
}
|
|
718
|
-
if (parsed.location) {
|
|
719
|
-
body.location = parsed.location;
|
|
720
|
-
changes.push(`location → "${parsed.location}"`);
|
|
721
|
-
}
|
|
722
|
-
if (parsed.startTime) {
|
|
723
|
-
const newStart = parseDateTime(parsed.startTime);
|
|
724
|
-
const durationMs = parsed.duration
|
|
725
|
-
? parseDuration(parsed.duration) * 60 * 1000
|
|
726
|
-
: (target.end?.dateTime && target.start?.dateTime)
|
|
727
|
-
? new Date(target.end.dateTime).getTime() - new Date(target.start.dateTime).getTime()
|
|
728
|
-
: 60 * 60 * 1000;
|
|
729
|
-
const newEnd = new Date(newStart.getTime() + durationMs);
|
|
730
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
731
|
-
body.start = { dateTime: newStart.toISOString(), timeZone: tz };
|
|
732
|
-
body.end = { dateTime: newEnd.toISOString(), timeZone: tz };
|
|
733
|
-
changes.push(`start → ${formatDateTime(body.start)}`);
|
|
734
|
-
if (parsed.duration)
|
|
735
|
-
changes.push(`duration → ${parsed.duration}`);
|
|
736
|
-
}
|
|
737
|
-
else if (parsed.duration) {
|
|
738
|
-
// Duration change without start change — shift end time
|
|
739
|
-
if (target.start?.dateTime) {
|
|
740
|
-
const startMs = new Date(target.start.dateTime).getTime();
|
|
741
|
-
const newEnd = new Date(startMs + parseDuration(parsed.duration) * 60 * 1000);
|
|
742
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
743
|
-
body.end = { dateTime: newEnd.toISOString(), timeZone: tz };
|
|
744
|
-
changes.push(`duration → ${parsed.duration}`);
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
console.error('Cannot change duration of all-day event');
|
|
748
|
-
process.exit(1);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
if (parsed.noReminders) {
|
|
752
|
-
body.reminders = { useDefault: false, overrides: [] };
|
|
753
|
-
changes.push('reminders → none');
|
|
754
|
-
}
|
|
755
|
-
else if (parsed.reminders.length > 0) {
|
|
756
|
-
body.reminders = { useDefault: false, overrides: parsed.reminders };
|
|
757
|
-
const rlist = parsed.reminders.map(r => {
|
|
758
|
-
const mins = r.minutes;
|
|
759
|
-
const label = mins >= 1440 ? `${mins / 1440}d` : mins >= 60 ? `${mins / 60}h` : `${mins}m`;
|
|
760
|
-
return `${label}${r.method === 'email' ? ':email' : ''}`;
|
|
761
|
-
}).join(', ');
|
|
762
|
-
changes.push(`reminders → ${rlist}`);
|
|
763
|
-
}
|
|
764
|
-
if (changes.length === 0) {
|
|
765
|
-
console.error('No update flags provided. Use -title, -loc, -start, -dur, or -r');
|
|
766
|
-
process.exit(1);
|
|
767
|
-
}
|
|
768
|
-
const updated = await updateEvent(token, target.id, body, parsed.calendar);
|
|
769
|
-
console.log(`\nUpdated: ${cleanUrls(updated.summary || '')}`);
|
|
770
|
-
for (const c of changes) {
|
|
771
|
-
console.log(` ${c}`);
|
|
772
|
-
}
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
case 'assoc': {
|
|
776
|
-
setupFileAssociation();
|
|
777
|
-
break;
|
|
778
|
-
}
|
|
779
494
|
default:
|
|
780
495
|
console.error(`Unknown command: ${parsed.command}`);
|
|
781
496
|
showUsage();
|
|
@@ -783,11 +498,9 @@ async function main() {
|
|
|
783
498
|
}
|
|
784
499
|
}
|
|
785
500
|
if (import.meta.main) {
|
|
786
|
-
main().
|
|
787
|
-
process.exitCode = 0;
|
|
788
|
-
}).catch(e => {
|
|
501
|
+
main().catch(e => {
|
|
789
502
|
console.error(`Error: ${e.message}`);
|
|
790
|
-
process.
|
|
503
|
+
process.exit(1);
|
|
791
504
|
});
|
|
792
505
|
}
|
|
793
506
|
//# sourceMappingURL=gcal.js.map
|