@nbtca/prompt 1.4.1 → 1.4.2

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 NingboTech University, Computer Association.
3
+ Copyright (c) 2025 NBTCA
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # NBTCA Prompt
2
2
 
3
- Terminal-based information system for NingboTech Computer Association.
3
+ Terminal-based information system for the NBTCA community.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@nbtca/prompt)](https://www.npmjs.com/package/@nbtca/prompt)
6
6
  [![License](https://img.shields.io/npm/l/@nbtca/prompt)](LICENSE)
package/SECURITY.md CHANGED
@@ -13,7 +13,7 @@ fails closed. It does not automate or bypass the challenge.
13
13
  ## Authenticated transport
14
14
 
15
15
  Authentication redirects are restricted to exact HTTPS hosts and routes for
16
- the NingboTech WebVPN, authentication service and JWXT. CAS `service` and
16
+ the campus WebVPN, authentication service and JWXT. CAS `service` and
17
17
  WebVPN `origin` parameters are validated against explicit callback routes.
18
18
  Caller-supplied `Cookie`, `Authorization` and `Host` headers are rejected.
19
19
 
@@ -40,13 +40,6 @@ function buildSectionsField() {
40
40
  function buildFilesField(section, maxVisible, initialIndex = 0) {
41
41
  const trans = t();
42
42
  const isIndex = (f) => f.name === 'index.md' || f.name.startsWith('index.');
43
- // nbtca/documents' repair/ and concepts/ sections are explicitly built
44
- // "hub + inline-link + search, no full sidebar" (.vitepress/config.mts) --
45
- // each has a hand-curated index.md landing page (concepts/index.md groups
46
- // all 21 entries by topic with one-line definitions; nothing like that
47
- // exists in a flat alphabetical list). It used to be filtered out
48
- // entirely here, making it unreachable from the Docs tab -- now it's
49
- // pinned to the top as a distinctly-labeled entry instead.
50
43
  const index = section.files.find(isIndex);
51
44
  const files = section.files.filter((f) => !isIndex(f));
52
45
  const options = [
@@ -30,7 +30,6 @@ function wrappedRenderedLines(line, cols) {
30
30
  return wrappedIndentedLines(content, cols, (value) => value);
31
31
  }
32
32
  const DAY_PROGRESS_WIDTH = 20;
33
- /** Pure: a block-character bar for how far into the calendar day `now` is. */
34
33
  function renderDayProgress(now, cols) {
35
34
  const minutesElapsed = now.getHours() * 60 + now.getMinutes();
36
35
  const fraction = Math.min(1, Math.max(0, minutesElapsed / 1440));
@@ -48,13 +47,6 @@ function renderDayProgress(now, cols) {
48
47
  const bar = filledChar.repeat(filled) + emptyChar.repeat(barWidth - filled);
49
48
  return `${indent}${type.body(bar)}${gap}${type.hint(percentage)}`;
50
49
  }
51
- /** Combined class+event density grid for the coming campus week — the one
52
- * visualization neither Schedule nor Events alone can produce, since it
53
- * needs both data sources at once. Deliberately coarser (binary, not
54
- * 5-level) than Schedule's own term-density strip, and deliberately
55
- * uncolored (see the design spec's "Visual language decision") since it's
56
- * an overview of two other already-colored things, not a third color
57
- * language to learn. */
58
50
  function renderWeekAheadGrid(classDays, eventDays, cols) {
59
51
  const trans = t();
60
52
  const hasClassChar = pickIcon('▓▓', '##');
@@ -65,20 +57,12 @@ function renderWeekAheadGrid(classDays, eventDays, cols) {
65
57
  const days = [1, 2, 3, 4, 5, 6, 7];
66
58
  const dayLabels = days.map((wd) => type.hint(weekdayShortLabel(wd))).join(' ');
67
59
  const headerLine = `${space.indent}${padEndV('', rowLabelW)}${dayLabels}`;
68
- // Class row: weekend is hardcoded to the "N/A" glyph regardless of
69
- // classDays data (campus never has weekend classes) -- the same
70
- // weekend treatment used throughout the Schedule tab's own renderers.
71
60
  const classCells = days.map((wd) => {
72
61
  const isWeekend = wd === 6 || wd === 7;
73
62
  const glyphChar = isWeekend ? weekendChar : (classDays[wd - 1] ? hasClassChar : freeChar);
74
63
  return type.body(glyphChar);
75
64
  }).join(' ');
76
65
  const classLine = `${space.indent}${type.hint(padEndV(trans.timetable.weekAheadClasses, rowLabelW))}${classCells}`;
77
- // Event row: deliberately NOT hardcoding weekend -- a club event can
78
- // happen on a Saturday, so this row checks real data for all 7 days.
79
- // undefined eventDays (events still loading, or the fetch failed) means
80
- // "not yet known" -- rendered as blank, not the "free" glyph, to
81
- // visually distinguish "no data yet" from "checked, nothing happening".
82
66
  const eventCells = days.map((wd) => {
83
67
  if (!eventDays)
84
68
  return blankCell;
@@ -104,18 +88,15 @@ function renderWeekAheadGrid(classDays, eventDays, cols) {
104
88
  const compactLegend = wrappedIndentedLines(`${hasClassChar} ${trans.timetable.weekAheadBusy} ${freeChar} ${trans.timetable.weekAheadFree} ${weekendChar} ${trans.timetable.weekAheadNone}`, cols, type.hint);
105
89
  return [...compactHeading, ...compactDays, ...compactLegend].join('\n');
106
90
  }
107
- /** Pure: renders the schedule-first dashboard from already-fetched data. No I/O. */
108
91
  export function renderHome(data, now, bodyRows = 100, cols = 80) {
109
92
  const trans = t();
110
93
  const lines = [];
111
- // Next class (cache-only, instant).
112
94
  const nextClass = data.nextClassLine !== undefined && data.nextClassLine.trim().length > 0
113
95
  ? wrappedRenderedLines(data.nextClassLine, cols)
114
96
  : wrappedIndentedLines(trans.timetable.noNextClass, cols, type.hint);
115
97
  lines.push(...panelHeading(trans.timetable.nextClass, cols));
116
98
  lines.push(...nextClass);
117
99
  lines.push('');
118
- // Today's classes (cache-only, instant).
119
100
  lines.push(...panelHeading(trans.timetable.hubToday, cols));
120
101
  lines.push(renderDayProgress(now, cols));
121
102
  if (data.todayLines && data.todayLines.length > 0) {
@@ -126,28 +107,15 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
126
107
  lines.push(...wrappedIndentedLines(trans.timetable.noClassToday, cols, type.hint));
127
108
  }
128
109
  lines.push('');
129
- // Week overview (Part D): only when the student has a set-up, in-term
130
- // personal timetable -- mirrors peekWeekAheadInfo's own "not set up yet
131
- // / term hasn't started" -> null contract, hiding the whole panel rather
132
- // than showing empty/misleading cells.
133
110
  if (data.weekAhead) {
134
111
  lines.push(...panelHeading(trans.timetable.weekOverviewTitle, cols));
135
112
  lines.push(...renderWeekAheadGrid(data.weekAhead.classDays, data.weekAhead.eventDays, cols).split('\n'));
136
113
  lines.push('');
137
114
  }
138
- // Unresolved schedule items (Part E): surfaced directly on Home instead
139
- // of only inside Schedule's own hub menu -- same c.warn + ⚠ treatment
140
- // buildHubField() (schedule.ts) already uses for this exact condition,
141
- // matching the "everything that needs your attention, in one place"
142
- // spirit of a gh-status-like control center.
143
115
  if ((data.unresolvedCount ?? 0) > 0) {
144
116
  lines.push(...wrappedIndentedLines(`${pickIcon('⚠', '!')} ${trans.timetable.hubUnresolved} · ${data.unresolvedCount}`, cols, c.warn));
145
117
  lines.push('');
146
118
  }
147
- // Upcoming events (network, best-effort). How many fit is whatever room
148
- // is actually left after next-class/today above — on a tall terminal
149
- // that's most of `data.eventLines`; on a normal one, still just a few,
150
- // same as before this was ever adaptive.
151
119
  lines.push(...panelHeading(trans.menu.events, cols));
152
120
  if (data.eventLines && data.eventLines.length > 0) {
153
121
  const remaining = Number.isFinite(bodyRows)
@@ -168,7 +136,7 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
168
136
  else if (data.loading) {
169
137
  lines.push(...loadingLines(cols));
170
138
  }
171
- else if (data.eventsError) {
139
+ else if (data.eventsLoadFailed) {
172
140
  lines.push(...wrappedIndentedLines(trans.calendar.error, cols, type.hint));
173
141
  }
174
142
  else {
@@ -184,29 +152,20 @@ export const homeView = {
184
152
  return passiveFooterHint(tabCount, cols);
185
153
  },
186
154
  async load(ctx) {
187
- // Schedule panels are cache-only and instant — populate them
188
- // synchronously first. weekAheadSync is computed once here and reused
189
- // below (peekWeekAheadInfo is itself cache-only/cheap, but capturing
190
- // its result avoids a second, redundant cache read for weekStartDate).
191
- const weekAheadSync = peekWeekAheadInfo();
155
+ const weekAheadInfo = peekWeekAheadInfo();
192
156
  try {
193
157
  data = {
194
158
  loading: true,
195
159
  nextClassLine: peekNextClassLine(),
196
160
  todayLines: peekTodayLines(),
197
161
  unresolvedCount: peekUnresolvedCount(),
198
- weekAhead: weekAheadSync ? { classDays: weekAheadSync.classDays } : undefined,
162
+ weekAhead: weekAheadInfo ? { classDays: weekAheadInfo.classDays } : undefined,
199
163
  };
200
164
  }
201
165
  catch {
202
166
  data = { loading: true };
203
167
  }
204
168
  ctx.rerender();
205
- // Events is the only networked panel; best-effort. Fetches the calendar
206
- // exactly once and reuses that same Calendar instance for both the
207
- // upcoming-events list below and the week-ahead event row (when there's
208
- // a personal timetable to correlate it against) — not two separate
209
- // network round-trips for what both come from the same public feed.
210
169
  const HOME_EVENT_FETCH_CAP = 15;
211
170
  try {
212
171
  const cal = await loadCalendarOrThrow();
@@ -214,16 +173,16 @@ export const homeView = {
214
173
  const items = cal.upcoming({ days: 30 }).slice(0, HOME_EVENT_FETCH_CAP).map(toDisplayEvent);
215
174
  const eventLines = items.map((e) => renderEventBrief(e, now));
216
175
  let weekAhead = data.weekAhead;
217
- if (weekAheadSync) {
218
- const weekEnd = new Date(weekAheadSync.weekStartDate.getTime() + 7 * 86400000);
219
- const weekEvents = cal.inRange(weekAheadSync.weekStartDate, weekEnd);
176
+ if (weekAheadInfo) {
177
+ const weekEnd = new Date(weekAheadInfo.weekStartDate.getTime() + 7 * 86400000);
178
+ const weekEvents = cal.inRange(weekAheadInfo.weekStartDate, weekEnd);
220
179
  const daySet = new Set(weekEvents.map((e) => campusWeekday(e.start)));
221
- weekAhead = { classDays: weekAheadSync.classDays, eventDays: [1, 2, 3, 4, 5, 6, 7].map((wd) => daySet.has(wd)) };
180
+ weekAhead = { classDays: weekAheadInfo.classDays, eventDays: [1, 2, 3, 4, 5, 6, 7].map((wd) => daySet.has(wd)) };
222
181
  }
223
182
  data = { ...data, eventLines, weekAhead };
224
183
  }
225
184
  catch {
226
- data = { ...data, eventsError: true };
185
+ data = { ...data, eventsLoadFailed: true };
227
186
  }
228
187
  finally {
229
188
  data = { ...data, loading: false };
@@ -139,20 +139,11 @@ async function afterAuthenticated(ctx, s) {
139
139
  }
140
140
  catch (err) {
141
141
  if (isSessionExpired(err)) {
142
- // A dead session must be cleared and routed back to the login
143
- // field — leaving it as a bare error message here was a dead end:
144
- // the stale session would keep failing the same way on every
145
- // future launch/tab-switch, and there was no way back into the
146
- // login form short of quitting the app.
147
142
  createSessionStore().clear();
148
143
  if (!hadCache)
149
144
  goToLoginId(t().timetable.expiredRelogin);
150
145
  }
151
146
  else if (!hadCache) {
152
- // Only replace the screen with an error if there was nothing
153
- // useful showing already — a background refresh failure must not
154
- // blow away a working cached timetable (matches the same
155
- // best-effort contract refreshFromNetwork documents below).
156
147
  state = { mode: 'error', errorMessage: safeMessage(err) };
157
148
  }
158
149
  ctx.rerender();
@@ -3,7 +3,7 @@ import { renderSettings } from './settings-render.js';
3
3
  import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../../config/preferences.js';
4
4
  import { resetIconCache, pickIcon } from '../../core/icons.js';
5
5
  import { APP_INFO, URLS } from '../../config/data.js';
6
- import { t, getCurrentLanguage, setLanguage, clearTranslationCache } from '../../i18n/index.js';
6
+ import { t, getCurrentLanguage, saveLanguagePreference, clearTranslationCache } from '../../i18n/index.js';
7
7
  import { padEndV } from '../../core/text.js';
8
8
  let state = { mode: 'menu' };
9
9
  function buildMenuField(statusMessage) {
@@ -113,7 +113,7 @@ export const settingsView = {
113
113
  return;
114
114
  const currentLang = getCurrentLanguage();
115
115
  if (result.selected !== currentLang) {
116
- const saved = setLanguage(result.selected);
116
+ const saved = saveLanguagePreference(result.selected);
117
117
  clearTranslationCache();
118
118
  goToMenu(saved ? t().language.changed : t().language.changedSessionOnly);
119
119
  }
@@ -1,4 +1,3 @@
1
- /** Core URL and application constants. */
2
1
  import { readFileSync } from 'fs';
3
2
  import { fileURLToPath } from 'url';
4
3
  import { dirname, join } from 'path';
@@ -33,7 +32,7 @@ export const GITHUB_REPO = {
33
32
  export const APP_INFO = {
34
33
  name: 'Prompt',
35
34
  version: readPackageVersion(),
36
- description: 'NingboTech Computer Association',
35
+ description: 'NBTCA community',
37
36
  author: 'm1ngsama <contact@m1ng.space>',
38
37
  license: 'MIT',
39
38
  repository: 'https://github.com/nbtca/prompt'
package/dist/core/logo.js CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Startup logo: a high-precision braille dot-matrix render of the NBTCA emblem
3
- * (generated from CA-logo.svg), shown with the brand blue->cyan gradient.
4
- * Falls back to plain ASCII on terminals without Unicode/braille support.
5
- */
6
1
  import { readFileSync } from 'fs';
7
2
  import { fileURLToPath } from 'url';
8
3
  import { dirname, join } from 'path';
@@ -21,14 +16,7 @@ function readArt(file) {
21
16
  return null;
22
17
  }
23
18
  }
24
- // Three braille dot-matrix tiers of the same emblem (all rendered from the
25
- // same text-ring-stripped SVG, so none of them reintroduce the illegible-blob
26
- // problem -- see ca-dotmatrix.txt's own history). Picking a tier is not just
27
- // "shrink to fit": a narrow terminal gets a purpose-built lower-detail render
28
- // rather than a squashed version of the big one, mirroring how the schedule
29
- // grid swaps in a whole different layout below its own width floor instead
30
- // of cramming columns.
31
- const TIERS = [
19
+ const LOGO_TIERS = [
32
20
  { file: 'ca-dotmatrix-large.txt', minCols: 60, minRows: 34 },
33
21
  { file: 'ca-dotmatrix.txt', minCols: 44, minRows: 24 },
34
22
  { file: 'ca-dotmatrix-small.txt', minCols: 0, minRows: 0 },
@@ -36,14 +24,12 @@ const TIERS = [
36
24
  function dotmatrixFile() {
37
25
  const cols = process.stdout.columns ?? 0;
38
26
  const rows = process.stdout.rows ?? 0;
39
- const tier = TIERS.find((t) => cols >= t.minCols && rows >= t.minRows);
27
+ const tier = LOGO_TIERS.find((t) => cols >= t.minCols && rows >= t.minRows);
40
28
  return tier?.file ?? 'ca-dotmatrix-small.txt';
41
29
  }
42
30
  function paint(text, color) {
43
31
  if (!color)
44
32
  return text;
45
- // multiline keeps the gradient aligned down the whole block; fall back to a
46
- // per-line gradient if the installed gradient-string lacks .multiline.
47
33
  const fn = brand;
48
34
  return typeof fn.multiline === 'function'
49
35
  ? fn.multiline(text)
package/dist/core/menu.js CHANGED
@@ -1,6 +1,3 @@
1
- /**
2
- * Minimalist menu system
3
- */
4
1
  import { runMenu } from './components/menu.js';
5
2
  import { type, space, glyph } from './theme.js';
6
3
  import { clearScreen } from './ui.js';
package/dist/core/text.js CHANGED
@@ -25,13 +25,7 @@ function charWidth(ch) {
25
25
  (cp >= 0x2A700 && cp <= 0x2CEAF) ||
26
26
  (cp >= 0x2CEB0 && cp <= 0x2EBEF) ||
27
27
  (cp >= 0x30000 && cp <= 0x323AF) ||
28
- // Emoji block (Misc Symbols & Pictographs, Emoticons, Transport, Chess
29
- // Symbols, Supplemental Symbols & Pictographs, Extended-A). Real
30
- // terminals render these as double-width glyphs; undercounting even one
31
- // emoji is enough to push a line one column past the terminal width and
32
- // trigger an unwanted auto-wrap — this is a real bug that was found: an
33
- // emoji-titled event line loading in scrolled the app's header out of
34
- // view because of exactly this miscount.
28
+ // Emoji blocks render as double-width terminal glyphs.
35
29
  (cp >= 0x1F300 && cp <= 0x1FAFF)) ? 2 : 1;
36
30
  }
37
31
  /** Strip ANSI escape sequences from a string. */
package/dist/core/ui.js CHANGED
@@ -1,41 +1,24 @@
1
- /**
2
- * Minimalist UI component library
3
- * Delegates to self-rendered widgets for terminal output
4
- */
5
1
  import { success, error, warning, info } from './components/messages.js';
6
2
  import { startSpinner } from './components/spinner.js';
7
3
  import chalk from 'chalk';
8
4
  import { pickIcon } from './icons.js';
9
5
  import { t } from '../i18n/index.js';
10
6
  export { success, error, warning, info };
11
- /**
12
- * Display divider line
13
- */
14
7
  export function printDivider() {
15
8
  const terminalWidth = process.stdout.columns || 80;
16
9
  const dividerChar = pickIcon('─', '-');
17
10
  console.log(chalk.dim(dividerChar.repeat(Math.min(terminalWidth, 80))));
18
11
  }
19
- /**
20
- * Clear screen
21
- */
22
12
  export function clearScreen() {
23
13
  if (process.stdout.isTTY) {
24
14
  console.clear();
25
15
  }
26
16
  }
27
- /**
28
- * Print empty lines
29
- */
30
17
  export function printNewLine(count = 1) {
31
18
  for (let i = 0; i < count; i++) {
32
19
  console.log();
33
20
  }
34
21
  }
35
- /**
36
- * Create and start a real async spinner.
37
- * Caller is responsible for calling .stop(msg) or .stop(msg, 1) on error.
38
- */
39
22
  export function createSpinner(msg) {
40
23
  return startSpinner(msg);
41
24
  }
@@ -401,21 +401,8 @@ export async function loadDocForReader(filePath) {
401
401
  return { path: filePath, title: renderedDoc.title, lines: renderedDoc.rendered.split('\n'), links };
402
402
  }
403
403
  // ─── Document tree ────────────────────────────────────────────────────────────
404
- // Sourced from a live audit of nbtca/documents (2026-07-18): `about` and
405
- // `concepts` are two whole new top-level sections added in the repo's wiki
406
- // reconstruction (5abcc4d, 5beee27) -- omitted here, buildSections() below
407
- // silently drops every file under them, which is exactly what happened
408
- // before this fix caught up to the upstream restructuring. `about` leads
409
- // (org intro for newcomers) and `concepts` sits after the practical guide
410
- // as reference material.
411
404
  const TOP_SECTION_ORDER = ['about', 'guide', 'repair', 'concepts', 'archived'];
412
405
  const TOP_SECTION_SKIP = new Set(['docs', 'index.md', 'README.md']);
413
- // tutorial/ and process/ are two folders on disk but one section everywhere
414
- // a reader actually sees them: nbtca/documents' own site nav collapses both
415
- // under a single "指南/Guide" entry, and tutorial/sidebar.ts spells out why
416
- // ("「指南」= 教程(学技术)+流程(办社务)高内聚合并为一栏") -- presenting
417
- // them as two separate top-level categories in the terminal was true to the
418
- // folder layout but false to how the content is actually meant to be read.
419
406
  const SECTION_ALIAS = { tutorial: 'guide', process: 'guide' };
420
407
  export function localizeDocSections(sections, trans = t()) {
421
408
  const labels = {
@@ -427,10 +414,6 @@ export function localizeDocSections(sections, trans = t()) {
427
414
  };
428
415
  return sections.map((section) => ({ ...section, label: labels[section.key] ?? section.label }));
429
416
  }
430
- /**
431
- * Convert a kebab-case filename to a display-friendly title.
432
- * Preserves Chinese characters and date prefixes.
433
- */
434
417
  export function cleanFileName(name) {
435
418
  const base = name.replace(/\.md$/, '');
436
419
  if (/^[\d.]/.test(base))
@@ -439,23 +422,6 @@ export function cleanFileName(name) {
439
422
  .replace(/[-_]/g, ' ')
440
423
  .replace(/\b([a-z])/g, (_, c) => c.toUpperCase());
441
424
  }
442
- /**
443
- * Real titles (each document's own top-level `# heading`) for the
444
- * curated tutorial/process/repair sections, keyed by repo-relative path.
445
- * These are hand-authored English-filename docs with Chinese content —
446
- * mechanically title-casing the filename ("Clean Drive C") reads as a
447
- * different, lower-quality product than the document's own title ("C盘
448
- * 清理标准化流程"). Deliberately scoped to these three sections only:
449
- * `archived/`'s meeting notes are informal and often share the same
450
- * generic real heading across many different dates (e.g. five different
451
- * files all titled just "维修日") — there, the current filename-derived,
452
- * date-prefixed label is more useful for telling entries apart than the
453
- * real heading would be, so it is intentionally left as-is.
454
- *
455
- * Pulled from a live audit of the actual nbtca/documents content
456
- * (2026-07-16). A doc added later without an entry here simply falls
457
- * back to `cleanFileName` — never an error, never a blank label.
458
- */
459
425
  const KNOWN_DOC_TITLES = {
460
426
  'tutorial/2025/clean-drive-c.md': 'C盘清理标准化流程',
461
427
  'tutorial/2025/edu-email.md': '教育邮箱用途',
@@ -479,14 +445,9 @@ const KNOWN_DOC_TITLES = {
479
445
  'repair/tools.md': '软件仓库(校内镜像站)',
480
446
  'repair/weekend.md': '维修工单系统 (weekend)',
481
447
  };
482
- /** Display title for a tutorial/process/repair doc: the real, known title
483
- * when we have one, otherwise the same filename-derived fallback used
484
- * everywhere else (including for every archived/ doc, which never has a
485
- * known-title entry by design). */
486
448
  export function displayDocTitle(path, name) {
487
449
  return KNOWN_DOC_TITLES[path] ?? cleanFileName(name);
488
450
  }
489
- /** Group flat DocItem list into top-level sections. */
490
451
  export function buildSections(all) {
491
452
  const groups = new Map();
492
453
  for (const item of all) {
@@ -512,7 +473,6 @@ export function buildSections(all) {
512
473
  files: groups.get(k),
513
474
  })));
514
475
  }
515
- /** Group archived files by their second path component (year / manual / etc.). */
516
476
  export function getArchivedGroups(files) {
517
477
  const groups = new Map();
518
478
  for (const item of files) {
@@ -523,8 +483,6 @@ export function getArchivedGroups(files) {
523
483
  }
524
484
  return groups;
525
485
  }
526
- /** Raw fetch, no spinner/UI — throws on failure. Shared by the classic and
527
- * native-view loaders. */
528
486
  export async function fetchAllDocs() {
529
487
  return docsClient.listAll();
530
488
  }
@@ -1,6 +1,3 @@
1
- /**
2
- * Links — open NBTCA resources in browser
3
- */
4
1
  import open from 'open';
5
2
  import chalk from 'chalk';
6
3
  import { runMenu, menuFooter } from '../core/components/menu.js';
@@ -298,8 +298,7 @@ export function renderWeekGrid(meetings, periods, weekNumber, now, cols = 80, cu
298
298
  // at its starting period — later periods in its span show a plain
299
299
  // connector instead of repeating the same course/location text down the
300
300
  // whole column. A genuine conflict (two meetings both starting at the
301
- // same weekday+period) is rare and, like the pre-existing lookup, just
302
- // shows whichever one is found first.
301
+ // same weekday+period) shows whichever one is found first.
303
302
  const startingAt = (wd, period) => week.find((m) => m.weekday === wd && m.startPeriod === period);
304
303
  const continuingAt = (wd, period) => week.find((m) => m.weekday === wd && m.startPeriod < period && period <= m.endPeriod);
305
304
  const lines = [];
@@ -212,13 +212,6 @@ export function peekTodayLines(now = new Date()) {
212
212
  return [];
213
213
  }
214
214
  }
215
- /** Best-effort, cache-only (no network) computation of this week's per-day
216
- * class signal, for Home's combined week-overview grid. Returns null when
217
- * there's no set-up personal timetable, the cache is unusable, or the term
218
- * hasn't started yet (current week < 1) — the same "before term start"
219
- * guard already fixed once for Schedule's own hub (a future-dated weekOne,
220
- * auto-inferred while on break, must not render a nonsensical negative
221
- * week's worth of content). */
222
215
  export function peekWeekAheadInfo(now = new Date()) {
223
216
  try {
224
217
  const ptr = loadCurrentPointer();
@@ -1,6 +1,3 @@
1
- /**
2
- * Unified settings — language, theme, about
3
- */
4
1
  import chalk from 'chalk';
5
2
  import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../config/preferences.js';
6
3
  import { pickIcon } from '../core/icons.js';
@@ -8,7 +5,7 @@ import { resetIconCache } from '../core/icons.js';
8
5
  import { padEndV } from '../core/text.js';
9
6
  import { success, warning } from '../core/ui.js';
10
7
  import { APP_INFO, URLS } from '../config/data.js';
11
- import { t, getCurrentLanguage, setLanguage, clearTranslationCache } from '../i18n/index.js';
8
+ import { t, getCurrentLanguage, saveLanguagePreference, clearTranslationCache } from '../i18n/index.js';
12
9
  import { runMenu, menuFooter } from '../core/components/menu.js';
13
10
  import { note } from '../core/components/note.js';
14
11
  import { enterScreen, breadcrumb } from '../core/transitions.js';
@@ -76,7 +73,7 @@ export async function showSettingsMenu() {
76
73
  if (language === null)
77
74
  continue;
78
75
  if (language !== currentLang) {
79
- const saved = setLanguage(language);
76
+ const saved = saveLanguagePreference(language);
80
77
  clearTranslationCache();
81
78
  notifyResult(saved, t().language.changed, t().language.changedSessionOnly);
82
79
  }
@@ -11,15 +11,12 @@ import { t } from '../i18n/index.js';
11
11
  function getServiceTargets() {
12
12
  const trans = t();
13
13
  return [
14
- // NBTCA-owned services
15
14
  { name: trans.status.serviceHomepage, url: URLS.homepage, group: 'nbtca' },
16
15
  { name: trans.status.serviceDocs, url: URLS.docs, group: 'nbtca' },
17
16
  { name: trans.status.serviceIcal, url: URLS.calendar, group: 'nbtca' },
18
17
  { name: trans.status.serviceRepair, url: URLS.repair, group: 'nbtca' },
19
- // External platforms
20
18
  { name: trans.status.serviceGithub, url: URLS.github, group: 'external' },
21
19
  { name: trans.status.serviceRoadmap, url: URLS.roadmap, group: 'external' },
22
- // Intranet services (campus LAN only)
23
20
  { name: trans.status.serviceCloud, url: URLS.cloud, group: 'intranet', intranet: true },
24
21
  { name: trans.status.serviceMirror, url: URLS.mirror, group: 'intranet', intranet: true },
25
22
  ];
@@ -1,6 +1,3 @@
1
- /**
2
- * Theme CLI command handler (non-interactive)
3
- */
4
1
  import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../config/preferences.js';
5
2
  import { resetIconCache } from '../core/icons.js';
6
3
  import { t } from '../i18n/index.js';
@@ -1,7 +1,3 @@
1
- /**
2
- * Version update checker
3
- * Non-blocking check against npm registry for newer versions.
4
- */
5
1
  import chalk from 'chalk';
6
2
  import { APP_INFO } from '../config/data.js';
7
3
  import { t, fmt } from '../i18n/index.js';
@@ -34,9 +30,6 @@ async function fetchLatestVersion(signal) {
34
30
  signal?.removeEventListener('abort', onExternalAbort);
35
31
  }
36
32
  }
37
- /**
38
- * Compare semver strings. Returns true if remote > local.
39
- */
40
33
  function isNewer(local, remote) {
41
34
  const parse = (v) => v.split('.').map(Number);
42
35
  const l = parse(local);
@@ -62,9 +55,6 @@ export async function checkForUpdate(signal) {
62
55
  const trans = t();
63
56
  return `${fmt(trans.update.available, { latest, current: APP_INFO.version })} ${chalk.dim(trans.update.command)}`;
64
57
  }
65
- /**
66
- * Explicit update check command (nbtca update).
67
- */
68
58
  export async function runUpdateCheck() {
69
59
  const trans = t();
70
60
  const latest = await fetchLatestVersion();
@@ -1,7 +1,3 @@
1
- /**
2
- * Internationalization (i18n) System
3
- * Multi-language support for the application
4
- */
5
1
  import fs from 'fs';
6
2
  import path from 'path';
7
3
  import { fileURLToPath } from 'url';
@@ -9,25 +5,13 @@ import { dirname } from 'path';
9
5
  import { getConfigDir, getWritableConfigDir } from '../config/paths.js';
10
6
  const __filename = fileURLToPath(import.meta.url);
11
7
  const __dirname = dirname(__filename);
12
- /**
13
- * Language configuration
14
- */
15
- let currentLanguage = 'zh'; // Default to Chinese
16
- /**
17
- * Get language configuration file path (read, with legacy fallback)
18
- */
8
+ let currentLanguage = 'zh';
19
9
  function getLanguageConfigPath() {
20
10
  return path.join(getConfigDir(), 'language.json');
21
11
  }
22
- /**
23
- * Get writable language configuration file path (XDG, creates dir)
24
- */
25
12
  function getWritableLanguageConfigPath() {
26
13
  return path.join(getWritableConfigDir(), 'language.json');
27
14
  }
28
- /**
29
- * Load language preference from config file
30
- */
31
15
  export function loadLanguagePreference() {
32
16
  try {
33
17
  const configPath = getLanguageConfigPath();
@@ -36,61 +20,39 @@ export function loadLanguagePreference() {
36
20
  currentLanguage = config.language;
37
21
  }
38
22
  }
39
- catch {
40
- // If loading fails (file missing or invalid), use default (Chinese)
41
- }
23
+ catch { }
42
24
  return currentLanguage;
43
25
  }
44
- /**
45
- * Save language preference to config file
46
- */
47
26
  export function saveLanguagePreference(language) {
27
+ setLanguage(language);
48
28
  try {
49
29
  const configPath = getWritableLanguageConfigPath();
50
30
  fs.writeFileSync(configPath, JSON.stringify({ language }, null, 2));
51
- currentLanguage = language;
52
31
  return true;
53
32
  }
54
33
  catch {
55
34
  return false;
56
35
  }
57
36
  }
58
- /**
59
- * Get current language
60
- */
61
37
  export function getCurrentLanguage() {
62
38
  return currentLanguage;
63
39
  }
64
- /**
65
- * Set current language
66
- */
67
40
  export function setLanguage(language) {
68
41
  currentLanguage = language;
69
- return saveLanguagePreference(language);
70
42
  }
71
- /**
72
- * Load translation file
73
- */
74
43
  function loadTranslations(language) {
75
44
  try {
76
45
  const translationPath = path.join(__dirname, 'locales', `${language}.json`);
77
46
  const content = fs.readFileSync(translationPath, 'utf-8');
78
47
  return JSON.parse(content);
79
48
  }
80
- catch (err) {
81
- // Fallback to Chinese if loading fails
49
+ catch {
82
50
  const fallbackPath = path.join(__dirname, 'locales', 'zh.json');
83
51
  const content = fs.readFileSync(fallbackPath, 'utf-8');
84
52
  return JSON.parse(content);
85
53
  }
86
54
  }
87
- /**
88
- * Translation cache
89
- */
90
- let translationsCache = new Map();
91
- /**
92
- * Get translations for current language
93
- */
55
+ const translationsCache = new Map();
94
56
  export function t() {
95
57
  if (!translationsCache.has(currentLanguage)) {
96
58
  translationsCache.set(currentLanguage, loadTranslations(currentLanguage));
@@ -103,11 +65,7 @@ export function fmt(template, vars) {
103
65
  return val !== undefined ? String(val) : `{${key}}`;
104
66
  });
105
67
  }
106
- /**
107
- * Clear translation cache (useful when switching languages)
108
- */
109
68
  export function clearTranslationCache() {
110
69
  translationsCache.clear();
111
70
  }
112
- // Initialize language preference on module load
113
71
  loadLanguagePreference();
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- /**
2
- * NBTCA Prompt entry point
3
- */
4
1
  import chalk from 'chalk';
5
2
  import open from 'open';
6
3
  import { main } from './main.js';
@@ -11,7 +8,7 @@ import { pickIcon } from './core/icons.js';
11
8
  import { applyColorModePreference } from './config/preferences.js';
12
9
  import { openDocsInBrowser } from './features/docs.js';
13
10
  import { runThemeCommand } from './features/theme.js';
14
- import { setLanguage, t, fmt } from './i18n/index.js';
11
+ import { saveLanguagePreference, t, fmt } from './i18n/index.js';
15
12
  import { clearScreen, handleGracefulExit } from './core/ui.js';
16
13
  import { APP_INFO, URLS } from './config/data.js';
17
14
  import { runUpdateCheck } from './features/update.js';
@@ -376,7 +373,7 @@ async function runCommandMode(argv) {
376
373
  console.error(chalk.red(t().cli.invalidLang));
377
374
  process.exit(1);
378
375
  }
379
- const persisted = setLanguage(language);
376
+ const persisted = saveLanguagePreference(language);
380
377
  if (persisted) {
381
378
  console.log(chalk.green(`${pickIcon('✓', 'OK')}: ${t().language.changed}`));
382
379
  }
@@ -464,7 +461,6 @@ async function runCommandMode(argv) {
464
461
  note(content, trans.about.title);
465
462
  return;
466
463
  }
467
- // URL actions: repair, website, github, roadmap
468
464
  const mappedUrl = URL_ACTIONS[action];
469
465
  if (mappedUrl) {
470
466
  if (flags.has('--open')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbtca/prompt",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -33,14 +33,14 @@
33
33
  "terminal",
34
34
  "welcome",
35
35
  "nbtca",
36
- "ningbotech",
37
- "computer-association",
36
+ "community",
37
+ "calendar",
38
38
  "tui",
39
39
  "interactive"
40
40
  ],
41
41
  "dependencies": {
42
- "@nbtca/docs": "^0.2.2",
43
- "@nbtca/nbtcal": "^0.3.5",
42
+ "@nbtca/docs": "^0.2.3",
43
+ "@nbtca/nbtcal": "^0.3.7",
44
44
  "chalk": "^5.6.2",
45
45
  "cheerio": "1.0.0",
46
46
  "fetch-cookie": "^3.2.0",
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/gradient-string": "^1.1.6",
55
- "@types/node": "^22.19.17",
56
- "tsx": "^4.21.0",
55
+ "@types/node": "^22.20.1",
56
+ "tsx": "^4.23.5",
57
57
  "typescript": "^5.9.3",
58
58
  "vitest": "^3.2.7"
59
59
  },
@@ -73,5 +73,8 @@
73
73
  "url": "https://github.com/nbtca/prompt/issues"
74
74
  },
75
75
  "homepage": "https://github.com/nbtca/prompt#readme",
76
- "description": "NBTCA Prompt - Minimalist CLI tool for NingboTech Computer Association"
76
+ "description": "Terminal information system for the NBTCA community",
77
+ "directories": {
78
+ "doc": "docs"
79
+ }
77
80
  }