diff --git a/scripts/test-cli.sh b/scripts/test-cli.sh index 93f571d..244d4b9 100644 --- a/scripts/test-cli.sh +++ b/scripts/test-cli.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -euo pipefail +# Ensure tests run in English locale +node dist/index.js lang en >/dev/null 2>&1 || true + version_output="$(node dist/index.js --version)" if [[ ! "$version_output" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "version output not semver: $version_output" >&2 diff --git a/src/config/data.ts b/src/config/data.ts index 4bfc0f4..3a11293 100644 --- a/src/config/data.ts +++ b/src/config/data.ts @@ -40,7 +40,6 @@ export const APP_INFO = { name: 'Prompt', version: readPackageVersion(), description: '浙大宁波理工学院计算机协会', - fullDescription: 'NBTCA Prompt - 极简命令行工具', author: 'm1ngsama ', license: 'MIT', repository: 'https://github.com/nbtca/prompt' diff --git a/src/core/vim-keys.ts b/src/core/vim-keys.ts index aff7818..3448f3b 100644 --- a/src/core/vim-keys.ts +++ b/src/core/vim-keys.ts @@ -7,7 +7,6 @@ // Maps single-byte vim keys to terminal escape sequences (ranger-style hjkl) const VIM_TO_SEQ: Record = { - h: Buffer.from('\u0003'), // back/cancel (ranger: go to parent) j: Buffer.from('\u001b[B'), // down arrow k: Buffer.from('\u001b[A'), // up arrow l: Buffer.from('\r'), // enter/confirm (ranger: open/enter) diff --git a/src/features/docs.ts b/src/features/docs.ts index f0fbf05..059fe06 100644 --- a/src/features/docs.ts +++ b/src/features/docs.ts @@ -38,7 +38,8 @@ function detectTerminalType(): TerminalType { /** Check whether an external command exists on PATH (once at startup). */ function commandExists(cmd: string): boolean { try { - execFileSync('which', [cmd], { stdio: 'ignore' }); + const check = process.platform === 'win32' ? 'where' : 'which'; + execFileSync(check, [cmd], { stdio: 'ignore' }); return true; } catch { return false; @@ -578,7 +579,7 @@ async function viewMarkdownFile(filePath: string): Promise { while (true) { try { ensureMarkedConfigured(); - const s = createSpinner(`${trans.docs.loading.replace('...', '')}: ${filePath}`); + const s = createSpinner(`${trans.docs.loadingFile}: ${filePath}`); const rawResult = await fetchGitHubRawContent(filePath); if (rawResult.staleFallback) { @@ -706,7 +707,9 @@ async function searchDocs(): Promise { 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 }); + const parentDir = cachedPath.split('/').slice(0, -1).join('/'); + const matchedCat = categories.find(c => parentDir.startsWith(c.path)); + results.push({ name, path: cachedPath, category: matchedCat?.name ?? parentDir }); } } diff --git a/src/features/settings.ts b/src/features/settings.ts index 1cbb622..c770023 100644 --- a/src/features/settings.ts +++ b/src/features/settings.ts @@ -37,7 +37,7 @@ function showAbout(): void { const content = [ row(trans.about.project, APP_INFO.name), row(trans.about.version, `v${APP_INFO.version}`), - row(trans.about.description, APP_INFO.fullDescription), + row(trans.about.description, trans.about.descriptionText), '', link(trans.about.github, APP_INFO.repository), link(trans.about.website, URLS.homepage), diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 7bca3e0..ca5bcb2 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -47,6 +47,7 @@ export interface Translations { project: string; version: string; description: string; + descriptionText: string; github: string; website: string; email: string; @@ -111,6 +112,7 @@ export interface Translations { searching: string; searchResults: string; searchNoResults: string; + loadingFile: string; }; links: { choose: string; @@ -181,6 +183,42 @@ export interface Translations { checkFailed: string; command: string; }; + cli: { + usage: string; + interactive: string; + runCommand: string; + commands: string; + flags: string; + cmdWebsite: string; + cmdGithub: string; + cmdRoadmap: string; + cmdRepair: string; + cmdTheme: string; + cmdLang: string; + cmdUpdate: string; + flagVersion: string; + flagHelp: string; + flagOpen: string; + flagJson: string; + flagToday: string; + flagNext: string; + flagWatch: string; + flagInterval: string; + flagTimeout: string; + flagRetries: string; + flagPlain: string; + flagNoLogo: string; + unknownCommand: string; + unknownCommandHint: string; + unknownFlag: string; + unknownFlagHint: string; + invalidFlag: string; + invalidFlagHint: string; + invalidLang: string; + invalidNext: string; + requiresTty: string; + requiresTtyHint: string; + }; } /** diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 450b0d7..6ebe04d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -28,6 +28,7 @@ "project": "Project", "version": "Version", "description": "Description", + "descriptionText": "NBTCA Prompt - Minimalist CLI Tool", "github": "GitHub", "website": "Website", "email": "Email", @@ -91,7 +92,8 @@ "searchPlaceholder": "Enter keyword...", "searching": "Searching documents...", "searchResults": "results found", - "searchNoResults": "No documents match your search" + "searchNoResults": "No documents match your search", + "loadingFile": "Loading" }, "links": { "choose": "Open a link:", @@ -161,5 +163,41 @@ "upToDate": "You are on the latest version ({version})", "checkFailed": "Could not check for updates", "command": "Run: npm i -g @nbtca/prompt" + }, + "cli": { + "usage": "Usage:", + "interactive": "Interactive menu", + "runCommand": "Run a command", + "commands": "Commands:", + "flags": "Flags:", + "cmdWebsite": "Official website URL", + "cmdGithub": "GitHub organization URL", + "cmdRoadmap": "Project roadmap URL", + "cmdRepair": "Repair service URL", + "cmdTheme": "View or set theme", + "cmdLang": "Set language", + "cmdUpdate": "Check for updates", + "flagVersion": "Show version", + "flagHelp": "Show help", + "flagOpen": "Open in browser (URL commands)", + "flagJson": "JSON output (events, status)", + "flagToday": "Today only (events)", + "flagNext": "Limit to next N (events)", + "flagWatch": "Live refresh (status)", + "flagInterval": "Refresh interval (status --watch)", + "flagTimeout": "HTTP timeout (status)", + "flagRetries": "Retry count (status)", + "flagPlain": "Disable colors", + "flagNoLogo": "Skip logo", + "unknownCommand": "Unknown command: {command}", + "unknownCommandHint": "Run `nbtca --help` to see available commands.", + "unknownFlag": "Unknown flag: {flag}", + "unknownFlagHint": "Run `nbtca --help` to see available flags.", + "invalidFlag": "Flag {flag} is not valid for this command.", + "invalidFlagHint": "Run `nbtca --help` to see command usage.", + "invalidLang": "Invalid language. Use `zh` or `en`.", + "invalidNext": "Invalid --next value. Use --next= (>= 1).", + "requiresTty": "Interactive mode requires a TTY terminal.", + "requiresTtyHint": "Use `nbtca --help` for command mode." } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index fba2186..b69e34a 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -28,6 +28,7 @@ "project": "项目", "version": "版本", "description": "描述", + "descriptionText": "NBTCA Prompt - 极简命令行工具", "github": "GitHub", "website": "网站", "email": "邮箱", @@ -91,7 +92,8 @@ "searchPlaceholder": "输入关键词...", "searching": "正在搜索文档...", "searchResults": "个结果", - "searchNoResults": "未找到匹配的文档" + "searchNoResults": "未找到匹配的文档", + "loadingFile": "正在加载" }, "links": { "choose": "打开链接:", @@ -161,5 +163,41 @@ "upToDate": "已是最新版本 ({version})", "checkFailed": "无法检查更新", "command": "运行: npm i -g @nbtca/prompt" + }, + "cli": { + "usage": "用法:", + "interactive": "交互式菜单", + "runCommand": "运行命令", + "commands": "命令:", + "flags": "选项:", + "cmdWebsite": "官方网站 URL", + "cmdGithub": "GitHub 组织 URL", + "cmdRoadmap": "项目路线图 URL", + "cmdRepair": "维修服务 URL", + "cmdTheme": "查看或设置主题", + "cmdLang": "设置语言", + "cmdUpdate": "检查更新", + "flagVersion": "显示版本号", + "flagHelp": "显示帮助", + "flagOpen": "在浏览器中打开(URL 命令)", + "flagJson": "JSON 输出(events, status)", + "flagToday": "仅显示今日(events)", + "flagNext": "限制为前 N 个(events)", + "flagWatch": "实时刷新(status)", + "flagInterval": "刷新间隔(status --watch)", + "flagTimeout": "HTTP 超时时间(status)", + "flagRetries": "重试次数(status)", + "flagPlain": "禁用颜色", + "flagNoLogo": "跳过 Logo", + "unknownCommand": "未知命令: {command}", + "unknownCommandHint": "运行 `nbtca --help` 查看可用命令。", + "unknownFlag": "未知选项: {flag}", + "unknownFlagHint": "运行 `nbtca --help` 查看可用选项。", + "invalidFlag": "选项 {flag} 对此命令无效。", + "invalidFlagHint": "运行 `nbtca --help` 查看命令用法。", + "invalidLang": "无效语言。请使用 `zh` 或 `en`。", + "invalidNext": "无效的 --next 值。请使用 --next=<数字>(>= 1)。", + "requiresTty": "交互模式需要 TTY 终端。", + "requiresTtyHint": "使用 `nbtca --help` 查看命令模式。" } } diff --git a/src/index.ts b/src/index.ts index c7ece50..2af3569 100644 --- a/src/index.ts +++ b/src/index.ts @@ -130,8 +130,9 @@ function validateFlags(command: string | undefined, flags: Set): void { return !KNOWN_FLAG_PREFIXES.some((prefix) => flag.startsWith(prefix)); }); if (unknown.length > 0) { - console.error(chalk.red(`Unknown flag: ${unknown[0]}`)); - console.error(chalk.dim('Run `nbtca --help` to see available flags.')); + const trans0 = t(); + console.error(chalk.red(fmt(trans0.cli.unknownFlag, { flag: unknown[0]! }))); + console.error(chalk.dim(trans0.cli.unknownFlagHint)); process.exit(1); } @@ -142,44 +143,47 @@ function validateFlags(command: string | undefined, flags: Set): void { return !allowedPrefixes.some((prefix) => flag.startsWith(prefix)); }); if (disallowed.length > 0) { - console.error(chalk.red(`Flag ${disallowed[0]} is not valid for this command.`)); - console.error(chalk.dim('Run `nbtca --help` to see command usage.')); + const trans1 = t(); + console.error(chalk.red(fmt(trans1.cli.invalidFlag, { flag: disallowed[0]! }))); + console.error(chalk.dim(trans1.cli.invalidFlagHint)); process.exit(1); } } function printHelp(): void { + const trans = t(); + const c = trans.cli; console.log(chalk.bold('NBTCA Prompt')); console.log(); - console.log('Usage:'); - console.log(' nbtca Interactive menu'); - console.log(' nbtca [flags] Run a command'); + console.log(c.usage); + console.log(` nbtca ${c.interactive}`); + console.log(` nbtca [flags] ${c.runCommand}`); console.log(); - console.log('Commands:'); - console.log(' events Upcoming activities'); - console.log(' docs Knowledge base'); - console.log(' status Service health'); - console.log(' website Official website URL'); - console.log(' github GitHub organization URL'); - console.log(' roadmap Project roadmap URL'); - console.log(' repair Repair service URL'); - console.log(' theme View or set theme'); - console.log(' lang Set language'); - console.log(' update Check for updates'); + console.log(c.commands); + console.log(` events ${trans.menu.eventsDesc}`); + console.log(` docs ${trans.menu.docsDesc}`); + console.log(` status ${trans.menu.statusDesc}`); + console.log(` website ${c.cmdWebsite}`); + console.log(` github ${c.cmdGithub}`); + console.log(` roadmap ${c.cmdRoadmap}`); + console.log(` repair ${c.cmdRepair}`); + console.log(` theme ${c.cmdTheme}`); + console.log(` lang ${c.cmdLang}`); + console.log(` update ${c.cmdUpdate}`); console.log(); - console.log('Flags:'); - console.log(' --version Show version'); - console.log(' --help Show help'); - console.log(' --open Open in browser (URL commands)'); - console.log(' --json JSON output (events, status)'); - console.log(' --today Today only (events)'); - console.log(' --next= Limit to next N (events)'); - console.log(' --watch Live refresh (status)'); - console.log(' --interval= Refresh interval (status --watch)'); - console.log(' --timeout= HTTP timeout (status)'); - console.log(' --retries= Retry count (status)'); - console.log(' --plain No color'); - console.log(' --no-logo Skip logo'); + console.log(c.flags); + console.log(` --version ${c.flagVersion}`); + console.log(` --help ${c.flagHelp}`); + console.log(` --open ${c.flagOpen}`); + console.log(` --json ${c.flagJson}`); + console.log(` --today ${c.flagToday}`); + console.log(` --next= ${c.flagNext}`); + console.log(` --watch ${c.flagWatch}`); + console.log(` --interval= ${c.flagInterval}`); + console.log(` --timeout= ${c.flagTimeout}`); + console.log(` --retries= ${c.flagRetries}`); + console.log(` --plain ${c.flagPlain}`); + console.log(` --no-logo ${c.flagNoLogo}`); } async function runEventsCommand(flags: Set): Promise { @@ -199,7 +203,7 @@ async function runEventsCommand(flags: Set): Promise { if (nextFlag) { const n = Number.parseInt(nextFlag.split('=')[1] || '', 10); if (!Number.isInteger(n) || n < 1) { - console.error(chalk.red('Invalid --next value. Use --next= (>= 1).')); + console.error(chalk.red(t().cli.invalidNext)); process.exit(1); } events = events.slice(0, n); @@ -339,8 +343,9 @@ async function runCommandMode(argv: string[]): Promise { if (!command) { if (!hasInteractiveTerminal()) { - console.error(chalk.red('Interactive mode requires a TTY terminal.')); - console.error(chalk.dim('Use `nbtca --help` for command mode.')); + const cliTrans = t().cli; + console.error(chalk.red(cliTrans.requiresTty)); + console.error(chalk.dim(cliTrans.requiresTtyHint)); process.exit(1); } await main({ skipLogo: flags.has('--no-logo') }); @@ -350,7 +355,7 @@ async function runCommandMode(argv: string[]): Promise { if (command === 'lang' || command === 'language') { const language = (args[0] || '').toLowerCase() as Language; if (language !== 'zh' && language !== 'en') { - console.error(chalk.red('Invalid language. Use `zh` or `en`.')); + console.error(chalk.red(t().cli.invalidLang)); process.exit(1); } const persisted = setLanguage(language); @@ -381,8 +386,9 @@ async function runCommandMode(argv: string[]): Promise { const action = ACTION_ALIASES[command]; if (!action) { - console.error(chalk.red(`Unknown command: ${command}`)); - console.error(chalk.dim('Run `nbtca --help` to see available commands.')); + const cliT = t().cli; + console.error(chalk.red(fmt(cliT.unknownCommand, { command }))); + console.error(chalk.dim(cliT.unknownCommandHint)); process.exit(1); } @@ -393,7 +399,7 @@ async function runCommandMode(argv: string[]): Promise { if (action === 'status') { const ok = await runStatusCommand(flags); - if (!ok) process.exit(1); + if (!ok && !flags.has('--json')) process.exit(1); return; } @@ -421,9 +427,13 @@ async function runCommandMode(argv: string[]): Promise { const content = [ row(trans.about.project, APP_INFO.name), row(trans.about.version, `v${APP_INFO.version}`), + row(trans.about.description, trans.about.descriptionText), '', link(trans.about.github, APP_INFO.repository), link(trans.about.website, URLS.homepage), + link(trans.about.email, URLS.email), + '', + row(trans.about.license, `MIT | ${trans.about.author}: m1ngsama`), ].join('\n'); note(content, trans.about.title); return;