floq 1.7.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 +43 -15
- 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/project.d.ts +6 -0
- package/dist/commands/project.js +95 -0
- 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/db/projectDelete.d.ts +20 -0
- package/dist/db/projectDelete.js +49 -0
- package/dist/i18n/en.d.ts +16 -0
- package/dist/i18n/en.js +11 -0
- package/dist/i18n/ja.js +11 -0
- package/dist/ui/App.js +78 -2
- package/dist/ui/components/CalendarModal.js +8 -7
- package/dist/ui/components/GtdDQ.js +77 -2
- package/dist/ui/components/GtdMario.js +76 -2
- package/dist/ui/history/commands/DeleteProjectCommand.d.ts +31 -0
- package/dist/ui/history/commands/DeleteProjectCommand.js +133 -0
- package/dist/ui/history/commands/index.d.ts +1 -0
- package/dist/ui/history/commands/index.js +1 -0
- package/dist/ui/history/commands/registry.js +2 -0
- package/dist/ui/history/index.d.ts +1 -1
- package/dist/ui/history/index.js +1 -1
- package/package.json +5 -6
|
@@ -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) {
|
|
@@ -5,4 +5,10 @@ export declare function addProject(name: string, options: AddProjectOptions): Pr
|
|
|
5
5
|
export declare function listProjectsCommand(): Promise<void>;
|
|
6
6
|
export declare function showProject(projectId: string): Promise<void>;
|
|
7
7
|
export declare function completeProject(projectId: string): Promise<void>;
|
|
8
|
+
interface DeleteProjectOptions {
|
|
9
|
+
withTasks?: boolean;
|
|
10
|
+
keepTasks?: boolean;
|
|
11
|
+
force?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function deleteProjectCommand(projectId: string, options: DeleteProjectOptions): Promise<void>;
|
|
8
14
|
export {};
|
package/dist/commands/project.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
2
3
|
import { eq, like, and } from 'drizzle-orm';
|
|
3
4
|
import { getDb, schema } from '../db/index.js';
|
|
5
|
+
import { deleteProject } from '../db/projectDelete.js';
|
|
4
6
|
import { t, fmt } from '../i18n/index.js';
|
|
5
7
|
export async function addProject(name, options) {
|
|
6
8
|
const db = getDb();
|
|
@@ -150,3 +152,96 @@ export async function completeProject(projectId) {
|
|
|
150
152
|
.where(eq(schema.tasks.id, project.id));
|
|
151
153
|
console.log(fmt(i18n.commands.project.completed, { name: project.title }));
|
|
152
154
|
}
|
|
155
|
+
function ask(rl, question) {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
rl.question(question, (answer) => {
|
|
158
|
+
resolve(answer.trim().toLowerCase());
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function deleteProjectCommand(projectId, options) {
|
|
163
|
+
const db = getDb();
|
|
164
|
+
const i18n = t();
|
|
165
|
+
// Find project by ID prefix, then by exact name.
|
|
166
|
+
let projects = await db
|
|
167
|
+
.select()
|
|
168
|
+
.from(schema.tasks)
|
|
169
|
+
.where(and(eq(schema.tasks.isProject, true), like(schema.tasks.id, `${projectId}%`)));
|
|
170
|
+
if (projects.length === 0) {
|
|
171
|
+
projects = await db
|
|
172
|
+
.select()
|
|
173
|
+
.from(schema.tasks)
|
|
174
|
+
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.title, projectId)));
|
|
175
|
+
}
|
|
176
|
+
if (projects.length === 0) {
|
|
177
|
+
console.error(fmt(i18n.commands.project.notFound, { id: projectId }));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
if (projects.length > 1) {
|
|
181
|
+
console.error(fmt(i18n.commands.project.multipleMatch, { id: projectId }));
|
|
182
|
+
for (const p of projects) {
|
|
183
|
+
console.error(` [${p.id.slice(0, 8)}] ${p.title}`);
|
|
184
|
+
}
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const project = projects[0];
|
|
188
|
+
const children = await db
|
|
189
|
+
.select()
|
|
190
|
+
.from(schema.tasks)
|
|
191
|
+
.where(eq(schema.tasks.parentId, project.id));
|
|
192
|
+
const childCount = children.length;
|
|
193
|
+
// Decide how to handle child tasks. When neither flag is given and the
|
|
194
|
+
// project has tasks, the choice is resolved interactively below.
|
|
195
|
+
let mode = options.withTasks ? 'cascade' :
|
|
196
|
+
options.keepTasks ? 'keep' :
|
|
197
|
+
childCount === 0 ? 'cascade' :
|
|
198
|
+
null;
|
|
199
|
+
if (!options.force) {
|
|
200
|
+
// A single prompt keeps this reliable for piped/non-TTY input.
|
|
201
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
202
|
+
try {
|
|
203
|
+
if (mode !== null) {
|
|
204
|
+
const answer = await ask(rl, `${fmt(i18n.commands.project.deletePrompt, { name: project.title })} (y/N): `);
|
|
205
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
206
|
+
console.log(i18n.commands.project.deleteCancelled);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const answer = await ask(rl, `${fmt(i18n.commands.project.deleteChildrenPrompt, { name: project.title, count: childCount })}: `);
|
|
212
|
+
if (answer === 'a' || answer === 'all') {
|
|
213
|
+
mode = 'cascade';
|
|
214
|
+
}
|
|
215
|
+
else if (answer === 'k' || answer === 'keep') {
|
|
216
|
+
mode = 'keep';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.log(i18n.commands.project.deleteCancelled);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
rl.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (mode === null) {
|
|
229
|
+
// Forced with no explicit choice: keep tasks to avoid silent data loss.
|
|
230
|
+
mode = 'keep';
|
|
231
|
+
}
|
|
232
|
+
await deleteProject(project, mode);
|
|
233
|
+
if (mode === 'cascade') {
|
|
234
|
+
if (childCount > 0) {
|
|
235
|
+
console.log(fmt(i18n.commands.project.deletedWithTasks, { name: project.title, count: childCount }));
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
console.log(fmt(i18n.commands.project.deleted, { name: project.title }));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(fmt(i18n.commands.project.deleted, { name: project.title }));
|
|
243
|
+
if (childCount > 0) {
|
|
244
|
+
console.log(fmt(i18n.commands.project.tasksMovedToInbox, { count: childCount }));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|