@nbtca/prompt 1.0.23 → 1.0.24
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 +5 -1
- package/dist/config/paths.js +5 -0
- package/dist/config/preferences.js +1 -8
- package/dist/core/icons.js +18 -5
- package/dist/core/logo.js +1 -2
- package/dist/core/menu.js +7 -122
- package/dist/core/text.js +9 -3
- package/dist/core/ui.js +0 -1
- package/dist/core/vim-keys.js +6 -5
- package/dist/features/calendar.js +50 -9
- package/dist/features/docs.js +128 -49
- package/dist/features/links.js +42 -0
- package/dist/features/settings.js +120 -0
- package/dist/features/status.js +19 -26
- package/dist/features/theme.js +9 -101
- package/dist/features/update.js +74 -0
- package/dist/i18n/index.js +7 -16
- package/dist/i18n/locales/en.json +43 -54
- package/dist/i18n/locales/zh.json +43 -54
- package/dist/index.js +107 -67
- package/dist/main.js +14 -5
- package/package.json +4 -1
- package/dist/config/data.d.ts +0 -23
- package/dist/config/data.d.ts.map +0 -1
- package/dist/config/data.js.map +0 -1
- package/dist/config/preferences.d.ts +0 -14
- package/dist/config/preferences.d.ts.map +0 -1
- package/dist/config/preferences.js.map +0 -1
- package/dist/config/theme.d.ts +0 -23
- package/dist/config/theme.d.ts.map +0 -1
- package/dist/config/theme.js +0 -25
- package/dist/config/theme.js.map +0 -1
- package/dist/core/icons.d.ts +0 -3
- package/dist/core/icons.d.ts.map +0 -1
- package/dist/core/icons.js.map +0 -1
- package/dist/core/logo.d.ts +0 -9
- package/dist/core/logo.d.ts.map +0 -1
- package/dist/core/logo.js.map +0 -1
- package/dist/core/menu.d.ts +0 -14
- package/dist/core/menu.d.ts.map +0 -1
- package/dist/core/menu.js.map +0 -1
- package/dist/core/text.d.ts +0 -7
- package/dist/core/text.d.ts.map +0 -1
- package/dist/core/text.js.map +0 -1
- package/dist/core/ui.d.ts +0 -38
- package/dist/core/ui.d.ts.map +0 -1
- package/dist/core/ui.js.map +0 -1
- package/dist/core/vim-keys.d.ts +0 -8
- package/dist/core/vim-keys.d.ts.map +0 -1
- package/dist/core/vim-keys.js.map +0 -1
- package/dist/features/calendar.d.ts +0 -29
- package/dist/features/calendar.d.ts.map +0 -1
- package/dist/features/calendar.js.map +0 -1
- package/dist/features/docs.d.ts +0 -8
- package/dist/features/docs.d.ts.map +0 -1
- package/dist/features/docs.js.map +0 -1
- package/dist/features/repair.d.ts +0 -10
- package/dist/features/repair.d.ts.map +0 -1
- package/dist/features/repair.js +0 -29
- package/dist/features/repair.js.map +0 -1
- package/dist/features/status.d.ts +0 -31
- package/dist/features/status.d.ts.map +0 -1
- package/dist/features/status.js.map +0 -1
- package/dist/features/theme.d.ts +0 -8
- package/dist/features/theme.d.ts.map +0 -1
- package/dist/features/theme.js.map +0 -1
- package/dist/features/website.d.ts +0 -30
- package/dist/features/website.d.ts.map +0 -1
- package/dist/features/website.js +0 -48
- package/dist/features/website.js.map +0 -1
- package/dist/i18n/index.d.ts +0 -209
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/index.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/main.d.ts +0 -12
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/types.d.ts +0 -48
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
package/dist/features/docs.js
CHANGED
|
@@ -7,11 +7,11 @@ import { marked } from 'marked';
|
|
|
7
7
|
import TerminalRenderer from 'marked-terminal';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import open from 'open';
|
|
10
|
-
import { select, isCancel, confirm } from '@clack/prompts';
|
|
10
|
+
import { select, isCancel, confirm, text } from '@clack/prompts';
|
|
11
11
|
import { error, warning, success, createSpinner } from '../core/ui.js';
|
|
12
12
|
import { pickIcon } from '../core/icons.js';
|
|
13
13
|
import { spawn, execFileSync } from 'child_process';
|
|
14
|
-
import { URLS } from '../config/data.js';
|
|
14
|
+
import { APP_INFO, GITHUB_REPO, URLS } from '../config/data.js';
|
|
15
15
|
import { t } from '../i18n/index.js';
|
|
16
16
|
function detectTerminalType() {
|
|
17
17
|
const term = (process.env['TERM'] || '').toLowerCase();
|
|
@@ -38,8 +38,26 @@ function commandExists(cmd) {
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
let _terminalType = null;
|
|
42
|
+
function getTerminalType() {
|
|
43
|
+
if (_terminalType === null)
|
|
44
|
+
_terminalType = detectTerminalType();
|
|
45
|
+
return _terminalType;
|
|
46
|
+
}
|
|
47
|
+
let _hasGlow = null;
|
|
48
|
+
function hasGlow() {
|
|
49
|
+
if (_hasGlow === null)
|
|
50
|
+
_hasGlow = commandExists('glow');
|
|
51
|
+
return _hasGlow;
|
|
52
|
+
}
|
|
53
|
+
let _markedConfigured = false;
|
|
54
|
+
function ensureMarkedConfigured() {
|
|
55
|
+
if (_markedConfigured)
|
|
56
|
+
return;
|
|
57
|
+
_markedConfigured = true;
|
|
58
|
+
// @ts-ignore - marked v11 / marked-terminal v7 type incompatibility
|
|
59
|
+
marked.setOptions({ renderer: new TerminalRenderer(getRendererOptions(getTerminalType())) });
|
|
60
|
+
}
|
|
43
61
|
// ─── marked-terminal renderer ─────────────────────────────────────────────────
|
|
44
62
|
function getRendererOptions(type) {
|
|
45
63
|
// Cap at 80 columns — optimal prose reading width regardless of terminal size
|
|
@@ -82,10 +100,7 @@ function getRendererOptions(type) {
|
|
|
82
100
|
}
|
|
83
101
|
};
|
|
84
102
|
}
|
|
85
|
-
// @ts-ignore
|
|
86
|
-
marked.setOptions({ renderer: new TerminalRenderer(getRendererOptions(TERMINAL_TYPE)) });
|
|
87
103
|
// ─── GitHub data layer ────────────────────────────────────────────────────────
|
|
88
|
-
const GITHUB_REPO = { owner: 'nbtca', repo: 'documents', branch: 'main' };
|
|
89
104
|
const GITHUB_TOKEN = process.env['GITHUB_TOKEN'] || process.env['GH_TOKEN'];
|
|
90
105
|
const SKIP_NAMES = new Set(['node_modules', 'package.json', 'pnpm-lock.yaml']);
|
|
91
106
|
const DIR_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -96,15 +111,13 @@ const fileCache = new Map();
|
|
|
96
111
|
const renderCache = new Map();
|
|
97
112
|
function getDocCategories() {
|
|
98
113
|
const trans = t();
|
|
99
|
-
const withIcon = (unicodeIcon, asciiIcon, label) => `${pickIcon(unicodeIcon, asciiIcon)} ${label}`;
|
|
100
114
|
return [
|
|
101
|
-
{ name:
|
|
102
|
-
{ name:
|
|
103
|
-
{ name:
|
|
104
|
-
{ name:
|
|
105
|
-
{ name:
|
|
106
|
-
{ name:
|
|
107
|
-
{ name: withIcon('📄', '[README]', trans.docs.categoryReadme), path: 'README.md' },
|
|
115
|
+
{ name: trans.docs.categoryTutorial, path: 'tutorial' },
|
|
116
|
+
{ name: trans.docs.categoryRepairLogs, path: '维修日' },
|
|
117
|
+
{ name: trans.docs.categoryEvents, path: '相关活动举办' },
|
|
118
|
+
{ name: trans.docs.categoryProcess, path: 'process' },
|
|
119
|
+
{ name: trans.docs.categoryRepair, path: 'repair' },
|
|
120
|
+
{ name: trans.docs.categoryArchived, path: 'archived' },
|
|
108
121
|
];
|
|
109
122
|
}
|
|
110
123
|
function getFreshCacheValue(cache, key) {
|
|
@@ -144,7 +157,7 @@ async function fetchGitHubDirectory(path = '', options = {}) {
|
|
|
144
157
|
try {
|
|
145
158
|
const headers = {
|
|
146
159
|
'Accept': 'application/vnd.github.v3+json',
|
|
147
|
-
'User-Agent':
|
|
160
|
+
'User-Agent': `NBTCA-CLI/${APP_INFO.version}`
|
|
148
161
|
};
|
|
149
162
|
if (GITHUB_TOKEN)
|
|
150
163
|
headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`;
|
|
@@ -156,7 +169,7 @@ async function fetchGitHubDirectory(path = '', options = {}) {
|
|
|
156
169
|
.map((item) => ({
|
|
157
170
|
name: item.name,
|
|
158
171
|
path: item.path,
|
|
159
|
-
type: item.type === 'dir' ? 'dir' : 'file',
|
|
172
|
+
type: (item.type === 'dir' ? 'dir' : 'file'),
|
|
160
173
|
sha: item.sha
|
|
161
174
|
}))
|
|
162
175
|
.sort((a, b) => {
|
|
@@ -176,11 +189,12 @@ async function fetchGitHubDirectory(path = '', options = {}) {
|
|
|
176
189
|
}
|
|
177
190
|
const trans = t();
|
|
178
191
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
192
|
+
const axiosErr = err;
|
|
193
|
+
if (axiosErr.response?.status === 403) {
|
|
194
|
+
const rateLimitRemaining = axiosErr.response.headers?.['x-ratelimit-remaining'];
|
|
195
|
+
const rateLimitReset = axiosErr.response.headers?.['x-ratelimit-reset'];
|
|
196
|
+
if (rateLimitRemaining === '0' && rateLimitReset) {
|
|
197
|
+
const resetDate = new Date(Number.parseInt(rateLimitReset, 10) * 1000);
|
|
184
198
|
throw new Error(`${trans.docs.githubRateLimited.replace('{time}', resetDate.toLocaleTimeString())}\n${trans.docs.githubTokenHint}`);
|
|
185
199
|
}
|
|
186
200
|
throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`);
|
|
@@ -197,7 +211,7 @@ async function fetchGitHubRawContent(path, options = {}) {
|
|
|
197
211
|
}
|
|
198
212
|
const url = `https://raw.githubusercontent.com/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}/${GITHUB_REPO.branch}/${path}`;
|
|
199
213
|
try {
|
|
200
|
-
const response = await axios.get(url, { timeout: 15000, headers: { 'User-Agent':
|
|
214
|
+
const response = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` } });
|
|
201
215
|
const content = String(response.data);
|
|
202
216
|
setCacheValue(fileCache, path, content, FILE_CACHE_TTL_MS);
|
|
203
217
|
return { data: content, fromCache: false, staleFallback: false };
|
|
@@ -219,7 +233,7 @@ const CONTAINER_ICONS_ASCII = {
|
|
|
219
233
|
const CONTAINER_ICONS_UNICODE = {
|
|
220
234
|
info: 'ℹ️', tip: '💡', warning: '⚠️', danger: '🚨', details: '▶️'
|
|
221
235
|
};
|
|
222
|
-
function cleanMarkdownContent(content, type) {
|
|
236
|
+
function cleanMarkdownContent(content, type = getTerminalType()) {
|
|
223
237
|
let c = content;
|
|
224
238
|
// 1. YAML frontmatter
|
|
225
239
|
c = c.replace(/^---\n[\s\S]*?\n---\n?/m, '');
|
|
@@ -259,9 +273,17 @@ function cleanMarkdownContent(content, type) {
|
|
|
259
273
|
c = c.replace(/\n{3,}/g, '\n\n');
|
|
260
274
|
return c.trim();
|
|
261
275
|
}
|
|
262
|
-
function extractDocTitle(
|
|
263
|
-
|
|
264
|
-
|
|
276
|
+
function extractDocTitle(rawContent, cleanedContent) {
|
|
277
|
+
// 1. Try YAML frontmatter title: field (before it was stripped)
|
|
278
|
+
const fmMatch = rawContent.match(/^---\n[\s\S]*?\n---/m);
|
|
279
|
+
if (fmMatch) {
|
|
280
|
+
const titleMatch = fmMatch[0].match(/^title:\s*['"]?(.+?)['"]?\s*$/m);
|
|
281
|
+
if (titleMatch?.[1])
|
|
282
|
+
return titleMatch[1].trim();
|
|
283
|
+
}
|
|
284
|
+
// 2. Fallback to first # H1 heading in cleaned content
|
|
285
|
+
const h1Match = cleanedContent.match(/^#\s+(.+)$/m);
|
|
286
|
+
return h1Match?.[1]?.trim() ?? null;
|
|
265
287
|
}
|
|
266
288
|
/** Approximate reading time: ~200 words/min for technical Chinese/English prose. */
|
|
267
289
|
function estimateReadTime(text) {
|
|
@@ -345,7 +367,7 @@ async function browseDirectory(dirPath = '') {
|
|
|
345
367
|
const s = createSpinner(dirPath ? `${trans.docs.loadingDir}: ${dirPath}` : trans.docs.loading);
|
|
346
368
|
const result = await fetchGitHubDirectory(dirPath);
|
|
347
369
|
const items = result.data;
|
|
348
|
-
s.stop(
|
|
370
|
+
s.stop(dirPath || trans.docs.chooseDoc);
|
|
349
371
|
if (result.staleFallback) {
|
|
350
372
|
warning(trans.docs.usingCachedData);
|
|
351
373
|
}
|
|
@@ -354,15 +376,15 @@ async function browseDirectory(dirPath = '') {
|
|
|
354
376
|
return;
|
|
355
377
|
}
|
|
356
378
|
const options = [
|
|
357
|
-
...(dirPath ? [{ value: '__back__', label: chalk.
|
|
379
|
+
...(dirPath ? [{ value: '__back__', label: chalk.dim(trans.docs.upToParent) }] : []),
|
|
358
380
|
...items.map(item => ({
|
|
359
381
|
value: item.path,
|
|
360
382
|
label: item.type === 'dir'
|
|
361
|
-
? chalk.cyan(`${
|
|
362
|
-
:
|
|
363
|
-
hint: item.type === 'dir' ? 'dir' :
|
|
383
|
+
? chalk.cyan(`${item.name}/`)
|
|
384
|
+
: item.name,
|
|
385
|
+
hint: item.type === 'dir' ? 'dir' : undefined,
|
|
364
386
|
})),
|
|
365
|
-
{ value: '__exit__', label: chalk.
|
|
387
|
+
{ value: '__exit__', label: chalk.dim(trans.docs.returnToMenu) },
|
|
366
388
|
];
|
|
367
389
|
const selected = await select({
|
|
368
390
|
message: dirPath ? `${trans.docs.currentDir}: ${dirPath}` : trans.docs.chooseDoc,
|
|
@@ -387,7 +409,8 @@ async function browseDirectory(dirPath = '') {
|
|
|
387
409
|
}
|
|
388
410
|
catch (err) {
|
|
389
411
|
error(trans.docs.loadError);
|
|
390
|
-
|
|
412
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
413
|
+
console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
|
|
391
414
|
const retry = await confirm({ message: trans.docs.retry });
|
|
392
415
|
if (!isCancel(retry) && retry) {
|
|
393
416
|
await browseDirectory(dirPath);
|
|
@@ -398,6 +421,7 @@ async function browseDirectory(dirPath = '') {
|
|
|
398
421
|
async function viewMarkdownFile(filePath) {
|
|
399
422
|
const trans = t();
|
|
400
423
|
try {
|
|
424
|
+
ensureMarkedConfigured();
|
|
401
425
|
const s = createSpinner(`${trans.docs.loading.replace('...', '')}: ${filePath}`);
|
|
402
426
|
const rawResult = await fetchGitHubRawContent(filePath);
|
|
403
427
|
if (rawResult.staleFallback) {
|
|
@@ -411,15 +435,15 @@ async function viewMarkdownFile(filePath) {
|
|
|
411
435
|
renderedDoc = cachedRendered;
|
|
412
436
|
}
|
|
413
437
|
else {
|
|
414
|
-
const cleaned = cleanMarkdownContent(rawContent,
|
|
415
|
-
const title = extractDocTitle(cleaned) || filePath.split('/').pop() || filePath;
|
|
438
|
+
const cleaned = cleanMarkdownContent(rawContent, getTerminalType());
|
|
439
|
+
const title = extractDocTitle(rawContent, cleaned) || filePath.split('/').pop() || filePath;
|
|
416
440
|
const readTime = estimateReadTime(cleaned);
|
|
417
441
|
const rendered = await marked(cleaned);
|
|
418
442
|
renderedDoc = { fingerprint, cleaned, rendered, title, readTime };
|
|
419
443
|
setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS);
|
|
420
444
|
}
|
|
421
445
|
s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`);
|
|
422
|
-
if (
|
|
446
|
+
if (hasGlow()) {
|
|
423
447
|
await displayWithGlow(renderedDoc.cleaned);
|
|
424
448
|
}
|
|
425
449
|
else {
|
|
@@ -431,9 +455,9 @@ async function viewMarkdownFile(filePath) {
|
|
|
431
455
|
const action = await select({
|
|
432
456
|
message: trans.docs.chooseAction,
|
|
433
457
|
options: [
|
|
434
|
-
{ value: 'back', label:
|
|
435
|
-
{ value: 'reread', label:
|
|
436
|
-
{ value: 'browser', label:
|
|
458
|
+
{ value: 'back', label: trans.docs.backToList },
|
|
459
|
+
{ value: 'reread', label: trans.docs.reread },
|
|
460
|
+
{ value: 'browser', label: trans.docs.openBrowser },
|
|
437
461
|
],
|
|
438
462
|
});
|
|
439
463
|
if (isCancel(action))
|
|
@@ -445,7 +469,8 @@ async function viewMarkdownFile(filePath) {
|
|
|
445
469
|
}
|
|
446
470
|
catch (err) {
|
|
447
471
|
error(trans.docs.loadError);
|
|
448
|
-
|
|
472
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
473
|
+
console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
|
|
449
474
|
const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt });
|
|
450
475
|
if (!isCancel(openBrowser) && openBrowser) {
|
|
451
476
|
await openDocsInBrowser(filePath);
|
|
@@ -469,6 +494,60 @@ export async function openDocsInBrowser(path) {
|
|
|
469
494
|
}
|
|
470
495
|
console.log();
|
|
471
496
|
}
|
|
497
|
+
// ─── Search ────────────────────────────────────────────────────────────────────
|
|
498
|
+
async function searchDocs() {
|
|
499
|
+
const trans = t();
|
|
500
|
+
const query = await text({
|
|
501
|
+
message: trans.docs.searchPrompt,
|
|
502
|
+
placeholder: trans.docs.searchPlaceholder,
|
|
503
|
+
});
|
|
504
|
+
if (isCancel(query) || !query.trim())
|
|
505
|
+
return;
|
|
506
|
+
const keyword = query.trim().toLowerCase();
|
|
507
|
+
const s = createSpinner(trans.docs.searching);
|
|
508
|
+
// Fetch all category directories in parallel
|
|
509
|
+
const categories = getDocCategories().filter(c => c.path !== 'README.md');
|
|
510
|
+
const results = [];
|
|
511
|
+
try {
|
|
512
|
+
const fetches = await Promise.allSettled(categories.map(async (cat) => {
|
|
513
|
+
const res = await fetchGitHubDirectory(cat.path);
|
|
514
|
+
return { items: res.data, category: cat.name };
|
|
515
|
+
}));
|
|
516
|
+
for (const result of fetches) {
|
|
517
|
+
if (result.status !== 'fulfilled')
|
|
518
|
+
continue;
|
|
519
|
+
for (const item of result.value.items) {
|
|
520
|
+
const nameLC = item.name.toLowerCase();
|
|
521
|
+
if (nameLC.includes(keyword)) {
|
|
522
|
+
results.push({ name: item.name, path: item.path, category: result.value.category });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
s.stop(`${results.length} ${trans.docs.searchResults}`);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
s.error(trans.docs.loadError);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (results.length === 0) {
|
|
533
|
+
warning(trans.docs.searchNoResults);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const selected = await select({
|
|
537
|
+
message: trans.docs.chooseDoc,
|
|
538
|
+
options: [
|
|
539
|
+
...results.map(r => ({
|
|
540
|
+
value: r.path,
|
|
541
|
+
label: r.name,
|
|
542
|
+
hint: r.category,
|
|
543
|
+
})),
|
|
544
|
+
{ value: '__back__', label: chalk.dim(trans.docs.returnToMenu) },
|
|
545
|
+
],
|
|
546
|
+
});
|
|
547
|
+
if (isCancel(selected) || selected === '__back__')
|
|
548
|
+
return;
|
|
549
|
+
await viewMarkdownFile(selected);
|
|
550
|
+
}
|
|
472
551
|
// ─── Menu ─────────────────────────────────────────────────────────────────────
|
|
473
552
|
export async function showDocsMenu() {
|
|
474
553
|
while (true) {
|
|
@@ -476,9 +555,10 @@ export async function showDocsMenu() {
|
|
|
476
555
|
const categories = getDocCategories();
|
|
477
556
|
const options = [
|
|
478
557
|
...categories.map(cat => ({ value: cat.path, label: cat.name })),
|
|
479
|
-
{ value: '
|
|
480
|
-
{ value: '
|
|
481
|
-
{ value: '
|
|
558
|
+
{ value: 'search', label: chalk.dim(trans.docs.searchPrompt.replace(':', '')) },
|
|
559
|
+
{ value: 'refresh-cache', label: chalk.dim(trans.docs.refreshCache) },
|
|
560
|
+
{ value: 'browser', label: chalk.dim(trans.docs.openBrowser) },
|
|
561
|
+
{ value: 'back', label: chalk.dim(trans.docs.returnToMenu) },
|
|
482
562
|
];
|
|
483
563
|
const action = await select({
|
|
484
564
|
message: trans.docs.chooseCategory,
|
|
@@ -486,19 +566,18 @@ export async function showDocsMenu() {
|
|
|
486
566
|
});
|
|
487
567
|
if (isCancel(action) || action === 'back')
|
|
488
568
|
return;
|
|
489
|
-
if (action === '
|
|
569
|
+
if (action === 'search') {
|
|
570
|
+
await searchDocs();
|
|
571
|
+
}
|
|
572
|
+
else if (action === 'refresh-cache') {
|
|
490
573
|
clearDocsCache();
|
|
491
574
|
success(trans.docs.cacheCleared);
|
|
492
575
|
}
|
|
493
576
|
else if (action === 'browser') {
|
|
494
577
|
await openDocsInBrowser();
|
|
495
578
|
}
|
|
496
|
-
else if (action === 'README.md') {
|
|
497
|
-
await viewMarkdownFile('README.md');
|
|
498
|
-
}
|
|
499
579
|
else {
|
|
500
580
|
await browseDirectory(action);
|
|
501
581
|
}
|
|
502
582
|
}
|
|
503
583
|
}
|
|
504
|
-
//# sourceMappingURL=docs.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links — open NBTCA resources in browser
|
|
3
|
+
*/
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { select, isCancel } from '@clack/prompts';
|
|
7
|
+
import { createSpinner } from '../core/ui.js';
|
|
8
|
+
import { URLS } from '../config/data.js';
|
|
9
|
+
import { t } from '../i18n/index.js';
|
|
10
|
+
async function openUrl(url) {
|
|
11
|
+
const trans = t();
|
|
12
|
+
const s = createSpinner(trans.links.opening);
|
|
13
|
+
try {
|
|
14
|
+
await open(url);
|
|
15
|
+
s.stop(trans.links.opened);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
s.error(trans.links.error);
|
|
19
|
+
console.log(chalk.dim(` ${url}`));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function showLinksMenu() {
|
|
23
|
+
const trans = t();
|
|
24
|
+
const selected = await select({
|
|
25
|
+
message: trans.links.choose,
|
|
26
|
+
options: [
|
|
27
|
+
{ value: URLS.homepage, label: trans.links.website },
|
|
28
|
+
{ value: URLS.github, label: trans.links.github },
|
|
29
|
+
{ value: URLS.roadmap, label: trans.links.roadmap },
|
|
30
|
+
{ value: URLS.repair, label: trans.links.repair },
|
|
31
|
+
{ value: '__back__', label: chalk.dim(trans.common.back) },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
if (isCancel(selected) || selected === '__back__')
|
|
35
|
+
return;
|
|
36
|
+
await openUrl(selected);
|
|
37
|
+
}
|
|
38
|
+
/** Direct openers for CLI non-interactive mode */
|
|
39
|
+
export async function openHomepage() { await openUrl(URLS.homepage); }
|
|
40
|
+
export async function openGithub() { await openUrl(URLS.github); }
|
|
41
|
+
export async function openRoadmap() { await openUrl(URLS.roadmap); }
|
|
42
|
+
export async function openRepairService() { await openUrl(URLS.repair); }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified settings — language, theme, about
|
|
3
|
+
*/
|
|
4
|
+
import { select, isCancel, note } from '@clack/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../config/preferences.js';
|
|
7
|
+
import { pickIcon } from '../core/icons.js';
|
|
8
|
+
import { resetIconCache } from '../core/icons.js';
|
|
9
|
+
import { padEndV } from '../core/text.js';
|
|
10
|
+
import { success, warning } from '../core/ui.js';
|
|
11
|
+
import { APP_INFO, URLS } from '../config/data.js';
|
|
12
|
+
import { t, getCurrentLanguage, setLanguage, clearTranslationCache } from '../i18n/index.js';
|
|
13
|
+
function notifyResult(saved, successMsg, warningMsg) {
|
|
14
|
+
if (saved) {
|
|
15
|
+
success(successMsg);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
warning(warningMsg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function showAbout() {
|
|
22
|
+
const trans = t();
|
|
23
|
+
const pad = 12;
|
|
24
|
+
const row = (label, value) => `${chalk.dim(padEndV(label, pad))}${value}`;
|
|
25
|
+
const link = (label, url) => row(label, chalk.cyan(url));
|
|
26
|
+
const content = [
|
|
27
|
+
row(trans.about.project, APP_INFO.name),
|
|
28
|
+
row(trans.about.version, `v${APP_INFO.version}`),
|
|
29
|
+
row(trans.about.description, APP_INFO.fullDescription),
|
|
30
|
+
'',
|
|
31
|
+
link(trans.about.github, APP_INFO.repository),
|
|
32
|
+
link(trans.about.website, URLS.homepage),
|
|
33
|
+
link(trans.about.email, URLS.email),
|
|
34
|
+
'',
|
|
35
|
+
row(trans.about.license, `MIT ${pickIcon('·', '|')} ${trans.about.author}: m1ngsama`),
|
|
36
|
+
].join('\n');
|
|
37
|
+
note(content, trans.about.title);
|
|
38
|
+
}
|
|
39
|
+
export async function showSettingsMenu() {
|
|
40
|
+
while (true) {
|
|
41
|
+
const trans = t();
|
|
42
|
+
const prefs = loadPreferences();
|
|
43
|
+
const currentLang = getCurrentLanguage();
|
|
44
|
+
const action = await select({
|
|
45
|
+
message: trans.theme.chooseAction,
|
|
46
|
+
options: [
|
|
47
|
+
{ value: 'language', label: trans.language.selectLanguage.replace(':', ''), hint: currentLang === 'zh' ? trans.language.zh : trans.language.en },
|
|
48
|
+
{ value: 'icon', label: trans.theme.iconMode, hint: prefs.iconMode },
|
|
49
|
+
{ value: 'color', label: trans.theme.colorMode, hint: prefs.colorMode },
|
|
50
|
+
{ value: 'reset', label: trans.theme.reset },
|
|
51
|
+
{ value: 'about', label: trans.about.title },
|
|
52
|
+
{ value: 'back', label: chalk.dim(trans.common.back) },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
if (isCancel(action) || action === 'back')
|
|
56
|
+
return;
|
|
57
|
+
if (action === 'about') {
|
|
58
|
+
showAbout();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (action === 'language') {
|
|
62
|
+
const language = await select({
|
|
63
|
+
message: trans.language.selectLanguage,
|
|
64
|
+
options: [
|
|
65
|
+
{ value: 'zh', label: trans.language.zh, hint: currentLang === 'zh' ? trans.common.current : undefined },
|
|
66
|
+
{ value: 'en', label: trans.language.en, hint: currentLang === 'en' ? trans.common.current : undefined },
|
|
67
|
+
],
|
|
68
|
+
initialValue: currentLang,
|
|
69
|
+
});
|
|
70
|
+
if (isCancel(language))
|
|
71
|
+
continue;
|
|
72
|
+
if (language !== currentLang) {
|
|
73
|
+
const saved = setLanguage(language);
|
|
74
|
+
clearTranslationCache();
|
|
75
|
+
notifyResult(saved, t().language.changed, t().language.changedSessionOnly);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (action === 'icon') {
|
|
80
|
+
const mode = await select({
|
|
81
|
+
message: trans.theme.chooseIconMode,
|
|
82
|
+
options: [
|
|
83
|
+
{ value: 'auto', label: trans.theme.modeAuto, hint: prefs.iconMode === 'auto' ? trans.common.current : undefined },
|
|
84
|
+
{ value: 'ascii', label: trans.theme.modeAscii, hint: prefs.iconMode === 'ascii' ? trans.common.current : undefined },
|
|
85
|
+
{ value: 'unicode', label: trans.theme.modeUnicode, hint: prefs.iconMode === 'unicode' ? trans.common.current : undefined },
|
|
86
|
+
],
|
|
87
|
+
initialValue: prefs.iconMode,
|
|
88
|
+
});
|
|
89
|
+
if (isCancel(mode))
|
|
90
|
+
continue;
|
|
91
|
+
const saved = setIconMode(mode);
|
|
92
|
+
resetIconCache();
|
|
93
|
+
notifyResult(saved, trans.theme.updated, trans.theme.updatedSessionOnly);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (action === 'color') {
|
|
97
|
+
const mode = await select({
|
|
98
|
+
message: trans.theme.chooseColorMode,
|
|
99
|
+
options: [
|
|
100
|
+
{ value: 'auto', label: trans.theme.modeAuto, hint: prefs.colorMode === 'auto' ? trans.common.current : undefined },
|
|
101
|
+
{ value: 'on', label: trans.theme.modeOn, hint: prefs.colorMode === 'on' ? trans.common.current : undefined },
|
|
102
|
+
{ value: 'off', label: trans.theme.modeOff, hint: prefs.colorMode === 'off' ? trans.common.current : undefined },
|
|
103
|
+
],
|
|
104
|
+
initialValue: prefs.colorMode,
|
|
105
|
+
});
|
|
106
|
+
if (isCancel(mode))
|
|
107
|
+
continue;
|
|
108
|
+
const saved = setColorMode(mode);
|
|
109
|
+
applyColorModePreference(false);
|
|
110
|
+
notifyResult(saved, trans.theme.updated, trans.theme.updatedSessionOnly);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (action === 'reset') {
|
|
114
|
+
const saved = resetPreferences();
|
|
115
|
+
resetIconCache();
|
|
116
|
+
applyColorModePreference(false);
|
|
117
|
+
notifyResult(saved, trans.theme.reset, trans.theme.resetSessionOnly);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/features/status.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { URLS } from '../config/data.js';
|
|
3
|
+
import { APP_INFO, URLS } from '../config/data.js';
|
|
4
4
|
import { pickIcon } from '../core/icons.js';
|
|
5
5
|
import { padEndV } from '../core/text.js';
|
|
6
|
-
import { createSpinner
|
|
6
|
+
import { createSpinner } from '../core/ui.js';
|
|
7
7
|
import { t } from '../i18n/index.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
function getServiceTargets() {
|
|
9
|
+
const trans = t();
|
|
10
|
+
return [
|
|
11
|
+
{ name: trans.status.serviceWebsite, url: URLS.homepage },
|
|
12
|
+
{ name: trans.status.serviceDocs, url: URLS.docs },
|
|
13
|
+
{ name: trans.status.serviceCalendar, url: URLS.calendar },
|
|
14
|
+
{ name: trans.status.serviceGithub, url: URLS.github },
|
|
15
|
+
{ name: trans.status.serviceRoadmap, url: URLS.roadmap },
|
|
16
|
+
];
|
|
17
|
+
}
|
|
15
18
|
async function checkService(name, url, timeoutMs) {
|
|
16
19
|
const start = Date.now();
|
|
17
20
|
try {
|
|
@@ -19,7 +22,7 @@ async function checkService(name, url, timeoutMs) {
|
|
|
19
22
|
timeout: timeoutMs,
|
|
20
23
|
maxRedirects: 5,
|
|
21
24
|
validateStatus: () => true,
|
|
22
|
-
headers: { 'User-Agent':
|
|
25
|
+
headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
|
|
23
26
|
});
|
|
24
27
|
const latencyMs = Date.now() - start;
|
|
25
28
|
const ok = response.status >= 200 && response.status < 400;
|
|
@@ -49,7 +52,7 @@ async function checkServiceWithRetry(name, url, timeoutMs, retries) {
|
|
|
49
52
|
export async function checkServices(options = {}) {
|
|
50
53
|
const timeoutMs = options.timeoutMs ?? 6000;
|
|
51
54
|
const retries = options.retries ?? 1;
|
|
52
|
-
return Promise.all(
|
|
55
|
+
return Promise.all(getServiceTargets().map((service) => checkServiceWithRetry(service.name, service.url, timeoutMs, retries)));
|
|
53
56
|
}
|
|
54
57
|
export function serializeServiceStatus(items) {
|
|
55
58
|
return items.map((item) => ({
|
|
@@ -87,7 +90,6 @@ export function renderServiceStatusTable(items, options) {
|
|
|
87
90
|
const trans = t();
|
|
88
91
|
const nameWidth = 10;
|
|
89
92
|
const statusWidth = 9;
|
|
90
|
-
const codeWidth = 7;
|
|
91
93
|
const latencyWidth = 10;
|
|
92
94
|
const h = pickIcon('─', '-');
|
|
93
95
|
const v = pickIcon('│', '|');
|
|
@@ -100,19 +102,17 @@ export function renderServiceStatusTable(items, options) {
|
|
|
100
102
|
const bottomLeft = pickIcon('└', '+');
|
|
101
103
|
const bottomMid = pickIcon('┴', '+');
|
|
102
104
|
const bottomRight = pickIcon('┘', '+');
|
|
103
|
-
const top = `${topLeft}${h.repeat(nameWidth + 2)}${topMid}${h.repeat(statusWidth + 2)}${topMid}${h.repeat(
|
|
104
|
-
const divider = `${midLeft}${h.repeat(nameWidth + 2)}${midMid}${h.repeat(statusWidth + 2)}${midMid}${h.repeat(
|
|
105
|
-
const bottom = `${bottomLeft}${h.repeat(nameWidth + 2)}${bottomMid}${h.repeat(statusWidth + 2)}${bottomMid}${h.repeat(
|
|
106
|
-
const header = `${v} ${padEndV(trans.status.service, nameWidth)} ${v} ${padEndV(trans.status.health, statusWidth)} ${v} ${padEndV(trans.status.
|
|
105
|
+
const top = `${topLeft}${h.repeat(nameWidth + 2)}${topMid}${h.repeat(statusWidth + 2)}${topMid}${h.repeat(latencyWidth + 2)}${topRight}`;
|
|
106
|
+
const divider = `${midLeft}${h.repeat(nameWidth + 2)}${midMid}${h.repeat(statusWidth + 2)}${midMid}${h.repeat(latencyWidth + 2)}${midRight}`;
|
|
107
|
+
const bottom = `${bottomLeft}${h.repeat(nameWidth + 2)}${bottomMid}${h.repeat(statusWidth + 2)}${bottomMid}${h.repeat(latencyWidth + 2)}${bottomRight}`;
|
|
108
|
+
const header = `${v} ${padEndV(trans.status.service, nameWidth)} ${v} ${padEndV(trans.status.health, statusWidth)} ${v} ${padEndV(trans.status.latency, latencyWidth)} ${v}`;
|
|
107
109
|
const lines = [dim(top), header, dim(divider)];
|
|
108
110
|
for (const item of items) {
|
|
109
111
|
const statusLabel = item.ok
|
|
110
112
|
? green(`${pickIcon('●', 'OK')} ${trans.status.up}`)
|
|
111
113
|
: red(`${pickIcon('●', '!!')} ${trans.status.down}`);
|
|
112
|
-
const code = item.statusCode ? String(item.statusCode) : '-';
|
|
113
114
|
const latency = item.latencyMs != null ? `${item.latencyMs}ms` : '-';
|
|
114
|
-
|
|
115
|
-
lines.push(`${v} ${padEndV(cyan(item.name), nameWidth)} ${v} ${padEndV(statusLabel, statusWidth)} ${v} ${padEndV(code, codeWidth)} ${v} ${padEndV(latency, latencyWidth)} ${v} ${padEndV(url, 34)} ${v}`);
|
|
115
|
+
lines.push(`${v} ${padEndV(cyan(item.name), nameWidth)} ${v} ${padEndV(statusLabel, statusWidth)} ${v} ${padEndV(latency, latencyWidth)} ${v}`);
|
|
116
116
|
}
|
|
117
117
|
lines.push(dim(bottom));
|
|
118
118
|
return lines.join('\n');
|
|
@@ -129,12 +129,5 @@ export async function showServiceStatus() {
|
|
|
129
129
|
spinner.stop(trans.status.summaryOk);
|
|
130
130
|
}
|
|
131
131
|
console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
|
|
132
|
-
if (hasFailures) {
|
|
133
|
-
warning(trans.status.summaryFail);
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
success(trans.status.summaryOk);
|
|
137
|
-
}
|
|
138
132
|
return items;
|
|
139
133
|
}
|
|
140
|
-
//# sourceMappingURL=status.js.map
|