diff --git a/.changeset/heavy-apples-appear.md b/.changeset/heavy-apples-appear.md new file mode 100644 index 0000000..ffbcdc9 --- /dev/null +++ b/.changeset/heavy-apples-appear.md @@ -0,0 +1,5 @@ +--- +"eth-tech-tree": patch +--- + +Update validation messages when submitting a completed CA for a challenge, Add searchable list when setting up or submitting with command, few extra bug-fixes/tweaks diff --git a/src/actions/submit-challenge.ts b/src/actions/submit-challenge.ts index 4cc15a2..70e3f2a 100644 --- a/src/actions/submit-challenge.ts +++ b/src/actions/submit-challenge.ts @@ -2,14 +2,15 @@ import { loadUserState } from "../utils/state-manager"; import { submitChallengeToServer } from "../modules/api"; import chalk from "chalk"; import { input } from "@inquirer/prompts"; +import { isValidAddress } from "../utils/helpers"; export async function submitChallenge(name: string, contractAddress?: string) { const { address: userAddress } = loadUserState(); if (!contractAddress) { // Prompt the user for the contract address const question = { - message: "Completed challenge contract address on Sepolia:", - validate: (value: string) => /^0x[a-fA-F0-9]{40}$/.test(value), + message: "What is the contract address of your completed challenge?:", + validate: (value: string) => isValidAddress(value) ? true : "Please enter a valid contract address", }; const answer = await input(question); contractAddress = answer; diff --git a/src/cli.ts b/src/cli.ts index 8ee5652..409315f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,18 +10,26 @@ import { TechTree } from "."; export async function cli(args: Args) { - const commands = await parseCommandArgumentsAndOptions(args); - const userState = loadUserState(); - if (commands.command || commands.help) { - const parsedCommands = await promptForMissingCommandArgs(commands, userState); - await handleCommand(parsedCommands); - } else { - await renderIntroMessage(); - await init(userState); - // Navigate tree - const techTree = new TechTree(); + try { + const commands = await parseCommandArgumentsAndOptions(args); + const userState = loadUserState(); + if (commands.command || commands.help) { + const parsedCommands = await promptForMissingCommandArgs(commands, userState); + await handleCommand(parsedCommands); + } else { + await renderIntroMessage(); + await init(userState); + // Navigate tree + const techTree = new TechTree(); - await techTree.start(); + await techTree.start(); + } + } catch (error) { + if (error instanceof Error && error.name === 'ExitPromptError') { + // Because canceling the promise (e.g. ctrl+c) can cause the inquirer prompt to throw we need to silence this error + } else { + throw error; + } } } diff --git a/src/index.ts b/src/index.ts index 4203aef..af3cc05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,7 +91,7 @@ export class TechTree { } } catch (error) { if (error instanceof Error && error.name === 'ExitPromptError') { - // Because canceling the promise can cause the inquirer prompt to throw we need to silence this error + // Because canceling the promise (e.g. ctrl+c) can cause the inquirer prompt to throw we need to silence this error } else { throw error; } diff --git a/src/tasks/parse-command-arguments-and-options.ts b/src/tasks/parse-command-arguments-and-options.ts index b70de80..71df57f 100644 --- a/src/tasks/parse-command-arguments-and-options.ts +++ b/src/tasks/parse-command-arguments-and-options.ts @@ -1,8 +1,8 @@ import arg from "arg"; import { IUser } from "../types"; import fs from "fs"; -import { select, input } from "@inquirer/prompts"; -import { isValidAddress } from "../utils/helpers"; +import { search, input } from "@inquirer/prompts"; +import { isValidAddress, searchChallenges } from "../utils/helpers"; import { promptForMissingUserState } from "./prompt-for-missing-user-state"; type Commands = { @@ -27,6 +27,28 @@ type SubmitCommand = { export type CommandOptions = BaseOptions & { command: string | null } & SetupCommand & SubmitCommand; +export type Choice = { + value: Value; + name?: string; + description?: string; + short?: string; + disabled?: boolean | string; +}; + +type SearchOptions = { + type: "search"; + name: string; + message: string; + source: (term: string | undefined) => Promise[]>; +} + +type InputOptions = { + type: "input"; + name: string; + message: string; + validate: (value: string) => string | true; +} + const commandArguments = { setup: { 1: "challenge", @@ -76,9 +98,6 @@ export async function parseCommandArgumentsAndOptions( } export async function promptForMissingCommandArgs(commands: CommandOptions, userState: IUser): Promise { - const cliAnswers = Object.fromEntries( - Object.entries(commands).filter(([key, value]) => value !== null) - ); const questions = []; const { command, challenge, contractAddress } = commands; @@ -90,9 +109,10 @@ export async function promptForMissingCommandArgs(commands: CommandOptions, user if (command === "setup") { if (!challenge) { questions.push({ - type: "input", + type: "search", name: "challenge", message: "Which challenge would you like to setup?", + source: searchChallenges }); } if (!installLocation) { @@ -108,28 +128,34 @@ export async function promptForMissingCommandArgs(commands: CommandOptions, user if (command === "submit") { // Need user state so direct to promptForMissingUserState - await promptForMissingUserState(userState); + await promptForMissingUserState(userState, true); if (!challenge) { questions.push({ - type: "input", + type: "search", name: "challenge", message: "Which challenge would you like to submit?", + source: searchChallenges }); } if (!contractAddress) { questions.push({ type: "input", name: "contractAddress", - message: "What is the deployed contract address?", - validate: isValidAddress, + message: "What is the contract address of your completed challenge?", + validate: (value: string) => isValidAddress(value) ? true : "Please enter a valid contract address", }); } } - const answers = []; + const answers: Record = {}; for (const question of questions) { - const answer = await input(question); - answers.push(answer); + if (question.type === "search") { + const answer = await search(question as unknown as SearchOptions); + answers[question.name] = answer; + } else if (question.type === "input") { + const answer = await input(question as InputOptions); + answers[question.name] = answer; + } } return { diff --git a/src/tasks/prompt-for-missing-user-state.ts b/src/tasks/prompt-for-missing-user-state.ts index 9ac06c5..1ceaca6 100644 --- a/src/tasks/prompt-for-missing-user-state.ts +++ b/src/tasks/prompt-for-missing-user-state.ts @@ -10,7 +10,8 @@ const defaultOptions: Partial = { }; export async function promptForMissingUserState( - userState: IUser + userState: IUser, + skipInstallLocation: boolean = false ): Promise { const userDevice = getDevice(); let identifier = userState.address; @@ -39,7 +40,7 @@ export async function promptForMissingUserState( } // Prompt for install location if it doesn't exist on device - if (!existingInstallLocation) { + if (!existingInstallLocation && !skipInstallLocation) { const answer = await input({ message: "Where would you like to download the challenges?", default: defaultOptions.installLocation, @@ -53,8 +54,8 @@ export async function promptForMissingUserState( } const { address, ens, installLocations, challenges, creationTimestamp } = user; - const thisDeviceLocation = installLocations.find((loc: {location: string, device: string}) => loc.device === userDevice); - const newState = { address, ens, installLocation: thisDeviceLocation.location, challenges, creationTimestamp }; + const thisDeviceLocation = installLocations?.find((loc: {location: string, device: string}) => loc.device === userDevice); + const newState = { address, ens, installLocation: thisDeviceLocation?.location, challenges, creationTimestamp }; if (JSON.stringify(userState) !== JSON.stringify(newState)) { // Save the new state locally await saveUserState(newState); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index f63520a..a7bb2c0 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,6 +1,9 @@ import os from "os"; import fs from "fs"; import { IChallenge } from "../types"; +import { loadChallenges } from "./state-manager"; +import { fetchChallenges } from "../modules/api"; +import { Choice } from "../tasks/parse-command-arguments-and-options"; export function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -53,4 +56,14 @@ export const calculatePoints = (completedChallenges: Array<{ challenge: IChallen const points = pointsPerLevel[challenge!.level - 1] || 100; return total + points; }, 0); +} + +export const searchChallenges = async (term: string = "") => { + const challenges = (await fetchChallenges()).filter((challenge: IChallenge) => challenge.enabled); + const choices = challenges.map((challenge: IChallenge) => ({ + value: challenge.name, + name: challenge.label, + description: "" + })); + return choices.filter((choice: Choice) => choice.name?.toLowerCase().includes(term.toLowerCase())); } \ No newline at end of file