floq 1.8.0 → 1.9.0
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.ja.md +24 -9
- package/README.md +24 -9
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +49 -41
- package/dist/calendar/oauth.js +5 -9
- package/dist/cli.js +32 -14
- package/dist/commands/calendar.d.ts +12 -12
- package/dist/commands/calendar.js +196 -87
- package/dist/commands/config.js +5 -13
- package/dist/commands/schedule.d.ts +6 -0
- package/dist/commands/schedule.js +109 -0
- package/dist/config.d.ts +19 -5
- package/dist/config.js +101 -23
- package/dist/ui/components/CalendarModal.js +8 -7
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -274,7 +274,7 @@ floq config turso --disable
|
|
|
274
274
|
|
|
275
275
|
## Googleカレンダー連携
|
|
276
276
|
|
|
277
|
-
FloqはGoogleカレンダーの予定をTUI
|
|
277
|
+
FloqはGoogleカレンダーの予定をTUIに表示できます。複数のカレンダーを登録でき(iCalとOAuthの混在も可能)、有効なカレンダーすべての予定がマージして表示されます。2つの方法があります:
|
|
278
278
|
|
|
279
279
|
### 方法1: iCal URL(シンプル、認証不要)
|
|
280
280
|
|
|
@@ -284,7 +284,9 @@ GoogleカレンダーのシークレットiCal URLを使用して、OAuth設定
|
|
|
284
284
|
# GoogleカレンダーからiCal URLを取得:
|
|
285
285
|
# 設定 > (カレンダー名) > カレンダーの統合 > "iCal形式の非公開URL"
|
|
286
286
|
|
|
287
|
-
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "
|
|
287
|
+
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "仕事"
|
|
288
|
+
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "プライベート"
|
|
289
|
+
floq calendar list
|
|
288
290
|
floq calendar show
|
|
289
291
|
```
|
|
290
292
|
|
|
@@ -326,10 +328,10 @@ floq calendar login
|
|
|
326
328
|
# → ブラウザが開いて認証画面へ
|
|
327
329
|
# → 表示されたコードを入力
|
|
328
330
|
|
|
329
|
-
#
|
|
331
|
+
# 登録するカレンダーを選択(複数選択可)
|
|
330
332
|
floq calendar select
|
|
331
333
|
# → カレンダー一覧が表示される
|
|
332
|
-
# →
|
|
334
|
+
# → 番号をカンマ区切りで入力(例: 1,3)
|
|
333
335
|
|
|
334
336
|
# 設定と今日の予定を表示
|
|
335
337
|
floq calendar show
|
|
@@ -345,20 +347,33 @@ floq calendar logout
|
|
|
345
347
|
|
|
346
348
|
```bash
|
|
347
349
|
# iCalモード
|
|
348
|
-
floq calendar add <url> [-n name] # iCal
|
|
349
|
-
floq calendar remove # カレンダーを削除
|
|
350
|
+
floq calendar add <url> [-n name] # iCalカレンダーを追加(複数登録可)
|
|
350
351
|
|
|
351
352
|
# OAuthモード
|
|
352
353
|
floq calendar config --client-id <id> --client-secret <secret>
|
|
353
354
|
floq calendar login # Google OAuthログイン
|
|
354
355
|
floq calendar logout # OAuthトークンをクリア
|
|
355
|
-
floq calendar select #
|
|
356
|
+
floq calendar select # Googleカレンダーを登録(対話式・複数可)
|
|
356
357
|
|
|
357
358
|
# 共通コマンド
|
|
359
|
+
floq calendar list # 登録済みカレンダー一覧
|
|
360
|
+
floq calendar remove <id|番号|名前> # カレンダーを削除
|
|
361
|
+
floq calendar remove --all # カレンダー設定をすべて削除
|
|
358
362
|
floq calendar show # 設定と今日の予定を表示
|
|
359
363
|
floq calendar sync # キャッシュを更新
|
|
360
|
-
floq calendar enable
|
|
361
|
-
floq calendar disable
|
|
364
|
+
floq calendar enable [id] # 表示を有効化(全体または個別)
|
|
365
|
+
floq calendar disable [id] # 表示を無効化(全体または個別)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### スケジュールコマンド
|
|
369
|
+
|
|
370
|
+
登録したすべてのカレンダーの予定をまとめて確認できます:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
floq schedule # 今日の予定
|
|
374
|
+
floq schedule tomorrow # 明日の予定
|
|
375
|
+
floq schedule week # 今後7日間
|
|
376
|
+
floq schedule --days 3 # 今後N日間
|
|
362
377
|
```
|
|
363
378
|
|
|
364
379
|
## テーマ
|
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ floq config turso --disable
|
|
|
274
274
|
|
|
275
275
|
## Google Calendar Integration
|
|
276
276
|
|
|
277
|
-
Floq can display your Google Calendar events in the TUI. Two methods are available:
|
|
277
|
+
Floq can display your Google Calendar events in the TUI. Multiple calendars can be registered (iCal and OAuth sources can be mixed), and events from all enabled calendars are merged. Two methods are available:
|
|
278
278
|
|
|
279
279
|
### Option 1: iCal URL (Simple, No Authentication)
|
|
280
280
|
|
|
@@ -284,7 +284,9 @@ Use Google Calendar's secret iCal URL for read-only access without OAuth setup.
|
|
|
284
284
|
# Get your iCal URL from Google Calendar:
|
|
285
285
|
# Settings > (Your Calendar) > Integrate calendar > "Secret address in iCal format"
|
|
286
286
|
|
|
287
|
-
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "
|
|
287
|
+
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "Work"
|
|
288
|
+
floq calendar add "https://calendar.google.com/calendar/ical/..." -n "Personal"
|
|
289
|
+
floq calendar list
|
|
288
290
|
floq calendar show
|
|
289
291
|
```
|
|
290
292
|
|
|
@@ -326,10 +328,10 @@ floq calendar login
|
|
|
326
328
|
# → Opens browser for authentication
|
|
327
329
|
# → Enter the displayed code when prompted
|
|
328
330
|
|
|
329
|
-
# Select
|
|
331
|
+
# Select calendars to register (multiple selections supported)
|
|
330
332
|
floq calendar select
|
|
331
333
|
# → Shows list of your calendars
|
|
332
|
-
# → Enter
|
|
334
|
+
# → Enter numbers, comma-separated (e.g. 1,3)
|
|
333
335
|
|
|
334
336
|
# View configuration and today's events
|
|
335
337
|
floq calendar show
|
|
@@ -345,20 +347,33 @@ floq calendar logout
|
|
|
345
347
|
|
|
346
348
|
```bash
|
|
347
349
|
# iCal mode
|
|
348
|
-
floq calendar add <url> [-n name] # Add iCal
|
|
349
|
-
floq calendar remove # Remove calendar
|
|
350
|
+
floq calendar add <url> [-n name] # Add an iCal calendar (repeatable)
|
|
350
351
|
|
|
351
352
|
# OAuth mode
|
|
352
353
|
floq calendar config --client-id <id> --client-secret <secret>
|
|
353
354
|
floq calendar login # Google OAuth login
|
|
354
355
|
floq calendar logout # Clear OAuth tokens
|
|
355
|
-
floq calendar select #
|
|
356
|
+
floq calendar select # Register Google calendars (interactive, multiple)
|
|
356
357
|
|
|
357
358
|
# Common commands
|
|
359
|
+
floq calendar list # List registered calendars
|
|
360
|
+
floq calendar remove <id|num|name> # Remove a calendar
|
|
361
|
+
floq calendar remove --all # Remove all calendar configuration
|
|
358
362
|
floq calendar show # Show config and today's events
|
|
359
363
|
floq calendar sync # Refresh cache
|
|
360
|
-
floq calendar enable
|
|
361
|
-
floq calendar disable
|
|
364
|
+
floq calendar enable [id] # Enable display (all, or one calendar)
|
|
365
|
+
floq calendar disable [id] # Disable display (all, or one calendar)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Schedule Command
|
|
369
|
+
|
|
370
|
+
View merged events from all registered calendars:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
floq schedule # Today's events
|
|
374
|
+
floq schedule tomorrow # Tomorrow's events
|
|
375
|
+
floq schedule week # Next 7 days
|
|
376
|
+
floq schedule --days 3 # Next N days
|
|
362
377
|
```
|
|
363
378
|
|
|
364
379
|
## Themes
|
package/dist/calendar/index.d.ts
CHANGED
|
@@ -5,9 +5,12 @@ export interface CalendarEvent {
|
|
|
5
5
|
end: Date;
|
|
6
6
|
allDay: boolean;
|
|
7
7
|
location?: string;
|
|
8
|
+
calendarName?: string;
|
|
9
|
+
sourceId?: string;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
|
-
* Fetch calendar events from
|
|
12
|
+
* Fetch and merge calendar events from all enabled calendar sources.
|
|
13
|
+
* When a URL is passed explicitly, fetch only that iCal URL (validation use; cache is not updated).
|
|
11
14
|
*/
|
|
12
15
|
export declare function fetchCalendarEvents(url?: string): Promise<CalendarEvent[]>;
|
|
13
16
|
/**
|
package/dist/calendar/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ICAL from 'ical.js';
|
|
2
|
-
import { getCalendarConfig, isCalendarEnabled,
|
|
2
|
+
import { getCalendarConfig, isCalendarEnabled, isCalendarSourceUsable } from '../config.js';
|
|
3
3
|
import { getValidAccessToken } from './oauth.js';
|
|
4
4
|
import { listEvents as listGoogleEvents } from './google-api.js';
|
|
5
5
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
@@ -90,29 +90,6 @@ function normalizeUrl(url) {
|
|
|
90
90
|
}
|
|
91
91
|
return url;
|
|
92
92
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Fetch calendar events via OAuth (Google Calendar API)
|
|
95
|
-
*/
|
|
96
|
-
async function fetchEventsViaOAuth() {
|
|
97
|
-
const oauthConfig = getCalendarOAuthConfig();
|
|
98
|
-
if (!oauthConfig) {
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
const accessToken = await getValidAccessToken();
|
|
102
|
-
if (!accessToken) {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
const now = new Date();
|
|
106
|
-
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
107
|
-
const endOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 2, 0); // Last day of next month
|
|
108
|
-
try {
|
|
109
|
-
return await listGoogleEvents(accessToken, oauthConfig.calendarId, startOfMonth, endOfNextMonth);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
console.error('Failed to fetch Google Calendar events:', error);
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
93
|
/**
|
|
117
94
|
* Fetch calendar events via iCal URL
|
|
118
95
|
*/
|
|
@@ -130,28 +107,59 @@ async function fetchEventsViaIcal(url) {
|
|
|
130
107
|
return parseICalData(icalData);
|
|
131
108
|
}
|
|
132
109
|
/**
|
|
133
|
-
* Fetch
|
|
110
|
+
* Fetch events for a single calendar source
|
|
111
|
+
*/
|
|
112
|
+
async function fetchEventsForSource(source, accessToken) {
|
|
113
|
+
let events;
|
|
114
|
+
if (source.type === 'oauth') {
|
|
115
|
+
if (!accessToken || !source.calendarId) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const now = new Date();
|
|
119
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
120
|
+
const endOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 2, 0); // Last day of next month
|
|
121
|
+
events = await listGoogleEvents(accessToken, source.calendarId, startOfMonth, endOfNextMonth);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
if (!source.url) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
events = await fetchEventsViaIcal(source.url);
|
|
128
|
+
}
|
|
129
|
+
return events.map(event => ({
|
|
130
|
+
...event,
|
|
131
|
+
calendarName: source.name,
|
|
132
|
+
sourceId: source.id,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Fetch and merge calendar events from all enabled calendar sources.
|
|
137
|
+
* When a URL is passed explicitly, fetch only that iCal URL (validation use; cache is not updated).
|
|
134
138
|
*/
|
|
135
139
|
export async function fetchCalendarEvents(url) {
|
|
140
|
+
if (url) {
|
|
141
|
+
return fetchEventsViaIcal(url);
|
|
142
|
+
}
|
|
136
143
|
try {
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
events = await fetchEventsViaIcal(url);
|
|
142
|
-
}
|
|
143
|
-
else if (calendarType === 'oauth') {
|
|
144
|
-
// OAuth mode
|
|
145
|
-
events = await fetchEventsViaOAuth();
|
|
144
|
+
const config = getCalendarConfig();
|
|
145
|
+
const sources = (config?.calendars || []).filter(s => s.enabled !== false && isCalendarSourceUsable(s, config));
|
|
146
|
+
if (sources.length === 0) {
|
|
147
|
+
return [];
|
|
146
148
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
// Resolve the access token once if any oauth source is present
|
|
150
|
+
const needsOAuth = sources.some(s => s.type === 'oauth');
|
|
151
|
+
const accessToken = needsOAuth ? await getValidAccessToken() : null;
|
|
152
|
+
const results = await Promise.allSettled(sources.map(source => fetchEventsForSource(source, accessToken)));
|
|
153
|
+
const events = [];
|
|
154
|
+
results.forEach((result, index) => {
|
|
155
|
+
if (result.status === 'fulfilled') {
|
|
156
|
+
events.push(...result.value);
|
|
152
157
|
}
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
else {
|
|
159
|
+
console.error(`Failed to fetch calendar "${sources[index].name}":`, result.reason);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
events.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
155
163
|
// Update cache
|
|
156
164
|
eventsCache = {
|
|
157
165
|
events,
|
package/dist/calendar/oauth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getGoogleOAuthClient,
|
|
1
|
+
import { getGoogleOAuthClient, getCalendarOAuthTokens, setCalendarOAuthTokens, } from '../config.js';
|
|
2
2
|
const GOOGLE_OAUTH_DEVICE_CODE_URL = 'https://oauth2.googleapis.com/device/code';
|
|
3
3
|
const GOOGLE_OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
4
4
|
const CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly';
|
|
@@ -125,19 +125,15 @@ export function isTokenExpired(tokens) {
|
|
|
125
125
|
* Get a valid access token, refreshing if necessary
|
|
126
126
|
*/
|
|
127
127
|
export async function getValidAccessToken() {
|
|
128
|
-
|
|
129
|
-
if (!
|
|
128
|
+
let tokens = getCalendarOAuthTokens();
|
|
129
|
+
if (!tokens) {
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
132
|
-
let tokens = oauthConfig.tokens;
|
|
133
132
|
if (isTokenExpired(tokens)) {
|
|
134
133
|
try {
|
|
135
134
|
tokens = await refreshAccessToken(tokens.refreshToken);
|
|
136
135
|
// Update stored tokens
|
|
137
|
-
|
|
138
|
-
...oauthConfig,
|
|
139
|
-
tokens,
|
|
140
|
-
});
|
|
136
|
+
setCalendarOAuthTokens(tokens);
|
|
141
137
|
}
|
|
142
138
|
catch (error) {
|
|
143
139
|
console.error('Failed to refresh access token:', error);
|
|
@@ -150,5 +146,5 @@ export async function getValidAccessToken() {
|
|
|
150
146
|
* Clear OAuth tokens (logout)
|
|
151
147
|
*/
|
|
152
148
|
export function clearOAuthTokens() {
|
|
153
|
-
|
|
149
|
+
setCalendarOAuthTokens(undefined);
|
|
154
150
|
}
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,8 @@ import { addComment, listComments } from './commands/comment.js';
|
|
|
12
12
|
import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
|
|
13
13
|
import { showInsights } from './commands/insights.js';
|
|
14
14
|
import { runSetupWizard } from './commands/setup.js';
|
|
15
|
-
import { addCalendar, removeCalendar, showCalendar, syncCalendar, enableCalendar, disableCalendar, configOAuthClient, loginCalendar, logoutCalendar, selectCalendar } from './commands/calendar.js';
|
|
15
|
+
import { addCalendar, removeCalendar, showCalendar, syncCalendar, enableCalendar, disableCalendar, configOAuthClient, loginCalendar, logoutCalendar, selectCalendar, listCalendarSourcesCommand } from './commands/calendar.js';
|
|
16
|
+
import { showSchedule } from './commands/schedule.js';
|
|
16
17
|
import { VERSION } from './version.js';
|
|
17
18
|
const program = new Command();
|
|
18
19
|
program
|
|
@@ -316,22 +317,39 @@ program
|
|
|
316
317
|
.action(async () => {
|
|
317
318
|
await runSetupWizard();
|
|
318
319
|
});
|
|
320
|
+
// Schedule command
|
|
321
|
+
program
|
|
322
|
+
.command('schedule [period]')
|
|
323
|
+
.description('Show schedule from registered calendars (today, tomorrow, week)')
|
|
324
|
+
.option('-d, --days <n>', 'Number of days to show')
|
|
325
|
+
.action(async (period, options) => {
|
|
326
|
+
await showSchedule(period, options);
|
|
327
|
+
});
|
|
319
328
|
// Calendar commands
|
|
320
329
|
const calendarCmd = program
|
|
321
330
|
.command('calendar')
|
|
322
331
|
.description('Google Calendar (iCal) integration');
|
|
323
332
|
calendarCmd
|
|
324
333
|
.command('add <url>')
|
|
325
|
-
.description('Add
|
|
334
|
+
.description('Add a calendar by iCal URL (multiple calendars supported)')
|
|
326
335
|
.option('-n, --name <name>', 'Display name for the calendar')
|
|
327
336
|
.action(async (url, options) => {
|
|
328
337
|
await addCalendar(url, options);
|
|
329
338
|
});
|
|
330
339
|
calendarCmd
|
|
331
|
-
.command('
|
|
332
|
-
.
|
|
340
|
+
.command('list')
|
|
341
|
+
.alias('ls')
|
|
342
|
+
.description('List registered calendars')
|
|
333
343
|
.action(async () => {
|
|
334
|
-
await
|
|
344
|
+
await listCalendarSourcesCommand();
|
|
345
|
+
});
|
|
346
|
+
calendarCmd
|
|
347
|
+
.command('remove [id]')
|
|
348
|
+
.alias('rm')
|
|
349
|
+
.description('Remove a calendar by id/number/name')
|
|
350
|
+
.option('--all', 'Remove all calendar configuration')
|
|
351
|
+
.action(async (id, options) => {
|
|
352
|
+
await removeCalendar(id, options);
|
|
335
353
|
});
|
|
336
354
|
calendarCmd
|
|
337
355
|
.command('show')
|
|
@@ -346,16 +364,16 @@ calendarCmd
|
|
|
346
364
|
await syncCalendar();
|
|
347
365
|
});
|
|
348
366
|
calendarCmd
|
|
349
|
-
.command('enable')
|
|
350
|
-
.description('Enable calendar display')
|
|
351
|
-
.action(async () => {
|
|
352
|
-
await enableCalendar();
|
|
367
|
+
.command('enable [id]')
|
|
368
|
+
.description('Enable calendar display (all, or a specific calendar by id)')
|
|
369
|
+
.action(async (id) => {
|
|
370
|
+
await enableCalendar(id);
|
|
353
371
|
});
|
|
354
372
|
calendarCmd
|
|
355
|
-
.command('disable')
|
|
356
|
-
.description('Disable calendar display')
|
|
357
|
-
.action(async () => {
|
|
358
|
-
await disableCalendar();
|
|
373
|
+
.command('disable [id]')
|
|
374
|
+
.description('Disable calendar display (all, or a specific calendar by id)')
|
|
375
|
+
.action(async (id) => {
|
|
376
|
+
await disableCalendar(id);
|
|
359
377
|
});
|
|
360
378
|
// OAuth commands
|
|
361
379
|
calendarCmd
|
|
@@ -390,7 +408,7 @@ calendarCmd
|
|
|
390
408
|
});
|
|
391
409
|
calendarCmd
|
|
392
410
|
.command('select')
|
|
393
|
-
.description('Select
|
|
411
|
+
.description('Select Google calendars to register (multiple supported)')
|
|
394
412
|
.action(async () => {
|
|
395
413
|
await selectCalendar();
|
|
396
414
|
});
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Add
|
|
2
|
+
* Add a new iCal calendar (multiple calendars can be registered)
|
|
3
3
|
*/
|
|
4
4
|
export declare function addCalendar(url: string, options: {
|
|
5
5
|
name?: string;
|
|
6
6
|
}): Promise<void>;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* List registered calendars
|
|
9
9
|
*/
|
|
10
|
-
export declare function
|
|
10
|
+
export declare function listCalendarSourcesCommand(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Remove a calendar (by id/index/name), or all calendar configuration with --all
|
|
13
|
+
*/
|
|
14
|
+
export declare function removeCalendar(idOrIndex?: string, options?: {
|
|
15
|
+
all?: boolean;
|
|
16
|
+
}): Promise<void>;
|
|
11
17
|
/**
|
|
12
18
|
* Show calendar configuration and today's events
|
|
13
19
|
*/
|
|
@@ -16,14 +22,8 @@ export declare function showCalendar(): Promise<void>;
|
|
|
16
22
|
* Sync/refresh calendar cache
|
|
17
23
|
*/
|
|
18
24
|
export declare function syncCalendar(): Promise<void>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
export declare function enableCalendar(): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Disable calendar display
|
|
25
|
-
*/
|
|
26
|
-
export declare function disableCalendar(): Promise<void>;
|
|
25
|
+
export declare function enableCalendar(idOrIndex?: string): Promise<void>;
|
|
26
|
+
export declare function disableCalendar(idOrIndex?: string): Promise<void>;
|
|
27
27
|
/**
|
|
28
28
|
* Configure OAuth client credentials
|
|
29
29
|
*/
|
|
@@ -37,6 +37,6 @@ export declare function loginCalendar(): Promise<void>;
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function logoutCalendar(): Promise<void>;
|
|
39
39
|
/**
|
|
40
|
-
* Select
|
|
40
|
+
* Select Google calendars to register (multiple selections supported)
|
|
41
41
|
*/
|
|
42
42
|
export declare function selectCalendar(): Promise<void>;
|
|
@@ -1,11 +1,38 @@
|
|
|
1
|
-
import { getCalendarConfig, setCalendarConfig, setCalendarEnabled, getGoogleOAuthClient, setGoogleOAuthClient,
|
|
1
|
+
import { getCalendarConfig, setCalendarConfig, getCalendarSources, addCalendarSource, removeCalendarSource, updateCalendarSource, setCalendarEnabled, getGoogleOAuthClient, setGoogleOAuthClient, getCalendarOAuthTokens, setCalendarOAuthTokens, } from '../config.js';
|
|
2
2
|
import { fetchCalendarEvents, getTodayEvents, clearCalendarCache, formatEventTime } from '../calendar/index.js';
|
|
3
3
|
import { getLocale } from '../config.js';
|
|
4
|
-
import { startOAuthFlow, pollForTokens, clearOAuthTokens } from '../calendar/oauth.js';
|
|
4
|
+
import { startOAuthFlow, pollForTokens, clearOAuthTokens, getValidAccessToken } from '../calendar/oauth.js';
|
|
5
5
|
import { listCalendars } from '../calendar/google-api.js';
|
|
6
6
|
import * as readline from 'readline';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Resolve a calendar source by id, 1-based index, or name
|
|
9
|
+
*/
|
|
10
|
+
function resolveSource(idOrIndex) {
|
|
11
|
+
const sources = getCalendarSources();
|
|
12
|
+
// Exact id match
|
|
13
|
+
const byId = sources.find(s => s.id === idOrIndex);
|
|
14
|
+
if (byId)
|
|
15
|
+
return byId;
|
|
16
|
+
// 1-based index
|
|
17
|
+
const index = parseInt(idOrIndex, 10);
|
|
18
|
+
if (!isNaN(index) && index >= 1 && index <= sources.length) {
|
|
19
|
+
return sources[index - 1];
|
|
20
|
+
}
|
|
21
|
+
// Name match
|
|
22
|
+
return sources.find(s => s.name === idOrIndex);
|
|
23
|
+
}
|
|
24
|
+
function printSourceList(sources) {
|
|
25
|
+
sources.forEach((source, index) => {
|
|
26
|
+
const status = source.enabled !== false ? 'enabled' : 'disabled';
|
|
27
|
+
const detail = source.type === 'ical'
|
|
28
|
+
? (source.url && source.url.length > 50 ? source.url.substring(0, 50) + '...' : source.url)
|
|
29
|
+
: source.calendarId;
|
|
30
|
+
console.log(` ${index + 1}. [${source.id}] ${source.name} (${source.type}, ${status})`);
|
|
31
|
+
console.log(` ${detail}`);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Add a new iCal calendar (multiple calendars can be registered)
|
|
9
36
|
*/
|
|
10
37
|
export async function addCalendar(url, options) {
|
|
11
38
|
// Validate URL
|
|
@@ -13,50 +40,112 @@ export async function addCalendar(url, options) {
|
|
|
13
40
|
console.error('Error: Invalid URL. Must start with https://, webcal://, or http://');
|
|
14
41
|
process.exit(1);
|
|
15
42
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
43
|
+
const existing = getCalendarSources().find(s => s.url === url);
|
|
44
|
+
if (existing) {
|
|
45
|
+
console.error(`Error: This URL is already registered as "${existing.name}" [${existing.id}].`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
// Default name from URL hostname when not provided
|
|
49
|
+
let name = options.name;
|
|
50
|
+
if (!name) {
|
|
51
|
+
try {
|
|
52
|
+
name = new URL(url.replace('webcal://', 'https://')).hostname;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
name = `Calendar ${getCalendarSources().length + 1}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const source = addCalendarSource({
|
|
59
|
+
name,
|
|
19
60
|
type: 'ical',
|
|
61
|
+
url,
|
|
20
62
|
enabled: true,
|
|
21
63
|
});
|
|
22
64
|
console.log('Calendar added successfully!');
|
|
65
|
+
console.log(` ID: ${source.id}`);
|
|
66
|
+
console.log(` Name: ${source.name}`);
|
|
23
67
|
console.log(` URL: ${url}`);
|
|
24
|
-
if (options.name) {
|
|
25
|
-
console.log(` Name: ${options.name}`);
|
|
26
|
-
}
|
|
27
68
|
// Try to fetch events to validate the URL
|
|
28
69
|
console.log('\nFetching events...');
|
|
29
70
|
try {
|
|
30
71
|
const events = await fetchCalendarEvents(url);
|
|
31
72
|
console.log(`Found ${events.length} events.`);
|
|
32
73
|
}
|
|
33
|
-
catch
|
|
74
|
+
catch {
|
|
34
75
|
console.log('Warning: Could not fetch events. Please check the URL.');
|
|
35
76
|
}
|
|
77
|
+
if (getCalendarConfig()?.enabled === false) {
|
|
78
|
+
console.log('Note: calendar display is currently disabled. Run "floq calendar enable" to show events.');
|
|
79
|
+
}
|
|
36
80
|
}
|
|
37
81
|
/**
|
|
38
|
-
*
|
|
82
|
+
* List registered calendars
|
|
39
83
|
*/
|
|
40
|
-
export async function
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
console.log('No
|
|
84
|
+
export async function listCalendarSourcesCommand() {
|
|
85
|
+
const sources = getCalendarSources();
|
|
86
|
+
if (sources.length === 0) {
|
|
87
|
+
console.log('No calendars registered.');
|
|
88
|
+
console.log('Use "floq calendar add <url>" or "floq calendar select" to add one.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(`Registered calendars (${sources.length})`);
|
|
92
|
+
console.log('-'.repeat(40));
|
|
93
|
+
printSourceList(sources);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Remove a calendar (by id/index/name), or all calendar configuration with --all
|
|
97
|
+
*/
|
|
98
|
+
export async function removeCalendar(idOrIndex, options = {}) {
|
|
99
|
+
const sources = getCalendarSources();
|
|
100
|
+
if (options.all) {
|
|
101
|
+
if (sources.length === 0 && !getCalendarConfig()) {
|
|
102
|
+
console.log('No calendar configured.');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
setCalendarConfig(undefined);
|
|
106
|
+
clearCalendarCache();
|
|
107
|
+
console.log('All calendar configuration removed.');
|
|
44
108
|
return;
|
|
45
109
|
}
|
|
46
|
-
|
|
110
|
+
if (sources.length === 0) {
|
|
111
|
+
console.log('No calendars registered.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let target;
|
|
115
|
+
if (idOrIndex) {
|
|
116
|
+
target = resolveSource(idOrIndex);
|
|
117
|
+
if (!target) {
|
|
118
|
+
console.error(`Error: Calendar "${idOrIndex}" not found.`);
|
|
119
|
+
console.log('');
|
|
120
|
+
printSourceList(sources);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (sources.length === 1) {
|
|
125
|
+
target = sources[0];
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log('Multiple calendars are registered. Specify which one to remove:');
|
|
129
|
+
console.log('');
|
|
130
|
+
printSourceList(sources);
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log('Usage: floq calendar remove <id|number|name>');
|
|
133
|
+
console.log(' floq calendar remove --all (remove everything)');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
removeCalendarSource(target.id);
|
|
47
137
|
clearCalendarCache();
|
|
48
|
-
console.log(
|
|
138
|
+
console.log(`Calendar "${target.name}" [${target.id}] removed.`);
|
|
49
139
|
}
|
|
50
140
|
/**
|
|
51
141
|
* Show calendar configuration and today's events
|
|
52
142
|
*/
|
|
53
143
|
export async function showCalendar() {
|
|
54
|
-
const
|
|
144
|
+
const sources = getCalendarSources();
|
|
55
145
|
const locale = getLocale();
|
|
56
|
-
const calendarType = getCalendarType();
|
|
57
146
|
console.log('Calendar Configuration');
|
|
58
147
|
console.log('-'.repeat(40));
|
|
59
|
-
if (
|
|
148
|
+
if (sources.length === 0) {
|
|
60
149
|
console.log('No calendar configured.');
|
|
61
150
|
console.log('');
|
|
62
151
|
console.log('Option 1: iCal URL (no authentication required)');
|
|
@@ -68,18 +157,9 @@ export async function showCalendar() {
|
|
|
68
157
|
console.log(' floq calendar select');
|
|
69
158
|
return;
|
|
70
159
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
console.log(`Calendar ID: ${config.oauth.calendarId}`);
|
|
75
|
-
}
|
|
76
|
-
else if (config.url) {
|
|
77
|
-
console.log(`URL: ${config.url}`);
|
|
78
|
-
if (config.name) {
|
|
79
|
-
console.log(`Name: ${config.name}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
console.log(`Status: ${config.enabled !== false ? 'enabled' : 'disabled'}`);
|
|
160
|
+
printSourceList(sources);
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(`Display: ${getCalendarConfig()?.enabled !== false ? 'enabled' : 'disabled'}`);
|
|
83
163
|
console.log('');
|
|
84
164
|
console.log("Today's Events");
|
|
85
165
|
console.log('-'.repeat(40));
|
|
@@ -90,11 +170,13 @@ export async function showCalendar() {
|
|
|
90
170
|
console.log(locale === 'ja' ? '今日の予定はありません' : 'No events today');
|
|
91
171
|
return;
|
|
92
172
|
}
|
|
173
|
+
const showCalendarName = sources.length > 1;
|
|
93
174
|
for (const event of todayEvents) {
|
|
94
175
|
const timeStr = event.allDay
|
|
95
176
|
? (locale === 'ja' ? '終日' : 'All day')
|
|
96
177
|
: `${formatEventTime(event.start)} - ${formatEventTime(event.end)}`;
|
|
97
|
-
|
|
178
|
+
const calLabel = showCalendarName && event.calendarName ? ` [${event.calendarName}]` : '';
|
|
179
|
+
console.log(` ${timeStr} ${event.title}${calLabel}`);
|
|
98
180
|
if (event.location) {
|
|
99
181
|
console.log(` 📍 ${event.location}`);
|
|
100
182
|
}
|
|
@@ -104,16 +186,16 @@ export async function showCalendar() {
|
|
|
104
186
|
* Sync/refresh calendar cache
|
|
105
187
|
*/
|
|
106
188
|
export async function syncCalendar() {
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
189
|
+
const sources = getCalendarSources();
|
|
190
|
+
if (sources.length === 0) {
|
|
109
191
|
console.log('No calendar configured.');
|
|
110
192
|
return;
|
|
111
193
|
}
|
|
112
|
-
console.log('Syncing
|
|
194
|
+
console.log('Syncing calendars...');
|
|
113
195
|
clearCalendarCache();
|
|
114
196
|
try {
|
|
115
197
|
const events = await fetchCalendarEvents();
|
|
116
|
-
console.log(`Synced ${events.length} events.`);
|
|
198
|
+
console.log(`Synced ${events.length} events from ${sources.filter(s => s.enabled !== false).length} calendar(s).`);
|
|
117
199
|
const todayEvents = getTodayEvents();
|
|
118
200
|
console.log(`${todayEvents.length} events today.`);
|
|
119
201
|
}
|
|
@@ -123,29 +205,43 @@ export async function syncCalendar() {
|
|
|
123
205
|
}
|
|
124
206
|
}
|
|
125
207
|
/**
|
|
126
|
-
*
|
|
208
|
+
* Toggle calendar display (overall, or a specific calendar by id)
|
|
127
209
|
*/
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
210
|
+
async function setCalendarDisplay(enabled, idOrIndex) {
|
|
211
|
+
const sources = getCalendarSources();
|
|
212
|
+
const label = enabled ? 'enabled' : 'disabled';
|
|
213
|
+
if (sources.length === 0) {
|
|
131
214
|
console.log('No calendar configured.');
|
|
132
|
-
|
|
215
|
+
if (enabled) {
|
|
216
|
+
console.log('Use "floq calendar add <url>" to add a calendar first.');
|
|
217
|
+
}
|
|
133
218
|
return;
|
|
134
219
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
220
|
+
if (idOrIndex) {
|
|
221
|
+
const target = resolveSource(idOrIndex);
|
|
222
|
+
if (!target) {
|
|
223
|
+
console.error(`Error: Calendar "${idOrIndex}" not found.`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
updateCalendarSource(target.id, { enabled });
|
|
227
|
+
// Enabling a specific calendar implies the user wants events shown —
|
|
228
|
+
// also lift a global disable so the change is actually visible
|
|
229
|
+
if (enabled && getCalendarConfig()?.enabled === false) {
|
|
230
|
+
setCalendarEnabled(true);
|
|
231
|
+
console.log('Calendar display was disabled — re-enabled.');
|
|
232
|
+
}
|
|
233
|
+
clearCalendarCache();
|
|
234
|
+
console.log(`Calendar "${target.name}" ${label}.`);
|
|
145
235
|
return;
|
|
146
236
|
}
|
|
147
|
-
setCalendarEnabled(
|
|
148
|
-
console.log(
|
|
237
|
+
setCalendarEnabled(enabled);
|
|
238
|
+
console.log(`Calendar display ${label}.`);
|
|
239
|
+
}
|
|
240
|
+
export async function enableCalendar(idOrIndex) {
|
|
241
|
+
await setCalendarDisplay(true, idOrIndex);
|
|
242
|
+
}
|
|
243
|
+
export async function disableCalendar(idOrIndex) {
|
|
244
|
+
await setCalendarDisplay(false, idOrIndex);
|
|
149
245
|
}
|
|
150
246
|
/**
|
|
151
247
|
* Configure OAuth client credentials
|
|
@@ -156,7 +252,7 @@ export async function configOAuthClient(clientId, clientSecret) {
|
|
|
156
252
|
console.log('');
|
|
157
253
|
console.log('Next steps:');
|
|
158
254
|
console.log(' 1. Run "floq calendar login" to authenticate');
|
|
159
|
-
console.log(' 2. Run "floq calendar select" to choose
|
|
255
|
+
console.log(' 2. Run "floq calendar select" to choose calendars');
|
|
160
256
|
}
|
|
161
257
|
/**
|
|
162
258
|
* Start OAuth login flow
|
|
@@ -206,21 +302,11 @@ export async function loginCalendar() {
|
|
|
206
302
|
console.log('');
|
|
207
303
|
console.log('Waiting for authorization...');
|
|
208
304
|
const tokens = await pollForTokens(deviceCode, interval);
|
|
209
|
-
|
|
210
|
-
const config = getCalendarConfig() || {};
|
|
211
|
-
setCalendarConfig({
|
|
212
|
-
...config,
|
|
213
|
-
type: 'oauth',
|
|
214
|
-
oauth: {
|
|
215
|
-
tokens,
|
|
216
|
-
calendarId: '',
|
|
217
|
-
calendarName: '',
|
|
218
|
-
},
|
|
219
|
-
});
|
|
305
|
+
setCalendarOAuthTokens(tokens);
|
|
220
306
|
console.log('');
|
|
221
307
|
console.log('Login successful!');
|
|
222
308
|
console.log('');
|
|
223
|
-
console.log('Now run "floq calendar select" to choose
|
|
309
|
+
console.log('Now run "floq calendar select" to choose calendars.');
|
|
224
310
|
}
|
|
225
311
|
catch (error) {
|
|
226
312
|
console.error('Login failed:', error instanceof Error ? error.message : error);
|
|
@@ -231,24 +317,27 @@ export async function loginCalendar() {
|
|
|
231
317
|
* Logout from OAuth (clear tokens)
|
|
232
318
|
*/
|
|
233
319
|
export async function logoutCalendar() {
|
|
234
|
-
const
|
|
235
|
-
if (!
|
|
320
|
+
const tokens = getCalendarOAuthTokens();
|
|
321
|
+
if (!tokens) {
|
|
236
322
|
console.log('Not logged in.');
|
|
237
323
|
return;
|
|
238
324
|
}
|
|
239
325
|
clearOAuthTokens();
|
|
240
326
|
console.log('Logged out successfully.');
|
|
327
|
+
const oauthSources = getCalendarSources().filter(s => s.type === 'oauth');
|
|
328
|
+
if (oauthSources.length > 0) {
|
|
329
|
+
console.log(`Note: ${oauthSources.length} Google calendar(s) remain registered but need login to fetch events.`);
|
|
330
|
+
}
|
|
241
331
|
}
|
|
242
332
|
/**
|
|
243
|
-
* Select
|
|
333
|
+
* Select Google calendars to register (multiple selections supported)
|
|
244
334
|
*/
|
|
245
335
|
export async function selectCalendar() {
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
336
|
+
const tokens = getCalendarOAuthTokens();
|
|
337
|
+
if (!tokens) {
|
|
248
338
|
console.log('Not logged in. Run "floq calendar login" first.');
|
|
249
339
|
return;
|
|
250
340
|
}
|
|
251
|
-
const { getValidAccessToken } = await import('../calendar/oauth.js');
|
|
252
341
|
const accessToken = await getValidAccessToken();
|
|
253
342
|
if (!accessToken) {
|
|
254
343
|
console.log('Failed to get access token. Try logging in again.');
|
|
@@ -262,38 +351,58 @@ export async function selectCalendar() {
|
|
|
262
351
|
console.log('No calendars found.');
|
|
263
352
|
return;
|
|
264
353
|
}
|
|
354
|
+
const registeredIds = new Set(getCalendarSources()
|
|
355
|
+
.filter(s => s.type === 'oauth')
|
|
356
|
+
.map(s => s.calendarId));
|
|
265
357
|
console.log('Available calendars:');
|
|
266
358
|
console.log('');
|
|
267
359
|
calendars.forEach((cal, index) => {
|
|
268
360
|
const primary = cal.primary ? ' (primary)' : '';
|
|
269
|
-
|
|
361
|
+
const registered = registeredIds.has(cal.id) ? ' [registered]' : '';
|
|
362
|
+
console.log(` ${index + 1}. ${cal.summary}${primary}${registered}`);
|
|
270
363
|
});
|
|
271
364
|
console.log('');
|
|
272
|
-
// Interactive selection
|
|
365
|
+
// Interactive selection (comma-separated numbers for multiple calendars)
|
|
273
366
|
const rl = readline.createInterface({
|
|
274
367
|
input: process.stdin,
|
|
275
368
|
output: process.stdout,
|
|
276
369
|
});
|
|
277
370
|
const answer = await new Promise((resolve) => {
|
|
278
|
-
rl.question('Select
|
|
371
|
+
rl.question('Select calendars (numbers, comma-separated): ', resolve);
|
|
279
372
|
});
|
|
280
373
|
rl.close();
|
|
281
|
-
const
|
|
282
|
-
|
|
374
|
+
const selections = answer
|
|
375
|
+
.split(',')
|
|
376
|
+
.map(s => parseInt(s.trim(), 10))
|
|
377
|
+
.filter(n => !isNaN(n) && n >= 1 && n <= calendars.length);
|
|
378
|
+
if (selections.length === 0) {
|
|
283
379
|
console.log('Invalid selection.');
|
|
284
380
|
return;
|
|
285
381
|
}
|
|
286
|
-
const selectedCalendar = calendars[selection - 1];
|
|
287
|
-
// Update config with selected calendar
|
|
288
|
-
setCalendarOAuthConfig({
|
|
289
|
-
tokens: oauthConfig.tokens,
|
|
290
|
-
calendarId: selectedCalendar.id,
|
|
291
|
-
calendarName: selectedCalendar.summary,
|
|
292
|
-
});
|
|
293
382
|
console.log('');
|
|
294
|
-
|
|
383
|
+
for (const selection of [...new Set(selections)]) {
|
|
384
|
+
const selectedCalendar = calendars[selection - 1];
|
|
385
|
+
if (registeredIds.has(selectedCalendar.id)) {
|
|
386
|
+
console.log(`Calendar "${selectedCalendar.summary}" is already registered. Skipped.`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const source = addCalendarSource({
|
|
390
|
+
name: selectedCalendar.summary,
|
|
391
|
+
type: 'oauth',
|
|
392
|
+
calendarId: selectedCalendar.id,
|
|
393
|
+
enabled: true,
|
|
394
|
+
});
|
|
395
|
+
registeredIds.add(selectedCalendar.id);
|
|
396
|
+
console.log(`Calendar "${selectedCalendar.summary}" added! [${source.id}]`);
|
|
397
|
+
}
|
|
398
|
+
clearCalendarCache();
|
|
295
399
|
console.log('');
|
|
296
|
-
|
|
400
|
+
if (getCalendarConfig()?.enabled === false) {
|
|
401
|
+
console.log('Note: calendar display is currently disabled. Run "floq calendar enable" to show events.');
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
console.log('Calendar integration is now active. Run "floq schedule" to see events.');
|
|
405
|
+
}
|
|
297
406
|
}
|
|
298
407
|
catch (error) {
|
|
299
408
|
console.error('Failed to list calendars:', error instanceof Error ? error.message : error);
|
package/dist/commands/config.js
CHANGED
|
@@ -35,19 +35,11 @@ export async function showConfig() {
|
|
|
35
35
|
}
|
|
36
36
|
// Calendar configuration
|
|
37
37
|
const calendarConfig = getCalendarConfig();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.log(` Name: ${calendarConfig.name}`);
|
|
44
|
-
}
|
|
45
|
-
if (calendarConfig.url) {
|
|
46
|
-
console.log(` URL: ${calendarConfig.url.substring(0, 50)}...`);
|
|
47
|
-
}
|
|
48
|
-
if (calendarConfig.oauth) {
|
|
49
|
-
console.log(` Calendar: ${calendarConfig.oauth.calendarName}`);
|
|
50
|
-
}
|
|
38
|
+
const calendarSources = calendarConfig?.calendars || [];
|
|
39
|
+
console.log(`Calendar: ${isCalendarEnabled() ? 'enabled' : calendarSources.length > 0 ? 'disabled' : 'not configured'}`);
|
|
40
|
+
for (const source of calendarSources) {
|
|
41
|
+
const status = source.enabled !== false ? '' : ' (disabled)';
|
|
42
|
+
console.log(` - ${source.name} [${source.type}]${status}`);
|
|
51
43
|
}
|
|
52
44
|
}
|
|
53
45
|
export async function setLanguage(locale) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { getCalendarSources, getLocale } from '../config.js';
|
|
2
|
+
import { fetchCalendarEvents, getEventsForDate, formatEventTime, isEventOngoing } from '../calendar/index.js';
|
|
3
|
+
const WEEKDAYS_EN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
4
|
+
const WEEKDAYS_JA = ['日', '月', '火', '水', '木', '金', '土'];
|
|
5
|
+
function resolveRange(period, daysOption) {
|
|
6
|
+
const today = new Date();
|
|
7
|
+
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
8
|
+
if (daysOption) {
|
|
9
|
+
const days = parseInt(daysOption, 10);
|
|
10
|
+
if (isNaN(days) || days < 1) {
|
|
11
|
+
console.error('Error: --days must be a positive integer');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return { start: startOfToday, days };
|
|
15
|
+
}
|
|
16
|
+
switch (period) {
|
|
17
|
+
case undefined:
|
|
18
|
+
case 'today':
|
|
19
|
+
return { start: startOfToday, days: 1 };
|
|
20
|
+
case 'tomorrow': {
|
|
21
|
+
const tomorrow = new Date(startOfToday);
|
|
22
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
23
|
+
return { start: tomorrow, days: 1 };
|
|
24
|
+
}
|
|
25
|
+
case 'week':
|
|
26
|
+
return { start: startOfToday, days: 7 };
|
|
27
|
+
default:
|
|
28
|
+
console.error(`Error: Unknown period "${period}". Use: today, tomorrow, week`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function formatDateHeader(date, locale) {
|
|
33
|
+
const today = new Date();
|
|
34
|
+
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
35
|
+
const diff = Math.round((date.getTime() - startOfToday.getTime()) / (1000 * 60 * 60 * 24));
|
|
36
|
+
const weekdays = locale === 'ja' ? WEEKDAYS_JA : WEEKDAYS_EN;
|
|
37
|
+
const dateStr = locale === 'ja'
|
|
38
|
+
? `${date.getMonth() + 1}/${date.getDate()}(${weekdays[date.getDay()]})`
|
|
39
|
+
: `${weekdays[date.getDay()]}, ${date.toLocaleString('en', { month: 'short' })} ${date.getDate()}`;
|
|
40
|
+
if (diff === 0)
|
|
41
|
+
return `${dateStr} ${locale === 'ja' ? '今日' : '(Today)'}`;
|
|
42
|
+
if (diff === 1)
|
|
43
|
+
return `${dateStr} ${locale === 'ja' ? '明日' : '(Tomorrow)'}`;
|
|
44
|
+
return dateStr;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Show schedule from all registered calendars
|
|
48
|
+
*/
|
|
49
|
+
export async function showSchedule(period, options = {}) {
|
|
50
|
+
const sources = getCalendarSources();
|
|
51
|
+
const locale = getLocale();
|
|
52
|
+
if (sources.length === 0) {
|
|
53
|
+
console.log(locale === 'ja' ? 'カレンダーが設定されていません。' : 'No calendar configured.');
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(' floq calendar add <url> [-n name] (iCal URL)');
|
|
56
|
+
console.log(' floq calendar login && floq calendar select (Google OAuth)');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const range = resolveRange(period, options.days);
|
|
60
|
+
await fetchCalendarEvents();
|
|
61
|
+
const showCalendarName = sources.filter(s => s.enabled !== false).length > 1;
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
64
|
+
const baseOffset = Math.round((range.start.getTime() - startOfToday.getTime()) / (1000 * 60 * 60 * 24));
|
|
65
|
+
// Events are only fetched through the end of next month — warn if the range extends past it
|
|
66
|
+
const fetchWindowEnd = new Date(now.getFullYear(), now.getMonth() + 2, 0, 23, 59, 59);
|
|
67
|
+
const rangeEnd = new Date(range.start);
|
|
68
|
+
rangeEnd.setDate(rangeEnd.getDate() + range.days - 1);
|
|
69
|
+
if (rangeEnd > fetchWindowEnd) {
|
|
70
|
+
console.log(locale === 'ja'
|
|
71
|
+
? `注意: 来月末より先の予定は表示されません`
|
|
72
|
+
: 'Note: events beyond the end of next month are not shown');
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
let totalShown = 0;
|
|
76
|
+
for (let i = 0; i < range.days; i++) {
|
|
77
|
+
const dayStart = new Date(range.start);
|
|
78
|
+
dayStart.setDate(dayStart.getDate() + i);
|
|
79
|
+
const dayEvents = getEventsForDate(baseOffset + i);
|
|
80
|
+
// For multi-day views, skip empty days
|
|
81
|
+
if (dayEvents.length === 0 && range.days > 1) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (totalShown > 0 || i > 0) {
|
|
85
|
+
console.log('');
|
|
86
|
+
}
|
|
87
|
+
console.log(formatDateHeader(dayStart, locale));
|
|
88
|
+
console.log('-'.repeat(40));
|
|
89
|
+
if (dayEvents.length === 0) {
|
|
90
|
+
console.log(locale === 'ja' ? ' 予定はありません' : ' No events');
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const event of dayEvents) {
|
|
94
|
+
const timeStr = event.allDay
|
|
95
|
+
? (locale === 'ja' ? '終日 ' : 'All day ')
|
|
96
|
+
: `${formatEventTime(event.start)}-${formatEventTime(event.end)}`;
|
|
97
|
+
const ongoing = isEventOngoing(event) ? ' ◀' : '';
|
|
98
|
+
const calLabel = showCalendarName && event.calendarName ? ` [${event.calendarName}]` : '';
|
|
99
|
+
console.log(` ${timeStr} ${event.title}${calLabel}${ongoing}`);
|
|
100
|
+
if (event.location) {
|
|
101
|
+
console.log(` 📍 ${event.location}`);
|
|
102
|
+
}
|
|
103
|
+
totalShown++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (range.days > 1 && totalShown === 0) {
|
|
107
|
+
console.log(locale === 'ja' ? '期間内に予定はありません' : 'No events in this period');
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -21,12 +21,22 @@ export interface CalendarOAuthConfig {
|
|
|
21
21
|
calendarId: string;
|
|
22
22
|
calendarName: string;
|
|
23
23
|
}
|
|
24
|
+
export interface CalendarSource {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
type: 'ical' | 'oauth';
|
|
28
|
+
url?: string;
|
|
29
|
+
calendarId?: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}
|
|
24
32
|
export interface CalendarConfig {
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
calendars?: CalendarSource[];
|
|
35
|
+
googleOAuth?: GoogleOAuthClient;
|
|
36
|
+
oauthTokens?: CalendarOAuthTokens;
|
|
25
37
|
url?: string;
|
|
26
38
|
name?: string;
|
|
27
|
-
enabled?: boolean;
|
|
28
39
|
type?: 'ical' | 'oauth';
|
|
29
|
-
googleOAuth?: GoogleOAuthClient;
|
|
30
40
|
oauth?: CalendarOAuthConfig;
|
|
31
41
|
}
|
|
32
42
|
export type DateFormat = 'auto' | 'ddd, MMM D' | 'MM/DD(ddd)' | 'YYYY-MM-DD' | 'MM-DD' | 'DD/MM' | 'none';
|
|
@@ -74,12 +84,16 @@ export declare function getDateFormat(): DateFormat;
|
|
|
74
84
|
export declare function setDateFormat(format: DateFormat): void;
|
|
75
85
|
export declare function getCalendarConfig(): CalendarConfig | undefined;
|
|
76
86
|
export declare function setCalendarConfig(config: CalendarConfig | undefined): void;
|
|
87
|
+
export declare function getCalendarSources(): CalendarSource[];
|
|
88
|
+
export declare function addCalendarSource(source: Omit<CalendarSource, 'id'>): CalendarSource;
|
|
89
|
+
export declare function removeCalendarSource(id: string): boolean;
|
|
90
|
+
export declare function updateCalendarSource(id: string, updates: Partial<Omit<CalendarSource, 'id'>>): boolean;
|
|
91
|
+
export declare function isCalendarSourceUsable(source: CalendarSource, config?: CalendarConfig): boolean;
|
|
77
92
|
export declare function isCalendarEnabled(): boolean;
|
|
78
93
|
export declare function setCalendarEnabled(enabled: boolean): void;
|
|
79
94
|
export declare function getGoogleOAuthClient(): GoogleOAuthClient | undefined;
|
|
80
95
|
export declare function setGoogleOAuthClient(client: GoogleOAuthClient): void;
|
|
81
|
-
export declare function
|
|
82
|
-
export declare function
|
|
83
|
-
export declare function getCalendarType(): 'ical' | 'oauth' | undefined;
|
|
96
|
+
export declare function getCalendarOAuthTokens(): CalendarOAuthTokens | undefined;
|
|
97
|
+
export declare function setCalendarOAuthTokens(tokens: CalendarOAuthTokens | undefined): void;
|
|
84
98
|
export declare function getInsightsWeeks(): number;
|
|
85
99
|
export declare function setInsightsWeeks(weeks: number): void;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
|
|
2
2
|
import { dirname, join, isAbsolute } from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
3
4
|
import { CONFIG_FILE, DATA_DIR } from './paths.js';
|
|
4
5
|
// Migrate legacy DB file names (including related metadata files)
|
|
5
6
|
function migrateDbFiles() {
|
|
@@ -183,25 +184,109 @@ export function getDateFormat() {
|
|
|
183
184
|
export function setDateFormat(format) {
|
|
184
185
|
saveConfig({ dateFormat: format });
|
|
185
186
|
}
|
|
187
|
+
function generateCalendarId() {
|
|
188
|
+
return randomUUID().slice(0, 8);
|
|
189
|
+
}
|
|
190
|
+
// Migrate legacy single-calendar config to the multi-calendar format
|
|
191
|
+
function migrateCalendarConfig(calendar) {
|
|
192
|
+
const hasLegacyFields = calendar.url !== undefined ||
|
|
193
|
+
calendar.name !== undefined ||
|
|
194
|
+
calendar.type !== undefined ||
|
|
195
|
+
calendar.oauth !== undefined;
|
|
196
|
+
if (!hasLegacyFields)
|
|
197
|
+
return null;
|
|
198
|
+
const calendars = [...(calendar.calendars || [])];
|
|
199
|
+
let oauthTokens = calendar.oauthTokens;
|
|
200
|
+
if (calendar.url && !calendars.some(c => c.url === calendar.url)) {
|
|
201
|
+
calendars.push({
|
|
202
|
+
id: generateCalendarId(),
|
|
203
|
+
name: calendar.name || 'Calendar',
|
|
204
|
+
type: 'ical',
|
|
205
|
+
url: calendar.url,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (calendar.oauth) {
|
|
209
|
+
oauthTokens = oauthTokens || calendar.oauth.tokens;
|
|
210
|
+
if (calendar.oauth.calendarId && !calendars.some(c => c.calendarId === calendar.oauth?.calendarId)) {
|
|
211
|
+
calendars.push({
|
|
212
|
+
id: generateCalendarId(),
|
|
213
|
+
name: calendar.oauth.calendarName || 'Google Calendar',
|
|
214
|
+
type: 'oauth',
|
|
215
|
+
calendarId: calendar.oauth.calendarId,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
enabled: calendar.enabled,
|
|
221
|
+
calendars,
|
|
222
|
+
googleOAuth: calendar.googleOAuth,
|
|
223
|
+
oauthTokens,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
186
226
|
export function getCalendarConfig() {
|
|
187
|
-
|
|
227
|
+
const calendar = loadConfig().calendar;
|
|
228
|
+
if (!calendar)
|
|
229
|
+
return undefined;
|
|
230
|
+
const migrated = migrateCalendarConfig(calendar);
|
|
231
|
+
if (migrated) {
|
|
232
|
+
saveConfig({ calendar: migrated });
|
|
233
|
+
return migrated;
|
|
234
|
+
}
|
|
235
|
+
return calendar;
|
|
188
236
|
}
|
|
189
237
|
export function setCalendarConfig(config) {
|
|
190
238
|
saveConfig({ calendar: config });
|
|
191
239
|
}
|
|
240
|
+
export function getCalendarSources() {
|
|
241
|
+
return getCalendarConfig()?.calendars || [];
|
|
242
|
+
}
|
|
243
|
+
export function addCalendarSource(source) {
|
|
244
|
+
const calendar = getCalendarConfig() || {};
|
|
245
|
+
const newSource = { id: generateCalendarId(), ...source };
|
|
246
|
+
setCalendarConfig({
|
|
247
|
+
...calendar,
|
|
248
|
+
calendars: [...(calendar.calendars || []), newSource],
|
|
249
|
+
enabled: calendar.enabled === false ? calendar.enabled : true,
|
|
250
|
+
});
|
|
251
|
+
return newSource;
|
|
252
|
+
}
|
|
253
|
+
export function removeCalendarSource(id) {
|
|
254
|
+
const calendar = getCalendarConfig();
|
|
255
|
+
if (!calendar)
|
|
256
|
+
return false;
|
|
257
|
+
const calendars = calendar.calendars || [];
|
|
258
|
+
const newCalendars = calendars.filter(c => c.id !== id);
|
|
259
|
+
if (newCalendars.length === calendars.length)
|
|
260
|
+
return false;
|
|
261
|
+
setCalendarConfig({ ...calendar, calendars: newCalendars });
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
export function updateCalendarSource(id, updates) {
|
|
265
|
+
const calendar = getCalendarConfig();
|
|
266
|
+
if (!calendar)
|
|
267
|
+
return false;
|
|
268
|
+
const calendars = calendar.calendars || [];
|
|
269
|
+
const index = calendars.findIndex(c => c.id === id);
|
|
270
|
+
if (index === -1)
|
|
271
|
+
return false;
|
|
272
|
+
const newCalendars = [...calendars];
|
|
273
|
+
newCalendars[index] = { ...newCalendars[index], ...updates };
|
|
274
|
+
setCalendarConfig({ ...calendar, calendars: newCalendars });
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
// True when a calendar source has everything it needs to fetch events
|
|
278
|
+
export function isCalendarSourceUsable(source, config) {
|
|
279
|
+
const calendar = config ?? getCalendarConfig();
|
|
280
|
+
if (source.type === 'ical') {
|
|
281
|
+
return !!source.url;
|
|
282
|
+
}
|
|
283
|
+
return !!source.calendarId && !!calendar?.oauthTokens;
|
|
284
|
+
}
|
|
192
285
|
export function isCalendarEnabled() {
|
|
193
286
|
const calendar = getCalendarConfig();
|
|
194
287
|
if (!calendar || calendar.enabled === false)
|
|
195
288
|
return false;
|
|
196
|
-
|
|
197
|
-
if (calendar.type === 'oauth' && calendar.oauth) {
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
// iCal mode
|
|
201
|
-
if (calendar.url && calendar.url !== '') {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
return false;
|
|
289
|
+
return (calendar.calendars || []).some(c => c.enabled !== false && isCalendarSourceUsable(c, calendar));
|
|
205
290
|
}
|
|
206
291
|
export function setCalendarEnabled(enabled) {
|
|
207
292
|
const calendar = getCalendarConfig();
|
|
@@ -224,26 +309,19 @@ export function setGoogleOAuthClient(client) {
|
|
|
224
309
|
const calendar = getCalendarConfig() || {};
|
|
225
310
|
setCalendarConfig({ ...calendar, googleOAuth: client });
|
|
226
311
|
}
|
|
227
|
-
export function
|
|
228
|
-
return getCalendarConfig()?.
|
|
312
|
+
export function getCalendarOAuthTokens() {
|
|
313
|
+
return getCalendarConfig()?.oauthTokens;
|
|
229
314
|
}
|
|
230
|
-
export function
|
|
315
|
+
export function setCalendarOAuthTokens(tokens) {
|
|
231
316
|
const calendar = getCalendarConfig() || {};
|
|
232
|
-
if (
|
|
233
|
-
setCalendarConfig({ ...calendar,
|
|
317
|
+
if (tokens) {
|
|
318
|
+
setCalendarConfig({ ...calendar, oauthTokens: tokens });
|
|
234
319
|
}
|
|
235
320
|
else {
|
|
236
|
-
|
|
237
|
-
const { oauth: _removed, ...rest } = calendar;
|
|
321
|
+
const { oauthTokens: _removed, ...rest } = calendar;
|
|
238
322
|
setCalendarConfig(rest);
|
|
239
323
|
}
|
|
240
324
|
}
|
|
241
|
-
export function getCalendarType() {
|
|
242
|
-
const calendar = getCalendarConfig();
|
|
243
|
-
if (!calendar)
|
|
244
|
-
return undefined;
|
|
245
|
-
return calendar.type || (calendar.url ? 'ical' : undefined);
|
|
246
|
-
}
|
|
247
325
|
const DEFAULT_INSIGHTS_WEEKS = 2;
|
|
248
326
|
export function getInsightsWeeks() {
|
|
249
327
|
const weeks = loadConfig().insightsWeeks;
|
|
@@ -3,7 +3,7 @@ import { useState, useEffect, useMemo } from 'react';
|
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { t } from '../../i18n/index.js';
|
|
5
5
|
import { useTheme } from '../theme/index.js';
|
|
6
|
-
import { isCalendarEnabled,
|
|
6
|
+
import { isCalendarEnabled, getCalendarSources } from '../../config.js';
|
|
7
7
|
import { getCalendarEvents, getEventsForDate, formatEventTime, isEventOngoing, } from '../../calendar/index.js';
|
|
8
8
|
const WEEKDAYS_EN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
9
9
|
const WEEKDAYS_JA = ['日', '月', '火', '水', '木', '金', '土'];
|
|
@@ -19,8 +19,7 @@ export function CalendarModal({ onClose }) {
|
|
|
19
19
|
const [viewMonth, setViewMonth] = useState(new Date());
|
|
20
20
|
const [mode, setMode] = useState('calendar');
|
|
21
21
|
const calendarEnabled = isCalendarEnabled();
|
|
22
|
-
const
|
|
23
|
-
const calendarType = getCalendarType();
|
|
22
|
+
const calendarSources = getCalendarSources().filter(s => s.enabled !== false);
|
|
24
23
|
const isJapanese = i18n.tui.calendar?.yesterday === '昨日';
|
|
25
24
|
const weekdays = isJapanese ? WEEKDAYS_JA : WEEKDAYS_EN;
|
|
26
25
|
// Load all events on mount
|
|
@@ -211,10 +210,12 @@ export function CalendarModal({ onClose }) {
|
|
|
211
210
|
}
|
|
212
211
|
});
|
|
213
212
|
const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
|
|
214
|
-
// Get calendar name for header
|
|
215
|
-
const calendarName =
|
|
216
|
-
?
|
|
217
|
-
:
|
|
213
|
+
// Get calendar name for header (single calendar shows its name, multiple show a count)
|
|
214
|
+
const calendarName = calendarSources.length === 1
|
|
215
|
+
? calendarSources[0].name
|
|
216
|
+
: calendarSources.length > 1
|
|
217
|
+
? `Calendar (${calendarSources.length})`
|
|
218
|
+
: 'Calendar';
|
|
218
219
|
// Format month header
|
|
219
220
|
const monthNames = isJapanese
|
|
220
221
|
? ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|