@bobfrankston/gcal 0.1.45 → 0.1.47

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/README.md CHANGED
@@ -1,92 +1,150 @@
1
- # @bobfrankston/gcal
2
-
3
- Google Calendar CLI tool with ICS import support.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install -g @bobfrankston/gcal
9
- ```
10
-
11
- ## Usage
12
-
13
- ```bash
14
- gcal <file.ics> Import ICS file (file association)
15
- gcal <command> [options] Run command
16
- ```
17
-
18
- ## Commands
19
-
20
- | Command | Description |
21
- |---------|-------------|
22
- | `list [n]` | List upcoming n events (default: 10) |
23
- | `list -since <date>` | List events from `<date>` forward (past dates allowed) |
24
- | `add <title> <when> [duration]` | Add event (explicit args) |
25
- | `add` | Add event (type description interactively) |
26
- | `add "free text description"` | Add event (AI parses single text arg) |
27
- | `add -clip` | Add event from clipboard text (AI-parsed) |
28
- | `del\|delete <id> [id2...]` | Delete event(s) by ID (prefix match) |
29
- | `remind <id> <dur> [dur2...]` | Add reminder(s) to existing event |
30
- | `resched <id> <when> [duration]` | Reschedule event (preserves duration by default) |
31
- | `snooze <id> [when]` | Snooze event; defaults to `+1d` |
32
- | `import <file.ics>` | Import events from ICS file |
33
- | `calendars` | List available calendars |
34
- | `assoc` | Set up .ics file association (Windows) |
35
- | `help` | Show help |
36
-
37
- ## Options
38
-
39
- | Flag | Description |
40
- |------|-------------|
41
- | `-u`, `-user <email>` | Set default Google account |
42
- | `-c`, `-calendar <id>` | Calendar ID (default: primary) |
43
- | `-n <count>` | Number of events to list |
44
- | `-v`, `-verbose` | Show event IDs and links |
45
- | `-b`, `-birthdays` | Include birthday events (hidden by default) |
46
- | `-clip` | Read from clipboard (for add command) |
47
- | `-r`, `-reminder <dur>` | Add popup reminder (e.g., 30m, 1h); repeatable |
48
- | `-since <date>` | Start listing from `<date>` (e.g. `"10 days ago"`, `"April 1"`, `yesterday`) |
49
- | `-all` | Delete all instances of recurring event |
50
-
51
- ## Examples
52
-
53
- ```bash
54
- gcal meeting.ics # Import ICS file
55
- gcal list # List next 10 events
56
- gcal list -since "10 days ago" # Include events from 10 days ago forward
57
- gcal list -since "april 1" -n 50 # 50 events since April 1
58
- gcal add "Dentist" "Friday 3pm" "1h"
59
- gcal add "Lunch" "1/14/2026 12:00" "1h"
60
- gcal add "Meeting" "tomorrow 10:00"
61
- gcal add "Appointment" "jan 15 2pm"
62
- gcal add "Dentist appointment Friday 3pm for 1 hour"
63
- gcal add -clip # Add from clipboard text
64
- gcal add "Dentist" "Friday 3pm" -r 30m # Add with 30-min reminder
65
- gcal remind abc12345 30m # Add 30-min reminder to event
66
- gcal resched abc12345 "next friday 3pm" # Reschedule to new date/time
67
- gcal resched abc12345 tomorrow # Move to tomorrow (preserves time-of-day)
68
- gcal snooze abc12345 # Snooze +1 day
69
- gcal snooze abc12345 +1w # Snooze +1 week
70
- gcal add # Type event description
71
- gcal -u bob@gmail.com # Set default user
72
- ```
73
-
74
- ### Reschedule / snooze
75
-
76
- `resched` and `snooze` find events up to 30 days in the past by default, so stale
77
- reminder events remain findable. Use `-since <date>` to widen that window. For
78
- timed events, if `<when>` lacks a time-of-day (e.g. `tomorrow`, `next friday`) the
79
- original start time is preserved. All-day events stay all-day. Relative offsets
80
- `+1d` / `+1w` / `+1h` / `+1m` advance from the event's current start.
81
-
82
- ## File Association (Windows)
83
-
84
- ```bash
85
- gcal assoc # Sets up .ics → gcal automatically
86
- ```
87
-
88
- On first run, gcal will offer to set this up. Run `gcal assoc` anytime to (re)configure it.
89
-
90
- ## License
91
-
92
- MIT
1
+ # @bobfrankston/gcal
2
+
3
+ Google Calendar **and** Google Tasks CLI tools. Two binaries (`gcal`, `gtask`) shipped together — they share OAuth credentials, scopes, and token files, so you authenticate once.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @bobfrankston/gcal
9
+ ```
10
+
11
+ Provides:
12
+ - `gcal` — Google Calendar
13
+ - `gtask` — Google Tasks
14
+
15
+ Both share OAuth client credentials with `gcards` (`%APPDATA%\gcards\credentials.json`).
16
+
17
+ ## gcal — Google Calendar
18
+
19
+ ```bash
20
+ gcal <file.ics> # Import ICS file (file association)
21
+ gcal <command> [options]
22
+ gcal help <command> # Detailed help for one command
23
+ ```
24
+
25
+ ### Commands
26
+
27
+ | Command | Description |
28
+ |---------|-------------|
29
+ | `list [n]` | List upcoming n events (default: 10) |
30
+ | `add <title> <when> [duration]` | Add event (explicit) |
31
+ | `add "<free text>"` | Add event (AI-parsed) |
32
+ | `add -clip` | Add event from clipboard (AI-parsed) |
33
+ | `add` | Add event interactively |
34
+ | `del \| delete <id> [id2...]` | Delete event(s) by ID prefix |
35
+ | `remind <id> <dur> [dur2...]` | Add reminder(s) |
36
+ | `resched <id> <when> [duration]` | Reschedule (preserves duration) |
37
+ | `snooze <id> [when]` | Snooze (default `+1d`) |
38
+ | `import <file.ics>` | Import events from ICS |
39
+ | `calendars` | List available calendars |
40
+ | `assoc` | Set up `.ics` file association (Windows) |
41
+ | `help [command]` | Show help |
42
+
43
+ ### Options
44
+
45
+ | Flag | Description |
46
+ |------|-------------|
47
+ | `-u`, `-user <email>` | Set / use default Google account |
48
+ | `-c`, `-calendar <id>` | Calendar ID (default: primary) |
49
+ | `-n <count>` | Number of events to list |
50
+ | `-v`, `-verbose` | Show event IDs and links |
51
+ | `-b`, `-birthdays` | Include birthday events |
52
+ | `-clip` | Read from clipboard (for `add`) |
53
+ | `-r`, `-reminder <dur>` | Add popup reminder (e.g. `30m`, `1h`); repeatable |
54
+ | `-since <date>` | Start listing from `<date>` |
55
+ | `-till <date>` | End listing at `<date>` |
56
+ | `-all` | Delete all instances of recurring event |
57
+
58
+ ### Examples
59
+
60
+ ```bash
61
+ gcal meeting.ics
62
+ gcal list
63
+ gcal list -since "10 days ago"
64
+ gcal list -since "april 1" -till "may 1"
65
+ gcal add "Dentist" "Friday 3pm" "1h"
66
+ gcal add "Lunch" "1/14/2026 12:00" "1h"
67
+ gcal add "Dentist appointment Friday 3pm for 1 hour"
68
+ gcal add -clip
69
+ gcal add "Dentist" "Friday 3pm" -r 30m
70
+ gcal remind abc12345 30m
71
+ gcal resched abc12345 "next friday 3pm"
72
+ gcal snooze abc12345 +1w
73
+ gcal -u bob@gmail.com
74
+ ```
75
+
76
+ ### Reschedule / snooze notes
77
+
78
+ `resched` and `snooze` find events up to 30 days in the past by default (so stale reminders remain findable). Widen with `-since <date>`. For timed events, if `<when>` lacks a time-of-day (e.g. `tomorrow`), the original time is preserved. All-day events stay all-day. Relative offsets `+1d` / `+1w` / `+1h` / `+1m` advance from the event's current start.
79
+
80
+ ### Windows file association
81
+
82
+ ```bash
83
+ gcal assoc # Sets up .ics → gcal
84
+ ```
85
+
86
+ On first run, gcal offers to set this up. Run `gcal assoc` anytime to (re)configure.
87
+
88
+ ## gtask Google Tasks
89
+
90
+ ```bash
91
+ gtask <command> [options]
92
+ gtask help <command>
93
+ ```
94
+
95
+ ### Commands
96
+
97
+ | Command | Description |
98
+ |---------|-------------|
99
+ | `add <title> [when]` | Add a task (optional due date — date-only) |
100
+ | `list` | List open tasks |
101
+ | `lists` | List all tasklists |
102
+ | `done <id>` | Mark task completed |
103
+ | `undone <id>` | Reopen a completed task |
104
+ | `del <id>` | Delete a task |
105
+ | `edit <id> [-t title] [-when date] [-n notes]` | Update fields |
106
+ | `clear` | Remove all completed tasks from list |
107
+ | `move <id> -l <list>` | Move task to another tasklist |
108
+ | `help [command]` | Show help |
109
+
110
+ ### Options
111
+
112
+ | Flag | Description |
113
+ |------|-------------|
114
+ | `-u`, `-user <email>` | Google account |
115
+ | `-l`, `-list <name\|id>` | Tasklist (default: primary) |
116
+ | `-n`, `-notes <text>` | Notes for `add` / `edit` |
117
+ | `-t`, `-title <text>` | New title for `edit` |
118
+ | `-when <date>` | New due date for `edit` |
119
+ | `-a`, `-all` | Include completed tasks in `list` |
120
+
121
+ ### Examples
122
+
123
+ ```bash
124
+ gtask add "Write report"
125
+ gtask add "Write report" friday
126
+ gtask add "Pay bills" "april 30" -n "rent + utilities"
127
+ gtask add "Call plumber" tomorrow -l Errands
128
+ gtask list
129
+ gtask list -l Errands
130
+ gtask list -a
131
+ gtask done abc12345
132
+ gtask edit abc12345 -when "next monday"
133
+ gtask move abc12345 -l Personal
134
+ gtask clear -l Errands
135
+ ```
136
+
137
+ ### Notes on Google Tasks
138
+
139
+ - **Due dates are date-only.** The Tasks API stores RFC3339 timestamps, but the Google UI ignores time-of-day. `gtask` writes midnight UTC.
140
+ - **No reminders.** Tasks have no notification mechanism in the API. Reminders only appear if you create the task through the Calendar UI's task-with-time feature.
141
+ - **No recurrence.** Recurring tasks aren't exposed via the API.
142
+ - **Hierarchy is flat.** One level of subtasks via `move?parent=`. Not currently surfaced by `gtask`.
143
+
144
+ ## Shared OAuth
145
+
146
+ Both tools request the combined scope set `calendar + tasks` (read or write depending on operation). The first time you run either tool after upgrading, Google prompts once for the new combined consent; afterward both share `token.json` / `token-write.json`.
147
+
148
+ ## License
149
+
150
+ MIT
package/gcal.js CHANGED
@@ -11,65 +11,12 @@ import fs from 'fs';
11
11
  import path from 'path';
12
12
  import { execSync } from 'child_process';
13
13
  import { createInterface } from 'readline/promises';
14
- import { authenticateOAuth } from '@bobfrankston/oauthsupport';
15
- import { CREDENTIALS_FILE, loadConfig, saveConfig, getUserPaths, ensureUserDir, formatDateTime, formatDuration, parseDuration, parseDateTime, hasTimeComponent, parseAllDay, formatYMD, ts, normalizeUser } from './glib/gutils.js';
14
+ import { loadConfig, saveConfig, formatDateTime, formatDuration, parseDuration, parseDateTime, hasTimeComponent, parseAllDay, formatYMD, normalizeUser } from './glib/gutils.js';
15
+ import { setupAbortHandler, getAccessToken, apiFetch } from './glib/goauth.js';
16
16
  import { extractEventsFromText, readClipboard } from './glib/aihelper.js';
17
17
  import pkg from './package.json' with { type: 'json' };
18
18
  const VERSION = pkg.version;
19
19
  const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
20
- const CALENDAR_SCOPE_READ = 'https://www.googleapis.com/auth/calendar.readonly';
21
- const CALENDAR_SCOPE_WRITE = 'https://www.googleapis.com/auth/calendar';
22
- let abortController = null;
23
- function setupAbortHandler() {
24
- abortController = new AbortController();
25
- let ctrlCCount = 0;
26
- process.on('SIGINT', () => {
27
- ctrlCCount++;
28
- abortController?.abort();
29
- if (ctrlCCount >= 2) {
30
- console.log('\n\nForce exit.');
31
- process.exit(1);
32
- }
33
- console.log('\n\nCtrl+C pressed - aborting... (press again to force exit)');
34
- });
35
- }
36
- async function getAccessToken(user, writeAccess = false, forceRefresh = false) {
37
- if (!fs.existsSync(CREDENTIALS_FILE)) {
38
- console.error(`\nCredentials file not found: ${CREDENTIALS_FILE}\n`);
39
- console.error(`gcal uses the same credentials as gcards.`);
40
- console.error(`Make sure gcards is set up with OAuth credentials first.`);
41
- console.error(`See: https://github.com/BobFrankston/oauthsupport/blob/master/SETUP-GOOGLE-OAUTH.md`);
42
- process.exit(1);
43
- }
44
- const paths = getUserPaths(user);
45
- ensureUserDir(user);
46
- const scope = writeAccess ? CALENDAR_SCOPE_WRITE : CALENDAR_SCOPE_READ;
47
- const tokenFileName = writeAccess ? 'token-write.json' : 'token.json';
48
- const tokenFilePath = path.join(paths.userDir, tokenFileName);
49
- if (forceRefresh && fs.existsSync(tokenFilePath)) {
50
- fs.unlinkSync(tokenFilePath);
51
- console.log(`${ts()} Token expired, refreshing...`);
52
- }
53
- const token = await authenticateOAuth(CREDENTIALS_FILE, {
54
- scope,
55
- tokenDirectory: paths.userDir,
56
- tokenFileName,
57
- credentialsKey: 'installed',
58
- signal: abortController?.signal
59
- });
60
- if (!token) {
61
- throw new Error('OAuth authentication failed');
62
- }
63
- return token.access_token;
64
- }
65
- async function apiFetch(url, accessToken, options = {}) {
66
- const headers = {
67
- 'Authorization': `Bearer ${accessToken}`,
68
- 'Content-Type': 'application/json',
69
- ...options.headers
70
- };
71
- return fetch(url, { ...options, headers });
72
- }
73
20
  async function listCalendars(accessToken) {
74
21
  const url = `${CALENDAR_API_BASE}/users/me/calendarList`;
75
22
  const res = await apiFetch(url, accessToken);
@@ -79,13 +26,15 @@ async function listCalendars(accessToken) {
79
26
  const data = await res.json();
80
27
  return data.items || [];
81
28
  }
82
- async function listEvents(accessToken, calendarId = 'primary', maxResults = 10, timeMin) {
29
+ async function listEvents(accessToken, calendarId = 'primary', maxResults = 10, timeMin, timeMax) {
83
30
  const params = new URLSearchParams({
84
31
  maxResults: maxResults.toString(),
85
32
  singleEvents: 'true',
86
33
  orderBy: 'startTime',
87
34
  timeMin: timeMin || new Date().toISOString()
88
35
  });
36
+ if (timeMax)
37
+ params.set('timeMax', timeMax);
89
38
  const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
90
39
  const res = await apiFetch(url, accessToken);
91
40
  if (!res.ok) {
@@ -195,64 +144,117 @@ async function importIcsFile(filePath, accessToken, calendarId = 'primary') {
195
144
  }
196
145
  return result;
197
146
  }
198
- function showUsage() {
199
- console.log(`
200
- gcal v${VERSION} - Google Calendar CLI
147
+ const USAGE_SUMMARY = `gcal v${VERSION} - Google Calendar CLI
201
148
 
202
- Usage:
203
- gcal <file.ics> Import ICS file (file association)
204
- gcal <command> [options] Run command
149
+ Usage: gcal <file.ics> Import ICS file (file association)
150
+ gcal <command> [options] Run command
151
+ gcal help <command> Detailed help for a command
205
152
 
206
153
  Commands:
207
- list [n] List upcoming n events (default: 10)
208
- list -since <date> List events from <date> forward (past ok)
209
- add <title> <when> [duration] Add event (explicit args)
210
- add Add event (type description interactively)
211
- add "free text description" Add event (AI parses single text arg)
212
- add -clip Add event from clipboard text (AI-parsed)
213
- del|delete <id> [id2...] Delete event(s) by ID (prefix match)
214
- remind <id> <dur> [dur2...] Add reminder(s) to existing event
215
- resched <id> <when> [duration] Reschedule event (preserve duration by default)
216
- snooze <id> [when] Snooze event (default: +1 day)
217
- import <file.ics> Import events from ICS file
218
- calendars List available calendars
219
- assoc Set up .ics file association (Windows)
220
- help Show this help
154
+ list List upcoming events
155
+ add Add event (explicit, AI, or interactive)
156
+ del | delete Delete event(s) by ID
157
+ remind Add reminder(s) to existing event
158
+ resched Reschedule event
159
+ snooze Snooze event (default: +1d)
160
+ import Import events from ICS file
161
+ calendars List available calendars
162
+ assoc Set up .ics file association (Windows)
163
+ help [command] Show help
164
+
165
+ Global options:
166
+ -u, -user <email> Set / use default Google account
167
+ -c, -calendar <id> Calendar ID (default: primary)
168
+
169
+ Companion tool: gtask (Google Tasks - shares OAuth with gcal)
170
+ `;
171
+ const USAGE = {
172
+ list: `gcal list [n] [-since <date>] [-till <date>] [-c <calendar>] [-b] [-v]
173
+ List upcoming events. Default n=10.
174
+ -since <date> Start from date (past dates allowed)
175
+ -till <date> End at date
176
+ -b Include birthday events (hidden by default)
177
+ -v Verbose (show full IDs and links)
178
+
179
+ Examples:
180
+ gcal list
181
+ gcal list 20
182
+ gcal list -since "10 days ago"
183
+ gcal list -since "april 1" -till "may 1"
184
+ gcal list -since "april 1" -n 50
185
+ `,
186
+ add: `gcal add <title> <when> [duration] Explicit
187
+ gcal add "<free text>" AI-parsed single arg
188
+ gcal add -clip AI-parsed from clipboard
189
+ gcal add Interactive (type description)
190
+ Add a calendar event. Default duration 1h. Use -r <dur> to add reminder(s).
191
+
192
+ Examples:
193
+ gcal add "Dentist" "Friday 3pm" "1h"
194
+ gcal add "Lunch" "1/14/2026 12:00" "1h"
195
+ gcal add "Meeting" "tomorrow 10:00"
196
+ gcal add "Appointment" "jan 15 2pm"
197
+ gcal add "Dentist appointment Friday 3pm for 1 hour"
198
+ gcal add -clip
199
+ gcal add "Dentist" "Friday 3pm" -r 30m
200
+ `,
201
+ del: `gcal del <id> [id2...] [-all] [-b]
202
+ gcal delete <id> [id2...]
203
+ Delete event(s) by ID prefix.
204
+ -all Delete entire recurring series (not just instance)
205
+ -b Allow deletion of birthday events
206
+ `,
207
+ delete: `gcal delete <id> [id2...] [-all]
208
+ Alias for "del".
209
+ `,
210
+ remind: `gcal remind <id> <duration> [duration2...]
211
+ Add popup reminder(s) to an existing event.
221
212
 
222
- Options:
223
- -u, -user <email> Set default Google account
224
- -c, -calendar <id> Calendar ID (default: primary)
225
- -n <count> Number of events to list
226
- -v, -verbose Show event IDs and links
227
- -b, -birthdays Include birthday events (hidden by default)
228
- -clip Read from clipboard (for add command)
229
- -r, -reminder <dur> Add popup reminder (e.g., 30m, 1h); repeatable
230
- -since <date> Start listing from <date> (e.g. "10 days ago", "April 1")
231
- -all Delete all instances of recurring event
213
+ Examples:
214
+ gcal remind abc12345 30m
215
+ gcal remind abc12345 30m 1h
216
+ `,
217
+ resched: `gcal resched <id> <when> [duration]
218
+ Reschedule an event. Preserves duration unless [duration] given.
219
+ If <when> lacks a time-of-day, the original time is preserved.
220
+ All-day events stay all-day. Searches up to 30 days back by default
221
+ (widen with -since).
232
222
 
233
- Examples:
234
- gcal meeting.ics Import ICS file
235
- gcal list List next 10 events
236
- gcal list -since "10 days ago" List events from 10 days ago forward
237
- gcal list -since "april 1" -n 50 List 50 events since April 1
238
- gcal add "Dentist" "Friday 3pm" "1h"
239
- gcal add "Lunch" "1/14/2026 12:00" "1h"
240
- gcal add "Meeting" "tomorrow 10:00"
241
- gcal add "Appointment" "jan 15 2pm"
242
- gcal add "Dentist appointment Friday 3pm for 1 hour"
243
- gcal add -clip Add from clipboard text
244
- gcal add "Dentist" "Friday 3pm" -r 30m Add with 30-min reminder
245
- gcal remind abc12345 30m Add 30-min reminder to event
246
- gcal resched abc12345 "next friday 3pm" Reschedule to new date/time
247
- gcal resched abc12345 tomorrow Move to tomorrow (preserve time-of-day)
248
- gcal snooze abc12345 Snooze event +1 day
249
- gcal snooze abc12345 +1w Snooze event +1 week
250
- gcal add Type event description (one or multiple)
251
- gcal -u bob@gmail.com Set default user
223
+ Examples:
224
+ gcal resched abc12345 "next friday 3pm"
225
+ gcal resched abc12345 tomorrow
226
+ gcal resched abc12345 +1w
227
+ `,
228
+ snooze: `gcal snooze <id> [when]
229
+ Like resched, but defaults to +1d if no <when> given.
252
230
 
253
- File Association (Windows):
254
- gcal assoc Set up automatically
255
- `);
231
+ Examples:
232
+ gcal snooze abc12345
233
+ gcal snooze abc12345 +1w
234
+ `,
235
+ import: `gcal import <file.ics>
236
+ gcal <file.ics> (via file association)
237
+ Import events from an iCalendar file.
238
+ `,
239
+ calendars: `gcal calendars
240
+ List available calendars (id, name, access role).
241
+ `,
242
+ assoc: `gcal assoc
243
+ Set up Windows .ics file association so double-clicking imports to gcal.
244
+ `,
245
+ help: `gcal help [command]
246
+ Show summary, or detailed help for a single command.
247
+ `
248
+ };
249
+ function showUsage(cmd) {
250
+ if (cmd && USAGE[cmd]) {
251
+ console.log(USAGE[cmd]);
252
+ return;
253
+ }
254
+ if (cmd) {
255
+ console.error(`Unknown command: ${cmd}\n`);
256
+ }
257
+ console.log(USAGE_SUMMARY);
256
258
  }
257
259
  function parseArgs(argv) {
258
260
  const result = {
@@ -267,7 +269,8 @@ function parseArgs(argv) {
267
269
  birthdays: false,
268
270
  clip: false,
269
271
  all: false,
270
- reminders: []
272
+ reminders: [],
273
+ helpCmd: ''
271
274
  };
272
275
  const unknown = [];
273
276
  let i = 0;
@@ -325,10 +328,21 @@ function parseArgs(argv) {
325
328
  }
326
329
  break;
327
330
  }
331
+ case '-till':
332
+ case '--till': {
333
+ const val = argv[++i] || '';
334
+ try {
335
+ result.till = parseDateTime(val);
336
+ }
337
+ catch {
338
+ console.error(`Invalid -till value: ${val}`);
339
+ process.exit(1);
340
+ }
341
+ break;
342
+ }
328
343
  case '-h':
329
344
  case '-help':
330
345
  case '--help':
331
- case 'help':
332
346
  result.help = true;
333
347
  break;
334
348
  case '-V':
@@ -360,6 +374,10 @@ function parseArgs(argv) {
360
374
  console.error(`Unknown options: ${unknown.join(', ')}`);
361
375
  process.exit(1);
362
376
  }
377
+ if (result.command === 'help') {
378
+ result.help = true;
379
+ result.helpCmd = result.args[0] || '';
380
+ }
363
381
  return result;
364
382
  }
365
383
  function buildReminders(minutes) {
@@ -426,8 +444,8 @@ async function main() {
426
444
  }
427
445
  }
428
446
  if (parsed.help) {
429
- showUsage();
430
- if (process.platform === 'win32' && !checkIcsAssoc()) {
447
+ showUsage(parsed.helpCmd);
448
+ if (!parsed.helpCmd && process.platform === 'win32' && !checkIcsAssoc()) {
431
449
  console.log('Note: .ics file association not set. Run "gcal assoc" to set it up.');
432
450
  }
433
451
  process.exit(0);
@@ -477,7 +495,6 @@ async function main() {
477
495
  console.error('No user configured. Use -u <email> to set default user.');
478
496
  process.exit(1);
479
497
  }
480
- console.log(`${ts()} User: ${user}`);
481
498
  switch (parsed.command) {
482
499
  case 'import': {
483
500
  const filePath = parsed.icsFile || parsed.args[0];
@@ -501,10 +518,19 @@ async function main() {
501
518
  break;
502
519
  }
503
520
  case 'list': {
504
- const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
521
+ let count = parsed.count;
522
+ if (parsed.args.length > 0) {
523
+ if (parsed.args.length > 1 || !/^\d+$/.test(parsed.args[0])) {
524
+ console.error(`Invalid list arguments: ${parsed.args.join(' ')}`);
525
+ console.error('Usage: gcal list [n] [-since <date>] [-till <date>]');
526
+ process.exit(1);
527
+ }
528
+ count = parseInt(parsed.args[0], 10);
529
+ }
505
530
  const token = await getAccessToken(user, false);
506
531
  const timeMin = parsed.since ? parsed.since.toISOString() : undefined;
507
- let events = await listEvents(token, parsed.calendar, count, timeMin);
532
+ const timeMax = parsed.till ? parsed.till.toISOString() : undefined;
533
+ let events = await listEvents(token, parsed.calendar, count, timeMin, timeMax);
508
534
  const birthdayCount = events.filter(e => e.eventType === 'birthday').length;
509
535
  if (!parsed.birthdays) {
510
536
  events = events.filter(e => e.eventType !== 'birthday');
@@ -537,14 +563,12 @@ async function main() {
537
563
  ? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
538
564
  : ['ID', 'When', 'Dur', 'Event', 'Location'];
539
565
  const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] || '').length)));
540
- // Print header
541
- const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
542
- console.log(headerLine);
566
+ const lastIdx = headers.length - 1;
567
+ const padCell = (s, i) => i === lastIdx ? s : s.padEnd(colWidths[i]);
568
+ console.log(headers.map(padCell).join(' '));
543
569
  console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
544
- // Print rows
545
570
  for (const row of rows) {
546
- const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
547
- console.log(line);
571
+ console.log(row.map((cell, i) => padCell(cell || '', i)).join(' '));
548
572
  }
549
573
  }
550
574
  if (birthdayCount > 0 && !parsed.birthdays) {