@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 +7 -7
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/modes/components/welcome-checks.ts +4 -3
- package/src/modes/components/welcome.ts +6 -0
- package/src/modes/controllers/context-command-controller.ts +4 -2
- package/src/services/f5xc-context-command.ts +54 -30
- package/src/services/f5xc-context.ts +12 -2
- package/src/services/f5xc-table.ts +25 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
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.
|
|
51
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.28.0",
|
|
21
|
+
"commit": "aefe3468aa6c23cfe1f6ac0575e011b2416389cf",
|
|
22
|
+
"shortCommit": "aefe346",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-04-
|
|
26
|
-
"buildDate": "2026-04-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
141
|
-
const
|
|
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
|
|
156
|
+
return { key: `${marker}${sanitize(p.name)}`, value: `${sanitize(p.apiUrl)}${versionSuffix}` };
|
|
145
157
|
});
|
|
146
|
-
ctx.showStatus(
|
|
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(
|
|
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(
|
|
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(`
|
|
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
|
|
456
|
-
|
|
457
|
-
for (const name of result.imported)
|
|
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
|
-
|
|
476
|
+
bodyLines.push(`Overwrote ${result.overwritten.length}: ${result.overwritten.join(", ")}`);
|
|
460
477
|
}
|
|
461
|
-
ctx.showStatus(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
676
|
+
ctx.showStatus(renderContextMessage(contextName, "No matching variables found."), { dim: false });
|
|
657
677
|
return;
|
|
658
678
|
}
|
|
659
679
|
ctx.showStatus(
|
|
660
|
-
|
|
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
|
+
}
|