@nbtca/prompt 1.5.11 → 1.5.13

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.
@@ -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'), '');
@@ -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 { loadCalendarOrThrow, toDisplayEvent, exportEventIcs } from '../../features/calendar.js';
9
- import { weekRange, monthRange, filterEvents } from '../../features/calendar-query.js';
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 upcoming = calendar ? calendar.upcoming({ days: 30 }) : [];
58
- const nextEvent = upcoming[0];
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 loadedCalendar = await loadCalendarOrThrow(ctx.signal);
98
+ const loaded = await loadCalendarOrCache(ctx.signal);
102
99
  if (ctx.signal?.aborted)
103
100
  return;
104
- calendar = loadedCalendar;
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.upcoming({ days: 30 }), ctx);
154
+ showList(t().menu.events, currentEvents(calendar, now), ctx);
157
155
  return;
158
156
  }
159
157
  if (result.selected === 'week') {
@@ -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: t().timetable.genericError };
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 abortSignal(signal, timeoutMs) {
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 controlled = abortSignal(init.signal, timeoutMs);
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: controlled.signal,
157
+ signal,
178
158
  maxRedirect: 8,
179
159
  });
180
160
  }
181
161
  catch (error) {
182
- throw safeFetchError(error, stage, controlled.timedOut());
183
- }
184
- finally {
185
- controlled.cleanup();
162
+ throw safeFetchError(error, stage);
186
163
  }
187
164
  }
188
165
  async function timetableTransport(url, init) {
@@ -195,6 +172,7 @@ export function createCampusCookieSession(options = {}) {
195
172
  if (finalUrl.hostname.toLowerCase() !== JWXT_HOST ||
196
173
  finalUrl.pathname.includes('/authserver/login') ||
197
174
  finalUrl.pathname.includes('/users/sign_in') ||
175
+ finalUrl.pathname === '/jwglxt/xtgl/login_slogin.html' ||
198
176
  finalUrl.pathname === '/vpn_key/update')
199
177
  throw new SessionExpiredError();
200
178
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { main } from './main.js';
3
- import { fetchEvents, fetchHeatmapBuckets, renderEventsTable, serializeEvents, } from './features/calendar.js';
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 = await fetchHeatmapBuckets();
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
- console.log(renderHeatmap(buckets, new Date(), { color: useColor }));
276
+ const cols = terminalWidth();
277
+ console.log(renderHeatmap(buckets, now, { color: useColor, ...(cols === undefined ? {} : { cols }) }));
272
278
  }
273
279
  return;
274
280
  }
275
- const { weekRange, monthRange } = await import('./features/calendar-query.js');
276
- const { fetchInRange } = await import('./features/calendar.js');
277
- const now0 = new Date();
278
- let events;
279
- if (flags.has('--week')) {
280
- const r = weekRange(now0);
281
- events = await fetchInRange(r.start, r.end);
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
- const monthChars = new Array(cellsWidth).fill(' ');
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); // e.g. "Jun"
97
+ const label = monthFmt.format(labelDate);
97
98
  const start = col * cellWidth;
98
- for (let i = 0; i < label.length && start + i < cellsWidth; i++) {
99
- monthChars[start + i] = label[i] ?? ' ';
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 + monthChars.join('');
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 fetchEvents() {
80
- return (await loadCalendarOrThrow()).upcoming({ days: 30 }).map(toDisplayEvent);
81
- }
82
- export async function fetchInRange(start, end) {
83
- return (await loadCalendarOrThrow()).inRange(start, end).map(toDisplayEvent);
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 async function fetchHeatmapBuckets() {
86
- const now = new Date();
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
- if (!username)
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
- return value === null || value === '' ? undefined : value;
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(flagValue(options.flags, '--week-one='), timetable.calendarDays.length > 0, isInteractive);
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": "切换学期",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbtca/prompt",
3
- "version": "1.5.11",
3
+ "version": "1.5.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -47,7 +47,7 @@
47
47
  ],
48
48
  "dependencies": {
49
49
  "@nbtca/docs": "^0.3.1",
50
- "@nbtca/nbtcal": "^0.4.1",
50
+ "@nbtca/nbtcal": "^0.4.2",
51
51
  "chalk": "^5.6.2",
52
52
  "cheerio": "1.0.0",
53
53
  "fetch-cookie": "^3.2.0",