@nbtca/prompt 1.0.23 → 1.0.25
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 +24 -0
- package/dist/config/preferences.js +4 -15
- 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 +23 -14
- package/dist/core/ui.js +16 -1
- package/dist/core/vim-keys.js +11 -6
- package/dist/features/calendar.js +66 -16
- package/dist/features/docs.js +270 -131
- package/dist/features/links.js +37 -0
- package/dist/features/settings.js +120 -0
- package/dist/features/status.js +28 -32
- package/dist/features/theme.js +9 -101
- package/dist/features/update.js +74 -0
- package/dist/i18n/index.js +20 -21
- package/dist/i18n/locales/en.json +43 -54
- package/dist/i18n/locales/zh.json +43 -54
- package/dist/index.js +115 -84
- package/dist/main.js +13 -14
- package/package.json +13 -11
- 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
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
* 知识库终端查看模块
|
|
3
3
|
* 获取并渲染Markdown文档
|
|
4
4
|
*/
|
|
5
|
-
import axios from 'axios';
|
|
6
5
|
import { marked } from 'marked';
|
|
7
6
|
import TerminalRenderer from 'marked-terminal';
|
|
8
7
|
import chalk from 'chalk';
|
|
9
8
|
import open from 'open';
|
|
10
|
-
import { select, isCancel, confirm } from '@clack/prompts';
|
|
9
|
+
import { select, isCancel, confirm, text } from '@clack/prompts';
|
|
11
10
|
import { error, warning, success, createSpinner } from '../core/ui.js';
|
|
12
11
|
import { pickIcon } from '../core/icons.js';
|
|
13
12
|
import { spawn, execFileSync } from 'child_process';
|
|
14
|
-
import { URLS } from '../config/data.js';
|
|
15
|
-
import { t } from '../i18n/index.js';
|
|
13
|
+
import { APP_INFO, GITHUB_REPO, URLS } from '../config/data.js';
|
|
14
|
+
import { t, fmt } from '../i18n/index.js';
|
|
15
|
+
import { setVimKeysActive } from '../core/vim-keys.js';
|
|
16
16
|
function detectTerminalType() {
|
|
17
17
|
const term = (process.env['TERM'] || '').toLowerCase();
|
|
18
18
|
const termProgram = (process.env['TERM_PROGRAM'] || '').toLowerCase();
|
|
@@ -38,8 +38,27 @@ 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
|
+
marked.use({
|
|
59
|
+
renderer: new TerminalRenderer(getRendererOptions(getTerminalType()))
|
|
60
|
+
});
|
|
61
|
+
}
|
|
43
62
|
// ─── marked-terminal renderer ─────────────────────────────────────────────────
|
|
44
63
|
function getRendererOptions(type) {
|
|
45
64
|
// Cap at 80 columns — optimal prose reading width regardless of terminal size
|
|
@@ -82,10 +101,7 @@ function getRendererOptions(type) {
|
|
|
82
101
|
}
|
|
83
102
|
};
|
|
84
103
|
}
|
|
85
|
-
// @ts-ignore
|
|
86
|
-
marked.setOptions({ renderer: new TerminalRenderer(getRendererOptions(TERMINAL_TYPE)) });
|
|
87
104
|
// ─── GitHub data layer ────────────────────────────────────────────────────────
|
|
88
|
-
const GITHUB_REPO = { owner: 'nbtca', repo: 'documents', branch: 'main' };
|
|
89
105
|
const GITHUB_TOKEN = process.env['GITHUB_TOKEN'] || process.env['GH_TOKEN'];
|
|
90
106
|
const SKIP_NAMES = new Set(['node_modules', 'package.json', 'pnpm-lock.yaml']);
|
|
91
107
|
const DIR_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -96,15 +112,13 @@ const fileCache = new Map();
|
|
|
96
112
|
const renderCache = new Map();
|
|
97
113
|
function getDocCategories() {
|
|
98
114
|
const trans = t();
|
|
99
|
-
const withIcon = (unicodeIcon, asciiIcon, label) => `${pickIcon(unicodeIcon, asciiIcon)} ${label}`;
|
|
100
115
|
return [
|
|
101
|
-
{ name:
|
|
102
|
-
{ name:
|
|
103
|
-
{ name:
|
|
104
|
-
{ name:
|
|
105
|
-
{ name:
|
|
106
|
-
{ name:
|
|
107
|
-
{ name: withIcon('📄', '[README]', trans.docs.categoryReadme), path: 'README.md' },
|
|
116
|
+
{ name: trans.docs.categoryTutorial, path: 'tutorial' },
|
|
117
|
+
{ name: trans.docs.categoryRepairLogs, path: '维修日' },
|
|
118
|
+
{ name: trans.docs.categoryEvents, path: '相关活动举办' },
|
|
119
|
+
{ name: trans.docs.categoryProcess, path: 'process' },
|
|
120
|
+
{ name: trans.docs.categoryRepair, path: 'repair' },
|
|
121
|
+
{ name: trans.docs.categoryArchived, path: 'archived' },
|
|
108
122
|
];
|
|
109
123
|
}
|
|
110
124
|
function getFreshCacheValue(cache, key) {
|
|
@@ -122,6 +136,18 @@ function getAnyCacheValue(cache, key) {
|
|
|
122
136
|
function setCacheValue(cache, key, value, ttlMs) {
|
|
123
137
|
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
124
138
|
}
|
|
139
|
+
const DIR_CACHE_MAX = 30;
|
|
140
|
+
const FILE_CACHE_MAX = 50;
|
|
141
|
+
const RENDER_CACHE_MAX = 50;
|
|
142
|
+
function evictStalest(cache, maxSize) {
|
|
143
|
+
if (cache.size <= maxSize)
|
|
144
|
+
return;
|
|
145
|
+
const oldest = [...cache.entries()]
|
|
146
|
+
.sort((a, b) => a[1].expiresAt - b[1].expiresAt)
|
|
147
|
+
.slice(0, cache.size - maxSize);
|
|
148
|
+
for (const [key] of oldest)
|
|
149
|
+
cache.delete(key);
|
|
150
|
+
}
|
|
125
151
|
function contentFingerprint(content) {
|
|
126
152
|
const head = content.slice(0, 80);
|
|
127
153
|
const tail = content.slice(-80);
|
|
@@ -144,19 +170,36 @@ async function fetchGitHubDirectory(path = '', options = {}) {
|
|
|
144
170
|
try {
|
|
145
171
|
const headers = {
|
|
146
172
|
'Accept': 'application/vnd.github.v3+json',
|
|
147
|
-
'User-Agent':
|
|
173
|
+
'User-Agent': `NBTCA-CLI/${APP_INFO.version}`,
|
|
148
174
|
};
|
|
149
175
|
if (GITHUB_TOKEN)
|
|
150
176
|
headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`;
|
|
151
|
-
const
|
|
152
|
-
const
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
179
|
+
const response = await fetch(url, { signal: controller.signal, headers });
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const trans = t();
|
|
183
|
+
if (response.status === 403) {
|
|
184
|
+
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
|
|
185
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
186
|
+
if (rateLimitRemaining === '0' && rateLimitReset) {
|
|
187
|
+
const resetDate = new Date(Number.parseInt(rateLimitReset, 10) * 1000);
|
|
188
|
+
throw new Error(`${fmt(trans.docs.githubRateLimited, { time: resetDate.toLocaleTimeString() })}\n${trans.docs.githubTokenHint}`);
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`);
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`HTTP ${response.status}`);
|
|
193
|
+
}
|
|
194
|
+
const data = (await response.json());
|
|
195
|
+
const items = data
|
|
153
196
|
.filter((item) => !item.name.startsWith('.') &&
|
|
154
197
|
!SKIP_NAMES.has(item.name) &&
|
|
155
198
|
!(item.type === 'file' && !item.name.endsWith('.md')))
|
|
156
199
|
.map((item) => ({
|
|
157
200
|
name: item.name,
|
|
158
201
|
path: item.path,
|
|
159
|
-
type: item.type === 'dir' ? 'dir' : 'file',
|
|
202
|
+
type: (item.type === 'dir' ? 'dir' : 'file'),
|
|
160
203
|
sha: item.sha
|
|
161
204
|
}))
|
|
162
205
|
.sort((a, b) => {
|
|
@@ -167,6 +210,7 @@ async function fetchGitHubDirectory(path = '', options = {}) {
|
|
|
167
210
|
return a.name.localeCompare(b.name);
|
|
168
211
|
});
|
|
169
212
|
setCacheValue(dirCache, cacheKey, items, DIR_CACHE_TTL_MS);
|
|
213
|
+
evictStalest(dirCache, DIR_CACHE_MAX);
|
|
170
214
|
return { data: items, fromCache: false, staleFallback: false };
|
|
171
215
|
}
|
|
172
216
|
catch (err) {
|
|
@@ -175,17 +219,10 @@ async function fetchGitHubDirectory(path = '', options = {}) {
|
|
|
175
219
|
return { data: staleCached, fromCache: true, staleFallback: true };
|
|
176
220
|
}
|
|
177
221
|
const trans = t();
|
|
178
|
-
const errorMessage = err instanceof Error
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (rateLimitRemaining === '0') {
|
|
183
|
-
const resetDate = new Date(parseInt(rateLimitReset) * 1000);
|
|
184
|
-
throw new Error(`${trans.docs.githubRateLimited.replace('{time}', resetDate.toLocaleTimeString())}\n${trans.docs.githubTokenHint}`);
|
|
185
|
-
}
|
|
186
|
-
throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`);
|
|
187
|
-
}
|
|
188
|
-
throw new Error(trans.docs.fetchDirFailed.replace('{error}', errorMessage));
|
|
222
|
+
const errorMessage = err instanceof Error
|
|
223
|
+
? (err.name === 'AbortError' ? 'Request timed out' : err.message)
|
|
224
|
+
: String(err);
|
|
225
|
+
throw new Error(fmt(trans.docs.fetchDirFailed, { error: errorMessage }));
|
|
189
226
|
}
|
|
190
227
|
}
|
|
191
228
|
async function fetchGitHubRawContent(path, options = {}) {
|
|
@@ -197,9 +234,18 @@ async function fetchGitHubRawContent(path, options = {}) {
|
|
|
197
234
|
}
|
|
198
235
|
const url = `https://raw.githubusercontent.com/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}/${GITHUB_REPO.branch}/${path}`;
|
|
199
236
|
try {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
239
|
+
const response = await fetch(url, {
|
|
240
|
+
signal: controller.signal,
|
|
241
|
+
headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
|
|
242
|
+
});
|
|
243
|
+
clearTimeout(timeout);
|
|
244
|
+
if (!response.ok)
|
|
245
|
+
throw new Error(`HTTP ${response.status}`);
|
|
246
|
+
const content = await response.text();
|
|
202
247
|
setCacheValue(fileCache, path, content, FILE_CACHE_TTL_MS);
|
|
248
|
+
evictStalest(fileCache, FILE_CACHE_MAX);
|
|
203
249
|
return { data: content, fromCache: false, staleFallback: false };
|
|
204
250
|
}
|
|
205
251
|
catch (err) {
|
|
@@ -208,8 +254,10 @@ async function fetchGitHubRawContent(path, options = {}) {
|
|
|
208
254
|
return { data: staleCached, fromCache: true, staleFallback: true };
|
|
209
255
|
}
|
|
210
256
|
const trans = t();
|
|
211
|
-
const errorMessage = err instanceof Error
|
|
212
|
-
|
|
257
|
+
const errorMessage = err instanceof Error
|
|
258
|
+
? (err.name === 'AbortError' ? 'Request timed out' : err.message)
|
|
259
|
+
: String(err);
|
|
260
|
+
throw new Error(fmt(trans.docs.fetchFileFailed, { error: errorMessage }));
|
|
213
261
|
}
|
|
214
262
|
}
|
|
215
263
|
// ─── Content cleaning ─────────────────────────────────────────────────────────
|
|
@@ -219,7 +267,7 @@ const CONTAINER_ICONS_ASCII = {
|
|
|
219
267
|
const CONTAINER_ICONS_UNICODE = {
|
|
220
268
|
info: 'ℹ️', tip: '💡', warning: '⚠️', danger: '🚨', details: '▶️'
|
|
221
269
|
};
|
|
222
|
-
function cleanMarkdownContent(content, type) {
|
|
270
|
+
function cleanMarkdownContent(content, type = getTerminalType()) {
|
|
223
271
|
let c = content;
|
|
224
272
|
// 1. YAML frontmatter
|
|
225
273
|
c = c.replace(/^---\n[\s\S]*?\n---\n?/m, '');
|
|
@@ -259,9 +307,17 @@ function cleanMarkdownContent(content, type) {
|
|
|
259
307
|
c = c.replace(/\n{3,}/g, '\n\n');
|
|
260
308
|
return c.trim();
|
|
261
309
|
}
|
|
262
|
-
function extractDocTitle(
|
|
263
|
-
|
|
264
|
-
|
|
310
|
+
function extractDocTitle(rawContent, cleanedContent) {
|
|
311
|
+
// 1. Try YAML frontmatter title: field (before it was stripped)
|
|
312
|
+
const fmMatch = rawContent.match(/^---\n[\s\S]*?\n---/m);
|
|
313
|
+
if (fmMatch) {
|
|
314
|
+
const titleMatch = fmMatch[0].match(/^title:\s*['"]?(.+?)['"]?\s*$/m);
|
|
315
|
+
if (titleMatch?.[1])
|
|
316
|
+
return titleMatch[1].trim();
|
|
317
|
+
}
|
|
318
|
+
// 2. Fallback to first # H1 heading in cleaned content
|
|
319
|
+
const h1Match = cleanedContent.match(/^#\s+(.+)$/m);
|
|
320
|
+
return h1Match?.[1]?.trim() ?? null;
|
|
265
321
|
}
|
|
266
322
|
/** Approximate reading time: ~200 words/min for technical Chinese/English prose. */
|
|
267
323
|
function estimateReadTime(text) {
|
|
@@ -339,116 +395,134 @@ async function displayWithLess(rendered, title, filePath, readTime) {
|
|
|
339
395
|
});
|
|
340
396
|
}
|
|
341
397
|
// ─── Directory browser ────────────────────────────────────────────────────────
|
|
342
|
-
async function browseDirectory(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
398
|
+
async function browseDirectory(initialPath = '') {
|
|
399
|
+
let currentPath = initialPath;
|
|
400
|
+
while (true) {
|
|
401
|
+
const trans = t();
|
|
402
|
+
let items;
|
|
403
|
+
try {
|
|
404
|
+
const s = createSpinner(currentPath ? `${trans.docs.loadingDir}: ${currentPath}` : trans.docs.loading);
|
|
405
|
+
const result = await fetchGitHubDirectory(currentPath);
|
|
406
|
+
items = result.data;
|
|
407
|
+
s.stop(currentPath || trans.docs.chooseDoc);
|
|
408
|
+
if (result.staleFallback) {
|
|
409
|
+
warning(trans.docs.usingCachedData);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
error(trans.docs.loadError);
|
|
414
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
415
|
+
console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
|
|
416
|
+
setVimKeysActive(false);
|
|
417
|
+
const retry = await confirm({ message: trans.docs.retry });
|
|
418
|
+
setVimKeysActive(true);
|
|
419
|
+
if (!isCancel(retry) && retry)
|
|
420
|
+
continue;
|
|
421
|
+
return;
|
|
351
422
|
}
|
|
352
423
|
if (items.length === 0) {
|
|
353
424
|
warning(trans.docs.emptyDir);
|
|
425
|
+
if (currentPath) {
|
|
426
|
+
currentPath = currentPath.split('/').slice(0, -1).join('/');
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
354
429
|
return;
|
|
355
430
|
}
|
|
356
431
|
const options = [
|
|
357
|
-
...(
|
|
432
|
+
...(currentPath ? [{ value: '__back__', label: chalk.dim(trans.docs.upToParent) }] : []),
|
|
358
433
|
...items.map(item => ({
|
|
359
434
|
value: item.path,
|
|
360
435
|
label: item.type === 'dir'
|
|
361
|
-
? chalk.cyan(`${
|
|
362
|
-
:
|
|
363
|
-
hint: item.type === 'dir' ? 'dir' :
|
|
436
|
+
? chalk.cyan(`${item.name}/`)
|
|
437
|
+
: item.name,
|
|
438
|
+
hint: item.type === 'dir' ? 'dir' : undefined,
|
|
364
439
|
})),
|
|
365
|
-
{ value: '__exit__', label: chalk.
|
|
440
|
+
{ value: '__exit__', label: chalk.dim(trans.docs.returnToMenu) },
|
|
366
441
|
];
|
|
367
442
|
const selected = await select({
|
|
368
|
-
message:
|
|
443
|
+
message: currentPath ? `${trans.docs.currentDir}: ${currentPath}` : trans.docs.chooseDoc,
|
|
369
444
|
options,
|
|
370
445
|
});
|
|
371
446
|
if (isCancel(selected) || selected === '__exit__')
|
|
372
447
|
return;
|
|
373
448
|
if (selected === '__back__') {
|
|
374
|
-
|
|
375
|
-
|
|
449
|
+
currentPath = currentPath.split('/').slice(0, -1).join('/');
|
|
450
|
+
continue;
|
|
376
451
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
else if (item?.type === 'file') {
|
|
383
|
-
await viewMarkdownFile(selected);
|
|
384
|
-
await browseDirectory(dirPath);
|
|
385
|
-
}
|
|
452
|
+
const item = items.find(i => i.path === selected);
|
|
453
|
+
if (item?.type === 'dir') {
|
|
454
|
+
currentPath = selected;
|
|
455
|
+
continue;
|
|
386
456
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
error(trans.docs.loadError);
|
|
390
|
-
console.log(chalk.gray(` ${trans.docs.errorHint}: ${err.message}`));
|
|
391
|
-
const retry = await confirm({ message: trans.docs.retry });
|
|
392
|
-
if (!isCancel(retry) && retry) {
|
|
393
|
-
await browseDirectory(dirPath);
|
|
457
|
+
if (item?.type === 'file') {
|
|
458
|
+
await viewMarkdownFile(selected);
|
|
394
459
|
}
|
|
395
460
|
}
|
|
396
461
|
}
|
|
397
462
|
// ─── Document viewer ──────────────────────────────────────────────────────────
|
|
398
463
|
async function viewMarkdownFile(filePath) {
|
|
399
464
|
const trans = t();
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
renderedDoc
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
465
|
+
while (true) {
|
|
466
|
+
try {
|
|
467
|
+
ensureMarkedConfigured();
|
|
468
|
+
const s = createSpinner(`${trans.docs.loading.replace('...', '')}: ${filePath}`);
|
|
469
|
+
const rawResult = await fetchGitHubRawContent(filePath);
|
|
470
|
+
if (rawResult.staleFallback) {
|
|
471
|
+
warning(trans.docs.usingCachedData);
|
|
472
|
+
}
|
|
473
|
+
const rawContent = rawResult.data;
|
|
474
|
+
const fingerprint = contentFingerprint(rawContent);
|
|
475
|
+
const cachedRendered = getFreshCacheValue(renderCache, filePath);
|
|
476
|
+
let renderedDoc;
|
|
477
|
+
if (cachedRendered && cachedRendered.fingerprint === fingerprint) {
|
|
478
|
+
renderedDoc = cachedRendered;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
const cleaned = cleanMarkdownContent(rawContent, getTerminalType());
|
|
482
|
+
const title = extractDocTitle(rawContent, cleaned) || filePath.split('/').pop() || filePath;
|
|
483
|
+
const readTime = estimateReadTime(cleaned);
|
|
484
|
+
const rendered = await marked(cleaned);
|
|
485
|
+
renderedDoc = { fingerprint, cleaned, rendered, title, readTime };
|
|
486
|
+
setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS);
|
|
487
|
+
evictStalest(renderCache, RENDER_CACHE_MAX);
|
|
488
|
+
}
|
|
489
|
+
s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`);
|
|
490
|
+
if (hasGlow()) {
|
|
491
|
+
await displayWithGlow(renderedDoc.cleaned);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime);
|
|
495
|
+
}
|
|
496
|
+
console.log();
|
|
497
|
+
success(trans.docs.docCompleted);
|
|
498
|
+
console.log();
|
|
499
|
+
const action = await select({
|
|
500
|
+
message: trans.docs.chooseAction,
|
|
501
|
+
options: [
|
|
502
|
+
{ value: 'back', label: trans.docs.backToList },
|
|
503
|
+
{ value: 'reread', label: trans.docs.reread },
|
|
504
|
+
{ value: 'browser', label: trans.docs.openBrowser },
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
if (isCancel(action) || action === 'back')
|
|
508
|
+
return;
|
|
509
|
+
if (action === 'browser') {
|
|
510
|
+
await openDocsInBrowser(filePath);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// action === 'reread' → continue loop
|
|
427
514
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
439
|
-
if (isCancel(action))
|
|
515
|
+
catch (err) {
|
|
516
|
+
error(trans.docs.loadError);
|
|
517
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
518
|
+
console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
|
|
519
|
+
setVimKeysActive(false);
|
|
520
|
+
const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt });
|
|
521
|
+
setVimKeysActive(true);
|
|
522
|
+
if (!isCancel(openBrowser) && openBrowser) {
|
|
523
|
+
await openDocsInBrowser(filePath);
|
|
524
|
+
}
|
|
440
525
|
return;
|
|
441
|
-
if (action === 'browser')
|
|
442
|
-
await openDocsInBrowser(filePath);
|
|
443
|
-
if (action === 'reread')
|
|
444
|
-
await viewMarkdownFile(filePath);
|
|
445
|
-
}
|
|
446
|
-
catch (err) {
|
|
447
|
-
error(trans.docs.loadError);
|
|
448
|
-
console.log(chalk.gray(` ${trans.docs.errorHint}: ${err.message}`));
|
|
449
|
-
const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt });
|
|
450
|
-
if (!isCancel(openBrowser) && openBrowser) {
|
|
451
|
-
await openDocsInBrowser(filePath);
|
|
452
526
|
}
|
|
453
527
|
}
|
|
454
528
|
}
|
|
@@ -469,6 +543,71 @@ export async function openDocsInBrowser(path) {
|
|
|
469
543
|
}
|
|
470
544
|
console.log();
|
|
471
545
|
}
|
|
546
|
+
// ─── Search ────────────────────────────────────────────────────────────────────
|
|
547
|
+
async function searchDocs() {
|
|
548
|
+
const trans = t();
|
|
549
|
+
setVimKeysActive(false);
|
|
550
|
+
const query = await text({
|
|
551
|
+
message: trans.docs.searchPrompt,
|
|
552
|
+
placeholder: trans.docs.searchPlaceholder,
|
|
553
|
+
});
|
|
554
|
+
setVimKeysActive(true);
|
|
555
|
+
if (isCancel(query) || !query.trim())
|
|
556
|
+
return;
|
|
557
|
+
const keyword = query.trim().toLowerCase();
|
|
558
|
+
const s = createSpinner(trans.docs.searching);
|
|
559
|
+
// Fetch all category directories in parallel
|
|
560
|
+
const categories = getDocCategories().filter(c => c.path !== 'README.md');
|
|
561
|
+
const results = [];
|
|
562
|
+
try {
|
|
563
|
+
const fetches = await Promise.allSettled(categories.map(async (cat) => {
|
|
564
|
+
const res = await fetchGitHubDirectory(cat.path);
|
|
565
|
+
return { items: res.data, category: cat.name };
|
|
566
|
+
}));
|
|
567
|
+
for (const result of fetches) {
|
|
568
|
+
if (result.status !== 'fulfilled')
|
|
569
|
+
continue;
|
|
570
|
+
for (const item of result.value.items) {
|
|
571
|
+
const nameLC = item.name.toLowerCase();
|
|
572
|
+
if (nameLC.includes(keyword)) {
|
|
573
|
+
results.push({ name: item.name, path: item.path, category: result.value.category });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Also search already-cached file content
|
|
578
|
+
for (const [cachedPath, entry] of fileCache) {
|
|
579
|
+
if (results.some(r => r.path === cachedPath))
|
|
580
|
+
continue;
|
|
581
|
+
if (entry.value.toLowerCase().includes(keyword)) {
|
|
582
|
+
const name = cachedPath.split('/').pop() || cachedPath;
|
|
583
|
+
results.push({ name, path: cachedPath, category: trans.docs.searchResults });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
s.stop(`${results.length} ${trans.docs.searchResults}`);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
s.error(trans.docs.loadError);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (results.length === 0) {
|
|
593
|
+
warning(trans.docs.searchNoResults);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const selected = await select({
|
|
597
|
+
message: trans.docs.chooseDoc,
|
|
598
|
+
options: [
|
|
599
|
+
...results.map(r => ({
|
|
600
|
+
value: r.path,
|
|
601
|
+
label: r.name,
|
|
602
|
+
hint: r.category,
|
|
603
|
+
})),
|
|
604
|
+
{ value: '__back__', label: chalk.dim(trans.docs.returnToMenu) },
|
|
605
|
+
],
|
|
606
|
+
});
|
|
607
|
+
if (isCancel(selected) || selected === '__back__')
|
|
608
|
+
return;
|
|
609
|
+
await viewMarkdownFile(selected);
|
|
610
|
+
}
|
|
472
611
|
// ─── Menu ─────────────────────────────────────────────────────────────────────
|
|
473
612
|
export async function showDocsMenu() {
|
|
474
613
|
while (true) {
|
|
@@ -476,9 +615,10 @@ export async function showDocsMenu() {
|
|
|
476
615
|
const categories = getDocCategories();
|
|
477
616
|
const options = [
|
|
478
617
|
...categories.map(cat => ({ value: cat.path, label: cat.name })),
|
|
479
|
-
{ value: '
|
|
480
|
-
{ value: '
|
|
481
|
-
{ value: '
|
|
618
|
+
{ value: 'search', label: chalk.dim(trans.docs.searchPrompt.replace(':', '')) },
|
|
619
|
+
{ value: 'refresh-cache', label: chalk.dim(trans.docs.refreshCache) },
|
|
620
|
+
{ value: 'browser', label: chalk.dim(trans.docs.openBrowser) },
|
|
621
|
+
{ value: 'back', label: chalk.dim(trans.docs.returnToMenu) },
|
|
482
622
|
];
|
|
483
623
|
const action = await select({
|
|
484
624
|
message: trans.docs.chooseCategory,
|
|
@@ -486,19 +626,18 @@ export async function showDocsMenu() {
|
|
|
486
626
|
});
|
|
487
627
|
if (isCancel(action) || action === 'back')
|
|
488
628
|
return;
|
|
489
|
-
if (action === '
|
|
629
|
+
if (action === 'search') {
|
|
630
|
+
await searchDocs();
|
|
631
|
+
}
|
|
632
|
+
else if (action === 'refresh-cache') {
|
|
490
633
|
clearDocsCache();
|
|
491
634
|
success(trans.docs.cacheCleared);
|
|
492
635
|
}
|
|
493
636
|
else if (action === 'browser') {
|
|
494
637
|
await openDocsInBrowser();
|
|
495
638
|
}
|
|
496
|
-
else if (action === 'README.md') {
|
|
497
|
-
await viewMarkdownFile('README.md');
|
|
498
|
-
}
|
|
499
639
|
else {
|
|
500
640
|
await browseDirectory(action);
|
|
501
641
|
}
|
|
502
642
|
}
|
|
503
643
|
}
|
|
504
|
-
//# sourceMappingURL=docs.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|