/**
* Base handler utilities for reducing boilerplate across handlers
*
* Consolidates common patterns found in all handler files:
* - Initialization (service extraction, format resolution)
* - Host lookup and resolution
* - Action validation
* - Response formatting
* - Pagination
* - String utilities
*/
import type { HostConfig, ResponseFormat } from "../../types.js";
import { ResponseFormat as ResponseFormatEnum } from "../../types.js";
import type { ServiceContainer } from "../../services/container.js";
/**
* Common handler context with services and configuration
*/
export interface HandlerContext<TService> {
service: TService;
hosts: HostConfig[];
format: ResponseFormat;
}
/**
* Initialize handler context with common setup
* Consolidates repeated initialization from 6+ handlers
*
* @param input - Input object containing optional response_format
* @param container - Service container for dependency injection
* @param getService - Function to extract specific service from container
* @param hosts - Pre-loaded host configurations
* @returns Handler context with service, hosts, and format
*/
export function initializeHandler<TService>(
input: { response_format?: ResponseFormat },
container: ServiceContainer,
getService: (container: ServiceContainer) => TService,
hosts: HostConfig[]
): HandlerContext<TService> {
const service = getService(container);
const format = input.response_format ?? ResponseFormatEnum.MARKDOWN;
return { service, hosts, format };
}
/**
* Find host by name or throw if not found
* Consolidates host lookup pattern from container.ts, docker.ts, host.ts, scout-logs.ts
*
* @param hosts - Array of host configurations to search
* @param hostName - Name of the host to find
* @returns The found host configuration
* @throws Error if host is not found
*/
export function getHostConfigOrThrow(
hosts: HostConfig[],
hostName: string
): HostConfig {
const host = hosts.find((h) => h.name === hostName);
if (!host) {
throw new Error(`Host not found: ${hostName}`);
}
return host;
}
/**
* Find host by name, returns undefined if not provided or not found
*
* @param hosts - Array of host configurations to search
* @param hostName - Name of the host to find (optional)
* @returns The found host configuration or undefined
*/
export function getHostConfigOptional(
hosts: HostConfig[],
hostName: string | undefined
): HostConfig | undefined {
if (!hostName) return undefined;
return hosts.find((h) => h.name === hostName);
}
/**
* Resolve target hosts - single host if specified, all hosts otherwise
* Consolidates pattern from compose.ts, host.ts, docker.ts
*
* @param hosts - Array of all available host configurations
* @param hostName - Optional specific host name to filter to
* @returns Array containing single host if specified, or all hosts
* @throws Error if specified host is not found
*/
export function resolveTargetHosts(
hosts: HostConfig[],
hostName: string | undefined
): HostConfig[] {
if (!hostName) return hosts;
const host = hosts.find((h) => h.name === hostName);
if (!host) {
throw new Error(`Host not found: ${hostName}`);
}
return [host];
}
/**
* Validate action matches handler type
* Consolidates action validation from all handlers
*
* @param input - Input object with action property
* @param expectedAction - The expected action value
* @param handlerName - Name of the handler (for error messages)
* @throws Error if action doesn't match expected
*/
export function assertValidAction(
input: { action: string } | Record<string, unknown>,
expectedAction: string,
handlerName: string
): void {
const action = 'action' in input ? input.action : undefined;
if (action !== expectedAction) {
throw new Error(`Invalid action for ${handlerName} handler: ${action}`);
}
}
/**
* Format response based on format preference
* Consolidates 40+ occurrences across all handlers
*
* @param data - The data to format
* @param format - The desired output format (JSON or MARKDOWN)
* @param markdownFormatter - Function to convert data to markdown
* @returns Formatted string (JSON or markdown)
*/
export function formatResponse<T>(
data: T,
format: ResponseFormat,
markdownFormatter: (data: T) => string
): string {
if (format === ResponseFormatEnum.JSON) {
return JSON.stringify(data, null, 2);
}
return markdownFormatter(data);
}
/**
* Pagination options for list operations
*/
export interface PaginationOptions {
offset?: number;
limit?: number;
}
/**
* Result of pagination operation
*/
export interface PaginatedResult<T> {
items: T[];
total: number;
offset: number;
limit: number;
hasMore: boolean;
}
/**
* Apply pagination to array and return paginated result
* Consolidates pagination pattern from 8+ locations
*
* @param items - Full array of items to paginate
* @param options - Pagination options (offset, limit)
* @returns Paginated result with items and metadata
*/
export function paginateItems<T>(
items: T[],
options: PaginationOptions
): PaginatedResult<T> {
const offset = options.offset ?? 0;
const limit = options.limit ?? 50;
const total = items.length;
const paginatedItems = items.slice(offset, offset + limit);
const hasMore = offset + limit < total;
return {
items: paginatedItems,
total,
offset,
limit,
hasMore
};
}
/**
* Create exhaustive check error for switch statements
* Consolidates pattern from all handlers
*
* @param value - The unhandled value (should be never type)
* @param context - Context description for error message
* @returns Error with descriptive message
*/
export function createExhaustiveError(value: never, context: string): Error {
const subaction = (value as { subaction?: string }).subaction;
return new Error(`Unknown ${context}: ${subaction ?? value}`);
}
/**
* Resolve non-empty string or undefined
* Consolidates utility from container.ts
*
* @param value - Value to check
* @returns Trimmed string if valid and non-empty, undefined otherwise
*/
export function resolveNonEmptyString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
/**
* Get optional string field with type checking
* Consolidates utility from host.ts
*
* @param value - Value to check
* @param fieldName - Name of the field (for error messages)
* @returns The string value or undefined
* @throws Error if value is not undefined and not a string
*/
export function getOptionalString(
value: unknown,
fieldName: string
): string | undefined {
if (value === undefined) return undefined;
if (typeof value === "string") return value;
throw new Error(`Invalid ${fieldName}: expected string`);
}
/**
* Get host name from input object with validation
* Consolidates getHostName from host.ts
*
* @param input - Input object that may contain a host field
* @returns The host name string or undefined
* @throws Error if host is present but not a string
*/
export function getHostNameFromInput(
input: Record<string, unknown>
): string | undefined {
if (!("host" in input)) return undefined;
const hostValue = input.host;
if (hostValue === undefined) return undefined;
if (typeof hostValue === "string") return hostValue;
throw new Error("Invalid host: expected string");
}