@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.
Files changed (83) hide show
  1. package/dist/config/data.js +5 -1
  2. package/dist/config/paths.js +5 -0
  3. package/dist/config/preferences.js +1 -8
  4. package/dist/core/icons.js +18 -5
  5. package/dist/core/logo.js +1 -2
  6. package/dist/core/menu.js +7 -122
  7. package/dist/core/text.js +9 -3
  8. package/dist/core/ui.js +0 -1
  9. package/dist/core/vim-keys.js +6 -5
  10. package/dist/features/calendar.js +50 -9
  11. package/dist/features/docs.js +128 -49
  12. package/dist/features/links.js +42 -0
  13. package/dist/features/settings.js +120 -0
  14. package/dist/features/status.js +19 -26
  15. package/dist/features/theme.js +9 -101
  16. package/dist/features/update.js +74 -0
  17. package/dist/i18n/index.js +7 -16
  18. package/dist/i18n/locales/en.json +43 -54
  19. package/dist/i18n/locales/zh.json +43 -54
  20. package/dist/index.js +107 -67
  21. package/dist/main.js +14 -5
  22. package/package.json +4 -1
  23. package/dist/config/data.d.ts +0 -23
  24. package/dist/config/data.d.ts.map +0 -1
  25. package/dist/config/data.js.map +0 -1
  26. package/dist/config/preferences.d.ts +0 -14
  27. package/dist/config/preferences.d.ts.map +0 -1
  28. package/dist/config/preferences.js.map +0 -1
  29. package/dist/config/theme.d.ts +0 -23
  30. package/dist/config/theme.d.ts.map +0 -1
  31. package/dist/config/theme.js +0 -25
  32. package/dist/config/theme.js.map +0 -1
  33. package/dist/core/icons.d.ts +0 -3
  34. package/dist/core/icons.d.ts.map +0 -1
  35. package/dist/core/icons.js.map +0 -1
  36. package/dist/core/logo.d.ts +0 -9
  37. package/dist/core/logo.d.ts.map +0 -1
  38. package/dist/core/logo.js.map +0 -1
  39. package/dist/core/menu.d.ts +0 -14
  40. package/dist/core/menu.d.ts.map +0 -1
  41. package/dist/core/menu.js.map +0 -1
  42. package/dist/core/text.d.ts +0 -7
  43. package/dist/core/text.d.ts.map +0 -1
  44. package/dist/core/text.js.map +0 -1
  45. package/dist/core/ui.d.ts +0 -38
  46. package/dist/core/ui.d.ts.map +0 -1
  47. package/dist/core/ui.js.map +0 -1
  48. package/dist/core/vim-keys.d.ts +0 -8
  49. package/dist/core/vim-keys.d.ts.map +0 -1
  50. package/dist/core/vim-keys.js.map +0 -1
  51. package/dist/features/calendar.d.ts +0 -29
  52. package/dist/features/calendar.d.ts.map +0 -1
  53. package/dist/features/calendar.js.map +0 -1
  54. package/dist/features/docs.d.ts +0 -8
  55. package/dist/features/docs.d.ts.map +0 -1
  56. package/dist/features/docs.js.map +0 -1
  57. package/dist/features/repair.d.ts +0 -10
  58. package/dist/features/repair.d.ts.map +0 -1
  59. package/dist/features/repair.js +0 -29
  60. package/dist/features/repair.js.map +0 -1
  61. package/dist/features/status.d.ts +0 -31
  62. package/dist/features/status.d.ts.map +0 -1
  63. package/dist/features/status.js.map +0 -1
  64. package/dist/features/theme.d.ts +0 -8
  65. package/dist/features/theme.d.ts.map +0 -1
  66. package/dist/features/theme.js.map +0 -1
  67. package/dist/features/website.d.ts +0 -30
  68. package/dist/features/website.d.ts.map +0 -1
  69. package/dist/features/website.js +0 -48
  70. package/dist/features/website.js.map +0 -1
  71. package/dist/i18n/index.d.ts +0 -209
  72. package/dist/i18n/index.d.ts.map +0 -1
  73. package/dist/i18n/index.js.map +0 -1
  74. package/dist/index.d.ts +0 -5
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js.map +0 -1
  77. package/dist/main.d.ts +0 -12
  78. package/dist/main.d.ts.map +0 -1
  79. package/dist/main.js.map +0 -1
  80. package/dist/types.d.ts +0 -48
  81. package/dist/types.d.ts.map +0 -1
  82. package/dist/types.js +0 -6
  83. package/dist/types.js.map +0 -1
@@ -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
- const TERMINAL_TYPE = detectTerminalType();
42
- const HAS_GLOW = commandExists('glow');
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: withIcon('📖', '[DOC]', trans.docs.categoryTutorial), path: 'tutorial' },
102
- { name: withIcon('🔧', '[LOG]', trans.docs.categoryRepairLogs), path: '维修日' },
103
- { name: withIcon('🎉', '[EVT]', trans.docs.categoryEvents), path: '相关活动举办' },
104
- { name: withIcon('📋', '[PROC]', trans.docs.categoryProcess), path: 'process' },
105
- { name: withIcon('🛠', '[FIX]', trans.docs.categoryRepair), path: 'repair' },
106
- { name: withIcon('📦', '[ARC]', trans.docs.categoryArchived), path: 'archived' },
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': 'NBTCA-CLI'
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
- if (err.response?.status === 403) {
180
- const rateLimitRemaining = err.response.headers['x-ratelimit-remaining'];
181
- const rateLimitReset = err.response.headers['x-ratelimit-reset'];
182
- if (rateLimitRemaining === '0') {
183
- const resetDate = new Date(parseInt(rateLimitReset) * 1000);
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': 'NBTCA-CLI' } });
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(content) {
263
- const match = content.match(/^#\s+(.+)$/m);
264
- return match?.[1]?.trim() ?? null;
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.gray(`${pickIcon('↑', '[..]')} ${trans.docs.upToParent}`) }] : []),
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(`${pickIcon('📁', '[DIR]')} ${item.name}/`)
362
- : chalk.white(`${pickIcon('📄', '[MD]')} ${item.name}`),
363
- hint: item.type === 'dir' ? 'dir' : '.md',
383
+ ? chalk.cyan(`${item.name}/`)
384
+ : item.name,
385
+ hint: item.type === 'dir' ? 'dir' : undefined,
364
386
  })),
365
- { value: '__exit__', label: chalk.gray(`${pickIcon('✕', '[x]')} ${trans.docs.returnToMenu}`) },
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
- console.log(chalk.gray(` ${trans.docs.errorHint}: ${err.message}`));
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, TERMINAL_TYPE);
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 (HAS_GLOW) {
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: `${pickIcon('←', '[<]')} ${trans.docs.backToList}` },
435
- { value: 'reread', label: `${pickIcon('↻', '[r]')} ${trans.docs.reread}` },
436
- { value: 'browser', label: `${pickIcon('🌐', '[*]')} ${trans.docs.openBrowser}` },
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
- console.log(chalk.gray(` ${trans.docs.errorHint}: ${err.message}`));
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: 'refresh-cache', label: chalk.gray(`${pickIcon('♻️', '[r]')} ${trans.docs.refreshCache}`) },
480
- { value: 'browser', label: chalk.gray(`${pickIcon('🌐', '[*]')} ${trans.docs.openBrowser}`) },
481
- { value: 'back', label: chalk.gray(`${pickIcon('←', '[^]')} ${trans.docs.returnToMenu}`) },
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 === 'refresh-cache') {
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
+ }
@@ -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, success, warning } from '../core/ui.js';
6
+ import { createSpinner } from '../core/ui.js';
7
7
  import { t } from '../i18n/index.js';
8
- const SERVICE_TARGETS = [
9
- { name: 'Website', url: URLS.homepage },
10
- { name: 'Docs', url: URLS.docs },
11
- { name: 'Calendar', url: URLS.calendar },
12
- { name: 'GitHub', url: URLS.github },
13
- { name: 'Roadmap', url: URLS.roadmap },
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': 'NBTCA-CLI/2.4.0' },
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(SERVICE_TARGETS.map((service) => checkServiceWithRetry(service.name, service.url, timeoutMs, retries)));
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(codeWidth + 2)}${topMid}${h.repeat(latencyWidth + 2)}${topMid}${h.repeat(34)}${topRight}`;
104
- const divider = `${midLeft}${h.repeat(nameWidth + 2)}${midMid}${h.repeat(statusWidth + 2)}${midMid}${h.repeat(codeWidth + 2)}${midMid}${h.repeat(latencyWidth + 2)}${midMid}${h.repeat(34)}${midRight}`;
105
- const bottom = `${bottomLeft}${h.repeat(nameWidth + 2)}${bottomMid}${h.repeat(statusWidth + 2)}${bottomMid}${h.repeat(codeWidth + 2)}${bottomMid}${h.repeat(latencyWidth + 2)}${bottomMid}${h.repeat(34)}${bottomRight}`;
106
- const header = `${v} ${padEndV(trans.status.service, nameWidth)} ${v} ${padEndV(trans.status.health, statusWidth)} ${v} ${padEndV(trans.status.code, codeWidth)} ${v} ${padEndV(trans.status.latency, latencyWidth)} ${v} ${padEndV(trans.status.url, 34)} ${v}`;
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
- const url = item.url.length > 34 ? `${item.url.slice(0, 31)}...` : item.url;
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