@bobfrankston/gcal 0.1.5 → 0.1.7
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 +91 -8
- package/glib/gutils.ts +57 -3
- package/package.json +1 -1
package/gcal.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { authenticateOAuth } from '@bobfrankston/oauthsupport';
|
|
|
14
14
|
import type { GoogleEvent, EventsListResponse, CalendarListEntry, CalendarListResponse } from './glib/types.ts';
|
|
15
15
|
import {
|
|
16
16
|
CREDENTIALS_FILE, loadConfig, saveConfig, getUserPaths,
|
|
17
|
-
ensureUserDir, formatDateTime, parseDuration, parseDateTime, ts, normalizeUser
|
|
17
|
+
ensureUserDir, formatDateTime, formatDuration, parseDuration, parseDateTime, ts, normalizeUser
|
|
18
18
|
} from './glib/gutils.ts';
|
|
19
19
|
|
|
20
20
|
const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
|
|
@@ -125,6 +125,19 @@ async function createEvent(
|
|
|
125
125
|
return await res.json() as GoogleEvent;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
async function deleteEvent(
|
|
129
|
+
accessToken: string,
|
|
130
|
+
eventId: string,
|
|
131
|
+
calendarId = 'primary'
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`;
|
|
134
|
+
const res = await apiFetch(url, accessToken, { method: 'DELETE' });
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const errText = await res.text();
|
|
137
|
+
throw new Error(`Failed to delete event: ${res.status} ${errText}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
128
141
|
async function importIcsFile(
|
|
129
142
|
filePath: string,
|
|
130
143
|
accessToken: string,
|
|
@@ -216,6 +229,7 @@ Usage:
|
|
|
216
229
|
Commands:
|
|
217
230
|
list [n] List upcoming n events (default: 10)
|
|
218
231
|
add <title> <when> [duration] Add event
|
|
232
|
+
del|delete <id> [id2...] Delete event(s) by ID (prefix match)
|
|
219
233
|
import <file.ics> Import events from ICS file
|
|
220
234
|
calendars List available calendars
|
|
221
235
|
help Show this help
|
|
@@ -225,12 +239,15 @@ Options:
|
|
|
225
239
|
-defaultUser <email> Set default user for future use
|
|
226
240
|
-c, -calendar <id> Calendar ID (default: primary)
|
|
227
241
|
-n <count> Number of events to list
|
|
242
|
+
-v, -verbose Show event IDs and links
|
|
228
243
|
|
|
229
244
|
Examples:
|
|
230
|
-
gcal meeting.ics
|
|
231
|
-
gcal list
|
|
245
|
+
gcal meeting.ics Import ICS file
|
|
246
|
+
gcal list List next 10 events
|
|
232
247
|
gcal add "Dentist" "Friday 3pm" "1h"
|
|
233
|
-
gcal
|
|
248
|
+
gcal add "Lunch" "1/14/2026 12:00" "1h"
|
|
249
|
+
gcal add "Meeting" "tomorrow 10:00"
|
|
250
|
+
gcal -defaultUser bob@gmail.com Set default user
|
|
234
251
|
|
|
235
252
|
File Association (Windows):
|
|
236
253
|
assoc .ics=icsfile
|
|
@@ -246,6 +263,7 @@ interface ParsedArgs {
|
|
|
246
263
|
calendar: string;
|
|
247
264
|
count: number;
|
|
248
265
|
help: boolean;
|
|
266
|
+
verbose: boolean;
|
|
249
267
|
icsFile: string; /** Direct .ics file path */
|
|
250
268
|
}
|
|
251
269
|
|
|
@@ -258,6 +276,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
258
276
|
calendar: 'primary',
|
|
259
277
|
count: 10,
|
|
260
278
|
help: false,
|
|
279
|
+
verbose: false,
|
|
261
280
|
icsFile: ''
|
|
262
281
|
};
|
|
263
282
|
|
|
@@ -283,6 +302,11 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
283
302
|
case '-n':
|
|
284
303
|
result.count = parseInt(argv[++i]) || 10;
|
|
285
304
|
break;
|
|
305
|
+
case '-v':
|
|
306
|
+
case '-verbose':
|
|
307
|
+
case '--verbose':
|
|
308
|
+
result.verbose = true;
|
|
309
|
+
break;
|
|
286
310
|
case '-h':
|
|
287
311
|
case '-help':
|
|
288
312
|
case '--help':
|
|
@@ -409,14 +433,40 @@ async function main(): Promise<void> {
|
|
|
409
433
|
console.log('No upcoming events found.');
|
|
410
434
|
} else {
|
|
411
435
|
console.log(`\nUpcoming events (${events.length}):\n`);
|
|
436
|
+
|
|
437
|
+
// Build table data
|
|
438
|
+
const rows: string[][] = [];
|
|
412
439
|
for (const event of events) {
|
|
440
|
+
const shortId = (event.id || '').slice(0, 8);
|
|
413
441
|
const start = event.start ? formatDateTime(event.start) : '?';
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
442
|
+
const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
|
|
443
|
+
const summary = event.summary || '(no title)';
|
|
444
|
+
const loc = event.location || '';
|
|
445
|
+
if (parsed.verbose) {
|
|
446
|
+
rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
|
|
447
|
+
} else {
|
|
448
|
+
rows.push([shortId, start, duration, summary, loc]);
|
|
418
449
|
}
|
|
419
450
|
}
|
|
451
|
+
|
|
452
|
+
// Calculate column widths
|
|
453
|
+
const headers = parsed.verbose
|
|
454
|
+
? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
|
|
455
|
+
: ['ID', 'When', 'Dur', 'Event', 'Location'];
|
|
456
|
+
const colWidths = headers.map((h, i) =>
|
|
457
|
+
Math.max(h.length, ...rows.map(r => (r[i] || '').length))
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Print header
|
|
461
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
462
|
+
console.log(headerLine);
|
|
463
|
+
console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
|
|
464
|
+
|
|
465
|
+
// Print rows
|
|
466
|
+
for (const row of rows) {
|
|
467
|
+
const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
|
|
468
|
+
console.log(line);
|
|
469
|
+
}
|
|
420
470
|
}
|
|
421
471
|
break;
|
|
422
472
|
}
|
|
@@ -455,6 +505,39 @@ async function main(): Promise<void> {
|
|
|
455
505
|
break;
|
|
456
506
|
}
|
|
457
507
|
|
|
508
|
+
case 'del':
|
|
509
|
+
case 'delete': {
|
|
510
|
+
if (parsed.args.length === 0) {
|
|
511
|
+
console.error('Usage: gcal delete <id> [id2] [id3] ...');
|
|
512
|
+
console.error('Use "gcal list" to see event IDs');
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const token = await getAccessToken(user, true);
|
|
517
|
+
const events = await listEvents(token, parsed.calendar, 50);
|
|
518
|
+
|
|
519
|
+
for (const idPrefix of parsed.args) {
|
|
520
|
+
const matches = events.filter(e => e.id?.startsWith(idPrefix));
|
|
521
|
+
|
|
522
|
+
if (matches.length === 0) {
|
|
523
|
+
console.error(`${idPrefix}: not found`);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (matches.length > 1) {
|
|
527
|
+
console.error(`${idPrefix}: ambiguous (${matches.length} matches)`);
|
|
528
|
+
for (const e of matches) {
|
|
529
|
+
console.error(` ${e.id?.slice(0, 8)} - ${e.summary}`);
|
|
530
|
+
}
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const event = matches[0];
|
|
535
|
+
await deleteEvent(token, event.id!, parsed.calendar);
|
|
536
|
+
console.log(`Deleted: ${event.summary}`);
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
|
|
458
541
|
case 'calendars': {
|
|
459
542
|
const token = await getAccessToken(user, false);
|
|
460
543
|
const calendars = await listCalendars(token);
|
package/glib/gutils.ts
CHANGED
|
@@ -163,18 +163,44 @@ export function resolveUser(cliUser: string, setAsDefault = false): string {
|
|
|
163
163
|
process.exit(1);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/** Format datetime for display */
|
|
166
|
+
/** Format datetime for display - yyyy-mm-dd HH:mm format */
|
|
167
167
|
export function formatDateTime(dt: { date?: string; dateTime?: string; timeZone?: string }): string {
|
|
168
168
|
if (dt.date) {
|
|
169
|
-
return dt.date; //
|
|
169
|
+
return dt.date; // Already yyyy-mm-dd
|
|
170
170
|
}
|
|
171
171
|
if (dt.dateTime) {
|
|
172
172
|
const d = new Date(dt.dateTime);
|
|
173
|
-
|
|
173
|
+
const yyyy = d.getFullYear();
|
|
174
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
175
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
176
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
177
|
+
const min = String(d.getMinutes()).padStart(2, '0');
|
|
178
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
|
|
174
179
|
}
|
|
175
180
|
return '(no time)';
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
/** Format duration in hours (e.g., "1.5 hrs", "2 hrs", "30 min") */
|
|
184
|
+
export function formatDuration(start: { date?: string; dateTime?: string }, end: { date?: string; dateTime?: string }): string {
|
|
185
|
+
if (start.date || end.date) {
|
|
186
|
+
return ''; // All-day event
|
|
187
|
+
}
|
|
188
|
+
if (!start.dateTime || !end.dateTime) {
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
const startMs = new Date(start.dateTime).getTime();
|
|
192
|
+
const endMs = new Date(end.dateTime).getTime();
|
|
193
|
+
const mins = (endMs - startMs) / 60000;
|
|
194
|
+
if (mins < 60) {
|
|
195
|
+
return `${mins} min`;
|
|
196
|
+
}
|
|
197
|
+
const hrs = mins / 60;
|
|
198
|
+
if (hrs === Math.floor(hrs)) {
|
|
199
|
+
return `${hrs} hr${hrs !== 1 ? 's' : ''}`;
|
|
200
|
+
}
|
|
201
|
+
return `${hrs.toFixed(1)} hrs`;
|
|
202
|
+
}
|
|
203
|
+
|
|
178
204
|
/** Parse duration string like "1h", "30m", "1h30m" to minutes */
|
|
179
205
|
export function parseDuration(duration: string): number {
|
|
180
206
|
let minutes = 0;
|
|
@@ -233,6 +259,34 @@ export function parseDateTime(input: string): Date {
|
|
|
233
259
|
return d;
|
|
234
260
|
}
|
|
235
261
|
|
|
262
|
+
// Handle explicit date/time formats: "MM/DD/YYYY HH:mm" or "YYYY-MM-DD HH:mm"
|
|
263
|
+
const dateTimeMatch = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2})$/);
|
|
264
|
+
if (dateTimeMatch) {
|
|
265
|
+
const [, month, day, year, hour, min] = dateTimeMatch;
|
|
266
|
+
const d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(min));
|
|
267
|
+
if (!isNaN(d.getTime())) {
|
|
268
|
+
return d;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const isoDateTimeMatch = input.match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})$/);
|
|
273
|
+
if (isoDateTimeMatch) {
|
|
274
|
+
const [, year, month, day, hour, min] = isoDateTimeMatch;
|
|
275
|
+
const d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(min));
|
|
276
|
+
if (!isNaN(d.getTime())) {
|
|
277
|
+
return d;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Handle time only (HH:mm) - assume today
|
|
282
|
+
const timeMatch = input.match(/^(\d{1,2}):(\d{2})$/);
|
|
283
|
+
if (timeMatch) {
|
|
284
|
+
const [, hour, min] = timeMatch;
|
|
285
|
+
const d = new Date(now);
|
|
286
|
+
d.setHours(parseInt(hour), parseInt(min), 0, 0);
|
|
287
|
+
return d;
|
|
288
|
+
}
|
|
289
|
+
|
|
236
290
|
// Try native Date parsing
|
|
237
291
|
const parsed = new Date(input);
|
|
238
292
|
if (!isNaN(parsed.getTime())) {
|