@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 +1 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/dist/app/views/docs.js +0 -7
- package/dist/app/views/home.js +8 -49
- package/dist/app/views/schedule.js +0 -9
- package/dist/app/views/settings.js +2 -2
- package/dist/config/data.js +1 -2
- package/dist/core/logo.js +2 -16
- package/dist/core/menu.js +0 -3
- package/dist/core/text.js +1 -7
- package/dist/core/ui.js +0 -17
- package/dist/features/docs.js +0 -42
- package/dist/features/links.js +0 -3
- package/dist/features/schedule-render.js +1 -2
- package/dist/features/schedule-view.js +0 -7
- package/dist/features/settings.js +2 -5
- package/dist/features/status.js +0 -3
- package/dist/features/theme.js +0 -3
- package/dist/features/update.js +0 -10
- package/dist/i18n/index.js +5 -47
- package/dist/index.js +2 -6
- package/package.json +11 -8
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# NBTCA Prompt
|
|
2
2
|
|
|
3
|
-
Terminal-based information system for
|
|
3
|
+
Terminal-based information system for the NBTCA community.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@nbtca/prompt)
|
|
6
6
|
[](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
|
|
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
|
|
package/dist/app/views/docs.js
CHANGED
|
@@ -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 = [
|
package/dist/app/views/home.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
218
|
-
const weekEnd = new Date(
|
|
219
|
-
const weekEvents = cal.inRange(
|
|
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:
|
|
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,
|
|
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,
|
|
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 =
|
|
116
|
+
const saved = saveLanguagePreference(result.selected);
|
|
117
117
|
clearTranslationCache();
|
|
118
118
|
goToMenu(saved ? t().language.changed : t().language.changedSessionOnly);
|
|
119
119
|
}
|
package/dist/config/data.js
CHANGED
|
@@ -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: '
|
|
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
|
-
|
|
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 =
|
|
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
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
|
|
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
|
}
|
package/dist/features/docs.js
CHANGED
|
@@ -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
|
}
|
package/dist/features/links.js
CHANGED
|
@@ -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)
|
|
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,
|
|
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 =
|
|
76
|
+
const saved = saveLanguagePreference(language);
|
|
80
77
|
clearTranslationCache();
|
|
81
78
|
notifyResult(saved, t().language.changed, t().language.changedSessionOnly);
|
|
82
79
|
}
|
package/dist/features/status.js
CHANGED
|
@@ -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
|
];
|
package/dist/features/theme.js
CHANGED
|
@@ -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';
|
package/dist/features/update.js
CHANGED
|
@@ -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();
|
package/dist/i18n/index.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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 =
|
|
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.
|
|
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
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"community",
|
|
37
|
+
"calendar",
|
|
38
38
|
"tui",
|
|
39
39
|
"interactive"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@nbtca/docs": "^0.2.
|
|
43
|
-
"@nbtca/nbtcal": "^0.3.
|
|
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.
|
|
56
|
-
"tsx": "^4.
|
|
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": "
|
|
76
|
+
"description": "Terminal information system for the NBTCA community",
|
|
77
|
+
"directories": {
|
|
78
|
+
"doc": "docs"
|
|
79
|
+
}
|
|
77
80
|
}
|