@nbtca/prompt 1.4.2 → 1.5.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.
Files changed (67) hide show
  1. package/README.md +27 -58
  2. package/SECURITY.md +16 -45
  3. package/dist/app/app.js +53 -55
  4. package/dist/app/chrome.js +67 -50
  5. package/dist/app/fields/list-field.js +12 -25
  6. package/dist/app/fields/text-field.js +3 -8
  7. package/dist/app/frame.js +2 -21
  8. package/dist/app/keys.js +10 -2
  9. package/dist/app/views/docs-render.js +31 -24
  10. package/dist/app/views/docs.js +211 -60
  11. package/dist/app/views/events-render.js +19 -26
  12. package/dist/app/views/events.js +44 -31
  13. package/dist/app/views/home.js +28 -30
  14. package/dist/app/views/schedule-grid-cursor.js +9 -18
  15. package/dist/app/views/schedule-render.js +47 -71
  16. package/dist/app/views/schedule.js +158 -81
  17. package/dist/app/views/settings-render.js +8 -19
  18. package/dist/app/views/settings.js +92 -17
  19. package/dist/auth/cookie-transport.js +31 -32
  20. package/dist/auth/errors.js +3 -1
  21. package/dist/auth/nbt-auth.js +42 -25
  22. package/dist/auth/session-store.js +17 -9
  23. package/dist/config/data.js +9 -11
  24. package/dist/config/preferences.js +14 -7
  25. package/dist/core/calendar-day.js +37 -0
  26. package/dist/core/capabilities.js +6 -3
  27. package/dist/core/components/confirm.js +9 -8
  28. package/dist/core/components/menu.js +41 -16
  29. package/dist/core/components/messages.js +12 -4
  30. package/dist/core/components/painter.js +3 -1
  31. package/dist/core/components/spinner.js +17 -6
  32. package/dist/core/components/text-input.js +24 -18
  33. package/dist/core/icons.js +2 -2
  34. package/dist/core/logo.js +23 -5
  35. package/dist/core/motion.js +25 -19
  36. package/dist/core/text.js +182 -69
  37. package/dist/core/theme.js +0 -28
  38. package/dist/core/transitions.js +2 -2
  39. package/dist/core/ui.js +15 -13
  40. package/dist/core/vim-keys.js +9 -15
  41. package/dist/features/about.js +23 -0
  42. package/dist/features/calendar-heatmap.js +16 -40
  43. package/dist/features/calendar-query.js +1 -2
  44. package/dist/features/calendar.js +12 -185
  45. package/dist/features/docs.js +436 -275
  46. package/dist/features/schedule-render.js +65 -101
  47. package/dist/features/schedule-store.js +51 -9
  48. package/dist/features/schedule-view.js +46 -213
  49. package/dist/features/status.js +44 -56
  50. package/dist/features/student-timetable.js +73 -95
  51. package/dist/features/theme.js +6 -2
  52. package/dist/features/timetable-sanitize.js +40 -0
  53. package/dist/features/update.js +9 -27
  54. package/dist/i18n/index.js +83 -19
  55. package/dist/i18n/locales/en.json +1 -1
  56. package/dist/i18n/locales/zh.json +1 -1
  57. package/dist/index.js +83 -58
  58. package/dist/logo/ca-dotmatrix.txt +16 -18
  59. package/dist/main.js +7 -48
  60. package/package.json +27 -18
  61. package/bin/nbtca-welcome.js +0 -2
  62. package/dist/core/components/screen.js +0 -18
  63. package/dist/core/menu.js +0 -68
  64. package/dist/features/links.js +0 -36
  65. package/dist/features/schedule-query.js +0 -47
  66. package/dist/features/settings.js +0 -127
  67. package/dist/logo/ca-logo.png +0 -0
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
+ #!/usr/bin/env node
1
2
  import chalk from 'chalk';
2
3
  import open from 'open';
3
4
  import { main } from './main.js';
4
- import { fetchEvents, fetchHeatmapBuckets, renderEventsTable, serializeEvents } from './features/calendar.js';
5
+ import { fetchEvents, fetchHeatmapBuckets, renderEventsTable, serializeEvents, } from './features/calendar.js';
5
6
  import { renderHeatmap } from './features/calendar-heatmap.js';
6
- import { checkServices, countServiceHealth, hasServiceFailures, renderServiceStatusTable, serializeServiceStatus } from './features/status.js';
7
+ import { checkServices, countServiceHealth, hasServiceFailures, renderServiceStatusTable, serializeServiceStatus, } from './features/status.js';
7
8
  import { pickIcon } from './core/icons.js';
8
9
  import { applyColorModePreference } from './config/preferences.js';
9
10
  import { openDocsInBrowser } from './features/docs.js';
@@ -13,6 +14,7 @@ import { clearScreen, handleGracefulExit } from './core/ui.js';
13
14
  import { APP_INFO, URLS } from './config/data.js';
14
15
  import { runUpdateCheck } from './features/update.js';
15
16
  import { runStudentTimetableCommand } from './features/student-timetable.js';
17
+ import { showAbout } from './features/about.js';
16
18
  const ACTION_ALIASES = {
17
19
  events: 'events',
18
20
  event: 'events',
@@ -32,13 +34,33 @@ const URL_ACTIONS = {
32
34
  repair: URLS.repair,
33
35
  website: URLS.homepage,
34
36
  github: URLS.github,
35
- roadmap: URLS.roadmap
37
+ roadmap: URLS.roadmap,
36
38
  };
37
39
  const KNOWN_FLAGS = new Set([
38
- '--help', '--version', '--open', '--json', '--plain', '--no-logo', '--watch', '--today', '--heatmap',
39
- '--week', '--month', '--one-shot', '--no-save',
40
+ '--help',
41
+ '--version',
42
+ '--open',
43
+ '--json',
44
+ '--plain',
45
+ '--no-logo',
46
+ '--watch',
47
+ '--today',
48
+ '--heatmap',
49
+ '--week',
50
+ '--month',
51
+ '--one-shot',
52
+ '--no-save',
40
53
  ]);
41
- const KNOWN_FLAG_PREFIXES = ['--interval=', '--timeout=', '--retries=', '--next=', '--search=', '--term=', '--output=', '--week-one='];
54
+ const KNOWN_FLAG_PREFIXES = [
55
+ '--interval=',
56
+ '--timeout=',
57
+ '--retries=',
58
+ '--next=',
59
+ '--search=',
60
+ '--term=',
61
+ '--output=',
62
+ '--week-one=',
63
+ ];
42
64
  const STATUS_WATCH_INTERVAL_MIN = 3;
43
65
  const STATUS_WATCH_INTERVAL_MAX = 300;
44
66
  const STATUS_TIMEOUT_MIN = 1000;
@@ -57,13 +79,16 @@ function parseArgs(argv) {
57
79
  }
58
80
  }
59
81
  return {
60
- command: positionals[0]?.toLowerCase(),
82
+ ...(positionals[0] === undefined ? {} : { command: positionals[0].toLowerCase() }),
61
83
  args: positionals.slice(1),
62
- flags
84
+ flags,
63
85
  };
64
86
  }
87
+ function isTty(value) {
88
+ return value === true;
89
+ }
65
90
  function hasInteractiveTerminal() {
66
- return !!process.stdin.isTTY && !!process.stdout.isTTY;
91
+ return isTty(process.stdin.isTTY) && isTty(process.stdout.isTTY);
67
92
  }
68
93
  function getAllowedFlagsFor(command) {
69
94
  const allowed = new Set(['--help', '--plain']);
@@ -122,27 +147,27 @@ function getAllowedFlagPrefixesFor(command) {
122
147
  return [];
123
148
  }
124
149
  function validateFlags(command, flags) {
125
- const unknown = Array.from(flags).filter((flag) => {
150
+ const unknownFlag = Array.from(flags).find((flag) => {
126
151
  if (KNOWN_FLAGS.has(flag))
127
152
  return false;
128
153
  return !KNOWN_FLAG_PREFIXES.some((prefix) => flag.startsWith(prefix));
129
154
  });
130
- if (unknown.length > 0) {
155
+ if (unknownFlag) {
131
156
  const trans0 = t();
132
- console.error(chalk.red(fmt(trans0.cli.unknownFlag, { flag: unknown[0] })));
157
+ console.error(chalk.red(fmt(trans0.cli.unknownFlag, { flag: unknownFlag })));
133
158
  console.error(chalk.dim(trans0.cli.unknownFlagHint));
134
159
  process.exit(1);
135
160
  }
136
161
  const allowed = getAllowedFlagsFor(command);
137
162
  const allowedPrefixes = getAllowedFlagPrefixesFor(command);
138
- const disallowed = Array.from(flags).filter((flag) => {
163
+ const disallowedFlag = Array.from(flags).find((flag) => {
139
164
  if (allowed.has(flag))
140
165
  return false;
141
166
  return !allowedPrefixes.some((prefix) => flag.startsWith(prefix));
142
167
  });
143
- if (disallowed.length > 0) {
168
+ if (disallowedFlag) {
144
169
  const trans1 = t();
145
- console.error(chalk.red(fmt(trans1.cli.invalidFlag, { flag: disallowed[0] })));
170
+ console.error(chalk.red(fmt(trans1.cli.invalidFlag, { flag: disallowedFlag })));
146
171
  console.error(chalk.dim(trans1.cli.invalidFlagHint));
147
172
  process.exit(1);
148
173
  }
@@ -200,7 +225,7 @@ async function runEventsCommand(flags) {
200
225
  process.stdout.write(JSON.stringify(buckets, null, 2) + '\n');
201
226
  }
202
227
  else {
203
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
228
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
204
229
  console.log(renderHeatmap(buckets, new Date(), { color: useColor }));
205
230
  }
206
231
  return;
@@ -220,23 +245,23 @@ async function runEventsCommand(flags) {
220
245
  else {
221
246
  events = await fetchEvents();
222
247
  }
223
- const searchFlag = Array.from(flags).find(f => f.startsWith('--search='));
248
+ const searchFlag = Array.from(flags).find((f) => f.startsWith('--search='));
224
249
  if (searchFlag) {
225
250
  const q = searchFlag.slice('--search='.length).toLowerCase();
226
- events = events.filter(e => `${e.title} ${e.location}`.toLowerCase().includes(q));
251
+ events = events.filter((e) => `${e.title} ${e.location}`.toLowerCase().includes(q));
227
252
  }
228
253
  if (flags.has('--today')) {
229
254
  const now = new Date();
230
- events = events.filter(e => {
255
+ events = events.filter((e) => {
231
256
  const d = e.startDate;
232
- return d.getFullYear() === now.getFullYear() &&
257
+ return (d.getFullYear() === now.getFullYear() &&
233
258
  d.getMonth() === now.getMonth() &&
234
- d.getDate() === now.getDate();
259
+ d.getDate() === now.getDate());
235
260
  });
236
261
  }
237
- const nextFlag = Array.from(flags).find(f => f.startsWith('--next='));
262
+ const nextFlag = Array.from(flags).find((f) => f.startsWith('--next='));
238
263
  if (nextFlag) {
239
- const n = Number.parseInt(nextFlag.split('=')[1] || '', 10);
264
+ const n = Number.parseInt(nextFlag.slice('--next='.length), 10);
240
265
  if (!Number.isInteger(n) || n < 1) {
241
266
  console.error(chalk.red(t().cli.invalidNext));
242
267
  process.exit(1);
@@ -247,7 +272,7 @@ async function runEventsCommand(flags) {
247
272
  process.stdout.write(JSON.stringify(serializeEvents(events), null, 2) + '\n');
248
273
  return;
249
274
  }
250
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
275
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
251
276
  console.log(renderEventsTable(events, { color: useColor }));
252
277
  }
253
278
  async function runStatusCommand(flags) {
@@ -256,14 +281,20 @@ async function runStatusCommand(flags) {
256
281
  const intervalFlag = Array.from(flags).find((flag) => flag.startsWith('--interval='));
257
282
  const timeoutFlag = Array.from(flags).find((flag) => flag.startsWith('--timeout='));
258
283
  const retriesFlag = Array.from(flags).find((flag) => flag.startsWith('--retries='));
259
- const intervalSeconds = intervalFlag ? Number.parseInt(intervalFlag.split('=')[1] || '', 10) : 10;
260
- const timeoutMs = timeoutFlag ? Number.parseInt(timeoutFlag.split('=')[1] || '', 10) : 6000;
261
- const retries = retriesFlag ? Number.parseInt(retriesFlag.split('=')[1] || '', 10) : 1;
284
+ const intervalSeconds = intervalFlag
285
+ ? Number.parseInt(intervalFlag.slice('--interval='.length), 10)
286
+ : 10;
287
+ const timeoutMs = timeoutFlag
288
+ ? Number.parseInt(timeoutFlag.slice('--timeout='.length), 10)
289
+ : 6000;
290
+ const retries = retriesFlag ? Number.parseInt(retriesFlag.slice('--retries='.length), 10) : 1;
262
291
  if (!watch && intervalFlag) {
263
292
  console.error(chalk.red(trans.status.intervalNeedsWatch));
264
293
  process.exit(1);
265
294
  }
266
- if (!Number.isInteger(timeoutMs) || timeoutMs < STATUS_TIMEOUT_MIN || timeoutMs > STATUS_TIMEOUT_MAX) {
295
+ if (!Number.isInteger(timeoutMs) ||
296
+ timeoutMs < STATUS_TIMEOUT_MIN ||
297
+ timeoutMs > STATUS_TIMEOUT_MAX) {
267
298
  console.error(chalk.red(fmt(trans.status.invalidTimeout, { min: STATUS_TIMEOUT_MIN, max: STATUS_TIMEOUT_MAX })));
268
299
  process.exit(1);
269
300
  }
@@ -275,8 +306,14 @@ async function runStatusCommand(flags) {
275
306
  console.error(chalk.red(trans.status.watchJsonConflict));
276
307
  process.exit(1);
277
308
  }
278
- if (watch && (!Number.isInteger(intervalSeconds) || intervalSeconds < STATUS_WATCH_INTERVAL_MIN || intervalSeconds > STATUS_WATCH_INTERVAL_MAX)) {
279
- console.error(chalk.red(fmt(trans.status.invalidInterval, { min: STATUS_WATCH_INTERVAL_MIN, max: STATUS_WATCH_INTERVAL_MAX })));
309
+ if (watch &&
310
+ (!Number.isInteger(intervalSeconds) ||
311
+ intervalSeconds < STATUS_WATCH_INTERVAL_MIN ||
312
+ intervalSeconds > STATUS_WATCH_INTERVAL_MAX)) {
313
+ console.error(chalk.red(fmt(trans.status.invalidInterval, {
314
+ min: STATUS_WATCH_INTERVAL_MIN,
315
+ max: STATUS_WATCH_INTERVAL_MAX,
316
+ })));
280
317
  process.exit(1);
281
318
  }
282
319
  if (watch && !hasInteractiveTerminal()) {
@@ -284,12 +321,14 @@ async function runStatusCommand(flags) {
284
321
  process.exit(1);
285
322
  }
286
323
  if (watch) {
287
- let stopped = false;
288
- const onSigint = () => { stopped = true; };
324
+ const stopController = new AbortController();
325
+ const onSigint = () => {
326
+ stopController.abort();
327
+ };
289
328
  process.once('SIGINT', onSigint);
290
329
  console.log(chalk.dim(`${fmt(trans.status.watchStarted, { seconds: intervalSeconds })} | ${trans.status.watchHint}`));
291
330
  try {
292
- while (!stopped) {
331
+ while (!stopController.signal.aborted) {
293
332
  const services = await checkServices({ timeoutMs, retries });
294
333
  const hasFailures = hasServiceFailures(services);
295
334
  const health = countServiceHealth(services);
@@ -297,7 +336,7 @@ async function runStatusCommand(flags) {
297
336
  console.log(chalk.bold(`${trans.status.watchUpdated}: ${new Date().toLocaleString()}`));
298
337
  console.log(chalk.dim(`${trans.status.up}: ${health.up} | ${trans.status.down}: ${health.down} | ${trans.status.watchHint}`));
299
338
  console.log();
300
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
339
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
301
340
  console.log(renderServiceStatusTable(services, { color: useColor }));
302
341
  if (hasFailures) {
303
342
  console.log(chalk.yellow(trans.status.summaryFail));
@@ -308,12 +347,12 @@ async function runStatusCommand(flags) {
308
347
  await new Promise((resolve) => {
309
348
  const stopWait = () => {
310
349
  clearTimeout(timer);
311
- process.removeListener('SIGINT', stopWait);
350
+ stopController.signal.removeEventListener('abort', stopWait);
312
351
  resolve();
313
352
  };
314
353
  const timer = setTimeout(stopWait, intervalSeconds * 1000);
315
- process.once('SIGINT', stopWait);
316
- if (stopped)
354
+ stopController.signal.addEventListener('abort', stopWait, { once: true });
355
+ if (stopController.signal.aborted)
317
356
  stopWait();
318
357
  });
319
358
  }
@@ -331,7 +370,7 @@ async function runStatusCommand(flags) {
331
370
  process.stdout.write(JSON.stringify(serializeServiceStatus(services), null, 2) + '\n');
332
371
  }
333
372
  else {
334
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
373
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
335
374
  console.log(renderServiceStatusTable(services, { color: useColor }));
336
375
  if (hasFailures) {
337
376
  console.error(chalk.yellow(t().status.summaryFail));
@@ -348,7 +387,10 @@ function maybeDisableColor(flags) {
348
387
  async function runCommandMode(argv) {
349
388
  const { command, args, flags } = parseArgs(argv);
350
389
  maybeDisableColor(flags);
351
- if (flags.has('--version') || command === '--version' || command === '-v' || command === 'version') {
390
+ if (flags.has('--version') ||
391
+ command === '--version' ||
392
+ command === '-v' ||
393
+ command === 'version') {
352
394
  console.log(APP_INFO.version);
353
395
  return;
354
396
  }
@@ -368,7 +410,7 @@ async function runCommandMode(argv) {
368
410
  return;
369
411
  }
370
412
  if (command === 'lang' || command === 'language') {
371
- const language = (args[0] || '').toLowerCase();
413
+ const language = (args[0] ?? '').toLowerCase();
372
414
  if (language !== 'zh' && language !== 'en') {
373
415
  console.error(chalk.red(t().cli.invalidLang));
374
416
  process.exit(1);
@@ -441,24 +483,7 @@ async function runCommandMode(argv) {
441
483
  return;
442
484
  }
443
485
  if (action === 'about') {
444
- const { note } = await import('./core/components/note.js');
445
- const { padEndV } = await import('./core/text.js');
446
- const pad = 12;
447
- const row = (label, value) => `${chalk.dim(padEndV(label, pad))}${value}`;
448
- const link = (label, url) => row(label, chalk.cyan(url));
449
- const trans = t();
450
- const content = [
451
- row(trans.about.project, APP_INFO.name),
452
- row(trans.about.version, `v${APP_INFO.version}`),
453
- row(trans.about.description, trans.about.descriptionText),
454
- '',
455
- link(trans.about.github, APP_INFO.repository),
456
- link(trans.about.website, URLS.homepage),
457
- link(trans.about.email, URLS.email),
458
- '',
459
- row(trans.about.license, `MIT | ${trans.about.author}: m1ngsama`),
460
- ].join('\n');
461
- note(content, trans.about.title);
486
+ showAbout();
462
487
  return;
463
488
  }
464
489
  const mappedUrl = URL_ACTIONS[action];
@@ -1,18 +1,16 @@
1
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⠶⠒⠛⠛⢛⠛⠛⠛⠒⠶⠤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
2
- ⠀⠀⠀⠀⠀⠀⠀⣠⡴⠚⠉⠀⢎⠔⠕⢃⣊⣐⣋⣚⣃⠛⠄⢀⣴⢉⠓⢦⣄⠀⠀⠀⠀⠀⠀⠀
3
- ⠀⠀⠀⠀⠀⣠⠞⢁⠂⠲⣀⡤⠖⠚⠉⠉⠀⠀⠀⠀⠉⠉⠓⠲⢬⣘⠰⠂⠈⠳⣄⠀⠀⠀⠀⠀
4
- ⠀⠀⠀⣠⠞⢁⠈⢕⡠⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠓⢬⡡⠡⠈⠳⣄⠀⠀⠀
5
- ⠀⠀⣰⠋⢄⠑⣠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣌⠪⡂⠙⣆⠀⠀
6
- ⠀⣰⠃⠸⠁⡼⠓⠋⠉⠁⠀⠉⠉⠒⢄⠀⠀⠀⠀⢀⠴⠒⠉⠉⠉⠉⠉⠒⢤⡈⢧⠈⠃⠘⣆⠀
7
- ⢠⡇⠴⢫⣾⣶⣆⠀⣀⠤⠤⠤⢄⡀⠀⠑⡄⠀⣴⣷⣦⠀⢀⣀⠤⠤⢄⡀⠀⠈⢎⣇⢈⢀⢸⡄
8
- ⣼⠀⠒⢸⣿⣿⠟⠉⠀⠀⠀⠀⠀⠈⠢⠤⠇⠀⢿⣿⡿⠋⠁⠀⠀⠀⠀⠈⠑⢄⡸⢸⡌⠑⠂⣧
9
- ⣿⢀⣉⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠠⠂⣿
10
- ⣿⠀⣉⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠫⠅⣿
11
- ⢻⠀⣕⠺⡇⠸⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀⢸⠃⠉⠁⡟
12
- ⠘⡇⠨⡅⢹⡀⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼⠀⢀⡏⣀⠤⢸⠃
13
- ⠀⠹⡄⠪⡂⠳⡀⠳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡜⠁⢀⡞⣀⠀⢠⠏⠀
14
- ⠀⠀⠹⣄⢩⡢⡙⢄⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⠊⠀⣠⠋⢔⠄⣡⠏⠀⠀
15
- ⠀⠀⠀⠙⢦⡀⢢⢆⠑⢤⡈⠑⠦⢄⣀⡀⠀⠀⠀⠀⣀⣠⠤⠒⠉⢀⡤⠚⢕⠵⢀⡴⠋⠀⠀⠀
16
- ⠀⠀⠀⠀⠀⠙⢦⡁⠠⠀⡌⠓⠦⢤⣀⣉⠉⠉⠉⠉⣀⣀⡤⠴⠚⢉⢔⠕⢀⡴⠋⠀⠀⠀⠀⠀
17
- ⠀⠀⠀⠀⠀⠀⠀⠙⠳⢤⣀⠂⠱⠄⢄⠈⣍⢍⣭⢩⢁⣤⠢⠞⠀⣉⡤⠞⠋⠀⠀⠀⠀⠀⠀⠀
18
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠒⠶⠤⣤⣤⣥⣬⣤⣤⠤⠶⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
1
+ ⢀⣠⡤⠶⠚⢚⣋⢋⣙⠛⡛⠓⠦⢤⣄⡀
2
+ ⢀⣤⠞⡋⠁⠘⢤⣑⣩⡥⡭⠭⢥⣙⣑⠁⠴⣊⡙⠳⣤⡀
3
+ ⢀⡴⢋⣡⡓⣙⡴⠚⠉⠁ ⠈⠉⠓⠮⣙⢓⣄⡙⢦⡀
4
+ ⣠⠞⣰⡄⣡⠚⠁ ⠈⠓⣍⢪⢌⠻⣄
5
+ ⢰⠋⢬⢊⣼⠥⠴⠒⠒⠢⠤⡀ ⢀⣀⠤⠤⠒⠢⠤⣄⡈⢳⡱⣡⠙⡆
6
+ ⢠⡏⣸⣧⣾⣅ ⢀⣀⣀⣀⡀⠈⠱⡄ ⣰⣮⣄ ⢀⢀⣀⡀ ⠉⢢⣳⡨⠃⢹⡄
7
+ ⣼⢰⡤⢿⣿⣿⠞⠉⠁ ⠈⠉⠲⣀⡹⠐⣿⣿⡿⠚⠉⠁⠈⠈⠑⠦⣀⡏⣇⠳⠌⣧
8
+ ⡟⢘⡅⡟⠈ ⠁ ⠈ ⢻⠰⡖⢻
9
+ ⣧⠨⣁⣧ ⣼⠸⢛⣼
10
+ ⢻⠘⢶⢻⡀⢇ ⢸⠁⢀⡏⠈⠉⡟
11
+ ⠘⣇⠵⢇⢧⠘⣄ ⢠⠇⢀⡼⢩⡕⣹⠃
12
+ ⠸⣆⠹⢌⢣⡈⢢⡀ ⢀⡰⠋⢀⡼⣑⠦⣠⠇
13
+ ⠙⢦⡓⢴⡙⢤⡉⠲⣄⡀ ⢀⣀⠴⠋⢀⡤⢫⡌⠋⡴⠋
14
+ ⠈⠳⣌⡙⢁⡍⡲⢤⣉⡙⠓⠒⠲⠖⠒⠋⢉⣁⡤⢖⡩⣠⠙⣡⠞⠁
15
+ ⠈⠛⢦⣌⡀⠲⢜⠭⡝⢓⣒⣚⢚⣍⡭⡰⠆⠙⣨⡴⠛⠁
16
+ ⠈⠙⠓⠲⢥⣥⣅⣉⣩⣤⣤⡬⠖⠚⠋⠁
package/dist/main.js CHANGED
@@ -1,52 +1,11 @@
1
1
  import { runStartup } from './core/logo.js';
2
- import { clearScreen, handleGracefulExit } from './core/ui.js';
3
- import { showMainMenu } from './core/menu.js';
4
- import { c } from './core/theme.js';
2
+ import { clearScreen } from './core/ui.js';
5
3
  import { enableVimKeys } from './core/vim-keys.js';
6
- import { checkForUpdate } from './features/update.js';
7
- import { showEventsPreview } from './features/calendar.js';
8
4
  export async function main(options = {}) {
9
- try {
10
- enableVimKeys();
11
- if (process.stdout.isTTY) {
12
- clearScreen();
13
- }
14
- if (!options.skipLogo) {
15
- await runStartup();
16
- }
17
- // Fire update check in background; events fetch provides natural wait time.
18
- // Its own AbortController is tied to whichever path finishes first, so a
19
- // still-in-flight request never keeps the process alive after we're done
20
- // with (or have stopped caring about) its result.
21
- const updateAbort = new AbortController();
22
- const updatePromise = checkForUpdate(updateAbort.signal);
23
- if (process.stdin.isTTY && process.stdout.isTTY) {
24
- // Launch app shell for interactive terminals
25
- const { runApp } = await import('./app/app.js');
26
- await runApp();
27
- updateAbort.abort();
28
- }
29
- else {
30
- // Classic path for non-TTY (CI, pipes, redirects)
31
- await showEventsPreview();
32
- try {
33
- const line = (await import('./features/schedule-view.js')).peekNextClassLine();
34
- if (line)
35
- console.log(line);
36
- }
37
- catch { /* best effort */ }
38
- // Update check is very likely done by now; give it a short window if not
39
- const updateMsg = await Promise.race([
40
- updatePromise,
41
- new Promise(r => setTimeout(r, 100, null)),
42
- ]);
43
- updateAbort.abort();
44
- if (updateMsg)
45
- console.log(c.warn(updateMsg));
46
- await showMainMenu();
47
- }
48
- }
49
- catch (err) {
50
- handleGracefulExit(err);
51
- }
5
+ enableVimKeys();
6
+ clearScreen();
7
+ if (!options.skipLogo)
8
+ await runStartup();
9
+ const { runApp } = await import('./app/app.js');
10
+ await runApp();
52
11
  }
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@nbtca/prompt",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
7
7
  ".": "./dist/index.js"
8
8
  },
9
9
  "bin": {
10
- "nbtca": "bin/nbtca-welcome.js",
11
- "nbtca-welcome": "bin/nbtca-welcome.js"
10
+ "nbtca": "dist/index.js",
11
+ "nbtca-welcome": "dist/index.js"
12
12
  },
13
13
  "files": [
14
14
  "dist",
15
- "bin/nbtca-welcome.js",
16
15
  "README.md",
17
16
  "SECURITY.md",
18
17
  "LICENSE"
@@ -23,10 +22,17 @@
23
22
  "dev:watch": "tsx watch src/index.ts",
24
23
  "build": "tsc",
25
24
  "postbuild": "node scripts/copy-assets.js",
26
- "clean": "rm -rf dist",
25
+ "clean": "node scripts/clean.js",
27
26
  "prebuild": "npm run clean",
28
- "prepublishOnly": "npm run build",
29
- "test": "npm run build && vitest run && bash scripts/test-cli.sh"
27
+ "format": "prettier --write .",
28
+ "format:check": "prettier --check .",
29
+ "lint": "eslint . --max-warnings=0",
30
+ "typecheck": "tsc --project tsconfig.typecheck.json",
31
+ "test": "vitest run && npm run test:cli",
32
+ "test:cli": "npm run build && bash scripts/test-cli.sh",
33
+ "check:package": "node scripts/check-package.mjs",
34
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run check:package && npm audit --audit-level=moderate",
35
+ "prepublishOnly": "npm run check"
30
36
  },
31
37
  "keywords": [
32
38
  "cli",
@@ -39,8 +45,8 @@
39
45
  "interactive"
40
46
  ],
41
47
  "dependencies": {
42
- "@nbtca/docs": "^0.2.3",
43
- "@nbtca/nbtcal": "^0.3.7",
48
+ "@nbtca/docs": "^0.3.0",
49
+ "@nbtca/nbtcal": "^0.4.0",
44
50
  "chalk": "^5.6.2",
45
51
  "cheerio": "1.0.0",
46
52
  "fetch-cookie": "^3.2.0",
@@ -51,10 +57,16 @@
51
57
  "tough-cookie": "^6.0.2"
52
58
  },
53
59
  "devDependencies": {
54
- "@types/gradient-string": "^1.1.6",
55
- "@types/node": "^22.20.1",
60
+ "@eslint/js": "^9.39.5",
61
+ "@types/node": "20.12.12",
62
+ "eslint": "^9.39.5",
63
+ "eslint-config-prettier": "^10.1.8",
64
+ "globals": "^17.9.0",
65
+ "prettier": "^3.9.6",
56
66
  "tsx": "^4.23.5",
57
67
  "typescript": "^5.9.3",
68
+ "typescript-eslint": "^8.67.0",
69
+ "vite": "^6.4.3",
58
70
  "vitest": "^3.2.7"
59
71
  },
60
72
  "engines": {
@@ -67,14 +79,11 @@
67
79
  },
68
80
  "repository": {
69
81
  "type": "git",
70
- "url": "git+https://github.com/nbtca/prompt.git"
82
+ "url": "git+https://github.com/nbtca/Prompt.git"
71
83
  },
72
84
  "bugs": {
73
- "url": "https://github.com/nbtca/prompt/issues"
85
+ "url": "https://github.com/nbtca/Prompt/issues"
74
86
  },
75
- "homepage": "https://github.com/nbtca/prompt#readme",
76
- "description": "Terminal information system for the NBTCA community",
77
- "directories": {
78
- "doc": "docs"
79
- }
87
+ "homepage": "https://github.com/nbtca/Prompt#readme",
88
+ "description": "Terminal information system for the NBTCA community"
80
89
  }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import "../dist/index.js";
@@ -1,18 +0,0 @@
1
- import { glyph, type, space } from '../theme.js';
2
- export function screenWidth() {
3
- return Math.min(process.stdout.columns || 80, 64);
4
- }
5
- export function renderScreen(opts) {
6
- const width = opts.width ?? screenWidth();
7
- const lines = [];
8
- if (opts.title) {
9
- lines.push(space.indent + type.heading(opts.title));
10
- lines.push(space.indent + type.hint(glyph.rule().repeat(width)));
11
- }
12
- lines.push(opts.body);
13
- if (opts.footer) {
14
- lines.push('');
15
- lines.push(space.indent + type.hint(opts.footer));
16
- }
17
- return lines.join('\n');
18
- }
package/dist/core/menu.js DELETED
@@ -1,68 +0,0 @@
1
- import { runMenu } from './components/menu.js';
2
- import { type, space, glyph } from './theme.js';
3
- import { clearScreen } from './ui.js';
4
- import { showCalendar } from '../features/calendar.js';
5
- import { showDocsMenu } from '../features/docs.js';
6
- import { showServiceStatus } from '../features/status.js';
7
- import { showLinksMenu } from '../features/links.js';
8
- import { showSettingsMenu } from '../features/settings.js';
9
- import { showStudentTimetableMenu } from '../features/student-timetable.js';
10
- import { showSchedule } from '../features/schedule-view.js';
11
- import { t } from '../i18n/index.js';
12
- function getMainMenuOptions() {
13
- const trans = t();
14
- return [
15
- { value: 'events', label: trans.menu.events, hint: trans.menu.eventsDesc || undefined },
16
- { value: 'schedule', label: t().timetable.menuEntry },
17
- { value: 'timetable', label: trans.menu.timetable, hint: trans.menu.timetableDesc || undefined },
18
- { value: 'docs', label: trans.menu.docs, hint: trans.menu.docsDesc || undefined },
19
- { value: 'status', label: trans.menu.status, hint: trans.menu.statusDesc || undefined },
20
- { value: 'links', label: trans.menu.links, hint: trans.menu.linksDesc || undefined },
21
- { value: 'settings', label: trans.menu.settings, hint: trans.menu.settingsDesc || undefined },
22
- ];
23
- }
24
- export async function showMainMenu() {
25
- let first = true;
26
- while (true) {
27
- if (!first)
28
- clearScreen();
29
- first = false;
30
- const trans = t();
31
- const footer = `${glyph.updown()} ${trans.menu.hintMove} ${glyph.enter()} ${trans.menu.hintOpen} q ${trans.menu.hintQuit}`;
32
- const action = await runMenu({
33
- title: trans.menu.chooseAction,
34
- options: getMainMenuOptions(),
35
- footer,
36
- });
37
- if (action === null) {
38
- console.log(space.indent + type.hint(t().common.goodbye));
39
- process.exit(0);
40
- }
41
- await runMenuAction(action);
42
- }
43
- }
44
- export async function runMenuAction(action) {
45
- switch (action) {
46
- case 'events':
47
- await showCalendar();
48
- break;
49
- case 'schedule':
50
- await showSchedule();
51
- break;
52
- case 'timetable':
53
- await showStudentTimetableMenu();
54
- break;
55
- case 'docs':
56
- await showDocsMenu();
57
- break;
58
- case 'status':
59
- await showServiceStatus();
60
- break;
61
- case 'links':
62
- await showLinksMenu();
63
- break;
64
- case 'settings':
65
- await showSettingsMenu();
66
- break;
67
- }
68
- }
@@ -1,36 +0,0 @@
1
- import open from 'open';
2
- import chalk from 'chalk';
3
- import { runMenu, menuFooter } from '../core/components/menu.js';
4
- import { createSpinner } from '../core/ui.js';
5
- import { URLS } from '../config/data.js';
6
- import { t } from '../i18n/index.js';
7
- import { enterScreen, breadcrumb } from '../core/transitions.js';
8
- async function openUrl(url) {
9
- const trans = t();
10
- const s = createSpinner(trans.links.opening);
11
- try {
12
- await open(url);
13
- s.stop(trans.links.opened);
14
- }
15
- catch {
16
- s.error(trans.links.error);
17
- console.log(chalk.dim(` ${url}`));
18
- }
19
- }
20
- export async function showLinksMenu() {
21
- const trans = t();
22
- await enterScreen(breadcrumb(trans.menu.links));
23
- const selected = await runMenu({
24
- title: trans.links.choose,
25
- options: [
26
- { value: URLS.homepage, label: trans.links.website },
27
- { value: URLS.github, label: trans.links.github },
28
- { value: URLS.roadmap, label: trans.links.roadmap },
29
- { value: URLS.repair, label: trans.links.repair },
30
- ],
31
- footer: menuFooter(),
32
- });
33
- if (selected === null)
34
- return;
35
- await openUrl(selected);
36
- }
@@ -1,47 +0,0 @@
1
- const DAY_MS = 86400000;
2
- export function currentWeekNumber(weekOneMonday, now) {
3
- const base = new Date(`${weekOneMonday}T00:00:00`);
4
- const days = Math.floor((now.getTime() - base.getTime()) / DAY_MS);
5
- return Math.floor(days / 7) + 1;
6
- }
7
- export function campusWeekday(now) {
8
- return ((now.getDay() + 6) % 7) + 1;
9
- }
10
- export function meetingsInWeek(meetings, week) {
11
- return meetings.filter((mtg) => mtg.weeks.includes(week));
12
- }
13
- export function meetingsOnDay(meetings, weekday, week) {
14
- return meetings
15
- .filter((mtg) => mtg.weekday === weekday && mtg.weeks.includes(week))
16
- .sort((a, b) => a.startPeriod - b.startPeriod);
17
- }
18
- export function periodStartDate(weekOneMonday, week, weekday, period, periods) {
19
- const p = periods.find((x) => x.period === period);
20
- if (!p)
21
- return null;
22
- const base = new Date(`${weekOneMonday}T00:00:00`);
23
- const date = new Date(base.getTime() + ((week - 1) * 7 + (weekday - 1)) * DAY_MS);
24
- const parts = p.start.split(':');
25
- date.setHours(Number.parseInt(parts[0] ?? '0', 10), Number.parseInt(parts[1] ?? '0', 10), 0, 0);
26
- return date;
27
- }
28
- export function nextMeeting(meetings, periods, weekOneMonday, now) {
29
- let best = null;
30
- for (const meeting of meetings) {
31
- for (const week of meeting.weeks) {
32
- const start = periodStartDate(weekOneMonday, week, meeting.weekday, meeting.startPeriod, periods);
33
- if (start && start.getTime() > now.getTime() && (!best || start.getTime() < best.start.getTime())) {
34
- best = { meeting, start };
35
- }
36
- }
37
- }
38
- return best;
39
- }
40
- /** The meeting occupying a grid cell, whether it starts there or is a later
41
- * period of a meeting that started earlier the same day -- one condition
42
- * (`startPeriod <= period <= endPeriod`) covers both cases, matching
43
- * renderWeekGrid's own starting/continuing lookup so "does this cell have a
44
- * meeting" and "what does the grid actually draw there" never disagree. */
45
- export function meetingAtCursor(meetings, week, cursor) {
46
- return meetingsInWeek(meetings, week).find((m) => m.weekday === cursor.weekday && m.startPeriod <= cursor.period && cursor.period <= m.endPeriod) ?? null;
47
- }