diff --git a/packages/tools/kadena-cli/src/config/initConfigCommand.ts b/packages/tools/kadena-cli/src/config/initConfigCommand.ts index e266b7c00c..719f392d91 100644 --- a/packages/tools/kadena-cli/src/config/initConfigCommand.ts +++ b/packages/tools/kadena-cli/src/config/initConfigCommand.ts @@ -12,6 +12,9 @@ export function initCommand(program: Command, version: string): void { await import('./../networks/init.js'); console.log(chalk.green('Configured default networks.')); + await import('./../devnet/init.js'); + console.log(chalk.green('Configured default devnets.')); + console.log(chalk.green('Configuration complete!')); }); } diff --git a/packages/tools/kadena-cli/src/constants/devnets.ts b/packages/tools/kadena-cli/src/constants/devnets.ts index fdd22a4b89..689cd89019 100644 --- a/packages/tools/kadena-cli/src/constants/devnets.ts +++ b/packages/tools/kadena-cli/src/constants/devnets.ts @@ -1,7 +1,7 @@ -import type { TDevnetsCreateOptions } from '../devnet/devnetsCreateQuestions.js'; +import { IDevnetsCreateOptions } from "../devnet/devnetHelpers.js"; export interface IDefaultDevnetOptions { - [key: string]: TDevnetsCreateOptions; + [key: string]: IDevnetsCreateOptions; } /** @@ -16,13 +16,6 @@ export const devnetDefaults: IDefaultDevnetOptions = { mountPactFolder: '', version: 'latest', }, - other: { - name: '', - port: 8080, - useVolume: false, - mountPactFolder: '', - version: '', - }, }; export const defaultDevnetsPath: string = `${process.cwd()}/.kadena/devnets`; diff --git a/packages/tools/kadena-cli/src/constants/options.ts b/packages/tools/kadena-cli/src/constants/options.ts deleted file mode 100644 index 85303435a7..0000000000 --- a/packages/tools/kadena-cli/src/constants/options.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Option } from "commander"; - -export const account = new Option('-a, --account ', 'Receiver (k:) wallet address'); - -export const chainId = new Option('-c, --chainId ', 'Chain to retrieve from (default 1)') - .argParser((value) => parseInt(value, 10)); - -export const network = new Option('-n, --network ', 'Kadena network (e.g. "mainnet")'); - -export const networkId = new Option('-nid, --networkId ', 'Kadena network Id (e.g. "mainnet01")'); - -export const networkHost = new Option('-h, --networkHost ', 'Kadena network host (e.g. "https://api.chainweb.com")'); - -export const networkExplorerUrl = new Option('-e, --networkExplorerUrl ', 'Kadena network explorer URL (e.g. "https://explorer.chainweb.com/mainnet/tx/")'); diff --git a/packages/tools/kadena-cli/src/constants/prompts.ts b/packages/tools/kadena-cli/src/constants/prompts.ts index b4f901ae15..9e358103bd 100644 --- a/packages/tools/kadena-cli/src/constants/prompts.ts +++ b/packages/tools/kadena-cli/src/constants/prompts.ts @@ -3,7 +3,7 @@ import { program } from 'commander'; import path from 'path'; import { ICustomNetworksChoice } from '../networks/networksHelpers.js'; import { ensureFileExists } from '../utils/filesystem.js'; -import { getExistingAccounts, getExistingKeypairs, getExistingKeysets, getExistingNetworks } from '../utils/helpers.js'; +import { getExistingAccounts, getExistingDevnets, getExistingKeypairs, getExistingKeysets, getExistingNetworks } from '../utils/helpers.js'; import { defaultNetworksPath } from './networks.js'; import { ChainId } from '@kadena/types'; import { defaultKeypairsPath } from './keypairs.js'; @@ -11,6 +11,8 @@ import { ICustomKeypairsChoice } from '../keypair/keypairHelpers.js'; import { defaultKeysetsPath } from './keysets.js'; import { ICustomKeysetsChoice } from '../keyset/keysetHelpers.js'; import { ICustomAccountsChoice } from '../account/accountHelpers.js'; +import { defaultDevnetsPath } from './devnets.js'; +import { ICustomDevnetsChoice } from '../devnet/devnetHelpers.js'; export const gasPayerPrompt = async (): Promise => { const existingAccounts: ICustomAccountsChoice[] = await getExistingAccounts(); @@ -353,3 +355,122 @@ export const selectKeypairsPrompt = async () => { choices: existingKeypairs, }) } + +export const devnetNamePrompt = async (): Promise => { + const containerName = await input({ + message: 'Enter a devnet name (e.g. "devnet")', + }); + + const filePath = path.join(defaultDevnetsPath, `${containerName}.yaml`); + if (ensureFileExists(filePath)) { + const overwrite = await devnetOverwritePrompt(); + if (overwrite === 'no') { + return await devnetNamePrompt(); + } + } + + return containerName; +}; + +export const devnetOverwritePrompt = async (name?: string) => { + const message = name + ? `Are you sure you want to save this configuration for devnet "${name}"?` + : 'A devnet configuration with this name already exists. Do you want to update it?' + + return await select({ + message, + choices: [ + { value: 'yes', name: 'Yes' }, + { value: 'no', name: 'No' }, + ], + }); +}; + +export const devnetPortPrompt = async (): Promise => { + const port = await input({ + default: '8080', + message: 'Enter a port number to forward to the Chainweb node API', + validate: function (input) { + const port = parseInt(input); + if (isNaN(port)) { + return 'Port must be a number! Please enter a valid port number.'; + } + return true; + }, + }); + return parseInt(port); +}; + +export const devnetUseVolumePrompt = async (): Promise => + await select({ + message: 'Would you like to create a persistent volume?', + choices: [ + { value: false, name: 'No' }, + { value: true, name: 'Yes' }, + ], + }); + +export const devnetMountPactFolderPrompt = async (): Promise => + await input({ + default: '', + message: + 'Enter the relative path to a folder containing your Pact files to mount (e.g. ./pact) or leave empty to skip.', + }); + + export const devnetVersionPrompt = async (): Promise => + await input({ + default: 'latest', + message: + 'Enter the version of the kadena/devnet image you would like to use.', + }); + +export const devnetSelectPrompt = async (): Promise => { + const existingDevnets: ICustomDevnetsChoice[] = await getExistingDevnets(); + + if (existingDevnets.length > 0) { + return await select({ + message: 'Select a devnet', + choices: existingDevnets, + }); + } + + // At this point there is no devnet defined yet. + // Create and select a new devnet. + await program.parseAsync(['', '', 'devnet', 'create']); + + return await networkSelectPrompt(); +}; + +export const devnetDeletePrompt = async (name: string) => + await select({ + message: `Are you sure you want to delete the devnet "${name}"?`, + choices: [ + { value: 'yes', name: 'Yes' }, + { value: 'no', name: 'No' }, + ], + }); + +export const devnetPrompt = async (): Promise => { + const existingDevnets: ICustomDevnetsChoice[] = await getExistingDevnets(); + + if (existingDevnets.length > 0) { + const selectedDevnet = await select({ + message: 'Select a devnet', + choices: [ + ...existingDevnets, + { value: undefined, name: 'Create a new devnet' }, + ], + }); + + if (selectedDevnet !== undefined) { + return selectedDevnet; + } + } + + // At this point there is either no devnet defined yet, + // or the user chose to create a new devnet. + // Create and select new devnet. + await program.parseAsync(['', '', 'devnet', 'create']); + + return await devnetPrompt(); +}; diff --git a/packages/tools/kadena-cli/src/constants/questions.ts b/packages/tools/kadena-cli/src/constants/questions.ts deleted file mode 100644 index 552eabe3f4..0000000000 --- a/packages/tools/kadena-cli/src/constants/questions.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from "zod"; -import { accountPrompt } from "./prompts.js"; -import { account } from "./options.js"; - -// eslint-disable-next-line @rushstack/typedef-var -export const Questions = z.object({ - account: z - .string(), - // .min(60, { message: 'Account must be 60 or more characters long' }) - // .startsWith('k:', { message: 'Account should start with k:' }), - chainId: z - .number({ - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - invalid_type_error: 'Error: -c, --chain must be a number', - }) - .min(0) - .max(19), - network: z.string({}), - networkId: z.string().optional(), - networkHost: z.string().optional(), - networkExplorerUrl: z.string().optional(), -}); - -export const options = { - account: { - prompt: accountPrompt, - validation: z.string(), - option: new Option('-a, --account ', 'Receiver (k:) wallet address'), - }, - chainId: { - - }, - network: z.string({}), -}; - -export const chainId = z.number(); - -export type TQuestions = z.infer; - - diff --git a/packages/tools/kadena-cli/src/devnet/createDevnetCommand.ts b/packages/tools/kadena-cli/src/devnet/createDevnetCommand.ts new file mode 100644 index 0000000000..1cfc531cae --- /dev/null +++ b/packages/tools/kadena-cli/src/devnet/createDevnetCommand.ts @@ -0,0 +1,40 @@ +import { defaultDevnetsPath } from '../constants/devnets.js'; +import { ensureFileExists } from '../utils/filesystem.js'; +import { writeDevnet } from './devnetHelpers.js'; + +import debug from 'debug'; +import path from 'path'; + +import { createCommand } from '../utils/createCommand.js'; +import { globalOptions } from '../utils/globalOptions.js'; +import chalk from 'chalk'; +import { devnetOverwritePrompt } from '../constants/prompts.js'; + +export const createDevnetCommand = createCommand( + 'create', + 'Create devnet', + [ + globalOptions.devnetName(), + globalOptions.devnetPort(), + globalOptions.devnetUseVolume(), + globalOptions.devnetMountPactFolder(), + globalOptions.devnetVersion(), + ], + async (config) => { + debug('devnet-create:action')({config}); + + const filePath = path.join(defaultDevnetsPath, `${config.name}.yaml`); + + if (ensureFileExists(filePath)) { + const overwrite = await devnetOverwritePrompt(config.name); + if (overwrite === 'no') { + console.log(chalk.yellow(`\nThe existing devnet configuration "${config.name}" will not be updated.\n`)); + return; + } + } + + writeDevnet(config); + + console.log(chalk.green(`\nThe devnet configuration "${config.name}" has been saved.\n`)); + }, +); diff --git a/packages/tools/kadena-cli/src/devnet/createDevnetsCommand.ts b/packages/tools/kadena-cli/src/devnet/createDevnetsCommand.ts deleted file mode 100644 index 73f169a560..0000000000 --- a/packages/tools/kadena-cli/src/devnet/createDevnetsCommand.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { defaultDevnetsPath } from '../constants/devnets.js'; -import { ensureFileExists } from '../utils/filesystem.js'; -import { clearCLI, collectResponses } from '../utils/helpers.js'; -import { processZodErrors } from '../utils/processZodErrors.js'; - -import type { TDevnetsCreateOptions } from './devnetsCreateQuestions.js'; -import { - DevnetsCreateOptions, - devnetsCreateQuestions, -} from './devnetsCreateQuestions.js'; -import { displayDevnetConfig, writeDevnets } from './devnetsHelpers.js'; - -import { select } from '@inquirer/prompts'; -import chalk from 'chalk'; -import { Option, type Command } from 'commander'; -import debug from 'debug'; -import path from 'path'; - -async function shouldProceedWithDevnetCreate(devnet: string): Promise { - const filePath = path.join(defaultDevnetsPath, `${devnet}.yaml`); - if (ensureFileExists(filePath)) { - const overwrite = await select({ - message: `Your devnet (config) already exists. Do you want to update it?`, - choices: [ - { value: 'yes', name: 'Yes' }, - { value: 'no', name: 'No' }, - ], - }); - return overwrite === 'yes'; - } - return true; -} - -export async function runDevnetsCreate( - program: Command, - version: string, - args: TDevnetsCreateOptions, -): Promise { - try { - const responses = await collectResponses(args, devnetsCreateQuestions); - - const devnetConfig = { ...args, ...responses }; - - DevnetsCreateOptions.parse(devnetConfig); - - writeDevnets(devnetConfig); - - displayDevnetConfig(devnetConfig); - - const proceed = await select({ - message: 'Is the above devnet configuration correct?', - choices: [ - { value: 'yes', name: 'Yes' }, - { value: 'no', name: 'No' }, - ], - }); - - if (proceed === 'no') { - clearCLI(true); - console.log(chalk.yellow("Let's restart the configuration process.")); - await runDevnetsCreate(program, version, args); - } else { - console.log(chalk.green('Configuration complete. Goodbye!')); - } - } catch (e) { - console.error(e); - processZodErrors(program, e, args); - } -} - -export function createDevnetsCommand(program: Command, version: string): void { - program - .command('create') - .description('Create new devnet') - .option('-n, --name ', 'Container name (e.g. "devnet")') - .addOption( - new Option( - '-p, --port ', - 'Port to forward to the Chainweb node API (e.g. 8080)', - ).argParser((value) => parseInt(value, 10)), - ) - .option( - '-u, --useVolume', - 'Create a persistent volume to mount to the container', - ) - .option( - '-m, --mountPactFolder ', - 'Mount a folder containing Pact files to the container (e.g. "./pact")', - ) - .option( - '-v, --version ', - 'Version of the kadena/devnet Docker image to use (e.g. "latest")', - ) - .action(async (args: TDevnetsCreateOptions) => { - debug('devnet-create:action')({ args }); - - if ( - args.name && - !(await shouldProceedWithDevnetCreate(args.name.toLowerCase())) - ) { - console.log(chalk.red('Devnet creation aborted.')); - return; - } - - await runDevnetsCreate(program, version, args); - }); -} diff --git a/packages/tools/kadena-cli/src/devnet/deleteDevnetCommand.ts b/packages/tools/kadena-cli/src/devnet/deleteDevnetCommand.ts new file mode 100644 index 0000000000..96e82e60f1 --- /dev/null +++ b/packages/tools/kadena-cli/src/devnet/deleteDevnetCommand.ts @@ -0,0 +1,52 @@ +import debug from 'debug'; +import { devnetDeletePrompt } from '../constants/prompts.js'; +import { globalOptions } from '../utils/globalOptions.js'; +import { getDevnetConfiguration, removeDevnetConfiguration } from './devnetHelpers.js'; + +import chalk from 'chalk'; +import { createCommand } from '../utils/createCommand.js'; +import { dockerVolumeName, isDockerInstalled, removeDevnet, removeVolume } from './docker.js'; + +export const deleteDevnetCommand = createCommand( + 'delete', + 'Delete devnet', + [globalOptions.devnetSelect()], + async (config) => { + debug('devnet-delete:action')({config}); + + const deleteDevnet = await devnetDeletePrompt(config.name); + if (deleteDevnet === 'no') { + console.log(chalk.yellow(`\nThe devnet configuration "${config.name}" will not be deleted.\n`)); + return; + } + + if (!isDockerInstalled()) { + console.log( + chalk.red( + 'Stopping devnet requires Docker. Please install Docker and try again.', + ), + ); + return; + } + + removeDevnet(config.name); + console.log(chalk.green(`Removed devnet container: ${config.name}`)); + + const configuration = getDevnetConfiguration(config.name); + + if (configuration?.useVolume) { + removeVolume(config.name); + console.log( + chalk.green( + `Removed volume: ${dockerVolumeName(config.name)}`, + ), + ); + } + + console.log(chalk.green(`Successfully removed devnet container for configuration: ${config.name}`)); + + removeDevnetConfiguration(config); + + console.log(chalk.green(`Successfully removed devnet configuration: ${config.name}`)); + }, +); diff --git a/packages/tools/kadena-cli/src/devnet/devnetsHelpers.ts b/packages/tools/kadena-cli/src/devnet/devnetHelpers.ts similarity index 50% rename from packages/tools/kadena-cli/src/devnet/devnetsHelpers.ts rename to packages/tools/kadena-cli/src/devnet/devnetHelpers.ts index e431c948cc..2de92811fc 100644 --- a/packages/tools/kadena-cli/src/devnet/devnetsHelpers.ts +++ b/packages/tools/kadena-cli/src/devnet/devnetHelpers.ts @@ -1,17 +1,15 @@ import { defaultDevnet, defaultDevnetsPath, devnetDefaults } from '../constants/devnets.js'; -import { PathExists, writeFile } from '../utils/filesystem.js'; +import { PathExists, removeFile, writeFile } from '../utils/filesystem.js'; import { getExistingDevnets, mergeConfigs, sanitizeFilename, } from '../utils/helpers.js'; -import type { TDevnetsCreateOptions } from './devnetsCreateQuestions.js'; - import chalk from 'chalk'; import type { WriteFileOptions } from 'fs'; -import { existsSync, readFileSync, rmSync } from 'fs'; -import yaml, { load } from 'js-yaml'; +import { existsSync, readFileSync } from 'fs'; +import yaml from 'js-yaml'; import path from 'path'; export interface ICustomDevnetsChoice { @@ -21,28 +19,39 @@ export interface ICustomDevnetsChoice { disabled?: boolean | string; } +export interface IDevnetsCreateOptions { + name: string; + port: number; + useVolume: boolean; + mountPactFolder: string; + version: string; +} + /** - * Writes the given devnet setting to the devnet folder - * - * @param {TDevnetsCreateOptions} options - The set of configuration options. - * @param {string} options.name - The name of your devnet container. - * @param {number} options.port - The port to forward to the Chainweb node API. - * @param {boolean} options.useVolume - Whether or not to mount a persistent volume to the container. - * @param {string} options.mountPactFolder - The folder containing Pact files to mount to the container. - * @param {string} options.version - The version of the kadena/devnet image to use. - * @returns {void} - No return value; the function writes directly to a file. - */ -export function writeDevnets(options: TDevnetsCreateOptions): void { +* Writes the given devnet setting to the devnet folder +* +* @param {TDevnetsCreateOptions} options - The set of configuration options. +* @param {string} options.name - The name of your devnet container. +* @param {number} options.port - The port to forward to the Chainweb node API. +* @param {boolean} options.useVolume - Whether or not to mount a persistent volume to the container. +* @param {string} options.mountPactFolder - The folder containing Pact files to mount to the container. +* @param {string} options.version - The version of the kadena/devnet image to use. +* @returns {void} - No return value; the function writes directly to a file. +*/ +export function writeDevnet(options: IDevnetsCreateOptions): void { const { name } = options; - const sanitizedName = sanitizeFilename(name).toLowerCase(); - const devnetFilePath = path.join(defaultDevnetsPath, `${sanitizedName}.yaml`); + const sanitizedDevnet = sanitizeFilename(name).toLowerCase(); + const devnetFilePath = path.join( + defaultDevnetsPath, + `${sanitizedDevnet}.yaml`, + ); - let existingConfig: TDevnetsCreateOptions; + let existingConfig: IDevnetsCreateOptions; if (PathExists(devnetFilePath)) { existingConfig = yaml.load( readFileSync(devnetFilePath, 'utf8'), - ) as TDevnetsCreateOptions; + ) as IDevnetsCreateOptions; } else { // Explicitly check if devnet key exists in devnetDefaults and is not undefined existingConfig = @@ -53,8 +62,6 @@ export function writeDevnets(options: TDevnetsCreateOptions): void { const devnetConfig = mergeConfigs(existingConfig, options); - devnetConfig.mountPactFolder = options.mountPactFolder; - writeFile( devnetFilePath, yaml.dump(devnetConfig), @@ -62,50 +69,45 @@ export function writeDevnets(options: TDevnetsCreateOptions): void { ); } -export function loadDevnet(name: string): TDevnetsCreateOptions | null { - const devnetFilePath = path.join(defaultDevnetsPath, `${name}.yaml`); - const fileExists = existsSync(devnetFilePath); - - if (!fileExists) { - return null; - } - - return yaml.load( - readFileSync(devnetFilePath, 'utf8'), - ) as TDevnetsCreateOptions; -} - -export function removeDevnet(name: string): void { - const devnetFilePath = path.join(defaultDevnetsPath, `${name}.yaml`); - const fileExists = existsSync(devnetFilePath); - - if (!fileExists) { - return; - } +/** + * Removes the given devnet setting from the devnets folder + * + * @param {Pick} options - The set of configuration options. + * @param {string} options.name - The name of the devnet configuration. + */ +export function removeDevnetConfiguration(options: Pick): void { + const { name } = options; + const sanitizedDevnet = sanitizeFilename(name).toLowerCase(); + const devnetFilePath = path.join( + defaultDevnetsPath, + `${sanitizedDevnet}.yaml`, + ); - rmSync(devnetFilePath); + removeFile(devnetFilePath); } export function defaultDevnetIsConfigured(): boolean { return PathExists(path.join(defaultDevnetsPath, `${defaultDevnet}.yaml`)); } -export function getDevnetConfiguration(name: string): TDevnetsCreateOptions | null { +export function getDevnetConfiguration(name: string): IDevnetsCreateOptions | null { const devnetFilePath = path.join(defaultDevnetsPath, `${name}.yaml`); if (! PathExists(devnetFilePath)) { return null; } - return yaml.load(readFileSync(devnetFilePath, 'utf8')) as TDevnetsCreateOptions; + return yaml.load(readFileSync(devnetFilePath, 'utf8')) as IDevnetsCreateOptions; } /** * Displays the devnet configuration in a formatted manner. * - * @param {TDevnetsCreateOptions} devnetConfig - The devnet configuration to display. + * @param {IDevnetsCreateOptions} devnetConfig - The devnet configuration to display. */ -export function displayDevnetConfig(devnetConfig: TDevnetsCreateOptions): void { +export function displayDevnetConfig( + devnetConfig: IDevnetsCreateOptions, +): void { const log = console.log; const formatLength = 80; // Maximum width for the display @@ -138,7 +140,19 @@ export function displayDevnetConfig(devnetConfig: TDevnetsCreateOptions): void { displaySeparator(); } -export function displayDevnetsConfig(): void { +export function loadDevnetConfig(devnet: string): IDevnetsCreateOptions | never { + const devnetFilePath = path.join(defaultDevnetsPath, `${devnet}.yaml`); + + if (! existsSync(devnetFilePath)) { + throw new Error('Devnet configuration file not found.') + } + + return (yaml.load( + readFileSync(devnetFilePath, 'utf8'), + ) as IDevnetsCreateOptions); +} + +export async function displayDevnetsConfig(): Promise { const log = console.log; const formatLength = 80; // Maximum width for the display @@ -162,58 +176,29 @@ export function displayDevnetsConfig(): void { return ` ${keyValue}${' '.repeat(remainingWidth)} `; }; - const existingDevnets: ICustomDevnetsChoice[] = getExistingDevnets(); - const standardDevnets: string[] = ['devnet']; + + const existingDevnets: ICustomDevnetsChoice[] = await getExistingDevnets(); existingDevnets.forEach(({ value }) => { - const storedConfig = loadDevnet(value); - const fileExists = storedConfig !== null; - const devnetConfig = fileExists ? storedConfig : devnetDefaults[value]; + const devnetFilePath = path.join(defaultDevnetsPath, `${value}.yaml`); + const fileExists = existsSync(devnetFilePath); + const devnetConfig = fileExists + ? (yaml.load( + readFileSync(devnetFilePath, 'utf8'), + ) as IDevnetsCreateOptions) + : devnetDefaults[value]; displaySeparator(); - log(formatConfig('Name', value, !fileExists)); - log(formatConfig('Port', devnetConfig.port?.toString(), !fileExists)); + log(formatConfig('Name', devnetConfig.name)); + log(formatConfig('Port', devnetConfig.port?.toString())); log( formatConfig( 'Volume', devnetConfig.useVolume ? `kadena_${devnetConfig.name}` : 'N/A', - !fileExists, - ), - ); - log( - formatConfig( - 'Pact folder mount', - devnetConfig.mountPactFolder || 'N/A', - !fileExists, ), ); - log( - formatConfig('kadena/devnet version', devnetConfig.version, !fileExists), - ); - }); - - standardDevnets.forEach((devnet) => { - if (!existingDevnets.some(({ value }) => value === devnet)) { - const devnetConfig = devnetDefaults[devnet]; - displaySeparator(); - log(formatConfig('Name', devnet, true)); - log(formatConfig('Port', devnetConfig.port?.toString(), true)); - log( - formatConfig( - 'Volume', - devnetConfig.useVolume ? `kadena_${devnetConfig.name}` : 'N/A', - true, - ), - ); - log( - formatConfig( - 'Pact folder mount', - devnetConfig.mountPactFolder || 'N/A', - true, - ), - ); - log(formatConfig('kadena/devnet version', devnetConfig.version, true)); - } + log(formatConfig('Pact folder mount', devnetConfig.mountPactFolder || 'N/A')); + log(formatConfig('kadena/devnet version', devnetConfig.version)); }); displaySeparator(); diff --git a/packages/tools/kadena-cli/src/devnet/devnetsCreateQuestions.ts b/packages/tools/kadena-cli/src/devnet/devnetsCreateQuestions.ts deleted file mode 100644 index 79d1fcac5a..0000000000 --- a/packages/tools/kadena-cli/src/devnet/devnetsCreateQuestions.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { IQuestion } from '../utils/helpers.js'; -import { - capitalizeFirstLetter, - getExistingDevnets, - isAlphabetic, -} from '../utils/helpers.js'; - -import { input, select } from '@inquirer/prompts'; -import { z } from 'zod'; - -// eslint-disable-next-line @rushstack/typedef-var -export const DevnetsCreateOptions = z.object({ - name: z.string(), - port: z.number().optional(), - useVolume: z.boolean().optional(), - mountPactFolder: z.string().optional(), - version: z.string().optional(), -}); - -export type TDevnetsCreateOptions = z.infer; - -interface IDevnetManageQuestionsQuestions - extends Pick, 'key' | 'prompt'> {} - -interface ICustomChoice { - value: string; - name?: string; - description?: string; - disabled?: boolean | string; -} - -export async function askForDevnet(): Promise { - const existingDevnets: ICustomChoice[] = getExistingDevnets(); - - const allDevnetChoices: ICustomChoice[] = [...existingDevnets] - .filter((v, i, a) => a.findIndex((v2) => v2.name === v.name) === i) - .map((devnet) => { - return { - value: devnet.value, - name: capitalizeFirstLetter(devnet.value), - }; - }); - - const devnetChoice = await select({ - message: - 'Select an (default) existing devnet configuration or create a new one:', - choices: [ - ...allDevnetChoices, - { value: 'CREATE_NEW', name: 'Create a New Devnet' } as ICustomChoice, - ], - }); - - if (devnetChoice === 'CREATE_NEW') { - const newDevnetName = await input({ - validate: function (input) { - if (input === '') { - return 'Devnet name cannot be empty! Please enter something.'; - } - if (!isAlphabetic(input)) { - return 'Devnet name must be alphabetic! Please enter a valid name.'; - } - return true; - }, - message: 'Enter the name for your new devnet container:', - }); - return newDevnetName.toLowerCase(); - } - - return devnetChoice.toLowerCase(); -} - -export const devnetsCreateQuestions: IQuestion[] = [ - { - key: 'name', - prompt: async () => await askForDevnet(), - }, - { - key: 'port', - prompt: async () => { - const port = await input({ - default: '8080', - message: 'Enter a port number to forward to the Chainweb node API', - validate: function (input) { - const port = parseInt(input); - if (isNaN(port)) { - return 'Port must be a number! Please enter a valid port number.'; - } - return true; - }, - }); - return parseInt(port); - }, - }, - { - key: 'useVolume', - prompt: async () => - await select({ - message: 'Would you like to create a persistent volume?', - choices: [ - { value: false, name: 'No' }, - { value: true, name: 'Yes' }, - ], - }), - }, - { - key: 'mountPactFolder', - prompt: async () => - await input({ - default: '', - message: - 'Enter the relative path to a folder containing your Pact files to mount (e.g. ./pact) or leave empty to skip.', - }), - }, - { - key: 'version', - prompt: async () => - await input({ - default: 'latest', - message: - 'Enter the version of the kadena/devnet image you would like to use.', - }), - }, -]; - -export const devnetManageQuestions: IDevnetManageQuestionsQuestions[] = [ - ...devnetsCreateQuestions.filter((question) => question.key !== 'name'), -]; diff --git a/packages/tools/kadena-cli/src/devnet/docker.ts b/packages/tools/kadena-cli/src/devnet/docker.ts index cf543bbf3d..4ac9578dbc 100644 --- a/packages/tools/kadena-cli/src/devnet/docker.ts +++ b/packages/tools/kadena-cli/src/devnet/docker.ts @@ -1,6 +1,6 @@ import chalk from 'chalk'; import { execSync } from 'child_process'; -import { TDevnetsCreateOptions } from './devnetsCreateQuestions.js'; +import { IDevnetsCreateOptions } from './devnetHelpers.js'; const volumePrefix = 'kadena_'; const containerDataFolder = '/data'; @@ -57,7 +57,7 @@ const maybeCreateVolume = (useVolume: boolean, containerName: string): void => { }; const formatDockerRunOptions = ( - configuration: TDevnetsCreateOptions, + configuration: IDevnetsCreateOptions, ): string => { const options = ['-d']; @@ -107,7 +107,7 @@ const containerExists = (name: string): boolean => { } }; -export function runDevnet(configuration: TDevnetsCreateOptions): void { +export function runDevnet(configuration: IDevnetsCreateOptions): void { maybeCreateVolume(!!configuration.useVolume, configuration.name); const dockerRunOptions = formatDockerRunOptions(configuration); diff --git a/packages/tools/kadena-cli/src/devnet/index.ts b/packages/tools/kadena-cli/src/devnet/index.ts index 5023054f13..5e342f75eb 100644 --- a/packages/tools/kadena-cli/src/devnet/index.ts +++ b/packages/tools/kadena-cli/src/devnet/index.ts @@ -1,36 +1,28 @@ -import { createSimpleSubCommand } from '../utils/helpers.js'; - -import { createDevnetsCommand } from './createDevnetsCommand.js'; -import type { IListDevnetsArgs } from './listDevnetsCommand.js'; -import { listDevnetsAction } from './listDevnetsCommand.js'; -import { manageDevnets } from './manageDevnetsCommand.js'; +import { createDevnetCommand } from './createDevnetCommand.js'; +import { deleteDevnetCommand } from './deleteDevnetCommand.js'; +import { listDevnetsCommand } from './listDevnetsCommand.js'; +import { manageDevnetsCommand } from './manageDevnetsCommand.js'; import { runDevnetCommand } from './runDevnetCommand.js'; - -import type { Command } from 'commander'; -import { removeDevnetCommand } from './removeDevnetCommand.js'; import { stopDevnetCommand } from './stopDevnetCommand.js'; import { updateDevnetCommand } from './updateDevnetCommand.js'; +import type { Command } from 'commander'; + const SUBCOMMAND_ROOT: 'devnet' = 'devnet'; -export function devnetCommandFactory(program: Command, version: string): void { +export function devnetsCommandFactory( + program: Command, + version: string, +): void { const devnetsProgram = program .command(SUBCOMMAND_ROOT) - .description( - `Tool for configuring, starting, stopping and managing local devnet containers`, - ); - - // Attach list subcommands to the devnetsProgram - createSimpleSubCommand( - 'list', - 'List all available devnet container configurations', - listDevnetsAction, - )(devnetsProgram); + .description(`Tool to create and manage devnets`); - manageDevnets(devnetsProgram, version); - createDevnetsCommand(devnetsProgram, version); + listDevnetsCommand(devnetsProgram, version); + manageDevnetsCommand(devnetsProgram, version); + createDevnetCommand(devnetsProgram, version); + deleteDevnetCommand(devnetsProgram, version); runDevnetCommand(devnetsProgram, version); stopDevnetCommand(devnetsProgram, version); - removeDevnetCommand(devnetsProgram, version); updateDevnetCommand(devnetsProgram, version); } diff --git a/packages/tools/kadena-cli/src/devnet/init.ts b/packages/tools/kadena-cli/src/devnet/init.ts new file mode 100644 index 0000000000..833f4d26c2 --- /dev/null +++ b/packages/tools/kadena-cli/src/devnet/init.ts @@ -0,0 +1,4 @@ +import { devnetDefaults } from '../constants/devnets.js'; +import { writeDevnet } from './devnetHelpers.js'; + +writeDevnet(devnetDefaults.devnet); diff --git a/packages/tools/kadena-cli/src/devnet/listDevnetsCommand.ts b/packages/tools/kadena-cli/src/devnet/listDevnetsCommand.ts index a1336a3ac2..f9cbd2d89b 100644 --- a/packages/tools/kadena-cli/src/devnet/listDevnetsCommand.ts +++ b/packages/tools/kadena-cli/src/devnet/listDevnetsCommand.ts @@ -1,7 +1,15 @@ -import { displayDevnetsConfig } from './devnetsHelpers.js'; +import { displayDevnetsConfig } from './devnetHelpers.js'; -export interface IListDevnetsArgs {} +import debug from 'debug'; +import { createCommand } from '../utils/createCommand.js'; -export const listDevnetsAction = (args: IListDevnetsArgs): void => { - displayDevnetsConfig(); -}; +export const listDevnetsCommand = createCommand( + 'list', + 'List all available devnets', + [], + async (config) => { + debug('devnet-list:action')({config}); + + displayDevnetsConfig(); + }, +); diff --git a/packages/tools/kadena-cli/src/devnet/manageDevnetsCommand.ts b/packages/tools/kadena-cli/src/devnet/manageDevnetsCommand.ts index 99df9729e6..85872cf112 100644 --- a/packages/tools/kadena-cli/src/devnet/manageDevnetsCommand.ts +++ b/packages/tools/kadena-cli/src/devnet/manageDevnetsCommand.ts @@ -1,66 +1,32 @@ -import { defaultDevnetsPath } from '../constants/devnets.js'; -import { - clearCLI, - collectResponses, - getExistingDevnets, -} from '../utils/helpers.js'; -import { processZodErrors } from '../utils/processZodErrors.js'; +import debug from 'debug'; +import { devnetOverwritePrompt } from '../constants/prompts.js'; +import { globalOptions } from '../utils/globalOptions.js'; +import { writeDevnet } from './devnetHelpers.js'; -import type { TDevnetsCreateOptions } from './devnetsCreateQuestions.js'; -import { - devnetManageQuestions, - DevnetsCreateOptions, -} from './devnetsCreateQuestions.js'; -import type { ICustomDevnetsChoice } from './devnetsHelpers.js'; -import { writeDevnets } from './devnetsHelpers.js'; - -import { select } from '@inquirer/prompts'; import chalk from 'chalk'; -import type { Command } from 'commander'; -import { readFileSync } from 'fs'; -import yaml from 'js-yaml'; -import path from 'path'; - -export interface IManageDevnetsOptions {} - -export function manageDevnets(program: Command, version: string): void { - program - .command('manage') - .description('Manage devnet(s)') - .action(async (args: IManageDevnetsOptions) => { - try { - const existingDevnets: ICustomDevnetsChoice[] = getExistingDevnets(); - - if (existingDevnets.length === 0) { - console.log(chalk.red('No existing devnets found.')); - return; - } - - const selectedDevnet = await select({ - message: 'Select the devnet you want to manage:', - choices: existingDevnets, - }); - const devnetFilePath = path.join( - defaultDevnetsPath, - `${selectedDevnet}.yaml`, - ); - const existingConfig: TDevnetsCreateOptions = yaml.load( - readFileSync(devnetFilePath, 'utf8'), - ) as TDevnetsCreateOptions; - - const responses = await collectResponses( - { name: selectedDevnet }, - devnetManageQuestions, - ); - const devnetConfig = { ...existingConfig, ...responses }; - - DevnetsCreateOptions.parse(devnetConfig); - - writeDevnets(devnetConfig); - clearCLI(); - console.log(chalk.green('Devnet configurations updated.')); - } catch (e) { - processZodErrors(program, e, args); - } - }); -} +import { createCommand } from '../utils/createCommand.js'; + +export const manageDevnetsCommand = createCommand( + 'manage', + 'Manage devnets', + [ + globalOptions.devnetSelect(), + globalOptions.devnetPort(), + globalOptions.devnetUseVolume(), + globalOptions.devnetMountPactFolder(), + globalOptions.devnetVersion(), + ], + async (config) => { + debug('devnet-manage:action')({config}); + + const overwrite = await devnetOverwritePrompt(config.name); + if (overwrite === 'no') { + console.log(chalk.yellow(`\nThe devnet configuration "${config.name}" will not be updated.\n`)); + return; + } + + writeDevnet(config); + + console.log(chalk.green(`\nThe devnet configuration "${config.name}" has been updated.\n`)); + }, +); diff --git a/packages/tools/kadena-cli/src/devnet/removeDevnetCommand.ts b/packages/tools/kadena-cli/src/devnet/removeDevnetCommand.ts deleted file mode 100644 index a694e2fcbf..0000000000 --- a/packages/tools/kadena-cli/src/devnet/removeDevnetCommand.ts +++ /dev/null @@ -1,49 +0,0 @@ -import chalk from 'chalk'; -import { type Command } from 'commander'; -import debug from 'debug'; -import { defaultDevnet } from '../constants/devnets.js'; -import { processZodErrors } from '../utils/processZodErrors.js'; -import { getDevnetConfiguration, removeDevnet as removeDevnetConfiguration } from './devnetsHelpers.js'; -import { dockerVolumeName, isDockerInstalled, removeDevnet, removeVolume } from './docker.js'; - -export function removeDevnetCommand(program: Command, version: string): void { - program - .command('remove') - .description('Remove devnet container and configuration') - .option('-n, --name ', 'Container name (e.g. "devnet")') - .action(async (args: { name?: string }) => { - debug('devnet-remove:action')({ args }); - - try { - // Abort if Docker is not installed - if (!isDockerInstalled()) { - console.log( - chalk.red( - 'Stopping devnet requires Docker. Please install Docker and try again.', - ), - ); - return; - } - - const name = args.name || defaultDevnet; - removeDevnet(name); - console.log(chalk.green(`Removed devnet container: ${name}`)); - - const configuration = getDevnetConfiguration(name); - - if (configuration?.useVolume) { - removeVolume(name); - console.log( - chalk.green( - `Removed volume: ${dockerVolumeName(name)}`, - ), - ); - } - - removeDevnetConfiguration(name); - console.log(chalk.green(`Successfully removed devnet configuration: ${name}`)); - } catch (e) { - console.log(chalk.yellow(`Removing devnet did not go as planned: ${e.message}`)); - } - }); -} diff --git a/packages/tools/kadena-cli/src/devnet/runDevnetCommand.ts b/packages/tools/kadena-cli/src/devnet/runDevnetCommand.ts index 273a5b753c..7a8e92b4c0 100644 --- a/packages/tools/kadena-cli/src/devnet/runDevnetCommand.ts +++ b/packages/tools/kadena-cli/src/devnet/runDevnetCommand.ts @@ -1,83 +1,28 @@ -import { defaultDevnet, devnetDefaults } from '../constants/devnets.js'; -import { getExistingDevnets } from '../utils/helpers.js'; - -import type { TDevnetsCreateOptions } from './devnetsCreateQuestions.js'; -import { defaultDevnetIsConfigured, loadDevnet, writeDevnets } from './devnetsHelpers.js'; - -import chalk from 'chalk'; -import { Option, type Command } from 'commander'; import debug from 'debug'; -import { processZodErrors } from '../utils/processZodErrors.js'; -import { runDevnetsCreate } from './createDevnetsCommand.js'; -import { isDockerInstalled, runDevnet } from './docker.js'; - -export function runDevnetCommand(program: Command, version: string): void { - program - .command('run') - .description('Run devnet') - .option('-n, --name ', 'Container name (e.g. "devnet")') - .addOption( - new Option( - '-p, --port ', - 'Port to forward to the Chainweb node API (e.g. 8080)', - ).argParser((value) => parseInt(value, 10)), - ) - .option( - '-u, --useVolume', - 'Create a persistent volume to mount to the container', - ) - .option( - '-m, --mountPactFolder ', - 'Mount a folder containing Pact files to the container (e.g. "./pact")', - ) - .option( - '-v, --version ', - 'Version of the kadena/devnet Docker image to use (e.g. "latest")', - ) - .action(async (args: TDevnetsCreateOptions) => { - debug('devnet-run:action')({ args }); - - try { - // Abort if Docker is not installed - if (!isDockerInstalled()) { - console.log( - chalk.red( - 'Running devnet requires Docker. Please install Docker and try again.', - ), - ); - return; - } +import { globalOptions } from '../utils/globalOptions.js'; - /** - * `kadena devnet run` # run default devnet configuration - * `kadena devnet run --name exists` # run custom devnet configuration - * `kadena devnet run --name does-not-exist` # create new devnet configuration to run - */ - const name = args.name || defaultDevnet; - - if (name == defaultDevnet && !defaultDevnetIsConfigured()) { - writeDevnets(devnetDefaults[defaultDevnet]); - } - - const existingDevnets = getExistingDevnets(); - if (!existingDevnets.map((d) => d.name).find((n) => n === name)) { - await runDevnetsCreate(program, version, args); - } - const devnetConfig = loadDevnet(name); - - // This should never be true, but just in case. - if (devnetConfig === null) { - console.log( - chalk.red( - `Missing devnet configuration: ${name}. Cannot run devnet.`, - ), - ); - return; - } - - runDevnet(devnetConfig); - } catch (e) { - processZodErrors(program, e, args); - } - }); -} +import chalk from 'chalk'; +import { createCommand } from '../utils/createCommand.js'; +import { isDockerInstalled, runDevnet, stopDevnet } from './docker.js'; + +export const runDevnetCommand = createCommand( + 'run', + 'Run devnet', + [globalOptions.devnet()], + async (config) => { + debug('devnet-run:action')({config}); + + if (!isDockerInstalled()) { + console.log( + chalk.red( + 'Running devnet requires Docker. Please install Docker and try again.', + ), + ); + return; + } + + runDevnet(config.devnetConfig); + + console.log(chalk.green(`\nThe devnet configuration "${config.devnet}" is running.\n`)); + }, +); diff --git a/packages/tools/kadena-cli/src/devnet/stopDevnetCommand.ts b/packages/tools/kadena-cli/src/devnet/stopDevnetCommand.ts index 126909b39a..693c772b61 100644 --- a/packages/tools/kadena-cli/src/devnet/stopDevnetCommand.ts +++ b/packages/tools/kadena-cli/src/devnet/stopDevnetCommand.ts @@ -1,32 +1,28 @@ -import chalk from 'chalk'; -import { type Command } from 'commander'; import debug from 'debug'; -import { defaultDevnet } from '../constants/devnets.js'; -import { processZodErrors } from '../utils/processZodErrors.js'; +import { globalOptions } from '../utils/globalOptions.js'; + +import chalk from 'chalk'; +import { createCommand } from '../utils/createCommand.js'; import { isDockerInstalled, stopDevnet } from './docker.js'; -export function stopDevnetCommand(program: Command, version: string): void { - program - .command('stop') - .description('Stop devnet') - .option('-n, --name ', 'Container name (e.g. "devnet")') - .action(async (args: { name?: string }) => { - debug('devnet-stop:action')({ args }); +export const stopDevnetCommand = createCommand( + 'stop', + 'Stop devnet', + [globalOptions.devnetSelect()], + async (config) => { + debug('devnet-stop:action')({config}); + + if (!isDockerInstalled()) { + console.log( + chalk.red( + 'Stopping devnet requires Docker. Please install Docker and try again.', + ), + ); + return; + } - try { - // Abort if Docker is not installed - if (!isDockerInstalled()) { - console.log( - chalk.red( - 'Stopping devnet requires Docker. Please install Docker and try again.', - ), - ); - return; - } + stopDevnet(config.name); - stopDevnet(args.name || defaultDevnet); - } catch (e) { - processZodErrors(program, e, args); - } - }); -} + console.log(chalk.green(`\nThe devnet configuration "${config.name}" has been stopped.\n`)); + }, +); diff --git a/packages/tools/kadena-cli/src/devnet/updateDevnetCommand.ts b/packages/tools/kadena-cli/src/devnet/updateDevnetCommand.ts index 8434bb4b0e..e5732c73ee 100644 --- a/packages/tools/kadena-cli/src/devnet/updateDevnetCommand.ts +++ b/packages/tools/kadena-cli/src/devnet/updateDevnetCommand.ts @@ -1,34 +1,26 @@ import chalk from 'chalk'; -import { type Command } from 'commander'; import debug from 'debug'; -import { processZodErrors } from '../utils/processZodErrors.js'; +import { createCommand } from '../utils/createCommand.js'; +import { globalOptions } from '../utils/globalOptions.js'; import { isDockerInstalled, updateDevnet } from './docker.js'; -export function updateDevnetCommand(program: Command, version: string): void { - program - .command('update') - .description('Update devnet container image') - .option( - '-v, --version ', - 'The version of kadena/devnet to update (e.g. "latest")', - ) - .action(async (args: { version?: string }) => { - debug('devnet-update:action')({ args }); +export const updateDevnetCommand = createCommand( + 'update', + 'Update the Docker image of a given devnet container image', + [globalOptions.devnetVersion()], + async (config) => { + debug('devnet-update:action')({config}); - try { - // Abort if Docker is not installed - if (!isDockerInstalled()) { - console.log( - chalk.red( - 'Stopping devnet requires Docker. Please install Docker and try again.', - ), - ); - return; - } + // Abort if Docker is not installed + if (!isDockerInstalled()) { + console.log( + chalk.red( + 'Updating devnet requires Docker. Please install Docker and try again.', + ), + ); + return; + } - updateDevnet(args.version || 'latest'); - } catch (e) { - processZodErrors(program, e, args); - } - }); -} + updateDevnet(config.version || 'latest'); + }, +); diff --git a/packages/tools/kadena-cli/src/index.js b/packages/tools/kadena-cli/src/index.js deleted file mode 100644 index 1f81ede582..0000000000 --- a/packages/tools/kadena-cli/src/index.js +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env node -import { accountCommandFactory } from './account/index.js'; -import { configCommandFactory } from './config/index.js'; -import { contractCommandFactory } from './contract/index.js'; -// import { dappCommandFactory } from './dapp/index.js'; -import { program } from 'commander'; -import { readFileSync } from 'node:fs'; -import { devnetCommandFactory } from './devnet/index.js'; -import { keysCommandFactory } from './keys/index.js'; -import { marmaladeCommandFactory } from './marmalade/index.js'; -import { networksCommandFactory } from './networks/index.js'; -import { txCommandFactory } from './tx/index.js'; -import { typescriptCommandFactory } from './typescript/index.js'; -import { clearCLI } from './utils/helpers.js'; -const packageJson = JSON.parse( - readFileSync(new URL('../package.json', import.meta.url), 'utf8'), -); -// TODO: introduce root flag --no-interactive -// TODO: introduce root flag --ci -[ - configCommandFactory, - networksCommandFactory, - devnetCommandFactory, - keysCommandFactory, - accountCommandFactory, - txCommandFactory, - contractCommandFactory, - marmaladeCommandFactory, - typescriptCommandFactory, - // dappCommandFactory, -] - .flat() - .forEach(async (fn) => { - fn(program, packageJson.version); - }); -clearCLI(); -program - .description('CLI to interact with Kadena and its ecosystem') - .version(packageJson.version) - .parse(); diff --git a/packages/tools/kadena-cli/src/index.ts b/packages/tools/kadena-cli/src/index.ts index a6c4f0ec14..c04105dfce 100644 --- a/packages/tools/kadena-cli/src/index.ts +++ b/packages/tools/kadena-cli/src/index.ts @@ -3,7 +3,7 @@ import { accountCommandFactory } from './account/index.js'; import { configCommandFactory } from './config/index.js'; import { contractCommandFactory } from './contract/index.js'; // import { dappCommandFactory } from './dapp/index.js'; -import { devnetCommandFactory } from './devnet/index.js'; +import { devnetsCommandFactory } from './devnet/index.js'; import { keypairCommandFactory } from './keypair/index.js'; import { keysCommandFactory } from './keys/index.js'; import { keysetCommandFactory } from './keyset/index.js'; @@ -26,7 +26,7 @@ const packageJson: { version: string } = JSON.parse( [ configCommandFactory, networksCommandFactory, - devnetCommandFactory, + devnetsCommandFactory, keypairCommandFactory, keysCommandFactory, keysetCommandFactory, diff --git a/packages/tools/kadena-cli/src/utils/globalOptions.ts b/packages/tools/kadena-cli/src/utils/globalOptions.ts index 1ea8215704..3c67026099 100644 --- a/packages/tools/kadena-cli/src/utils/globalOptions.ts +++ b/packages/tools/kadena-cli/src/utils/globalOptions.ts @@ -4,6 +4,13 @@ import { accountNamePrompt, accountPrompt, chainIdPrompt, + devnetPrompt, + devnetMountPactFolderPrompt, + devnetNamePrompt, + devnetPortPrompt, + devnetSelectPrompt, + devnetUseVolumePrompt, + devnetVersionPrompt, gasPayerPrompt, keypairNamePrompt, keypairPrompt, @@ -22,12 +29,13 @@ import { selectKeypairsPrompt, } from '../constants/prompts.js'; import { loadNetworkConfig } from '../networks/networksHelpers.js'; -import { ensureAccountsConfiguration, ensureKeypairsConfiguration, ensureKeysetsConfiguration, ensureNetworksConfiguration } from './helpers.js'; +import { ensureAccountsConfiguration, ensureDevnetsConfiguration, ensureKeypairsConfiguration, ensureKeysetsConfiguration, ensureNetworksConfiguration } from './helpers.js'; import { program } from 'commander'; import chalk from 'chalk'; import { loadKeypairConfig } from '../keypair/keypairHelpers.js'; import { loadKeysetConfig } from '../keyset/keysetHelpers.js'; import { loadAccountConfig } from '../account/accountHelpers.js'; +import { loadDevnetConfig } from '../devnet/devnetHelpers.js'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export const createOption = < @@ -88,6 +96,74 @@ export const globalOptions = { .max(19), option: new Option('-c, --chain-id '), }), + devnet: createOption({ + key: 'devnet' as const, + prompt: devnetPrompt, + validation: z.string(), + option: new Option( + '-d, --devnet ', + 'Devnet name', + ), + expand: async (devnet: string) => { + await ensureDevnetsConfiguration(); + try { + return loadDevnetConfig(devnet); + } catch (e) { + console.log(chalk.yellow(`\nNo devnet "${devnet}" found. Please create the devnet.\n`)); + await program.parseAsync(['', '', 'devnet', 'create']); + const devnetName = await devnetPrompt(); + return loadDevnetConfig(devnetName); + } + }, + }), + devnetName: createOption({ + key: 'name' as const, + prompt: devnetNamePrompt, + validation: z.string(), + option: new Option('-n, --name ', 'Devnet name (e.g. "devnet")'), + }), + devnetPort: createOption({ + key: 'port' as const, + prompt: devnetPortPrompt, + validation: z.number(), + option: new Option( + '-p, --port ', + 'Port to forward to the Chainweb node API (e.g. 8080)', + ).argParser((value) => parseInt(value, 10)), + }), + devnetUseVolume: createOption({ + key: 'useVolume' as const, + prompt: devnetUseVolumePrompt, + validation: z.boolean(), + option: new Option( + '-u, --useVolume', + 'Create a persistent volume to mount to the container' + ), + }), + devnetMountPactFolder: createOption({ + key: 'mountPactFolder' as const, + prompt: devnetMountPactFolderPrompt, + validation: z.string(), + option: new Option( + '-m, --mountPactFolder ', + 'Mount a folder containing Pact files to the container (e.g. "./pact")', + ), + }), + devnetSelect: createOption({ + key: 'name' as const, + prompt: devnetSelectPrompt, + validation: z.string(), + option: new Option('-n, --name ', 'Devnet name'), + }), + devnetVersion: createOption({ + key: 'version' as const, + prompt: devnetVersionPrompt, + validation: z.string(), + option: new Option( + '-v, --version ', + 'Version of the kadena/devnet Docker image to use (e.g. "latest")', + ), + }), keypair: createOption({ key: 'keypair' as const, prompt: keypairPrompt, diff --git a/packages/tools/kadena-cli/src/utils/helpers.ts b/packages/tools/kadena-cli/src/utils/helpers.ts index 6467bcb285..e82286d863 100644 --- a/packages/tools/kadena-cli/src/utils/helpers.ts +++ b/packages/tools/kadena-cli/src/utils/helpers.ts @@ -315,9 +315,7 @@ export async function ensureNetworksConfiguration(): Promise { await import('./../networks/init.js'); } -export async function getExistingNetworks(): Promise { - await ensureNetworksConfiguration(); - +export async function getConfiguration(configurationPath: string): Promise { try { return readdirSync(configurationPath).map((filename) => ({ value: path.basename(filename.toLowerCase(), '.yaml'), @@ -329,11 +327,24 @@ export async function getExistingNetworks(): Promise { } } -export function getExistingNetworks(): ICustomChoice[] { +export async function getExistingNetworks(): Promise { + await ensureNetworksConfiguration(); + return getConfiguration(defaultNetworksPath); } -export function getExistingDevnets(): ICustomChoice[] { +export async function ensureDevnetsConfiguration(): Promise { + if (existsSync(defaultDevnetsPath)) { + return; + } + + mkdirSync(defaultDevnetsPath, { recursive: true }); + await import('./../devnet/init.js'); +} + +export async function getExistingDevnets(): Promise { + await ensureDevnetsConfiguration(); + return getConfiguration(defaultDevnetsPath); }