@hyperlane-xyz/cli
Version:
A command-line utility for common Hyperlane operations
128 lines • 5.13 kB
JavaScript
import { Separator, confirm } from '@inquirer/prompts';
import search from '@inquirer/search';
import select from '@inquirer/select';
import chalk from 'chalk';
import { toTitleCase } from '@hyperlane-xyz/utils';
import { log } from '../logger.js';
import { calculatePageSize } from './cli-options.js';
import { searchableCheckBox } from './input.js';
// A special value marker to indicate user selected
// a new chain in the list
const NEW_CHAIN_MARKER = '__new__';
export async function runSingleChainSelectionStep(chainMetadata, message = 'Select chain') {
const networkType = await selectNetworkType();
const { choices, networkTypeSeparator } = getChainChoices(chainMetadata, networkType);
const formattedMessage = message.endsWith(':') ? message : `${message}:`;
const options = [networkTypeSeparator, ...choices];
const chain = (await search({
message: formattedMessage,
source: (searchTerm) => {
if (!searchTerm) {
return options;
}
return options.filter((value) => Separator.isSeparator(value) || value.value.includes(searchTerm));
},
pageSize: calculatePageSize(2),
}));
handleNewChain([chain]);
return chain;
}
export async function runMultiChainSelectionStep({ chainMetadata, message = 'Select chains', requireNumber = 0, requiresConfirmation = false, networkType = undefined, }) {
const selectedNetworkType = networkType ?? (await selectNetworkType());
const { choices, networkTypeSeparator } = getChainChoices(chainMetadata, selectedNetworkType);
let currentChoiceSelection = new Set();
while (true) {
const chains = await searchableCheckBox({
message,
selectableOptionsSeparator: networkTypeSeparator,
choices: choices.map((choice) => currentChoiceSelection.has(choice.name)
? { ...choice, checked: true }
: choice),
instructions: `Use the TAB or the SPACE key to select at least ${requireNumber} chains, then press ENTER to proceed. Type to search for a specific chain.`,
theme: {
style: {
// The leading space is needed because the help tip will be tightly close to the message header
helpTip: (text) => ` ${chalk.bgYellow(text)}`,
},
helpMode: 'always',
},
pageSize: calculatePageSize(2),
validate: (answer) => {
if (answer.length < requireNumber) {
return `Please select at least ${requireNumber} chains`;
}
return true;
},
});
handleNewChain(chains);
const confirmed = requiresConfirmation
? await confirm({
message: `Is this chain selection correct?: ${chalk.cyan(chains.join(', '))}`,
})
: true;
if (!confirmed) {
currentChoiceSelection = new Set(chains);
continue;
}
return chains;
}
}
async function selectNetworkType() {
const networkType = await select({
message: 'Select network type',
choices: [
{ name: 'Mainnet', value: 'mainnet' },
{ name: 'Testnet', value: 'testnet' },
],
});
return networkType;
}
function getChainChoices(chainMetadata, networkType) {
const chainsToChoices = (chains) => chains
.map((c) => ({ name: c.name, value: c.name }))
.sort((a, b) => a.name.localeCompare(b.name));
const chains = Object.values(chainMetadata);
const filteredChains = chains.filter((c) => networkType === 'mainnet' ? !c.isTestnet : !!c.isTestnet);
const choices = [
{ name: '(New custom chain)', value: NEW_CHAIN_MARKER },
...chainsToChoices(filteredChains),
];
return {
choices,
networkTypeSeparator: new Separator(`--${toTitleCase(networkType)} Chains--`),
};
}
function handleNewChain(chainNames) {
if (chainNames.includes(NEW_CHAIN_MARKER)) {
log(chalk.blue('Use the'), chalk.magentaBright('hyperlane registry init'), chalk.blue('command to create new configs'));
process.exit(0);
}
}
/**
* @notice Extracts chain names from a nested configuration object
* @param config Object to search for chain names
* @return Array of discovered chain names
*/
export function extractChainsFromObj(config) {
const chains = [];
// Recursively search for chain/chainName fields
function findChainFields(obj) {
if (obj === null || typeof obj !== 'object')
return;
if (Array.isArray(obj)) {
obj.forEach((item) => findChainFields(item));
return;
}
if ('chain' in obj) {
chains.push(obj.chain);
}
if ('chainName' in obj) {
chains.push(obj.chainName);
}
// Recursively search in all nested values
Object.values(obj).forEach((value) => findChainFields(value));
}
findChainFields(config);
return chains;
}
//# sourceMappingURL=chains.js.map