@nbtca/prompt 1.5.11 → 1.5.12
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/dist/app/views/events-render.js +2 -0
- package/dist/app/views/events.js +12 -14
- package/dist/app/views/home.js +2 -2
- package/dist/app/views/schedule.js +2 -2
- package/dist/auth/cookie-transport.js +11 -34
- package/dist/cli.js +17 -27
- package/dist/features/calendar-heatmap.js +7 -5
- package/dist/features/calendar-query.js +19 -0
- package/dist/features/calendar.js +12 -9
- package/dist/features/student-timetable.js +66 -12
- package/dist/i18n/locales/en.json +4 -1
- package/dist/i18n/locales/zh.json +4 -1
- package/package.json +1 -1
|
@@ -19,6 +19,8 @@ function renderHubBody(state, now, bodyRows, cols) {
|
|
|
19
19
|
const rows = Number.isFinite(bodyRows)
|
|
20
20
|
? Math.max(0, Math.floor(bodyRows))
|
|
21
21
|
: Number.POSITIVE_INFINITY;
|
|
22
|
+
if (state.stale)
|
|
23
|
+
lines.push(...wrappedIndentedLines(trans.calendar.stale, cols, type.hint), '');
|
|
22
24
|
const banner = renderCountdownBanner(state.nextEvent, now, cols);
|
|
23
25
|
if (banner)
|
|
24
26
|
lines.push(...banner.split('\n'), '');
|
package/dist/app/views/events.js
CHANGED
|
@@ -5,11 +5,12 @@ import { renderEvents } from './events-render.js';
|
|
|
5
5
|
import { setVimKeysActive } from '../../core/vim-keys.js';
|
|
6
6
|
import { pickIcon } from '../../core/icons.js';
|
|
7
7
|
import { t } from '../../i18n/index.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { exportEventIcs, loadCalendarOrCache, toDisplayEvent, yearHeatmap, } from '../../features/calendar.js';
|
|
9
|
+
import { currentEvents, filterEvents, monthRange, weekRange, } from '../../features/calendar-query.js';
|
|
10
10
|
import { addLocalDays } from '../../core/calendar-day.js';
|
|
11
11
|
let state = { mode: 'loading' };
|
|
12
12
|
let calendar = null;
|
|
13
|
+
let stale = false;
|
|
13
14
|
let currentList = [];
|
|
14
15
|
function backLabel() {
|
|
15
16
|
return t().common.back;
|
|
@@ -54,19 +55,15 @@ function showList(title, events, ctx) {
|
|
|
54
55
|
}
|
|
55
56
|
const RECENT_ACTIVITY_FETCH_CAP = 15;
|
|
56
57
|
function goToHub() {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const upcoming = calendar ? currentEvents(calendar, now) : [];
|
|
60
|
+
const nextEvent = upcoming.find((event) => event.start >= now);
|
|
59
61
|
state = {
|
|
60
62
|
mode: 'hub',
|
|
61
63
|
hubField: buildHubField(),
|
|
64
|
+
...(stale ? { stale } : {}),
|
|
62
65
|
...(nextEvent === undefined ? {} : { nextEvent: toDisplayEvent(nextEvent) }),
|
|
63
|
-
heatmapBuckets: calendar
|
|
64
|
-
? calendar.heatmap({
|
|
65
|
-
start: addLocalDays(new Date(), -365),
|
|
66
|
-
end: new Date(),
|
|
67
|
-
bucket: 'day',
|
|
68
|
-
})
|
|
69
|
-
: [],
|
|
66
|
+
heatmapBuckets: calendar ? yearHeatmap(calendar, now) : [],
|
|
70
67
|
recentEvents: upcoming.slice(0, RECENT_ACTIVITY_FETCH_CAP).map(toDisplayEvent),
|
|
71
68
|
};
|
|
72
69
|
}
|
|
@@ -98,10 +95,11 @@ export const eventsView = {
|
|
|
98
95
|
state = { mode: 'loading' };
|
|
99
96
|
ctx.rerender();
|
|
100
97
|
try {
|
|
101
|
-
const
|
|
98
|
+
const loaded = await loadCalendarOrCache(ctx.signal);
|
|
102
99
|
if (ctx.signal?.aborted)
|
|
103
100
|
return;
|
|
104
|
-
calendar =
|
|
101
|
+
calendar = loaded.calendar;
|
|
102
|
+
stale = loaded.stale;
|
|
105
103
|
goToHub();
|
|
106
104
|
}
|
|
107
105
|
catch {
|
|
@@ -153,7 +151,7 @@ export const eventsView = {
|
|
|
153
151
|
return;
|
|
154
152
|
const now = new Date();
|
|
155
153
|
if (result.selected === 'upcoming') {
|
|
156
|
-
showList(t().menu.events, calendar
|
|
154
|
+
showList(t().menu.events, currentEvents(calendar, now), ctx);
|
|
157
155
|
return;
|
|
158
156
|
}
|
|
159
157
|
if (result.selected === 'week') {
|
package/dist/app/views/home.js
CHANGED
|
@@ -8,6 +8,7 @@ import { weekdayShortLabel } from '../../features/schedule-render.js';
|
|
|
8
8
|
import { passiveFooterHint } from '../chrome.js';
|
|
9
9
|
import { campusDateTime, campusIsoDate } from '@nbtca/nbtcal/timetable';
|
|
10
10
|
import { isoDayDifference, localDayDifference, parseLocalDate } from '../../core/calendar-day.js';
|
|
11
|
+
import { currentEvents } from '../../features/calendar-query.js';
|
|
11
12
|
import { loadingLines } from '../../core/components/spinner.js';
|
|
12
13
|
const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
|
|
13
14
|
function wrappedIndentedLines(label, cols, style) {
|
|
@@ -144,8 +145,7 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
|
|
|
144
145
|
const HOME_EVENT_FETCH_CAP = 15;
|
|
145
146
|
function calendarSnapshot(cal, weekAheadInfo) {
|
|
146
147
|
const now = new Date();
|
|
147
|
-
const eventLines = cal
|
|
148
|
-
.upcoming({ days: 30 })
|
|
148
|
+
const eventLines = currentEvents(cal, now)
|
|
149
149
|
.slice(0, HOME_EVENT_FETCH_CAP)
|
|
150
150
|
.map((event) => renderEventBrief(toDisplayEvent(event), now));
|
|
151
151
|
if (!weekAheadInfo)
|
|
@@ -513,8 +513,8 @@ export const scheduleView = {
|
|
|
513
513
|
writePrivateIcs(out, ics);
|
|
514
514
|
state = { ...state, statusMessage: `${t().common.success}: ${path.resolve(out)}` };
|
|
515
515
|
}
|
|
516
|
-
catch {
|
|
517
|
-
state = { ...state, statusMessage:
|
|
516
|
+
catch (error) {
|
|
517
|
+
state = { ...state, statusMessage: safeMessage(error) };
|
|
518
518
|
}
|
|
519
519
|
return;
|
|
520
520
|
}
|
|
@@ -108,36 +108,9 @@ function safeHeaders(headers) {
|
|
|
108
108
|
}
|
|
109
109
|
return Object.fromEntries(result.entries());
|
|
110
110
|
}
|
|
111
|
-
function
|
|
112
|
-
const controller = new AbortController();
|
|
113
|
-
let didTimeout = false;
|
|
114
|
-
const onAbort = () => {
|
|
115
|
-
controller.abort(signal?.reason);
|
|
116
|
-
};
|
|
117
|
-
signal?.addEventListener('abort', onAbort, { once: true });
|
|
118
|
-
if (signal?.aborted)
|
|
119
|
-
onAbort();
|
|
120
|
-
const timer = setTimeout(() => {
|
|
121
|
-
didTimeout = true;
|
|
122
|
-
controller.abort();
|
|
123
|
-
}, timeoutMs);
|
|
124
|
-
timer.unref();
|
|
125
|
-
return {
|
|
126
|
-
signal: controller.signal,
|
|
127
|
-
cleanup() {
|
|
128
|
-
clearTimeout(timer);
|
|
129
|
-
signal?.removeEventListener('abort', onAbort);
|
|
130
|
-
},
|
|
131
|
-
timedOut: () => didTimeout,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
function safeFetchError(error, stage, didTimeout) {
|
|
111
|
+
function safeFetchError(error, stage) {
|
|
135
112
|
if (error instanceof AuthError)
|
|
136
113
|
return error;
|
|
137
|
-
if (didTimeout)
|
|
138
|
-
return new AuthError('TIMEOUT', stage, 'The campus service request timed out.', {
|
|
139
|
-
retryable: true,
|
|
140
|
-
});
|
|
141
114
|
if (typeof error === 'object' && error !== null && Reflect.get(error, 'name') === 'AbortError') {
|
|
142
115
|
return new DOMException('The campus service request was aborted.', 'AbortError');
|
|
143
116
|
}
|
|
@@ -169,20 +142,24 @@ export function createCampusCookieSession(options = {}) {
|
|
|
169
142
|
if (init.method && init.method !== 'GET' && init.method !== 'POST') {
|
|
170
143
|
throw new AuthError('UNTRUSTED_URL', stage, 'Only read and login requests are allowed.');
|
|
171
144
|
}
|
|
172
|
-
const
|
|
145
|
+
const timeout = new AbortController();
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
timeout.abort(new AuthError('TIMEOUT', stage, 'The campus service request timed out.', {
|
|
148
|
+
retryable: true,
|
|
149
|
+
}));
|
|
150
|
+
}, timeoutMs).unref();
|
|
151
|
+
// Keep the timer running after the headers: the caller still has to read the body.
|
|
152
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeout.signal]) : timeout.signal;
|
|
173
153
|
try {
|
|
174
154
|
return await cookieFetch(url, {
|
|
175
155
|
...init,
|
|
176
156
|
headers: safeHeaders(init.headers),
|
|
177
|
-
signal
|
|
157
|
+
signal,
|
|
178
158
|
maxRedirect: 8,
|
|
179
159
|
});
|
|
180
160
|
}
|
|
181
161
|
catch (error) {
|
|
182
|
-
throw safeFetchError(error, stage
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
controlled.cleanup();
|
|
162
|
+
throw safeFetchError(error, stage);
|
|
186
163
|
}
|
|
187
164
|
}
|
|
188
165
|
async function timetableTransport(url, init) {
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { main } from './main.js';
|
|
3
|
-
import {
|
|
3
|
+
import { loadCalendarOrCache, renderEventsTable, serializeEvents, toDisplayEvent, yearHeatmap, } from './features/calendar.js';
|
|
4
4
|
import { renderHeatmap } from './features/calendar-heatmap.js';
|
|
5
|
+
import { currentEvents, dayRange, monthRange, weekRange } from './features/calendar-query.js';
|
|
5
6
|
import { checkServices, countServiceHealth, hasServiceFailures, renderServiceStatusTable, serializeServiceStatus, } from './features/status.js';
|
|
6
7
|
import { pickIcon } from './core/icons.js';
|
|
7
8
|
import { applyColorModePreference } from './config/preferences.js';
|
|
@@ -261,45 +262,34 @@ async function runEventsCommand(flags) {
|
|
|
261
262
|
console.error(chalk.red(t().cli.invalidNext));
|
|
262
263
|
process.exit(1);
|
|
263
264
|
}
|
|
265
|
+
const { calendar, stale } = await loadCalendarOrCache();
|
|
266
|
+
if (stale)
|
|
267
|
+
console.error(chalk.yellow(t().calendar.stale));
|
|
268
|
+
const now = new Date();
|
|
264
269
|
if (flags.has('--heatmap')) {
|
|
265
|
-
const buckets =
|
|
270
|
+
const buckets = yearHeatmap(calendar, now);
|
|
266
271
|
if (flags.has('--json')) {
|
|
267
272
|
process.stdout.write(JSON.stringify(buckets, null, 2) + '\n');
|
|
268
273
|
}
|
|
269
274
|
else {
|
|
270
275
|
const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
|
|
271
|
-
|
|
276
|
+
const cols = terminalWidth();
|
|
277
|
+
console.log(renderHeatmap(buckets, now, { color: useColor, ...(cols === undefined ? {} : { cols }) }));
|
|
272
278
|
}
|
|
273
279
|
return;
|
|
274
280
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
else if (flags.has('--month')) {
|
|
284
|
-
const r = monthRange(now0);
|
|
285
|
-
events = await fetchInRange(r.start, r.end);
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
events = await fetchEvents();
|
|
289
|
-
}
|
|
281
|
+
const range = flags.has('--today')
|
|
282
|
+
? dayRange(now)
|
|
283
|
+
: flags.has('--week')
|
|
284
|
+
? weekRange(now)
|
|
285
|
+
: flags.has('--month')
|
|
286
|
+
? monthRange(now)
|
|
287
|
+
: undefined;
|
|
288
|
+
let events = (range ? calendar.inRange(range.start, range.end) : currentEvents(calendar, now)).map(toDisplayEvent);
|
|
290
289
|
if (searchFlag) {
|
|
291
290
|
const q = searchFlag.slice('--search='.length).toLowerCase();
|
|
292
291
|
events = events.filter((e) => `${e.title} ${e.location}`.toLowerCase().includes(q));
|
|
293
292
|
}
|
|
294
|
-
if (flags.has('--today')) {
|
|
295
|
-
const now = new Date();
|
|
296
|
-
events = events.filter((e) => {
|
|
297
|
-
const d = e.startDate;
|
|
298
|
-
return (d.getFullYear() === now.getFullYear() &&
|
|
299
|
-
d.getMonth() === now.getMonth() &&
|
|
300
|
-
d.getDate() === now.getDate());
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
293
|
if (next !== undefined)
|
|
304
294
|
events = events.slice(0, next);
|
|
305
295
|
if (flags.has('--json')) {
|
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { pickIcon } from '../core/icons.js';
|
|
3
3
|
import { space, type } from '../core/theme.js';
|
|
4
4
|
import { t, getCurrentLanguage } from '../i18n/index.js';
|
|
5
|
+
import { visualWidth } from '../core/text.js';
|
|
5
6
|
function parseBucketDate(date) {
|
|
6
7
|
const parts = date.split('-').map(Number);
|
|
7
8
|
const y = parts[0] ?? 0;
|
|
@@ -77,7 +78,7 @@ export function renderHeatmap(buckets, today, options) {
|
|
|
77
78
|
timeZone: 'UTC',
|
|
78
79
|
});
|
|
79
80
|
const cellsWidth = numCols * cellWidth;
|
|
80
|
-
|
|
81
|
+
let monthLine = '';
|
|
81
82
|
let prevMonth = -1;
|
|
82
83
|
for (let col = 0; col < numCols; col++) {
|
|
83
84
|
let labelDate = null;
|
|
@@ -93,14 +94,15 @@ export function renderHeatmap(buckets, today, options) {
|
|
|
93
94
|
const month = labelDate.getUTCMonth();
|
|
94
95
|
if (month !== prevMonth) {
|
|
95
96
|
prevMonth = month;
|
|
96
|
-
const label = monthFmt.format(labelDate);
|
|
97
|
+
const label = monthFmt.format(labelDate);
|
|
97
98
|
const start = col * cellWidth;
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
const used = visualWidth(monthLine);
|
|
100
|
+
if (start >= used + (used > 0 ? 1 : 0) && start + visualWidth(label) <= cellsWidth) {
|
|
101
|
+
monthLine += ' '.repeat(start - used) + label;
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
|
-
const monthLabelLine = space.indent + weekdayLabel +
|
|
105
|
+
const monthLabelLine = space.indent + weekdayLabel + monthLine;
|
|
104
106
|
const weekdayNames = [
|
|
105
107
|
trans.timetable.weekdayMon.slice(0, 2),
|
|
106
108
|
' ',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const UPCOMING_DAYS = 30;
|
|
1
2
|
export function weekRange(now) {
|
|
2
3
|
const start = new Date(now);
|
|
3
4
|
start.setHours(0, 0, 0, 0);
|
|
@@ -7,6 +8,24 @@ export function weekRange(now) {
|
|
|
7
8
|
end.setDate(end.getDate() + 7);
|
|
8
9
|
return { start, end };
|
|
9
10
|
}
|
|
11
|
+
export function dayRange(now) {
|
|
12
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
13
|
+
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
14
|
+
return { start, end };
|
|
15
|
+
}
|
|
16
|
+
export function currentEvents(calendar, now) {
|
|
17
|
+
const { start } = dayRange(now);
|
|
18
|
+
const end = new Date(now.getTime() + UPCOMING_DAYS * 86_400_000);
|
|
19
|
+
return calendar.inRange(start, end).filter((event) => {
|
|
20
|
+
if (event.start >= now)
|
|
21
|
+
return true;
|
|
22
|
+
const eventEnd = event.end ??
|
|
23
|
+
(event.isAllDay
|
|
24
|
+
? new Date(event.start.getFullYear(), event.start.getMonth(), event.start.getDate() + 1)
|
|
25
|
+
: event.start);
|
|
26
|
+
return eventEnd > now;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
10
29
|
export function monthRange(now) {
|
|
11
30
|
const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
12
31
|
const end = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0);
|
|
@@ -76,16 +76,19 @@ export function toDisplayEvent(e) {
|
|
|
76
76
|
uid: e.uid,
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
export async function
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
export async function loadCalendarOrCache(signal) {
|
|
80
|
+
try {
|
|
81
|
+
return { calendar: await loadCalendarOrThrow(signal), stale: false };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const cached = peekCalendar();
|
|
85
|
+
if (!cached)
|
|
86
|
+
throw err;
|
|
87
|
+
return { calendar: cached, stale: true };
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
const start = addLocalDays(now, -365);
|
|
88
|
-
return (await loadCalendarOrThrow()).heatmap({ start, end: now, bucket: 'day' });
|
|
90
|
+
export function yearHeatmap(calendar, now) {
|
|
91
|
+
return calendar.heatmap({ start: addLocalDays(now, -365), end: now, bucket: 'day' });
|
|
89
92
|
}
|
|
90
93
|
export function serializeEvents(events) {
|
|
91
94
|
return events.map((event) => ({
|
|
@@ -6,7 +6,9 @@ import { runSecretInput, runTextInput } from '../core/components/text-input.js';
|
|
|
6
6
|
import { AuthError } from '../auth/errors.js';
|
|
7
7
|
import { loginWithStudentPassword, restoreNbtSession, } from '../auth/nbt-auth.js';
|
|
8
8
|
import { createSessionStore } from '../auth/session-store.js';
|
|
9
|
+
import { isoDayDifference, parseLocalMonday } from '../core/calendar-day.js';
|
|
9
10
|
import { clearScheduleCache, termKey } from './schedule-store.js';
|
|
11
|
+
import { sanitizeTerminalLine } from '../core/text.js';
|
|
10
12
|
import { fmt, t } from '../i18n/index.js';
|
|
11
13
|
import { sanitizeAcademicTerm, sanitizeTimetable } from './timetable-sanitize.js';
|
|
12
14
|
export const JWXT_ORIGIN = 'https://jwxt-443.webvpn.nbt.edu.cn';
|
|
@@ -43,12 +45,48 @@ function displaySemesterLabel(term) {
|
|
|
43
45
|
? fmt(t().timetable.semesterNumber, { number: term.semesterLabel })
|
|
44
46
|
: term.semesterLabel;
|
|
45
47
|
}
|
|
48
|
+
class WeekOneError extends Error {
|
|
49
|
+
reason;
|
|
50
|
+
constructor(reason) {
|
|
51
|
+
super(`--week-one is ${reason}.`);
|
|
52
|
+
this.reason = reason;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function assertWeekOne(weekOneMonday, calendarDays = []) {
|
|
56
|
+
try {
|
|
57
|
+
parseLocalMonday(weekOneMonday);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new WeekOneError('invalid');
|
|
61
|
+
}
|
|
62
|
+
if (calendarDays.some((day) => isoDayDifference(weekOneMonday, day.date) !== (day.week - 1) * 7 + day.weekday - 1)) {
|
|
63
|
+
throw new WeekOneError('conflict');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
class IcsWriteError extends Error {
|
|
67
|
+
file;
|
|
68
|
+
reason;
|
|
69
|
+
constructor(file, reason) {
|
|
70
|
+
super(`Could not write ${file}.`);
|
|
71
|
+
this.file = file;
|
|
72
|
+
this.reason = reason;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
46
75
|
export function isSessionExpired(error) {
|
|
47
76
|
return ((error instanceof AuthError && error.code === 'SESSION_EXPIRED') ||
|
|
48
77
|
(error instanceof TimetableError && error.code === 'SESSION_EXPIRED'));
|
|
49
78
|
}
|
|
50
79
|
export function safeMessage(error) {
|
|
51
80
|
const trans = t().timetable;
|
|
81
|
+
if (error instanceof IcsWriteError) {
|
|
82
|
+
return fmt(trans.writeFailed, {
|
|
83
|
+
file: sanitizeTerminalLine(error.file),
|
|
84
|
+
reason: sanitizeTerminalLine(error.reason),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (error instanceof WeekOneError) {
|
|
88
|
+
return error.reason === 'invalid' ? trans.invalidWeekOne : trans.weekOneConflict;
|
|
89
|
+
}
|
|
52
90
|
if (error instanceof AuthError) {
|
|
53
91
|
switch (error.code) {
|
|
54
92
|
case 'INVALID_CREDENTIALS':
|
|
@@ -110,25 +148,28 @@ export function safeMessage(error) {
|
|
|
110
148
|
}
|
|
111
149
|
return trans.genericError;
|
|
112
150
|
}
|
|
151
|
+
class PromptCancelledError extends Error {
|
|
152
|
+
}
|
|
153
|
+
function answered(value) {
|
|
154
|
+
if (value === null)
|
|
155
|
+
throw new PromptCancelledError();
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
113
158
|
async function interactiveLogin(isInteractive) {
|
|
114
159
|
const trans = t().timetable;
|
|
115
160
|
if (!isInteractive) {
|
|
116
161
|
throw new AuthError('INVALID_CREDENTIALS', 'credentials', 'Interactive login requires a terminal.');
|
|
117
162
|
}
|
|
118
|
-
const username = await runTextInput({
|
|
163
|
+
const username = answered(await runTextInput({
|
|
119
164
|
message: trans.studentId,
|
|
120
165
|
placeholder: trans.studentIdHint,
|
|
121
166
|
allowEmpty: false,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
throw new AuthError('INVALID_CREDENTIALS', 'credentials', 'Student id is required.');
|
|
125
|
-
const password = await runSecretInput({
|
|
167
|
+
}));
|
|
168
|
+
const password = answered(await runSecretInput({
|
|
126
169
|
message: trans.password,
|
|
127
170
|
placeholder: trans.passwordHint,
|
|
128
171
|
allowEmpty: false,
|
|
129
|
-
});
|
|
130
|
-
if (!password)
|
|
131
|
-
throw new AuthError('INVALID_CREDENTIALS', 'credentials', 'Password is required.');
|
|
172
|
+
}));
|
|
132
173
|
return loginWithStudentPassword(username, password);
|
|
133
174
|
}
|
|
134
175
|
export async function withAuthenticatedSession(operation, options) {
|
|
@@ -200,6 +241,11 @@ export function writePrivateIcs(filePath, contents) {
|
|
|
200
241
|
/* Best effort on non-POSIX filesystems. */
|
|
201
242
|
}
|
|
202
243
|
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
246
|
+
// Node appends the syscall and the temporary path; keep only "CODE: description".
|
|
247
|
+
throw new IcsWriteError(resolved, /^(.+?), \w+ '/.exec(message)?.[1] ?? message);
|
|
248
|
+
}
|
|
203
249
|
finally {
|
|
204
250
|
try {
|
|
205
251
|
fs.unlinkSync(temporaryPath);
|
|
@@ -219,12 +265,13 @@ async function resolveWeekOneMonday(explicitValue, hasAuthoritativeDates, isInte
|
|
|
219
265
|
return explicitValue;
|
|
220
266
|
if (!isInteractive)
|
|
221
267
|
return undefined;
|
|
222
|
-
const value = await runTextInput({
|
|
268
|
+
const value = answered(await runTextInput({
|
|
223
269
|
message: t().timetable.weekOne,
|
|
224
270
|
placeholder: t().timetable.weekOneHint,
|
|
225
271
|
allowEmpty: false,
|
|
226
|
-
});
|
|
227
|
-
|
|
272
|
+
})).trim();
|
|
273
|
+
assertWeekOne(value);
|
|
274
|
+
return value;
|
|
228
275
|
}
|
|
229
276
|
export async function runStudentTimetableCommand(subcommandValue, options) {
|
|
230
277
|
const subcommand = (subcommandValue ?? 'export').toLowerCase();
|
|
@@ -249,7 +296,10 @@ export async function runStudentTimetableCommand(subcommandValue, options) {
|
|
|
249
296
|
stderr.write(`${fmt(trans.invalidOption, { flag: invalidFlag })}\n`);
|
|
250
297
|
return 1;
|
|
251
298
|
}
|
|
299
|
+
const weekOneFlag = flagValue(options.flags, '--week-one=');
|
|
252
300
|
try {
|
|
301
|
+
if (weekOneFlag !== undefined)
|
|
302
|
+
assertWeekOne(weekOneFlag);
|
|
253
303
|
if (subcommand === 'logout') {
|
|
254
304
|
store.clear();
|
|
255
305
|
clearScheduleCache();
|
|
@@ -291,7 +341,9 @@ export async function runStudentTimetableCommand(subcommandValue, options) {
|
|
|
291
341
|
const output = outputFlag === undefined || outputFlag === ''
|
|
292
342
|
? `timetable-${termKey(selected)}.ics`
|
|
293
343
|
: outputFlag;
|
|
294
|
-
const weekOneMonday = await resolveWeekOneMonday(
|
|
344
|
+
const weekOneMonday = await resolveWeekOneMonday(weekOneFlag, timetable.calendarDays.length > 0, isInteractive);
|
|
345
|
+
if (weekOneMonday !== undefined)
|
|
346
|
+
assertWeekOne(weekOneMonday, timetable.calendarDays);
|
|
295
347
|
const ics = timetableToIcs(timetable, {
|
|
296
348
|
...(weekOneMonday === undefined ? {} : { weekOneMonday }),
|
|
297
349
|
calendarName: fmt(trans.calendarName, {
|
|
@@ -315,6 +367,8 @@ export async function runStudentTimetableCommand(subcommandValue, options) {
|
|
|
315
367
|
}, { oneShot, isInteractive, store, stderr });
|
|
316
368
|
}
|
|
317
369
|
catch (error) {
|
|
370
|
+
if (error instanceof PromptCancelledError)
|
|
371
|
+
return 130;
|
|
318
372
|
if (!isInteractive && error instanceof AuthError && error.code === 'INVALID_CREDENTIALS') {
|
|
319
373
|
stderr.write(`${trans.noSession}\n`);
|
|
320
374
|
return 2;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"menu": {
|
|
31
31
|
"events": "Events",
|
|
32
|
-
"eventsDesc": "",
|
|
32
|
+
"eventsDesc": "Upcoming activities",
|
|
33
33
|
"docs": "Docs",
|
|
34
34
|
"docsDesc": "Knowledge base",
|
|
35
35
|
"status": "Status",
|
|
@@ -239,6 +239,8 @@
|
|
|
239
239
|
"loginChanged": "The school login page changed; credentials were not submitted.",
|
|
240
240
|
"unexpectedResponse": "A valid JWXT session could not be confirmed after login.",
|
|
241
241
|
"missingDates": "JWXT returned no calendar dates. Provide --week-one=YYYY-MM-DD.",
|
|
242
|
+
"invalidWeekOne": "The first teaching week must start on a Monday, written as YYYY-MM-DD.",
|
|
243
|
+
"weekOneConflict": "--week-one does not match the dates JWXT provided; drop the option to use JWXT's dates.",
|
|
242
244
|
"missingPeriod": "JWXT returned no usable period times, so export cannot continue safely.",
|
|
243
245
|
"termMismatch": "JWXT returned a different academic term. Try again.",
|
|
244
246
|
"invalidData": "The timetable response format is not currently recognized.",
|
|
@@ -246,6 +248,7 @@
|
|
|
246
248
|
"noTerms": "JWXT returned no available terms.",
|
|
247
249
|
"currentTermUnknown": "JWXT did not identify one current term. Choose one with --term=year:code.",
|
|
248
250
|
"genericError": "The timetable operation failed.",
|
|
251
|
+
"writeFailed": "Could not write {file}: {reason}",
|
|
249
252
|
"hubToday": "Today",
|
|
250
253
|
"hubWeek": "This week",
|
|
251
254
|
"hubSwitchTerm": "Switch term",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"menu": {
|
|
31
31
|
"events": "活动",
|
|
32
|
-
"eventsDesc": "",
|
|
32
|
+
"eventsDesc": "近期活动安排",
|
|
33
33
|
"docs": "文档",
|
|
34
34
|
"docsDesc": "知识库",
|
|
35
35
|
"status": "状态",
|
|
@@ -239,6 +239,8 @@
|
|
|
239
239
|
"loginChanged": "学校登录页结构已经变化,当前版本未提交凭据。",
|
|
240
240
|
"unexpectedResponse": "登录后未能确认有效的教务会话。",
|
|
241
241
|
"missingDates": "教务响应没有可用日期,请用 --week-one=YYYY-MM-DD 指定第一周周一。",
|
|
242
|
+
"invalidWeekOne": "第一教学周的起始日期必须是周一,并写成 YYYY-MM-DD 格式。",
|
|
243
|
+
"weekOneConflict": "--week-one 与教务提供的日期对不上;去掉这个选项即可直接使用教务日期。",
|
|
242
244
|
"missingPeriod": "教务响应缺少课程节次时间,无法安全导出。",
|
|
243
245
|
"termMismatch": "教务系统返回了不同学期,请重试。",
|
|
244
246
|
"invalidData": "课表数据格式暂时无法识别。",
|
|
@@ -246,6 +248,7 @@
|
|
|
246
248
|
"noTerms": "教务系统没有返回可用学期。",
|
|
247
249
|
"currentTermUnknown": "教务没有标出唯一的当前学期;请用 --term=学年:代码 指定。",
|
|
248
250
|
"genericError": "课表操作失败。",
|
|
251
|
+
"writeFailed": "无法写入 {file}:{reason}",
|
|
249
252
|
"hubToday": "今日",
|
|
250
253
|
"hubWeek": "本周",
|
|
251
254
|
"hubSwitchTerm": "切换学期",
|