@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 +150 -92
- package/gcal.js +145 -121
- package/gcal.js.map +1 -1
- package/gcal.ts +155 -137
- package/glib/goauth.d.ts +13 -0
- package/glib/goauth.d.ts.map +1 -0
- package/glib/goauth.js +70 -0
- package/glib/goauth.js.map +1 -0
- package/glib/goauth.ts +93 -0
- package/glib/tasksapi.d.ts +23 -0
- package/glib/tasksapi.d.ts.map +1 -0
- package/glib/tasksapi.js +113 -0
- package/glib/tasksapi.js.map +1 -0
- package/glib/tasksapi.ts +155 -0
- package/glib/tasktypes.d.ts +47 -0
- package/glib/tasktypes.d.ts.map +1 -0
- package/glib/tasktypes.js +6 -0
- package/glib/tasktypes.js.map +1 -0
- package/glib/tasktypes.ts +51 -0
- package/gtask.d.ts +7 -0
- package/gtask.d.ts.map +1 -0
- package/gtask.js +429 -0
- package/gtask.js.map +1 -0
- package/gtask.ts +459 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,92 +1,150 @@
|
|
|
1
|
-
# @bobfrankston/gcal
|
|
2
|
-
|
|
3
|
-
Google Calendar CLI
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @bobfrankston/gcal
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
|
28
|
-
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
|
46
|
-
|
|
47
|
-
| `-
|
|
48
|
-
| `-
|
|
49
|
-
| `-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
gcal
|
|
62
|
-
gcal
|
|
63
|
-
gcal
|
|
64
|
-
gcal
|
|
65
|
-
gcal
|
|
66
|
-
gcal
|
|
67
|
-
gcal
|
|
68
|
-
gcal
|
|
69
|
-
gcal
|
|
70
|
-
gcal
|
|
71
|
-
gcal
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
timed events, if `<when>` lacks a time-of-day (e.g. `tomorrow
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 {
|
|
15
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
const
|
|
542
|
-
console.log(
|
|
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
|
-
|
|
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) {
|