@nbtca/prompt 1.0.24 → 1.0.26

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.
@@ -35,7 +35,6 @@ export const APP_INFO = {
35
35
  name: 'Prompt',
36
36
  version: readPackageVersion(),
37
37
  description: '浙大宁波理工学院计算机协会',
38
- fullDescription: 'NBTCA Prompt - 极简命令行工具',
39
38
  author: 'm1ngsama <contact@m1ng.space>',
40
39
  license: 'MIT',
41
40
  repository: 'https://github.com/nbtca/prompt'
@@ -1,5 +1,24 @@
1
- import path from 'path';
1
+ import { existsSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ function getXdgConfigDir() {
5
+ const xdgHome = process.env['XDG_CONFIG_HOME'] || join(homedir(), '.config');
6
+ return join(xdgHome, 'nbtca');
7
+ }
8
+ function getLegacyConfigDir() {
9
+ return join(homedir(), '.nbtca');
10
+ }
2
11
  export function getConfigDir() {
3
- const homeDir = process.env['HOME'] || process.env['USERPROFILE'] || '';
4
- return path.join(homeDir, '.nbtca');
12
+ const xdgDir = getXdgConfigDir();
13
+ if (existsSync(xdgDir))
14
+ return xdgDir;
15
+ const legacyDir = getLegacyConfigDir();
16
+ if (existsSync(legacyDir))
17
+ return legacyDir;
18
+ return xdgDir;
19
+ }
20
+ export function getWritableConfigDir() {
21
+ const dir = getXdgConfigDir();
22
+ mkdirSync(dir, { recursive: true });
23
+ return dir;
5
24
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { getConfigDir } from './paths.js';
3
+ import { getConfigDir, getWritableConfigDir } from './paths.js';
4
4
  const DEFAULT_PREFERENCES = {
5
5
  iconMode: 'auto',
6
6
  colorMode: 'auto',
@@ -8,11 +8,8 @@ const DEFAULT_PREFERENCES = {
8
8
  function getPreferencesPath() {
9
9
  return path.join(getConfigDir(), 'preferences.json');
10
10
  }
11
- function ensureConfigDir() {
12
- const configDir = getConfigDir();
13
- if (!fs.existsSync(configDir)) {
14
- fs.mkdirSync(configDir, { recursive: true });
15
- }
11
+ function getWritablePreferencesPath() {
12
+ return path.join(getWritableConfigDir(), 'preferences.json');
16
13
  }
17
14
  export function loadPreferences() {
18
15
  try {
@@ -32,8 +29,7 @@ export function loadPreferences() {
32
29
  }
33
30
  function savePreferences(preferences) {
34
31
  try {
35
- ensureConfigDir();
36
- fs.writeFileSync(getPreferencesPath(), JSON.stringify(preferences, null, 2));
32
+ fs.writeFileSync(getWritablePreferencesPath(), JSON.stringify(preferences, null, 2));
37
33
  return true;
38
34
  }
39
35
  catch {
package/dist/core/text.js CHANGED
@@ -1,17 +1,20 @@
1
1
  /** Width of a single Unicode character: 2 for CJK/fullwidth, 1 otherwise. */
2
2
  function charWidth(ch) {
3
3
  const cp = ch.codePointAt(0) ?? 0;
4
- return ((cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo
5
- (cp >= 0x2E80 && cp <= 0x303F) || // CJK Radicals / Kangxi
6
- (cp >= 0x3040 && cp <= 0x33FF) || // Japanese kana + CJK symbols
7
- (cp >= 0x3400 && cp <= 0x4DBF) || // CJK Extension A
8
- (cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified Ideographs
9
- (cp >= 0xAC00 && cp <= 0xD7AF) || // Hangul Syllables
10
- (cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compatibility Ideographs
11
- (cp >= 0xFE30 && cp <= 0xFE4F) || // CJK Compatibility Forms
12
- (cp >= 0xFF00 && cp <= 0xFF60) || // Fullwidth Forms
13
- (cp >= 0xFFE0 && cp <= 0xFFE6) // Fullwidth Signs
14
- ) ? 2 : 1;
4
+ return ((cp >= 0x1100 && cp <= 0x115F) ||
5
+ (cp >= 0x2E80 && cp <= 0x303F) ||
6
+ (cp >= 0x3040 && cp <= 0x33FF) ||
7
+ (cp >= 0x3400 && cp <= 0x4DBF) ||
8
+ (cp >= 0x4E00 && cp <= 0x9FFF) ||
9
+ (cp >= 0xAC00 && cp <= 0xD7AF) ||
10
+ (cp >= 0xF900 && cp <= 0xFAFF) ||
11
+ (cp >= 0xFE30 && cp <= 0xFE4F) ||
12
+ (cp >= 0xFF00 && cp <= 0xFF60) ||
13
+ (cp >= 0xFFE0 && cp <= 0xFFE6) ||
14
+ (cp >= 0x20000 && cp <= 0x2A6DF) ||
15
+ (cp >= 0x2A700 && cp <= 0x2CEAF) ||
16
+ (cp >= 0x2CEB0 && cp <= 0x2EBEF) ||
17
+ (cp >= 0x30000 && cp <= 0x323AF)) ? 2 : 1;
15
18
  }
16
19
  /** Strip ANSI escape sequences from a string. */
17
20
  // eslint-disable-next-line no-control-regex
package/dist/core/ui.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { log, spinner as clackSpinner } from '@clack/prompts';
6
6
  import chalk from 'chalk';
7
7
  import { pickIcon } from './icons.js';
8
+ import { t } from '../i18n/index.js';
8
9
  /**
9
10
  * Display success message
10
11
  */
@@ -62,3 +63,18 @@ export function createSpinner(msg) {
62
63
  s.start(msg);
63
64
  return s;
64
65
  }
66
+ export function handleGracefulExit(err) {
67
+ const message = err instanceof Error ? err.message : String(err ?? '');
68
+ if (message.includes('SIGINT') || message.includes('User force closed')) {
69
+ console.log();
70
+ console.log(chalk.dim(t().common.goodbye));
71
+ process.exit(0);
72
+ }
73
+ if (message) {
74
+ console.error(message);
75
+ }
76
+ else {
77
+ console.error('Error occurred:', err);
78
+ }
79
+ process.exit(1);
80
+ }
@@ -6,7 +6,6 @@
6
6
  */
7
7
  // Maps single-byte vim keys to terminal escape sequences (ranger-style hjkl)
8
8
  const VIM_TO_SEQ = {
9
- h: Buffer.from('\u0003'), // back/cancel (ranger: go to parent)
10
9
  j: Buffer.from('\u001b[B'), // down arrow
11
10
  k: Buffer.from('\u001b[A'), // up arrow
12
11
  l: Buffer.from('\r'), // enter/confirm (ranger: open/enter)
@@ -14,13 +13,17 @@ const VIM_TO_SEQ = {
14
13
  G: Buffer.from('\u001b[F'), // end (last item)
15
14
  q: Buffer.from('\u0003'), // quit
16
15
  };
16
+ let vimActive = true;
17
+ export function setVimKeysActive(active) {
18
+ vimActive = active;
19
+ }
17
20
  export function enableVimKeys() {
18
21
  const stdin = process.stdin;
19
22
  if (!stdin.isTTY)
20
23
  return;
21
24
  const originalEmit = stdin.emit.bind(stdin);
22
25
  stdin.emit = function (event, ...args) {
23
- if (event === 'data') {
26
+ if (event === 'data' && vimActive) {
24
27
  const chunk = args[0];
25
28
  if (Buffer.isBuffer(chunk) && chunk.length === 1) {
26
29
  const seq = VIM_TO_SEQ[String.fromCharCode(chunk[0])];
@@ -2,7 +2,6 @@
2
2
  * ICS calendar module
3
3
  * Fetches and renders upcoming events with Unicode box table.
4
4
  */
5
- import axios from 'axios';
6
5
  import ICAL from 'ical.js';
7
6
  import chalk from 'chalk';
8
7
  import { select, isCancel } from '@clack/prompts';
@@ -13,13 +12,17 @@ import { t } from '../i18n/index.js';
13
12
  import { APP_INFO, URLS } from '../config/data.js';
14
13
  export async function fetchEvents() {
15
14
  try {
16
- const response = await axios.get('https://ical.nbtca.space', {
17
- timeout: 5000,
18
- headers: {
19
- 'User-Agent': `NBTCA-CLI/${APP_INFO.version}`
20
- }
15
+ const controller = new AbortController();
16
+ const timeout = setTimeout(() => controller.abort(), 5000);
17
+ const response = await fetch('https://ical.nbtca.space', {
18
+ signal: controller.signal,
19
+ headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
21
20
  });
22
- const jcalData = ICAL.parse(response.data);
21
+ clearTimeout(timeout);
22
+ if (!response.ok)
23
+ throw new Error(`HTTP ${response.status}`);
24
+ const data = await response.text();
25
+ const jcalData = ICAL.parse(data);
23
26
  const comp = new ICAL.Component(jcalData);
24
27
  const vevents = comp.getAllSubcomponents('vevent');
25
28
  const events = [];
@@ -46,7 +49,9 @@ export async function fetchEvents() {
46
49
  return events;
47
50
  }
48
51
  catch (err) {
49
- const detail = err instanceof Error ? err.message : String(err);
52
+ const detail = err instanceof Error
53
+ ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
54
+ : String(err);
50
55
  throw new Error(`${t().calendar.error}: ${detail}`);
51
56
  }
52
57
  }
@@ -61,8 +66,12 @@ export function serializeEvents(events) {
61
66
  }));
62
67
  }
63
68
  function formatDate(date) {
69
+ const now = new Date();
64
70
  const month = String(date.getMonth() + 1).padStart(2, '0');
65
71
  const day = String(date.getDate()).padStart(2, '0');
72
+ if (date.getFullYear() !== now.getFullYear()) {
73
+ return `${date.getFullYear()}-${month}-${day}`;
74
+ }
66
75
  return `${month}-${day}`;
67
76
  }
68
77
  function formatTime(date) {
@@ -78,7 +87,7 @@ export function renderEventsTable(events, options) {
78
87
  const color = options?.color !== false;
79
88
  if (events.length === 0)
80
89
  return trans.calendar.noEvents;
81
- const dateWidth = 13;
90
+ const dateWidth = 16;
82
91
  const titleWidth = 30;
83
92
  const locationWidth = 16;
84
93
  const h = pickIcon('─', '-');
@@ -2,7 +2,6 @@
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';
@@ -12,7 +11,8 @@ 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
13
  import { APP_INFO, GITHUB_REPO, URLS } from '../config/data.js';
15
- import { t } from '../i18n/index.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();
@@ -31,7 +31,8 @@ function detectTerminalType() {
31
31
  /** Check whether an external command exists on PATH (once at startup). */
32
32
  function commandExists(cmd) {
33
33
  try {
34
- execFileSync('which', [cmd], { stdio: 'ignore' });
34
+ const check = process.platform === 'win32' ? 'where' : 'which';
35
+ execFileSync(check, [cmd], { stdio: 'ignore' });
35
36
  return true;
36
37
  }
37
38
  catch {
@@ -55,8 +56,9 @@ function ensureMarkedConfigured() {
55
56
  if (_markedConfigured)
56
57
  return;
57
58
  _markedConfigured = true;
58
- // @ts-ignore - marked v11 / marked-terminal v7 type incompatibility
59
- marked.setOptions({ renderer: new TerminalRenderer(getRendererOptions(getTerminalType())) });
59
+ marked.use({
60
+ renderer: new TerminalRenderer(getRendererOptions(getTerminalType()))
61
+ });
60
62
  }
61
63
  // ─── marked-terminal renderer ─────────────────────────────────────────────────
62
64
  function getRendererOptions(type) {
@@ -135,6 +137,18 @@ function getAnyCacheValue(cache, key) {
135
137
  function setCacheValue(cache, key, value, ttlMs) {
136
138
  cache.set(key, { value, expiresAt: Date.now() + ttlMs });
137
139
  }
140
+ const DIR_CACHE_MAX = 30;
141
+ const FILE_CACHE_MAX = 50;
142
+ const RENDER_CACHE_MAX = 50;
143
+ function evictStalest(cache, maxSize) {
144
+ if (cache.size <= maxSize)
145
+ return;
146
+ const oldest = [...cache.entries()]
147
+ .sort((a, b) => a[1].expiresAt - b[1].expiresAt)
148
+ .slice(0, cache.size - maxSize);
149
+ for (const [key] of oldest)
150
+ cache.delete(key);
151
+ }
138
152
  function contentFingerprint(content) {
139
153
  const head = content.slice(0, 80);
140
154
  const tail = content.slice(-80);
@@ -157,12 +171,29 @@ async function fetchGitHubDirectory(path = '', options = {}) {
157
171
  try {
158
172
  const headers = {
159
173
  'Accept': 'application/vnd.github.v3+json',
160
- 'User-Agent': `NBTCA-CLI/${APP_INFO.version}`
174
+ 'User-Agent': `NBTCA-CLI/${APP_INFO.version}`,
161
175
  };
162
176
  if (GITHUB_TOKEN)
163
177
  headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`;
164
- const response = await axios.get(url, { timeout: 10000, headers });
165
- const items = response.data
178
+ const controller = new AbortController();
179
+ const timeout = setTimeout(() => controller.abort(), 10000);
180
+ const response = await fetch(url, { signal: controller.signal, headers });
181
+ clearTimeout(timeout);
182
+ if (!response.ok) {
183
+ const trans = t();
184
+ if (response.status === 403) {
185
+ const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
186
+ const rateLimitReset = response.headers.get('x-ratelimit-reset');
187
+ if (rateLimitRemaining === '0' && rateLimitReset) {
188
+ const resetDate = new Date(Number.parseInt(rateLimitReset, 10) * 1000);
189
+ throw new Error(`${fmt(trans.docs.githubRateLimited, { time: resetDate.toLocaleTimeString() })}\n${trans.docs.githubTokenHint}`);
190
+ }
191
+ throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`);
192
+ }
193
+ throw new Error(`HTTP ${response.status}`);
194
+ }
195
+ const data = (await response.json());
196
+ const items = data
166
197
  .filter((item) => !item.name.startsWith('.') &&
167
198
  !SKIP_NAMES.has(item.name) &&
168
199
  !(item.type === 'file' && !item.name.endsWith('.md')))
@@ -180,6 +211,7 @@ async function fetchGitHubDirectory(path = '', options = {}) {
180
211
  return a.name.localeCompare(b.name);
181
212
  });
182
213
  setCacheValue(dirCache, cacheKey, items, DIR_CACHE_TTL_MS);
214
+ evictStalest(dirCache, DIR_CACHE_MAX);
183
215
  return { data: items, fromCache: false, staleFallback: false };
184
216
  }
185
217
  catch (err) {
@@ -188,18 +220,10 @@ async function fetchGitHubDirectory(path = '', options = {}) {
188
220
  return { data: staleCached, fromCache: true, staleFallback: true };
189
221
  }
190
222
  const trans = t();
191
- const errorMessage = err instanceof Error ? err.message : String(err);
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);
198
- throw new Error(`${trans.docs.githubRateLimited.replace('{time}', resetDate.toLocaleTimeString())}\n${trans.docs.githubTokenHint}`);
199
- }
200
- throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`);
201
- }
202
- throw new Error(trans.docs.fetchDirFailed.replace('{error}', errorMessage));
223
+ const errorMessage = err instanceof Error
224
+ ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
225
+ : String(err);
226
+ throw new Error(fmt(trans.docs.fetchDirFailed, { error: errorMessage }));
203
227
  }
204
228
  }
205
229
  async function fetchGitHubRawContent(path, options = {}) {
@@ -211,9 +235,18 @@ async function fetchGitHubRawContent(path, options = {}) {
211
235
  }
212
236
  const url = `https://raw.githubusercontent.com/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}/${GITHUB_REPO.branch}/${path}`;
213
237
  try {
214
- const response = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` } });
215
- const content = String(response.data);
238
+ const controller = new AbortController();
239
+ const timeout = setTimeout(() => controller.abort(), 15000);
240
+ const response = await fetch(url, {
241
+ signal: controller.signal,
242
+ headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
243
+ });
244
+ clearTimeout(timeout);
245
+ if (!response.ok)
246
+ throw new Error(`HTTP ${response.status}`);
247
+ const content = await response.text();
216
248
  setCacheValue(fileCache, path, content, FILE_CACHE_TTL_MS);
249
+ evictStalest(fileCache, FILE_CACHE_MAX);
217
250
  return { data: content, fromCache: false, staleFallback: false };
218
251
  }
219
252
  catch (err) {
@@ -222,8 +255,10 @@ async function fetchGitHubRawContent(path, options = {}) {
222
255
  return { data: staleCached, fromCache: true, staleFallback: true };
223
256
  }
224
257
  const trans = t();
225
- const errorMessage = err instanceof Error ? err.message : String(err);
226
- throw new Error(trans.docs.fetchFileFailed.replace('{error}', errorMessage));
258
+ const errorMessage = err instanceof Error
259
+ ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
260
+ : String(err);
261
+ throw new Error(fmt(trans.docs.fetchFileFailed, { error: errorMessage }));
227
262
  }
228
263
  }
229
264
  // ─── Content cleaning ─────────────────────────────────────────────────────────
@@ -361,22 +396,41 @@ async function displayWithLess(rendered, title, filePath, readTime) {
361
396
  });
362
397
  }
363
398
  // ─── Directory browser ────────────────────────────────────────────────────────
364
- async function browseDirectory(dirPath = '') {
365
- const trans = t();
366
- try {
367
- const s = createSpinner(dirPath ? `${trans.docs.loadingDir}: ${dirPath}` : trans.docs.loading);
368
- const result = await fetchGitHubDirectory(dirPath);
369
- const items = result.data;
370
- s.stop(dirPath || trans.docs.chooseDoc);
371
- if (result.staleFallback) {
372
- warning(trans.docs.usingCachedData);
399
+ async function browseDirectory(initialPath = '') {
400
+ let currentPath = initialPath;
401
+ while (true) {
402
+ const trans = t();
403
+ let items;
404
+ try {
405
+ const s = createSpinner(currentPath ? `${trans.docs.loadingDir}: ${currentPath}` : trans.docs.loading);
406
+ const result = await fetchGitHubDirectory(currentPath);
407
+ items = result.data;
408
+ s.stop(currentPath || trans.docs.chooseDoc);
409
+ if (result.staleFallback) {
410
+ warning(trans.docs.usingCachedData);
411
+ }
412
+ }
413
+ catch (err) {
414
+ error(trans.docs.loadError);
415
+ const errMsg = err instanceof Error ? err.message : String(err);
416
+ console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
417
+ setVimKeysActive(false);
418
+ const retry = await confirm({ message: trans.docs.retry });
419
+ setVimKeysActive(true);
420
+ if (!isCancel(retry) && retry)
421
+ continue;
422
+ return;
373
423
  }
374
424
  if (items.length === 0) {
375
425
  warning(trans.docs.emptyDir);
426
+ if (currentPath) {
427
+ currentPath = currentPath.split('/').slice(0, -1).join('/');
428
+ continue;
429
+ }
376
430
  return;
377
431
  }
378
432
  const options = [
379
- ...(dirPath ? [{ value: '__back__', label: chalk.dim(trans.docs.upToParent) }] : []),
433
+ ...(currentPath ? [{ value: '__back__', label: chalk.dim(trans.docs.upToParent) }] : []),
380
434
  ...items.map(item => ({
381
435
  value: item.path,
382
436
  label: item.type === 'dir'
@@ -387,93 +441,89 @@ async function browseDirectory(dirPath = '') {
387
441
  { value: '__exit__', label: chalk.dim(trans.docs.returnToMenu) },
388
442
  ];
389
443
  const selected = await select({
390
- message: dirPath ? `${trans.docs.currentDir}: ${dirPath}` : trans.docs.chooseDoc,
444
+ message: currentPath ? `${trans.docs.currentDir}: ${currentPath}` : trans.docs.chooseDoc,
391
445
  options,
392
446
  });
393
447
  if (isCancel(selected) || selected === '__exit__')
394
448
  return;
395
449
  if (selected === '__back__') {
396
- const parentPath = dirPath.split('/').slice(0, -1).join('/');
397
- await browseDirectory(parentPath);
450
+ currentPath = currentPath.split('/').slice(0, -1).join('/');
451
+ continue;
398
452
  }
399
- else {
400
- const item = items.find(i => i.path === selected);
401
- if (item?.type === 'dir') {
402
- await browseDirectory(selected);
403
- }
404
- else if (item?.type === 'file') {
405
- await viewMarkdownFile(selected);
406
- await browseDirectory(dirPath);
407
- }
453
+ const item = items.find(i => i.path === selected);
454
+ if (item?.type === 'dir') {
455
+ currentPath = selected;
456
+ continue;
408
457
  }
409
- }
410
- catch (err) {
411
- error(trans.docs.loadError);
412
- const errMsg = err instanceof Error ? err.message : String(err);
413
- console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
414
- const retry = await confirm({ message: trans.docs.retry });
415
- if (!isCancel(retry) && retry) {
416
- await browseDirectory(dirPath);
458
+ if (item?.type === 'file') {
459
+ await viewMarkdownFile(selected);
417
460
  }
418
461
  }
419
462
  }
420
463
  // ─── Document viewer ──────────────────────────────────────────────────────────
421
464
  async function viewMarkdownFile(filePath) {
422
465
  const trans = t();
423
- try {
424
- ensureMarkedConfigured();
425
- const s = createSpinner(`${trans.docs.loading.replace('...', '')}: ${filePath}`);
426
- const rawResult = await fetchGitHubRawContent(filePath);
427
- if (rawResult.staleFallback) {
428
- warning(trans.docs.usingCachedData);
429
- }
430
- const rawContent = rawResult.data;
431
- const fingerprint = contentFingerprint(rawContent);
432
- const cachedRendered = getFreshCacheValue(renderCache, filePath);
433
- let renderedDoc;
434
- if (cachedRendered && cachedRendered.fingerprint === fingerprint) {
435
- renderedDoc = cachedRendered;
436
- }
437
- else {
438
- const cleaned = cleanMarkdownContent(rawContent, getTerminalType());
439
- const title = extractDocTitle(rawContent, cleaned) || filePath.split('/').pop() || filePath;
440
- const readTime = estimateReadTime(cleaned);
441
- const rendered = await marked(cleaned);
442
- renderedDoc = { fingerprint, cleaned, rendered, title, readTime };
443
- setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS);
444
- }
445
- s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`);
446
- if (hasGlow()) {
447
- await displayWithGlow(renderedDoc.cleaned);
448
- }
449
- else {
450
- await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime);
466
+ while (true) {
467
+ try {
468
+ ensureMarkedConfigured();
469
+ const s = createSpinner(`${trans.docs.loadingFile}: ${filePath}`);
470
+ const rawResult = await fetchGitHubRawContent(filePath);
471
+ if (rawResult.staleFallback) {
472
+ warning(trans.docs.usingCachedData);
473
+ }
474
+ const rawContent = rawResult.data;
475
+ const fingerprint = contentFingerprint(rawContent);
476
+ const cachedRendered = getFreshCacheValue(renderCache, filePath);
477
+ let renderedDoc;
478
+ if (cachedRendered && cachedRendered.fingerprint === fingerprint) {
479
+ renderedDoc = cachedRendered;
480
+ }
481
+ else {
482
+ const cleaned = cleanMarkdownContent(rawContent, getTerminalType());
483
+ const title = extractDocTitle(rawContent, cleaned) || filePath.split('/').pop() || filePath;
484
+ const readTime = estimateReadTime(cleaned);
485
+ const rendered = await marked(cleaned);
486
+ renderedDoc = { fingerprint, cleaned, rendered, title, readTime };
487
+ setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS);
488
+ evictStalest(renderCache, RENDER_CACHE_MAX);
489
+ }
490
+ s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`);
491
+ if (hasGlow()) {
492
+ await displayWithGlow(renderedDoc.cleaned);
493
+ }
494
+ else {
495
+ await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime);
496
+ }
497
+ console.log();
498
+ success(trans.docs.docCompleted);
499
+ console.log();
500
+ const action = await select({
501
+ message: trans.docs.chooseAction,
502
+ options: [
503
+ { value: 'back', label: trans.docs.backToList },
504
+ { value: 'reread', label: trans.docs.reread },
505
+ { value: 'browser', label: trans.docs.openBrowser },
506
+ ],
507
+ });
508
+ if (isCancel(action) || action === 'back')
509
+ return;
510
+ if (action === 'browser') {
511
+ await openDocsInBrowser(filePath);
512
+ return;
513
+ }
514
+ // action === 'reread' → continue loop
451
515
  }
452
- console.log();
453
- success(trans.docs.docCompleted);
454
- console.log();
455
- const action = await select({
456
- message: trans.docs.chooseAction,
457
- options: [
458
- { value: 'back', label: trans.docs.backToList },
459
- { value: 'reread', label: trans.docs.reread },
460
- { value: 'browser', label: trans.docs.openBrowser },
461
- ],
462
- });
463
- if (isCancel(action))
516
+ catch (err) {
517
+ error(trans.docs.loadError);
518
+ const errMsg = err instanceof Error ? err.message : String(err);
519
+ console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
520
+ setVimKeysActive(false);
521
+ const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt });
522
+ setVimKeysActive(true);
523
+ if (!isCancel(openBrowser) && openBrowser) {
524
+ await openDocsInBrowser(filePath);
525
+ }
464
526
  return;
465
- if (action === 'browser')
466
- await openDocsInBrowser(filePath);
467
- if (action === 'reread')
468
- await viewMarkdownFile(filePath);
469
- }
470
- catch (err) {
471
- error(trans.docs.loadError);
472
- const errMsg = err instanceof Error ? err.message : String(err);
473
- console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`));
474
- const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt });
475
- if (!isCancel(openBrowser) && openBrowser) {
476
- await openDocsInBrowser(filePath);
477
527
  }
478
528
  }
479
529
  }
@@ -497,10 +547,12 @@ export async function openDocsInBrowser(path) {
497
547
  // ─── Search ────────────────────────────────────────────────────────────────────
498
548
  async function searchDocs() {
499
549
  const trans = t();
550
+ setVimKeysActive(false);
500
551
  const query = await text({
501
552
  message: trans.docs.searchPrompt,
502
553
  placeholder: trans.docs.searchPlaceholder,
503
554
  });
555
+ setVimKeysActive(true);
504
556
  if (isCancel(query) || !query.trim())
505
557
  return;
506
558
  const keyword = query.trim().toLowerCase();
@@ -523,6 +575,17 @@ async function searchDocs() {
523
575
  }
524
576
  }
525
577
  }
578
+ // Also search already-cached file content
579
+ for (const [cachedPath, entry] of fileCache) {
580
+ if (results.some(r => r.path === cachedPath))
581
+ continue;
582
+ if (entry.value.toLowerCase().includes(keyword)) {
583
+ const name = cachedPath.split('/').pop() || cachedPath;
584
+ const parentDir = cachedPath.split('/').slice(0, -1).join('/');
585
+ const matchedCat = categories.find(c => parentDir.startsWith(c.path));
586
+ results.push({ name, path: cachedPath, category: matchedCat?.name ?? parentDir });
587
+ }
588
+ }
526
589
  s.stop(`${results.length} ${trans.docs.searchResults}`);
527
590
  }
528
591
  catch {
@@ -35,8 +35,3 @@ export async function showLinksMenu() {
35
35
  return;
36
36
  await openUrl(selected);
37
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); }
@@ -26,7 +26,7 @@ function showAbout() {
26
26
  const content = [
27
27
  row(trans.about.project, APP_INFO.name),
28
28
  row(trans.about.version, `v${APP_INFO.version}`),
29
- row(trans.about.description, APP_INFO.fullDescription),
29
+ row(trans.about.description, trans.about.descriptionText),
30
30
  '',
31
31
  link(trans.about.github, APP_INFO.repository),
32
32
  link(trans.about.website, URLS.homepage),
@@ -1,4 +1,3 @@
1
- import axios from 'axios';
2
1
  import chalk from 'chalk';
3
2
  import { APP_INFO, URLS } from '../config/data.js';
4
3
  import { pickIcon } from '../core/icons.js';
@@ -18,19 +17,23 @@ function getServiceTargets() {
18
17
  async function checkService(name, url, timeoutMs) {
19
18
  const start = Date.now();
20
19
  try {
21
- const response = await axios.get(url, {
22
- timeout: timeoutMs,
23
- maxRedirects: 5,
24
- validateStatus: () => true,
20
+ const controller = new AbortController();
21
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
22
+ const response = await fetch(url, {
23
+ signal: controller.signal,
24
+ redirect: 'follow',
25
25
  headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
26
26
  });
27
+ clearTimeout(timeout);
27
28
  const latencyMs = Date.now() - start;
28
29
  const ok = response.status >= 200 && response.status < 400;
29
30
  return { name, url, ok, statusCode: response.status, latencyMs };
30
31
  }
31
32
  catch (err) {
32
33
  const latencyMs = Date.now() - start;
33
- const error = err instanceof Error ? err.message : String(err);
34
+ const error = err instanceof Error
35
+ ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
36
+ : String(err);
34
37
  return { name, url, ok: false, latencyMs, error };
35
38
  }
36
39
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import chalk from 'chalk';
6
6
  import { APP_INFO } from '../config/data.js';
7
- import { t } from '../i18n/index.js';
7
+ import { t, fmt } from '../i18n/index.js';
8
8
  const NPM_REGISTRY_URL = `https://registry.npmjs.org/@nbtca/prompt/latest`;
9
9
  /**
10
10
  * Fetch latest version from npm registry.
@@ -52,7 +52,7 @@ export async function checkForUpdate() {
52
52
  if (!latest || !isNewer(APP_INFO.version, latest))
53
53
  return null;
54
54
  const trans = t();
55
- return `${trans.update.available.replace('{latest}', latest).replace('{current}', APP_INFO.version)} ${chalk.dim(trans.update.command)}`;
55
+ return `${fmt(trans.update.available, { latest, current: APP_INFO.version })} ${chalk.dim(trans.update.command)}`;
56
56
  }
57
57
  /**
58
58
  * Explicit update check command (nbtca update).
@@ -65,10 +65,10 @@ export async function runUpdateCheck() {
65
65
  return;
66
66
  }
67
67
  if (isNewer(APP_INFO.version, latest)) {
68
- console.log(chalk.yellow(`${trans.update.available.replace('{latest}', latest).replace('{current}', APP_INFO.version)}`));
68
+ console.log(chalk.yellow(`${fmt(trans.update.available, { latest, current: APP_INFO.version })}`));
69
69
  console.log(chalk.dim(trans.update.command));
70
70
  }
71
71
  else {
72
- console.log(chalk.green(`${trans.update.upToDate.replace('{version}', APP_INFO.version)}`));
72
+ console.log(chalk.green(`${fmt(trans.update.upToDate, { version: APP_INFO.version })}`));
73
73
  }
74
74
  }
@@ -6,7 +6,7 @@ import fs from 'fs';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { dirname } from 'path';
9
- import { getConfigDir } from '../config/paths.js';
9
+ import { getConfigDir, getWritableConfigDir } from '../config/paths.js';
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
  /**
@@ -14,11 +14,17 @@ const __dirname = dirname(__filename);
14
14
  */
15
15
  let currentLanguage = 'zh'; // Default to Chinese
16
16
  /**
17
- * Get language configuration file path
17
+ * Get language configuration file path (read, with legacy fallback)
18
18
  */
19
19
  function getLanguageConfigPath() {
20
20
  return path.join(getConfigDir(), 'language.json');
21
21
  }
22
+ /**
23
+ * Get writable language configuration file path (XDG, creates dir)
24
+ */
25
+ function getWritableLanguageConfigPath() {
26
+ return path.join(getWritableConfigDir(), 'language.json');
27
+ }
22
28
  /**
23
29
  * Load language preference from config file
24
30
  */
@@ -40,11 +46,7 @@ export function loadLanguagePreference() {
40
46
  */
41
47
  export function saveLanguagePreference(language) {
42
48
  try {
43
- const configDir = getConfigDir();
44
- if (!fs.existsSync(configDir)) {
45
- fs.mkdirSync(configDir, { recursive: true });
46
- }
47
- const configPath = getLanguageConfigPath();
49
+ const configPath = getWritableLanguageConfigPath();
48
50
  fs.writeFileSync(configPath, JSON.stringify({ language }, null, 2));
49
51
  currentLanguage = language;
50
52
  return true;
@@ -95,6 +97,12 @@ export function t() {
95
97
  }
96
98
  return translationsCache.get(currentLanguage);
97
99
  }
100
+ export function fmt(template, vars) {
101
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
102
+ const val = vars[key];
103
+ return val !== undefined ? String(val) : `{${key}}`;
104
+ });
105
+ }
98
106
  /**
99
107
  * Clear translation cache (useful when switching languages)
100
108
  */
@@ -28,6 +28,7 @@
28
28
  "project": "Project",
29
29
  "version": "Version",
30
30
  "description": "Description",
31
+ "descriptionText": "NBTCA Prompt - Minimalist CLI Tool",
31
32
  "github": "GitHub",
32
33
  "website": "Website",
33
34
  "email": "Email",
@@ -91,7 +92,8 @@
91
92
  "searchPlaceholder": "Enter keyword...",
92
93
  "searching": "Searching documents...",
93
94
  "searchResults": "results found",
94
- "searchNoResults": "No documents match your search"
95
+ "searchNoResults": "No documents match your search",
96
+ "loadingFile": "Loading"
95
97
  },
96
98
  "links": {
97
99
  "choose": "Open a link:",
@@ -161,5 +163,41 @@
161
163
  "upToDate": "You are on the latest version ({version})",
162
164
  "checkFailed": "Could not check for updates",
163
165
  "command": "Run: npm i -g @nbtca/prompt"
166
+ },
167
+ "cli": {
168
+ "usage": "Usage:",
169
+ "interactive": "Interactive menu",
170
+ "runCommand": "Run a command",
171
+ "commands": "Commands:",
172
+ "flags": "Flags:",
173
+ "cmdWebsite": "Official website URL",
174
+ "cmdGithub": "GitHub organization URL",
175
+ "cmdRoadmap": "Project roadmap URL",
176
+ "cmdRepair": "Repair service URL",
177
+ "cmdTheme": "View or set theme",
178
+ "cmdLang": "Set language",
179
+ "cmdUpdate": "Check for updates",
180
+ "flagVersion": "Show version",
181
+ "flagHelp": "Show help",
182
+ "flagOpen": "Open in browser (URL commands)",
183
+ "flagJson": "JSON output (events, status)",
184
+ "flagToday": "Today only (events)",
185
+ "flagNext": "Limit to next N (events)",
186
+ "flagWatch": "Live refresh (status)",
187
+ "flagInterval": "Refresh interval (status --watch)",
188
+ "flagTimeout": "HTTP timeout (status)",
189
+ "flagRetries": "Retry count (status)",
190
+ "flagPlain": "Disable colors",
191
+ "flagNoLogo": "Skip logo",
192
+ "unknownCommand": "Unknown command: {command}",
193
+ "unknownCommandHint": "Run `nbtca --help` to see available commands.",
194
+ "unknownFlag": "Unknown flag: {flag}",
195
+ "unknownFlagHint": "Run `nbtca --help` to see available flags.",
196
+ "invalidFlag": "Flag {flag} is not valid for this command.",
197
+ "invalidFlagHint": "Run `nbtca --help` to see command usage.",
198
+ "invalidLang": "Invalid language. Use `zh` or `en`.",
199
+ "invalidNext": "Invalid --next value. Use --next=<number> (>= 1).",
200
+ "requiresTty": "Interactive mode requires a TTY terminal.",
201
+ "requiresTtyHint": "Use `nbtca --help` for command mode."
164
202
  }
165
203
  }
@@ -28,6 +28,7 @@
28
28
  "project": "项目",
29
29
  "version": "版本",
30
30
  "description": "描述",
31
+ "descriptionText": "NBTCA Prompt - 极简命令行工具",
31
32
  "github": "GitHub",
32
33
  "website": "网站",
33
34
  "email": "邮箱",
@@ -91,7 +92,8 @@
91
92
  "searchPlaceholder": "输入关键词...",
92
93
  "searching": "正在搜索文档...",
93
94
  "searchResults": "个结果",
94
- "searchNoResults": "未找到匹配的文档"
95
+ "searchNoResults": "未找到匹配的文档",
96
+ "loadingFile": "正在加载"
95
97
  },
96
98
  "links": {
97
99
  "choose": "打开链接:",
@@ -161,5 +163,41 @@
161
163
  "upToDate": "已是最新版本 ({version})",
162
164
  "checkFailed": "无法检查更新",
163
165
  "command": "运行: npm i -g @nbtca/prompt"
166
+ },
167
+ "cli": {
168
+ "usage": "用法:",
169
+ "interactive": "交互式菜单",
170
+ "runCommand": "运行命令",
171
+ "commands": "命令:",
172
+ "flags": "选项:",
173
+ "cmdWebsite": "官方网站 URL",
174
+ "cmdGithub": "GitHub 组织 URL",
175
+ "cmdRoadmap": "项目路线图 URL",
176
+ "cmdRepair": "维修服务 URL",
177
+ "cmdTheme": "查看或设置主题",
178
+ "cmdLang": "设置语言",
179
+ "cmdUpdate": "检查更新",
180
+ "flagVersion": "显示版本号",
181
+ "flagHelp": "显示帮助",
182
+ "flagOpen": "在浏览器中打开(URL 命令)",
183
+ "flagJson": "JSON 输出(events, status)",
184
+ "flagToday": "仅显示今日(events)",
185
+ "flagNext": "限制为前 N 个(events)",
186
+ "flagWatch": "实时刷新(status)",
187
+ "flagInterval": "刷新间隔(status --watch)",
188
+ "flagTimeout": "HTTP 超时时间(status)",
189
+ "flagRetries": "重试次数(status)",
190
+ "flagPlain": "禁用颜色",
191
+ "flagNoLogo": "跳过 Logo",
192
+ "unknownCommand": "未知命令: {command}",
193
+ "unknownCommandHint": "运行 `nbtca --help` 查看可用命令。",
194
+ "unknownFlag": "未知选项: {flag}",
195
+ "unknownFlagHint": "运行 `nbtca --help` 查看可用选项。",
196
+ "invalidFlag": "选项 {flag} 对此命令无效。",
197
+ "invalidFlagHint": "运行 `nbtca --help` 查看命令用法。",
198
+ "invalidLang": "无效语言。请使用 `zh` 或 `en`。",
199
+ "invalidNext": "无效的 --next 值。请使用 --next=<数字>(>= 1)。",
200
+ "requiresTty": "交互模式需要 TTY 终端。",
201
+ "requiresTtyHint": "使用 `nbtca --help` 查看命令模式。"
164
202
  }
165
203
  }
package/dist/index.js CHANGED
@@ -10,8 +10,8 @@ import { pickIcon } from './core/icons.js';
10
10
  import { applyColorModePreference } from './config/preferences.js';
11
11
  import { openDocsInBrowser } from './features/docs.js';
12
12
  import { runThemeCommand } from './features/theme.js';
13
- import { setLanguage, t } from './i18n/index.js';
14
- import { clearScreen } from './core/ui.js';
13
+ import { setLanguage, t, fmt } from './i18n/index.js';
14
+ import { clearScreen, handleGracefulExit } from './core/ui.js';
15
15
  import { APP_INFO, URLS } from './config/data.js';
16
16
  import { runUpdateCheck } from './features/update.js';
17
17
  const ACTION_ALIASES = {
@@ -115,8 +115,9 @@ function validateFlags(command, flags) {
115
115
  return !KNOWN_FLAG_PREFIXES.some((prefix) => flag.startsWith(prefix));
116
116
  });
117
117
  if (unknown.length > 0) {
118
- console.error(chalk.red(`Unknown flag: ${unknown[0]}`));
119
- console.error(chalk.dim('Run `nbtca --help` to see available flags.'));
118
+ const trans0 = t();
119
+ console.error(chalk.red(fmt(trans0.cli.unknownFlag, { flag: unknown[0] })));
120
+ console.error(chalk.dim(trans0.cli.unknownFlagHint));
120
121
  process.exit(1);
121
122
  }
122
123
  const allowed = getAllowedFlagsFor(command);
@@ -127,56 +128,63 @@ function validateFlags(command, flags) {
127
128
  return !allowedPrefixes.some((prefix) => flag.startsWith(prefix));
128
129
  });
129
130
  if (disallowed.length > 0) {
130
- console.error(chalk.red(`Flag ${disallowed[0]} is not valid for this command.`));
131
- console.error(chalk.dim('Run `nbtca --help` to see command usage.'));
131
+ const trans1 = t();
132
+ console.error(chalk.red(fmt(trans1.cli.invalidFlag, { flag: disallowed[0] })));
133
+ console.error(chalk.dim(trans1.cli.invalidFlagHint));
132
134
  process.exit(1);
133
135
  }
134
136
  }
135
137
  function printHelp() {
138
+ const trans = t();
139
+ const c = trans.cli;
136
140
  console.log(chalk.bold('NBTCA Prompt'));
137
141
  console.log();
138
- console.log('Usage:');
139
- console.log(' nbtca Interactive menu');
140
- console.log(' nbtca <command> [flags] Run a command');
142
+ console.log(c.usage);
143
+ console.log(` nbtca ${c.interactive}`);
144
+ console.log(` nbtca <command> [flags] ${c.runCommand}`);
141
145
  console.log();
142
- console.log('Commands:');
143
- console.log(' events Upcoming activities');
144
- console.log(' docs Knowledge base');
145
- console.log(' status Service health');
146
- console.log(' website Official website URL');
147
- console.log(' github GitHub organization URL');
148
- console.log(' roadmap Project roadmap URL');
149
- console.log(' repair Repair service URL');
150
- console.log(' theme View or set theme');
151
- console.log(' lang <zh|en> Set language');
152
- console.log(' update Check for updates');
146
+ console.log(c.commands);
147
+ console.log(` events ${trans.menu.eventsDesc}`);
148
+ console.log(` docs ${trans.menu.docsDesc}`);
149
+ console.log(` status ${trans.menu.statusDesc}`);
150
+ console.log(` website ${c.cmdWebsite}`);
151
+ console.log(` github ${c.cmdGithub}`);
152
+ console.log(` roadmap ${c.cmdRoadmap}`);
153
+ console.log(` repair ${c.cmdRepair}`);
154
+ console.log(` theme ${c.cmdTheme}`);
155
+ console.log(` lang <zh|en> ${c.cmdLang}`);
156
+ console.log(` update ${c.cmdUpdate}`);
153
157
  console.log();
154
- console.log('Flags:');
155
- console.log(' --version Show version');
156
- console.log(' --help Show help');
157
- console.log(' --open Open in browser (URL commands)');
158
- console.log(' --json JSON output (events, status)');
159
- console.log(' --today Today only (events)');
160
- console.log(' --next=<n> Limit to next N (events)');
161
- console.log(' --watch Live refresh (status)');
162
- console.log(' --interval=<s> Refresh interval (status --watch)');
163
- console.log(' --timeout=<ms> HTTP timeout (status)');
164
- console.log(' --retries=<n> Retry count (status)');
165
- console.log(' --plain No color');
166
- console.log(' --no-logo Skip logo');
158
+ console.log(c.flags);
159
+ console.log(` --version ${c.flagVersion}`);
160
+ console.log(` --help ${c.flagHelp}`);
161
+ console.log(` --open ${c.flagOpen}`);
162
+ console.log(` --json ${c.flagJson}`);
163
+ console.log(` --today ${c.flagToday}`);
164
+ console.log(` --next=<n> ${c.flagNext}`);
165
+ console.log(` --watch ${c.flagWatch}`);
166
+ console.log(` --interval=<s> ${c.flagInterval}`);
167
+ console.log(` --timeout=<ms> ${c.flagTimeout}`);
168
+ console.log(` --retries=<n> ${c.flagRetries}`);
169
+ console.log(` --plain ${c.flagPlain}`);
170
+ console.log(` --no-logo ${c.flagNoLogo}`);
167
171
  }
168
172
  async function runEventsCommand(flags) {
169
173
  let events = await fetchEvents();
170
174
  if (flags.has('--today')) {
171
175
  const now = new Date();
172
- const todayStr = `${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
173
- events = events.filter(e => e.date === todayStr);
176
+ events = events.filter(e => {
177
+ const d = e.startDate;
178
+ return d.getFullYear() === now.getFullYear() &&
179
+ d.getMonth() === now.getMonth() &&
180
+ d.getDate() === now.getDate();
181
+ });
174
182
  }
175
183
  const nextFlag = Array.from(flags).find(f => f.startsWith('--next='));
176
184
  if (nextFlag) {
177
185
  const n = Number.parseInt(nextFlag.split('=')[1] || '', 10);
178
186
  if (!Number.isInteger(n) || n < 1) {
179
- console.error(chalk.red('Invalid --next value. Use --next=<number> (>= 1).'));
187
+ console.error(chalk.red(t().cli.invalidNext));
180
188
  process.exit(1);
181
189
  }
182
190
  events = events.slice(0, n);
@@ -202,11 +210,11 @@ async function runStatusCommand(flags) {
202
210
  process.exit(1);
203
211
  }
204
212
  if (!Number.isInteger(timeoutMs) || timeoutMs < STATUS_TIMEOUT_MIN || timeoutMs > STATUS_TIMEOUT_MAX) {
205
- console.error(chalk.red(trans.status.invalidTimeout.replace('{min}', String(STATUS_TIMEOUT_MIN)).replace('{max}', String(STATUS_TIMEOUT_MAX))));
213
+ console.error(chalk.red(fmt(trans.status.invalidTimeout, { min: STATUS_TIMEOUT_MIN, max: STATUS_TIMEOUT_MAX })));
206
214
  process.exit(1);
207
215
  }
208
216
  if (!Number.isInteger(retries) || retries < STATUS_RETRIES_MIN || retries > STATUS_RETRIES_MAX) {
209
- console.error(chalk.red(trans.status.invalidRetries.replace('{min}', String(STATUS_RETRIES_MIN)).replace('{max}', String(STATUS_RETRIES_MAX))));
217
+ console.error(chalk.red(fmt(trans.status.invalidRetries, { min: STATUS_RETRIES_MIN, max: STATUS_RETRIES_MAX })));
210
218
  process.exit(1);
211
219
  }
212
220
  if (watch && flags.has('--json')) {
@@ -214,7 +222,7 @@ async function runStatusCommand(flags) {
214
222
  process.exit(1);
215
223
  }
216
224
  if (watch && (!Number.isInteger(intervalSeconds) || intervalSeconds < STATUS_WATCH_INTERVAL_MIN || intervalSeconds > STATUS_WATCH_INTERVAL_MAX)) {
217
- console.error(chalk.red(trans.status.invalidInterval.replace('{min}', String(STATUS_WATCH_INTERVAL_MIN)).replace('{max}', String(STATUS_WATCH_INTERVAL_MAX))));
225
+ console.error(chalk.red(fmt(trans.status.invalidInterval, { min: STATUS_WATCH_INTERVAL_MIN, max: STATUS_WATCH_INTERVAL_MAX })));
218
226
  process.exit(1);
219
227
  }
220
228
  if (watch && !hasInteractiveTerminal()) {
@@ -225,7 +233,7 @@ async function runStatusCommand(flags) {
225
233
  let stopped = false;
226
234
  const onSigint = () => { stopped = true; };
227
235
  process.once('SIGINT', onSigint);
228
- console.log(chalk.dim(`${trans.status.watchStarted.replace('{seconds}', String(intervalSeconds))} | ${trans.status.watchHint}`));
236
+ console.log(chalk.dim(`${fmt(trans.status.watchStarted, { seconds: intervalSeconds })} | ${trans.status.watchHint}`));
229
237
  try {
230
238
  while (!stopped) {
231
239
  const services = await checkServices({ timeoutMs, retries });
@@ -297,8 +305,9 @@ async function runCommandMode(argv) {
297
305
  validateFlags(command, flags);
298
306
  if (!command) {
299
307
  if (!hasInteractiveTerminal()) {
300
- console.error(chalk.red('Interactive mode requires a TTY terminal.'));
301
- console.error(chalk.dim('Use `nbtca --help` for command mode.'));
308
+ const cliTrans = t().cli;
309
+ console.error(chalk.red(cliTrans.requiresTty));
310
+ console.error(chalk.dim(cliTrans.requiresTtyHint));
302
311
  process.exit(1);
303
312
  }
304
313
  await main({ skipLogo: flags.has('--no-logo') });
@@ -307,7 +316,7 @@ async function runCommandMode(argv) {
307
316
  if (command === 'lang' || command === 'language') {
308
317
  const language = (args[0] || '').toLowerCase();
309
318
  if (language !== 'zh' && language !== 'en') {
310
- console.error(chalk.red('Invalid language. Use `zh` or `en`.'));
319
+ console.error(chalk.red(t().cli.invalidLang));
311
320
  process.exit(1);
312
321
  }
313
322
  const persisted = setLanguage(language);
@@ -336,8 +345,9 @@ async function runCommandMode(argv) {
336
345
  }
337
346
  const action = ACTION_ALIASES[command];
338
347
  if (!action) {
339
- console.error(chalk.red(`Unknown command: ${command}`));
340
- console.error(chalk.dim('Run `nbtca --help` to see available commands.'));
348
+ const cliT = t().cli;
349
+ console.error(chalk.red(fmt(cliT.unknownCommand, { command })));
350
+ console.error(chalk.dim(cliT.unknownCommandHint));
341
351
  process.exit(1);
342
352
  }
343
353
  if (action === 'events') {
@@ -346,7 +356,7 @@ async function runCommandMode(argv) {
346
356
  }
347
357
  if (action === 'status') {
348
358
  const ok = await runStatusCommand(flags);
349
- if (!ok)
359
+ if (!ok && !flags.has('--json'))
350
360
  process.exit(1);
351
361
  return;
352
362
  }
@@ -375,9 +385,13 @@ async function runCommandMode(argv) {
375
385
  const content = [
376
386
  row(trans.about.project, APP_INFO.name),
377
387
  row(trans.about.version, `v${APP_INFO.version}`),
388
+ row(trans.about.description, trans.about.descriptionText),
378
389
  '',
379
390
  link(trans.about.github, APP_INFO.repository),
380
391
  link(trans.about.website, URLS.homepage),
392
+ link(trans.about.email, URLS.email),
393
+ '',
394
+ row(trans.about.license, `MIT | ${trans.about.author}: m1ngsama`),
381
395
  ].join('\n');
382
396
  note(content, trans.about.title);
383
397
  return;
@@ -394,17 +408,4 @@ async function runCommandMode(argv) {
394
408
  return;
395
409
  }
396
410
  }
397
- runCommandMode(process.argv.slice(2)).catch((err) => {
398
- if (err?.message?.includes('SIGINT') || err?.message?.includes('User force closed')) {
399
- console.log();
400
- console.log(chalk.dim(t().common.goodbye));
401
- process.exit(0);
402
- }
403
- if (err?.message) {
404
- console.error(err.message);
405
- }
406
- else {
407
- console.error('Error occurred:', err);
408
- }
409
- process.exit(1);
410
- });
411
+ runCommandMode(process.argv.slice(2)).catch(handleGracefulExit);
package/dist/main.js CHANGED
@@ -5,11 +5,10 @@
5
5
  import chalk from 'chalk';
6
6
  import { intro } from '@clack/prompts';
7
7
  import { printLogo } from './core/logo.js';
8
- import { clearScreen } from './core/ui.js';
8
+ import { clearScreen, handleGracefulExit } from './core/ui.js';
9
9
  import { showMainMenu } from './core/menu.js';
10
10
  import { APP_INFO } from './config/data.js';
11
11
  import { enableVimKeys } from './core/vim-keys.js';
12
- import { t } from './i18n/index.js';
13
12
  import { checkForUpdate } from './features/update.js';
14
13
  /**
15
14
  * Main program entry point
@@ -41,15 +40,6 @@ export async function main(options = {}) {
41
40
  await showMainMenu();
42
41
  }
43
42
  catch (err) {
44
- const message = err instanceof Error ? err.message : String(err ?? '');
45
- if (message.includes('SIGINT') || message.includes('User force closed')) {
46
- console.log();
47
- console.log(chalk.dim(t().common.goodbye));
48
- process.exit(0);
49
- }
50
- else {
51
- console.error('Error occurred:', message || err);
52
- process.exit(1);
53
- }
43
+ handleGracefulExit(err);
54
44
  }
55
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbtca/prompt",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -21,7 +21,7 @@
21
21
  "dev": "tsx src/index.ts",
22
22
  "dev:watch": "tsx watch src/index.ts",
23
23
  "build": "tsc",
24
- "postbuild": "mkdir -p dist/logo dist/i18n/locales && cp src/logo/logo.txt dist/logo/ && cp src/logo/ascii-logo.txt dist/logo/ && cp src/i18n/locales/*.json dist/i18n/locales/",
24
+ "postbuild": "node scripts/copy-assets.js",
25
25
  "clean": "rm -rf dist",
26
26
  "prebuild": "npm run clean",
27
27
  "prepublishOnly": "npm run build",
@@ -38,21 +38,20 @@
38
38
  "interactive"
39
39
  ],
40
40
  "dependencies": {
41
- "@clack/prompts": "^1.0.1",
42
- "axios": "^1.6.2",
43
- "chalk": "^5.4.1",
41
+ "@clack/prompts": "^1.2.0",
42
+ "chalk": "^5.6.2",
44
43
  "gradient-string": "^3.0.0",
45
- "ical.js": "^2.0.1",
46
- "marked": "^11.1.0",
44
+ "ical.js": "^2.2.1",
45
+ "marked": "^15.0.12",
47
46
  "marked-terminal": "^7.0.0",
48
- "open": "^10.1.2"
47
+ "open": "^11.0.0"
49
48
  },
50
49
  "devDependencies": {
51
50
  "@types/gradient-string": "^1.1.6",
52
51
  "@types/marked-terminal": "^3.1.3",
53
- "@types/node": "^20.11.0",
54
- "tsx": "^4.7.0",
55
- "typescript": "^5.3.3"
52
+ "@types/node": "^22.19.17",
53
+ "tsx": "^4.21.0",
54
+ "typescript": "^5.9.3"
56
55
  },
57
56
  "engines": {
58
57
  "node": ">=20.12.0"