From f214f94cbb5c3993eab9c47e6429fe617c22fcda Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 11:49:39 +0800 Subject: [PATCH 01/10] refactor: XDG config directory with legacy fallback Co-Authored-By: Claude Sonnet 4.6 --- src/config/paths.ts | 28 +++++++++++++++++++++++++--- src/config/preferences.ts | 12 ++++-------- src/i18n/index.ts | 17 ++++++++++------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/config/paths.ts b/src/config/paths.ts index 68dd6b0..a116d5e 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -1,6 +1,28 @@ -import path from 'path'; +import { existsSync, mkdirSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; + +function getXdgConfigDir(): string { + const xdgHome = process.env['XDG_CONFIG_HOME'] || join(homedir(), '.config'); + return join(xdgHome, 'nbtca'); +} + +function getLegacyConfigDir(): string { + return join(homedir(), '.nbtca'); +} export function getConfigDir(): string { - const homeDir = process.env['HOME'] || process.env['USERPROFILE'] || ''; - return path.join(homeDir, '.nbtca'); + const xdgDir = getXdgConfigDir(); + if (existsSync(xdgDir)) return xdgDir; + + const legacyDir = getLegacyConfigDir(); + if (existsSync(legacyDir)) return legacyDir; + + return xdgDir; +} + +export function getWritableConfigDir(): string { + const dir = getXdgConfigDir(); + mkdirSync(dir, { recursive: true }); + return dir; } diff --git a/src/config/preferences.ts b/src/config/preferences.ts index 0373d88..27e31f3 100644 --- a/src/config/preferences.ts +++ b/src/config/preferences.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { getConfigDir } from './paths.js'; +import { getConfigDir, getWritableConfigDir } from './paths.js'; export type IconMode = 'auto' | 'ascii' | 'unicode'; export type ColorMode = 'auto' | 'on' | 'off'; @@ -19,11 +19,8 @@ function getPreferencesPath(): string { return path.join(getConfigDir(), 'preferences.json'); } -function ensureConfigDir(): void { - const configDir = getConfigDir(); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } +function getWritablePreferencesPath(): string { + return path.join(getWritableConfigDir(), 'preferences.json'); } export function loadPreferences(): Preferences { @@ -46,8 +43,7 @@ export function loadPreferences(): Preferences { function savePreferences(preferences: Preferences): boolean { try { - ensureConfigDir(); - fs.writeFileSync(getPreferencesPath(), JSON.stringify(preferences, null, 2)); + fs.writeFileSync(getWritablePreferencesPath(), JSON.stringify(preferences, null, 2)); return true; } catch { return false; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 9f9b461..824e459 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -7,7 +7,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; -import { getConfigDir } from '../config/paths.js'; +import { getConfigDir, getWritableConfigDir } from '../config/paths.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -189,12 +189,19 @@ export interface Translations { let currentLanguage: Language = 'zh'; // Default to Chinese /** - * Get language configuration file path + * Get language configuration file path (read, with legacy fallback) */ function getLanguageConfigPath(): string { return path.join(getConfigDir(), 'language.json'); } +/** + * Get writable language configuration file path (XDG, creates dir) + */ +function getWritableLanguageConfigPath(): string { + return path.join(getWritableConfigDir(), 'language.json'); +} + /** * Load language preference from config file */ @@ -216,11 +223,7 @@ export function loadLanguagePreference(): Language { */ export function saveLanguagePreference(language: Language): boolean { try { - const configDir = getConfigDir(); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - const configPath = getLanguageConfigPath(); + const configPath = getWritableLanguageConfigPath(); fs.writeFileSync(configPath, JSON.stringify({ language }, null, 2)); currentLanguage = language; return true; From befbdbdb46deb96efec940c2bd7675595c324dc1 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 11:51:34 +0800 Subject: [PATCH 02/10] refactor: add i18n fmt helper, replace .replace() chains Co-Authored-By: Claude Sonnet 4.6 --- src/features/update.ts | 8 ++++---- src/i18n/index.ts | 7 +++++++ src/index.ts | 10 +++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/features/update.ts b/src/features/update.ts index 052b724..593d44f 100644 --- a/src/features/update.ts +++ b/src/features/update.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import { APP_INFO } from '../config/data.js'; -import { t } from '../i18n/index.js'; +import { t, fmt } from '../i18n/index.js'; const NPM_REGISTRY_URL = `https://registry.npmjs.org/@nbtca/prompt/latest`; @@ -56,7 +56,7 @@ export async function checkForUpdate(): Promise { const latest = await fetchLatestVersion(); if (!latest || !isNewer(APP_INFO.version, latest)) return null; const trans = t(); - return `${trans.update.available.replace('{latest}', latest).replace('{current}', APP_INFO.version)} ${chalk.dim(trans.update.command)}`; + return `${fmt(trans.update.available, { latest, current: APP_INFO.version })} ${chalk.dim(trans.update.command)}`; } /** @@ -72,9 +72,9 @@ export async function runUpdateCheck(): Promise { } if (isNewer(APP_INFO.version, latest)) { - console.log(chalk.yellow(`${trans.update.available.replace('{latest}', latest).replace('{current}', APP_INFO.version)}`)); + console.log(chalk.yellow(`${fmt(trans.update.available, { latest, current: APP_INFO.version })}`)); console.log(chalk.dim(trans.update.command)); } else { - console.log(chalk.green(`${trans.update.upToDate.replace('{version}', APP_INFO.version)}`)); + console.log(chalk.green(`${fmt(trans.update.upToDate, { version: APP_INFO.version })}`)); } } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 824e459..7bca3e0 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -278,6 +278,13 @@ export function t(): Translations { return translationsCache.get(currentLanguage)!; } +export function fmt(template: string, vars: Record): string { + return template.replace(/\{(\w+)\}/g, (_, key: string) => { + const val = vars[key]; + return val !== undefined ? String(val) : `{${key}}`; + }); +} + /** * Clear translation cache (useful when switching languages) */ diff --git a/src/index.ts b/src/index.ts index 3864266..81b1d07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import { pickIcon } from './core/icons.js'; import { applyColorModePreference } from './config/preferences.js'; import { openDocsInBrowser } from './features/docs.js'; import { runThemeCommand } from './features/theme.js'; -import { setLanguage, t, type Language } from './i18n/index.js'; +import { setLanguage, t, fmt, type Language } from './i18n/index.js'; import { clearScreen } from './core/ui.js'; import { APP_INFO, URLS } from './config/data.js'; import { runUpdateCheck } from './features/update.js'; @@ -226,13 +226,13 @@ async function runStatusCommand(flags: Set): Promise { } if (!Number.isInteger(timeoutMs) || timeoutMs < STATUS_TIMEOUT_MIN || timeoutMs > STATUS_TIMEOUT_MAX) { console.error(chalk.red( - trans.status.invalidTimeout.replace('{min}', String(STATUS_TIMEOUT_MIN)).replace('{max}', String(STATUS_TIMEOUT_MAX)) + fmt(trans.status.invalidTimeout, { min: STATUS_TIMEOUT_MIN, max: STATUS_TIMEOUT_MAX }) )); process.exit(1); } if (!Number.isInteger(retries) || retries < STATUS_RETRIES_MIN || retries > STATUS_RETRIES_MAX) { console.error(chalk.red( - trans.status.invalidRetries.replace('{min}', String(STATUS_RETRIES_MIN)).replace('{max}', String(STATUS_RETRIES_MAX)) + fmt(trans.status.invalidRetries, { min: STATUS_RETRIES_MIN, max: STATUS_RETRIES_MAX }) )); process.exit(1); } @@ -242,7 +242,7 @@ async function runStatusCommand(flags: Set): Promise { } if (watch && (!Number.isInteger(intervalSeconds) || intervalSeconds < STATUS_WATCH_INTERVAL_MIN || intervalSeconds > STATUS_WATCH_INTERVAL_MAX)) { console.error(chalk.red( - trans.status.invalidInterval.replace('{min}', String(STATUS_WATCH_INTERVAL_MIN)).replace('{max}', String(STATUS_WATCH_INTERVAL_MAX)) + fmt(trans.status.invalidInterval, { min: STATUS_WATCH_INTERVAL_MIN, max: STATUS_WATCH_INTERVAL_MAX }) )); process.exit(1); } @@ -257,7 +257,7 @@ async function runStatusCommand(flags: Set): Promise { process.once('SIGINT', onSigint); console.log(chalk.dim( - `${trans.status.watchStarted.replace('{seconds}', String(intervalSeconds))} | ${trans.status.watchHint}` + `${fmt(trans.status.watchStarted, { seconds: intervalSeconds })} | ${trans.status.watchHint}` )); try { From eb11973234bcf7d67d9e4804d90afc484fffe2d8 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 12:04:12 +0800 Subject: [PATCH 03/10] fix: vim-keys text input conflict, shared SIGINT handler, extended CJK ranges Co-Authored-By: Claude Opus 4.6 --- src/core/text.ts | 24 ++++++++++++++---------- src/core/ui.ts | 16 ++++++++++++++++ src/core/vim-keys.ts | 8 +++++++- src/features/docs.ts | 7 +++++++ src/index.ts | 17 ++--------------- src/main.ts | 13 ++----------- 6 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/core/text.ts b/src/core/text.ts index c5527a5..c90a7f0 100644 --- a/src/core/text.ts +++ b/src/core/text.ts @@ -2,16 +2,20 @@ function charWidth(ch: string): 1 | 2 { const cp = ch.codePointAt(0) ?? 0; return ( - (cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo - (cp >= 0x2E80 && cp <= 0x303F) || // CJK Radicals / Kangxi - (cp >= 0x3040 && cp <= 0x33FF) || // Japanese kana + CJK symbols - (cp >= 0x3400 && cp <= 0x4DBF) || // CJK Extension A - (cp >= 0x4E00 && cp <= 0x9FFF) || // CJK Unified Ideographs - (cp >= 0xAC00 && cp <= 0xD7AF) || // Hangul Syllables - (cp >= 0xF900 && cp <= 0xFAFF) || // CJK Compatibility Ideographs - (cp >= 0xFE30 && cp <= 0xFE4F) || // CJK Compatibility Forms - (cp >= 0xFF00 && cp <= 0xFF60) || // Fullwidth Forms - (cp >= 0xFFE0 && cp <= 0xFFE6) // Fullwidth Signs + (cp >= 0x1100 && cp <= 0x115F) || + (cp >= 0x2E80 && cp <= 0x303F) || + (cp >= 0x3040 && cp <= 0x33FF) || + (cp >= 0x3400 && cp <= 0x4DBF) || + (cp >= 0x4E00 && cp <= 0x9FFF) || + (cp >= 0xAC00 && cp <= 0xD7AF) || + (cp >= 0xF900 && cp <= 0xFAFF) || + (cp >= 0xFE30 && cp <= 0xFE4F) || + (cp >= 0xFF00 && cp <= 0xFF60) || + (cp >= 0xFFE0 && cp <= 0xFFE6) || + (cp >= 0x20000 && cp <= 0x2A6DF) || + (cp >= 0x2A700 && cp <= 0x2CEAF) || + (cp >= 0x2CEB0 && cp <= 0x2EBEF) || + (cp >= 0x30000 && cp <= 0x323AF) ) ? 2 : 1; } diff --git a/src/core/ui.ts b/src/core/ui.ts index b04ad2b..63e29a1 100644 --- a/src/core/ui.ts +++ b/src/core/ui.ts @@ -6,6 +6,7 @@ import { log, spinner as clackSpinner } from '@clack/prompts'; import chalk from 'chalk'; import { pickIcon } from './icons.js'; +import { t } from '../i18n/index.js'; /** * Display success message @@ -71,3 +72,18 @@ export function createSpinner(msg: string) { s.start(msg); return s; } + +export function handleGracefulExit(err: unknown): never { + const message = err instanceof Error ? err.message : String(err ?? ''); + if (message.includes('SIGINT') || message.includes('User force closed')) { + console.log(); + console.log(chalk.dim(t().common.goodbye)); + process.exit(0); + } + if (message) { + console.error(message); + } else { + console.error('Error occurred:', err); + } + process.exit(1); +} diff --git a/src/core/vim-keys.ts b/src/core/vim-keys.ts index 18fe136..aff7818 100644 --- a/src/core/vim-keys.ts +++ b/src/core/vim-keys.ts @@ -16,6 +16,12 @@ const VIM_TO_SEQ: Record = { q: Buffer.from('\u0003'), // quit }; +let vimActive = true; + +export function setVimKeysActive(active: boolean): void { + vimActive = active; +} + export function enableVimKeys(): void { const stdin = process.stdin; if (!stdin.isTTY) return; @@ -23,7 +29,7 @@ export function enableVimKeys(): void { const originalEmit = stdin.emit.bind(stdin); (stdin.emit as any) = function (event: string, ...args: any[]) { - if (event === 'data') { + if (event === 'data' && vimActive) { const chunk = args[0]; if (Buffer.isBuffer(chunk) && chunk.length === 1) { const seq = VIM_TO_SEQ[String.fromCharCode(chunk[0] as number)]; diff --git a/src/features/docs.ts b/src/features/docs.ts index 26c3f27..00591ac 100644 --- a/src/features/docs.ts +++ b/src/features/docs.ts @@ -14,6 +14,7 @@ import { pickIcon } from '../core/icons.js'; import { spawn, execFileSync } from 'child_process'; import { APP_INFO, GITHUB_REPO, URLS } from '../config/data.js'; import { t } from '../i18n/index.js'; +import { setVimKeysActive } from '../core/vim-keys.js'; // ─── Terminal capability detection ─────────────────────────────────────────── @@ -518,7 +519,9 @@ async function browseDirectory(dirPath: string = ''): Promise { const errMsg = err instanceof Error ? err.message : String(err); console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`)); + setVimKeysActive(false); const retry = await confirm({ message: trans.docs.retry }); + setVimKeysActive(true); if (!isCancel(retry) && retry) { await browseDirectory(dirPath); } @@ -584,7 +587,9 @@ async function viewMarkdownFile(filePath: string): Promise { const errMsg = err instanceof Error ? err.message : String(err); console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`)); + setVimKeysActive(false); const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt }); + setVimKeysActive(true); if (!isCancel(openBrowser) && openBrowser) { await openDocsInBrowser(filePath); } @@ -613,10 +618,12 @@ export async function openDocsInBrowser(path?: string): Promise { async function searchDocs(): Promise { const trans = t(); + setVimKeysActive(false); const query = await text({ message: trans.docs.searchPrompt, placeholder: trans.docs.searchPlaceholder, }); + setVimKeysActive(true); if (isCancel(query) || !query.trim()) return; diff --git a/src/index.ts b/src/index.ts index 81b1d07..13c32ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { applyColorModePreference } from './config/preferences.js'; import { openDocsInBrowser } from './features/docs.js'; import { runThemeCommand } from './features/theme.js'; import { setLanguage, t, fmt, type Language } from './i18n/index.js'; -import { clearScreen } from './core/ui.js'; +import { clearScreen, handleGracefulExit } from './core/ui.js'; import { APP_INFO, URLS } from './config/data.js'; import { runUpdateCheck } from './features/update.js'; @@ -437,17 +437,4 @@ async function runCommandMode(argv: string[]): Promise { } } -runCommandMode(process.argv.slice(2)).catch((err: any) => { - if (err?.message?.includes('SIGINT') || err?.message?.includes('User force closed')) { - console.log(); - console.log(chalk.dim(t().common.goodbye)); - process.exit(0); - } - - if (err?.message) { - console.error(err.message); - } else { - console.error('Error occurred:', err); - } - process.exit(1); -}); +runCommandMode(process.argv.slice(2)).catch(handleGracefulExit); diff --git a/src/main.ts b/src/main.ts index 4b56c83..03cd9cc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,10 @@ import chalk from 'chalk'; import { intro } from '@clack/prompts'; import { printLogo } from './core/logo.js'; -import { clearScreen } from './core/ui.js'; +import { clearScreen, handleGracefulExit } from './core/ui.js'; import { showMainMenu } from './core/menu.js'; import { APP_INFO } from './config/data.js'; import { enableVimKeys } from './core/vim-keys.js'; -import { t } from './i18n/index.js'; import { checkForUpdate } from './features/update.js'; export interface MainOptions { @@ -52,14 +51,6 @@ export async function main(options: MainOptions = {}): Promise { await showMainMenu(); } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err ?? ''); - if (message.includes('SIGINT') || message.includes('User force closed')) { - console.log(); - console.log(chalk.dim(t().common.goodbye)); - process.exit(0); - } else { - console.error('Error occurred:', message || err); - process.exit(1); - } + handleGracefulExit(err); } } From 007d001bb267266dd048f27737e8205e45caea70 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 12:04:49 +0800 Subject: [PATCH 04/10] fix: remove dead code in links.ts, fix any type in status.ts --- src/features/links.ts | 6 ------ src/features/status.ts | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/features/links.ts b/src/features/links.ts index 50367c8..c16e44d 100644 --- a/src/features/links.ts +++ b/src/features/links.ts @@ -38,9 +38,3 @@ export async function showLinksMenu(): Promise { if (isCancel(selected) || selected === '__back__') return; await openUrl(selected); } - -/** Direct openers for CLI non-interactive mode */ -export async function openHomepage(): Promise { await openUrl(URLS.homepage); } -export async function openGithub(): Promise { await openUrl(URLS.github); } -export async function openRoadmap(): Promise { await openUrl(URLS.roadmap); } -export async function openRepairService(): Promise { await openUrl(URLS.repair); } diff --git a/src/features/status.ts b/src/features/status.ts index c786dc5..afb912f 100644 --- a/src/features/status.ts +++ b/src/features/status.ts @@ -43,7 +43,7 @@ async function checkService(name: string, url: string, timeoutMs: number): Promi const latencyMs = Date.now() - start; const ok = response.status >= 200 && response.status < 400; return { name, url, ok, statusCode: response.status, latencyMs }; - } catch (err: any) { + } catch (err: unknown) { const latencyMs = Date.now() - start; const error = err instanceof Error ? err.message : String(err); return { name, url, ok: false, latencyMs, error }; From beb921dd170f48d3cf1ea424440d590d69616a71 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 12:04:51 +0800 Subject: [PATCH 05/10] fix: show year in calendar dates for cross-year events --- src/features/calendar.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/calendar.ts b/src/features/calendar.ts index 886903b..b777a13 100644 --- a/src/features/calendar.ts +++ b/src/features/calendar.ts @@ -89,8 +89,12 @@ export function serializeEvents(events: Event[]): EventOutputItem[] { function formatDate(date: Date): string { + const now = new Date(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); + if (date.getFullYear() !== now.getFullYear()) { + return `${date.getFullYear()}-${month}-${day}`; + } return `${month}-${day}`; } @@ -109,7 +113,7 @@ export function renderEventsTable(events: Event[], options?: { color?: boolean } if (events.length === 0) return trans.calendar.noEvents; - const dateWidth = 13; + const dateWidth = 16; const titleWidth = 30; const locationWidth = 16; From 0ee3ec01da1ad38541cefff1a811ab927a9d5123 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 12:08:00 +0800 Subject: [PATCH 06/10] refactor: convert recursive directory browsing to iterative loops Co-Authored-By: Claude Sonnet 4.6 --- src/features/docs.ts | 183 +++++++++++++++++++++++-------------------- 1 file changed, 99 insertions(+), 84 deletions(-) diff --git a/src/features/docs.ts b/src/features/docs.ts index 00591ac..3ee6b58 100644 --- a/src/features/docs.ts +++ b/src/features/docs.ts @@ -466,25 +466,44 @@ async function displayWithLess( // ─── Directory browser ──────────────────────────────────────────────────────── -async function browseDirectory(dirPath: string = ''): Promise { - const trans = t(); - try { - const s = createSpinner(dirPath ? `${trans.docs.loadingDir}: ${dirPath}` : trans.docs.loading); - const result = await fetchGitHubDirectory(dirPath); - const items = result.data; - s.stop(dirPath || trans.docs.chooseDoc); +async function browseDirectory(initialPath: string = ''): Promise { + let currentPath = initialPath; - if (result.staleFallback) { - warning(trans.docs.usingCachedData); + while (true) { + const trans = t(); + let items: DocItem[]; + try { + const s = createSpinner(currentPath ? `${trans.docs.loadingDir}: ${currentPath}` : trans.docs.loading); + const result = await fetchGitHubDirectory(currentPath); + items = result.data; + s.stop(currentPath || trans.docs.chooseDoc); + + if (result.staleFallback) { + warning(trans.docs.usingCachedData); + } + } catch (err: unknown) { + error(trans.docs.loadError); + const errMsg = err instanceof Error ? err.message : String(err); + console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`)); + + setVimKeysActive(false); + const retry = await confirm({ message: trans.docs.retry }); + setVimKeysActive(true); + if (!isCancel(retry) && retry) continue; + return; } if (items.length === 0) { warning(trans.docs.emptyDir); + if (currentPath) { + currentPath = currentPath.split('/').slice(0, -1).join('/'); + continue; + } return; } const options = [ - ...(dirPath ? [{ value: '__back__', label: chalk.dim(trans.docs.upToParent) }] : []), + ...(currentPath ? [{ value: '__back__', label: chalk.dim(trans.docs.upToParent) }] : []), ...items.map(item => ({ value: item.path, label: item.type === 'dir' @@ -496,34 +515,24 @@ async function browseDirectory(dirPath: string = ''): Promise { ]; const selected = await select({ - message: dirPath ? `${trans.docs.currentDir}: ${dirPath}` : trans.docs.chooseDoc, + message: currentPath ? `${trans.docs.currentDir}: ${currentPath}` : trans.docs.chooseDoc, options, }); if (isCancel(selected) || selected === '__exit__') return; if (selected === '__back__') { - const parentPath = dirPath.split('/').slice(0, -1).join('/'); - await browseDirectory(parentPath); - } else { - const item = items.find(i => i.path === selected); - if (item?.type === 'dir') { - await browseDirectory(selected); - } else if (item?.type === 'file') { - await viewMarkdownFile(selected); - await browseDirectory(dirPath); - } + currentPath = currentPath.split('/').slice(0, -1).join('/'); + continue; } - } catch (err: unknown) { - error(trans.docs.loadError); - const errMsg = err instanceof Error ? err.message : String(err); - console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`)); - - setVimKeysActive(false); - const retry = await confirm({ message: trans.docs.retry }); - setVimKeysActive(true); - if (!isCancel(retry) && retry) { - await browseDirectory(dirPath); + + const item = items.find(i => i.path === selected); + if (item?.type === 'dir') { + currentPath = selected; + continue; + } + if (item?.type === 'file') { + await viewMarkdownFile(selected); } } } @@ -532,66 +541,72 @@ async function browseDirectory(dirPath: string = ''): Promise { async function viewMarkdownFile(filePath: string): Promise { const trans = t(); - try { - ensureMarkedConfigured(); - const s = createSpinner(`${trans.docs.loading.replace('...', '')}: ${filePath}`); - - const rawResult = await fetchGitHubRawContent(filePath); - if (rawResult.staleFallback) { - warning(trans.docs.usingCachedData); - } - const rawContent = rawResult.data; - const fingerprint = contentFingerprint(rawContent); - const cachedRendered = getFreshCacheValue(renderCache, filePath); - - let renderedDoc: RenderedDoc; - if (cachedRendered && cachedRendered.fingerprint === fingerprint) { - renderedDoc = cachedRendered; - } else { - const cleaned = cleanMarkdownContent(rawContent, getTerminalType()); - const title = extractDocTitle(rawContent, cleaned) || filePath.split('/').pop() || filePath; - const readTime = estimateReadTime(cleaned); - const rendered = await marked(cleaned) as string; - renderedDoc = { fingerprint, cleaned, rendered, title, readTime }; - setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS); - } + while (true) { + try { + ensureMarkedConfigured(); + const s = createSpinner(`${trans.docs.loading.replace('...', '')}: ${filePath}`); - s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`); + const rawResult = await fetchGitHubRawContent(filePath); + if (rawResult.staleFallback) { + warning(trans.docs.usingCachedData); + } - if (hasGlow()) { - await displayWithGlow(renderedDoc.cleaned); - } else { - await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime); - } + const rawContent = rawResult.data; + const fingerprint = contentFingerprint(rawContent); + const cachedRendered = getFreshCacheValue(renderCache, filePath); + + let renderedDoc: RenderedDoc; + if (cachedRendered && cachedRendered.fingerprint === fingerprint) { + renderedDoc = cachedRendered; + } else { + const cleaned = cleanMarkdownContent(rawContent, getTerminalType()); + const title = extractDocTitle(rawContent, cleaned) || filePath.split('/').pop() || filePath; + const readTime = estimateReadTime(cleaned); + const rendered = await marked(cleaned) as string; + renderedDoc = { fingerprint, cleaned, rendered, title, readTime }; + setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS); + } - console.log(); - success(trans.docs.docCompleted); - console.log(); + s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`); - const action = await select({ - message: trans.docs.chooseAction, - options: [ - { value: 'back', label: trans.docs.backToList }, - { value: 'reread', label: trans.docs.reread }, - { value: 'browser', label: trans.docs.openBrowser }, - ], - }); + if (hasGlow()) { + await displayWithGlow(renderedDoc.cleaned); + } else { + await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime); + } - if (isCancel(action)) return; - if (action === 'browser') await openDocsInBrowser(filePath); - if (action === 'reread') await viewMarkdownFile(filePath); + console.log(); + success(trans.docs.docCompleted); + console.log(); + + const action = await select({ + message: trans.docs.chooseAction, + options: [ + { value: 'back', label: trans.docs.backToList }, + { value: 'reread', label: trans.docs.reread }, + { value: 'browser', label: trans.docs.openBrowser }, + ], + }); - } catch (err: unknown) { - error(trans.docs.loadError); - const errMsg = err instanceof Error ? err.message : String(err); - console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`)); - - setVimKeysActive(false); - const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt }); - setVimKeysActive(true); - if (!isCancel(openBrowser) && openBrowser) { - await openDocsInBrowser(filePath); + if (isCancel(action) || action === 'back') return; + if (action === 'browser') { + await openDocsInBrowser(filePath); + return; + } + // action === 'reread' → continue loop + } catch (err: unknown) { + error(trans.docs.loadError); + const errMsg = err instanceof Error ? err.message : String(err); + console.log(chalk.gray(` ${trans.docs.errorHint}: ${errMsg}`)); + + setVimKeysActive(false); + const openBrowser = await confirm({ message: trans.docs.openBrowserPrompt }); + setVimKeysActive(true); + if (!isCancel(openBrowser) && openBrowser) { + await openDocsInBrowser(filePath); + } + return; } } } From 8858f1bd0e8dec74005e1746baf37839133bf79c Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 12:09:20 +0800 Subject: [PATCH 07/10] feat: LRU cache eviction and content-aware doc search Co-Authored-By: Claude Sonnet 4.6 --- src/features/docs.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/features/docs.ts b/src/features/docs.ts index 3ee6b58..1970deb 100644 --- a/src/features/docs.ts +++ b/src/features/docs.ts @@ -188,6 +188,18 @@ function setCacheValue(cache: Map>, key: string, value: cache.set(key, { value, expiresAt: Date.now() + ttlMs }); } +const DIR_CACHE_MAX = 30; +const FILE_CACHE_MAX = 50; +const RENDER_CACHE_MAX = 50; + +function evictOldest(cache: Map>, maxSize: number): void { + if (cache.size <= maxSize) return; + const oldest = [...cache.entries()] + .sort((a, b) => a[1].expiresAt - b[1].expiresAt) + .slice(0, cache.size - maxSize); + for (const [key] of oldest) cache.delete(key); +} + function contentFingerprint(content: string): string { const head = content.slice(0, 80); const tail = content.slice(-80); @@ -243,6 +255,7 @@ async function fetchGitHubDirectory( }); setCacheValue(dirCache, cacheKey, items, DIR_CACHE_TTL_MS); + evictOldest(dirCache, DIR_CACHE_MAX); return { data: items, fromCache: false, staleFallback: false }; } catch (err: unknown) { const staleCached = getAnyCacheValue(dirCache, cacheKey); @@ -284,6 +297,7 @@ async function fetchGitHubRawContent( const response = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` } }); const content = String(response.data); setCacheValue(fileCache, path, content, FILE_CACHE_TTL_MS); + evictOldest(fileCache, FILE_CACHE_MAX); return { data: content, fromCache: false, staleFallback: false }; } catch (err: unknown) { const staleCached = getAnyCacheValue(fileCache, path); @@ -566,6 +580,7 @@ async function viewMarkdownFile(filePath: string): Promise { const rendered = await marked(cleaned) as string; renderedDoc = { fingerprint, cleaned, rendered, title, readTime }; setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS); + evictOldest(renderCache, RENDER_CACHE_MAX); } s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`); @@ -667,6 +682,15 @@ async function searchDocs(): Promise { } } + // Also search already-cached file content + for (const [cachedPath, entry] of fileCache) { + if (results.some(r => r.path === cachedPath)) continue; + if (entry.value.toLowerCase().includes(keyword)) { + const name = cachedPath.split('/').pop() || cachedPath; + results.push({ name, path: cachedPath, category: trans.docs.searchResults }); + } + } + s.stop(`${results.length} ${trans.docs.searchResults}`); } catch { s.error(trans.docs.loadError); From c746117af969864223f72063f0eb094cc36e9935 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 12:17:55 +0800 Subject: [PATCH 08/10] chore: upgrade deps (marked 15, TS 5.9, open 11), remove axios for native fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove axios, replace all HTTP calls with native fetch + AbortController - Upgrade marked 11→15, typescript 5.3→5.9, open 10→11, @clack/prompts 1.2 - Upgrade @types/node 22, tsx 4.21 - Fix marked.setOptions→marked.use API for marked 15 Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 641 ++++++++++++++------------------------- package.json | 17 +- src/features/calendar.ts | 16 +- src/features/docs.ts | 55 ++-- src/features/status.ts | 11 +- 5 files changed, 278 insertions(+), 462 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ba8a87..9d80477 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,13 @@ "version": "1.0.24", "license": "MIT", "dependencies": { - "@clack/prompts": "^1.0.1", - "axios": "^1.6.2", - "chalk": "^5.4.1", + "@clack/prompts": "^1.2.0", + "chalk": "^5.6.2", "gradient-string": "^3.0.0", - "ical.js": "^2.0.1", - "marked": "^11.1.0", + "ical.js": "^2.2.1", + "marked": "^15.0.12", "marked-terminal": "^7.0.0", - "open": "^10.1.2" + "open": "^11.0.0" }, "bin": { "nbtca": "bin/nbtca-welcome.js", @@ -25,32 +24,33 @@ "devDependencies": { "@types/gradient-string": "^1.1.6", "@types/marked-terminal": "^3.1.3", - "@types/node": "^20.11.0", - "tsx": "^4.7.0", - "typescript": "^5.3.3" + "@types/node": "^22.19.17", + "tsx": "^4.21.0", + "typescript": "^5.9.3" }, "engines": { "node": ">=20.12.0" } }, "node_modules/@clack/core": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.1.tgz", - "integrity": "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.2.0.tgz", + "integrity": "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==", "license": "MIT", "dependencies": { - "picocolors": "^1.0.0", + "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "node_modules/@clack/prompts": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.1.tgz", - "integrity": "sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.2.0.tgz", + "integrity": "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==", "license": "MIT", "dependencies": { - "@clack/core": "1.0.1", - "picocolors": "^1.0.0", + "@clack/core": "1.2.0", + "fast-string-width": "^1.1.0", + "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, @@ -65,9 +65,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -82,9 +82,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -99,9 +99,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -116,9 +116,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -133,9 +133,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -150,9 +150,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -167,9 +167,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -184,9 +184,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -201,9 +201,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -218,9 +218,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -235,9 +235,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -252,9 +252,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -269,9 +269,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -286,9 +286,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -303,9 +303,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -337,9 +337,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -354,9 +354,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -371,9 +371,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -388,9 +388,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -405,9 +405,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -422,9 +422,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -439,9 +439,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -456,9 +456,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -473,9 +473,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -490,9 +490,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -621,9 +621,9 @@ } }, "node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -652,25 +652,10 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/bundle-name": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -682,21 +667,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/chalk": { - "version": "5.4.1", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -920,20 +894,10 @@ "version": "1.1.4", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/default-browser": { - "version": "5.2.1", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -947,7 +911,9 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.0", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "license": "MIT", "engines": { "node": ">=18" @@ -966,29 +932,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/emojilib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", @@ -1007,55 +950,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1066,32 +964,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -1113,40 +1011,28 @@ "node": ">=0.8.0" } }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/fast-string-truncated-width": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz", + "integrity": "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-1.1.0.tgz", + "integrity": "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==", "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "dependencies": { + "fast-string-truncated-width": "^1.2.0" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/fast-wrap-ansi": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.1.6.tgz", + "integrity": "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==", "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" + "fast-string-width": "^1.1.0" } }, "node_modules/fsevents": { @@ -1164,15 +1050,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1182,43 +1059,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -1232,18 +1072,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gradient-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", @@ -1266,45 +1094,6 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -1322,6 +1111,8 @@ }, "node_modules/is-docker": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "license": "MIT", "bin": { "is-docker": "cli.js" @@ -1340,8 +1131,22 @@ "node": ">=8" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -1357,7 +1162,9 @@ } }, "node_modules/is-wsl": { - "version": "3.1.0", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -1370,9 +1177,9 @@ } }, "node_modules/marked": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", - "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -1417,36 +1224,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -1483,16 +1260,20 @@ } }, "node_modules/open": { - "version": "10.1.2", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", + "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1519,17 +1300,17 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "license": "MIT" }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/require-directory": { "version": "2.1.1", @@ -1551,7 +1332,9 @@ } }, "node_modules/run-applescript": { - "version": "7.0.0", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "license": "MIT", "engines": { "node": ">=18" @@ -1644,13 +1427,13 @@ } }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -1693,6 +1476,22 @@ "node": ">=4" } }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 73ec4c5..7ea7b54 100644 --- a/package.json +++ b/package.json @@ -38,21 +38,20 @@ "interactive" ], "dependencies": { - "@clack/prompts": "^1.0.1", - "axios": "^1.6.2", - "chalk": "^5.4.1", + "@clack/prompts": "^1.2.0", + "chalk": "^5.6.2", "gradient-string": "^3.0.0", - "ical.js": "^2.0.1", - "marked": "^11.1.0", + "ical.js": "^2.2.1", + "marked": "^15.0.12", "marked-terminal": "^7.0.0", - "open": "^10.1.2" + "open": "^11.0.0" }, "devDependencies": { "@types/gradient-string": "^1.1.6", "@types/marked-terminal": "^3.1.3", - "@types/node": "^20.11.0", - "tsx": "^4.7.0", - "typescript": "^5.3.3" + "@types/node": "^22.19.17", + "tsx": "^4.21.0", + "typescript": "^5.9.3" }, "engines": { "node": ">=20.12.0" diff --git a/src/features/calendar.ts b/src/features/calendar.ts index b777a13..b3cfb34 100644 --- a/src/features/calendar.ts +++ b/src/features/calendar.ts @@ -3,7 +3,6 @@ * Fetches and renders upcoming events with Unicode box table. */ -import axios from 'axios'; import ICAL from 'ical.js'; import chalk from 'chalk'; import { select, isCancel } from '@clack/prompts'; @@ -33,14 +32,17 @@ export interface EventOutputItem { export async function fetchEvents(): Promise { try { - const response = await axios.get('https://ical.nbtca.space', { - timeout: 5000, - headers: { - 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` - } + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const response = await fetch('https://ical.nbtca.space', { + signal: controller.signal, + headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` }, }); + clearTimeout(timeout); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.text(); - const jcalData = ICAL.parse(response.data); + const jcalData = ICAL.parse(data); const comp = new ICAL.Component(jcalData); const vevents = comp.getAllSubcomponents('vevent'); diff --git a/src/features/docs.ts b/src/features/docs.ts index 1970deb..a85bc0e 100644 --- a/src/features/docs.ts +++ b/src/features/docs.ts @@ -3,7 +3,6 @@ * 获取并渲染Markdown文档 */ -import axios from 'axios'; import { marked } from 'marked'; import TerminalRenderer from 'marked-terminal'; import chalk from 'chalk'; @@ -62,8 +61,9 @@ let _markedConfigured = false; function ensureMarkedConfigured(): void { if (_markedConfigured) return; _markedConfigured = true; - // @ts-ignore - marked v11 / marked-terminal v7 type incompatibility - marked.setOptions({ renderer: new TerminalRenderer(getRendererOptions(getTerminalType())) }); + marked.use({ + renderer: new TerminalRenderer(getRendererOptions(getTerminalType())) as any + }); } // ─── marked-terminal renderer ───────────────────────────────────────────────── @@ -229,14 +229,34 @@ async function fetchGitHubDirectory( try { const headers: Record = { 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` + 'User-Agent': `NBTCA-CLI/${APP_INFO.version}`, }; if (GITHUB_TOKEN) headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`; - const response = await axios.get(url, { timeout: 10000, headers }); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + const response = await fetch(url, { signal: controller.signal, headers }); + clearTimeout(timeout); + + if (!response.ok) { + const trans = t(); + if (response.status === 403) { + const rateLimitRemaining = response.headers.get('x-ratelimit-remaining'); + const rateLimitReset = response.headers.get('x-ratelimit-reset'); + if (rateLimitRemaining === '0' && rateLimitReset) { + const resetDate = new Date(Number.parseInt(rateLimitReset, 10) * 1000); + throw new Error( + `${trans.docs.githubRateLimited.replace('{time}', resetDate.toLocaleTimeString())}\n${trans.docs.githubTokenHint}` + ); + } + throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`); + } + throw new Error(`HTTP ${response.status}`); + } interface GitHubContentItem { name: string; path: string; type: string; sha: string } - const items = (response.data as GitHubContentItem[]) + const data = (await response.json()) as GitHubContentItem[]; + const items = data .filter((item) => !item.name.startsWith('.') && !SKIP_NAMES.has(item.name) && @@ -265,18 +285,6 @@ async function fetchGitHubDirectory( const trans = t(); const errorMessage = err instanceof Error ? err.message : String(err); - const axiosErr = err as { response?: { status?: number; headers?: Record } }; - if (axiosErr.response?.status === 403) { - const rateLimitRemaining = axiosErr.response.headers?.['x-ratelimit-remaining']; - const rateLimitReset = axiosErr.response.headers?.['x-ratelimit-reset']; - if (rateLimitRemaining === '0' && rateLimitReset) { - const resetDate = new Date(Number.parseInt(rateLimitReset, 10) * 1000); - throw new Error( - `${trans.docs.githubRateLimited.replace('{time}', resetDate.toLocaleTimeString())}\n${trans.docs.githubTokenHint}` - ); - } - throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`); - } throw new Error(trans.docs.fetchDirFailed.replace('{error}', errorMessage)); } } @@ -294,8 +302,15 @@ async function fetchGitHubRawContent( const url = `https://raw.githubusercontent.com/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}/${GITHUB_REPO.branch}/${path}`; try { - const response = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` } }); - const content = String(response.data); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15000); + const response = await fetch(url, { + signal: controller.signal, + headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` }, + }); + clearTimeout(timeout); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const content = await response.text(); setCacheValue(fileCache, path, content, FILE_CACHE_TTL_MS); evictOldest(fileCache, FILE_CACHE_MAX); return { data: content, fromCache: false, staleFallback: false }; diff --git a/src/features/status.ts b/src/features/status.ts index afb912f..cbb6627 100644 --- a/src/features/status.ts +++ b/src/features/status.ts @@ -1,4 +1,3 @@ -import axios from 'axios'; import chalk from 'chalk'; import { APP_INFO, URLS } from '../config/data.js'; import { pickIcon } from '../core/icons.js'; @@ -34,12 +33,14 @@ function getServiceTargets() { async function checkService(name: string, url: string, timeoutMs: number): Promise { const start = Date.now(); try { - const response = await axios.get(url, { - timeout: timeoutMs, - maxRedirects: 5, - validateStatus: () => true, + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + const response = await fetch(url, { + signal: controller.signal, + redirect: 'follow', headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` }, }); + clearTimeout(timeout); const latencyMs = Date.now() - start; const ok = response.status >= 200 && response.status < 400; return { name, url, ok, statusCode: response.status, latencyMs }; From 6d277e1848583587b29a6490f243a99f136ff57c Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 12:22:04 +0800 Subject: [PATCH 09/10] feat: cross-platform build script and CI test step Replace shell-based postbuild with Node.js copy-assets script for cross-platform compatibility. Add test step to CI, remove redundant tsc --noEmit check. Fix test to use XDG config path. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 6 +++--- package.json | 4 ++-- scripts/copy-assets.js | 7 +++++++ scripts/test-cli.sh | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 scripts/copy-assets.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 372ad37..475ab3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,11 @@ jobs: - name: 🔨 Build TypeScript run: npm run build - - name: ✅ Type check - run: npx tsc --noEmit - - name: 📋 Verify build artifacts run: | test -d dist || (echo "dist directory not found" && exit 1) test -f dist/index.js || (echo "dist/index.js not found" && exit 1) echo "Build artifacts verified successfully" + + - name: ✅ Run tests + run: npm test diff --git a/package.json b/package.json index 7ea7b54..f848bb8 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "dev": "tsx src/index.ts", "dev:watch": "tsx watch src/index.ts", "build": "tsc", - "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/", + "postbuild": "node scripts/copy-assets.js", "clean": "rm -rf dist", "prebuild": "npm run clean", "prepublishOnly": "npm run build", - "test": "npm run build && bash scripts/test-cli.sh" + "test": "bash scripts/test-cli.sh" }, "keywords": [ "cli", diff --git a/scripts/copy-assets.js b/scripts/copy-assets.js new file mode 100644 index 0000000..0215d0d --- /dev/null +++ b/scripts/copy-assets.js @@ -0,0 +1,7 @@ +import { cpSync, mkdirSync } from 'fs'; + +mkdirSync('dist/logo', { recursive: true }); +mkdirSync('dist/i18n/locales', { recursive: true }); + +cpSync('src/logo', 'dist/logo', { recursive: true }); +cpSync('src/i18n/locales', 'dist/i18n/locales', { recursive: true }); diff --git a/scripts/test-cli.sh b/scripts/test-cli.sh index 66bd684..93f571d 100644 --- a/scripts/test-cli.sh +++ b/scripts/test-cli.sh @@ -33,8 +33,8 @@ fi tmp_home="$(mktemp -d)" trap 'rm -rf "$tmp_home"' EXIT -HOME="$tmp_home" node dist/index.js theme icon ascii >/dev/null -if ! grep -q '"iconMode": "ascii"' "$tmp_home/.nbtca/preferences.json"; then +HOME="$tmp_home" XDG_CONFIG_HOME="$tmp_home/.config" node dist/index.js theme icon ascii >/dev/null +if ! grep -q '"iconMode": "ascii"' "$tmp_home/.config/nbtca/preferences.json"; then echo "theme preference was not persisted" >&2 rm -rf "$tmp_home" exit 1 From 8476a5cd20e25880318aeabf887e4d9c9300bb94 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 27 Apr 2026 13:05:15 +0800 Subject: [PATCH 10/10] fix: address code review findings - Fix --today filter: compare Date objects instead of formatted strings to handle cross-year events correctly - Convert remaining .replace() calls in docs.ts to fmt() for consistency - Rename evictOldest to evictStalest to accurately reflect FIFO behavior - Improve abort timeout error messages: show "Request timed out" instead of cryptic "The operation was aborted" across all fetch call sites - Restore build step in npm test for local dev safety Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/features/calendar.ts | 4 +++- src/features/docs.ts | 24 ++++++++++++++---------- src/features/status.ts | 4 +++- src/index.ts | 8 ++++++-- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index f848bb8..839478d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "clean": "rm -rf dist", "prebuild": "npm run clean", "prepublishOnly": "npm run build", - "test": "bash scripts/test-cli.sh" + "test": "npm run build && bash scripts/test-cli.sh" }, "keywords": [ "cli", diff --git a/src/features/calendar.ts b/src/features/calendar.ts index b3cfb34..bb976aa 100644 --- a/src/features/calendar.ts +++ b/src/features/calendar.ts @@ -73,7 +73,9 @@ export async function fetchEvents(): Promise { events.sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); return events; } catch (err) { - const detail = err instanceof Error ? err.message : String(err); + const detail = err instanceof Error + ? (err.name === 'AbortError' ? 'Request timed out' : err.message) + : String(err); throw new Error(`${t().calendar.error}: ${detail}`); } } diff --git a/src/features/docs.ts b/src/features/docs.ts index a85bc0e..f0fbf05 100644 --- a/src/features/docs.ts +++ b/src/features/docs.ts @@ -12,7 +12,7 @@ import { error, warning, success, createSpinner } from '../core/ui.js'; import { pickIcon } from '../core/icons.js'; import { spawn, execFileSync } from 'child_process'; import { APP_INFO, GITHUB_REPO, URLS } from '../config/data.js'; -import { t } from '../i18n/index.js'; +import { t, fmt } from '../i18n/index.js'; import { setVimKeysActive } from '../core/vim-keys.js'; // ─── Terminal capability detection ─────────────────────────────────────────── @@ -192,7 +192,7 @@ const DIR_CACHE_MAX = 30; const FILE_CACHE_MAX = 50; const RENDER_CACHE_MAX = 50; -function evictOldest(cache: Map>, maxSize: number): void { +function evictStalest(cache: Map>, maxSize: number): void { if (cache.size <= maxSize) return; const oldest = [...cache.entries()] .sort((a, b) => a[1].expiresAt - b[1].expiresAt) @@ -246,7 +246,7 @@ async function fetchGitHubDirectory( if (rateLimitRemaining === '0' && rateLimitReset) { const resetDate = new Date(Number.parseInt(rateLimitReset, 10) * 1000); throw new Error( - `${trans.docs.githubRateLimited.replace('{time}', resetDate.toLocaleTimeString())}\n${trans.docs.githubTokenHint}` + `${fmt(trans.docs.githubRateLimited, { time: resetDate.toLocaleTimeString() })}\n${trans.docs.githubTokenHint}` ); } throw new Error(`${trans.docs.githubForbidden}\n${trans.docs.githubTokenHint}`); @@ -275,7 +275,7 @@ async function fetchGitHubDirectory( }); setCacheValue(dirCache, cacheKey, items, DIR_CACHE_TTL_MS); - evictOldest(dirCache, DIR_CACHE_MAX); + evictStalest(dirCache, DIR_CACHE_MAX); return { data: items, fromCache: false, staleFallback: false }; } catch (err: unknown) { const staleCached = getAnyCacheValue(dirCache, cacheKey); @@ -284,8 +284,10 @@ async function fetchGitHubDirectory( } const trans = t(); - const errorMessage = err instanceof Error ? err.message : String(err); - throw new Error(trans.docs.fetchDirFailed.replace('{error}', errorMessage)); + const errorMessage = err instanceof Error + ? (err.name === 'AbortError' ? 'Request timed out' : err.message) + : String(err); + throw new Error(fmt(trans.docs.fetchDirFailed, { error: errorMessage })); } } @@ -312,7 +314,7 @@ async function fetchGitHubRawContent( if (!response.ok) throw new Error(`HTTP ${response.status}`); const content = await response.text(); setCacheValue(fileCache, path, content, FILE_CACHE_TTL_MS); - evictOldest(fileCache, FILE_CACHE_MAX); + evictStalest(fileCache, FILE_CACHE_MAX); return { data: content, fromCache: false, staleFallback: false }; } catch (err: unknown) { const staleCached = getAnyCacheValue(fileCache, path); @@ -321,8 +323,10 @@ async function fetchGitHubRawContent( } const trans = t(); - const errorMessage = err instanceof Error ? err.message : String(err); - throw new Error(trans.docs.fetchFileFailed.replace('{error}', errorMessage)); + const errorMessage = err instanceof Error + ? (err.name === 'AbortError' ? 'Request timed out' : err.message) + : String(err); + throw new Error(fmt(trans.docs.fetchFileFailed, { error: errorMessage })); } } @@ -595,7 +599,7 @@ async function viewMarkdownFile(filePath: string): Promise { const rendered = await marked(cleaned) as string; renderedDoc = { fingerprint, cleaned, rendered, title, readTime }; setCacheValue(renderCache, filePath, renderedDoc, RENDER_CACHE_TTL_MS); - evictOldest(renderCache, RENDER_CACHE_MAX); + evictStalest(renderCache, RENDER_CACHE_MAX); } s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`); diff --git a/src/features/status.ts b/src/features/status.ts index cbb6627..7025e54 100644 --- a/src/features/status.ts +++ b/src/features/status.ts @@ -46,7 +46,9 @@ async function checkService(name: string, url: string, timeoutMs: number): Promi return { name, url, ok, statusCode: response.status, latencyMs }; } catch (err: unknown) { const latencyMs = Date.now() - start; - const error = err instanceof Error ? err.message : String(err); + const error = err instanceof Error + ? (err.name === 'AbortError' ? 'Request timed out' : err.message) + : String(err); return { name, url, ok: false, latencyMs, error }; } } diff --git a/src/index.ts b/src/index.ts index 13c32ee..c7ece50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -187,8 +187,12 @@ async function runEventsCommand(flags: Set): Promise { if (flags.has('--today')) { const now = new Date(); - const todayStr = `${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; - events = events.filter(e => e.date === todayStr); + events = events.filter(e => { + const d = e.startDate; + return d.getFullYear() === now.getFullYear() && + d.getMonth() === now.getMonth() && + d.getDate() === now.getDate(); + }); } const nextFlag = Array.from(flags).find(f => f.startsWith('--next='));