ecs-pf
Version:
CLI for port-forwarding to RDS via AWS ECS
384 lines (383 loc) • 15.6 kB
JavaScript
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);
}