@f5xc-salesdemos/xcsh 18.27.0 → 18.28.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.27.0",
4
+ "version": "18.28.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -47,12 +47,12 @@
47
47
  "dependencies": {
48
48
  "@agentclientprotocol/sdk": "0.16.1",
49
49
  "@mozilla/readability": "^0.6",
50
- "@f5xc-salesdemos/xcsh-stats": "18.27.0",
51
- "@f5xc-salesdemos/pi-agent-core": "18.27.0",
52
- "@f5xc-salesdemos/pi-ai": "18.27.0",
53
- "@f5xc-salesdemos/pi-natives": "18.27.0",
54
- "@f5xc-salesdemos/pi-tui": "18.27.0",
55
- "@f5xc-salesdemos/pi-utils": "18.27.0",
50
+ "@f5xc-salesdemos/xcsh-stats": "18.28.0",
51
+ "@f5xc-salesdemos/pi-agent-core": "18.28.0",
52
+ "@f5xc-salesdemos/pi-ai": "18.28.0",
53
+ "@f5xc-salesdemos/pi-natives": "18.28.0",
54
+ "@f5xc-salesdemos/pi-tui": "18.28.0",
55
+ "@f5xc-salesdemos/pi-utils": "18.28.0",
56
56
  "@sinclair/typebox": "^0.34",
57
57
  "@xterm/headless": "^6.0",
58
58
  "ajv": "^8.18",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.27.0",
21
- "commit": "5c365003b8532b2252bde64eeb9cc751e927752d",
22
- "shortCommit": "5c36500",
20
+ "version": "18.28.0",
21
+ "commit": "aefe3468aa6c23cfe1f6ac0575e011b2416389cf",
22
+ "shortCommit": "aefe346",
23
23
  "branch": "main",
24
- "tag": "v18.27.0",
25
- "commitDate": "2026-04-29T19:40:39Z",
26
- "buildDate": "2026-04-29T20:01:37.365Z",
24
+ "tag": "v18.28.0",
25
+ "commitDate": "2026-04-30T03:44:13Z",
26
+ "buildDate": "2026-04-30T04:02:30.628Z",
27
27
  "dirty": false,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/5c365003b8532b2252bde64eeb9cc751e927752d",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.27.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/aefe3468aa6c23cfe1f6ac0575e011b2416389cf",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.28.0"
33
33
  };
@@ -14,7 +14,7 @@ const STARTUP_RETRY_DELAY_MS = 500;
14
14
 
15
15
  type ContextValidator = (opts: {
16
16
  timeoutMs: number;
17
- }) => Promise<{ status: AuthStatus; latencyMs?: number; errorClass?: "network" | "credential" }>;
17
+ }) => Promise<{ status: AuthStatus; latencyMs?: number; errorClass?: "network" | "credential" | "url_not_found" }>;
18
18
 
19
19
  /**
20
20
  * Runs the context validator once with a startup-sized timeout; if the result is `offline`
@@ -28,7 +28,7 @@ export async function validateContextWithStartupRetry(
28
28
  retryTimeoutMs?: number;
29
29
  retryDelayMs?: number;
30
30
  },
31
- ): Promise<{ status: AuthStatus; latencyMs?: number }> {
31
+ ): Promise<{ status: AuthStatus; latencyMs?: number; errorClass?: "network" | "credential" | "url_not_found" }> {
32
32
  const firstTimeoutMs = options?.firstTimeoutMs ?? STARTUP_FIRST_TIMEOUT_MS;
33
33
  const retryTimeoutMs = options?.retryTimeoutMs ?? STARTUP_RETRY_TIMEOUT_MS;
34
34
  const retryDelayMs = options?.retryDelayMs ?? STARTUP_RETRY_DELAY_MS;
@@ -56,6 +56,7 @@ export interface WelcomeContextStatus {
56
56
  state: ContextCheckState;
57
57
  name?: string;
58
58
  latencyMs?: number;
59
+ errorClass?: "network" | "credential" | "url_not_found";
59
60
  }
60
61
 
61
62
  export interface WelcomeCheckResult {
@@ -167,7 +168,7 @@ async function checkContextStatus(): Promise<WelcomeContextStatus> {
167
168
  case "auth_error":
168
169
  return { state: "auth_error", name };
169
170
  case "offline":
170
- return { state: "offline", name };
171
+ return { state: "offline", name, errorClass: result.errorClass };
171
172
  default:
172
173
  return { state: "no_context" };
173
174
  }
@@ -232,6 +232,12 @@ export class WelcomeComponent implements Component {
232
232
  ` ${theme.fg("dim", "Run /context to update")}`,
233
233
  ];
234
234
  case "offline":
235
+ if (this.contextStatus?.errorClass === "url_not_found") {
236
+ return [
237
+ ` ${formatStatusIcon("error")} ${theme.fg("muted", n)} ${theme.fg("error", "\u2014 tenant not found")}`,
238
+ ` ${theme.fg("dim", "Recreate with /context create or check with /context show")}`,
239
+ ];
240
+ }
235
241
  return [
236
242
  ` ${formatStatusIcon("warning")} ${theme.fg("muted", n)} ${theme.fg("warning", "\u2014 unreachable")}`,
237
243
  ` ${theme.fg("dim", "Check network, /context")}`,
@@ -1,3 +1,4 @@
1
+ import { renderContextMessage } from "../../services/f5xc-table";
1
2
  import { ContextAddWizard } from "../components/context-add-wizard";
2
3
  import type { InteractiveModeContext } from "../types";
3
4
 
@@ -31,10 +32,11 @@ export class ContextCommandController {
31
32
  const { ContextService } = await import("../../services/f5xc-context");
32
33
  const service = await ContextService.getOrInit();
33
34
  await service.createContext(context);
34
- this.#ctx.showStatus(`Context '${context.name}' created.`);
35
35
  if (shouldActivate) {
36
36
  await service.activate(context.name);
37
- this.#ctx.showStatus(`Context '${context.name}' activated.`);
37
+ this.#ctx.showStatus(renderContextMessage(context.name, "Created and activated."), { dim: false });
38
+ } else {
39
+ this.#ctx.showStatus(renderContextMessage(context.name, "Created."), { dim: false });
38
40
  }
39
41
  this.#ctx.statusLine?.invalidate();
40
42
  this.#ctx.updateEditorTopBorder?.();
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import { SECRET_ENV_PATTERNS } from "../secrets/index";
3
3
  import { expandTilde } from "../tools/path-utils";
4
4
  import { ContextError, ContextService, CURRENT_SCHEMA_VERSION } from "./f5xc-context";
5
+ import { formatStatusIcon } from "./f5xc-context-indicators";
5
6
  import {
6
7
  deriveTenantFromUrl,
7
8
  F5XC_API_TOKEN,
@@ -16,12 +17,13 @@ import {
16
17
  formatExpiration,
17
18
  formatRelativeTime,
18
19
  formatRotation,
20
+ renderContextMessage,
19
21
  renderF5XCTable,
20
22
  type TableRow,
21
23
  } from "./f5xc-table";
22
24
 
23
25
  interface CommandContext {
24
- showStatus(msg: string): void;
26
+ showStatus(msg: string, options?: { dim?: boolean }): void;
25
27
  showError(msg: string): void;
26
28
  editor: { setText(text: string): void };
27
29
  statusLine?: { invalidate(): void };
@@ -130,20 +132,30 @@ async function handleList(ctx: CommandContext, service: ContextService): Promise
130
132
  const status = service.getStatus();
131
133
  if (status.credentialSource === "environment" && status.activeContextUrl) {
132
134
  const label = deriveTenantFromUrl(status.activeContextUrl) ?? "(environment)";
133
- ctx.showStatus(` * ${sanitize(label).padEnd(20)} ${sanitize(status.activeContextUrl)} (via env vars)`);
135
+ ctx.showStatus(
136
+ renderContextMessage(
137
+ "contexts",
138
+ ` * ${sanitize(label)} ${sanitize(status.activeContextUrl)} (via env vars)`,
139
+ ),
140
+ { dim: false },
141
+ );
134
142
  return;
135
143
  }
136
- ctx.showStatus("No F5 XC contexts found. Use /context create or ask me to help set one up.");
144
+ ctx.showStatus(
145
+ renderContextMessage("contexts", "No F5 XC contexts found. Use /context create or ask me to help set one up."),
146
+ { dim: false },
147
+ );
137
148
  return;
138
149
  }
139
150
  const status = service.getStatus();
140
- const lines = contexts.map(p => {
141
- const marker = p.name === status.activeContextName ? "*" : " ";
151
+ const rows: TableRow[] = contexts.map(p => {
152
+ const isActive = p.name === status.activeContextName;
153
+ const marker = isActive ? `${formatStatusIcon("connected")} ` : " ";
142
154
  const versionSuffix =
143
155
  p.version !== undefined && p.version > CURRENT_SCHEMA_VERSION ? ` (v${p.version} — upgrade required)` : "";
144
- return ` ${marker} ${sanitize(p.name).padEnd(20)} ${sanitize(p.apiUrl)}${versionSuffix}`;
156
+ return { key: `${marker}${sanitize(p.name)}`, value: `${sanitize(p.apiUrl)}${versionSuffix}` };
145
157
  });
146
- ctx.showStatus(lines.join("\n"));
158
+ ctx.showStatus(renderF5XCTable("contexts", rows), { dim: false });
147
159
  }
148
160
 
149
161
  async function handleActivate(ctx: CommandContext, service: ContextService, name: string): Promise<void> {
@@ -286,7 +298,7 @@ async function handleShow(ctx: CommandContext, service: ContextService, name?: s
286
298
  rows.push(...metaRows);
287
299
  }
288
300
 
289
- ctx.showStatus(renderF5XCTable(context.name, rows, { dividers }));
301
+ ctx.showStatus(renderF5XCTable(context.name, rows, { dividers }), { dim: false });
290
302
  }
291
303
 
292
304
  async function handleValidate(ctx: CommandContext, service: ContextService, name: string): Promise<void> {
@@ -305,7 +317,7 @@ async function handleValidate(ctx: CommandContext, service: ContextService, name
305
317
  { key: F5XC_API_TOKEN, value: service.maskToken(result.context.apiToken) },
306
318
  { key: "Status", value: formatAuthIndicator(result.status, result.latencyMs, result.errorClass) },
307
319
  ];
308
- ctx.showStatus(renderF5XCTable(`${result.context.name} (validation only)`, rows));
320
+ ctx.showStatus(renderF5XCTable(`${result.context.name} (validation only)`, rows), { dim: false });
309
321
  } catch (err) {
310
322
  ctx.showError(err instanceof ContextError ? err.message : String(err));
311
323
  }
@@ -314,7 +326,10 @@ async function handleValidate(ctx: CommandContext, service: ContextService, name
314
326
  async function handleStatus(ctx: CommandContext, service: ContextService): Promise<void> {
315
327
  const status = service.getStatus();
316
328
  if (!status.isConfigured) {
317
- ctx.showStatus("F5 XC: not configured. Use /context create or ask me to help set one up.");
329
+ ctx.showStatus(
330
+ renderContextMessage("status", "Not configured. Use /context create or ask me to help set one up."),
331
+ { dim: false },
332
+ );
318
333
  return;
319
334
  }
320
335
  const auth = await service.validateToken({ timeoutMs: 3000 });
@@ -325,7 +340,7 @@ async function handleStatus(ctx: CommandContext, service: ContextService): Promi
325
340
  { key: "Namespace", value: status.activeContextNamespace ?? "(not set)" },
326
341
  { key: "Status", value: formatAuthIndicator(auth.status, auth.latencyMs, auth.errorClass) },
327
342
  ];
328
- ctx.showStatus(renderF5XCTable(status.activeContextName ?? "status", rows));
343
+ ctx.showStatus(renderF5XCTable(status.activeContextName ?? "status", rows), { dim: false });
329
344
  }
330
345
 
331
346
  async function handleCreate(ctx: CommandContext, service: ContextService, args: string[]): Promise<void> {
@@ -355,7 +370,9 @@ async function handleCreate(ctx: CommandContext, service: ContextService, args:
355
370
  apiToken: token,
356
371
  defaultNamespace: namespace ?? "default",
357
372
  });
358
- ctx.showStatus(`Context '${name}' created. Use /context activate ${name} to switch to it.`);
373
+ ctx.showStatus(renderContextMessage(name, `Created. Use /context activate ${name} to switch to it.`), {
374
+ dim: false,
375
+ });
359
376
  } catch (err) {
360
377
  ctx.showError(err instanceof ContextError ? err.message : String(err));
361
378
  }
@@ -369,7 +386,7 @@ async function handleRename(ctx: CommandContext, service: ContextService, args:
369
386
  }
370
387
  try {
371
388
  await service.renameContext(oldName, newName);
372
- ctx.showStatus(`Context '${oldName}' renamed to '${newName}'.`);
389
+ ctx.showStatus(renderContextMessage(newName, `Renamed from '${oldName}'.`), { dim: false });
373
390
  ctx.statusLine?.invalidate();
374
391
  ctx.updateEditorTopBorder?.();
375
392
  ctx.ui?.requestRender();
@@ -452,13 +469,13 @@ async function handleImport(ctx: CommandContext, service: ContextService, rawArg
452
469
 
453
470
  try {
454
471
  const result = await service.importContexts(parsed, { overwrite });
455
- const lines: string[] = [];
456
- lines.push(`Imported ${result.imported.length} context${result.imported.length === 1 ? "" : "s"}:`);
457
- for (const name of result.imported) lines.push(` + ${name}`);
472
+ const bodyLines: string[] = [];
473
+ bodyLines.push(`Imported ${result.imported.length} context${result.imported.length === 1 ? "" : "s"}:`);
474
+ for (const name of result.imported) bodyLines.push(` + ${name}`);
458
475
  if (result.overwritten.length > 0) {
459
- lines.push(`Overwrote ${result.overwritten.length}: ${result.overwritten.join(", ")}`);
476
+ bodyLines.push(`Overwrote ${result.overwritten.length}: ${result.overwritten.join(", ")}`);
460
477
  }
461
- ctx.showStatus(lines.join("\n"));
478
+ ctx.showStatus(renderContextMessage("import", bodyLines.join("\n")), { dim: false });
462
479
  // Invalidate TUI chrome IF the active context was overwritten. The
463
480
  // service's importContexts re-activates the active context when an
464
481
  // overwrite touches it, which means #activeContext, bash.environment,
@@ -491,13 +508,17 @@ async function handleDelete(ctx: CommandContext, service: ContextService, args:
491
508
  }
492
509
  if (!confirmed) {
493
510
  ctx.showStatus(
494
- `This will permanently delete context '${name}' from ~/.config/f5xc/contexts/.\nRun /context delete ${name} --confirm to proceed.`,
511
+ renderContextMessage(
512
+ name,
513
+ `This will permanently delete context '${name}' from ~/.config/f5xc/contexts/.\nRun /context delete ${name} --confirm to proceed.`,
514
+ ),
515
+ { dim: false },
495
516
  );
496
517
  return;
497
518
  }
498
519
  try {
499
520
  await service.deleteContext(name);
500
- ctx.showStatus(`Context '${name}' deleted.`);
521
+ ctx.showStatus(renderContextMessage(name, "Deleted."), { dim: false });
501
522
  } catch (err) {
502
523
  ctx.showError(err instanceof ContextError ? err.message : String(err));
503
524
  }
@@ -512,7 +533,7 @@ async function handleNamespace(ctx: CommandContext, service: ContextService, nam
512
533
  }
513
534
  try {
514
535
  service.setNamespace(namespace);
515
- ctx.showStatus(`Namespace switched to: ${namespace}`);
536
+ ctx.showStatus(renderContextMessage("namespace", `Namespace ${namespace}`), { dim: false });
516
537
  ctx.statusLine?.invalidate();
517
538
  ctx.updateEditorTopBorder?.();
518
539
  ctx.ui?.requestRender();
@@ -597,7 +618,7 @@ async function handleEnvList(ctx: CommandContext, service: ContextService): Prom
597
618
  const contexts = await service.listContexts();
598
619
  const context = contexts.find(p => p.name === contextName);
599
620
  if (!context?.env || Object.keys(context.env).length === 0) {
600
- ctx.showStatus(`Context '${contextName}' has no custom environment variables.`);
621
+ ctx.showStatus(renderContextMessage(`${contextName} env`, "No custom environment variables."), { dim: false });
601
622
  return;
602
623
  }
603
624
  const rows: TableRow[] = [];
@@ -605,7 +626,7 @@ async function handleEnvList(ctx: CommandContext, service: ContextService): Prom
605
626
  const sensitive = isSensitiveKey(key) || (context.sensitiveKeys ?? []).includes(key);
606
627
  rows.push({ key: sanitize(key), value: sensitive ? service.maskToken(value) : sanitize(value) });
607
628
  }
608
- ctx.showStatus(renderF5XCTable(`${contextName} env`, rows));
629
+ ctx.showStatus(renderF5XCTable(`${contextName} env`, rows), { dim: false });
609
630
  }
610
631
 
611
632
  async function handleEnvSet(ctx: CommandContext, service: ContextService, args: string): Promise<void> {
@@ -623,15 +644,14 @@ async function handleEnvSet(ctx: CommandContext, service: ContextService, args:
623
644
  }
624
645
  try {
625
646
  const result = await service.setEnvVars(contextName, vars);
626
- const lines: string[] = [];
647
+ const bodyLines: string[] = [];
648
+ bodyLines.push(`Set ${keys.length} variable${keys.length > 1 ? "s" : ""} on '${contextName}':`);
627
649
  for (const key of keys) {
628
650
  const lock = result.sensitive.includes(key) ? " (auto-sensitive)" : "";
629
651
  const displayValue = isSensitiveKey(key) ? "***" : vars[key];
630
- lines.push(` ${key}=${displayValue}${lock}`);
652
+ bodyLines.push(` ${key}=${displayValue}${lock}`);
631
653
  }
632
- ctx.showStatus(
633
- `Set ${keys.length} variable${keys.length > 1 ? "s" : ""} on '${contextName}':\n${lines.join("\n")}`,
634
- );
654
+ ctx.showStatus(renderContextMessage(contextName, bodyLines.join("\n")), { dim: false });
635
655
  ctx.statusLine?.invalidate();
636
656
  } catch (err) {
637
657
  ctx.showError(err instanceof ContextError ? err.message : String(err));
@@ -653,11 +673,15 @@ async function handleEnvUnset(ctx: CommandContext, service: ContextService, args
653
673
  try {
654
674
  const result = await service.unsetEnvVars(contextName, keys);
655
675
  if (result.removed.length === 0) {
656
- ctx.showStatus(`No matching variables found on '${contextName}'.`);
676
+ ctx.showStatus(renderContextMessage(contextName, "No matching variables found."), { dim: false });
657
677
  return;
658
678
  }
659
679
  ctx.showStatus(
660
- `Removed ${result.removed.length} variable${result.removed.length > 1 ? "s" : ""} from '${contextName}':\n${result.removed.map(k => ` ${k}`).join("\n")}`,
680
+ renderContextMessage(
681
+ contextName,
682
+ `Removed ${result.removed.length} variable${result.removed.length > 1 ? "s" : ""} from '${contextName}':\n${result.removed.map(k => ` ${k}`).join("\n")}`,
683
+ ),
684
+ { dim: false },
661
685
  );
662
686
  ctx.statusLine?.invalidate();
663
687
  } catch (err) {
@@ -114,7 +114,7 @@ export interface ValidationResult {
114
114
  context: F5XCContext;
115
115
  status: AuthStatus;
116
116
  latencyMs?: number;
117
- errorClass?: "network" | "credential";
117
+ errorClass?: "network" | "credential" | "url_not_found";
118
118
  }
119
119
 
120
120
  export class ContextError extends Error {
@@ -915,7 +915,7 @@ export class ContextService {
915
915
  timeoutMs?: number;
916
916
  apiUrl?: string;
917
917
  apiToken?: string;
918
- }): Promise<{ status: AuthStatus; latencyMs?: number; errorClass?: "network" | "credential" }> {
918
+ }): Promise<{ status: AuthStatus; latencyMs?: number; errorClass?: "network" | "credential" | "url_not_found" }> {
919
919
  // Use explicit credentials if provided (for non-active contexts or env-backed sessions),
920
920
  // otherwise fall back to effective credentials (env override > active context)
921
921
  const effectiveUrl = options?.apiUrl ?? process.env[F5XC_API_URL] ?? this.#activeContext?.apiUrl;
@@ -946,13 +946,23 @@ export class ContextService {
946
946
  method: "GET",
947
947
  headers: { Authorization: `APIToken ${effectiveToken}`, Accept: "application/json" },
948
948
  signal: AbortSignal.timeout(timeout),
949
+ redirect: "manual",
949
950
  });
950
951
  const latencyMs = Math.round(performance.now() - start);
951
952
  if (!adHoc) {
952
953
  this.#lastAuthLatencyMs = latencyMs;
953
954
  this.#lastAuthCheckedAt = checkedAt;
954
955
  }
956
+ if (response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400)) {
957
+ if (!adHoc) this.#authStatus = "offline";
958
+ return { status: "offline", latencyMs, errorClass: "url_not_found" };
959
+ }
955
960
  if (response.ok) {
961
+ const contentType = response.headers.get("content-type") ?? "";
962
+ if (!contentType.includes("application/json")) {
963
+ if (!adHoc) this.#authStatus = "offline";
964
+ return { status: "offline", latencyMs, errorClass: "url_not_found" };
965
+ }
956
966
  if (!adHoc) this.#authStatus = "connected";
957
967
  return { status: "connected", latencyMs };
958
968
  }
@@ -23,7 +23,7 @@ const r = (s: string) => `${F5_RED}${s}${RESET}`;
23
23
  export function formatAuthIndicator(
24
24
  status: AuthStatus,
25
25
  latencyMs?: number,
26
- errorClass?: "network" | "credential",
26
+ errorClass?: "network" | "credential" | "url_not_found",
27
27
  ): string {
28
28
  const ms = latencyMs !== undefined ? ` (${latencyMs}ms)` : "";
29
29
  switch (status) {
@@ -32,6 +32,9 @@ export function formatAuthIndicator(
32
32
  case "auth_error":
33
33
  return `${formatStatusIcon("error")} Auth Error — check token${ms}`;
34
34
  case "offline":
35
+ if (errorClass === "url_not_found") {
36
+ return `${formatStatusIcon("error")} Offline — tenant URL not found${ms}`;
37
+ }
35
38
  return `${formatStatusIcon("warning")} Offline — ${errorClass === "credential" ? "auth issue" : "network issue"}${ms}`;
36
39
  default:
37
40
  return `${formatStatusIcon("unknown")} Unknown`;
@@ -138,3 +141,24 @@ export function renderF5XCTable(title: string, rows: TableRow[], options?: Table
138
141
 
139
142
  return lines.join("\n");
140
143
  }
144
+
145
+ export function renderContextMessage(title: string, body: string): string {
146
+ const bodyLines = body.split("\n");
147
+ const maxLine = Math.max(...bodyLines.map(l => visibleWidth(l)), 0);
148
+ const innerWidth = Math.max(maxLine + 2, visibleWidth(title) + 3, 40);
149
+
150
+ const lines: string[] = [];
151
+
152
+ const titleText = ` ${title} `;
153
+ const titlePad = innerWidth - visibleWidth(titleText) - 1;
154
+ lines.push(`${r(BOX.tl + BOX.h)}${BOLD}${titleText}${RESET}${r(BOX.h.repeat(Math.max(0, titlePad)) + BOX.tr)}`);
155
+
156
+ for (const bodyLine of bodyLines) {
157
+ const pad = innerWidth - visibleWidth(bodyLine) - 2;
158
+ lines.push(`${r(BOX.v)} ${bodyLine}${" ".repeat(Math.max(0, pad))} ${r(BOX.v)}`);
159
+ }
160
+
161
+ lines.push(r(BOX.bl + BOX.h.repeat(innerWidth) + BOX.br));
162
+
163
+ return lines.join("\n");
164
+ }