@nbtca/prompt 1.4.1 → 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 (68) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +27 -58
  3. package/SECURITY.md +16 -45
  4. package/dist/app/app.js +53 -55
  5. package/dist/app/chrome.js +67 -50
  6. package/dist/app/fields/list-field.js +12 -25
  7. package/dist/app/fields/text-field.js +3 -8
  8. package/dist/app/frame.js +2 -21
  9. package/dist/app/keys.js +10 -2
  10. package/dist/app/views/docs-render.js +31 -24
  11. package/dist/app/views/docs.js +211 -67
  12. package/dist/app/views/events-render.js +19 -26
  13. package/dist/app/views/events.js +44 -31
  14. package/dist/app/views/home.js +33 -76
  15. package/dist/app/views/schedule-grid-cursor.js +9 -18
  16. package/dist/app/views/schedule-render.js +47 -71
  17. package/dist/app/views/schedule.js +158 -90
  18. package/dist/app/views/settings-render.js +8 -19
  19. package/dist/app/views/settings.js +93 -18
  20. package/dist/auth/cookie-transport.js +31 -32
  21. package/dist/auth/errors.js +3 -1
  22. package/dist/auth/nbt-auth.js +42 -25
  23. package/dist/auth/session-store.js +17 -9
  24. package/dist/config/data.js +10 -13
  25. package/dist/config/preferences.js +14 -7
  26. package/dist/core/calendar-day.js +37 -0
  27. package/dist/core/capabilities.js +6 -3
  28. package/dist/core/components/confirm.js +9 -8
  29. package/dist/core/components/menu.js +41 -16
  30. package/dist/core/components/messages.js +12 -4
  31. package/dist/core/components/painter.js +3 -1
  32. package/dist/core/components/spinner.js +17 -6
  33. package/dist/core/components/text-input.js +24 -18
  34. package/dist/core/icons.js +2 -2
  35. package/dist/core/logo.js +25 -21
  36. package/dist/core/motion.js +25 -19
  37. package/dist/core/text.js +182 -75
  38. package/dist/core/theme.js +0 -28
  39. package/dist/core/transitions.js +2 -2
  40. package/dist/core/ui.js +15 -30
  41. package/dist/core/vim-keys.js +9 -15
  42. package/dist/features/about.js +23 -0
  43. package/dist/features/calendar-heatmap.js +16 -40
  44. package/dist/features/calendar-query.js +1 -2
  45. package/dist/features/calendar.js +12 -185
  46. package/dist/features/docs.js +439 -320
  47. package/dist/features/schedule-render.js +65 -102
  48. package/dist/features/schedule-store.js +51 -9
  49. package/dist/features/schedule-view.js +46 -220
  50. package/dist/features/status.js +44 -59
  51. package/dist/features/student-timetable.js +73 -95
  52. package/dist/features/theme.js +6 -5
  53. package/dist/features/timetable-sanitize.js +40 -0
  54. package/dist/features/update.js +9 -37
  55. package/dist/i18n/index.js +87 -65
  56. package/dist/i18n/locales/en.json +1 -1
  57. package/dist/i18n/locales/zh.json +1 -1
  58. package/dist/index.js +85 -64
  59. package/dist/logo/ca-dotmatrix.txt +16 -18
  60. package/dist/main.js +7 -48
  61. package/package.json +30 -18
  62. package/bin/nbtca-welcome.js +0 -2
  63. package/dist/core/components/screen.js +0 -18
  64. package/dist/core/menu.js +0 -71
  65. package/dist/features/links.js +0 -39
  66. package/dist/features/schedule-query.js +0 -47
  67. package/dist/features/settings.js +0 -130
  68. package/dist/logo/ca-logo.png +0 -0
package/dist/index.js CHANGED
@@ -1,21 +1,20 @@
1
- /**
2
- * NBTCA Prompt entry point
3
- */
1
+ #!/usr/bin/env node
4
2
  import chalk from 'chalk';
5
3
  import open from 'open';
6
4
  import { main } from './main.js';
7
- import { fetchEvents, fetchHeatmapBuckets, renderEventsTable, serializeEvents } from './features/calendar.js';
5
+ import { fetchEvents, fetchHeatmapBuckets, renderEventsTable, serializeEvents, } from './features/calendar.js';
8
6
  import { renderHeatmap } from './features/calendar-heatmap.js';
9
- import { checkServices, countServiceHealth, hasServiceFailures, renderServiceStatusTable, serializeServiceStatus } from './features/status.js';
7
+ import { checkServices, countServiceHealth, hasServiceFailures, renderServiceStatusTable, serializeServiceStatus, } from './features/status.js';
10
8
  import { pickIcon } from './core/icons.js';
11
9
  import { applyColorModePreference } from './config/preferences.js';
12
10
  import { openDocsInBrowser } from './features/docs.js';
13
11
  import { runThemeCommand } from './features/theme.js';
14
- import { setLanguage, t, fmt } from './i18n/index.js';
12
+ import { saveLanguagePreference, t, fmt } from './i18n/index.js';
15
13
  import { clearScreen, handleGracefulExit } from './core/ui.js';
16
14
  import { APP_INFO, URLS } from './config/data.js';
17
15
  import { runUpdateCheck } from './features/update.js';
18
16
  import { runStudentTimetableCommand } from './features/student-timetable.js';
17
+ import { showAbout } from './features/about.js';
19
18
  const ACTION_ALIASES = {
20
19
  events: 'events',
21
20
  event: 'events',
@@ -35,13 +34,33 @@ const URL_ACTIONS = {
35
34
  repair: URLS.repair,
36
35
  website: URLS.homepage,
37
36
  github: URLS.github,
38
- roadmap: URLS.roadmap
37
+ roadmap: URLS.roadmap,
39
38
  };
40
39
  const KNOWN_FLAGS = new Set([
41
- '--help', '--version', '--open', '--json', '--plain', '--no-logo', '--watch', '--today', '--heatmap',
42
- '--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',
43
53
  ]);
44
- 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
+ ];
45
64
  const STATUS_WATCH_INTERVAL_MIN = 3;
46
65
  const STATUS_WATCH_INTERVAL_MAX = 300;
47
66
  const STATUS_TIMEOUT_MIN = 1000;
@@ -60,13 +79,16 @@ function parseArgs(argv) {
60
79
  }
61
80
  }
62
81
  return {
63
- command: positionals[0]?.toLowerCase(),
82
+ ...(positionals[0] === undefined ? {} : { command: positionals[0].toLowerCase() }),
64
83
  args: positionals.slice(1),
65
- flags
84
+ flags,
66
85
  };
67
86
  }
87
+ function isTty(value) {
88
+ return value === true;
89
+ }
68
90
  function hasInteractiveTerminal() {
69
- return !!process.stdin.isTTY && !!process.stdout.isTTY;
91
+ return isTty(process.stdin.isTTY) && isTty(process.stdout.isTTY);
70
92
  }
71
93
  function getAllowedFlagsFor(command) {
72
94
  const allowed = new Set(['--help', '--plain']);
@@ -125,27 +147,27 @@ function getAllowedFlagPrefixesFor(command) {
125
147
  return [];
126
148
  }
127
149
  function validateFlags(command, flags) {
128
- const unknown = Array.from(flags).filter((flag) => {
150
+ const unknownFlag = Array.from(flags).find((flag) => {
129
151
  if (KNOWN_FLAGS.has(flag))
130
152
  return false;
131
153
  return !KNOWN_FLAG_PREFIXES.some((prefix) => flag.startsWith(prefix));
132
154
  });
133
- if (unknown.length > 0) {
155
+ if (unknownFlag) {
134
156
  const trans0 = t();
135
- console.error(chalk.red(fmt(trans0.cli.unknownFlag, { flag: unknown[0] })));
157
+ console.error(chalk.red(fmt(trans0.cli.unknownFlag, { flag: unknownFlag })));
136
158
  console.error(chalk.dim(trans0.cli.unknownFlagHint));
137
159
  process.exit(1);
138
160
  }
139
161
  const allowed = getAllowedFlagsFor(command);
140
162
  const allowedPrefixes = getAllowedFlagPrefixesFor(command);
141
- const disallowed = Array.from(flags).filter((flag) => {
163
+ const disallowedFlag = Array.from(flags).find((flag) => {
142
164
  if (allowed.has(flag))
143
165
  return false;
144
166
  return !allowedPrefixes.some((prefix) => flag.startsWith(prefix));
145
167
  });
146
- if (disallowed.length > 0) {
168
+ if (disallowedFlag) {
147
169
  const trans1 = t();
148
- console.error(chalk.red(fmt(trans1.cli.invalidFlag, { flag: disallowed[0] })));
170
+ console.error(chalk.red(fmt(trans1.cli.invalidFlag, { flag: disallowedFlag })));
149
171
  console.error(chalk.dim(trans1.cli.invalidFlagHint));
150
172
  process.exit(1);
151
173
  }
@@ -203,7 +225,7 @@ async function runEventsCommand(flags) {
203
225
  process.stdout.write(JSON.stringify(buckets, null, 2) + '\n');
204
226
  }
205
227
  else {
206
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
228
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
207
229
  console.log(renderHeatmap(buckets, new Date(), { color: useColor }));
208
230
  }
209
231
  return;
@@ -223,23 +245,23 @@ async function runEventsCommand(flags) {
223
245
  else {
224
246
  events = await fetchEvents();
225
247
  }
226
- const searchFlag = Array.from(flags).find(f => f.startsWith('--search='));
248
+ const searchFlag = Array.from(flags).find((f) => f.startsWith('--search='));
227
249
  if (searchFlag) {
228
250
  const q = searchFlag.slice('--search='.length).toLowerCase();
229
- events = events.filter(e => `${e.title} ${e.location}`.toLowerCase().includes(q));
251
+ events = events.filter((e) => `${e.title} ${e.location}`.toLowerCase().includes(q));
230
252
  }
231
253
  if (flags.has('--today')) {
232
254
  const now = new Date();
233
- events = events.filter(e => {
255
+ events = events.filter((e) => {
234
256
  const d = e.startDate;
235
- return d.getFullYear() === now.getFullYear() &&
257
+ return (d.getFullYear() === now.getFullYear() &&
236
258
  d.getMonth() === now.getMonth() &&
237
- d.getDate() === now.getDate();
259
+ d.getDate() === now.getDate());
238
260
  });
239
261
  }
240
- const nextFlag = Array.from(flags).find(f => f.startsWith('--next='));
262
+ const nextFlag = Array.from(flags).find((f) => f.startsWith('--next='));
241
263
  if (nextFlag) {
242
- const n = Number.parseInt(nextFlag.split('=')[1] || '', 10);
264
+ const n = Number.parseInt(nextFlag.slice('--next='.length), 10);
243
265
  if (!Number.isInteger(n) || n < 1) {
244
266
  console.error(chalk.red(t().cli.invalidNext));
245
267
  process.exit(1);
@@ -250,7 +272,7 @@ async function runEventsCommand(flags) {
250
272
  process.stdout.write(JSON.stringify(serializeEvents(events), null, 2) + '\n');
251
273
  return;
252
274
  }
253
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
275
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
254
276
  console.log(renderEventsTable(events, { color: useColor }));
255
277
  }
256
278
  async function runStatusCommand(flags) {
@@ -259,14 +281,20 @@ async function runStatusCommand(flags) {
259
281
  const intervalFlag = Array.from(flags).find((flag) => flag.startsWith('--interval='));
260
282
  const timeoutFlag = Array.from(flags).find((flag) => flag.startsWith('--timeout='));
261
283
  const retriesFlag = Array.from(flags).find((flag) => flag.startsWith('--retries='));
262
- const intervalSeconds = intervalFlag ? Number.parseInt(intervalFlag.split('=')[1] || '', 10) : 10;
263
- const timeoutMs = timeoutFlag ? Number.parseInt(timeoutFlag.split('=')[1] || '', 10) : 6000;
264
- 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;
265
291
  if (!watch && intervalFlag) {
266
292
  console.error(chalk.red(trans.status.intervalNeedsWatch));
267
293
  process.exit(1);
268
294
  }
269
- 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) {
270
298
  console.error(chalk.red(fmt(trans.status.invalidTimeout, { min: STATUS_TIMEOUT_MIN, max: STATUS_TIMEOUT_MAX })));
271
299
  process.exit(1);
272
300
  }
@@ -278,8 +306,14 @@ async function runStatusCommand(flags) {
278
306
  console.error(chalk.red(trans.status.watchJsonConflict));
279
307
  process.exit(1);
280
308
  }
281
- if (watch && (!Number.isInteger(intervalSeconds) || intervalSeconds < STATUS_WATCH_INTERVAL_MIN || intervalSeconds > STATUS_WATCH_INTERVAL_MAX)) {
282
- 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
+ })));
283
317
  process.exit(1);
284
318
  }
285
319
  if (watch && !hasInteractiveTerminal()) {
@@ -287,12 +321,14 @@ async function runStatusCommand(flags) {
287
321
  process.exit(1);
288
322
  }
289
323
  if (watch) {
290
- let stopped = false;
291
- const onSigint = () => { stopped = true; };
324
+ const stopController = new AbortController();
325
+ const onSigint = () => {
326
+ stopController.abort();
327
+ };
292
328
  process.once('SIGINT', onSigint);
293
329
  console.log(chalk.dim(`${fmt(trans.status.watchStarted, { seconds: intervalSeconds })} | ${trans.status.watchHint}`));
294
330
  try {
295
- while (!stopped) {
331
+ while (!stopController.signal.aborted) {
296
332
  const services = await checkServices({ timeoutMs, retries });
297
333
  const hasFailures = hasServiceFailures(services);
298
334
  const health = countServiceHealth(services);
@@ -300,7 +336,7 @@ async function runStatusCommand(flags) {
300
336
  console.log(chalk.bold(`${trans.status.watchUpdated}: ${new Date().toLocaleString()}`));
301
337
  console.log(chalk.dim(`${trans.status.up}: ${health.up} | ${trans.status.down}: ${health.down} | ${trans.status.watchHint}`));
302
338
  console.log();
303
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
339
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
304
340
  console.log(renderServiceStatusTable(services, { color: useColor }));
305
341
  if (hasFailures) {
306
342
  console.log(chalk.yellow(trans.status.summaryFail));
@@ -311,12 +347,12 @@ async function runStatusCommand(flags) {
311
347
  await new Promise((resolve) => {
312
348
  const stopWait = () => {
313
349
  clearTimeout(timer);
314
- process.removeListener('SIGINT', stopWait);
350
+ stopController.signal.removeEventListener('abort', stopWait);
315
351
  resolve();
316
352
  };
317
353
  const timer = setTimeout(stopWait, intervalSeconds * 1000);
318
- process.once('SIGINT', stopWait);
319
- if (stopped)
354
+ stopController.signal.addEventListener('abort', stopWait, { once: true });
355
+ if (stopController.signal.aborted)
320
356
  stopWait();
321
357
  });
322
358
  }
@@ -334,7 +370,7 @@ async function runStatusCommand(flags) {
334
370
  process.stdout.write(JSON.stringify(serializeServiceStatus(services), null, 2) + '\n');
335
371
  }
336
372
  else {
337
- const useColor = !flags.has('--plain') && !!process.stdout.isTTY;
373
+ const useColor = !flags.has('--plain') && isTty(process.stdout.isTTY);
338
374
  console.log(renderServiceStatusTable(services, { color: useColor }));
339
375
  if (hasFailures) {
340
376
  console.error(chalk.yellow(t().status.summaryFail));
@@ -351,7 +387,10 @@ function maybeDisableColor(flags) {
351
387
  async function runCommandMode(argv) {
352
388
  const { command, args, flags } = parseArgs(argv);
353
389
  maybeDisableColor(flags);
354
- if (flags.has('--version') || command === '--version' || command === '-v' || command === 'version') {
390
+ if (flags.has('--version') ||
391
+ command === '--version' ||
392
+ command === '-v' ||
393
+ command === 'version') {
355
394
  console.log(APP_INFO.version);
356
395
  return;
357
396
  }
@@ -371,12 +410,12 @@ async function runCommandMode(argv) {
371
410
  return;
372
411
  }
373
412
  if (command === 'lang' || command === 'language') {
374
- const language = (args[0] || '').toLowerCase();
413
+ const language = (args[0] ?? '').toLowerCase();
375
414
  if (language !== 'zh' && language !== 'en') {
376
415
  console.error(chalk.red(t().cli.invalidLang));
377
416
  process.exit(1);
378
417
  }
379
- const persisted = setLanguage(language);
418
+ const persisted = saveLanguagePreference(language);
380
419
  if (persisted) {
381
420
  console.log(chalk.green(`${pickIcon('✓', 'OK')}: ${t().language.changed}`));
382
421
  }
@@ -444,27 +483,9 @@ async function runCommandMode(argv) {
444
483
  return;
445
484
  }
446
485
  if (action === 'about') {
447
- const { note } = await import('./core/components/note.js');
448
- const { padEndV } = await import('./core/text.js');
449
- const pad = 12;
450
- const row = (label, value) => `${chalk.dim(padEndV(label, pad))}${value}`;
451
- const link = (label, url) => row(label, chalk.cyan(url));
452
- const trans = t();
453
- const content = [
454
- row(trans.about.project, APP_INFO.name),
455
- row(trans.about.version, `v${APP_INFO.version}`),
456
- row(trans.about.description, trans.about.descriptionText),
457
- '',
458
- link(trans.about.github, APP_INFO.repository),
459
- link(trans.about.website, URLS.homepage),
460
- link(trans.about.email, URLS.email),
461
- '',
462
- row(trans.about.license, `MIT | ${trans.about.author}: m1ngsama`),
463
- ].join('\n');
464
- note(content, trans.about.title);
486
+ showAbout();
465
487
  return;
466
488
  }
467
- // URL actions: repair, website, github, roadmap
468
489
  const mappedUrl = URL_ACTIONS[action];
469
490
  if (mappedUrl) {
470
491
  if (flags.has('--open')) {
@@ -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.1",
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,24 +22,31 @@
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",
33
39
  "terminal",
34
40
  "welcome",
35
41
  "nbtca",
36
- "ningbotech",
37
- "computer-association",
42
+ "community",
43
+ "calendar",
38
44
  "tui",
39
45
  "interactive"
40
46
  ],
41
47
  "dependencies": {
42
- "@nbtca/docs": "^0.2.2",
43
- "@nbtca/nbtcal": "^0.3.5",
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.19.17",
56
- "tsx": "^4.21.0",
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",
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,11 +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": "NBTCA Prompt - Minimalist CLI tool for NingboTech Computer Association"
87
+ "homepage": "https://github.com/nbtca/Prompt#readme",
88
+ "description": "Terminal information system for the NBTCA community"
77
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,71 +0,0 @@
1
- /**
2
- * Minimalist menu system
3
- */
4
- import { runMenu } from './components/menu.js';
5
- import { type, space, glyph } from './theme.js';
6
- import { clearScreen } from './ui.js';
7
- import { showCalendar } from '../features/calendar.js';
8
- import { showDocsMenu } from '../features/docs.js';
9
- import { showServiceStatus } from '../features/status.js';
10
- import { showLinksMenu } from '../features/links.js';
11
- import { showSettingsMenu } from '../features/settings.js';
12
- import { showStudentTimetableMenu } from '../features/student-timetable.js';
13
- import { showSchedule } from '../features/schedule-view.js';
14
- import { t } from '../i18n/index.js';
15
- function getMainMenuOptions() {
16
- const trans = t();
17
- return [
18
- { value: 'events', label: trans.menu.events, hint: trans.menu.eventsDesc || undefined },
19
- { value: 'schedule', label: t().timetable.menuEntry },
20
- { value: 'timetable', label: trans.menu.timetable, hint: trans.menu.timetableDesc || undefined },
21
- { value: 'docs', label: trans.menu.docs, hint: trans.menu.docsDesc || undefined },
22
- { value: 'status', label: trans.menu.status, hint: trans.menu.statusDesc || undefined },
23
- { value: 'links', label: trans.menu.links, hint: trans.menu.linksDesc || undefined },
24
- { value: 'settings', label: trans.menu.settings, hint: trans.menu.settingsDesc || undefined },
25
- ];
26
- }
27
- export async function showMainMenu() {
28
- let first = true;
29
- while (true) {
30
- if (!first)
31
- clearScreen();
32
- first = false;
33
- const trans = t();
34
- const footer = `${glyph.updown()} ${trans.menu.hintMove} ${glyph.enter()} ${trans.menu.hintOpen} q ${trans.menu.hintQuit}`;
35
- const action = await runMenu({
36
- title: trans.menu.chooseAction,
37
- options: getMainMenuOptions(),
38
- footer,
39
- });
40
- if (action === null) {
41
- console.log(space.indent + type.hint(t().common.goodbye));
42
- process.exit(0);
43
- }
44
- await runMenuAction(action);
45
- }
46
- }
47
- export async function runMenuAction(action) {
48
- switch (action) {
49
- case 'events':
50
- await showCalendar();
51
- break;
52
- case 'schedule':
53
- await showSchedule();
54
- break;
55
- case 'timetable':
56
- await showStudentTimetableMenu();
57
- break;
58
- case 'docs':
59
- await showDocsMenu();
60
- break;
61
- case 'status':
62
- await showServiceStatus();
63
- break;
64
- case 'links':
65
- await showLinksMenu();
66
- break;
67
- case 'settings':
68
- await showSettingsMenu();
69
- break;
70
- }
71
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Links — open NBTCA resources in browser
3
- */
4
- import open from 'open';
5
- import chalk from 'chalk';
6
- import { runMenu, menuFooter } from '../core/components/menu.js';
7
- import { createSpinner } from '../core/ui.js';
8
- import { URLS } from '../config/data.js';
9
- import { t } from '../i18n/index.js';
10
- import { enterScreen, breadcrumb } from '../core/transitions.js';
11
- async function openUrl(url) {
12
- const trans = t();
13
- const s = createSpinner(trans.links.opening);
14
- try {
15
- await open(url);
16
- s.stop(trans.links.opened);
17
- }
18
- catch {
19
- s.error(trans.links.error);
20
- console.log(chalk.dim(` ${url}`));
21
- }
22
- }
23
- export async function showLinksMenu() {
24
- const trans = t();
25
- await enterScreen(breadcrumb(trans.menu.links));
26
- const selected = await runMenu({
27
- title: trans.links.choose,
28
- options: [
29
- { value: URLS.homepage, label: trans.links.website },
30
- { value: URLS.github, label: trans.links.github },
31
- { value: URLS.roadmap, label: trans.links.roadmap },
32
- { value: URLS.repair, label: trans.links.repair },
33
- ],
34
- footer: menuFooter(),
35
- });
36
- if (selected === null)
37
- return;
38
- await openUrl(selected);
39
- }