// src/tools/handlers/docker.ts
import type { ServiceContainer } from '../../services/container.js';
import type { FluxInput } from '../../schemas/flux/index.js';
import { loadHostConfigs } from '../../services/docker.js';
import {
formatDockerInfoMarkdown,
formatDockerDfMarkdown,
formatPruneMarkdown,
formatImagesMarkdown,
formatNetworksMarkdown,
formatVolumesMarkdown
} from '../../formatters/index.js';
import { assertValidAction, getHostConfigOptional, initializeHandler, formatResponse, paginateItems, resolveTargetHosts, createExhaustiveError } from './base-handler.js';
/**
* Handle all docker subactions
*
* Subactions: info, df, prune, images, pull, build, rmi, networks, volumes
*/
export async function handleDockerAction(
input: FluxInput,
container: ServiceContainer
): Promise<string> {
assertValidAction(input, 'docker', 'docker');
const { service: dockerService, hosts, format } = initializeHandler(
input,
container,
(c) => c.getDockerService(),
loadHostConfigs()
);
// Use type assertion to access subaction-specific fields
const inp = input as Record<string, unknown>;
// Find the target host (some docker subactions may query all hosts if not specified)
const hostName = inp.host as string | undefined;
const hostConfig = getHostConfigOptional(hosts, hostName);
// For single-host operations, require the host
if (hostName && !hostConfig) {
throw new Error(`Host not found: ${hostName}`);
}
switch (inp.subaction) {
case 'info': {
if (!hostConfig) {
throw new Error('Host is required for docker:info');
}
const info = await dockerService.getDockerInfo(hostConfig);
return formatResponse(
info,
format,
(data) => formatDockerInfoMarkdown([{ host: hostConfig.name, info: data }])
);
}
case 'df': {
if (!hostConfig) {
throw new Error('Host is required for docker:df');
}
const usage = await dockerService.getDockerDiskUsage(hostConfig);
return formatResponse(
usage,
format,
(data) => formatDockerDfMarkdown([{ host: hostConfig.name, usage: data }])
);
}
case 'prune': {
if (!hostConfig) {
throw new Error('Host is required for docker:prune');
}
// Require force flag for prune operations
if (!inp.force) {
throw new Error('Prune requires force=true to prevent accidental data loss');
}
const target = inp.prune_target as "containers" | "images" | "volumes" | "networks" | "buildcache" | "all";
const results = await dockerService.pruneDocker(hostConfig, target);
const pruneResult = { host: hostConfig.name, results };
return formatResponse(
pruneResult,
format,
(data) => formatPruneMarkdown([data])
);
}
case 'images': {
// Can query all hosts or specific host
const targetHosts = resolveTargetHosts(hosts, hostName);
const options: { danglingOnly?: boolean } = {};
if (inp.dangling_only) {
options.danglingOnly = true;
}
const images = await dockerService.listImages(targetHosts, options);
// Sort by hostName BEFORE pagination to ensure correct ordering
const sortedImages = [...images].sort((a, b) => {
const hostA = a.hostName ?? '';
const hostB = b.hostName ?? '';
return hostA.localeCompare(hostB);
});
// Apply pagination
const { items: paginatedImages, total, offset } =
paginateItems(sortedImages, { offset: inp.offset as number | undefined, limit: inp.limit as number | undefined });
return formatResponse(
images,
format,
() => formatImagesMarkdown(paginatedImages, total, offset)
);
}
case 'networks': {
// Can query all hosts or specific host
const targetHosts = resolveTargetHosts(hosts, hostName);
const networks = await dockerService.listNetworks(targetHosts);
// Sort by hostName BEFORE pagination to ensure correct ordering
const sortedNetworks = [...networks].sort((a, b) => {
const hostA = a.hostName ?? '';
const hostB = b.hostName ?? '';
return hostA.localeCompare(hostB);
});
const { items: paginatedNetworks, total, offset } =
paginateItems(sortedNetworks, { offset: inp.offset as number | undefined, limit: inp.limit as number | undefined });
return formatResponse(
networks,
format,
() => formatNetworksMarkdown(paginatedNetworks, total, offset)
);
}
case 'volumes': {
// Can query all hosts or specific host
const targetHosts = resolveTargetHosts(hosts, hostName);
const volumes = await dockerService.listVolumes(targetHosts);
// Sort by hostName BEFORE pagination to ensure correct ordering
const sortedVolumes = [...volumes].sort((a, b) => {
const hostA = a.hostName ?? '';
const hostB = b.hostName ?? '';
return hostA.localeCompare(hostB);
});
const { items: paginatedVolumes, total, offset } =
paginateItems(sortedVolumes, { offset: inp.offset as number | undefined, limit: inp.limit as number | undefined });
return formatResponse(
volumes,
format,
() => formatVolumesMarkdown(paginatedVolumes, total, offset)
);
}
case 'pull': {
if (!hostConfig) {
throw new Error('Host is required for docker:pull');
}
const imageName = inp.image as string;
const result = await dockerService.pullImage(imageName, hostConfig);
const pullResult = { host: hostConfig.name, image: imageName, ...result };
return formatResponse(
pullResult,
format,
(data) => `Image '${data.image}' pull completed on ${data.host}: ${data.status}`
);
}
case 'build': {
if (!hostConfig) {
throw new Error('Host is required for docker:build');
}
const options = {
context: inp.context as string,
tag: inp.tag as string,
dockerfile: inp.dockerfile as string | undefined,
noCache: inp.no_cache as boolean | undefined
};
const result = await dockerService.buildImage(hostConfig, options);
const buildResult = { host: hostConfig.name, ...options, ...result };
return formatResponse(
buildResult,
format,
(data) => `Image '${options.tag}' build completed on ${data.host}: ${data.status}`
);
}
case 'rmi': {
if (!hostConfig) {
throw new Error('Host is required for docker:rmi');
}
const imageName = inp.image as string;
const force = (inp.force as boolean | undefined) ?? false;
const result = await dockerService.removeImage(imageName, hostConfig, { force });
const rmiResult = { host: hostConfig.name, image: imageName, ...result };
return formatResponse(
rmiResult,
format,
(data) => `Image '${data.image}' removed from ${data.host}: ${data.status}`
);
}
default:
throw createExhaustiveError(inp as never, 'docker subaction');
}
}