UNPKG

ecs-pf

Version:

CLI for port-forwarding to RDS via AWS ECS

384 lines (383 loc) 15.6 kB
import chalk from "chalk"; import Fuse from "fuse.js"; import { isEmpty, isObjectType } from "remeda"; import { formatInferenceResult, } from "./inference/index.js"; import { splitByWhitespace } from "./regex.js"; function hasRegionName(item) { return isObjectType(item) && "regionName" in item; } function hasClusterName(item) { return isObjectType(item) && "clusterName" in item; } function hasDbInstanceIdentifier(item) { return isObjectType(item) && "dbInstanceIdentifier" in item; } function getResourceIdentifier(item) { if (hasRegionName(item)) return item.regionName; if (hasClusterName(item)) return item.clusterName; if (hasDbInstanceIdentifier(item)) return item.dbInstanceIdentifier; return undefined; } export async function universalSearch(config, input, defaultValue) { const { items, searchKeys, displayFormatter, emptyInputFormatter = displayFormatter, threshold = 0.3, distance = 100, } = config; if (!input || input.trim() === "") { const sortedItems = defaultValue ? (() => { const defaultKey = typeof defaultValue === "string" ? defaultValue : null; return [ ...items.filter((item) => { if (defaultKey) { return getResourceIdentifier(item) === defaultKey; } return item === defaultValue; }), ...items.filter((item) => { if (defaultKey) { return getResourceIdentifier(item) !== defaultKey; } return item !== defaultValue; }), ]; })() : items; return sortedItems.map((item, index) => { const isDefault = defaultValue && ((typeof defaultValue === "string" && getResourceIdentifier(item) === defaultValue) || item === defaultValue); return emptyInputFormatter(item, index, !!isDefault); }); } const getSearchableText = (item) => { return searchKeys .map((key) => { const keys = key.split("."); let value = item; for (const k of keys) { if (value && typeof value === "object" && k in value) { value = value[k]; } else { value = undefined; break; } } return value || ""; }) .filter(Boolean) .join(" ") .toLowerCase(); }; const keywords = splitByWhitespace(input.trim().toLowerCase()).filter((keyword) => keyword.length > 0); if (keywords.length > 0) { const keywordFiltered = items.filter((item) => { const searchableText = getSearchableText(item); return keywords.every((keyword) => searchableText.includes(keyword)); }); if (keywordFiltered.length > 0) { return keywordFiltered .sort((a, b) => { if (defaultValue) { const aIsDefault = typeof defaultValue === "string" ? getResourceIdentifier(a) === defaultValue : a === defaultValue; const bIsDefault = typeof defaultValue === "string" ? getResourceIdentifier(b) === defaultValue : b === defaultValue; if (aIsDefault && !bIsDefault) return -1; if (!aIsDefault && bIsDefault) return 1; } const aText = getSearchableText(a); const bText = getSearchableText(b); const aExactMatches = keywords.filter((keyword) => splitByWhitespace(aText).includes(keyword)).length; const bExactMatches = keywords.filter((keyword) => splitByWhitespace(bText).includes(keyword)).length; if (aExactMatches !== bExactMatches) { return bExactMatches - aExactMatches; } return aText.localeCompare(bText); }) .map((item, index) => { const isDefault = defaultValue && ((typeof defaultValue === "string" && getResourceIdentifier(item) === defaultValue) || item === defaultValue); return displayFormatter(item, index, !!isDefault); }); } } const fuseOptions = { keys: searchKeys, threshold, distance, includeScore: true, minMatchCharLength: 2, findAllMatches: true, }; const fuse = new Fuse(items, fuseOptions); const results = fuse.search(input); return results .slice(0, 10) .sort((a, b) => { if (defaultValue) { const aIsDefault = typeof defaultValue === "string" ? getResourceIdentifier(a.item) === defaultValue : a.item === defaultValue; const bIsDefault = typeof defaultValue === "string" ? getResourceIdentifier(b.item) === defaultValue : b.item === defaultValue; if (aIsDefault && !bIsDefault) return -1; if (!aIsDefault && bIsDefault) return 1; } return (a.score || 0) - (b.score || 0); }) .map((result, index) => { const isDefault = defaultValue && ((typeof defaultValue === "string" && getResourceIdentifier(result.item) === defaultValue) || result.item === defaultValue); return displayFormatter(result.item, index, !!isDefault, result.score); }); } export function keywordSearch(items, input, searchFields) { if (!input || input.trim() === "") { return items; } const keywords = splitByWhitespace(input.trim().toLowerCase()).filter((keyword) => keyword.length > 0); if (isEmpty(keywords)) { return items; } return items.filter((item) => { const searchableText = searchFields(item) .filter(Boolean) .join(" ") .toLowerCase(); return keywords.every((keyword) => searchableText.includes(keyword)); }); } export async function searchRegions(regions, input, defaultRegion) { const config = { items: regions, searchKeys: ["regionName"], displayFormatter: (region, index, isDefault, score) => { const icon = index === 0 ? chalk.green("•") : " "; const defaultLabel = isDefault ? chalk.cyan(" (default)") : ""; const scoreLabel = score ? ` [${((1 - score) * 100).toFixed(0)}%]` : ""; return { name: `${icon} ${region.regionName}${defaultLabel} ${chalk.dim(`(${region.optInStatus})${scoreLabel}`)}`, value: region.regionName, }; }, emptyInputFormatter: (region, index, isDefault) => { const icon = index === 0 && isDefault ? chalk.green("•") : " "; const defaultLabel = isDefault ? chalk.cyan(" (default)") : ""; return { name: `${icon} ${region.regionName}${defaultLabel} ${chalk.dim(`(${region.optInStatus})`)}`, value: region.regionName, }; }, }; return universalSearch(config, input, defaultRegion); } export async function searchClusters(clusters, input) { const config = { items: clusters, searchKeys: ["clusterName"], displayFormatter: (cluster, index, _isDefault, score) => { const icon = index === 0 ? chalk.green("•") : " "; const clusterShortName = cluster.clusterArn.split("/").pop(); const scoreLabel = score ? ` [${((1 - score) * 100).toFixed(0)}%]` : ""; return { name: `${icon} ${cluster.clusterName} ${chalk.dim(`(${clusterShortName})${scoreLabel}`)}`, value: cluster, }; }, emptyInputFormatter: (cluster, _index) => { const icon = " "; const clusterShortName = cluster.clusterArn.split("/").pop(); return { name: `${icon} ${cluster.clusterName} ${chalk.dim(`(${clusterShortName})`)}`, value: cluster, }; }, }; return universalSearch(config, input); } export async function searchServices(services, input) { const config = { items: services, searchKeys: ["serviceName", "clusterName"], displayFormatter: (service, index, _isDefault, score) => { const icon = index === 0 ? chalk.green("•") : " "; const statusIcon = service.status === "ACTIVE" ? "🟢" : "🟡"; const scoreLabel = score ? ` [${((1 - score) * 100).toFixed(0)}%]` : ""; const execStatus = service.enableExecuteCommand ? chalk.green("exec enabled") : chalk.red("exec disabled"); return { name: `${icon} ${service.serviceName} ${chalk.dim(`(${service.clusterName}) ${statusIcon} ${service.runningCount}/${service.desiredCount} running - ${execStatus}${scoreLabel}`)}`, value: service, }; }, emptyInputFormatter: (service, _index) => { const icon = " "; const statusIcon = service.status === "ACTIVE" ? "🟢" : "🟡"; const execStatus = service.enableExecuteCommand ? chalk.green("exec enabled") : chalk.red("exec disabled"); return { name: `${icon} ${service.serviceName} ${chalk.dim(`(${service.clusterName}) ${statusIcon} ${service.runningCount}/${service.desiredCount} running - ${execStatus}`)}`, value: service, }; }, }; return universalSearch(config, input); } export async function searchTasks(tasks, input) { const config = { items: tasks, searchKeys: ["serviceName", "taskId", "displayName", "taskStatus"], displayFormatter: (task, index, _isDefault, score) => { const icon = index === 0 ? chalk.green("•") : " "; const taskShortId = task.taskId.substring(0, 8); const scoreLabel = score ? ` [${((1 - score) * 100).toFixed(0)}%]` : ""; return { name: `${icon} ${task.serviceName} ${chalk.dim(`(${taskShortId}...)${scoreLabel}`)}`, value: task.taskArn, }; }, emptyInputFormatter: (task) => { const taskShortId = task.taskId.substring(0, 8); return { name: ` ${task.serviceName} ${chalk.dim(`(${taskShortId}...)`)}`, value: task.taskArn, }; }, }; return universalSearch(config, input); } export async function searchRDS(rdsInstances, input) { const config = { items: rdsInstances, searchKeys: ["dbInstanceIdentifier", "engine", "endpoint"], displayFormatter: (rds, index, _isDefault, score) => { const icon = index === 0 ? chalk.green("•") : " "; const scoreLabel = score ? ` [${((1 - score) * 100).toFixed(0)}%]` : ""; return { name: `${icon} (${rds.engine}): ${rds.dbInstanceIdentifier} ${chalk.dim(scoreLabel)}`, value: rds, }; }, emptyInputFormatter: (rds) => { return { name: ` (${rds.engine}): ${rds.dbInstanceIdentifier}`, value: rds, }; }, }; return universalSearch(config, input); } export async function searchContainers(containers, input) { if (!input || input.trim() === "") { return containers.map((container) => ({ name: ` ${container}`, value: container, })); } const filteredContainers = keywordSearch(containers, input, (container) => [ container, ]); if (filteredContainers.length > 0) { return filteredContainers .sort((a, b) => a.localeCompare(b)) .map((container, index) => { const icon = index === 0 ? chalk.green("•") : " "; return { name: `${icon} ${container}`, value: container, }; }); } const fuzzyMatches = containers.filter((container) => container.toLowerCase().includes(input.toLowerCase())); return fuzzyMatches .slice(0, 5) .sort((a, b) => a.localeCompare(b)) .map((container, index) => { const icon = index === 0 ? chalk.green("•") : " "; return { name: `${icon} ${container} ${chalk.dim("[fuzzy]")}`, value: container, }; }); } export async function searchInferenceResults(results, input) { if (!input || input.trim() === "") { return results.map((result) => { const isUnavailable = result.reason.includes("接続不可"); return { name: ` ${formatInferenceResult(result)}`, value: result, disabled: isUnavailable ? "Task stopped - Cannot select" : undefined, }; }); } const keywordFiltered = keywordSearch(results, input, (result) => [ result.cluster.clusterName, result.task.displayName, result.task.serviceName, result.task.taskStatus, result.task.runtimeId, result.confidence, result.method, result.reason, formatInferenceResult(result), result.confidence === "high" ? "high 高" : "", result.confidence === "medium" ? "medium 中" : "", result.confidence === "low" ? "low 低" : "", ]); if (keywordFiltered.length > 0) { return keywordFiltered .sort((a, b) => { const confidenceOrder = { high: 3, medium: 2, low: 1 }; const confidenceDiff = confidenceOrder[b.confidence] - confidenceOrder[a.confidence]; if (confidenceDiff !== 0) return confidenceDiff; return b.score - a.score; }) .map((result, index) => { const icon = index === 0 ? chalk.green("•") : " "; const isUnavailable = result.reason.includes("接続不可"); return { name: `${icon} ${formatInferenceResult(result)}`, value: result, disabled: isUnavailable ? "Task stopped - Cannot select" : undefined, }; }); } const config = { items: results, searchKeys: [ "cluster.clusterName", "task.displayName", "task.serviceName", "confidence", ], displayFormatter: (result, index, _isDefault, score) => { const icon = index === 0 ? chalk.green("•") : " "; const scoreLabel = score ? ` [${((1 - score) * 100).toFixed(0)}%]` : ""; const isUnavailable = result.reason.includes("接続不可"); return { name: `${icon} ${formatInferenceResult(result)} ${chalk.dim(`[fuzzy]${scoreLabel}`)}`, value: result, disabled: isUnavailable ? "Task stopped - Cannot select" : undefined, }; }, }; return universalSearch(config, input); }