feat(FN-2729): merge fusion/fn-2729
- fix(FN-2729): silence ansi strip lint warning - test(FN-2729): complete Step 10 — cover node health indicators - feat(FN-2729): complete Step 9 — colorize CLI node status output - feat(FN-2729): complete Step 8 — use NodeHealthDot in settings routing status - test(FN-2729): align ListView node override option expectations - feat(FN-2729): complete Step 7 — add bulk node status indicators - fix(engine): prevent auto-merge cooldown loop on unresolvable conflicts - feat(FN-2911): improve chat attachment compose and preview UX - feat(FN-2921): merge fusion/fn-2921 - feat(FN-2909): merge fusion/fn-2909 - feat(FN-2914): merge fusion/fn-2914 - feat(FN-2902): merge fusion/fn-2902 - chore(release): v0.8.3 - Update changeset - fix(tui): swap 3/4 panel hotkeys so they match the help text - fix(tui): always show auth token and report accurate macOS memory usage Fusion-Task-Id: FN-2729
This commit is contained in:
@@ -245,6 +245,7 @@ describe("node commands", () => {
|
||||
expect(parsed).toHaveLength(1);
|
||||
expect(parsed[0].name).toBe("json-node");
|
||||
expect(parsed[0].apiKey).toBe("none");
|
||||
expect(output).not.toMatch(/\x1b\[[0-9;]*m/);
|
||||
});
|
||||
|
||||
it("runNodeList masks API keys in JSON output", async () => {
|
||||
@@ -277,9 +278,18 @@ describe("node commands", () => {
|
||||
await runNodeList();
|
||||
|
||||
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).toContain("● online");
|
||||
expect(output).toContain("○ offline");
|
||||
expect(output).toContain("✕ error");
|
||||
expect(output).toContain("\x1b[32m●\x1b[0m \x1b[32monline\x1b[0m");
|
||||
expect(output).toContain("\x1b[31m○\x1b[0m \x1b[31moffline\x1b[0m");
|
||||
expect(output).toContain("\x1b[31m✕\x1b[0m \x1b[31merror\x1b[0m");
|
||||
});
|
||||
|
||||
it("runNodeList colorizes connecting status", async () => {
|
||||
mockListNodes.mockResolvedValue([makeNode({ name: "connecting-node", status: "connecting" })]);
|
||||
|
||||
await runNodeList();
|
||||
|
||||
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).toContain("\x1b[33m◐\x1b[0m \x1b[33mconnecting\x1b[0m");
|
||||
});
|
||||
|
||||
// ── runNodeConnect Tests ─────────────────────────────────────────────────
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { CentralCore, type NodeConfig } from "@fusion/core";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
|
||||
const GREEN = "\x1b[32m";
|
||||
const RED = "\x1b[31m";
|
||||
const YELLOW = "\x1b[33m";
|
||||
const GRAY = "\x1b[90m";
|
||||
const RESET = "\x1b[0m";
|
||||
|
||||
// ── Options Interfaces ───────────────────────────────────────────────────────
|
||||
|
||||
/** Options for node list command. */
|
||||
@@ -133,19 +139,51 @@ export function formatLastActivity(timestamp?: string | null): string {
|
||||
*/
|
||||
function getStatusIndicator(status: string): string {
|
||||
switch (status) {
|
||||
case "online": return "●";
|
||||
case "offline": return "○";
|
||||
case "error": return "✕";
|
||||
case "connecting": return "◐";
|
||||
default: return "○";
|
||||
case "online":
|
||||
return `${GREEN}●${RESET}`;
|
||||
case "offline":
|
||||
return `${RED}○${RESET}`;
|
||||
case "error":
|
||||
return `${RED}✕${RESET}`;
|
||||
case "connecting":
|
||||
return `${YELLOW}◐${RESET}`;
|
||||
default:
|
||||
return `${GRAY}○${RESET}`;
|
||||
}
|
||||
}
|
||||
|
||||
function colorizeStatusText(status: string): string {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return `${GREEN}${status}${RESET}`;
|
||||
case "offline":
|
||||
case "error":
|
||||
return `${RED}${status}${RESET}`;
|
||||
case "connecting":
|
||||
return `${YELLOW}${status}${RESET}`;
|
||||
default:
|
||||
return `${GRAY}${status}${RESET}`;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ANSI_ESCAPE_PATTERN = new RegExp("\\u001b\\[[0-9;]*m", "g");
|
||||
|
||||
function stripAnsi(text: string): string {
|
||||
return text.replace(ANSI_ESCAPE_PATTERN, "");
|
||||
}
|
||||
|
||||
function visualPadEnd(text: string, minWidth: number): string {
|
||||
const visibleLength = stripAnsi(text).length;
|
||||
if (visibleLength >= minWidth) return text;
|
||||
return `${text}${" ".repeat(minWidth - visibleLength)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color-coded status string.
|
||||
*/
|
||||
function formatStatus(status: string): string {
|
||||
return `${getStatusIndicator(status)} ${status}`;
|
||||
return `${getStatusIndicator(status)} ${colorizeStatusText(status)}`;
|
||||
}
|
||||
|
||||
// ── Core Command Functions ──────────────────────────────────────────────────
|
||||
@@ -201,7 +239,7 @@ export async function runNodeList(options: NodeListOptions = {}): Promise<void>
|
||||
for (const node of sorted) {
|
||||
const name = node.name.padEnd(16);
|
||||
const type = node.type.padEnd(8);
|
||||
const statusStr = formatStatus(node.status).padEnd(12);
|
||||
const statusStr = visualPadEnd(formatStatus(node.status), 12);
|
||||
const max = String(node.maxConcurrent).padStart(3);
|
||||
|
||||
if (hasMetrics && node.systemMetrics) {
|
||||
@@ -527,7 +565,7 @@ export async function runMeshStatus(options: MeshStatusOptions = {}): Promise<vo
|
||||
for (const node of sorted) {
|
||||
const name = node.name.padEnd(16);
|
||||
const type = node.type.padEnd(8);
|
||||
const statusStr = formatStatus(node.status).padEnd(12);
|
||||
const statusStr = visualPadEnd(formatStatus(node.status), 12);
|
||||
const url = node.type === "remote" ? (node.url ?? "-") : "(local)";
|
||||
console.log(` ${name} ${type} ${statusStr} ${url}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user