@nbtca/prompt 1.1.1 → 1.1.3

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.
Files changed (2) hide show
  1. package/dist/features/docs.js +31 -159
  2. package/package.json +2 -1
@@ -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 { APP_INFO, GITHUB_REPO, URLS } from '../config/data.js';
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 dirCache = new Map();
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 getFreshCacheValue(cache, key) {
124
- const entry = cache.get(key);
125
- if (!entry)
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 setCacheValue(cache, key, value, ttlMs) {
136
- cache.set(key, { value, expiresAt: Date.now() + ttlMs });
137
- }
138
- const DIR_CACHE_MAX = 30;
139
- const FILE_CACHE_MAX = 50;
140
- const RENDER_CACHE_MAX = 50;
141
- function evictStalest(cache, maxSize) {
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
- const head = content.slice(0, 80);
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
- dirCache.clear();
157
- fileCache.clear();
131
+ docsClient.clear();
158
132
  renderCache.clear();
159
133
  }
160
- async function fetchGitHubDirectory(path = '', options = {}) {
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
- const headers = {
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 errorMessage = err instanceof Error
222
- ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
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: errorMessage }));
143
+ throw new Error(fmt(trans.docs.fetchDirFailed, { error: msg }));
225
144
  }
226
145
  }
227
- async function fetchGitHubRawContent(path, options = {}) {
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
- const controller = new AbortController();
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
- const errorMessage = err instanceof Error
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
- const result = await fetchGitHubDirectory(currentPath);
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 rawResult = await fetchGitHubRawContent(filePath);
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 = getFreshCacheValue(renderCache, filePath);
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
- setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS);
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 res = await fetchGitHubDirectory(cat.path);
564
- return { items: res.data, category: cat.name };
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
- const nameLC = item.name.toLowerCase();
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.1",
3
+ "version": "1.1.3",
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.2",
42
43
  "@nbtca/nbtcal": "^0.2.1",
43
44
  "chalk": "^5.6.2",
44
45
  "gradient-string": "^3.0.0",