diff --git a/yarn-project/aztec-cli/package.json b/yarn-project/aztec-cli/package.json index f929d946e3d..845b9296eed 100644 --- a/yarn-project/aztec-cli/package.json +++ b/yarn-project/aztec-cli/package.json @@ -56,7 +56,6 @@ "@types/node": "^18.7.23", "jest": "^29.5.0", "jest-mock-extended": "^3.0.5", - "string-argv": "^0.3.2", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typescript": "^5.0.4" diff --git a/yarn-project/aztec-cli/src/index.ts b/yarn-project/aztec-cli/src/index.ts index 34aa0c15950..4e42858bf9d 100644 --- a/yarn-project/aztec-cli/src/index.ts +++ b/yarn-project/aztec-cli/src/index.ts @@ -34,8 +34,6 @@ import { prepTx, } from './utils.js'; -export { cliTestSuite } from './test/cli_test_suite.js'; - const accountCreationSalt = Fr.ZERO; const stripLeadingHex = (hex: string) => { diff --git a/yarn-project/aztec-cli/src/test/cli_test_suite.ts b/yarn-project/aztec-cli/src/test/cli_test_suite.ts deleted file mode 100644 index 0dcb4998a3c..00000000000 --- a/yarn-project/aztec-cli/src/test/cli_test_suite.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { AztecAddress } from '@aztec/aztec.js'; -import { DebugLogger } from '@aztec/foundation/log'; -import { AztecRPC, CompleteAddress } from '@aztec/types'; - -import stringArgv from 'string-argv'; -import { format } from 'util'; - -import { getProgram } from '../index.js'; - -const INITIAL_BALANCE = 33000; -const TRANSFER_BALANCE = 3000; - -export const cliTestSuite = ( - testName: string, - aztecRpcSetup: () => Promise, - cleanup: () => Promise, - rpcUrl: string, - debug: DebugLogger, -) => { - return describe(testName, () => { - let cli: ReturnType; - let aztecRpcClient: AztecRPC; - let existingAccounts: CompleteAddress[]; - let contractAddress: AztecAddress; - let log: (...args: any[]) => void; - - // All logs emitted by the cli will be collected here, and reset between tests - const logs: string[] = []; - - beforeAll(async () => { - aztecRpcClient = await aztecRpcSetup(); - log = (...args: any[]) => { - logs.push(format(...args)); - debug(...args); - }; - }); - - afterAll(async () => { - await cleanup(); - }); - - // in order to run the same command twice, we need to create a new CLI instance - const resetCli = () => { - cli = getProgram(log, debug); - }; - - beforeEach(() => { - logs.splice(0); - resetCli(); - }); - - // Run a command on the CLI - const run = (cmd: string, addRpcUrl = true) => { - const args = stringArgv(cmd, 'node', 'dest/bin/index.js'); - if (addRpcUrl) { - args.push('--rpc-url', rpcUrl); - } - return cli.parseAsync(args); - }; - - // Returns first match across all logs collected so far - const findInLogs = (regex: RegExp) => { - for (const log of logs) { - const match = regex.exec(log); - if (match) return match; - } - }; - - const findMultipleInLogs = (regex: RegExp) => { - const matches = []; - for (const log of logs) { - const match = regex.exec(log); - if (match) matches.push(match); - } - return matches; - }; - - const clearLogs = () => { - logs.splice(0); - }; - - it('creates & retrieves an account', async () => { - existingAccounts = await aztecRpcClient.getAccounts(); - debug('Create an account'); - await run(`create-account`); - const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(foundAddress).toBeDefined(); - const newAddress = AztecAddress.fromString(foundAddress!); - - const accountsAfter = await aztecRpcClient.getAccounts(); - const expectedAccounts = [...existingAccounts.map(a => a.address), newAddress]; - expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts); - const newCompleteAddress = accountsAfter[accountsAfter.length - 1]; - - // Test get-accounts - debug('Check that account was added to the list of accs in RPC'); - await run('get-accounts'); - const fetchedAddresses = findMultipleInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/); - const foundFetchedAddress = fetchedAddresses.find(match => match.groups?.address === newAddress.toString()); - expect(foundFetchedAddress).toBeDefined(); - - // Test get-account - debug('Check we can retrieve the specific account'); - clearLogs(); - await run(`get-account ${newAddress.toString()}`); - const fetchedAddress = findInLogs(/Public Key:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(fetchedAddress).toEqual(newCompleteAddress.publicKey.toString()); - }); - - it('deploys a contract & sends transactions', async () => { - // generate a private key - debug('Create an account using a private key'); - await run('generate-private-key', false); - const privKey = findInLogs(/Private\sKey:\s+(?[a-fA-F0-9]+)/)?.groups?.privKey; - expect(privKey).toHaveLength(64); - await run(`create-account --private-key ${privKey}`); - const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(foundAddress).toBeDefined(); - const ownerAddress = AztecAddress.fromString(foundAddress!); - - debug('Deploy Private Token Contract using created account.'); - await run(`deploy PrivateTokenContractAbi --args ${INITIAL_BALANCE} ${ownerAddress} --salt 0`); - const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(loggedAddress).toBeDefined(); - contractAddress = AztecAddress.fromString(loggedAddress!); - - const deployedContract = await aztecRpcClient.getContractData(contractAddress); - expect(deployedContract?.contractAddress).toEqual(contractAddress); - - debug('Check contract can be found in returned address'); - await run(`check-deploy -ca ${loggedAddress}`); - const checkResult = findInLogs(/Contract\sfound\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(checkResult).toEqual(deployedContract?.contractAddress.toString()); - - // clear logs - clearLogs(); - await run(`get-contract-data ${loggedAddress}`); - const contractDataAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(contractDataAddress).toEqual(deployedContract?.contractAddress.toString()); - - debug("Check owner's balance"); - await run( - `call getBalance --args ${ownerAddress} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, - ); - const balance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; - expect(balance!).toEqual(`${BigInt(INITIAL_BALANCE).toString()}n`); - - debug('Transfer some tokens'); - const existingAccounts = await aztecRpcClient.getAccounts(); - // ensure we pick a different acc - const receiver = existingAccounts.find(acc => acc.address.toString() !== ownerAddress.toString()); - - await run( - `send transfer --args ${TRANSFER_BALANCE} ${receiver?.address.toString()} --contract-address ${contractAddress.toString()} --contract-abi PrivateTokenContractAbi --private-key ${privKey}`, - ); - const txHash = findInLogs(/Transaction\shash:\s+(?\S+)/)?.groups?.txHash; - - debug('Check the transfer receipt'); - await run(`get-tx-receipt ${txHash}`); - const txResult = findInLogs(/Transaction receipt:\s*(?[\s\S]*?\})/)?.groups?.txHash; - const parsedResult = JSON.parse(txResult!); - expect(parsedResult.txHash).toEqual(txHash); - expect(parsedResult.status).toEqual('mined'); - debug("Check Receiver's balance"); - // Reset CLI as we're calling getBalance again - resetCli(); - clearLogs(); - await run( - `call getBalance --args ${receiver?.address.toString()} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, - ); - const receiverBalance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; - expect(receiverBalance).toEqual(`${BigInt(TRANSFER_BALANCE).toString()}n`); - }); - }); -}; diff --git a/yarn-project/canary/Dockerfile b/yarn-project/canary/Dockerfile index b67dfcf68d1..5d5467d0d2c 100644 --- a/yarn-project/canary/Dockerfile +++ b/yarn-project/canary/Dockerfile @@ -1,7 +1,7 @@ FROM node:18-alpine RUN apk update && apk add --no-cache jq bash -ARG COMMIT_TAG="" +ARG COMMIT_TAG="" #Build canary WORKDIR /usr/src/ diff --git a/yarn-project/canary/src/cli.test.ts b/yarn-project/canary/src/cli.test.ts index 2fa302b8542..96e75a945df 100644 --- a/yarn-project/canary/src/cli.test.ts +++ b/yarn-project/canary/src/cli.test.ts @@ -1,13 +1,178 @@ -import { createAztecRpcClient, createDebugLogger, makeFetch } from '@aztec/aztec.js'; -import { cliTestSuite } from '@aztec/cli'; +import { + AztecAddress, + AztecRPC, + CompleteAddress, + createAztecRpcClient, + createDebugLogger, + makeFetch, + waitForSandbox, +} from '@aztec/aztec.js'; +import { getProgram } from '@aztec/cli'; + +import stringArgv from 'string-argv'; +import { format } from 'util'; const { SANDBOX_URL = 'http://localhost:8080' } = process.env; const debug = createDebugLogger('aztec:e2e_cli'); -const setupRPC = () => { - const aztecRpcClient = createAztecRpcClient(SANDBOX_URL, makeFetch([1, 2, 3], true)); - return Promise.resolve(aztecRpcClient); +const INITIAL_BALANCE = 33000; +const TRANSFER_BALANCE = 3000; + +const setupRPC = async () => { + const aztecRpcClient = createAztecRpcClient(SANDBOX_URL, makeFetch([1, 2, 3, 4, 5], true)); + await waitForSandbox(aztecRpcClient); + return aztecRpcClient; }; -cliTestSuite('CLI canary', setupRPC, () => Promise.resolve(), SANDBOX_URL, debug); +describe('CLI canary', () => { + let cli: ReturnType; + let aztecRpcClient: AztecRPC; + let existingAccounts: CompleteAddress[]; + let contractAddress: AztecAddress; + let log: (...args: any[]) => void; + + // All logs emitted by the cli will be collected here, and reset between tests + const logs: string[] = []; + + beforeAll(async () => { + aztecRpcClient = await setupRPC(); + log = (...args: any[]) => { + logs.push(format(...args)); + debug(...args); + }; + }, 60_000); + + // in order to run the same command twice, we need to create a new CLI instance + const resetCli = () => { + cli = getProgram(log, debug); + }; + + beforeEach(() => { + logs.splice(0); + resetCli(); + }); + + // Run a command on the CLI + const run = (cmd: string, addRpcUrl = true) => { + const args = stringArgv(cmd, 'node', 'dest/bin/index.js'); + if (addRpcUrl) { + args.push('--rpc-url', SANDBOX_URL); + } + return cli.parseAsync(args); + }; + + // Returns first match across all logs collected so far + const findInLogs = (regex: RegExp) => { + for (const log of logs) { + const match = regex.exec(log); + if (match) return match; + } + }; + + const findMultipleInLogs = (regex: RegExp) => { + const matches = []; + for (const log of logs) { + const match = regex.exec(log); + if (match) matches.push(match); + } + return matches; + }; + + const clearLogs = () => { + logs.splice(0); + }; + + it('creates & retrieves an account', async () => { + existingAccounts = await aztecRpcClient.getAccounts(); + debug('Create an account'); + await run(`create-account`); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const newAddress = AztecAddress.fromString(foundAddress!); + + const accountsAfter = await aztecRpcClient.getAccounts(); + const expectedAccounts = [...existingAccounts.map(a => a.address), newAddress]; + expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts); + const newCompleteAddress = accountsAfter[accountsAfter.length - 1]; + + // Test get-accounts + debug('Check that account was added to the list of accs in RPC'); + await run('get-accounts'); + const fetchedAddresses = findMultipleInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/); + const foundFetchedAddress = fetchedAddresses.find(match => match.groups?.address === newAddress.toString()); + expect(foundFetchedAddress).toBeDefined(); + + // Test get-account + debug('Check we can retrieve the specific account'); + clearLogs(); + await run(`get-account ${newAddress.toString()}`); + const fetchedAddress = findInLogs(/Public Key:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(fetchedAddress).toEqual(newCompleteAddress.publicKey.toString()); + }, 30_000); + + it('deploys a contract & sends transactions', async () => { + // generate a private key + debug('Create an account using a private key'); + await run('generate-private-key', false); + const privKey = findInLogs(/Private\sKey:\s+(?[a-fA-F0-9]+)/)?.groups?.privKey; + expect(privKey).toHaveLength(64); + await run(`create-account --private-key ${privKey}`); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const ownerAddress = AztecAddress.fromString(foundAddress!); + + debug('Deploy Private Token Contract using created account.'); + await run(`deploy PrivateTokenContractAbi --args ${INITIAL_BALANCE} ${ownerAddress} --salt 0`); + const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(loggedAddress).toBeDefined(); + contractAddress = AztecAddress.fromString(loggedAddress!); + + const deployedContract = await aztecRpcClient.getContractData(contractAddress); + expect(deployedContract?.contractAddress).toEqual(contractAddress); + + debug('Check contract can be found in returned address'); + await run(`check-deploy -ca ${loggedAddress}`); + const checkResult = findInLogs(/Contract\sfound\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(checkResult).toEqual(deployedContract?.contractAddress.toString()); + + // clear logs + clearLogs(); + await run(`get-contract-data ${loggedAddress}`); + const contractDataAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(contractDataAddress).toEqual(deployedContract?.contractAddress.toString()); + + debug("Check owner's balance"); + await run( + `call getBalance --args ${ownerAddress} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const balance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(balance!).toEqual(`${BigInt(INITIAL_BALANCE).toString()}n`); + + debug('Transfer some tokens'); + const existingAccounts = await aztecRpcClient.getAccounts(); + // ensure we pick a different acc + const receiver = existingAccounts.find(acc => acc.address.toString() !== ownerAddress.toString()); + + await run( + `send transfer --args ${TRANSFER_BALANCE} ${receiver?.address.toString()} --contract-address ${contractAddress.toString()} --contract-abi PrivateTokenContractAbi --private-key ${privKey}`, + ); + const txHash = findInLogs(/Transaction\shash:\s+(?\S+)/)?.groups?.txHash; + + debug('Check the transfer receipt'); + await run(`get-tx-receipt ${txHash}`); + const txResult = findInLogs(/Transaction receipt:\s*(?[\s\S]*?\})/)?.groups?.txHash; + const parsedResult = JSON.parse(txResult!); + expect(parsedResult.txHash).toEqual(txHash); + expect(parsedResult.status).toEqual('mined'); + debug("Check Receiver's balance"); + // Reset CLI as we're calling getBalance again + resetCli(); + clearLogs(); + await run( + `call getBalance --args ${receiver?.address.toString()} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const receiverBalance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(receiverBalance).toEqual(`${BigInt(TRANSFER_BALANCE).toString()}n`); + }, 60_000); +}); diff --git a/yarn-project/end-to-end/src/e2e_cli.test.ts b/yarn-project/end-to-end/src/e2e_cli.test.ts index 21d038e6f6b..b18af474804 100644 --- a/yarn-project/end-to-end/src/e2e_cli.test.ts +++ b/yarn-project/end-to-end/src/e2e_cli.test.ts @@ -1,13 +1,17 @@ import { AztecNodeService } from '@aztec/aztec-node'; -import { AztecRPCServer } from '@aztec/aztec-rpc'; +import { AztecAddress, AztecRPCServer } from '@aztec/aztec-rpc'; import { startHttpRpcServer } from '@aztec/aztec-sandbox/http'; import { createDebugLogger } from '@aztec/aztec.js'; -import { cliTestSuite } from '@aztec/cli'; -import { AztecRPC } from '@aztec/types'; +import { getProgram } from '@aztec/cli'; +import { AztecRPC, CompleteAddress } from '@aztec/types'; + +import stringArgv from 'string-argv'; +import { format } from 'util'; import { setup } from './fixtures/utils.js'; const HTTP_PORT = 9009; +const RPC_URL = `http://localhost:${HTTP_PORT}`; const debug = createDebugLogger('aztec:e2e_cli'); let http: ReturnType; @@ -30,4 +34,161 @@ const cleanup = async () => { await (aztecRpcServer as AztecRPCServer).stop(); }; -cliTestSuite('CLI e2e test', testSetup, cleanup, `http://localhost:${HTTP_PORT}`, debug); +const INITIAL_BALANCE = 33000; +const TRANSFER_BALANCE = 3000; + +describe('CLI e2e test', () => { + let cli: ReturnType; + let aztecRpcClient: AztecRPC; + let existingAccounts: CompleteAddress[]; + let contractAddress: AztecAddress; + let log: (...args: any[]) => void; + + // All logs emitted by the cli will be collected here, and reset between tests + const logs: string[] = []; + + beforeAll(async () => { + aztecRpcClient = await testSetup(); + log = (...args: any[]) => { + logs.push(format(...args)); + debug(...args); + }; + }); + + afterAll(async () => { + await cleanup(); + }); + + // in order to run the same command twice, we need to create a new CLI instance + const resetCli = () => { + cli = getProgram(log, debug); + }; + + beforeEach(() => { + logs.splice(0); + resetCli(); + }); + + // Run a command on the CLI + const run = (cmd: string, addRpcUrl = true) => { + const args = stringArgv(cmd, 'node', 'dest/bin/index.js'); + if (addRpcUrl) { + args.push('--rpc-url', RPC_URL); + } + return cli.parseAsync(args); + }; + + // Returns first match across all logs collected so far + const findInLogs = (regex: RegExp) => { + for (const log of logs) { + const match = regex.exec(log); + if (match) return match; + } + }; + + const findMultipleInLogs = (regex: RegExp) => { + const matches = []; + for (const log of logs) { + const match = regex.exec(log); + if (match) matches.push(match); + } + return matches; + }; + + const clearLogs = () => { + logs.splice(0); + }; + + it('creates & retrieves an account', async () => { + existingAccounts = await aztecRpcClient.getAccounts(); + debug('Create an account'); + await run(`create-account`); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const newAddress = AztecAddress.fromString(foundAddress!); + + const accountsAfter = await aztecRpcClient.getAccounts(); + const expectedAccounts = [...existingAccounts.map(a => a.address), newAddress]; + expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts); + const newCompleteAddress = accountsAfter[accountsAfter.length - 1]; + + // Test get-accounts + debug('Check that account was added to the list of accs in RPC'); + await run('get-accounts'); + const fetchedAddresses = findMultipleInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/); + const foundFetchedAddress = fetchedAddresses.find(match => match.groups?.address === newAddress.toString()); + expect(foundFetchedAddress).toBeDefined(); + + // Test get-account + debug('Check we can retrieve the specific account'); + clearLogs(); + await run(`get-account ${newAddress.toString()}`); + const fetchedAddress = findInLogs(/Public Key:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(fetchedAddress).toEqual(newCompleteAddress.publicKey.toString()); + }); + + it('deploys a contract & sends transactions', async () => { + // generate a private key + debug('Create an account using a private key'); + await run('generate-private-key', false); + const privKey = findInLogs(/Private\sKey:\s+(?[a-fA-F0-9]+)/)?.groups?.privKey; + expect(privKey).toHaveLength(64); + await run(`create-account --private-key ${privKey}`); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const ownerAddress = AztecAddress.fromString(foundAddress!); + + debug('Deploy Private Token Contract using created account.'); + await run(`deploy PrivateTokenContractAbi --args ${INITIAL_BALANCE} ${ownerAddress} --salt 0`); + const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(loggedAddress).toBeDefined(); + contractAddress = AztecAddress.fromString(loggedAddress!); + + const deployedContract = await aztecRpcClient.getContractData(contractAddress); + expect(deployedContract?.contractAddress).toEqual(contractAddress); + + debug('Check contract can be found in returned address'); + await run(`check-deploy -ca ${loggedAddress}`); + const checkResult = findInLogs(/Contract\sfound\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(checkResult).toEqual(deployedContract?.contractAddress.toString()); + + // clear logs + clearLogs(); + await run(`get-contract-data ${loggedAddress}`); + const contractDataAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(contractDataAddress).toEqual(deployedContract?.contractAddress.toString()); + + debug("Check owner's balance"); + await run( + `call getBalance --args ${ownerAddress} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const balance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(balance!).toEqual(`${BigInt(INITIAL_BALANCE).toString()}n`); + + debug('Transfer some tokens'); + const existingAccounts = await aztecRpcClient.getAccounts(); + // ensure we pick a different acc + const receiver = existingAccounts.find(acc => acc.address.toString() !== ownerAddress.toString()); + + await run( + `send transfer --args ${TRANSFER_BALANCE} ${receiver?.address.toString()} --contract-address ${contractAddress.toString()} --contract-abi PrivateTokenContractAbi --private-key ${privKey}`, + ); + const txHash = findInLogs(/Transaction\shash:\s+(?\S+)/)?.groups?.txHash; + + debug('Check the transfer receipt'); + await run(`get-tx-receipt ${txHash}`); + const txResult = findInLogs(/Transaction receipt:\s*(?[\s\S]*?\})/)?.groups?.txHash; + const parsedResult = JSON.parse(txResult!); + expect(parsedResult.txHash).toEqual(txHash); + expect(parsedResult.status).toEqual('mined'); + debug("Check Receiver's balance"); + // Reset CLI as we're calling getBalance again + resetCli(); + clearLogs(); + await run( + `call getBalance --args ${receiver?.address.toString()} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const receiverBalance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(receiverBalance).toEqual(`${BigInt(TRANSFER_BALANCE).toString()}n`); + }); +}); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index b9a7a3bb8b1..a63db18aeed 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -331,7 +331,6 @@ __metadata: jest-mock-extended: ^3.0.5 lodash.startcase: ^4.4.0 semver: ^7.5.4 - string-argv: ^0.3.2 ts-jest: ^29.1.0 ts-node: ^10.9.1 tslib: ^2.4.0