@nbtca/prompt 1.1.2 → 1.2.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.
@@ -1,7 +1,4 @@
1
- /**
2
- * 核心URL配置
3
- * 集中管理所有外部链接
4
- */
1
+ /** Core URL and application constants. */
5
2
  import { readFileSync } from 'fs';
6
3
  import { fileURLToPath } from 'url';
7
4
  import { dirname, join } from 'path';
@@ -25,6 +22,8 @@ export const URLS = {
25
22
  repair: 'https://nbtca.space/repair',
26
23
  calendar: 'https://ical.nbtca.space',
27
24
  email: 'contact@nbtca.space',
25
+ cloud: 'https://cloud.nbtca.space',
26
+ mirror: 'https://i.nbtca.space',
28
27
  };
29
28
  export const GITHUB_REPO = {
30
29
  owner: 'nbtca',
@@ -34,7 +33,7 @@ export const GITHUB_REPO = {
34
33
  export const APP_INFO = {
35
34
  name: 'Prompt',
36
35
  version: readPackageVersion(),
37
- description: '浙大宁波理工学院计算机协会',
36
+ description: 'NingboTech Computer Association',
38
37
  author: 'm1ngsama <contact@m1ng.space>',
39
38
  license: 'MIT',
40
39
  repository: 'https://github.com/nbtca/prompt'
package/dist/core/menu.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { select, isCancel, outro } from '@clack/prompts';
5
5
  import chalk from 'chalk';
6
- import { showCalendar } from '../features/calendar.js';
6
+ import { showCalendarMenu } from '../features/calendar.js';
7
7
  import { showDocsMenu } from '../features/docs.js';
8
8
  import { showServiceStatus } from '../features/status.js';
9
9
  import { showLinksMenu } from '../features/links.js';
@@ -36,7 +36,7 @@ export async function showMainMenu() {
36
36
  export async function runMenuAction(action) {
37
37
  switch (action) {
38
38
  case 'events':
39
- await showCalendar();
39
+ await showCalendarMenu();
40
40
  break;
41
41
  case 'docs':
42
42
  await showDocsMenu();
@@ -0,0 +1,22 @@
1
+ import chalk from 'chalk';
2
+ export const c = {
3
+ brand: (s) => chalk.hex('#0ea5e9')(s),
4
+ accent: (s) => chalk.cyan(s),
5
+ success: (s) => chalk.green(s),
6
+ error: (s) => chalk.red(s),
7
+ warn: (s) => chalk.yellow(s),
8
+ heading: (s) => chalk.bold.white(s),
9
+ muted: (s) => chalk.dim(s),
10
+ subtle: (s) => chalk.gray(s),
11
+ label: (s) => chalk.bold.cyan(s),
12
+ url: (s) => chalk.dim.underline(s),
13
+ version: (s) => chalk.dim(s),
14
+ latency: (ms) => {
15
+ const s = `${ms}ms`;
16
+ if (ms < 200)
17
+ return chalk.green(s);
18
+ if (ms < 1000)
19
+ return chalk.yellow(s);
20
+ return chalk.red(s);
21
+ },
22
+ };
@@ -1,12 +1,8 @@
1
- /**
2
- * Calendar module
3
- * Fetches and renders upcoming events with Unicode box table.
4
- * Data layer powered by @nbtca/nbtcal.
5
- */
6
1
  import { loadCalendar, FeedFetchError, FeedParseError } from '@nbtca/nbtcal';
7
2
  import chalk from 'chalk';
8
3
  import { select, isCancel } from '@clack/prompts';
9
4
  import { info, createSpinner } from '../core/ui.js';
5
+ import { c } from '../core/theme.js';
10
6
  import { pickIcon } from '../core/icons.js';
11
7
  import { padEndV, truncate } from '../core/text.js';
12
8
  import { t } from '../i18n/index.js';
@@ -26,9 +22,6 @@ function formatTime(date) {
26
22
  const minutes = String(date.getMinutes()).padStart(2, '0');
27
23
  return `${hours}:${minutes}`;
28
24
  }
29
- /**
30
- * Load the calendar, wrapping errors with a localized message.
31
- */
32
25
  async function loadCalendarOrThrow() {
33
26
  try {
34
27
  return await loadCalendar({ timeoutMs: 15000 });
@@ -40,9 +33,6 @@ async function loadCalendarOrThrow() {
40
33
  throw new Error(`${t().calendar.error}: ${detail}`);
41
34
  }
42
35
  }
43
- /**
44
- * Map a nbtcal CalendarEvent to prompt's Event type.
45
- */
46
36
  export function toDisplayEvent(e) {
47
37
  const trans = t();
48
38
  return {
@@ -54,15 +44,9 @@ export function toDisplayEvent(e) {
54
44
  startDate: e.start,
55
45
  };
56
46
  }
57
- /**
58
- * Fetch upcoming events (next 30 days), including recurring occurrences.
59
- */
60
47
  export async function fetchEvents() {
61
48
  return (await loadCalendarOrThrow()).upcoming({ days: 30 }).map(toDisplayEvent);
62
49
  }
63
- /**
64
- * Fetch trailing-year heatmap buckets.
65
- */
66
50
  export async function fetchHeatmapBuckets() {
67
51
  const now = new Date();
68
52
  const start = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
@@ -78,76 +62,139 @@ export function serializeEvents(events) {
78
62
  startDateISO: event.startDate.toISOString(),
79
63
  }));
80
64
  }
81
- /**
82
- * Render events as a Unicode box-drawing table
83
- */
84
65
  export function renderEventsTable(events, options) {
85
66
  const trans = t();
86
- const color = options?.color !== false;
67
+ const useColor = options?.color !== false;
87
68
  if (events.length === 0)
88
- return trans.calendar.noEvents;
89
- const dateWidth = 16;
90
- const titleWidth = 30;
91
- const locationWidth = 16;
92
- const h = pickIcon('─', '-');
93
- const v = pickIcon('│', '|');
94
- const topLeft = pickIcon('┌', '+');
95
- const topMid = pickIcon('┬', '+');
96
- const topRight = pickIcon('┐', '+');
97
- const midLeft = pickIcon('├', '+');
98
- const midMid = pickIcon('┼', '+');
99
- const midRight = pickIcon('┤', '+');
100
- const bottomLeft = pickIcon('└', '+');
101
- const bottomMid = pickIcon('┴', '+');
102
- const bottomRight = pickIcon('┘', '+');
103
- const top = `${topLeft}${h.repeat(dateWidth + 2)}${topMid}${h.repeat(titleWidth + 2)}${topMid}${h.repeat(locationWidth + 2)}${topRight}`;
104
- const divider = `${midLeft}${h.repeat(dateWidth + 2)}${midMid}${h.repeat(titleWidth + 2)}${midMid}${h.repeat(locationWidth + 2)}${midRight}`;
105
- const bottom = `${bottomLeft}${h.repeat(dateWidth + 2)}${bottomMid}${h.repeat(titleWidth + 2)}${bottomMid}${h.repeat(locationWidth + 2)}${bottomRight}`;
106
- const headerRow = `${v} ${padEndV(trans.calendar.dateTime, dateWidth)} ${v} ${padEndV(trans.calendar.eventName, titleWidth)} ${v} ${padEndV(trans.calendar.location, locationWidth)} ${v}`;
107
- // Formatters are identity functions when color is off — one loop, no duplication
69
+ return ` ${trans.calendar.noEvents}`;
108
70
  const id = (s) => s;
109
- const dim = color ? chalk.dim : id;
110
- const bold = color ? chalk.bold : id;
111
- const fmtDate = color ? chalk.cyan : id;
112
- const fmtTitle = color ? chalk.white : id;
113
- const fmtLoc = color ? chalk.gray : id;
114
- const lines = [dim(top), bold(headerRow), dim(divider)];
71
+ const applyDim = useColor ? chalk.dim : id;
72
+ const applyCyan = useColor ? chalk.cyan : id;
73
+ const applyBold = useColor ? chalk.bold : id;
74
+ const applyGray = useColor ? chalk.gray : id;
75
+ // dateWidth must fit YYYY-MM-DD HH:MM (16 chars) for cross-year events
76
+ const dateWidth = 16;
77
+ const titleWidth = 32;
78
+ const locWidth = 14;
79
+ const sep = pickIcon('─', '-');
80
+ const headerDate = padEndV(applyDim(trans.calendar.dateTime), dateWidth);
81
+ const headerTitle = padEndV(applyDim(trans.calendar.eventName), titleWidth);
82
+ const headerLoc = applyDim(trans.calendar.location);
83
+ // divider covers exactly: dateWidth + 2-char sep + titleWidth + 2-char sep + locWidth
84
+ const divider = applyDim(sep.repeat(dateWidth + 2 + titleWidth + 2 + locWidth));
85
+ const lines = [
86
+ ` ${headerDate} ${headerTitle} ${headerLoc}`,
87
+ ` ${divider}`,
88
+ ];
115
89
  for (const event of events) {
116
- const dateTime = `${event.date} ${event.time}`;
117
- const title = truncate(event.title, titleWidth);
118
- const location = truncate(event.location, locationWidth);
119
- lines.push(`${v} ${fmtDate(padEndV(dateTime, dateWidth))} ${v} ${fmtTitle(padEndV(title, titleWidth))} ${v} ${fmtLoc(padEndV(location, locationWidth))} ${v}`);
90
+ const dateTime = event.time ? `${event.date} ${event.time}` : event.date;
91
+ const dateCol = padEndV(applyCyan(dateTime), dateWidth);
92
+ const titleCol = padEndV(applyBold(truncate(event.title, titleWidth)), titleWidth);
93
+ const locCol = applyGray(truncate(event.location, locWidth));
94
+ lines.push(` ${dateCol} ${titleCol} ${locCol}`);
120
95
  }
121
- lines.push(dim(bottom));
122
96
  return lines.join('\n');
123
97
  }
124
- function displayEvents(events) {
125
- if (events.length === 0) {
126
- info(t().calendar.noEvents);
127
- return;
128
- }
129
- console.log();
130
- console.log(renderEventsTable(events, { color: true }));
131
- console.log(chalk.dim(` ${pickIcon('📅', '[ical]')} ${t().calendar.subscribeHint}: ${URLS.calendar}`));
132
- console.log();
98
+ function renderSubscribeHint() {
99
+ const icon = pickIcon('◆', '*');
100
+ console.log(c.muted(` ${icon} ${t().calendar.subscribeHint}: ${URLS.calendar}`));
133
101
  }
134
102
  async function showEventDetail(event) {
135
103
  const trans = t();
136
104
  console.log();
137
105
  console.log(chalk.bold.cyan(` ${event.title}`));
138
- console.log(chalk.dim(` ${event.date} ${event.time} ${pickIcon('·', '|')} ${event.location}`));
106
+ console.log(c.muted(` ${event.date}${event.time ? ' ' + event.time : ''} ${pickIcon('·', '|')} ${event.location}`));
139
107
  if (event.description) {
140
108
  console.log();
141
- const lines = event.description.trim().split('\n');
142
- for (const line of lines) {
143
- console.log(chalk.white(` ${line}`));
109
+ for (const line of event.description.trim().split('\n')) {
110
+ console.log(` ${line}`);
144
111
  }
145
112
  }
146
113
  else {
147
- console.log(chalk.dim(` ${trans.calendar.noDescription}`));
114
+ console.log(c.muted(` ${trans.calendar.noDescription}`));
148
115
  }
149
116
  console.log();
150
117
  }
118
+ /** Startup preview: auto-loads and displays upcoming events, then returns. */
119
+ export async function showEventsPreview() {
120
+ const trans = t();
121
+ const s = createSpinner(trans.calendar.loading);
122
+ try {
123
+ const cal = await loadCalendarOrThrow();
124
+ const events = cal.upcoming({ days: 30 }).map(toDisplayEvent);
125
+ if (events.length === 0) {
126
+ s.stop(trans.calendar.noEvents);
127
+ }
128
+ else {
129
+ s.stop(`${events.length} ${trans.calendar.eventsFound}`);
130
+ }
131
+ console.log();
132
+ console.log(renderEventsTable(events.slice(0, 5), { color: !!process.stdout.isTTY }));
133
+ console.log();
134
+ if (events.length > 0)
135
+ renderSubscribeHint();
136
+ console.log();
137
+ }
138
+ catch {
139
+ s.error(trans.calendar.error);
140
+ console.log();
141
+ }
142
+ }
143
+ /** Submenu: choose between upcoming and past events. */
144
+ export async function showCalendarMenu() {
145
+ const trans = t();
146
+ const choice = await select({
147
+ message: trans.menu.chooseAction,
148
+ options: [
149
+ { value: 'upcoming', label: trans.menu.events, hint: trans.menu.eventsDesc },
150
+ { value: 'past', label: trans.calendar.pastEvents, hint: trans.calendar.pastEventsDesc },
151
+ { value: '__back__', label: c.muted(trans.common.back) },
152
+ ],
153
+ });
154
+ if (isCancel(choice) || choice === '__back__')
155
+ return;
156
+ if (choice === 'upcoming')
157
+ await showCalendar();
158
+ else if (choice === 'past')
159
+ await showPastEvents();
160
+ }
161
+ /** Past events: shows historical events from the last 30 days with detail selection. */
162
+ export async function showPastEvents() {
163
+ const trans = t();
164
+ const s = createSpinner(trans.calendar.pastLoading);
165
+ try {
166
+ const cal = await loadCalendarOrThrow();
167
+ const events = cal.past({ days: 30 }).reverse().map(toDisplayEvent);
168
+ s.stop(`${events.length} ${trans.calendar.eventsFound}`);
169
+ console.log();
170
+ console.log(renderEventsTable(events, { color: true }));
171
+ console.log();
172
+ if (events.length === 0) {
173
+ info(trans.calendar.noPastEvents);
174
+ return;
175
+ }
176
+ const options = [
177
+ ...events.map((e, i) => ({
178
+ value: String(i),
179
+ label: `${e.date}${e.time ? ' ' + e.time : ''} ${e.title}`,
180
+ hint: e.location,
181
+ })),
182
+ { value: '__back__', label: c.muted(trans.common.back) },
183
+ ];
184
+ const selected = await select({ message: trans.calendar.viewPastDetail, options });
185
+ if (!isCancel(selected) && selected !== '__back__') {
186
+ const event = events[Number.parseInt(selected, 10)];
187
+ if (event)
188
+ await showEventDetail(event);
189
+ }
190
+ }
191
+ catch {
192
+ s.error(trans.calendar.error);
193
+ console.log(c.muted(' ' + trans.calendar.errorHint));
194
+ console.log();
195
+ }
196
+ }
197
+ /** Full interactive calendar: heatmap + event list + detail selection. */
151
198
  export async function showCalendar() {
152
199
  const trans = t();
153
200
  const s = createSpinner(trans.calendar.loading);
@@ -155,7 +202,6 @@ export async function showCalendar() {
155
202
  const cal = await loadCalendarOrThrow();
156
203
  const events = cal.upcoming({ days: 30 }).map(toDisplayEvent);
157
204
  s.stop(`${events.length} ${trans.calendar.eventsFound}`);
158
- // Render heatmap header
159
205
  const now = new Date();
160
206
  const heatmapBuckets = cal.heatmap({
161
207
  start: new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000),
@@ -164,31 +210,33 @@ export async function showCalendar() {
164
210
  });
165
211
  console.log();
166
212
  console.log(renderHeatmap(heatmapBuckets, now, { color: true }));
167
- displayEvents(events);
168
- if (events.length > 0) {
169
- const options = [
170
- ...events.map((e, i) => ({
171
- value: String(i),
172
- label: `${e.date} ${e.time} ${e.title}`,
173
- hint: e.location,
174
- })),
175
- { value: '__back__', label: chalk.dim(trans.common.back) },
176
- ];
177
- const selected = await select({
178
- message: trans.calendar.viewDetail,
179
- options,
180
- });
181
- if (!isCancel(selected) && selected !== '__back__') {
182
- const idx = Number.parseInt(selected, 10);
183
- const event = events[idx];
184
- if (event)
185
- await showEventDetail(event);
186
- }
213
+ console.log();
214
+ console.log(renderEventsTable(events, { color: true }));
215
+ console.log();
216
+ renderSubscribeHint();
217
+ console.log();
218
+ if (events.length === 0) {
219
+ info(trans.calendar.noEvents);
220
+ return;
221
+ }
222
+ const options = [
223
+ ...events.map((e, i) => ({
224
+ value: String(i),
225
+ label: `${e.date}${e.time ? ' ' + e.time : ''} ${e.title}`,
226
+ hint: e.location,
227
+ })),
228
+ { value: '__back__', label: c.muted(trans.common.back) },
229
+ ];
230
+ const selected = await select({ message: trans.calendar.viewDetail, options });
231
+ if (!isCancel(selected) && selected !== '__back__') {
232
+ const event = events[Number.parseInt(selected, 10)];
233
+ if (event)
234
+ await showEventDetail(event);
187
235
  }
188
236
  }
189
237
  catch {
190
238
  s.error(trans.calendar.error);
191
- console.log(chalk.gray(' ' + trans.calendar.errorHint));
239
+ console.log(c.muted(' ' + trans.calendar.errorHint));
192
240
  console.log();
193
241
  }
194
242
  }
@@ -128,7 +128,7 @@ function contentFingerprint(content) {
128
128
  return `${content.length}:${content.slice(0, 80)}:${content.slice(-80)}`;
129
129
  }
130
130
  export function clearDocsCache() {
131
- docsClient = createDocsClient(); // fresh instance resets dir + file caches
131
+ docsClient.clear();
132
132
  renderCache.clear();
133
133
  }
134
134
  async function fetchDirectory(path = '') {
@@ -439,47 +439,32 @@ async function searchDocs() {
439
439
  return;
440
440
  const keyword = query.trim().toLowerCase();
441
441
  const s = createSpinner(trans.docs.searching);
442
- // Fetch all category directories in parallel
443
- const categories = getDocCategories().filter(c => c.path !== 'README.md');
444
- const results = [];
445
442
  try {
446
- const fetches = await Promise.allSettled(categories.map(async (cat) => {
447
- const items = await fetchDirectory(cat.path);
448
- return { items, category: cat.name };
449
- }));
450
- for (const result of fetches) {
451
- if (result.status !== 'fulfilled')
452
- continue;
453
- for (const item of result.value.items) {
454
- if (item.name.toLowerCase().includes(keyword)) {
455
- results.push({ name: item.name, path: item.path, category: result.value.category });
456
- }
457
- }
458
- }
443
+ const all = await docsClient.listAll();
444
+ const results = all.filter(item => item.path.toLowerCase().includes(keyword));
459
445
  s.stop(`${results.length} ${trans.docs.searchResults}`);
446
+ if (results.length === 0) {
447
+ warning(trans.docs.searchNoResults);
448
+ return;
449
+ }
450
+ const selected = await select({
451
+ message: trans.docs.chooseDoc,
452
+ options: [
453
+ ...results.map(r => ({
454
+ value: r.path,
455
+ label: r.name,
456
+ hint: r.path.includes('/') ? r.path.split('/').slice(0, -1).join('/') : '',
457
+ })),
458
+ { value: '__back__', label: chalk.dim(trans.docs.returnToMenu) },
459
+ ],
460
+ });
461
+ if (isCancel(selected) || selected === '__back__')
462
+ return;
463
+ await viewMarkdownFile(selected);
460
464
  }
461
465
  catch {
462
466
  s.error(trans.docs.loadError);
463
- return;
464
- }
465
- if (results.length === 0) {
466
- warning(trans.docs.searchNoResults);
467
- return;
468
467
  }
469
- const selected = await select({
470
- message: trans.docs.chooseDoc,
471
- options: [
472
- ...results.map(r => ({
473
- value: r.path,
474
- label: r.name,
475
- hint: r.category,
476
- })),
477
- { value: '__back__', label: chalk.dim(trans.docs.returnToMenu) },
478
- ],
479
- });
480
- if (isCancel(selected) || selected === '__back__')
481
- return;
482
- await viewMarkdownFile(selected);
483
468
  }
484
469
  // ─── Menu ─────────────────────────────────────────────────────────────────────
485
470
  export async function showDocsMenu() {
@@ -1,17 +1,24 @@
1
1
  import chalk from 'chalk';
2
2
  import { APP_INFO, URLS } from '../config/data.js';
3
3
  import { pickIcon } from '../core/icons.js';
4
- import { padEndV } from '../core/text.js';
4
+ import { padEndV, visualWidth } from '../core/text.js';
5
+ import { c } from '../core/theme.js';
5
6
  import { createSpinner } from '../core/ui.js';
6
7
  import { t } from '../i18n/index.js';
7
8
  function getServiceTargets() {
8
9
  const trans = t();
9
10
  return [
10
- { name: trans.status.serviceWebsite, url: URLS.homepage },
11
- { name: trans.status.serviceDocs, url: URLS.docs },
12
- { name: trans.status.serviceCalendar, url: URLS.calendar },
13
- { name: trans.status.serviceGithub, url: URLS.github },
14
- { name: trans.status.serviceRoadmap, url: URLS.roadmap },
11
+ // NBTCA-owned services
12
+ { name: trans.status.serviceHomepage, url: URLS.homepage, group: 'nbtca' },
13
+ { name: trans.status.serviceDocs, url: URLS.docs, group: 'nbtca' },
14
+ { name: trans.status.serviceIcal, url: URLS.calendar, group: 'nbtca' },
15
+ { name: trans.status.serviceRepair, url: URLS.repair, group: 'nbtca' },
16
+ // External platforms
17
+ { name: trans.status.serviceGithub, url: URLS.github, group: 'external' },
18
+ { name: trans.status.serviceRoadmap, url: URLS.roadmap, group: 'external' },
19
+ // Intranet services (campus LAN only)
20
+ { name: trans.status.serviceCloud, url: URLS.cloud, group: 'intranet', intranet: true },
21
+ { name: trans.status.serviceMirror, url: URLS.mirror, group: 'intranet', intranet: true },
15
22
  ];
16
23
  }
17
24
  async function checkService(name, url, timeoutMs) {
@@ -37,25 +44,23 @@ async function checkService(name, url, timeoutMs) {
37
44
  return { name, url, ok: false, latencyMs, error };
38
45
  }
39
46
  }
40
- async function checkServiceWithRetry(name, url, timeoutMs, retries) {
41
- let lastResult = await checkService(name, url, timeoutMs);
42
- if (lastResult.ok)
43
- return lastResult;
44
- for (let attempt = 0; attempt < retries; attempt++) {
45
- // Retry for transient transport errors and upstream server errors.
46
- if (!lastResult.error && !(lastResult.statusCode != null && lastResult.statusCode >= 500)) {
47
- break;
47
+ async function checkServiceWithRetry(target, timeoutMs, retries) {
48
+ let lastResult = await checkService(target.name, target.url, timeoutMs);
49
+ if (!lastResult.ok) {
50
+ for (let attempt = 0; attempt < retries; attempt++) {
51
+ if (!lastResult.error && !(lastResult.statusCode != null && lastResult.statusCode >= 500))
52
+ break;
53
+ lastResult = await checkService(target.name, target.url, timeoutMs);
54
+ if (lastResult.ok)
55
+ break;
48
56
  }
49
- lastResult = await checkService(name, url, timeoutMs);
50
- if (lastResult.ok)
51
- return lastResult;
52
57
  }
53
- return lastResult;
58
+ return { ...lastResult, group: target.group, intranet: target.intranet };
54
59
  }
55
60
  export async function checkServices(options = {}) {
56
61
  const timeoutMs = options.timeoutMs ?? 6000;
57
62
  const retries = options.retries ?? 1;
58
- return Promise.all(getServiceTargets().map((service) => checkServiceWithRetry(service.name, service.url, timeoutMs, retries)));
63
+ return Promise.all(getServiceTargets().map(t => checkServiceWithRetry(t, timeoutMs, retries)));
59
64
  }
60
65
  export function serializeServiceStatus(items) {
61
66
  return items.map((item) => ({
@@ -65,59 +70,69 @@ export function serializeServiceStatus(items) {
65
70
  statusCode: item.statusCode ?? null,
66
71
  latencyMs: item.latencyMs ?? null,
67
72
  error: item.error ?? null,
73
+ group: item.group ?? null,
74
+ intranet: item.intranet ?? false,
68
75
  }));
69
76
  }
70
77
  export function hasServiceFailures(items) {
71
- return items.some((item) => !item.ok);
78
+ return items.some(item => !item.ok && !item.intranet);
72
79
  }
73
80
  export function countServiceHealth(items) {
74
81
  let up = 0;
75
82
  let down = 0;
76
- for (const item of items) {
77
- if (item.ok) {
78
- up += 1;
79
- }
80
- else {
81
- down += 1;
82
- }
83
+ for (const item of items.filter(i => !i.intranet)) {
84
+ if (item.ok)
85
+ up++;
86
+ else
87
+ down++;
83
88
  }
84
89
  return { up, down };
85
90
  }
86
91
  export function renderServiceStatusTable(items, options) {
87
- const color = options?.color !== false;
92
+ const useColor = options?.color !== false;
88
93
  const id = (s) => s;
89
- const dim = color ? chalk.dim : id;
90
- const green = color ? chalk.green : id;
91
- const red = color ? chalk.red : id;
92
- const cyan = color ? chalk.cyan : id;
94
+ const applyDim = useColor ? chalk.dim : id;
95
+ const applyGreen = useColor ? chalk.green : id;
96
+ const applyRed = useColor ? chalk.red : id;
97
+ const applyCyan = useColor ? chalk.cyan : id;
98
+ const applyLatency = useColor ? c.latency : (ms) => `${ms}ms`;
93
99
  const trans = t();
94
- const nameWidth = 10;
95
- const statusWidth = 9;
96
- const latencyWidth = 10;
97
- const h = pickIcon('─', '-');
98
- const v = pickIcon('│', '|');
99
- const topLeft = pickIcon('┌', '+');
100
- const topMid = pickIcon('┬', '+');
101
- const topRight = pickIcon('┐', '+');
102
- const midLeft = pickIcon('├', '+');
103
- const midMid = pickIcon('┼', '+');
104
- const midRight = pickIcon('┤', '+');
105
- const bottomLeft = pickIcon('└', '+');
106
- const bottomMid = pickIcon('┴', '+');
107
- const bottomRight = pickIcon('┘', '+');
108
- const top = `${topLeft}${h.repeat(nameWidth + 2)}${topMid}${h.repeat(statusWidth + 2)}${topMid}${h.repeat(latencyWidth + 2)}${topRight}`;
109
- const divider = `${midLeft}${h.repeat(nameWidth + 2)}${midMid}${h.repeat(statusWidth + 2)}${midMid}${h.repeat(latencyWidth + 2)}${midRight}`;
110
- const bottom = `${bottomLeft}${h.repeat(nameWidth + 2)}${bottomMid}${h.repeat(statusWidth + 2)}${bottomMid}${h.repeat(latencyWidth + 2)}${bottomRight}`;
111
- const header = `${v} ${padEndV(trans.status.service, nameWidth)} ${v} ${padEndV(trans.status.health, statusWidth)} ${v} ${padEndV(trans.status.latency, latencyWidth)} ${v}`;
112
- const lines = [dim(top), header, dim(divider)];
100
+ const nameWidth = Math.max(...items.map(i => visualWidth(i.name)), visualWidth(trans.status.service));
101
+ const statusWidth = 10;
102
+ const onIcon = pickIcon('●', '+');
103
+ const offIcon = pickIcon('✕', '!');
104
+ const lanIcon = pickIcon('○', 'o');
105
+ const sep = pickIcon('─', '-');
106
+ const lines = [];
107
+ let currentGroup;
113
108
  for (const item of items) {
114
- const statusLabel = item.ok
115
- ? green(`${pickIcon('●', 'OK')} ${trans.status.up}`)
116
- : red(`${pickIcon('●', '!!')} ${trans.status.down}`);
117
- const latency = item.latencyMs != null ? `${item.latencyMs}ms` : '-';
118
- lines.push(`${v} ${padEndV(cyan(item.name), nameWidth)} ${v} ${padEndV(statusLabel, statusWidth)} ${v} ${padEndV(latency, latencyWidth)} ${v}`);
109
+ if (item.group !== currentGroup) {
110
+ if (currentGroup !== undefined)
111
+ lines.push('');
112
+ const groupLabel = item.group === 'nbtca' ? trans.status.groupNbtca :
113
+ item.group === 'external' ? trans.status.groupExternal :
114
+ item.group === 'intranet' ? trans.status.groupIntranet : '';
115
+ lines.push(` ${applyDim(groupLabel)}`);
116
+ lines.push(` ${applyDim(sep.repeat(nameWidth + statusWidth + 12))}`);
117
+ currentGroup = item.group;
118
+ }
119
+ const nameCol = padEndV(item.intranet ? applyDim(item.name) : applyCyan(item.name), nameWidth);
120
+ let statusLabel;
121
+ if (item.ok) {
122
+ statusLabel = applyGreen(`${onIcon} ${trans.status.up}`);
123
+ }
124
+ else if (item.intranet) {
125
+ statusLabel = applyDim(`${lanIcon} ${trans.status.down}`);
126
+ }
127
+ else {
128
+ statusLabel = applyRed(`${offIcon} ${trans.status.down}`);
129
+ }
130
+ const statusCol = padEndV(statusLabel, statusWidth);
131
+ const latencyCol = item.ok && item.latencyMs != null
132
+ ? applyLatency(item.latencyMs)
133
+ : applyDim('—');
134
+ lines.push(` ${nameCol} ${statusCol} ${latencyCol}`);
119
135
  }
120
- lines.push(dim(bottom));
121
136
  return lines.join('\n');
122
137
  }
123
138
  export async function showServiceStatus() {
@@ -131,6 +146,8 @@ export async function showServiceStatus() {
131
146
  else {
132
147
  spinner.stop(trans.status.summaryOk);
133
148
  }
149
+ console.log();
134
150
  console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
151
+ console.log();
135
152
  return items;
136
153
  }
@@ -53,7 +53,12 @@
53
53
  "title": "Activity (last 12 months)",
54
54
  "legendLess": "Less",
55
55
  "legendMore": "More"
56
- }
56
+ },
57
+ "pastLoading": "Loading past events...",
58
+ "pastEvents": "Past Events",
59
+ "pastEventsDesc": "Recent activity history",
60
+ "noPastEvents": "No past events in the last 30 days",
61
+ "viewPastDetail": "Select an event for details:"
57
62
  },
58
63
  "docs": {
59
64
  "loading": "Loading documentation list...",
@@ -119,13 +124,19 @@
119
124
  "code": "Code",
120
125
  "latency": "Latency",
121
126
  "url": "URL",
122
- "up": "UP",
123
- "down": "DOWN",
124
- "serviceWebsite": "Website",
127
+ "up": "online",
128
+ "down": "offline",
129
+ "groupNbtca": "NBTCA Services",
130
+ "groupExternal": "External",
131
+ "groupIntranet": "Intranet (LAN only)",
132
+ "serviceHomepage": "Homepage",
125
133
  "serviceDocs": "Docs",
126
- "serviceCalendar": "Calendar",
134
+ "serviceIcal": "iCal Feed",
135
+ "serviceRepair": "Repair",
127
136
  "serviceGithub": "GitHub",
128
137
  "serviceRoadmap": "Roadmap",
138
+ "serviceCloud": "Cloud",
139
+ "serviceMirror": "Mirror",
129
140
  "watchStarted": "Starting status watch (every {seconds}s)",
130
141
  "watchUpdated": "Last updated",
131
142
  "watchHint": "Press Ctrl+C to stop",
@@ -53,7 +53,12 @@
53
53
  "title": "近一年活跃度",
54
54
  "legendLess": "少",
55
55
  "legendMore": "多"
56
- }
56
+ },
57
+ "pastLoading": "正在加载历史活动...",
58
+ "pastEvents": "历史活动",
59
+ "pastEventsDesc": "近期活动记录",
60
+ "noPastEvents": "最近 30 天内暂无历史活动",
61
+ "viewPastDetail": "选择活动查看详情:"
57
62
  },
58
63
  "docs": {
59
64
  "loading": "正在加载文档列表...",
@@ -119,13 +124,19 @@
119
124
  "code": "代码",
120
125
  "latency": "延迟",
121
126
  "url": "URL",
122
- "up": "正常",
123
- "down": "异常",
124
- "serviceWebsite": "官网",
127
+ "up": "在线",
128
+ "down": "离线",
129
+ "groupNbtca": "NBTCA 服务",
130
+ "groupExternal": "外部平台",
131
+ "groupIntranet": "内网服务(仅校园网)",
132
+ "serviceHomepage": "主页",
125
133
  "serviceDocs": "文档",
126
- "serviceCalendar": "日历",
134
+ "serviceIcal": "iCal 源",
135
+ "serviceRepair": "维修服务",
127
136
  "serviceGithub": "GitHub",
128
- "serviceRoadmap": "看板",
137
+ "serviceRoadmap": "路线图",
138
+ "serviceCloud": "云存储",
139
+ "serviceMirror": "镜像站",
129
140
  "watchStarted": "开始监控服务状态(每 {seconds} 秒)",
130
141
  "watchUpdated": "最近更新",
131
142
  "watchHint": "按 Ctrl+C 停止",
package/dist/main.js CHANGED
@@ -1,42 +1,29 @@
1
- /**
2
- * NBTCA Welcome Tool
3
- * Minimalist startup flow
4
- */
5
- import chalk from 'chalk';
6
- import { intro } from '@clack/prompts';
7
1
  import { printLogo } from './core/logo.js';
8
2
  import { clearScreen, handleGracefulExit } from './core/ui.js';
9
3
  import { showMainMenu } from './core/menu.js';
10
- import { APP_INFO } from './config/data.js';
4
+ import { c } from './core/theme.js';
11
5
  import { enableVimKeys } from './core/vim-keys.js';
12
6
  import { checkForUpdate } from './features/update.js';
13
- /**
14
- * Main program entry point
15
- */
7
+ import { showEventsPreview } from './features/calendar.js';
16
8
  export async function main(options = {}) {
17
9
  try {
18
- // Enable Vim key bindings
19
10
  enableVimKeys();
20
- // Clear screen
21
11
  if (process.stdout.isTTY) {
22
12
  clearScreen();
23
13
  }
24
- // Display logo (smart fallback)
25
14
  if (!options.skipLogo) {
26
15
  printLogo();
27
16
  }
28
- // Non-blocking update check (fire and forget, print before menu if resolved in time)
17
+ // Fire update check in background; events fetch provides natural wait time
29
18
  const updatePromise = checkForUpdate();
30
- // Open session frame
31
- intro(chalk.cyan('NBTCA Prompt') + chalk.dim(` v${APP_INFO.version}`));
32
- // Show update notification if ready
19
+ await showEventsPreview();
20
+ // Update check is very likely done by now; give it a short window if not
33
21
  const updateMsg = await Promise.race([
34
22
  updatePromise,
35
- new Promise(r => setTimeout(r, 500, null)),
23
+ new Promise(r => setTimeout(r, 100, null)),
36
24
  ]);
37
25
  if (updateMsg)
38
- console.log(chalk.yellow(updateMsg));
39
- // Show main menu (loop)
26
+ console.log(c.warn(updateMsg));
40
27
  await showMainMenu();
41
28
  }
42
29
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbtca/prompt",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -39,8 +39,8 @@
39
39
  ],
40
40
  "dependencies": {
41
41
  "@clack/prompts": "^1.2.0",
42
- "@nbtca/docs": "^0.1.0",
43
- "@nbtca/nbtcal": "^0.2.1",
42
+ "@nbtca/docs": "^0.2.0",
43
+ "@nbtca/nbtcal": "^0.3.0",
44
44
  "chalk": "^5.6.2",
45
45
  "gradient-string": "^3.0.0",
46
46
  "marked": "^15.0.12",