@nbtca/prompt 1.3.1 → 1.4.1
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 +44 -0
- package/SECURITY.md +47 -0
- package/dist/app/app.js +202 -0
- package/dist/app/chrome.js +104 -0
- package/dist/app/fields/list-field.js +174 -0
- package/dist/app/fields/text-field.js +38 -0
- package/dist/app/frame.js +48 -0
- package/dist/app/keys.js +20 -0
- package/dist/app/tabs.js +11 -0
- package/dist/app/view.js +1 -0
- package/dist/app/views/docs-render.js +82 -0
- package/dist/app/views/docs.js +457 -0
- package/dist/app/views/events-render.js +111 -0
- package/dist/app/views/events.js +228 -0
- package/dist/app/views/home.js +236 -0
- package/dist/app/views/schedule-grid-cursor.js +52 -0
- package/dist/app/views/schedule-render.js +317 -0
- package/dist/app/views/schedule.js +472 -0
- package/dist/app/views/settings-render.js +53 -0
- package/dist/app/views/settings.js +153 -0
- package/dist/auth/cookie-transport.js +222 -0
- package/dist/auth/errors.js +18 -0
- package/dist/auth/nbt-auth.js +239 -0
- package/dist/auth/session-store.js +118 -0
- package/dist/config/paths.js +22 -2
- package/dist/core/canvas.js +23 -0
- package/dist/core/capabilities.js +42 -0
- package/dist/core/components/confirm.js +75 -0
- package/dist/core/components/input-session.js +24 -0
- package/dist/core/components/menu.js +122 -0
- package/dist/core/components/messages.js +16 -0
- package/dist/core/components/note.js +18 -0
- package/dist/core/components/painter.js +26 -0
- package/dist/core/components/screen.js +18 -0
- package/dist/core/components/spinner.js +47 -0
- package/dist/core/components/text-input.js +98 -0
- package/dist/core/logo.js +40 -15
- package/dist/core/menu.js +30 -13
- package/dist/core/motion.js +86 -0
- package/dist/core/text.js +127 -5
- package/dist/core/theme.js +61 -0
- package/dist/core/transitions.js +19 -0
- package/dist/core/ui.js +5 -29
- package/dist/features/calendar-heatmap.js +29 -27
- package/dist/features/calendar-query.js +50 -0
- package/dist/features/calendar.js +192 -111
- package/dist/features/docs.js +382 -128
- package/dist/features/links.js +7 -5
- package/dist/features/schedule-query.js +47 -0
- package/dist/features/schedule-render.js +574 -0
- package/dist/features/schedule-store.js +73 -0
- package/dist/features/schedule-view.js +260 -0
- package/dist/features/settings.js +43 -33
- package/dist/features/status.js +37 -13
- package/dist/features/student-timetable.js +346 -0
- package/dist/features/update.js +16 -8
- package/dist/i18n/locales/en.json +162 -17
- package/dist/i18n/locales/zh.json +164 -19
- package/dist/index.js +59 -5
- package/dist/logo/ca-dotmatrix-large.txt +26 -0
- package/dist/logo/ca-dotmatrix-small.txt +12 -0
- package/dist/logo/ca-dotmatrix.txt +18 -16
- package/dist/logo/ca-logo.png +0 -0
- package/dist/main.js +33 -13
- package/package.json +10 -7
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { loadCalendar, FeedFetchError, FeedParseError } from '@nbtca/nbtcal';
|
|
1
|
+
import { loadCalendar, FeedFetchError, FeedParseError, eventToICS } from '@nbtca/nbtcal';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { createSpinner, success, error } from '../core/ui.js';
|
|
4
|
+
import { c, type, space, glyph } from '../core/theme.js';
|
|
5
|
+
import { runMenu, menuFooter } from '../core/components/menu.js';
|
|
6
|
+
import { runTextInput } from '../core/components/text-input.js';
|
|
6
7
|
import { pickIcon } from '../core/icons.js';
|
|
7
|
-
import { padEndV, truncate } from '../core/text.js';
|
|
8
|
+
import { padEndV, truncate, visualWidth, wrapAnsiToVisualWidth } from '../core/text.js';
|
|
8
9
|
import { t } from '../i18n/index.js';
|
|
10
|
+
import { enterScreen, breadcrumb } from '../core/transitions.js';
|
|
9
11
|
import { URLS } from '../config/data.js';
|
|
10
12
|
import { renderHeatmap } from './calendar-heatmap.js';
|
|
13
|
+
import { countdownParts, isCountdownUrgent, buildExportFilename, weekRange, monthRange, filterEvents } from './calendar-query.js';
|
|
14
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
11
16
|
function formatDate(date) {
|
|
12
17
|
const now = new Date();
|
|
13
18
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
@@ -22,7 +27,7 @@ function formatTime(date) {
|
|
|
22
27
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
23
28
|
return `${hours}:${minutes}`;
|
|
24
29
|
}
|
|
25
|
-
async function loadCalendarOrThrow() {
|
|
30
|
+
export async function loadCalendarOrThrow() {
|
|
26
31
|
try {
|
|
27
32
|
return await loadCalendar({ timeoutMs: 15000 });
|
|
28
33
|
}
|
|
@@ -42,11 +47,16 @@ export function toDisplayEvent(e) {
|
|
|
42
47
|
location: e.location ?? trans.calendar.tbdLocation,
|
|
43
48
|
description: e.description ?? '',
|
|
44
49
|
startDate: e.start,
|
|
50
|
+
recurring: e.recurring,
|
|
51
|
+
uid: e.uid,
|
|
45
52
|
};
|
|
46
53
|
}
|
|
47
54
|
export async function fetchEvents() {
|
|
48
55
|
return (await loadCalendarOrThrow()).upcoming({ days: 30 }).map(toDisplayEvent);
|
|
49
56
|
}
|
|
57
|
+
export async function fetchInRange(start, end) {
|
|
58
|
+
return (await loadCalendarOrThrow()).inRange(start, end).map(toDisplayEvent);
|
|
59
|
+
}
|
|
50
60
|
export async function fetchHeatmapBuckets() {
|
|
51
61
|
const now = new Date();
|
|
52
62
|
const start = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
|
|
@@ -60,13 +70,15 @@ export function serializeEvents(events) {
|
|
|
60
70
|
location: event.location,
|
|
61
71
|
description: event.description,
|
|
62
72
|
startDateISO: event.startDate.toISOString(),
|
|
73
|
+
recurring: event.recurring,
|
|
74
|
+
uid: event.uid,
|
|
63
75
|
}));
|
|
64
76
|
}
|
|
65
77
|
export function renderEventsTable(events, options) {
|
|
66
78
|
const trans = t();
|
|
67
79
|
const useColor = options?.color !== false;
|
|
68
80
|
if (events.length === 0)
|
|
69
|
-
return
|
|
81
|
+
return `${space.indent}${type.hint(trans.calendar.noEvents)}`;
|
|
70
82
|
const id = (s) => s;
|
|
71
83
|
const applyDim = useColor ? chalk.dim : id;
|
|
72
84
|
const applyCyan = useColor ? chalk.cyan : id;
|
|
@@ -89,32 +101,66 @@ export function renderEventsTable(events, options) {
|
|
|
89
101
|
for (const event of events) {
|
|
90
102
|
const dateTime = event.time ? `${event.date} ${event.time}` : event.date;
|
|
91
103
|
const dateCol = padEndV(applyCyan(dateTime), dateWidth);
|
|
92
|
-
const
|
|
104
|
+
const marker = event.recurring ? `${pickIcon('↻', '~')} ` : '';
|
|
105
|
+
const titleText = truncate(`${marker}${event.title}`, titleWidth);
|
|
106
|
+
const titleCol = padEndV(applyBold(titleText), titleWidth);
|
|
93
107
|
const locCol = applyGray(truncate(event.location, locWidth));
|
|
94
108
|
lines.push(` ${dateCol} ${titleCol} ${locCol}`);
|
|
95
109
|
}
|
|
96
110
|
return lines.join('\n');
|
|
97
111
|
}
|
|
112
|
+
/** One compact line for an "at a glance" activity briefing: date/time · title,
|
|
113
|
+
* with today's events picked out (bold + accent) so the one thing worth
|
|
114
|
+
* noticing doesn't read the same as everything else in the list — and a
|
|
115
|
+
* recurring marker, matching renderEventsTable's convention. Shared by Home's
|
|
116
|
+
* dashboard panel and the Events hub so the two surfaces read consistently
|
|
117
|
+
* instead of drifting into slightly different formats. */
|
|
118
|
+
export function renderEventBrief(e, now) {
|
|
119
|
+
const dot = pickIcon('·', '-');
|
|
120
|
+
const isToday = e.startDate.getFullYear() === now.getFullYear()
|
|
121
|
+
&& e.startDate.getMonth() === now.getMonth()
|
|
122
|
+
&& e.startDate.getDate() === now.getDate();
|
|
123
|
+
const dateTime = `${e.date}${e.time ? ' ' + e.time : ''}`;
|
|
124
|
+
// "Today" gets one consistent brand-colored signal across marker/date/
|
|
125
|
+
// title — c.warn (yellow) stays reserved for genuine time-pressure
|
|
126
|
+
// urgency (isCountdownUrgent below), not "happens today" alone, so the
|
|
127
|
+
// two alert levels never compete for the same color on one line.
|
|
128
|
+
const marker = isToday ? type.active(pickIcon('●', '*')) : type.hint(pickIcon('·', '-'));
|
|
129
|
+
const dateStyled = isToday ? type.active(dateTime) : type.hint(dateTime);
|
|
130
|
+
const titleStyled = isToday ? type.active(e.title) : type.body(e.title);
|
|
131
|
+
const recurringMark = e.recurring ? ` ${pickIcon('↻', '~')}` : '';
|
|
132
|
+
return `${space.indent}${marker} ${dateStyled} ${dot} ${titleStyled}${recurringMark}`;
|
|
133
|
+
}
|
|
134
|
+
export function renderCountdownBanner(event, now, cols = Number.POSITIVE_INFINITY) {
|
|
135
|
+
if (!event)
|
|
136
|
+
return '';
|
|
137
|
+
const trans = t();
|
|
138
|
+
const p = countdownParts(event.startDate, now);
|
|
139
|
+
const inp = trans.calendar.inPrefix;
|
|
140
|
+
const when = p.past
|
|
141
|
+
? trans.calendar.startingNow
|
|
142
|
+
: p.days > 0
|
|
143
|
+
? `${inp} ${p.days}d ${p.hours}h`
|
|
144
|
+
: p.hours > 0
|
|
145
|
+
? `${inp} ${p.hours}h ${p.minutes}m`
|
|
146
|
+
: `${inp} ${p.minutes}m`;
|
|
147
|
+
const whenStyled = isCountdownUrgent(p) ? c.warn(when) : type.hint(when);
|
|
148
|
+
const dot = pickIcon('·', '-');
|
|
149
|
+
const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : Number.POSITIVE_INFINITY;
|
|
150
|
+
const cursor = type.active(glyph.cursor());
|
|
151
|
+
const prefixes = [`${space.indent}${cursor} `, `${cursor} `, cursor, ''];
|
|
152
|
+
const prefix = prefixes.find((candidate) => visualWidth(candidate) < width) ?? '';
|
|
153
|
+
const continuation = ' '.repeat(visualWidth(prefix));
|
|
154
|
+
const contentWidth = Math.max(1, width - visualWidth(prefix));
|
|
155
|
+
const content = `${type.label(trans.calendar.next)} ${dot} ${type.body(event.title)} ${dot} ${whenStyled}`;
|
|
156
|
+
return wrapAnsiToVisualWidth(content, contentWidth)
|
|
157
|
+
.map((line, index) => `${index === 0 ? prefix : continuation}${line}`)
|
|
158
|
+
.join('\n');
|
|
159
|
+
}
|
|
98
160
|
function renderSubscribeHint() {
|
|
99
161
|
const icon = pickIcon('◆', '*');
|
|
100
162
|
console.log(c.muted(` ${icon} ${t().calendar.subscribeHint}: ${URLS.calendar}`));
|
|
101
163
|
}
|
|
102
|
-
async function showEventDetail(event) {
|
|
103
|
-
const trans = t();
|
|
104
|
-
console.log();
|
|
105
|
-
console.log(chalk.bold.cyan(` ${event.title}`));
|
|
106
|
-
console.log(c.muted(` ${event.date}${event.time ? ' ' + event.time : ''} ${pickIcon('·', '|')} ${event.location}`));
|
|
107
|
-
if (event.description) {
|
|
108
|
-
console.log();
|
|
109
|
-
for (const line of event.description.trim().split('\n')) {
|
|
110
|
-
console.log(` ${line}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
console.log(c.muted(` ${trans.calendar.noDescription}`));
|
|
115
|
-
}
|
|
116
|
-
console.log();
|
|
117
|
-
}
|
|
118
164
|
/** Startup preview: auto-loads and displays upcoming events, then returns. */
|
|
119
165
|
export async function showEventsPreview() {
|
|
120
166
|
const trans = t();
|
|
@@ -139,107 +185,142 @@ export async function showEventsPreview() {
|
|
|
139
185
|
console.log();
|
|
140
186
|
}
|
|
141
187
|
}
|
|
142
|
-
/**
|
|
143
|
-
export async function
|
|
144
|
-
const trans = t();
|
|
145
|
-
const choice = await select({
|
|
146
|
-
message: trans.menu.chooseAction,
|
|
147
|
-
options: [
|
|
148
|
-
{ value: 'upcoming', label: trans.menu.events, hint: trans.menu.eventsDesc },
|
|
149
|
-
{ value: 'past', label: trans.calendar.pastEvents, hint: trans.calendar.pastEventsDesc },
|
|
150
|
-
{ value: '__back__', label: c.muted(trans.common.back) },
|
|
151
|
-
],
|
|
152
|
-
});
|
|
153
|
-
if (isCancel(choice) || choice === '__back__')
|
|
154
|
-
return;
|
|
155
|
-
if (choice === 'upcoming')
|
|
156
|
-
await showCalendar();
|
|
157
|
-
else if (choice === 'past')
|
|
158
|
-
await showPastEvents();
|
|
159
|
-
}
|
|
160
|
-
/** Past events: shows historical events from the last 30 days with detail selection. */
|
|
161
|
-
export async function showPastEvents() {
|
|
188
|
+
/** Full interactive calendar hub: countdown + heatmap + a menu of range/search/past views. */
|
|
189
|
+
export async function showCalendar() {
|
|
162
190
|
const trans = t();
|
|
163
|
-
|
|
191
|
+
await enterScreen(breadcrumb(trans.menu.events));
|
|
192
|
+
const spinner = createSpinner(trans.calendar.loading);
|
|
193
|
+
let cal;
|
|
164
194
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (events.length === 0) {
|
|
168
|
-
s.stop(trans.calendar.noPastEvents);
|
|
169
|
-
console.log();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
s.stop(`${events.length} ${trans.calendar.eventsFound}`);
|
|
173
|
-
console.log();
|
|
174
|
-
console.log(renderEventsTable(events, { color: true }));
|
|
175
|
-
console.log();
|
|
176
|
-
const options = [
|
|
177
|
-
...events.map((e, i) => ({
|
|
178
|
-
value: String(i),
|
|
179
|
-
label: `${e.date}${e.time ? ' ' + e.time : ''} ${e.title}`,
|
|
180
|
-
hint: e.location,
|
|
181
|
-
})),
|
|
182
|
-
{ value: '__back__', label: c.muted(trans.common.back) },
|
|
183
|
-
];
|
|
184
|
-
const selected = await select({ message: trans.calendar.viewPastDetail, options });
|
|
185
|
-
if (!isCancel(selected) && selected !== '__back__') {
|
|
186
|
-
const event = events[Number.parseInt(selected, 10)];
|
|
187
|
-
if (event)
|
|
188
|
-
await showEventDetail(event);
|
|
189
|
-
}
|
|
195
|
+
cal = await loadCalendarOrThrow();
|
|
196
|
+
spinner.stop();
|
|
190
197
|
}
|
|
191
198
|
catch {
|
|
192
|
-
|
|
199
|
+
spinner.error(trans.calendar.error);
|
|
193
200
|
console.log(c.muted(' ' + trans.calendar.errorHint));
|
|
194
201
|
console.log();
|
|
202
|
+
return;
|
|
195
203
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
const now = new Date();
|
|
205
|
+
const upcoming = cal.upcoming({ days: 30 });
|
|
206
|
+
console.log();
|
|
207
|
+
console.log(renderCountdownBanner(upcoming[0] ? toDisplayEvent(upcoming[0]) : undefined, now));
|
|
208
|
+
console.log();
|
|
209
|
+
console.log(renderHeatmap(cal.heatmap({ start: new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000), end: now, bucket: 'day' }), now, { color: true }));
|
|
210
|
+
console.log();
|
|
211
|
+
while (true) {
|
|
212
|
+
const action = await runMenu({
|
|
213
|
+
title: trans.calendar.viewDetail,
|
|
214
|
+
options: [
|
|
215
|
+
{ value: 'upcoming', label: trans.menu.events, hint: String(upcoming.length) },
|
|
216
|
+
{ value: 'week', label: trans.calendar.thisWeek },
|
|
217
|
+
{ value: 'month', label: trans.calendar.thisMonth },
|
|
218
|
+
{ value: 'search', label: trans.calendar.search },
|
|
219
|
+
{ value: 'past', label: trans.calendar.pastEvents },
|
|
220
|
+
],
|
|
221
|
+
footer: menuFooter(),
|
|
209
222
|
});
|
|
210
|
-
if (
|
|
211
|
-
s.stop(trans.calendar.noEvents);
|
|
212
|
-
console.log();
|
|
213
|
-
console.log(renderHeatmap(heatmapBuckets, now, { color: true }));
|
|
214
|
-
console.log();
|
|
223
|
+
if (action === null)
|
|
215
224
|
return;
|
|
225
|
+
if (action === 'upcoming')
|
|
226
|
+
await showEventList(upcoming, trans.menu.events);
|
|
227
|
+
else if (action === 'week') {
|
|
228
|
+
const r = weekRange(now);
|
|
229
|
+
await showEventList(cal.inRange(r.start, r.end), trans.calendar.thisWeek);
|
|
216
230
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
231
|
+
else if (action === 'month') {
|
|
232
|
+
const r = monthRange(now);
|
|
233
|
+
await showEventList(cal.inRange(r.start, r.end), trans.calendar.thisMonth);
|
|
234
|
+
}
|
|
235
|
+
else if (action === 'search')
|
|
236
|
+
await showSearch(cal);
|
|
237
|
+
else if (action === 'past')
|
|
238
|
+
await showEventList(cal.past({ days: 30 }).reverse(), trans.calendar.pastEvents);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function showEventList(events, title) {
|
|
242
|
+
const trans = t();
|
|
243
|
+
if (events.length === 0) {
|
|
244
|
+
console.log(`${space.indent}${type.hint(trans.calendar.noEvents)}`);
|
|
220
245
|
console.log();
|
|
221
|
-
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const display = events.map(toDisplayEvent);
|
|
249
|
+
console.log();
|
|
250
|
+
console.log(renderEventsTable(display, { color: true }));
|
|
251
|
+
console.log();
|
|
252
|
+
const selected = await runMenu({
|
|
253
|
+
title,
|
|
254
|
+
options: events.map((_e, i) => ({
|
|
255
|
+
value: String(i),
|
|
256
|
+
label: `${display[i].date}${display[i].time ? ' ' + display[i].time : ''} ${display[i].title}`,
|
|
257
|
+
hint: display[i].location,
|
|
258
|
+
})),
|
|
259
|
+
footer: menuFooter(),
|
|
260
|
+
});
|
|
261
|
+
if (selected === null)
|
|
262
|
+
return;
|
|
263
|
+
const raw = events[Number.parseInt(selected, 10)];
|
|
264
|
+
if (raw)
|
|
265
|
+
await showEventDetailRaw(raw);
|
|
266
|
+
}
|
|
267
|
+
async function showEventDetailRaw(raw) {
|
|
268
|
+
const trans = t();
|
|
269
|
+
const e = toDisplayEvent(raw);
|
|
270
|
+
console.log();
|
|
271
|
+
console.log(chalk.bold.cyan(` ${e.title}`));
|
|
272
|
+
console.log(c.muted(` ${e.date}${e.time ? ' ' + e.time : ''} ${pickIcon('·', '|')} ${e.location}`));
|
|
273
|
+
if (raw.recurring)
|
|
274
|
+
console.log(c.muted(` ${pickIcon('↻', '~')} ${trans.calendar.recurringLabel}`));
|
|
275
|
+
if (e.description) {
|
|
222
276
|
console.log();
|
|
223
|
-
|
|
277
|
+
for (const line of e.description.trim().split('\n'))
|
|
278
|
+
console.log(` ${line}`);
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
console.log(c.muted(` ${trans.calendar.noDescription}`));
|
|
282
|
+
console.log();
|
|
283
|
+
const action = await runMenu({
|
|
284
|
+
title: e.title,
|
|
285
|
+
options: [{ value: 'export', label: trans.calendar.exportIcs }],
|
|
286
|
+
footer: menuFooter(),
|
|
287
|
+
});
|
|
288
|
+
if (action === 'export') {
|
|
289
|
+
const res = exportEventIcs(raw);
|
|
290
|
+
if (res.ok)
|
|
291
|
+
success(`${trans.calendar.exportSuccess}: ${res.path}`);
|
|
292
|
+
else
|
|
293
|
+
error(`${trans.calendar.exportError}: ${res.error ?? ''}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function showSearch(cal) {
|
|
297
|
+
const trans = t();
|
|
298
|
+
const query = await runTextInput({ message: trans.calendar.searchPrompt, placeholder: trans.calendar.searchPlaceholder });
|
|
299
|
+
if (query === null || !query.trim())
|
|
300
|
+
return;
|
|
301
|
+
const now = new Date();
|
|
302
|
+
const pool = cal.inRange(now, new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000));
|
|
303
|
+
const results = filterEvents(pool, query);
|
|
304
|
+
if (results.length === 0) {
|
|
305
|
+
console.log(`${space.indent}${type.hint(trans.calendar.searchNoResults)}`);
|
|
224
306
|
console.log();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await showEventDetail(event);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await showEventList(results, `${trans.calendar.search}: ${query.trim()}`);
|
|
310
|
+
}
|
|
311
|
+
export function exportEventIcs(event, dir = process.cwd()) {
|
|
312
|
+
const base = buildExportFilename(event);
|
|
313
|
+
let path = join(dir, base);
|
|
314
|
+
try {
|
|
315
|
+
let n = 1;
|
|
316
|
+
while (existsSync(path)) {
|
|
317
|
+
path = join(dir, base.replace(/\.ics$/, `-${n}.ics`));
|
|
318
|
+
n++;
|
|
238
319
|
}
|
|
320
|
+
writeFileSync(path, eventToICS(event), 'utf-8');
|
|
321
|
+
return { ok: true, path };
|
|
239
322
|
}
|
|
240
|
-
catch {
|
|
241
|
-
|
|
242
|
-
console.log(c.muted(' ' + trans.calendar.errorHint));
|
|
243
|
-
console.log();
|
|
323
|
+
catch (err) {
|
|
324
|
+
return { ok: false, path, error: err instanceof Error ? err.message : String(err) };
|
|
244
325
|
}
|
|
245
326
|
}
|