/**
* Docker system formatting utilities
*
* Provides consistent markdown formatting for Docker system info,
* disk usage, prune operations, images, networks, and volumes.
*/
import { formatBytes } from "../services/docker/utils/formatters.js";
import type {
DockerDiskUsage,
DockerNetworkInfo,
DockerVolumeInfo,
ImageInfo,
PruneResult,
} from "../types.js";
/**
* Successful Docker info result
*/
export interface DockerInfoSuccess {
status: "success";
dockerVersion: string;
apiVersion: string;
os: string;
arch: string;
kernelVersion: string;
cpus: number;
memoryBytes: number;
storageDriver: string;
rootDir: string;
containersRunning: number;
containersTotal: number;
images: number;
}
/**
* Error Docker info result (when a host is unreachable)
*/
export interface DockerInfoError {
status: "error";
errorMessage: string;
}
/**
* Docker info result - discriminated union for success/error
*/
export type DockerInfoResult = DockerInfoSuccess | DockerInfoError;
/**
* Format Docker info as markdown
*
* Output structure per STYLE.md:
* 1. Title (Section 3.1): "Docker System Info on {host}" or "(all)"
* 2. Summary (Section 3.2): "Hosts: N | OK: N | Fail: N"
* 3. Per-host details with canonical symbols (Section 4.1)
*
* Title format:
* - Single host: "Docker System Info on {host}"
* - Multiple hosts: "Docker System Info (all)"
* - No markdown ## prefix (plain text titles)
*
* Symbols:
* - ✗ for errors (never ❌ emoji)
*
* @param results - Array of host Docker info results (success or error)
* @returns Formatted markdown string with system information
*/
export function formatDockerInfoMarkdown(
results: Array<{ host: string; info: DockerInfoResult }>
): string {
// STYLE.md 3.1: Title format varies by result count
const title =
results.length === 1 ? `Docker System Info on ${results[0].host}` : "Docker System Info (all)";
// STYLE.md 3.2: Rollup summary with success/error counts
const totalHosts = results.length;
const okHosts = results.filter((r) => r.info.status === "success").length;
const failHosts = results.filter((r) => r.info.status === "error").length;
const summary = `Hosts: ${totalHosts} | OK: ${okHosts} | Fail: ${failHosts}`;
const lines = [title, summary, ""];
for (const { host, info } of results) {
lines.push(`### ${host}`);
if (info.status === "error") {
// Use canonical ✗ symbol per STYLE.md 4.1 (never emoji)
lines.push(`✗ Error: ${info.errorMessage}`);
} else {
lines.push(`- Docker: ${info.dockerVersion} (API ${info.apiVersion})`);
lines.push(`- OS: ${info.os} (${info.arch})`);
lines.push(`- Kernel: ${info.kernelVersion}`);
lines.push(`- CPUs: ${info.cpus} | Memory: ${formatBytes(info.memoryBytes)}`);
lines.push(`- Storage: ${info.storageDriver} @ ${info.rootDir}`);
lines.push(`- Containers: ${info.containersRunning} running / ${info.containersTotal} total`);
lines.push(`- Images: ${info.images}`);
}
lines.push("");
}
return lines.join("\n");
}
/**
* Format Docker disk usage as markdown
*
* Output structure per STYLE.md:
* 1. Title (Section 3.1): "Docker Disk Usage on {host}" or "(all)"
* 2. Summary (Section 3.2): Aggregate totals across all hosts
* 3. Per-host usage breakdown tables
*
* @param results - Array of host disk usage statistics
* @returns Formatted markdown string with disk usage by category
*/
export function formatDockerDfMarkdown(
results: Array<{ host: string; usage: DockerDiskUsage }>
): string {
// STYLE.md 3.1: Title format varies by result count
const title =
results.length === 1 ? `Docker Disk Usage on ${results[0].host}` : "Docker Disk Usage (all)";
// STYLE.md 3.2: Rollup summary aggregates size and reclaimable across all hosts
const totalSize = results.reduce((sum, r) => sum + r.usage.totalSize, 0);
const totalReclaimable = results.reduce((sum, r) => sum + r.usage.totalReclaimable, 0);
const summary = `Total: ${formatBytes(totalSize)} | Reclaimable: ${formatBytes(totalReclaimable)}`;
const lines = [title, summary, ""];
for (const { host, usage } of results) {
lines.push(
`### ${host}`,
"",
"| Type | Count | Size | Reclaimable |",
"|------|-------|------|-------------|"
);
lines.push(
`| Images | ${usage.images.total} (${usage.images.active} active) | ${formatBytes(usage.images.size)} | ${formatBytes(usage.images.reclaimable)} |`
);
lines.push(
`| Containers | ${usage.containers.total} (${usage.containers.running} running) | ${formatBytes(usage.containers.size)} | ${formatBytes(usage.containers.reclaimable)} |`
);
lines.push(
`| Volumes | ${usage.volumes.total} (${usage.volumes.active} active) | ${formatBytes(usage.volumes.size)} | ${formatBytes(usage.volumes.reclaimable)} |`
);
lines.push(
`| Build Cache | ${usage.buildCache.total} | ${formatBytes(usage.buildCache.size)} | ${formatBytes(usage.buildCache.reclaimable)} |`
);
lines.push(
`| **Total** | | **${formatBytes(usage.totalSize)}** | **${formatBytes(usage.totalReclaimable)}** |`
);
lines.push("");
}
return lines.join("\n");
}
/**
* Format prune results as markdown
*
* Output structure per STYLE.md:
* 1. Title (Section 3.1): "Docker Prune on {host}" or "(all)"
* 2. Per-host RECLAIMED sections (Section 7.8)
*
* RECLAIMED format:
* - Section header: "RECLAIMED:"
* - Indented list: " type: size"
* - Not a table (differs from previous implementation)
*
* @param allResults - Array of host prune results
* @returns Formatted markdown string with RECLAIMED sections
*/
export function formatPruneMarkdown(
allResults: Array<{ host: string; results: PruneResult[] }>
): string {
// STYLE.md 3.1: Title format varies by result count
const title =
allResults.length === 1 ? `Docker Prune on ${allResults[0].host}` : "Docker Prune (all)";
const lines = [title, ""];
// STYLE.md 7.8: Use RECLAIMED section format (indented list, not table)
for (const { host, results } of allResults) {
lines.push(`### ${host}`, "", "RECLAIMED:");
for (const r of results) {
lines.push(` ${r.type}: ${formatBytes(r.spaceReclaimed)}`);
}
lines.push("");
}
return lines.join("\n");
}
/**
* Format images list as markdown table
*
* Title format per STYLE.md 3.1:
* - With host: "Docker Images on {host}"
* - Without host: "Docker Images"
* - No markdown ## prefix
*
* @param images - Array of image information
* @param total - Total count of images (for pagination)
* @param offset - Current pagination offset
* @param host - Optional host name for single-host queries
* @returns Formatted markdown string with image table
*/
export function formatImagesMarkdown(
images: ImageInfo[],
total: number,
offset: number,
host?: string
): string {
if (images.length === 0) return "No images found.";
const title = host ? `Docker Images on ${host}` : "Docker Images";
const lines = [
title,
"",
`Showing ${images.length} of ${total} images (offset: ${offset})`,
"",
"| ID | Tags | Size | Host | Containers |",
"|-----|------|------|------|------------|",
];
for (const img of images) {
const tags = img.tags.slice(0, 2).join(", ") + (img.tags.length > 2 ? "..." : "");
lines.push(
`| ${img.id} | ${tags} | ${formatBytes(img.size)} | ${img.hostName} | ${img.containers} |`
);
}
return lines.join("\n");
}
/**
* Format Docker networks list as markdown
*
* Title format per STYLE.MD 3.1:
* - With host: "Docker Networks on {host}"
* - Without host: "Docker Networks"
* - No markdown ## prefix
*
* @param networks - Array of network information
* @param total - Total count of networks (for pagination)
* @param offset - Current pagination offset
* @param host - Optional host name for single-host queries
* @returns Formatted markdown string with networks list
*/
export function formatNetworksMarkdown(
networks: DockerNetworkInfo[],
total: number,
offset: number,
host?: string
): string {
const title = host ? `Docker Networks on ${host}` : "Docker Networks";
if (networks.length === 0) {
return `${title}\n\nNo networks found.`;
}
const lines = [
title,
"",
`Showing ${networks.length} of ${total} networks (offset: ${offset})`,
"",
];
let currentHost = "";
for (const network of networks) {
if (network.hostName !== currentHost) {
currentHost = network.hostName;
lines.push(`### ${currentHost}`);
}
lines.push(`- ${network.name} (${network.driver}, ${network.scope})`);
}
return lines.join("\n");
}
/**
* Format Docker volumes list as markdown
*
* Title format per STYLE.md 3.1:
* - With host: "Docker Volumes on {host}"
* - Without host: "Docker Volumes"
* - No markdown ## prefix
*
* @param volumes - Array of volume information
* @param total - Total count of volumes (for pagination)
* @param offset - Current pagination offset
* @param host - Optional host name for single-host queries
* @returns Formatted markdown string with volumes list
*/
export function formatVolumesMarkdown(
volumes: DockerVolumeInfo[],
total: number,
offset: number,
host?: string
): string {
const title = host ? `Docker Volumes on ${host}` : "Docker Volumes";
if (volumes.length === 0) {
return `${title}\n\nNo volumes found.`;
}
const lines = [
title,
"",
`Showing ${volumes.length} of ${total} volumes (offset: ${offset})`,
"",
];
let currentHost = "";
for (const volume of volumes) {
if (volume.hostName !== currentHost) {
currentHost = volume.hostName;
lines.push(`### ${currentHost}`);
}
lines.push(`- ${volume.name} (${volume.driver}, ${volume.scope})`);
}
return lines.join("\n");
}