Skip to content

Commit

Permalink
feat(kadena-cli): add kadena-cli devnet commands
Browse files Browse the repository at this point in the history
  • Loading branch information
jessevanmuijden committed Nov 16, 2023
1 parent 8e95215 commit f4282bb
Show file tree
Hide file tree
Showing 16 changed files with 994 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/large-snails-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/kadena-cli': patch
---

add devnet commands to kadena cli
30 changes: 30 additions & 0 deletions packages/tools/kadena-cli/src/constants/devnets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { TDevnetsCreateOptions } from '../devnet/devnetsCreateQuestions.js';

export interface IDefaultDevnetOptions {
[key: string]: TDevnetsCreateOptions;
}

/**
* @const devnetDefaults
* Provides the default devnet configurations.
*/
export const devnetDefaults: IDefaultDevnetOptions = {
devnet: {
name: 'devnet',
port: 8080,
useVolume: false,
mountPactFolder: '',
version: 'latest',
},
other: {
name: '',
port: 8080,
useVolume: false,
mountPactFolder: '',
version: '',
},
};

export const defaultDevnetsPath: string = `${process.cwd()}/.kadena/devnets`;
export const standardDevnets: string[] = ['devnet'];
export const defaultDevnet: string = 'devnet';
107 changes: 107 additions & 0 deletions packages/tools/kadena-cli/src/devnet/createDevnetsCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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<boolean> {
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<void> {
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 <name>', 'Container name (e.g. "devnet")')
.addOption(
new Option(
'-p, --port <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 <mountPactFolder>',
'Mount a folder containing Pact files to the container (e.g. "./pact")',
)
.option(
'-v, --version <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);
});
}
127 changes: 127 additions & 0 deletions packages/tools/kadena-cli/src/devnet/devnetsCreateQuestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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<typeof DevnetsCreateOptions>;

interface IDevnetManageQuestionsQuestions
extends Pick<IQuestion<TDevnetsCreateOptions>, 'key' | 'prompt'> {}

interface ICustomChoice {
value: string;
name?: string;
description?: string;
disabled?: boolean | string;
}

export async function askForDevnet(): Promise<string> {
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<TDevnetsCreateOptions>[] = [
{
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'),
];
Loading

0 comments on commit f4282bb

Please sign in to comment.