@nbtca/prompt 1.0.23 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/config/data.js +5 -1
  2. package/dist/config/paths.js +24 -0
  3. package/dist/config/preferences.js +4 -15
  4. package/dist/core/icons.js +18 -5
  5. package/dist/core/logo.js +1 -2
  6. package/dist/core/menu.js +7 -122
  7. package/dist/core/text.js +23 -14
  8. package/dist/core/ui.js +16 -1
  9. package/dist/core/vim-keys.js +11 -6
  10. package/dist/features/calendar.js +66 -16
  11. package/dist/features/docs.js +270 -131
  12. package/dist/features/links.js +37 -0
  13. package/dist/features/settings.js +120 -0
  14. package/dist/features/status.js +28 -32
  15. package/dist/features/theme.js +9 -101
  16. package/dist/features/update.js +74 -0
  17. package/dist/i18n/index.js +20 -21
  18. package/dist/i18n/locales/en.json +43 -54
  19. package/dist/i18n/locales/zh.json +43 -54
  20. package/dist/index.js +115 -84
  21. package/dist/main.js +13 -14
  22. package/package.json +13 -11
  23. package/dist/config/data.d.ts +0 -23
  24. package/dist/config/data.d.ts.map +0 -1
  25. package/dist/config/data.js.map +0 -1
  26. package/dist/config/preferences.d.ts +0 -14
  27. package/dist/config/preferences.d.ts.map +0 -1
  28. package/dist/config/preferences.js.map +0 -1
  29. package/dist/config/theme.d.ts +0 -23
  30. package/dist/config/theme.d.ts.map +0 -1
  31. package/dist/config/theme.js +0 -25
  32. package/dist/config/theme.js.map +0 -1
  33. package/dist/core/icons.d.ts +0 -3
  34. package/dist/core/icons.d.ts.map +0 -1
  35. package/dist/core/icons.js.map +0 -1
  36. package/dist/core/logo.d.ts +0 -9
  37. package/dist/core/logo.d.ts.map +0 -1
  38. package/dist/core/logo.js.map +0 -1
  39. package/dist/core/menu.d.ts +0 -14
  40. package/dist/core/menu.d.ts.map +0 -1
  41. package/dist/core/menu.js.map +0 -1
  42. package/dist/core/text.d.ts +0 -7
  43. package/dist/core/text.d.ts.map +0 -1
  44. package/dist/core/text.js.map +0 -1
  45. package/dist/core/ui.d.ts +0 -38
  46. package/dist/core/ui.d.ts.map +0 -1
  47. package/dist/core/ui.js.map +0 -1
  48. package/dist/core/vim-keys.d.ts +0 -8
  49. package/dist/core/vim-keys.d.ts.map +0 -1
  50. package/dist/core/vim-keys.js.map +0 -1
  51. package/dist/features/calendar.d.ts +0 -29
  52. package/dist/features/calendar.d.ts.map +0 -1
  53. package/dist/features/calendar.js.map +0 -1
  54. package/dist/features/docs.d.ts +0 -8
  55. package/dist/features/docs.d.ts.map +0 -1
  56. package/dist/features/docs.js.map +0 -1
  57. package/dist/features/repair.d.ts +0 -10
  58. package/dist/features/repair.d.ts.map +0 -1
  59. package/dist/features/repair.js +0 -29
  60. package/dist/features/repair.js.map +0 -1
  61. package/dist/features/status.d.ts +0 -31
  62. package/dist/features/status.d.ts.map +0 -1
  63. package/dist/features/status.js.map +0 -1
  64. package/dist/features/theme.d.ts +0 -8
  65. package/dist/features/theme.d.ts.map +0 -1
  66. package/dist/features/theme.js.map +0 -1
  67. package/dist/features/website.d.ts +0 -30
  68. package/dist/features/website.d.ts.map +0 -1
  69. package/dist/features/website.js +0 -48
  70. package/dist/features/website.js.map +0 -1
  71. package/dist/i18n/index.d.ts +0 -209
  72. package/dist/i18n/index.d.ts.map +0 -1
  73. package/dist/i18n/index.js.map +0 -1
  74. package/dist/index.d.ts +0 -5
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js.map +0 -1
  77. package/dist/main.d.ts +0 -12
  78. package/dist/main.d.ts.map +0 -1
  79. package/dist/main.js.map +0 -1
  80. package/dist/types.d.ts +0 -48
  81. package/dist/types.d.ts.map +0 -1
  82. package/dist/types.js +0 -6
  83. package/dist/types.js.map +0 -1
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Unified settings — language, theme, about
3
+ */
4
+ import { select, isCancel, note } from '@clack/prompts';
5
+ import chalk from 'chalk';
6
+ import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../config/preferences.js';
7
+ import { pickIcon } from '../core/icons.js';
8
+ import { resetIconCache } from '../core/icons.js';
9
+ import { padEndV } from '../core/text.js';
10
+ import { success, warning } from '../core/ui.js';
11
+ import { APP_INFO, URLS } from '../config/data.js';
12
+ import { t, getCurrentLanguage, setLanguage, clearTranslationCache } from '../i18n/index.js';
13
+ function notifyResult(saved, successMsg, warningMsg) {
14
+ if (saved) {
15
+ success(successMsg);
16
+ }
17
+ else {
18
+ warning(warningMsg);
19
+ }
20
+ }
21
+ function showAbout() {
22
+ const trans = t();
23
+ const pad = 12;
24
+ const row = (label, value) => `${chalk.dim(padEndV(label, pad))}${value}`;
25
+ const link = (label, url) => row(label, chalk.cyan(url));
26
+ const content = [
27
+ row(trans.about.project, APP_INFO.name),
28
+ row(trans.about.version, `v${APP_INFO.version}`),
29
+ row(trans.about.description, APP_INFO.fullDescription),
30
+ '',
31
+ link(trans.about.github, APP_INFO.repository),
32
+ link(trans.about.website, URLS.homepage),
33
+ link(trans.about.email, URLS.email),
34
+ '',
35
+ row(trans.about.license, `MIT ${pickIcon('·', '|')} ${trans.about.author}: m1ngsama`),
36
+ ].join('\n');
37
+ note(content, trans.about.title);
38
+ }
39
+ export async function showSettingsMenu() {
40
+ while (true) {
41
+ const trans = t();
42
+ const prefs = loadPreferences();
43
+ const currentLang = getCurrentLanguage();
44
+ const action = await select({
45
+ message: trans.theme.chooseAction,
46
+ options: [
47
+ { value: 'language', label: trans.language.selectLanguage.replace(':', ''), hint: currentLang === 'zh' ? trans.language.zh : trans.language.en },
48
+ { value: 'icon', label: trans.theme.iconMode, hint: prefs.iconMode },
49
+ { value: 'color', label: trans.theme.colorMode, hint: prefs.colorMode },
50
+ { value: 'reset', label: trans.theme.reset },
51
+ { value: 'about', label: trans.about.title },
52
+ { value: 'back', label: chalk.dim(trans.common.back) },
53
+ ],
54
+ });
55
+ if (isCancel(action) || action === 'back')
56
+ return;
57
+ if (action === 'about') {
58
+ showAbout();
59
+ continue;
60
+ }
61
+ if (action === 'language') {
62
+ const language = await select({
63
+ message: trans.language.selectLanguage,
64
+ options: [
65
+ { value: 'zh', label: trans.language.zh, hint: currentLang === 'zh' ? trans.common.current : undefined },
66
+ { value: 'en', label: trans.language.en, hint: currentLang === 'en' ? trans.common.current : undefined },
67
+ ],
68
+ initialValue: currentLang,
69
+ });
70
+ if (isCancel(language))
71
+ continue;
72
+ if (language !== currentLang) {
73
+ const saved = setLanguage(language);
74
+ clearTranslationCache();
75
+ notifyResult(saved, t().language.changed, t().language.changedSessionOnly);
76
+ }
77
+ continue;
78
+ }
79
+ if (action === 'icon') {
80
+ const mode = await select({
81
+ message: trans.theme.chooseIconMode,
82
+ options: [
83
+ { value: 'auto', label: trans.theme.modeAuto, hint: prefs.iconMode === 'auto' ? trans.common.current : undefined },
84
+ { value: 'ascii', label: trans.theme.modeAscii, hint: prefs.iconMode === 'ascii' ? trans.common.current : undefined },
85
+ { value: 'unicode', label: trans.theme.modeUnicode, hint: prefs.iconMode === 'unicode' ? trans.common.current : undefined },
86
+ ],
87
+ initialValue: prefs.iconMode,
88
+ });
89
+ if (isCancel(mode))
90
+ continue;
91
+ const saved = setIconMode(mode);
92
+ resetIconCache();
93
+ notifyResult(saved, trans.theme.updated, trans.theme.updatedSessionOnly);
94
+ continue;
95
+ }
96
+ if (action === 'color') {
97
+ const mode = await select({
98
+ message: trans.theme.chooseColorMode,
99
+ options: [
100
+ { value: 'auto', label: trans.theme.modeAuto, hint: prefs.colorMode === 'auto' ? trans.common.current : undefined },
101
+ { value: 'on', label: trans.theme.modeOn, hint: prefs.colorMode === 'on' ? trans.common.current : undefined },
102
+ { value: 'off', label: trans.theme.modeOff, hint: prefs.colorMode === 'off' ? trans.common.current : undefined },
103
+ ],
104
+ initialValue: prefs.colorMode,
105
+ });
106
+ if (isCancel(mode))
107
+ continue;
108
+ const saved = setColorMode(mode);
109
+ applyColorModePreference(false);
110
+ notifyResult(saved, trans.theme.updated, trans.theme.updatedSessionOnly);
111
+ continue;
112
+ }
113
+ if (action === 'reset') {
114
+ const saved = resetPreferences();
115
+ resetIconCache();
116
+ applyColorModePreference(false);
117
+ notifyResult(saved, trans.theme.reset, trans.theme.resetSessionOnly);
118
+ }
119
+ }
120
+ }
@@ -1,33 +1,39 @@
1
- import axios from 'axios';
2
1
  import chalk from 'chalk';
3
- import { URLS } from '../config/data.js';
2
+ import { APP_INFO, URLS } from '../config/data.js';
4
3
  import { pickIcon } from '../core/icons.js';
5
4
  import { padEndV } from '../core/text.js';
6
- import { createSpinner, success, warning } from '../core/ui.js';
5
+ import { createSpinner } from '../core/ui.js';
7
6
  import { t } from '../i18n/index.js';
8
- const SERVICE_TARGETS = [
9
- { name: 'Website', url: URLS.homepage },
10
- { name: 'Docs', url: URLS.docs },
11
- { name: 'Calendar', url: URLS.calendar },
12
- { name: 'GitHub', url: URLS.github },
13
- { name: 'Roadmap', url: URLS.roadmap },
14
- ];
7
+ function getServiceTargets() {
8
+ const trans = t();
9
+ return [
10
+ { name: trans.status.serviceWebsite, url: URLS.homepage },
11
+ { name: trans.status.serviceDocs, url: URLS.docs },
12
+ { name: trans.status.serviceCalendar, url: URLS.calendar },
13
+ { name: trans.status.serviceGithub, url: URLS.github },
14
+ { name: trans.status.serviceRoadmap, url: URLS.roadmap },
15
+ ];
16
+ }
15
17
  async function checkService(name, url, timeoutMs) {
16
18
  const start = Date.now();
17
19
  try {
18
- const response = await axios.get(url, {
19
- timeout: timeoutMs,
20
- maxRedirects: 5,
21
- validateStatus: () => true,
22
- headers: { 'User-Agent': 'NBTCA-CLI/2.4.0' },
20
+ const controller = new AbortController();
21
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
22
+ const response = await fetch(url, {
23
+ signal: controller.signal,
24
+ redirect: 'follow',
25
+ headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
23
26
  });
27
+ clearTimeout(timeout);
24
28
  const latencyMs = Date.now() - start;
25
29
  const ok = response.status >= 200 && response.status < 400;
26
30
  return { name, url, ok, statusCode: response.status, latencyMs };
27
31
  }
28
32
  catch (err) {
29
33
  const latencyMs = Date.now() - start;
30
- const error = err instanceof Error ? err.message : String(err);
34
+ const error = err instanceof Error
35
+ ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
36
+ : String(err);
31
37
  return { name, url, ok: false, latencyMs, error };
32
38
  }
33
39
  }
@@ -49,7 +55,7 @@ async function checkServiceWithRetry(name, url, timeoutMs, retries) {
49
55
  export async function checkServices(options = {}) {
50
56
  const timeoutMs = options.timeoutMs ?? 6000;
51
57
  const retries = options.retries ?? 1;
52
- return Promise.all(SERVICE_TARGETS.map((service) => checkServiceWithRetry(service.name, service.url, timeoutMs, retries)));
58
+ return Promise.all(getServiceTargets().map((service) => checkServiceWithRetry(service.name, service.url, timeoutMs, retries)));
53
59
  }
54
60
  export function serializeServiceStatus(items) {
55
61
  return items.map((item) => ({
@@ -87,7 +93,6 @@ export function renderServiceStatusTable(items, options) {
87
93
  const trans = t();
88
94
  const nameWidth = 10;
89
95
  const statusWidth = 9;
90
- const codeWidth = 7;
91
96
  const latencyWidth = 10;
92
97
  const h = pickIcon('─', '-');
93
98
  const v = pickIcon('│', '|');
@@ -100,19 +105,17 @@ export function renderServiceStatusTable(items, options) {
100
105
  const bottomLeft = pickIcon('└', '+');
101
106
  const bottomMid = pickIcon('┴', '+');
102
107
  const bottomRight = pickIcon('┘', '+');
103
- const top = `${topLeft}${h.repeat(nameWidth + 2)}${topMid}${h.repeat(statusWidth + 2)}${topMid}${h.repeat(codeWidth + 2)}${topMid}${h.repeat(latencyWidth + 2)}${topMid}${h.repeat(34)}${topRight}`;
104
- const divider = `${midLeft}${h.repeat(nameWidth + 2)}${midMid}${h.repeat(statusWidth + 2)}${midMid}${h.repeat(codeWidth + 2)}${midMid}${h.repeat(latencyWidth + 2)}${midMid}${h.repeat(34)}${midRight}`;
105
- const bottom = `${bottomLeft}${h.repeat(nameWidth + 2)}${bottomMid}${h.repeat(statusWidth + 2)}${bottomMid}${h.repeat(codeWidth + 2)}${bottomMid}${h.repeat(latencyWidth + 2)}${bottomMid}${h.repeat(34)}${bottomRight}`;
106
- const header = `${v} ${padEndV(trans.status.service, nameWidth)} ${v} ${padEndV(trans.status.health, statusWidth)} ${v} ${padEndV(trans.status.code, codeWidth)} ${v} ${padEndV(trans.status.latency, latencyWidth)} ${v} ${padEndV(trans.status.url, 34)} ${v}`;
108
+ const top = `${topLeft}${h.repeat(nameWidth + 2)}${topMid}${h.repeat(statusWidth + 2)}${topMid}${h.repeat(latencyWidth + 2)}${topRight}`;
109
+ const divider = `${midLeft}${h.repeat(nameWidth + 2)}${midMid}${h.repeat(statusWidth + 2)}${midMid}${h.repeat(latencyWidth + 2)}${midRight}`;
110
+ const bottom = `${bottomLeft}${h.repeat(nameWidth + 2)}${bottomMid}${h.repeat(statusWidth + 2)}${bottomMid}${h.repeat(latencyWidth + 2)}${bottomRight}`;
111
+ const header = `${v} ${padEndV(trans.status.service, nameWidth)} ${v} ${padEndV(trans.status.health, statusWidth)} ${v} ${padEndV(trans.status.latency, latencyWidth)} ${v}`;
107
112
  const lines = [dim(top), header, dim(divider)];
108
113
  for (const item of items) {
109
114
  const statusLabel = item.ok
110
115
  ? green(`${pickIcon('●', 'OK')} ${trans.status.up}`)
111
116
  : red(`${pickIcon('●', '!!')} ${trans.status.down}`);
112
- const code = item.statusCode ? String(item.statusCode) : '-';
113
117
  const latency = item.latencyMs != null ? `${item.latencyMs}ms` : '-';
114
- const url = item.url.length > 34 ? `${item.url.slice(0, 31)}...` : item.url;
115
- lines.push(`${v} ${padEndV(cyan(item.name), nameWidth)} ${v} ${padEndV(statusLabel, statusWidth)} ${v} ${padEndV(code, codeWidth)} ${v} ${padEndV(latency, latencyWidth)} ${v} ${padEndV(url, 34)} ${v}`);
118
+ lines.push(`${v} ${padEndV(cyan(item.name), nameWidth)} ${v} ${padEndV(statusLabel, statusWidth)} ${v} ${padEndV(latency, latencyWidth)} ${v}`);
116
119
  }
117
120
  lines.push(dim(bottom));
118
121
  return lines.join('\n');
@@ -129,12 +132,5 @@ export async function showServiceStatus() {
129
132
  spinner.stop(trans.status.summaryOk);
130
133
  }
131
134
  console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
132
- if (hasFailures) {
133
- warning(trans.status.summaryFail);
134
- }
135
- else {
136
- success(trans.status.summaryOk);
137
- }
138
135
  return items;
139
136
  }
140
- //# sourceMappingURL=status.js.map
@@ -1,117 +1,25 @@
1
- import chalk from 'chalk';
2
- import { isCancel, note, select } from '@clack/prompts';
1
+ /**
2
+ * Theme CLI command handler (non-interactive)
3
+ */
3
4
  import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../config/preferences.js';
4
- import { pickIcon } from '../core/icons.js';
5
- import { success, warning } from '../core/ui.js';
5
+ import { resetIconCache } from '../core/icons.js';
6
6
  import { t } from '../i18n/index.js';
7
7
  const ICON_MODES = ['auto', 'ascii', 'unicode'];
8
8
  const COLOR_MODES = ['auto', 'on', 'off'];
9
- export function printThemeSummary() {
9
+ function formatThemeSummary() {
10
10
  const trans = t();
11
11
  const prefs = loadPreferences();
12
- const content = [
13
- `${chalk.dim(trans.theme.iconMode + ':')} ${prefs.iconMode}`,
14
- `${chalk.dim(trans.theme.colorMode + ':')} ${prefs.colorMode}`,
15
- '',
16
- chalk.dim('NBTCA_ICON_MODE / NBTCA_COLOR_MODE can override saved preferences'),
17
- ].join('\n');
18
- note(content, trans.theme.current);
19
- }
20
- function notifyThemeChange(saved, successMessage, warningMessage) {
21
- if (saved) {
22
- success(successMessage);
23
- }
24
- else {
25
- warning(warningMessage);
26
- }
27
- }
28
- export async function showThemeMenu() {
29
- while (true) {
30
- const trans = t();
31
- const prefs = loadPreferences();
32
- const action = await select({
33
- message: trans.theme.chooseAction,
34
- options: [
35
- {
36
- value: 'summary',
37
- label: `${pickIcon('ℹ️', '[i]')} ${trans.theme.current}`,
38
- hint: `${trans.theme.iconMode}: ${prefs.iconMode} | ${trans.theme.colorMode}: ${prefs.colorMode}`,
39
- },
40
- {
41
- value: 'icon',
42
- label: `${pickIcon('🎨', '[*]')} ${trans.theme.iconMode}`,
43
- hint: prefs.iconMode,
44
- },
45
- {
46
- value: 'color',
47
- label: `${pickIcon('🖌️', '[*]')} ${trans.theme.colorMode}`,
48
- hint: prefs.colorMode,
49
- },
50
- {
51
- value: 'reset',
52
- label: `${pickIcon('♻️', '[r]')} ${trans.theme.reset}`,
53
- },
54
- {
55
- value: 'back',
56
- label: `${pickIcon('←', '[^]')} ${trans.theme.backToMenu}`,
57
- },
58
- ],
59
- });
60
- if (isCancel(action) || action === 'back')
61
- return;
62
- if (action === 'summary') {
63
- printThemeSummary();
64
- continue;
65
- }
66
- if (action === 'icon') {
67
- const mode = await select({
68
- message: trans.theme.chooseIconMode,
69
- options: [
70
- { value: 'auto', label: trans.theme.modeAuto, hint: prefs.iconMode === 'auto' ? `${pickIcon('✓', '*')} current` : undefined },
71
- { value: 'ascii', label: trans.theme.modeAscii, hint: prefs.iconMode === 'ascii' ? `${pickIcon('✓', '*')} current` : undefined },
72
- { value: 'unicode', label: trans.theme.modeUnicode, hint: prefs.iconMode === 'unicode' ? `${pickIcon('✓', '*')} current` : undefined },
73
- ],
74
- initialValue: prefs.iconMode,
75
- });
76
- if (isCancel(mode))
77
- continue;
78
- const saved = setIconMode(mode);
79
- notifyThemeChange(saved, trans.theme.updated, trans.theme.updatedSessionOnly);
80
- continue;
81
- }
82
- if (action === 'color') {
83
- const mode = await select({
84
- message: trans.theme.chooseColorMode,
85
- options: [
86
- { value: 'auto', label: trans.theme.modeAuto, hint: prefs.colorMode === 'auto' ? `${pickIcon('✓', '*')} current` : undefined },
87
- { value: 'on', label: trans.theme.modeOn, hint: prefs.colorMode === 'on' ? `${pickIcon('✓', '*')} current` : undefined },
88
- { value: 'off', label: trans.theme.modeOff, hint: prefs.colorMode === 'off' ? `${pickIcon('✓', '*')} current` : undefined },
89
- ],
90
- initialValue: prefs.colorMode,
91
- });
92
- if (isCancel(mode))
93
- continue;
94
- const saved = setColorMode(mode);
95
- applyColorModePreference(false);
96
- notifyThemeChange(saved, trans.theme.updated, trans.theme.updatedSessionOnly);
97
- continue;
98
- }
99
- if (action === 'reset') {
100
- const saved = resetPreferences();
101
- applyColorModePreference(false);
102
- notifyThemeChange(saved, trans.theme.reset, trans.theme.resetSessionOnly);
103
- }
104
- }
12
+ return `${trans.theme.iconMode}: ${prefs.iconMode}, ${trans.theme.colorMode}: ${prefs.colorMode}`;
105
13
  }
106
14
  export function runThemeCommand(args) {
107
15
  const trans = t();
108
16
  const [scope, value] = args;
109
17
  if (!scope) {
110
- printThemeSummary();
111
- return { ok: true, message: '' };
18
+ return { ok: true, message: formatThemeSummary() };
112
19
  }
113
20
  if (scope === 'reset') {
114
21
  const saved = resetPreferences();
22
+ resetIconCache();
115
23
  applyColorModePreference(false);
116
24
  const message = saved ? trans.theme.reset : trans.theme.resetSessionOnly;
117
25
  return { ok: true, message };
@@ -122,6 +30,7 @@ export function runThemeCommand(args) {
122
30
  return { ok: false, message: `${trans.theme.invalidValue} auto, ascii, unicode` };
123
31
  }
124
32
  const saved = setIconMode(mode);
33
+ resetIconCache();
125
34
  return { ok: true, message: saved ? trans.theme.updated : trans.theme.updatedSessionOnly };
126
35
  }
127
36
  if (scope === 'color') {
@@ -135,4 +44,3 @@ export function runThemeCommand(args) {
135
44
  }
136
45
  return { ok: false, message: trans.theme.usage };
137
46
  }
138
- //# sourceMappingURL=theme.js.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Version update checker
3
+ * Non-blocking check against npm registry for newer versions.
4
+ */
5
+ import chalk from 'chalk';
6
+ import { APP_INFO } from '../config/data.js';
7
+ import { t, fmt } from '../i18n/index.js';
8
+ const NPM_REGISTRY_URL = `https://registry.npmjs.org/@nbtca/prompt/latest`;
9
+ /**
10
+ * Fetch latest version from npm registry.
11
+ * Returns null on any failure (network, timeout, parse).
12
+ */
13
+ async function fetchLatestVersion() {
14
+ try {
15
+ const controller = new AbortController();
16
+ const timeout = setTimeout(() => controller.abort(), 3000);
17
+ const res = await fetch(NPM_REGISTRY_URL, {
18
+ signal: controller.signal,
19
+ headers: { 'Accept': 'application/json' },
20
+ });
21
+ clearTimeout(timeout);
22
+ if (!res.ok)
23
+ return null;
24
+ const data = (await res.json());
25
+ return data.version ?? null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /**
32
+ * Compare semver strings. Returns true if remote > local.
33
+ */
34
+ function isNewer(local, remote) {
35
+ const parse = (v) => v.split('.').map(Number);
36
+ const l = parse(local);
37
+ const r = parse(remote);
38
+ for (let i = 0; i < 3; i++) {
39
+ if ((r[i] ?? 0) > (l[i] ?? 0))
40
+ return true;
41
+ if ((r[i] ?? 0) < (l[i] ?? 0))
42
+ return false;
43
+ }
44
+ return false;
45
+ }
46
+ /**
47
+ * Non-blocking update check for TUI startup.
48
+ * Resolves to a notification string or null.
49
+ */
50
+ export async function checkForUpdate() {
51
+ const latest = await fetchLatestVersion();
52
+ if (!latest || !isNewer(APP_INFO.version, latest))
53
+ return null;
54
+ const trans = t();
55
+ return `${fmt(trans.update.available, { latest, current: APP_INFO.version })} ${chalk.dim(trans.update.command)}`;
56
+ }
57
+ /**
58
+ * Explicit update check command (nbtca update).
59
+ */
60
+ export async function runUpdateCheck() {
61
+ const trans = t();
62
+ const latest = await fetchLatestVersion();
63
+ if (!latest) {
64
+ console.log(chalk.yellow(trans.update.checkFailed));
65
+ return;
66
+ }
67
+ if (isNewer(APP_INFO.version, latest)) {
68
+ console.log(chalk.yellow(`${fmt(trans.update.available, { latest, current: APP_INFO.version })}`));
69
+ console.log(chalk.dim(trans.update.command));
70
+ }
71
+ else {
72
+ console.log(chalk.green(`${fmt(trans.update.upToDate, { version: APP_INFO.version })}`));
73
+ }
74
+ }
@@ -6,6 +6,7 @@ import fs from 'fs';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { dirname } from 'path';
9
+ import { getConfigDir, getWritableConfigDir } from '../config/paths.js';
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
11
12
  /**
@@ -13,17 +14,16 @@ const __dirname = dirname(__filename);
13
14
  */
14
15
  let currentLanguage = 'zh'; // Default to Chinese
15
16
  /**
16
- * Get configuration directory path
17
+ * Get language configuration file path (read, with legacy fallback)
17
18
  */
18
- function getConfigDir() {
19
- const homeDir = process.env['HOME'] || process.env['USERPROFILE'] || '';
20
- return path.join(homeDir, '.nbtca');
19
+ function getLanguageConfigPath() {
20
+ return path.join(getConfigDir(), 'language.json');
21
21
  }
22
22
  /**
23
- * Get language configuration file path
23
+ * Get writable language configuration file path (XDG, creates dir)
24
24
  */
25
- function getLanguageConfigPath() {
26
- return path.join(getConfigDir(), 'language.json');
25
+ function getWritableLanguageConfigPath() {
26
+ return path.join(getWritableConfigDir(), 'language.json');
27
27
  }
28
28
  /**
29
29
  * Load language preference from config file
@@ -31,15 +31,13 @@ function getLanguageConfigPath() {
31
31
  export function loadLanguagePreference() {
32
32
  try {
33
33
  const configPath = getLanguageConfigPath();
34
- if (fs.existsSync(configPath)) {
35
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
36
- if (config.language === 'zh' || config.language === 'en') {
37
- currentLanguage = config.language;
38
- }
34
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
35
+ if (config.language === 'zh' || config.language === 'en') {
36
+ currentLanguage = config.language;
39
37
  }
40
38
  }
41
- catch (err) {
42
- // If loading fails, use default (Chinese)
39
+ catch {
40
+ // If loading fails (file missing or invalid), use default (Chinese)
43
41
  }
44
42
  return currentLanguage;
45
43
  }
@@ -48,16 +46,12 @@ export function loadLanguagePreference() {
48
46
  */
49
47
  export function saveLanguagePreference(language) {
50
48
  try {
51
- const configDir = getConfigDir();
52
- if (!fs.existsSync(configDir)) {
53
- fs.mkdirSync(configDir, { recursive: true });
54
- }
55
- const configPath = getLanguageConfigPath();
49
+ const configPath = getWritableLanguageConfigPath();
56
50
  fs.writeFileSync(configPath, JSON.stringify({ language }, null, 2));
57
51
  currentLanguage = language;
58
52
  return true;
59
53
  }
60
- catch (err) {
54
+ catch {
61
55
  return false;
62
56
  }
63
57
  }
@@ -103,6 +97,12 @@ export function t() {
103
97
  }
104
98
  return translationsCache.get(currentLanguage);
105
99
  }
100
+ export function fmt(template, vars) {
101
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
102
+ const val = vars[key];
103
+ return val !== undefined ? String(val) : `{${key}}`;
104
+ });
105
+ }
106
106
  /**
107
107
  * Clear translation cache (useful when switching languages)
108
108
  */
@@ -111,4 +111,3 @@ export function clearTranslationCache() {
111
111
  }
112
112
  // Initialize language preference on module load
113
113
  loadLanguagePreference();
114
- //# sourceMappingURL=index.js.map