// src/tools/handlers/compose.ts
import type { ServiceContainer } from '../../services/container.js';
import type { FluxInput } from '../../schemas/flux/index.js';
import type {
ComposeActionInput,
ComposeListInput,
ComposeStatusInput,
ComposeUpInput,
ComposeDownInput,
ComposeRestartInput,
ComposeLogsInput,
ComposeBuildInput,
ComposePullInput,
ComposeRecreateInput,
ComposeRefreshInput
} from '../../schemas/flux/compose.js';
import { loadHostConfigs } from '../../services/docker.js';
import type { HostConfig } from '../../types.js';
import { createExhaustiveError } from './base-handler.js';
import {
formatComposeListMarkdown,
formatComposeStatusMarkdown
} from '../../formatters/index.js';
import {
handleComposeUp,
handleComposeDown,
handleComposeRestart,
handleComposeLogs,
handleComposeBuild,
handleComposePull,
handleComposeRecreate,
handleComposeRefresh
} from './compose-handlers.js';
import { HostResolver } from '../../services/host-resolver.js';
import { assertValidAction, initializeHandler, formatResponse, paginateItems } from './base-handler.js';
/**
* Handle all compose subactions
*
* Subactions: list, status, up, down, restart, logs, build, pull, recreate
*/
export async function handleComposeAction(
input: FluxInput,
container: ServiceContainer
): Promise<string> {
assertValidAction(input, 'compose', 'compose');
const { service: composeService, hosts, format } = initializeHandler(
input,
container,
(c) => c.getComposeService(),
loadHostConfigs()
);
// Cast to the compose action union type - validated by Zod
const inp = input as ComposeActionInput;
switch (inp.subaction) {
case 'list': {
const listInput = inp as ComposeListInput;
// If no host specified, aggregate from ALL hosts
if (!listInput.host) {
const allProjects = await Promise.all(
hosts.map(async (h) => {
try {
const projects = await composeService.listComposeProjects(h);
return projects.map(p => ({ ...p, host: h.name }));
} catch (error) {
console.error(`Failed to list projects on ${h.name}:`, error);
return [];
}
})
);
let projects = allProjects.flat();
// Apply name filter
if (listInput.name_filter) {
const nameFilter = listInput.name_filter;
projects = projects.filter(p =>
p.name.toLowerCase().includes(nameFilter.toLowerCase())
);
}
// Apply pagination
const { items: paginatedProjects, total, offset, hasMore } =
paginateItems(projects, { offset: listInput.offset, limit: listInput.limit });
return formatResponse(
projects,
format,
() => formatComposeListMarkdown(paginatedProjects, 'all-hosts', total, offset, hasMore)
);
}
// Single host mode (existing logic)
const hostConfig = hosts.find(h => h.name === listInput.host);
if (!hostConfig) {
throw new Error(`Host not found: ${listInput.host}`);
}
let projects = await composeService.listComposeProjects(hostConfig);
// Apply name filter if specified
if (listInput.name_filter) {
const nameFilter = listInput.name_filter;
projects = projects.filter(p =>
p.name.toLowerCase().includes(nameFilter.toLowerCase())
);
}
// Apply pagination
const { items: paginatedProjects, total, offset, hasMore } =
paginateItems(projects, { offset: listInput.offset, limit: listInput.limit });
return formatResponse(
projects,
format,
() => formatComposeListMarkdown(paginatedProjects, hostConfig.name, total, offset, hasMore)
);
}
case 'status': {
const statusInput = inp as ComposeStatusInput;
let hostConfig: HostConfig;
// Use HostResolver for auto-discovery if no host specified
if (!statusInput.host) {
const resolver = new HostResolver(container.getComposeDiscovery(), hosts);
hostConfig = await resolver.resolveHost(statusInput.project, undefined);
} else {
const found = hosts.find(h => h.name === statusInput.host);
if (!found) {
throw new Error(`Host not found: ${statusInput.host}`);
}
hostConfig = found;
}
const project = await composeService.getComposeStatus(hostConfig, statusInput.project);
// Apply service filter if specified
let services = project.services;
if (statusInput.service_filter) {
const serviceFilter = statusInput.service_filter;
services = services.filter(s =>
s.name.toLowerCase().includes(serviceFilter.toLowerCase())
);
project.services = services;
}
// Apply pagination to services
const { items: paginatedServices, total: totalServices, offset, hasMore } =
paginateItems(services, { offset: statusInput.offset, limit: statusInput.limit });
const paginatedProject = { ...project, services: paginatedServices };
return formatResponse(
project,
format,
() => formatComposeStatusMarkdown(paginatedProject, totalServices, offset, hasMore)
);
}
case 'up': {
const upInput = inp as ComposeUpInput;
return handleComposeUp(upInput, hosts, container);
}
case 'down': {
const downInput = inp as ComposeDownInput;
return handleComposeDown(downInput, hosts, container);
}
case 'restart': {
const restartInput = inp as ComposeRestartInput;
return handleComposeRestart(restartInput, hosts, container);
}
case 'logs': {
const logsInput = inp as ComposeLogsInput;
return handleComposeLogs(logsInput, hosts, container);
}
case 'build': {
const buildInput = inp as ComposeBuildInput;
return handleComposeBuild(buildInput, hosts, container);
}
case 'pull': {
const pullInput = inp as ComposePullInput;
return handleComposePull(pullInput, hosts, container);
}
case 'recreate': {
const recreateInput = inp as ComposeRecreateInput;
return handleComposeRecreate(recreateInput, hosts, container);
}
case 'refresh': {
const refreshInput = inp as ComposeRefreshInput;
return handleComposeRefresh(refreshInput, hosts, container);
}
default:
throw createExhaustiveError(inp as never, 'compose subaction');
}
}