@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.
Files changed (3) hide show
  1. package/gcal.ts +87 -6
  2. package/glib/gutils.ts +29 -3
  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 loc = event.location ? ` @ ${event.location}` : '';
415
- console.log(` ${start} - ${event.summary || '(no title)'}${loc}`);
416
- if (event.htmlLink) {
417
- console.log(` ${event.htmlLink}`);
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; // All-day event
169
+ return dt.date; // Already yyyy-mm-dd
170
170
  }
171
171
  if (dt.dateTime) {
172
172
  const d = new Date(dt.dateTime);
173
- return d.toLocaleString();
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/gcal",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Google Calendar CLI tool with ICS import support",
5
5
  "type": "module",
6
6
  "main": "gcal.ts",