@bobfrankston/gcal 0.1.10 → 0.1.12
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 +61 -67
- package/glib/gutils.ts +12 -17
- package/package.json +50 -46
package/gcal.ts
CHANGED
|
@@ -96,12 +96,11 @@ async function listEvents(
|
|
|
96
96
|
const params = new URLSearchParams({
|
|
97
97
|
maxResults: maxResults.toString(),
|
|
98
98
|
singleEvents: 'true',
|
|
99
|
-
orderBy: 'startTime'
|
|
100
|
-
timeMin: timeMin || new Date().toISOString()
|
|
99
|
+
orderBy: 'startTime'
|
|
101
100
|
});
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if (timeMin) params.set('timeMin', timeMin);
|
|
102
|
+
if (timeMax) params.set('timeMax', timeMax);
|
|
103
|
+
if (!timeMin && !timeMax) params.set('timeMin', new Date().toISOString());
|
|
105
104
|
|
|
106
105
|
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
|
|
107
106
|
const res = await apiFetch(url, accessToken);
|
|
@@ -232,6 +231,7 @@ Usage:
|
|
|
232
231
|
|
|
233
232
|
Commands:
|
|
234
233
|
list [n] List upcoming n events (default: 10)
|
|
234
|
+
past [n] List past n events (default: 10)
|
|
235
235
|
add <title> <when> [duration] Add event
|
|
236
236
|
del|delete <id> [id2...] Delete event(s) by ID (prefix match)
|
|
237
237
|
import <file.ics> Import events from ICS file
|
|
@@ -243,19 +243,14 @@ Options:
|
|
|
243
243
|
-defaultUser <email> Set default user for future use
|
|
244
244
|
-c, -calendar <id> Calendar ID (default: primary)
|
|
245
245
|
-n <count> Number of events to list
|
|
246
|
-
-
|
|
247
|
-
-before <when> List events before this date/time
|
|
248
|
-
-verbose Show event IDs and links
|
|
249
|
-
-v, --version Show version
|
|
246
|
+
-v, -verbose Show event IDs and links
|
|
250
247
|
|
|
251
248
|
Examples:
|
|
252
249
|
gcal meeting.ics Import ICS file
|
|
253
250
|
gcal list List next 10 events
|
|
254
|
-
gcal list -after tomorrow -before "jan 30"
|
|
255
251
|
gcal add "Dentist" "Friday 3pm" "1h"
|
|
256
252
|
gcal add "Lunch" "1/14/2026 12:00" "1h"
|
|
257
|
-
gcal add "Meeting" "tomorrow
|
|
258
|
-
gcal add "Call" "3pm" "30m"
|
|
253
|
+
gcal add "Meeting" "tomorrow 10:00"
|
|
259
254
|
gcal add "Appointment" "jan 15 2pm"
|
|
260
255
|
gcal -defaultUser bob@gmail.com Set default user
|
|
261
256
|
|
|
@@ -274,10 +269,7 @@ interface ParsedArgs {
|
|
|
274
269
|
count: number;
|
|
275
270
|
help: boolean;
|
|
276
271
|
verbose: boolean;
|
|
277
|
-
version: boolean;
|
|
278
272
|
icsFile: string; /** Direct .ics file path */
|
|
279
|
-
after: string; /** Filter: events after this date/time */
|
|
280
|
-
before: string; /** Filter: events before this date/time */
|
|
281
273
|
}
|
|
282
274
|
|
|
283
275
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
@@ -290,10 +282,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
290
282
|
count: 10,
|
|
291
283
|
help: false,
|
|
292
284
|
verbose: false,
|
|
293
|
-
|
|
294
|
-
icsFile: '',
|
|
295
|
-
after: '',
|
|
296
|
-
before: ''
|
|
285
|
+
icsFile: ''
|
|
297
286
|
};
|
|
298
287
|
|
|
299
288
|
const unknown: string[] = [];
|
|
@@ -318,23 +307,11 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
318
307
|
case '-n':
|
|
319
308
|
result.count = parseInt(argv[++i]) || 10;
|
|
320
309
|
break;
|
|
321
|
-
case '-
|
|
322
|
-
case '--after':
|
|
323
|
-
result.after = argv[++i] || '';
|
|
324
|
-
break;
|
|
325
|
-
case '-before':
|
|
326
|
-
case '--before':
|
|
327
|
-
result.before = argv[++i] || '';
|
|
328
|
-
break;
|
|
310
|
+
case '-v':
|
|
329
311
|
case '-verbose':
|
|
330
312
|
case '--verbose':
|
|
331
313
|
result.verbose = true;
|
|
332
314
|
break;
|
|
333
|
-
case '-v':
|
|
334
|
-
case '-V':
|
|
335
|
-
case '--version':
|
|
336
|
-
result.version = true;
|
|
337
|
-
break;
|
|
338
315
|
case '-h':
|
|
339
316
|
case '-help':
|
|
340
317
|
case '--help':
|
|
@@ -405,12 +382,6 @@ async function main(): Promise<void> {
|
|
|
405
382
|
}
|
|
406
383
|
}
|
|
407
384
|
|
|
408
|
-
if (parsed.version) {
|
|
409
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, 'package.json'), 'utf-8'));
|
|
410
|
-
console.log(`gcal ${pkg.version}`);
|
|
411
|
-
process.exit(0);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
385
|
if (parsed.help) {
|
|
415
386
|
showUsage();
|
|
416
387
|
process.exit(0);
|
|
@@ -461,9 +432,7 @@ async function main(): Promise<void> {
|
|
|
461
432
|
case 'list': {
|
|
462
433
|
const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
|
|
463
434
|
const token = await getAccessToken(user, false);
|
|
464
|
-
const
|
|
465
|
-
const timeMax = parsed.before ? parseDateTime(parsed.before).toISOString() : undefined;
|
|
466
|
-
const events = await listEvents(token, parsed.calendar, count, timeMin, timeMax);
|
|
435
|
+
const events = await listEvents(token, parsed.calendar, count);
|
|
467
436
|
|
|
468
437
|
if (events.length === 0) {
|
|
469
438
|
console.log('No upcoming events found.');
|
|
@@ -507,6 +476,57 @@ async function main(): Promise<void> {
|
|
|
507
476
|
break;
|
|
508
477
|
}
|
|
509
478
|
|
|
479
|
+
case 'past': {
|
|
480
|
+
const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
|
|
481
|
+
const token = await getAccessToken(user, false);
|
|
482
|
+
// Get events before now, then reverse to show most recent first
|
|
483
|
+
const now = new Date();
|
|
484
|
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
485
|
+
const events = await listEvents(token, parsed.calendar, count, thirtyDaysAgo.toISOString(), now.toISOString());
|
|
486
|
+
events.reverse(); // Most recent first
|
|
487
|
+
|
|
488
|
+
if (events.length === 0) {
|
|
489
|
+
console.log('No past events found in last 30 days.');
|
|
490
|
+
} else {
|
|
491
|
+
console.log(`\nPast events (${events.length}):\n`);
|
|
492
|
+
|
|
493
|
+
// Build table data (same format as list)
|
|
494
|
+
const rows: string[][] = [];
|
|
495
|
+
for (const event of events) {
|
|
496
|
+
const shortId = (event.id || '').slice(0, 8);
|
|
497
|
+
const start = event.start ? formatDateTime(event.start) : '?';
|
|
498
|
+
const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
|
|
499
|
+
const summary = event.summary || '(no title)';
|
|
500
|
+
const loc = event.location || '';
|
|
501
|
+
if (parsed.verbose) {
|
|
502
|
+
rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
|
|
503
|
+
} else {
|
|
504
|
+
rows.push([shortId, start, duration, summary, loc]);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Calculate column widths
|
|
509
|
+
const headers = parsed.verbose
|
|
510
|
+
? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
|
|
511
|
+
: ['ID', 'When', 'Dur', 'Event', 'Location'];
|
|
512
|
+
const colWidths = headers.map((h, i) =>
|
|
513
|
+
Math.max(h.length, ...rows.map(r => (r[i] || '').length))
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// Print header
|
|
517
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
518
|
+
console.log(headerLine);
|
|
519
|
+
console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
|
|
520
|
+
|
|
521
|
+
// Print rows
|
|
522
|
+
for (const row of rows) {
|
|
523
|
+
const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
|
|
524
|
+
console.log(line);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
510
530
|
case 'add': {
|
|
511
531
|
if (parsed.args.length < 2) {
|
|
512
532
|
console.error('Usage: gcal add <title> <when> [duration]');
|
|
@@ -519,32 +539,6 @@ async function main(): Promise<void> {
|
|
|
519
539
|
const durationMins = parseDuration(duration);
|
|
520
540
|
const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
|
|
521
541
|
|
|
522
|
-
// Check for suspicious dates (likely parsing errors)
|
|
523
|
-
const now = new Date();
|
|
524
|
-
const twoYearsFromNow = new Date(now);
|
|
525
|
-
twoYearsFromNow.setFullYear(twoYearsFromNow.getFullYear() + 2);
|
|
526
|
-
|
|
527
|
-
let warning = '';
|
|
528
|
-
if (startTime < now) {
|
|
529
|
-
warning = `Date is in the past: ${formatDateTime({ dateTime: startTime.toISOString() })}`;
|
|
530
|
-
} else if (startTime > twoYearsFromNow) {
|
|
531
|
-
warning = `Date is more than 2 years away: ${formatDateTime({ dateTime: startTime.toISOString() })}`;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (warning) {
|
|
535
|
-
console.log(`\nWarning: ${warning}`);
|
|
536
|
-
console.log(`Input was: "${when}"`);
|
|
537
|
-
const readline = await import('readline');
|
|
538
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
539
|
-
const response = await new Promise<string>(resolve => {
|
|
540
|
-
rl.question('Continue? (y/N) ', answer => { rl.close(); resolve(answer); });
|
|
541
|
-
});
|
|
542
|
-
if (response.toLowerCase() !== 'y') {
|
|
543
|
-
console.log('Cancelled.');
|
|
544
|
-
process.exit(0);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
542
|
const event: GoogleEvent = {
|
|
549
543
|
summary: title,
|
|
550
544
|
start: {
|
package/glib/gutils.ts
CHANGED
|
@@ -298,36 +298,31 @@ export function parseDateTime(input: string): Date {
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
// Handle time only (HH:mm) - assume today
|
|
301
|
+
// Handle time only (HH:mm) - assume today, or tomorrow if time already passed
|
|
302
302
|
const timeMatch = input.match(/^(\d{1,2}):(\d{2})$/);
|
|
303
303
|
if (timeMatch) {
|
|
304
304
|
const [, hour, min] = timeMatch;
|
|
305
305
|
const d = new Date(now);
|
|
306
306
|
d.setHours(parseInt(hour), parseInt(min), 0, 0);
|
|
307
|
+
if (d <= now) {
|
|
308
|
+
d.setDate(d.getDate() + 1); // Time passed, use tomorrow
|
|
309
|
+
}
|
|
307
310
|
return d;
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
// Handle time with am/pm
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
const [, hour, min, ampm] =
|
|
314
|
-
const d = new Date(now);
|
|
313
|
+
// Handle time with am/pm (2pm, 10am, 2:30pm) - assume today, or tomorrow if time already passed
|
|
314
|
+
const ampmMatch = lower.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/);
|
|
315
|
+
if (ampmMatch) {
|
|
316
|
+
const [, hour, min, ampm] = ampmMatch;
|
|
315
317
|
let h = parseInt(hour);
|
|
316
318
|
if (ampm === 'pm' && h < 12) h += 12;
|
|
317
319
|
if (ampm === 'am' && h === 12) h = 0;
|
|
320
|
+
const d = new Date(now);
|
|
318
321
|
d.setHours(h, parseInt(min || '0'), 0, 0);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// Handle bare hour: "10", "14" - interpret as today at that hour (not a month)
|
|
323
|
-
const bareHourMatch = input.match(/^(\d{1,2})$/);
|
|
324
|
-
if (bareHourMatch) {
|
|
325
|
-
const hour = parseInt(bareHourMatch[1]);
|
|
326
|
-
if (hour >= 0 && hour <= 23) {
|
|
327
|
-
const d = new Date(now);
|
|
328
|
-
d.setHours(hour, 0, 0, 0);
|
|
329
|
-
return d;
|
|
322
|
+
if (d <= now) {
|
|
323
|
+
d.setDate(d.getDate() + 1); // Time passed, use tomorrow
|
|
330
324
|
}
|
|
325
|
+
return d;
|
|
331
326
|
}
|
|
332
327
|
|
|
333
328
|
// Try native Date parsing
|
package/package.json
CHANGED
|
@@ -1,46 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bobfrankston/gcal",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Google Calendar CLI tool with ICS import support",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "gcal.ts",
|
|
7
|
-
"bin": {
|
|
8
|
-
"gcal": "gcal.ts"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"gcal.ts",
|
|
12
|
-
"glib/**/*.ts",
|
|
13
|
-
"tsconfig.json",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/BobFrankston/gcal.git"
|
|
19
|
-
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"check": "tsc --noEmit",
|
|
22
|
-
"prerelease:local": "git add -A && (git diff-index --quiet HEAD || git commit -m \"Pre-release commit\")",
|
|
23
|
-
"preversion": "npm run check && git add -A",
|
|
24
|
-
"postversion": "git push && git push --tags",
|
|
25
|
-
"release": "npm run prerelease:local && npm version patch && npm publish --access public",
|
|
26
|
-
"release:local": "npm run prerelease:local && npm version patch && npm publish --access public --ignore-scripts",
|
|
27
|
-
"installer": "npm run release && npm install -g ."
|
|
28
|
-
},
|
|
29
|
-
"keywords": [
|
|
30
|
-
"google",
|
|
31
|
-
"calendar",
|
|
32
|
-
"ics",
|
|
33
|
-
"ical",
|
|
34
|
-
"oauth",
|
|
35
|
-
"cli"
|
|
36
|
-
],
|
|
37
|
-
"author": "Bob Frankston",
|
|
38
|
-
"license": "MIT",
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@types/node": "^25.0.
|
|
41
|
-
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@bobfrankston/oauthsupport": "^1.0.
|
|
44
|
-
"ical.js": "^2.1.0"
|
|
45
|
-
}
|
|
46
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@bobfrankston/gcal",
|
|
3
|
+
"version": "0.1.12",
|
|
4
|
+
"description": "Google Calendar CLI tool with ICS import support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "gcal.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gcal": "gcal.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"gcal.ts",
|
|
12
|
+
"glib/**/*.ts",
|
|
13
|
+
"tsconfig.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/BobFrankston/gcal.git"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"check": "tsc --noEmit",
|
|
22
|
+
"prerelease:local": "git add -A && (git diff-index --quiet HEAD || git commit -m \"Pre-release commit\")",
|
|
23
|
+
"preversion": "npm run check && git add -A",
|
|
24
|
+
"postversion": "git push && git push --tags",
|
|
25
|
+
"release": "npm run prerelease:local && npm version patch && npm publish --access public",
|
|
26
|
+
"release:local": "npm run prerelease:local && npm version patch && npm publish --access public --ignore-scripts",
|
|
27
|
+
"installer": "npm run release && npm install -g ."
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"google",
|
|
31
|
+
"calendar",
|
|
32
|
+
"ics",
|
|
33
|
+
"ical",
|
|
34
|
+
"oauth",
|
|
35
|
+
"cli"
|
|
36
|
+
],
|
|
37
|
+
"author": "Bob Frankston",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.0.9"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@bobfrankston/oauthsupport": "^1.0.5",
|
|
44
|
+
"ical.js": "^2.1.0"
|
|
45
|
+
},
|
|
46
|
+
".dependencies": {
|
|
47
|
+
"@bobfrankston/oauthsupport": "file:../../../projects/OAuth/OauthSupport",
|
|
48
|
+
"ical.js": "^2.1.0"
|
|
49
|
+
}
|
|
50
|
+
}
|