@nbtca/prompt 1.4.1 → 1.5.0

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 (68) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +27 -58
  3. package/SECURITY.md +16 -45
  4. package/dist/app/app.js +53 -55
  5. package/dist/app/chrome.js +67 -50
  6. package/dist/app/fields/list-field.js +12 -25
  7. package/dist/app/fields/text-field.js +3 -8
  8. package/dist/app/frame.js +2 -21
  9. package/dist/app/keys.js +10 -2
  10. package/dist/app/views/docs-render.js +31 -24
  11. package/dist/app/views/docs.js +211 -67
  12. package/dist/app/views/events-render.js +19 -26
  13. package/dist/app/views/events.js +44 -31
  14. package/dist/app/views/home.js +33 -76
  15. package/dist/app/views/schedule-grid-cursor.js +9 -18
  16. package/dist/app/views/schedule-render.js +47 -71
  17. package/dist/app/views/schedule.js +158 -90
  18. package/dist/app/views/settings-render.js +8 -19
  19. package/dist/app/views/settings.js +93 -18
  20. package/dist/auth/cookie-transport.js +31 -32
  21. package/dist/auth/errors.js +3 -1
  22. package/dist/auth/nbt-auth.js +42 -25
  23. package/dist/auth/session-store.js +17 -9
  24. package/dist/config/data.js +10 -13
  25. package/dist/config/preferences.js +14 -7
  26. package/dist/core/calendar-day.js +37 -0
  27. package/dist/core/capabilities.js +6 -3
  28. package/dist/core/components/confirm.js +9 -8
  29. package/dist/core/components/menu.js +41 -16
  30. package/dist/core/components/messages.js +12 -4
  31. package/dist/core/components/painter.js +3 -1
  32. package/dist/core/components/spinner.js +17 -6
  33. package/dist/core/components/text-input.js +24 -18
  34. package/dist/core/icons.js +2 -2
  35. package/dist/core/logo.js +25 -21
  36. package/dist/core/motion.js +25 -19
  37. package/dist/core/text.js +182 -75
  38. package/dist/core/theme.js +0 -28
  39. package/dist/core/transitions.js +2 -2
  40. package/dist/core/ui.js +15 -30
  41. package/dist/core/vim-keys.js +9 -15
  42. package/dist/features/about.js +23 -0
  43. package/dist/features/calendar-heatmap.js +16 -40
  44. package/dist/features/calendar-query.js +1 -2
  45. package/dist/features/calendar.js +12 -185
  46. package/dist/features/docs.js +439 -320
  47. package/dist/features/schedule-render.js +65 -102
  48. package/dist/features/schedule-store.js +51 -9
  49. package/dist/features/schedule-view.js +46 -220
  50. package/dist/features/status.js +44 -59
  51. package/dist/features/student-timetable.js +73 -95
  52. package/dist/features/theme.js +6 -5
  53. package/dist/features/timetable-sanitize.js +40 -0
  54. package/dist/features/update.js +9 -37
  55. package/dist/i18n/index.js +87 -65
  56. package/dist/i18n/locales/en.json +1 -1
  57. package/dist/i18n/locales/zh.json +1 -1
  58. package/dist/index.js +85 -64
  59. package/dist/logo/ca-dotmatrix.txt +16 -18
  60. package/dist/main.js +7 -48
  61. package/package.json +30 -18
  62. package/bin/nbtca-welcome.js +0 -2
  63. package/dist/core/components/screen.js +0 -18
  64. package/dist/core/menu.js +0 -71
  65. package/dist/features/links.js +0 -39
  66. package/dist/features/schedule-query.js +0 -47
  67. package/dist/features/settings.js +0 -130
  68. package/dist/logo/ca-logo.png +0 -0
@@ -1,258 +1,84 @@
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 [];
213
58
  }
214
59
  }
215
- /** Best-effort, cache-only (no network) computation of this week's per-day
216
- * class signal, for Home's combined week-overview grid. Returns null when
217
- * there's no set-up personal timetable, the cache is unusable, or the term
218
- * hasn't started yet (current week < 1) — the same "before term start"
219
- * guard already fixed once for Schedule's own hub (a future-dated weekOne,
220
- * auto-inferred while on break, must not render a nonsensical negative
221
- * week's worth of content). */
222
60
  export function peekWeekAheadInfo(now = new Date()) {
223
61
  try {
224
- const ptr = loadCurrentPointer();
225
- if (!ptr)
226
- return null;
227
- const cached = loadTimetableCache(ptr.termKey);
228
- if (!cached || !Array.isArray(cached.meetings))
62
+ const cached = loadCachedTimetable();
63
+ if (!cached)
229
64
  return null;
230
- const week = currentWeekNumber(ptr.weekOneMonday, now);
65
+ const schedule = createTimetableSchedule(cached.timetable, {
66
+ weekOneMonday: cached.weekOneMonday,
67
+ });
68
+ const week = schedule.weekAt(now);
231
69
  if (week < 1)
232
70
  return null;
233
- const meetings = cached.meetings;
234
- const classDays = [1, 2, 3, 4, 5, 6, 7].map((wd) => meetingsOnDay(meetings, wd, week).length > 0);
235
- const base = new Date(`${ptr.weekOneMonday}T00:00:00`);
236
- 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);
237
73
  return { weekStartDate, classDays };
238
74
  }
239
75
  catch {
240
76
  return null;
241
77
  }
242
78
  }
243
- /** Best-effort, cache-only (no network) read of how many timetable items
244
- * still need the student's attention — the same data buildHubField()
245
- * (schedule.ts) already surfaces inside Schedule's own hub menu, exposed
246
- * here so Home can show it too without a second source of truth. */
247
79
  export function peekUnresolvedCount() {
248
80
  try {
249
- const ptr = loadCurrentPointer();
250
- if (!ptr)
251
- return 0;
252
- const cached = loadTimetableCache(ptr.termKey);
253
- if (!cached || !Array.isArray(cached.unresolvedItems))
254
- return 0;
255
- return cached.unresolvedItems.length;
81
+ return loadCachedTimetable()?.timetable.unresolvedItems.length ?? 0;
256
82
  }
257
83
  catch {
258
84
  return 0;
@@ -1,53 +1,60 @@
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';
11
7
  function getServiceTargets() {
12
8
  const trans = t();
13
9
  return [
14
- // NBTCA-owned services
15
10
  { name: trans.status.serviceHomepage, url: URLS.homepage, group: 'nbtca' },
16
11
  { name: trans.status.serviceDocs, url: URLS.docs, group: 'nbtca' },
17
12
  { name: trans.status.serviceIcal, url: URLS.calendar, group: 'nbtca' },
18
13
  { name: trans.status.serviceRepair, url: URLS.repair, group: 'nbtca' },
19
- // External platforms
20
14
  { name: trans.status.serviceGithub, url: URLS.github, group: 'external' },
21
15
  { name: trans.status.serviceRoadmap, url: URLS.roadmap, group: 'external' },
22
- // Intranet services (campus LAN only)
23
16
  { name: trans.status.serviceCloud, url: URLS.cloud, group: 'intranet', intranet: true },
24
17
  { name: trans.status.serviceMirror, url: URLS.mirror, group: 'intranet', intranet: true },
25
18
  ];
26
19
  }
27
20
  async function checkService(name, url, timeoutMs) {
28
21
  const start = Date.now();
22
+ const controller = new AbortController();
23
+ const timeout = setTimeout(() => {
24
+ controller.abort();
25
+ }, timeoutMs);
26
+ timeout.unref();
27
+ let response;
29
28
  try {
30
- const controller = new AbortController();
31
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
32
- const response = await fetch(url, {
29
+ response = await fetch(url, {
33
30
  signal: controller.signal,
34
31
  redirect: 'follow',
35
32
  headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
36
33
  });
37
- clearTimeout(timeout);
38
34
  const latencyMs = Date.now() - start;
39
35
  const ok = response.status >= 200 && response.status < 400;
40
36
  return { name, url, ok, statusCode: response.status, latencyMs };
41
37
  }
42
38
  catch (err) {
43
39
  const latencyMs = Date.now() - start;
44
- const error = err instanceof Error
45
- ? (err.name === 'AbortError' ? 'Request timed out' : err.message)
46
- : String(err);
40
+ const error = sanitizeTerminalLine(err instanceof Error
41
+ ? err.name === 'AbortError'
42
+ ? 'Request timed out'
43
+ : err.message
44
+ : String(err));
47
45
  return { name, url, ok: false, latencyMs, error };
48
46
  }
47
+ finally {
48
+ clearTimeout(timeout);
49
+ try {
50
+ await response?.body?.cancel();
51
+ }
52
+ catch {
53
+ // The response may already be closed by the runtime.
54
+ }
55
+ }
49
56
  }
50
- export async function checkServiceWithRetry(target, timeoutMs, retries) {
57
+ async function checkServiceWithRetry(target, timeoutMs, retries) {
51
58
  let lastResult = await checkService(target.name, target.url, timeoutMs);
52
59
  if (!lastResult.ok) {
53
60
  for (let attempt = 0; attempt < retries; attempt++) {
@@ -58,12 +65,16 @@ export async function checkServiceWithRetry(target, timeoutMs, retries) {
58
65
  break;
59
66
  }
60
67
  }
61
- return { ...lastResult, group: target.group, intranet: target.intranet };
68
+ return {
69
+ ...lastResult,
70
+ group: target.group,
71
+ ...(target.intranet === undefined ? {} : { intranet: target.intranet }),
72
+ };
62
73
  }
63
74
  export async function checkServices(options = {}) {
64
75
  const timeoutMs = options.timeoutMs ?? 6000;
65
76
  const retries = options.retries ?? 1;
66
- return Promise.all(getServiceTargets().map(t => checkServiceWithRetry(t, timeoutMs, retries)));
77
+ return Promise.all(getServiceTargets().map((t) => checkServiceWithRetry(t, timeoutMs, retries)));
67
78
  }
68
79
  export function serializeServiceStatus(items) {
69
80
  return items.map((item) => ({
@@ -78,12 +89,12 @@ export function serializeServiceStatus(items) {
78
89
  }));
79
90
  }
80
91
  export function hasServiceFailures(items) {
81
- return items.some(item => !item.ok && !item.intranet);
92
+ return items.some((item) => !item.ok && !item.intranet);
82
93
  }
83
94
  export function countServiceHealth(items) {
84
95
  let up = 0;
85
96
  let down = 0;
86
- for (const item of items.filter(i => !i.intranet)) {
97
+ for (const item of items.filter((i) => !i.intranet)) {
87
98
  if (item.ok)
88
99
  up++;
89
100
  else
@@ -100,7 +111,7 @@ export function renderServiceStatusTable(items, options) {
100
111
  const applyCyan = useColor ? chalk.cyan : id;
101
112
  const applyLatency = useColor ? c.latency : (ms) => `${ms}ms`;
102
113
  const trans = t();
103
- const nameWidth = Math.max(...items.map(i => visualWidth(i.name)), visualWidth(trans.status.service));
114
+ const nameWidth = Math.max(...items.map((i) => visualWidth(i.name)), visualWidth(trans.status.service));
104
115
  const statusWidth = 10;
105
116
  const onIcon = pickIcon('●', '+');
106
117
  const offIcon = pickIcon('✕', '!');
@@ -112,14 +123,22 @@ export function renderServiceStatusTable(items, options) {
112
123
  if (item.group !== currentGroup) {
113
124
  if (currentGroup !== undefined)
114
125
  lines.push('');
115
- const groupLabel = item.group === 'nbtca' ? trans.status.groupNbtca :
116
- item.group === 'external' ? trans.status.groupExternal :
117
- item.group === 'intranet' ? trans.status.groupIntranet : '';
126
+ const groupLabel = item.group === 'nbtca'
127
+ ? trans.status.groupNbtca
128
+ : item.group === 'external'
129
+ ? trans.status.groupExternal
130
+ : item.group === 'intranet'
131
+ ? trans.status.groupIntranet
132
+ : '';
118
133
  lines.push(` ${applyDim(groupLabel)}`);
119
134
  lines.push(` ${applyDim(sep.repeat(nameWidth + statusWidth + 12))}`);
120
135
  currentGroup = item.group;
121
136
  }
122
- const nameCol = padEndV(item.pending ? applyDim(item.name) : (item.intranet ? applyDim(item.name) : applyCyan(item.name)), nameWidth);
137
+ const nameCol = padEndV(item.pending
138
+ ? applyDim(item.name)
139
+ : item.intranet
140
+ ? applyDim(item.name)
141
+ : applyCyan(item.name), nameWidth);
123
142
  let statusLabel;
124
143
  if (item.pending) {
125
144
  statusLabel = applyDim(pickIcon('…', '.'));
@@ -141,37 +160,3 @@ export function renderServiceStatusTable(items, options) {
141
160
  }
142
161
  return lines.join('\n');
143
162
  }
144
- export async function showServiceStatus() {
145
- const trans = t();
146
- await enterScreen(breadcrumb(trans.menu.status));
147
- const targets = getServiceTargets();
148
- if (getCapabilities().reducedMotion) {
149
- const spinner = createSpinner(trans.status.checking);
150
- const items = await checkServices();
151
- const hasFailures = hasServiceFailures(items);
152
- if (hasFailures)
153
- spinner.error(trans.status.summaryFail);
154
- else
155
- spinner.stop(trans.status.summaryOk);
156
- console.log();
157
- console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
158
- console.log();
159
- return items;
160
- }
161
- const items = targets.map((tg) => ({
162
- name: tg.name, url: tg.url, ok: false, group: tg.group, intranet: tg.intranet, pending: true,
163
- }));
164
- const paint = createPainter(() => renderServiceStatusTable(items, { color: getCapabilities().color }));
165
- console.log();
166
- paint();
167
- await Promise.all(targets.map(async (target, i) => {
168
- const status = await checkServiceWithRetry(target, 6000, 1);
169
- items[i] = { ...status, pending: false };
170
- paint();
171
- }));
172
- console.log('\n');
173
- const hasFailures = hasServiceFailures(items);
174
- console.log(hasFailures ? c.warn(trans.status.summaryFail) : c.success(trans.status.summaryOk));
175
- console.log();
176
- return items;
177
- }