@nbtca/prompt 1.1.3 → 1.3.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 +216 -164
- package/dist/features/status.js +74 -57
- package/dist/i18n/locales/en.json +18 -5
- package/dist/i18n/locales/zh.json +19 -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
|
@@ -9,7 +9,7 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
9
9
|
import { URLS } from '../config/data.js';
|
|
10
10
|
import { t, fmt } from '../i18n/index.js';
|
|
11
11
|
import { setVimKeysActive } from '../core/vim-keys.js';
|
|
12
|
-
import { createDocsClient
|
|
12
|
+
import { createDocsClient } from '@nbtca/docs';
|
|
13
13
|
function detectTerminalType() {
|
|
14
14
|
const term = (process.env['TERM'] || '').toLowerCase();
|
|
15
15
|
const termProgram = (process.env['TERM_PROGRAM'] || '').toLowerCase();
|
|
@@ -57,7 +57,6 @@ function ensureMarkedConfigured() {
|
|
|
57
57
|
}
|
|
58
58
|
// ─── marked-terminal renderer ─────────────────────────────────────────────────
|
|
59
59
|
function getRendererOptions(type) {
|
|
60
|
-
// Cap at 80 columns — optimal prose reading width regardless of terminal size
|
|
61
60
|
const width = Math.min(process.stdout.columns || 80, 80);
|
|
62
61
|
const unicodeTableChars = {
|
|
63
62
|
top: '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
|
|
@@ -75,23 +74,16 @@ function getRendererOptions(type) {
|
|
|
75
74
|
width,
|
|
76
75
|
emoji: true,
|
|
77
76
|
unescape: true,
|
|
78
|
-
// Heading hierarchy: h1 cyan, h2+ white bold
|
|
79
77
|
firstHeading: chalk.bold.cyan,
|
|
80
78
|
heading: chalk.bold.white,
|
|
81
|
-
// Inline code: bright yellow, distinct from prose
|
|
82
79
|
codespan: chalk.yellowBright,
|
|
83
|
-
// Block code: yellow (marked-terminal applies per-line)
|
|
84
80
|
code: chalk.yellow,
|
|
85
|
-
// Blockquotes: italic gray, visually recessed
|
|
86
81
|
blockquote: chalk.italic.gray,
|
|
87
|
-
// Prose emphasis
|
|
88
82
|
strong: chalk.bold,
|
|
89
83
|
em: chalk.italic,
|
|
90
84
|
del: chalk.dim.strikethrough,
|
|
91
|
-
// Links: cyan underline
|
|
92
85
|
link: chalk.cyan,
|
|
93
86
|
href: chalk.cyan.underline,
|
|
94
|
-
// Tables with Unicode borders (fallback to ASCII on basic terminals)
|
|
95
87
|
tableOptions: {
|
|
96
88
|
chars: type === 'basic' ? asciiTableChars : unicodeTableChars
|
|
97
89
|
}
|
|
@@ -101,17 +93,6 @@ const RENDER_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
|
101
93
|
const RENDER_CACHE_MAX = 50;
|
|
102
94
|
const renderCache = new Map();
|
|
103
95
|
let docsClient = createDocsClient();
|
|
104
|
-
function getDocCategories() {
|
|
105
|
-
const trans = t();
|
|
106
|
-
return [
|
|
107
|
-
{ name: trans.docs.categoryTutorial, path: 'tutorial' },
|
|
108
|
-
{ name: trans.docs.categoryRepairLogs, path: '维修日' },
|
|
109
|
-
{ name: trans.docs.categoryEvents, path: '相关活动举办' },
|
|
110
|
-
{ name: trans.docs.categoryProcess, path: 'process' },
|
|
111
|
-
{ name: trans.docs.categoryRepair, path: 'repair' },
|
|
112
|
-
{ name: trans.docs.categoryArchived, path: 'archived' },
|
|
113
|
-
];
|
|
114
|
-
}
|
|
115
96
|
function getFreshRender(key) {
|
|
116
97
|
const entry = renderCache.get(key);
|
|
117
98
|
return entry && entry.expiresAt > Date.now() ? entry.value : null;
|
|
@@ -131,18 +112,6 @@ export function clearDocsCache() {
|
|
|
131
112
|
docsClient.clear();
|
|
132
113
|
renderCache.clear();
|
|
133
114
|
}
|
|
134
|
-
async function fetchDirectory(path = '') {
|
|
135
|
-
try {
|
|
136
|
-
return await docsClient.listDir(path);
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
const trans = t();
|
|
140
|
-
const msg = err instanceof DocsFetchError
|
|
141
|
-
? (err.status === 403 ? `${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}` : `HTTP ${err.status}`)
|
|
142
|
-
: String(err);
|
|
143
|
-
throw new Error(fmt(trans.docs.fetchDirFailed, { error: msg }));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
115
|
async function fetchFileContent(path) {
|
|
147
116
|
try {
|
|
148
117
|
return await docsClient.getFile(path);
|
|
@@ -167,14 +136,12 @@ function cleanMarkdownContent(content, type = getTerminalType()) {
|
|
|
167
136
|
c = c.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
168
137
|
c = c.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
169
138
|
// 3. VitePress containers → blockquote with icon
|
|
170
|
-
// ::: warning Title\ncontent\n:::
|
|
171
139
|
c = c.replace(/^:::\s*(info|tip|warning|danger|details)\s*(.*?)\n([\s\S]*?)^:::\s*$/gm, (_m, type, title, body) => {
|
|
172
140
|
const label = (title.trim() || type.charAt(0).toUpperCase() + type.slice(1));
|
|
173
141
|
const icon = pickIcon(CONTAINER_ICONS_UNICODE[type] ?? '', CONTAINER_ICONS_ASCII[type] ?? '');
|
|
174
142
|
const quoted = body.trimEnd().split('\n').map(l => `> ${l}`).join('\n');
|
|
175
143
|
return `> ${icon} **${label}**\n>\n${quoted}\n`;
|
|
176
144
|
});
|
|
177
|
-
// Remaining bare ::: markers
|
|
178
145
|
c = c.replace(/^:::\s*\w*.*$/gm, '');
|
|
179
146
|
// 4. GitHub / GitLab callout alerts (> [!NOTE])
|
|
180
147
|
c = c.replace(/^>\s*\[!(NOTE|TIP|WARNING|CAUTION|IMPORTANT)\]\s*$/gim, (_, type) => `> **${type.charAt(0) + type.slice(1).toLowerCase()}:**`);
|
|
@@ -200,32 +167,111 @@ function cleanMarkdownContent(content, type = getTerminalType()) {
|
|
|
200
167
|
return c.trim();
|
|
201
168
|
}
|
|
202
169
|
function extractDocTitle(rawContent, cleanedContent) {
|
|
203
|
-
// 1. Try YAML frontmatter title: field (before it was stripped)
|
|
204
170
|
const fmMatch = rawContent.match(/^---\n[\s\S]*?\n---/m);
|
|
205
171
|
if (fmMatch) {
|
|
206
172
|
const titleMatch = fmMatch[0].match(/^title:\s*['"]?(.+?)['"]?\s*$/m);
|
|
207
173
|
if (titleMatch?.[1])
|
|
208
174
|
return titleMatch[1].trim();
|
|
209
175
|
}
|
|
210
|
-
// 2. Fallback to first # H1 heading in cleaned content
|
|
211
176
|
const h1Match = cleanedContent.match(/^#\s+(.+)$/m);
|
|
212
177
|
return h1Match?.[1]?.trim() ?? null;
|
|
213
178
|
}
|
|
214
179
|
/** Approximate reading time: ~200 words/min for technical Chinese/English prose. */
|
|
215
180
|
function estimateReadTime(text) {
|
|
216
|
-
const cjkChars = (text.match(/[
|
|
217
|
-
const nonCjk = text.replace(/[
|
|
181
|
+
const cjkChars = (text.match(/[㐀-鿿]/g) || []).length;
|
|
182
|
+
const nonCjk = text.replace(/[㐀-鿿]/g, ' ');
|
|
218
183
|
const words = nonCjk.trim().split(/\s+/).filter(Boolean).length;
|
|
219
|
-
// Rough equivalence: 2 CJK chars ~= 1 "word" unit
|
|
220
184
|
const units = words + cjkChars / 2;
|
|
221
185
|
const mins = Math.max(1, Math.ceil(units / 220));
|
|
222
186
|
return mins === 1 ? '~1 min' : `~${mins} min`;
|
|
223
187
|
}
|
|
224
|
-
|
|
188
|
+
/** Extract h2/h3 headings for TOC display (skips the h1 title). */
|
|
189
|
+
function extractTOC(content) {
|
|
190
|
+
const lines = content.split('\n').filter(l => /^#{2,3}\s/.test(l));
|
|
191
|
+
return lines.map(l => {
|
|
192
|
+
const m = l.match(/^(#+)/);
|
|
193
|
+
const level = m?.[1]?.length ?? 2;
|
|
194
|
+
const text = l.replace(/^#+\s+/, '').trim();
|
|
195
|
+
return (level === 3 ? ' ' : '') + text;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/** True if the markdown source contains a table (pipe-delimited with separator row). */
|
|
199
|
+
function hasMarkdownTable(content) {
|
|
200
|
+
return /^\|.+\|/m.test(content) && /^\|[-: |]+\|/m.test(content);
|
|
201
|
+
}
|
|
202
|
+
// ─── Document tree ────────────────────────────────────────────────────────────
|
|
203
|
+
const TOP_SECTION_ORDER = ['tutorial', 'process', 'repair', 'archived'];
|
|
204
|
+
const TOP_SECTION_SKIP = new Set(['docs', 'index.md', 'README.md']);
|
|
225
205
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
206
|
+
* Convert a kebab-case filename to a display-friendly title.
|
|
207
|
+
* Preserves Chinese characters and date prefixes.
|
|
228
208
|
*/
|
|
209
|
+
function cleanFileName(name) {
|
|
210
|
+
const base = name.replace(/\.md$/, '');
|
|
211
|
+
if (/^[\d.]/.test(base))
|
|
212
|
+
return base;
|
|
213
|
+
return base
|
|
214
|
+
.replace(/[-_]/g, ' ')
|
|
215
|
+
.replace(/\b([a-z])/g, (_, c) => c.toUpperCase());
|
|
216
|
+
}
|
|
217
|
+
/** Group flat DocItem list into top-level sections. */
|
|
218
|
+
function buildSections(all) {
|
|
219
|
+
const trans = t();
|
|
220
|
+
const labelMap = {
|
|
221
|
+
tutorial: trans.docs.categoryTutorial,
|
|
222
|
+
process: trans.docs.categoryProcess,
|
|
223
|
+
repair: trans.docs.categoryRepair,
|
|
224
|
+
archived: trans.docs.categoryArchived,
|
|
225
|
+
};
|
|
226
|
+
const groups = new Map();
|
|
227
|
+
for (const item of all) {
|
|
228
|
+
const parts = item.path.split('/');
|
|
229
|
+
if (parts.length < 2)
|
|
230
|
+
continue;
|
|
231
|
+
const top = parts[0];
|
|
232
|
+
if (TOP_SECTION_SKIP.has(top))
|
|
233
|
+
continue;
|
|
234
|
+
if (!TOP_SECTION_ORDER.includes(top))
|
|
235
|
+
continue;
|
|
236
|
+
if (!groups.has(top))
|
|
237
|
+
groups.set(top, []);
|
|
238
|
+
groups.get(top).push(item);
|
|
239
|
+
}
|
|
240
|
+
return TOP_SECTION_ORDER
|
|
241
|
+
.filter(k => groups.has(k))
|
|
242
|
+
.map(k => ({
|
|
243
|
+
key: k,
|
|
244
|
+
label: labelMap[k] ?? k,
|
|
245
|
+
count: groups.get(k).length,
|
|
246
|
+
files: groups.get(k),
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
/** Group archived files by their second path component (year / manual / etc.). */
|
|
250
|
+
function getArchivedGroups(files) {
|
|
251
|
+
const groups = new Map();
|
|
252
|
+
for (const item of files) {
|
|
253
|
+
const group = item.path.split('/')[1] ?? 'other';
|
|
254
|
+
if (!groups.has(group))
|
|
255
|
+
groups.set(group, []);
|
|
256
|
+
groups.get(group).push(item);
|
|
257
|
+
}
|
|
258
|
+
return groups;
|
|
259
|
+
}
|
|
260
|
+
async function loadSections() {
|
|
261
|
+
const trans = t();
|
|
262
|
+
const s = createSpinner(trans.docs.loading);
|
|
263
|
+
try {
|
|
264
|
+
const all = await docsClient.listAll();
|
|
265
|
+
const sections = buildSections(all);
|
|
266
|
+
s.stop();
|
|
267
|
+
return sections;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
s.error(trans.docs.loadError);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// ─── Pager layer ──────────────────────────────────────────────────────────────
|
|
229
275
|
async function displayWithGlow(cleanedMarkdown) {
|
|
230
276
|
const cols = String(Math.min(process.stdout.columns || 80, 80));
|
|
231
277
|
return new Promise(resolve => {
|
|
@@ -235,33 +281,34 @@ async function displayWithGlow(cleanedMarkdown) {
|
|
|
235
281
|
child.stdin.write(cleanedMarkdown, 'utf-8');
|
|
236
282
|
child.stdin.end();
|
|
237
283
|
child.on('close', resolve);
|
|
238
|
-
child.on('error', resolve);
|
|
284
|
+
child.on('error', resolve);
|
|
239
285
|
});
|
|
240
286
|
}
|
|
241
|
-
|
|
242
|
-
* Display rendered markdown via `less` with a structured document frame.
|
|
243
|
-
* Flags:
|
|
244
|
-
* -R pass raw ANSI codes through
|
|
245
|
-
* -F exit immediately if content fits on one screen
|
|
246
|
-
* -X don't clear the screen on exit
|
|
247
|
-
* -i case-insensitive search (/ to search)
|
|
248
|
-
* -j4 place search hits 4 lines from the top (less jarring)
|
|
249
|
-
*/
|
|
250
|
-
async function displayWithLess(rendered, title, filePath, readTime) {
|
|
287
|
+
async function displayWithLess(rendered, title, filePath, readTime, toc) {
|
|
251
288
|
const trans = t();
|
|
252
289
|
const cols = Math.min(process.stdout.columns || 80, 80);
|
|
253
|
-
const rule = chalk.dim('
|
|
290
|
+
const rule = chalk.dim('─'.repeat(cols));
|
|
291
|
+
const tocBlock = toc.length >= 3
|
|
292
|
+
? [
|
|
293
|
+
chalk.dim(` ${trans.docs.tocTitle}`),
|
|
294
|
+
chalk.dim(` ${'─'.repeat(36)}`),
|
|
295
|
+
...toc.map(h => chalk.dim(` ${h}`)),
|
|
296
|
+
chalk.dim(` ${'─'.repeat(36)}`),
|
|
297
|
+
'',
|
|
298
|
+
].join('\n')
|
|
299
|
+
: '';
|
|
254
300
|
const header = [
|
|
255
301
|
'',
|
|
256
302
|
chalk.bold.cyan(` ${title}`),
|
|
257
|
-
chalk.dim(` ${filePath}`) + chalk.dim(`
|
|
303
|
+
chalk.dim(` ${filePath}`) + chalk.dim(` · ${readTime}`),
|
|
258
304
|
rule,
|
|
305
|
+
...(tocBlock ? [tocBlock] : []),
|
|
259
306
|
'',
|
|
260
307
|
].join('\n');
|
|
261
308
|
const footer = [
|
|
262
309
|
'',
|
|
263
310
|
rule,
|
|
264
|
-
chalk.dim(` ${trans.docs.endOfDocument}
|
|
311
|
+
chalk.dim(` ${trans.docs.endOfDocument} · / to search`),
|
|
265
312
|
'',
|
|
266
313
|
].join('\n');
|
|
267
314
|
const fullContent = header + rendered + footer;
|
|
@@ -286,66 +333,75 @@ async function displayWithLess(rendered, title, filePath, readTime) {
|
|
|
286
333
|
}
|
|
287
334
|
});
|
|
288
335
|
}
|
|
289
|
-
// ───
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const s = createSpinner(currentPath ? `${trans.docs.loadingDir}: ${currentPath}` : trans.docs.loading);
|
|
297
|
-
items = await fetchDirectory(currentPath);
|
|
298
|
-
s.stop(currentPath || trans.docs.chooseDoc);
|
|
299
|
-
}
|
|
300
|
-
catch (err) {
|
|
301
|
-
error(trans.docs.loadError);
|
|
302
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
303
|
-
console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
|
|
304
|
-
setVimKeysActive(false);
|
|
305
|
-
const retry = await confirm({ message: trans.docs.retry });
|
|
306
|
-
setVimKeysActive(true);
|
|
307
|
-
if (!isCancel(retry) && retry)
|
|
308
|
-
continue;
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
if (items.length === 0) {
|
|
312
|
-
warning(trans.docs.emptyDir);
|
|
313
|
-
if (currentPath) {
|
|
314
|
-
currentPath = currentPath.split('/').slice(0, -1).join('/');
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
const options = [
|
|
320
|
-
...(currentPath ? [{ value: '__back__', label: chalk.dim(trans.docs.upToParent) }] : []),
|
|
321
|
-
...items.map(item => ({
|
|
322
|
-
value: item.path,
|
|
323
|
-
label: item.type === 'dir'
|
|
324
|
-
? chalk.cyan(`${item.name}/`)
|
|
325
|
-
: item.name,
|
|
326
|
-
hint: item.type === 'dir' ? 'dir' : undefined,
|
|
327
|
-
})),
|
|
328
|
-
{ value: '__exit__', label: chalk.dim(trans.docs.returnToMenu) },
|
|
329
|
-
];
|
|
330
|
-
const selected = await select({
|
|
331
|
-
message: currentPath ? `${trans.docs.currentDir}: ${currentPath}` : trans.docs.chooseDoc,
|
|
332
|
-
options,
|
|
333
|
-
});
|
|
334
|
-
if (isCancel(selected) || selected === '__exit__')
|
|
335
|
-
return;
|
|
336
|
-
if (selected === '__back__') {
|
|
337
|
-
currentPath = currentPath.split('/').slice(0, -1).join('/');
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
const item = items.find(i => i.path === selected);
|
|
341
|
-
if (item?.type === 'dir') {
|
|
342
|
-
currentPath = selected;
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
if (item?.type === 'file') {
|
|
346
|
-
await viewMarkdownFile(selected);
|
|
347
|
-
}
|
|
336
|
+
// ─── Section browsers ─────────────────────────────────────────────────────────
|
|
337
|
+
/** Show a flat file list for tutorial / process / repair. */
|
|
338
|
+
async function showDocSection(section) {
|
|
339
|
+
const trans = t();
|
|
340
|
+
if (section.key === 'archived') {
|
|
341
|
+
await showArchivedSection(section.files);
|
|
342
|
+
return;
|
|
348
343
|
}
|
|
344
|
+
const files = section.files.filter(f => f.name !== 'index.md' && !f.name.startsWith('index.'));
|
|
345
|
+
if (files.length === 0)
|
|
346
|
+
return;
|
|
347
|
+
const selected = await select({
|
|
348
|
+
message: section.label,
|
|
349
|
+
options: [
|
|
350
|
+
...files.map(f => {
|
|
351
|
+
const parts = f.path.split('/');
|
|
352
|
+
const hint = parts.length > 2 ? parts.slice(1, -1).join('/') : '';
|
|
353
|
+
return { value: f.path, label: cleanFileName(f.name), hint };
|
|
354
|
+
}),
|
|
355
|
+
{ value: '__back__', label: chalk.dim(trans.common.back) },
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
if (isCancel(selected) || selected === '__back__')
|
|
359
|
+
return;
|
|
360
|
+
await viewMarkdownFile(selected);
|
|
361
|
+
}
|
|
362
|
+
/** Show archived docs grouped by year, then files within the year. */
|
|
363
|
+
async function showArchivedSection(files) {
|
|
364
|
+
const trans = t();
|
|
365
|
+
const groups = getArchivedGroups(files);
|
|
366
|
+
const sortedKeys = [...groups.keys()].sort((a, b) => {
|
|
367
|
+
const aYear = /^\d{4}$/.test(a);
|
|
368
|
+
const bYear = /^\d{4}$/.test(b);
|
|
369
|
+
if (aYear && bYear)
|
|
370
|
+
return Number(b) - Number(a);
|
|
371
|
+
if (aYear)
|
|
372
|
+
return -1;
|
|
373
|
+
if (bYear)
|
|
374
|
+
return 1;
|
|
375
|
+
return a.localeCompare(b);
|
|
376
|
+
});
|
|
377
|
+
const groupKey = await select({
|
|
378
|
+
message: trans.docs.categoryArchived,
|
|
379
|
+
options: [
|
|
380
|
+
...sortedKeys.map(k => ({
|
|
381
|
+
value: k,
|
|
382
|
+
label: k,
|
|
383
|
+
hint: `${groups.get(k).length} docs`,
|
|
384
|
+
})),
|
|
385
|
+
{ value: '__back__', label: chalk.dim(trans.common.back) },
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
if (isCancel(groupKey) || groupKey === '__back__')
|
|
389
|
+
return;
|
|
390
|
+
const groupFiles = groups.get(groupKey) ?? [];
|
|
391
|
+
const fileSelected = await select({
|
|
392
|
+
message: `${trans.docs.categoryArchived} / ${groupKey}`,
|
|
393
|
+
options: [
|
|
394
|
+
...groupFiles.map(f => ({
|
|
395
|
+
value: f.path,
|
|
396
|
+
label: cleanFileName(f.name),
|
|
397
|
+
hint: f.path.split('/').slice(2, -1).join('/'),
|
|
398
|
+
})),
|
|
399
|
+
{ value: '__back__', label: chalk.dim(trans.common.back) },
|
|
400
|
+
],
|
|
401
|
+
});
|
|
402
|
+
if (isCancel(fileSelected) || fileSelected === '__back__')
|
|
403
|
+
return;
|
|
404
|
+
await viewMarkdownFile(fileSelected);
|
|
349
405
|
}
|
|
350
406
|
// ─── Document viewer ──────────────────────────────────────────────────────────
|
|
351
407
|
async function viewMarkdownFile(filePath) {
|
|
@@ -363,28 +419,31 @@ async function viewMarkdownFile(filePath) {
|
|
|
363
419
|
}
|
|
364
420
|
else {
|
|
365
421
|
const cleaned = cleanMarkdownContent(rawContent, getTerminalType());
|
|
366
|
-
const title = extractDocTitle(rawContent, cleaned) || filePath.split('/').pop()
|
|
422
|
+
const title = extractDocTitle(rawContent, cleaned) || cleanFileName(filePath.split('/').pop() ?? filePath);
|
|
367
423
|
const readTime = estimateReadTime(cleaned);
|
|
368
424
|
const rendered = await marked(cleaned);
|
|
369
425
|
renderedDoc = { fingerprint, cleaned, rendered, title, readTime };
|
|
370
426
|
setRender(filePath, renderedDoc);
|
|
371
427
|
}
|
|
372
428
|
s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`);
|
|
429
|
+
const toc = extractTOC(renderedDoc.cleaned);
|
|
373
430
|
if (hasGlow()) {
|
|
374
431
|
await displayWithGlow(renderedDoc.cleaned);
|
|
375
432
|
}
|
|
376
433
|
else {
|
|
377
|
-
await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime);
|
|
434
|
+
await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime, toc);
|
|
378
435
|
}
|
|
379
436
|
console.log();
|
|
380
437
|
success(trans.docs.docCompleted);
|
|
381
438
|
console.log();
|
|
439
|
+
const hasTable = hasMarkdownTable(rawContent);
|
|
382
440
|
const action = await select({
|
|
383
441
|
message: trans.docs.chooseAction,
|
|
384
442
|
options: [
|
|
385
443
|
{ value: 'back', label: trans.docs.backToList },
|
|
386
444
|
{ value: 'reread', label: trans.docs.reread },
|
|
387
|
-
{ value: 'browser', label: trans.docs.openBrowser
|
|
445
|
+
{ value: 'browser', label: trans.docs.openBrowser,
|
|
446
|
+
hint: hasTable ? trans.docs.tableHint : undefined },
|
|
388
447
|
],
|
|
389
448
|
});
|
|
390
449
|
if (isCancel(action) || action === 'back')
|
|
@@ -393,7 +452,7 @@ async function viewMarkdownFile(filePath) {
|
|
|
393
452
|
await openDocsInBrowser(filePath);
|
|
394
453
|
return;
|
|
395
454
|
}
|
|
396
|
-
//
|
|
455
|
+
// 'reread' → loop
|
|
397
456
|
}
|
|
398
457
|
catch (err) {
|
|
399
458
|
error(trans.docs.loadError);
|
|
@@ -439,63 +498,53 @@ async function searchDocs() {
|
|
|
439
498
|
return;
|
|
440
499
|
const keyword = query.trim().toLowerCase();
|
|
441
500
|
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
501
|
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
|
-
}
|
|
502
|
+
const all = await docsClient.listAll();
|
|
503
|
+
const results = all.filter(item => item.path.toLowerCase().includes(keyword));
|
|
459
504
|
s.stop(`${results.length} ${trans.docs.searchResults}`);
|
|
505
|
+
if (results.length === 0) {
|
|
506
|
+
warning(trans.docs.searchNoResults);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const selected = await select({
|
|
510
|
+
message: trans.docs.chooseDoc,
|
|
511
|
+
options: [
|
|
512
|
+
...results.map(r => ({
|
|
513
|
+
value: r.path,
|
|
514
|
+
label: cleanFileName(r.name),
|
|
515
|
+
hint: r.path.includes('/') ? r.path.split('/').slice(0, -1).join('/') : '',
|
|
516
|
+
})),
|
|
517
|
+
{ value: '__back__', label: chalk.dim(trans.docs.returnToMenu) },
|
|
518
|
+
],
|
|
519
|
+
});
|
|
520
|
+
if (isCancel(selected) || selected === '__back__')
|
|
521
|
+
return;
|
|
522
|
+
await viewMarkdownFile(selected);
|
|
460
523
|
}
|
|
461
524
|
catch {
|
|
462
525
|
s.error(trans.docs.loadError);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
if (results.length === 0) {
|
|
466
|
-
warning(trans.docs.searchNoResults);
|
|
467
|
-
return;
|
|
468
526
|
}
|
|
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
527
|
}
|
|
484
528
|
// ─── Menu ─────────────────────────────────────────────────────────────────────
|
|
485
529
|
export async function showDocsMenu() {
|
|
530
|
+
let sections = await loadSections();
|
|
531
|
+
if (!sections)
|
|
532
|
+
return;
|
|
486
533
|
while (true) {
|
|
487
534
|
const trans = t();
|
|
488
|
-
const categories = getDocCategories();
|
|
489
|
-
const options = [
|
|
490
|
-
...categories.map(cat => ({ value: cat.path, label: cat.name })),
|
|
491
|
-
{ value: 'search', label: chalk.dim(trans.docs.searchPrompt.replace(':', '')) },
|
|
492
|
-
{ value: 'refresh-cache', label: chalk.dim(trans.docs.refreshCache) },
|
|
493
|
-
{ value: 'browser', label: chalk.dim(trans.docs.openBrowser) },
|
|
494
|
-
{ value: 'back', label: chalk.dim(trans.docs.returnToMenu) },
|
|
495
|
-
];
|
|
496
535
|
const action = await select({
|
|
497
536
|
message: trans.docs.chooseCategory,
|
|
498
|
-
options
|
|
537
|
+
options: [
|
|
538
|
+
...sections.map(sec => ({
|
|
539
|
+
value: sec.key,
|
|
540
|
+
label: sec.label,
|
|
541
|
+
hint: `${sec.count} docs`,
|
|
542
|
+
})),
|
|
543
|
+
{ value: 'search', label: chalk.dim(trans.docs.searchPrompt.replace(':', '')) },
|
|
544
|
+
{ value: 'refresh-cache', label: chalk.dim(trans.docs.refreshCache) },
|
|
545
|
+
{ value: 'browser', label: chalk.dim(trans.docs.openBrowser) },
|
|
546
|
+
{ value: 'back', label: chalk.dim(trans.docs.returnToMenu) },
|
|
547
|
+
],
|
|
499
548
|
});
|
|
500
549
|
if (isCancel(action) || action === 'back')
|
|
501
550
|
return;
|
|
@@ -504,13 +553,16 @@ export async function showDocsMenu() {
|
|
|
504
553
|
}
|
|
505
554
|
else if (action === 'refresh-cache') {
|
|
506
555
|
clearDocsCache();
|
|
556
|
+
sections = (await loadSections()) ?? sections;
|
|
507
557
|
success(trans.docs.cacheCleared);
|
|
508
558
|
}
|
|
509
559
|
else if (action === 'browser') {
|
|
510
560
|
await openDocsInBrowser();
|
|
511
561
|
}
|
|
512
562
|
else {
|
|
513
|
-
|
|
563
|
+
const section = sections.find(s => s.key === action);
|
|
564
|
+
if (section)
|
|
565
|
+
await showDocSection(section);
|
|
514
566
|
}
|
|
515
567
|
}
|
|
516
568
|
}
|
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...",
|
|
@@ -87,6 +92,8 @@
|
|
|
87
92
|
"browserError": "Failed to open browser",
|
|
88
93
|
"browserErrorHint": "Please visit manually: https://docs.nbtca.space",
|
|
89
94
|
"retry": "Retry?",
|
|
95
|
+
"tocTitle": "Table of Contents",
|
|
96
|
+
"tableHint": "tables render better in browser",
|
|
90
97
|
"endOfDocument": "End of document - Press q to quit",
|
|
91
98
|
"githubRateLimited": "GitHub API rate limit reached. Resets at {time}.",
|
|
92
99
|
"githubForbidden": "GitHub API access denied (403).",
|
|
@@ -119,13 +126,19 @@
|
|
|
119
126
|
"code": "Code",
|
|
120
127
|
"latency": "Latency",
|
|
121
128
|
"url": "URL",
|
|
122
|
-
"up": "
|
|
123
|
-
"down": "
|
|
124
|
-
"
|
|
129
|
+
"up": "online",
|
|
130
|
+
"down": "offline",
|
|
131
|
+
"groupNbtca": "NBTCA Services",
|
|
132
|
+
"groupExternal": "External",
|
|
133
|
+
"groupIntranet": "Intranet (LAN only)",
|
|
134
|
+
"serviceHomepage": "Homepage",
|
|
125
135
|
"serviceDocs": "Docs",
|
|
126
|
-
"
|
|
136
|
+
"serviceIcal": "iCal Feed",
|
|
137
|
+
"serviceRepair": "Repair",
|
|
127
138
|
"serviceGithub": "GitHub",
|
|
128
139
|
"serviceRoadmap": "Roadmap",
|
|
140
|
+
"serviceCloud": "Cloud",
|
|
141
|
+
"serviceMirror": "Mirror",
|
|
129
142
|
"watchStarted": "Starting status watch (every {seconds}s)",
|
|
130
143
|
"watchUpdated": "Last updated",
|
|
131
144
|
"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": "正在加载文档列表...",
|
|
@@ -87,6 +92,8 @@
|
|
|
87
92
|
"browserError": "无法打开浏览器",
|
|
88
93
|
"browserErrorHint": "请手动访问: https://docs.nbtca.space",
|
|
89
94
|
"retry": "是否重试?",
|
|
95
|
+
"tocTitle": "目录",
|
|
96
|
+
"tableHint": "表格在浏览器中效果更佳",
|
|
90
97
|
"endOfDocument": "文档结束 - 按 q 退出",
|
|
91
98
|
"githubRateLimited": "GitHub API 速率限制已达上限,将在 {time} 重置。",
|
|
92
99
|
"githubForbidden": "GitHub API 拒绝访问 (403)。",
|
|
@@ -119,13 +126,19 @@
|
|
|
119
126
|
"code": "代码",
|
|
120
127
|
"latency": "延迟",
|
|
121
128
|
"url": "URL",
|
|
122
|
-
"up": "
|
|
123
|
-
"down": "
|
|
124
|
-
"
|
|
129
|
+
"up": "在线",
|
|
130
|
+
"down": "离线",
|
|
131
|
+
"groupNbtca": "NBTCA 服务",
|
|
132
|
+
"groupExternal": "外部平台",
|
|
133
|
+
"groupIntranet": "内网服务(仅校园网)",
|
|
134
|
+
"serviceHomepage": "主页",
|
|
125
135
|
"serviceDocs": "文档",
|
|
126
|
-
"
|
|
136
|
+
"serviceIcal": "iCal 源",
|
|
137
|
+
"serviceRepair": "维修服务",
|
|
127
138
|
"serviceGithub": "GitHub",
|
|
128
|
-
"serviceRoadmap": "
|
|
139
|
+
"serviceRoadmap": "路线图",
|
|
140
|
+
"serviceCloud": "云存储",
|
|
141
|
+
"serviceMirror": "镜像站",
|
|
129
142
|
"watchStarted": "开始监控服务状态(每 {seconds} 秒)",
|
|
130
143
|
"watchUpdated": "最近更新",
|
|
131
144
|
"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.3.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",
|