@nbtca/prompt 1.4.2 → 1.5.1

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 (71) hide show
  1. package/README.md +27 -58
  2. package/SECURITY.md +16 -45
  3. package/dist/app/app.js +167 -64
  4. package/dist/app/chrome.js +67 -50
  5. package/dist/app/fields/list-field.js +12 -25
  6. package/dist/app/fields/text-field.js +3 -8
  7. package/dist/app/frame.js +16 -23
  8. package/dist/app/keys.js +110 -2
  9. package/dist/app/views/docs-render.js +34 -26
  10. package/dist/app/views/docs.js +280 -68
  11. package/dist/app/views/events-render.js +21 -27
  12. package/dist/app/views/events.js +57 -33
  13. package/dist/app/views/home.js +67 -47
  14. package/dist/app/views/schedule-grid-cursor.js +9 -18
  15. package/dist/app/views/schedule-render.js +51 -74
  16. package/dist/app/views/schedule.js +246 -101
  17. package/dist/app/views/settings-render.js +8 -19
  18. package/dist/app/views/settings.js +92 -17
  19. package/dist/auth/cookie-transport.js +31 -32
  20. package/dist/auth/errors.js +3 -1
  21. package/dist/auth/nbt-auth.js +42 -25
  22. package/dist/auth/session-store.js +17 -9
  23. package/dist/cli.js +570 -0
  24. package/dist/config/data.js +9 -11
  25. package/dist/config/preferences.js +21 -7
  26. package/dist/core/calendar-day.js +37 -0
  27. package/dist/core/canvas.js +1 -0
  28. package/dist/core/capabilities.js +6 -3
  29. package/dist/core/components/confirm.js +9 -8
  30. package/dist/core/components/menu.js +64 -38
  31. package/dist/core/components/messages.js +12 -4
  32. package/dist/core/components/painter.js +3 -1
  33. package/dist/core/components/spinner.js +34 -7
  34. package/dist/core/components/text-input.js +24 -18
  35. package/dist/core/icons.js +2 -2
  36. package/dist/core/logo.js +23 -5
  37. package/dist/core/motion.js +25 -19
  38. package/dist/core/text.js +186 -69
  39. package/dist/core/theme.js +0 -28
  40. package/dist/core/transitions.js +2 -2
  41. package/dist/core/ui.js +15 -13
  42. package/dist/core/vim-keys.js +156 -19
  43. package/dist/features/about.js +23 -0
  44. package/dist/features/calendar-heatmap.js +16 -40
  45. package/dist/features/calendar-query.js +1 -2
  46. package/dist/features/calendar-store.js +27 -0
  47. package/dist/features/calendar.js +66 -190
  48. package/dist/features/docs-client.js +225 -0
  49. package/dist/features/docs.js +615 -298
  50. package/dist/features/links.js +44 -29
  51. package/dist/features/schedule-render.js +65 -101
  52. package/dist/features/schedule-store.js +51 -9
  53. package/dist/features/schedule-view.js +46 -213
  54. package/dist/features/status.js +117 -60
  55. package/dist/features/student-timetable.js +74 -97
  56. package/dist/features/theme.js +9 -5
  57. package/dist/features/timetable-sanitize.js +40 -0
  58. package/dist/features/update.js +12 -29
  59. package/dist/i18n/index.js +83 -19
  60. package/dist/i18n/locales/en.json +8 -3
  61. package/dist/i18n/locales/zh.json +8 -3
  62. package/dist/index.js +6 -474
  63. package/dist/logo/ca-dotmatrix.txt +16 -18
  64. package/dist/main.js +7 -48
  65. package/package.json +28 -18
  66. package/bin/nbtca-welcome.js +0 -2
  67. package/dist/core/components/screen.js +0 -18
  68. package/dist/core/menu.js +0 -68
  69. package/dist/features/schedule-query.js +0 -47
  70. package/dist/features/settings.js +0 -127
  71. package/dist/logo/ca-logo.png +0 -0
@@ -1,212 +1,57 @@
1
- import path from 'node:path';
2
- import { createNbtTimetableClient, timetableToIcs, } from '@nbtca/nbtcal/timetable';
3
- import { runMenu, menuFooter } from '../core/components/menu.js';
4
- import { runTextInput } from '../core/components/text-input.js';
5
- import { enterScreen, breadcrumb } from '../core/transitions.js';
6
- import { createSpinner, success, error } from '../core/ui.js';
7
- import { c, type, space } from '../core/theme.js';
8
- import { t, fmt } from '../i18n/index.js';
9
- import { createSessionStore } from '../auth/session-store.js';
10
- import { withAuthenticatedSession, resolveTerm, relevantTerms, writePrivateIcs, isSessionExpired, safeMessage, JWXT_ORIGIN, } from './student-timetable.js';
11
- import { currentWeekNumber, campusWeekday, meetingsOnDay, nextMeeting } from './schedule-query.js';
12
- import { renderNextClassBanner, renderTodayClasses, renderTodayTimeline, renderWeekGrid } from './schedule-render.js';
13
- import { termKey, loadWeekOne, saveWeekOne, saveTimetableCache, saveCurrentPointer, loadCurrentPointer, loadTimetableCache, clearScheduleCache, } from './schedule-store.js';
14
- /** Loads a saved week-one Monday for `key`, or prompts for and persists a new one.
15
- * Returns null when the user cancels or enters an unparsable date (caller aborts). */
16
- async function ensureWeekOne(key) {
17
- const saved = loadWeekOne(key);
18
- if (saved)
19
- return saved;
20
- const value = await runTextInput({
21
- message: t().timetable.promptWeekOne,
22
- placeholder: 'YYYY-MM-DD',
23
- });
24
- if (value === null)
1
+ import { createTimetableSchedule } from '@nbtca/nbtcal/timetable';
2
+ import { addLocalDays, parseLocalMonday } from '../core/calendar-day.js';
3
+ import { renderNextClassBanner, renderTodayTimeline } from './schedule-render.js';
4
+ import { loadCurrentPointer, loadTimetableCache } from './schedule-store.js';
5
+ import { sanitizeTimetable } from './timetable-sanitize.js';
6
+ const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
7
+ function loadCachedTimetable() {
8
+ const pointer = loadCurrentPointer();
9
+ if (!pointer)
25
10
  return null;
26
- const trimmed = value.trim();
27
- if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed))
11
+ const value = loadTimetableCache(pointer.termKey);
12
+ if (!value || typeof value !== 'object')
28
13
  return null;
29
- if (Number.isNaN(new Date(`${trimmed}T00:00:00`).getTime()))
30
- return null;
31
- saveWeekOne(key, trimmed);
32
- return trimmed;
33
- }
34
- /** Fetches a term's timetable behind a spinner, caching it on success. Returns null
35
- * (after reporting the error) on failure so callers can keep the previous state. */
36
- async function fetchTimetableWithSpinner(client, term, key, weekOne) {
37
- const trans = t();
38
- const spinner = createSpinner(trans.calendar.loading);
39
- try {
40
- const tt = await client.fetchTerm(term);
41
- spinner.stop();
42
- saveTimetableCache(key, tt);
43
- saveCurrentPointer(key, weekOne);
44
- return tt;
45
- }
46
- catch (err) {
47
- if (isSessionExpired(err)) {
48
- spinner.stop();
49
- throw err;
50
- }
51
- spinner.error(trans.timetable.genericError);
14
+ const candidate = value;
15
+ if (!Array.isArray(candidate.meetings) ||
16
+ !Array.isArray(candidate.periods) ||
17
+ !Array.isArray(candidate.unresolvedItems) ||
18
+ !Array.isArray(candidate.calendarDays) ||
19
+ !Array.isArray(candidate.warnings)) {
52
20
  return null;
53
21
  }
54
- }
55
- function renderHub(tt, weekOne, now) {
56
- const week = currentWeekNumber(weekOne, now);
57
- const today = meetingsOnDay(tt.meetings, campusWeekday(now), week);
58
- const trans = t();
59
- console.log();
60
- const banner = renderNextClassBanner(nextMeeting(tt.meetings, tt.periods, weekOne, now), now);
61
- console.log(banner || `${space.indent}${type.hint(trans.timetable.noNextClass)}`);
62
- console.log();
63
- console.log(renderTodayClasses(today, tt.periods, now));
64
- console.log();
65
- return { week, today };
66
- }
67
- async function switchTerm(client, catalog) {
68
- const trans = t();
69
- const picked = await runMenu({
70
- title: trans.timetable.hubSwitchTerm,
71
- options: relevantTerms(catalog).map((tm) => ({
72
- value: `${tm.academicYear}:${tm.semester}`,
73
- label: tm.academicYearLabel,
74
- hint: tm.current ? trans.common.current : undefined,
75
- })),
76
- footer: menuFooter(),
77
- });
78
- if (picked === null)
22
+ const fetchedAt = new Date(candidate.fetchedAt);
23
+ if (Number.isNaN(fetchedAt.getTime()))
79
24
  return null;
80
- const term = resolveTerm(catalog, picked);
81
- const key = termKey(term);
82
- const weekOne = await ensureWeekOne(key);
83
- if (!weekOne)
84
- return null;
85
- const tt = await fetchTimetableWithSpinner(client, term, key, weekOne);
86
- if (!tt)
87
- return null;
88
- return { term, key, weekOne, tt };
89
- }
90
- function exportTimetable(tt, term, key, weekOne) {
91
- const trans = t();
92
- const ics = timetableToIcs(tt, { weekOneMonday: weekOne, calendarName: `NBT ${term.academicYearLabel}` });
93
- const out = `timetable-${key}.ics`;
94
- try {
95
- writePrivateIcs(out, ics);
96
- success(fmt(trans.timetable.exported, { count: tt.meetings.length, file: path.resolve(out) }));
97
- }
98
- catch {
99
- error(trans.timetable.genericError);
100
- }
101
- }
102
- /** Interactive schedule hub: login/restore, resolve the current term, load or prompt
103
- * for the week-one Monday, fetch the timetable, then loop a menu of today / week-grid /
104
- * switch-term / export actions until the user cancels. */
105
- export async function showSchedule() {
106
- const trans = t();
107
- await enterScreen(breadcrumb(trans.timetable.menuEntry));
108
- try {
109
- await withAuthenticatedSession(async (session) => {
110
- const client = createNbtTimetableClient(session.timetableTransport, { baseUrl: JWXT_ORIGIN });
111
- const catalog = await client.listTerms();
112
- let term = resolveTerm(catalog);
113
- let key = termKey(term);
114
- let weekOne = await ensureWeekOne(key);
115
- if (!weekOne)
116
- return 0;
117
- const initial = await fetchTimetableWithSpinner(client, term, key, weekOne);
118
- if (!initial)
119
- return 1;
120
- let tt = initial;
121
- while (true) {
122
- const now = new Date();
123
- const { week, today } = renderHub(tt, weekOne, now);
124
- const action = await runMenu({
125
- title: `${trans.timetable.menuEntry} ${c.muted(term.academicYearLabel)} ${c.muted(trans.timetable.weekLabel + String(week))}`,
126
- options: [
127
- { value: 'today', label: trans.timetable.hubToday, hint: String(today.length) },
128
- { value: 'week', label: trans.timetable.hubWeek },
129
- { value: 'term', label: trans.timetable.hubSwitchTerm, hint: term.academicYearLabel },
130
- { value: 'export', label: trans.timetable.hubExport },
131
- { value: 'logout', label: trans.timetable.hubLogout },
132
- ],
133
- footer: menuFooter(),
134
- });
135
- if (action === null)
136
- return 0;
137
- if (action === 'today') {
138
- continue; // The next loop iteration repaints today's classes.
139
- }
140
- if (action === 'week') {
141
- console.log();
142
- console.log(renderWeekGrid(tt.meetings, tt.periods, week, now));
143
- console.log();
144
- continue;
145
- }
146
- if (action === 'term') {
147
- const switched = await switchTerm(client, catalog);
148
- if (!switched)
149
- continue;
150
- term = switched.term;
151
- key = switched.key;
152
- weekOne = switched.weekOne;
153
- tt = switched.tt;
154
- continue;
155
- }
156
- if (action === 'export') {
157
- exportTimetable(tt, term, key, weekOne);
158
- continue;
159
- }
160
- if (action === 'logout') {
161
- createSessionStore().clear();
162
- clearScheduleCache();
163
- return 0;
164
- }
165
- }
166
- }, {
167
- oneShot: false,
168
- isInteractive: true,
169
- store: createSessionStore(),
170
- stderr: process.stderr,
171
- });
172
- }
173
- catch (err) {
174
- error(safeMessage(err));
175
- }
25
+ return {
26
+ timetable: sanitizeTimetable({ ...candidate, fetchedAt }),
27
+ weekOneMonday: pointer.weekOneMonday,
28
+ };
176
29
  }
177
- /** Best-effort, cache-only startup line: reads the last-used term pointer and its
178
- * cached timetable (no network) and renders the next-class banner, or '' if the
179
- * student isn't "set up" yet or anything about the cache is missing/corrupt. */
180
30
  export function peekNextClassLine(now = new Date()) {
181
31
  try {
182
- const ptr = loadCurrentPointer();
183
- if (!ptr)
32
+ const cached = loadCachedTimetable();
33
+ if (!cached)
184
34
  return '';
185
- const cached = loadTimetableCache(ptr.termKey);
186
- if (!cached || !Array.isArray(cached.meetings) || !Array.isArray(cached.periods))
187
- return '';
188
- const next = nextMeeting(cached.meetings, cached.periods, ptr.weekOneMonday, now);
189
- return renderNextClassBanner(next, now);
35
+ const schedule = createTimetableSchedule(cached.timetable, {
36
+ weekOneMonday: cached.weekOneMonday,
37
+ });
38
+ return renderNextClassBanner(schedule.next(now), now);
190
39
  }
191
40
  catch {
192
41
  return '';
193
42
  }
194
43
  }
195
- /** Cache-only (no network) render of today's classes for the current term, or
196
- * [] if not set up. Uses the same renderTodayTimeline Schedule's own hub
197
- * renders, so Home's preview and Schedule's detail view show the exact same
198
- * visual language for the exact same data instead of drifting apart. */
199
44
  export function peekTodayLines(now = new Date()) {
200
45
  try {
201
- const ptr = loadCurrentPointer();
202
- if (!ptr)
203
- return [];
204
- const cached = loadTimetableCache(ptr.termKey);
205
- if (!cached || !Array.isArray(cached.meetings) || !Array.isArray(cached.periods))
46
+ const cached = loadCachedTimetable();
47
+ if (!cached)
206
48
  return [];
207
- const week = currentWeekNumber(ptr.weekOneMonday, now);
208
- const today = meetingsOnDay(cached.meetings, campusWeekday(now), week);
209
- return renderTodayTimeline(today, cached.periods, now).split('\n');
49
+ const schedule = createTimetableSchedule(cached.timetable, {
50
+ weekOneMonday: cached.weekOneMonday,
51
+ });
52
+ const week = schedule.weekAt(now);
53
+ const today = schedule.meetingsOnDay(week, schedule.weekdayAt(now));
54
+ return renderTodayTimeline(today, cached.timetable.periods, now).split('\n');
210
55
  }
211
56
  catch {
212
57
  return [];
@@ -214,38 +59,26 @@ export function peekTodayLines(now = new Date()) {
214
59
  }
215
60
  export function peekWeekAheadInfo(now = new Date()) {
216
61
  try {
217
- const ptr = loadCurrentPointer();
218
- if (!ptr)
219
- return null;
220
- const cached = loadTimetableCache(ptr.termKey);
221
- if (!cached || !Array.isArray(cached.meetings))
62
+ const cached = loadCachedTimetable();
63
+ if (!cached)
222
64
  return null;
223
- const week = currentWeekNumber(ptr.weekOneMonday, now);
65
+ const schedule = createTimetableSchedule(cached.timetable, {
66
+ weekOneMonday: cached.weekOneMonday,
67
+ });
68
+ const week = schedule.weekAt(now);
224
69
  if (week < 1)
225
70
  return null;
226
- const meetings = cached.meetings;
227
- const classDays = [1, 2, 3, 4, 5, 6, 7].map((wd) => meetingsOnDay(meetings, wd, week).length > 0);
228
- const base = new Date(`${ptr.weekOneMonday}T00:00:00`);
229
- const weekStartDate = new Date(base.getTime() + (week - 1) * 7 * 86400000);
71
+ const classDays = WEEKDAYS.map((weekday) => schedule.meetingsOnDay(week, weekday).length > 0);
72
+ const weekStartDate = addLocalDays(parseLocalMonday(cached.weekOneMonday), (week - 1) * 7);
230
73
  return { weekStartDate, classDays };
231
74
  }
232
75
  catch {
233
76
  return null;
234
77
  }
235
78
  }
236
- /** Best-effort, cache-only (no network) read of how many timetable items
237
- * still need the student's attention — the same data buildHubField()
238
- * (schedule.ts) already surfaces inside Schedule's own hub menu, exposed
239
- * here so Home can show it too without a second source of truth. */
240
79
  export function peekUnresolvedCount() {
241
80
  try {
242
- const ptr = loadCurrentPointer();
243
- if (!ptr)
244
- return 0;
245
- const cached = loadTimetableCache(ptr.termKey);
246
- if (!cached || !Array.isArray(cached.unresolvedItems))
247
- return 0;
248
- return cached.unresolvedItems.length;
81
+ return loadCachedTimetable()?.timetable.unresolvedItems.length ?? 0;
249
82
  }
250
83
  catch {
251
84
  return 0;
@@ -1,13 +1,69 @@
1
1
  import chalk from 'chalk';
2
2
  import { APP_INFO, URLS } from '../config/data.js';
3
- import { getCapabilities } from '../core/capabilities.js';
4
- import { createPainter } from '../core/components/painter.js';
5
3
  import { pickIcon } from '../core/icons.js';
6
- import { padEndV, visualWidth } from '../core/text.js';
4
+ import { padEndV, sanitizeTerminalLine, visualWidth } from '../core/text.js';
7
5
  import { c } from '../core/theme.js';
8
- import { createSpinner } from '../core/ui.js';
9
- import { enterScreen, breadcrumb } from '../core/transitions.js';
10
6
  import { t } from '../i18n/index.js';
7
+ function abortError(signal) {
8
+ return signal.reason instanceof Error
9
+ ? signal.reason
10
+ : new DOMException('The status check was aborted.', 'AbortError');
11
+ }
12
+ function cancelResponseBody(response) {
13
+ try {
14
+ const body = response?.body;
15
+ if (!body)
16
+ return;
17
+ void body.cancel().catch(() => {
18
+ // Cancellation is best-effort and must not delay status results.
19
+ });
20
+ }
21
+ catch {
22
+ // A custom transport may expose an inaccessible, locked, or closed body.
23
+ }
24
+ }
25
+ function raceWithSignal(operation, signal, onLateValue) {
26
+ return new Promise((resolve, reject) => {
27
+ let settled = false;
28
+ const cleanup = () => {
29
+ signal.removeEventListener('abort', onAbort);
30
+ };
31
+ const onAbort = () => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ cleanup();
36
+ reject(abortError(signal));
37
+ };
38
+ signal.addEventListener('abort', onAbort, { once: true });
39
+ void operation
40
+ .then((value) => {
41
+ if (settled) {
42
+ try {
43
+ onLateValue?.(value);
44
+ }
45
+ catch {
46
+ // Late cleanup is best-effort.
47
+ }
48
+ return;
49
+ }
50
+ settled = true;
51
+ cleanup();
52
+ resolve(value);
53
+ }, (error) => {
54
+ if (settled)
55
+ return;
56
+ settled = true;
57
+ cleanup();
58
+ reject(error instanceof Error ? error : new Error(String(error)));
59
+ })
60
+ .catch(() => {
61
+ // Both operation outcomes are handled above. This only guards cleanup code.
62
+ });
63
+ if (signal.aborted)
64
+ onAbort();
65
+ });
66
+ }
11
67
  function getServiceTargets() {
12
68
  const trans = t();
13
69
  return [
@@ -21,46 +77,73 @@ function getServiceTargets() {
21
77
  { name: trans.status.serviceMirror, url: URLS.mirror, group: 'intranet', intranet: true },
22
78
  ];
23
79
  }
24
- async function checkService(name, url, timeoutMs) {
80
+ async function checkService(name, url, timeoutMs, signal) {
25
81
  const start = Date.now();
82
+ const controller = new AbortController();
83
+ const onAbort = () => {
84
+ controller.abort(signal?.reason);
85
+ };
86
+ signal?.addEventListener('abort', onAbort, { once: true });
87
+ if (signal?.aborted)
88
+ onAbort();
89
+ const timeout = setTimeout(() => {
90
+ controller.abort();
91
+ }, timeoutMs);
92
+ let response;
26
93
  try {
27
- const controller = new AbortController();
28
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
29
- const response = await fetch(url, {
94
+ response = await raceWithSignal(fetch(url, {
30
95
  signal: controller.signal,
31
96
  redirect: 'follow',
32
97
  headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
33
- });
34
- clearTimeout(timeout);
98
+ }), controller.signal, cancelResponseBody);
99
+ if (signal?.aborted)
100
+ throw abortError(signal);
35
101
  const latencyMs = Date.now() - start;
36
102
  const ok = response.status >= 200 && response.status < 400;
37
103
  return { name, url, ok, statusCode: response.status, latencyMs };
38
104
  }
39
105
  catch (err) {
106
+ if (signal?.aborted)
107
+ throw abortError(signal);
40
108
  const latencyMs = Date.now() - start;
41
- const error = err instanceof Error
42
- ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
43
- : String(err);
109
+ const error = sanitizeTerminalLine(err instanceof Error
110
+ ? err.name === 'AbortError'
111
+ ? 'Request timed out'
112
+ : err.message
113
+ : String(err));
44
114
  return { name, url, ok: false, latencyMs, error };
45
115
  }
116
+ finally {
117
+ clearTimeout(timeout);
118
+ signal?.removeEventListener('abort', onAbort);
119
+ cancelResponseBody(response);
120
+ }
46
121
  }
47
- export async function checkServiceWithRetry(target, timeoutMs, retries) {
48
- let lastResult = await checkService(target.name, target.url, timeoutMs);
122
+ async function checkServiceWithRetry(target, timeoutMs, retries, signal) {
123
+ let lastResult = await checkService(target.name, target.url, timeoutMs, signal);
49
124
  if (!lastResult.ok) {
50
125
  for (let attempt = 0; attempt < retries; attempt++) {
51
126
  if (!lastResult.error && !(lastResult.statusCode != null && lastResult.statusCode >= 500))
52
127
  break;
53
- lastResult = await checkService(target.name, target.url, timeoutMs);
128
+ if (signal?.aborted)
129
+ throw abortError(signal);
130
+ lastResult = await checkService(target.name, target.url, timeoutMs, signal);
54
131
  if (lastResult.ok)
55
132
  break;
56
133
  }
57
134
  }
58
- return { ...lastResult, group: target.group, intranet: target.intranet };
135
+ return {
136
+ ...lastResult,
137
+ group: target.group,
138
+ ...(target.intranet === undefined ? {} : { intranet: target.intranet }),
139
+ };
59
140
  }
60
141
  export async function checkServices(options = {}) {
61
142
  const timeoutMs = options.timeoutMs ?? 6000;
62
143
  const retries = options.retries ?? 1;
63
- return Promise.all(getServiceTargets().map(t => checkServiceWithRetry(t, timeoutMs, retries)));
144
+ if (options.signal?.aborted)
145
+ throw abortError(options.signal);
146
+ return Promise.all(getServiceTargets().map((target) => checkServiceWithRetry(target, timeoutMs, retries, options.signal)));
64
147
  }
65
148
  export function serializeServiceStatus(items) {
66
149
  return items.map((item) => ({
@@ -75,12 +158,12 @@ export function serializeServiceStatus(items) {
75
158
  }));
76
159
  }
77
160
  export function hasServiceFailures(items) {
78
- return items.some(item => !item.ok && !item.intranet);
161
+ return items.some((item) => !item.ok && !item.intranet);
79
162
  }
80
163
  export function countServiceHealth(items) {
81
164
  let up = 0;
82
165
  let down = 0;
83
- for (const item of items.filter(i => !i.intranet)) {
166
+ for (const item of items.filter((i) => !i.intranet)) {
84
167
  if (item.ok)
85
168
  up++;
86
169
  else
@@ -97,7 +180,7 @@ export function renderServiceStatusTable(items, options) {
97
180
  const applyCyan = useColor ? chalk.cyan : id;
98
181
  const applyLatency = useColor ? c.latency : (ms) => `${ms}ms`;
99
182
  const trans = t();
100
- const nameWidth = Math.max(...items.map(i => visualWidth(i.name)), visualWidth(trans.status.service));
183
+ const nameWidth = Math.max(...items.map((i) => visualWidth(i.name)), visualWidth(trans.status.service));
101
184
  const statusWidth = 10;
102
185
  const onIcon = pickIcon('●', '+');
103
186
  const offIcon = pickIcon('✕', '!');
@@ -109,14 +192,22 @@ export function renderServiceStatusTable(items, options) {
109
192
  if (item.group !== currentGroup) {
110
193
  if (currentGroup !== undefined)
111
194
  lines.push('');
112
- const groupLabel = item.group === 'nbtca' ? trans.status.groupNbtca :
113
- item.group === 'external' ? trans.status.groupExternal :
114
- item.group === 'intranet' ? trans.status.groupIntranet : '';
195
+ const groupLabel = item.group === 'nbtca'
196
+ ? trans.status.groupNbtca
197
+ : item.group === 'external'
198
+ ? trans.status.groupExternal
199
+ : item.group === 'intranet'
200
+ ? trans.status.groupIntranet
201
+ : '';
115
202
  lines.push(` ${applyDim(groupLabel)}`);
116
203
  lines.push(` ${applyDim(sep.repeat(nameWidth + statusWidth + 12))}`);
117
204
  currentGroup = item.group;
118
205
  }
119
- const nameCol = padEndV(item.pending ? applyDim(item.name) : (item.intranet ? applyDim(item.name) : applyCyan(item.name)), nameWidth);
206
+ const nameCol = padEndV(item.pending
207
+ ? applyDim(item.name)
208
+ : item.intranet
209
+ ? applyDim(item.name)
210
+ : applyCyan(item.name), nameWidth);
120
211
  let statusLabel;
121
212
  if (item.pending) {
122
213
  statusLabel = applyDim(pickIcon('…', '.'));
@@ -138,37 +229,3 @@ export function renderServiceStatusTable(items, options) {
138
229
  }
139
230
  return lines.join('\n');
140
231
  }
141
- export async function showServiceStatus() {
142
- const trans = t();
143
- await enterScreen(breadcrumb(trans.menu.status));
144
- const targets = getServiceTargets();
145
- if (getCapabilities().reducedMotion) {
146
- const spinner = createSpinner(trans.status.checking);
147
- const items = await checkServices();
148
- const hasFailures = hasServiceFailures(items);
149
- if (hasFailures)
150
- spinner.error(trans.status.summaryFail);
151
- else
152
- spinner.stop(trans.status.summaryOk);
153
- console.log();
154
- console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
155
- console.log();
156
- return items;
157
- }
158
- const items = targets.map((tg) => ({
159
- name: tg.name, url: tg.url, ok: false, group: tg.group, intranet: tg.intranet, pending: true,
160
- }));
161
- const paint = createPainter(() => renderServiceStatusTable(items, { color: getCapabilities().color }));
162
- console.log();
163
- paint();
164
- await Promise.all(targets.map(async (target, i) => {
165
- const status = await checkServiceWithRetry(target, 6000, 1);
166
- items[i] = { ...status, pending: false };
167
- paint();
168
- }));
169
- console.log('\n');
170
- const hasFailures = hasServiceFailures(items);
171
- console.log(hasFailures ? c.warn(trans.status.summaryFail) : c.success(trans.status.summaryOk));
172
- console.log();
173
- return items;
174
- }