@nbtca/prompt 1.1.1 → 1.1.2
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/features/docs.js +31 -159
- package/package.json +2 -1
package/dist/features/docs.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 知识库终端查看模块
|
|
3
|
-
* 获取并渲染Markdown文档
|
|
4
|
-
*/
|
|
5
1
|
import { marked } from 'marked';
|
|
6
2
|
import { markedTerminal } from 'marked-terminal';
|
|
7
3
|
import chalk from 'chalk';
|
|
@@ -10,9 +6,10 @@ import { select, isCancel, confirm, text } from '@clack/prompts';
|
|
|
10
6
|
import { error, warning, success, createSpinner } from '../core/ui.js';
|
|
11
7
|
import { pickIcon } from '../core/icons.js';
|
|
12
8
|
import { spawn, execFileSync } from 'child_process';
|
|
13
|
-
import {
|
|
9
|
+
import { URLS } from '../config/data.js';
|
|
14
10
|
import { t, fmt } from '../i18n/index.js';
|
|
15
11
|
import { setVimKeysActive } from '../core/vim-keys.js';
|
|
12
|
+
import { createDocsClient, DocsFetchError } from '@nbtca/docs';
|
|
16
13
|
function detectTerminalType() {
|
|
17
14
|
const term = (process.env['TERM'] || '').toLowerCase();
|
|
18
15
|
const termProgram = (process.env['TERM_PROGRAM'] || '').toLowerCase();
|
|
@@ -100,15 +97,10 @@ function getRendererOptions(type) {
|
|
|
100
97
|
}
|
|
101
98
|
};
|
|
102
99
|
}
|
|
103
|
-
// ─── GitHub data layer ────────────────────────────────────────────────────────
|
|
104
|
-
const GITHUB_TOKEN = process.env['GITHUB_TOKEN'] || process.env['GH_TOKEN'];
|
|
105
|
-
const SKIP_NAMES = new Set(['node_modules', 'package.json', 'pnpm-lock.yaml']);
|
|
106
|
-
const DIR_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
107
|
-
const FILE_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
108
100
|
const RENDER_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
109
|
-
const
|
|
110
|
-
const fileCache = new Map();
|
|
101
|
+
const RENDER_CACHE_MAX = 50;
|
|
111
102
|
const renderCache = new Map();
|
|
103
|
+
let docsClient = createDocsClient();
|
|
112
104
|
function getDocCategories() {
|
|
113
105
|
const trans = t();
|
|
114
106
|
return [
|
|
@@ -120,143 +112,44 @@ function getDocCategories() {
|
|
|
120
112
|
{ name: trans.docs.categoryArchived, path: 'archived' },
|
|
121
113
|
];
|
|
122
114
|
}
|
|
123
|
-
function
|
|
124
|
-
const entry =
|
|
125
|
-
|
|
126
|
-
return null;
|
|
127
|
-
if (entry.expiresAt > Date.now())
|
|
128
|
-
return entry.value;
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
function getAnyCacheValue(cache, key) {
|
|
132
|
-
const entry = cache.get(key);
|
|
133
|
-
return entry?.value ?? null;
|
|
115
|
+
function getFreshRender(key) {
|
|
116
|
+
const entry = renderCache.get(key);
|
|
117
|
+
return entry && entry.expiresAt > Date.now() ? entry.value : null;
|
|
134
118
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (cache.size <= maxSize)
|
|
143
|
-
return;
|
|
144
|
-
const oldest = [...cache.entries()]
|
|
145
|
-
.sort((a, b) => a[1].expiresAt - b[1].expiresAt)
|
|
146
|
-
.slice(0, cache.size - maxSize);
|
|
147
|
-
for (const [key] of oldest)
|
|
148
|
-
cache.delete(key);
|
|
119
|
+
function setRender(key, value) {
|
|
120
|
+
renderCache.set(key, { value, expiresAt: Date.now() + RENDER_CACHE_TTL_MS });
|
|
121
|
+
if (renderCache.size > RENDER_CACHE_MAX) {
|
|
122
|
+
const oldest = [...renderCache.entries()].sort((a, b) => a[1].expiresAt - b[1].expiresAt)[0];
|
|
123
|
+
if (oldest)
|
|
124
|
+
renderCache.delete(oldest[0]);
|
|
125
|
+
}
|
|
149
126
|
}
|
|
150
127
|
function contentFingerprint(content) {
|
|
151
|
-
|
|
152
|
-
const tail = content.slice(-80);
|
|
153
|
-
return `${content.length}:${head}:${tail}`;
|
|
128
|
+
return `${content.length}:${content.slice(0, 80)}:${content.slice(-80)}`;
|
|
154
129
|
}
|
|
155
130
|
export function clearDocsCache() {
|
|
156
|
-
|
|
157
|
-
fileCache.clear();
|
|
131
|
+
docsClient = createDocsClient(); // fresh instance resets dir + file caches
|
|
158
132
|
renderCache.clear();
|
|
159
133
|
}
|
|
160
|
-
async function
|
|
161
|
-
const cacheKey = path || '__root__';
|
|
162
|
-
if (!options.forceRefresh) {
|
|
163
|
-
const cached = getFreshCacheValue(dirCache, cacheKey);
|
|
164
|
-
if (cached) {
|
|
165
|
-
return { data: cached, fromCache: true, staleFallback: false };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const url = `https://api.github.com/repos/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}/contents/${path}?ref=${GITHUB_REPO.branch}`;
|
|
134
|
+
async function fetchDirectory(path = '') {
|
|
169
135
|
try {
|
|
170
|
-
|
|
171
|
-
'Accept': 'application/vnd.github.v3+json',
|
|
172
|
-
'User-Agent': `NBTCA-CLI/${APP_INFO.version}`,
|
|
173
|
-
};
|
|
174
|
-
if (GITHUB_TOKEN)
|
|
175
|
-
headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`;
|
|
176
|
-
const controller = new AbortController();
|
|
177
|
-
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
178
|
-
const response = await fetch(url, { signal: controller.signal, headers });
|
|
179
|
-
clearTimeout(timeout);
|
|
180
|
-
if (!response.ok) {
|
|
181
|
-
const trans = t();
|
|
182
|
-
if (response.status === 403) {
|
|
183
|
-
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
|
|
184
|
-
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
185
|
-
if (rateLimitRemaining === '0' && rateLimitReset) {
|
|
186
|
-
const resetDate = new Date(Number.parseInt(rateLimitReset, 10) * 1000);
|
|
187
|
-
throw new Error(`${fmt(trans.docs.githubRateLimited, { time: resetDate.toLocaleTimeString() })}\n${trans.docs.githubTokenHint}`);
|
|
188
|
-
}
|
|
189
|
-
throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`);
|
|
190
|
-
}
|
|
191
|
-
throw new Error(`HTTP ${response.status}`);
|
|
192
|
-
}
|
|
193
|
-
const data = (await response.json());
|
|
194
|
-
const items = data
|
|
195
|
-
.filter((item) => !item.name.startsWith('.') &&
|
|
196
|
-
!SKIP_NAMES.has(item.name) &&
|
|
197
|
-
!(item.type === 'file' && !item.name.endsWith('.md')))
|
|
198
|
-
.map((item) => ({
|
|
199
|
-
name: item.name,
|
|
200
|
-
path: item.path,
|
|
201
|
-
type: (item.type === 'dir' ? 'dir' : 'file'),
|
|
202
|
-
sha: item.sha
|
|
203
|
-
}))
|
|
204
|
-
.sort((a, b) => {
|
|
205
|
-
if (a.type === 'dir' && b.type === 'file')
|
|
206
|
-
return -1;
|
|
207
|
-
if (a.type === 'file' && b.type === 'dir')
|
|
208
|
-
return 1;
|
|
209
|
-
return a.name.localeCompare(b.name);
|
|
210
|
-
});
|
|
211
|
-
setCacheValue(dirCache, cacheKey, items, DIR_CACHE_TTL_MS);
|
|
212
|
-
evictStalest(dirCache, DIR_CACHE_MAX);
|
|
213
|
-
return { data: items, fromCache: false, staleFallback: false };
|
|
136
|
+
return await docsClient.listDir(path);
|
|
214
137
|
}
|
|
215
138
|
catch (err) {
|
|
216
|
-
const staleCached = getAnyCacheValue(dirCache, cacheKey);
|
|
217
|
-
if (staleCached) {
|
|
218
|
-
return { data: staleCached, fromCache: true, staleFallback: true };
|
|
219
|
-
}
|
|
220
139
|
const trans = t();
|
|
221
|
-
const
|
|
222
|
-
? (err.
|
|
140
|
+
const msg = err instanceof DocsFetchError
|
|
141
|
+
? (err.status === 403 ? `${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}` : `HTTP ${err.status}`)
|
|
223
142
|
: String(err);
|
|
224
|
-
throw new Error(fmt(trans.docs.fetchDirFailed, { error:
|
|
143
|
+
throw new Error(fmt(trans.docs.fetchDirFailed, { error: msg }));
|
|
225
144
|
}
|
|
226
145
|
}
|
|
227
|
-
async function
|
|
228
|
-
if (!options.forceRefresh) {
|
|
229
|
-
const cached = getFreshCacheValue(fileCache, path);
|
|
230
|
-
if (cached) {
|
|
231
|
-
return { data: cached, fromCache: true, staleFallback: false };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
const url = `https://raw.githubusercontent.com/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}/${GITHUB_REPO.branch}/${path}`;
|
|
146
|
+
async function fetchFileContent(path) {
|
|
235
147
|
try {
|
|
236
|
-
|
|
237
|
-
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
238
|
-
const response = await fetch(url, {
|
|
239
|
-
signal: controller.signal,
|
|
240
|
-
headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
|
|
241
|
-
});
|
|
242
|
-
clearTimeout(timeout);
|
|
243
|
-
if (!response.ok)
|
|
244
|
-
throw new Error(`HTTP ${response.status}`);
|
|
245
|
-
const content = await response.text();
|
|
246
|
-
setCacheValue(fileCache, path, content, FILE_CACHE_TTL_MS);
|
|
247
|
-
evictStalest(fileCache, FILE_CACHE_MAX);
|
|
248
|
-
return { data: content, fromCache: false, staleFallback: false };
|
|
148
|
+
return await docsClient.getFile(path);
|
|
249
149
|
}
|
|
250
150
|
catch (err) {
|
|
251
|
-
const staleCached = getAnyCacheValue(fileCache, path);
|
|
252
|
-
if (staleCached) {
|
|
253
|
-
return { data: staleCached, fromCache: true, staleFallback: true };
|
|
254
|
-
}
|
|
255
151
|
const trans = t();
|
|
256
|
-
|
|
257
|
-
? (err.name === 'AbortError' ? 'Request timed out' : err.message)
|
|
258
|
-
: String(err);
|
|
259
|
-
throw new Error(fmt(trans.docs.fetchFileFailed, { error: errorMessage }));
|
|
152
|
+
throw new Error(fmt(trans.docs.fetchFileFailed, { error: String(err) }));
|
|
260
153
|
}
|
|
261
154
|
}
|
|
262
155
|
// ─── Content cleaning ─────────────────────────────────────────────────────────
|
|
@@ -401,12 +294,8 @@ async function browseDirectory(initialPath = '') {
|
|
|
401
294
|
let items;
|
|
402
295
|
try {
|
|
403
296
|
const s = createSpinner(currentPath ? `${trans.docs.loadingDir}: ${currentPath}` : trans.docs.loading);
|
|
404
|
-
|
|
405
|
-
items = result.data;
|
|
297
|
+
items = await fetchDirectory(currentPath);
|
|
406
298
|
s.stop(currentPath || trans.docs.chooseDoc);
|
|
407
|
-
if (result.staleFallback) {
|
|
408
|
-
warning(trans.docs.usingCachedData);
|
|
409
|
-
}
|
|
410
299
|
}
|
|
411
300
|
catch (err) {
|
|
412
301
|
error(trans.docs.loadError);
|
|
@@ -465,13 +354,9 @@ async function viewMarkdownFile(filePath) {
|
|
|
465
354
|
try {
|
|
466
355
|
ensureMarkedConfigured();
|
|
467
356
|
const s = createSpinner(`${trans.docs.loadingFile}: ${filePath}`);
|
|
468
|
-
const
|
|
469
|
-
if (rawResult.staleFallback) {
|
|
470
|
-
warning(trans.docs.usingCachedData);
|
|
471
|
-
}
|
|
472
|
-
const rawContent = rawResult.data;
|
|
357
|
+
const rawContent = await fetchFileContent(filePath);
|
|
473
358
|
const fingerprint = contentFingerprint(rawContent);
|
|
474
|
-
const cachedRendered =
|
|
359
|
+
const cachedRendered = getFreshRender(filePath);
|
|
475
360
|
let renderedDoc;
|
|
476
361
|
if (cachedRendered && cachedRendered.fingerprint === fingerprint) {
|
|
477
362
|
renderedDoc = cachedRendered;
|
|
@@ -482,8 +367,7 @@ async function viewMarkdownFile(filePath) {
|
|
|
482
367
|
const readTime = estimateReadTime(cleaned);
|
|
483
368
|
const rendered = await marked(cleaned);
|
|
484
369
|
renderedDoc = { fingerprint, cleaned, rendered, title, readTime };
|
|
485
|
-
|
|
486
|
-
evictStalest(renderCache, RENDER_CACHE_MAX);
|
|
370
|
+
setRender(filePath, renderedDoc);
|
|
487
371
|
}
|
|
488
372
|
s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`);
|
|
489
373
|
if (hasGlow()) {
|
|
@@ -560,30 +444,18 @@ async function searchDocs() {
|
|
|
560
444
|
const results = [];
|
|
561
445
|
try {
|
|
562
446
|
const fetches = await Promise.allSettled(categories.map(async (cat) => {
|
|
563
|
-
const
|
|
564
|
-
return { items
|
|
447
|
+
const items = await fetchDirectory(cat.path);
|
|
448
|
+
return { items, category: cat.name };
|
|
565
449
|
}));
|
|
566
450
|
for (const result of fetches) {
|
|
567
451
|
if (result.status !== 'fulfilled')
|
|
568
452
|
continue;
|
|
569
453
|
for (const item of result.value.items) {
|
|
570
|
-
|
|
571
|
-
if (nameLC.includes(keyword)) {
|
|
454
|
+
if (item.name.toLowerCase().includes(keyword)) {
|
|
572
455
|
results.push({ name: item.name, path: item.path, category: result.value.category });
|
|
573
456
|
}
|
|
574
457
|
}
|
|
575
458
|
}
|
|
576
|
-
// Also search already-cached file content
|
|
577
|
-
for (const [cachedPath, entry] of fileCache) {
|
|
578
|
-
if (results.some(r => r.path === cachedPath))
|
|
579
|
-
continue;
|
|
580
|
-
if (entry.value.toLowerCase().includes(keyword)) {
|
|
581
|
-
const name = cachedPath.split('/').pop() || cachedPath;
|
|
582
|
-
const parentDir = cachedPath.split('/').slice(0, -1).join('/');
|
|
583
|
-
const matchedCat = categories.find(c => parentDir.startsWith(c.path));
|
|
584
|
-
results.push({ name, path: cachedPath, category: matchedCat?.name ?? parentDir });
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
459
|
s.stop(`${results.length} ${trans.docs.searchResults}`);
|
|
588
460
|
}
|
|
589
461
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nbtca/prompt",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@clack/prompts": "^1.2.0",
|
|
42
|
+
"@nbtca/docs": "^0.1.0",
|
|
42
43
|
"@nbtca/nbtcal": "^0.2.1",
|
|
43
44
|
"chalk": "^5.6.2",
|
|
44
45
|
"gradient-string": "^3.0.0",
|