@bobfrankston/gcal 0.1.5 → 0.1.6
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 +87 -6
- package/glib/gutils.ts +29 -3
- package/package.json +1 -1
package/gcal.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* gcal - Google Calendar CLI tool
|
|
4
4
|
* Manage Google Calendar events with ICS import support
|
|
@@ -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,6 +239,7 @@ 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
245
|
gcal meeting.ics Import ICS file
|
|
@@ -246,6 +261,7 @@ interface ParsedArgs {
|
|
|
246
261
|
calendar: string;
|
|
247
262
|
count: number;
|
|
248
263
|
help: boolean;
|
|
264
|
+
verbose: boolean;
|
|
249
265
|
icsFile: string; /** Direct .ics file path */
|
|
250
266
|
}
|
|
251
267
|
|
|
@@ -258,6 +274,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
258
274
|
calendar: 'primary',
|
|
259
275
|
count: 10,
|
|
260
276
|
help: false,
|
|
277
|
+
verbose: false,
|
|
261
278
|
icsFile: ''
|
|
262
279
|
};
|
|
263
280
|
|
|
@@ -283,6 +300,11 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
283
300
|
case '-n':
|
|
284
301
|
result.count = parseInt(argv[++i]) || 10;
|
|
285
302
|
break;
|
|
303
|
+
case '-v':
|
|
304
|
+
case '-verbose':
|
|
305
|
+
case '--verbose':
|
|
306
|
+
result.verbose = true;
|
|
307
|
+
break;
|
|
286
308
|
case '-h':
|
|
287
309
|
case '-help':
|
|
288
310
|
case '--help':
|
|
@@ -409,14 +431,40 @@ async function main(): Promise<void> {
|
|
|
409
431
|
console.log('No upcoming events found.');
|
|
410
432
|
} else {
|
|
411
433
|
console.log(`\nUpcoming events (${events.length}):\n`);
|
|
434
|
+
|
|
435
|
+
// Build table data
|
|
436
|
+
const rows: string[][] = [];
|
|
412
437
|
for (const event of events) {
|
|
438
|
+
const shortId = (event.id || '').slice(0, 8);
|
|
413
439
|
const start = event.start ? formatDateTime(event.start) : '?';
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
440
|
+
const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
|
|
441
|
+
const summary = event.summary || '(no title)';
|
|
442
|
+
const loc = event.location || '';
|
|
443
|
+
if (parsed.verbose) {
|
|
444
|
+
rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
|
|
445
|
+
} else {
|
|
446
|
+
rows.push([shortId, start, duration, summary, loc]);
|
|
418
447
|
}
|
|
419
448
|
}
|
|
449
|
+
|
|
450
|
+
// Calculate column widths
|
|
451
|
+
const headers = parsed.verbose
|
|
452
|
+
? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
|
|
453
|
+
: ['ID', 'When', 'Dur', 'Event', 'Location'];
|
|
454
|
+
const colWidths = headers.map((h, i) =>
|
|
455
|
+
Math.max(h.length, ...rows.map(r => (r[i] || '').length))
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Print header
|
|
459
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
460
|
+
console.log(headerLine);
|
|
461
|
+
console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
|
|
462
|
+
|
|
463
|
+
// Print rows
|
|
464
|
+
for (const row of rows) {
|
|
465
|
+
const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
|
|
466
|
+
console.log(line);
|
|
467
|
+
}
|
|
420
468
|
}
|
|
421
469
|
break;
|
|
422
470
|
}
|
|
@@ -455,6 +503,39 @@ async function main(): Promise<void> {
|
|
|
455
503
|
break;
|
|
456
504
|
}
|
|
457
505
|
|
|
506
|
+
case 'del':
|
|
507
|
+
case 'delete': {
|
|
508
|
+
if (parsed.args.length === 0) {
|
|
509
|
+
console.error('Usage: gcal delete <id> [id2] [id3] ...');
|
|
510
|
+
console.error('Use "gcal list" to see event IDs');
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const token = await getAccessToken(user, true);
|
|
515
|
+
const events = await listEvents(token, parsed.calendar, 50);
|
|
516
|
+
|
|
517
|
+
for (const idPrefix of parsed.args) {
|
|
518
|
+
const matches = events.filter(e => e.id?.startsWith(idPrefix));
|
|
519
|
+
|
|
520
|
+
if (matches.length === 0) {
|
|
521
|
+
console.error(`${idPrefix}: not found`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (matches.length > 1) {
|
|
525
|
+
console.error(`${idPrefix}: ambiguous (${matches.length} matches)`);
|
|
526
|
+
for (const e of matches) {
|
|
527
|
+
console.error(` ${e.id?.slice(0, 8)} - ${e.summary}`);
|
|
528
|
+
}
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const event = matches[0];
|
|
533
|
+
await deleteEvent(token, event.id!, parsed.calendar);
|
|
534
|
+
console.log(`Deleted: ${event.summary}`);
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
|
|
458
539
|
case 'calendars': {
|
|
459
540
|
const token = await getAccessToken(user, false);
|
|
460
541
|
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;
|