@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.
- package/dist/config/data.js +4 -5
- package/dist/core/menu.js +2 -2
- package/dist/core/theme.js +22 -0
- package/dist/features/calendar.js +136 -88
- package/dist/features/docs.js +21 -36
- package/dist/features/status.js +74 -57
- package/dist/i18n/locales/en.json +16 -5
- package/dist/i18n/locales/zh.json +17 -6
- package/dist/main.js +7 -20
- package/package.json +3 -3
package/dist/config/data.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
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
|
|
118
|
-
const
|
|
119
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
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
|
|
142
|
-
|
|
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(
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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(
|
|
239
|
+
console.log(c.muted(' ' + trans.calendar.errorHint));
|
|
192
240
|
console.log();
|
|
193
241
|
}
|
|
194
242
|
}
|
package/dist/features/docs.js
CHANGED
|
@@ -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
|
|
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
|
|
447
|
-
|
|
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() {
|
package/dist/features/status.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
{ name: trans.status.
|
|
12
|
-
{ name: trans.status.
|
|
13
|
-
{ name: trans.status.
|
|
14
|
-
{ name: trans.status.
|
|
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(
|
|
41
|
-
let lastResult = await checkService(name, url, timeoutMs);
|
|
42
|
-
if (lastResult.ok)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
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(
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
92
|
+
const useColor = options?.color !== false;
|
|
88
93
|
const id = (s) => s;
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
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 =
|
|
95
|
-
const statusWidth =
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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": "
|
|
123
|
-
"down": "
|
|
124
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
127
|
+
"up": "在线",
|
|
128
|
+
"down": "离线",
|
|
129
|
+
"groupNbtca": "NBTCA 服务",
|
|
130
|
+
"groupExternal": "外部平台",
|
|
131
|
+
"groupIntranet": "内网服务(仅校园网)",
|
|
132
|
+
"serviceHomepage": "主页",
|
|
125
133
|
"serviceDocs": "文档",
|
|
126
|
-
"
|
|
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 {
|
|
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
|
-
//
|
|
17
|
+
// Fire update check in background; events fetch provides natural wait time
|
|
29
18
|
const updatePromise = checkForUpdate();
|
|
30
|
-
|
|
31
|
-
|
|
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,
|
|
23
|
+
new Promise(r => setTimeout(r, 100, null)),
|
|
36
24
|
]);
|
|
37
25
|
if (updateMsg)
|
|
38
|
-
console.log(
|
|
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.
|
|
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.
|
|
43
|
-
"@nbtca/nbtcal": "^0.
|
|
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",
|