From c081c819de9bd1a90230c4f972d08b520d0704fc Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Wed, 8 Jan 2025 12:08:23 -0500 Subject: [PATCH 1/6] clearer messaging when requests CA --- src/actions/submit-challenge.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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; From ff7a4a4e2a0c02aab60fe0fa3ae2fb3ee0f9140d Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Wed, 8 Jan 2025 12:08:55 -0500 Subject: [PATCH 2/6] search list when using commands --- .../parse-command-arguments-and-options.ts | 50 ++++++++++++++----- src/utils/helpers.ts | 13 +++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/tasks/parse-command-arguments-and-options.ts b/src/tasks/parse-command-arguments-and-options.ts index b70de80..dde8fe4 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) { @@ -112,24 +132,30 @@ export async function promptForMissingCommandArgs(commands: CommandOptions, user 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/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 From 831284909b4b9f3a406e034fd31da3856c73a578 Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Wed, 8 Jan 2025 12:30:59 -0500 Subject: [PATCH 3/6] skip asking for installlocation on submit --- src/tasks/parse-command-arguments-and-options.ts | 2 +- src/tasks/prompt-for-missing-user-state.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tasks/parse-command-arguments-and-options.ts b/src/tasks/parse-command-arguments-and-options.ts index dde8fe4..71df57f 100644 --- a/src/tasks/parse-command-arguments-and-options.ts +++ b/src/tasks/parse-command-arguments-and-options.ts @@ -128,7 +128,7 @@ 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({ 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); From a5667c29a1b856bfac666764e43c187a27a1fb7d Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Wed, 8 Jan 2025 12:35:39 -0500 Subject: [PATCH 4/6] silence ExitPromptErrors --- src/cli.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 8ee5652..2332174 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 can cause the inquirer prompt to throw we need to silence this error + } else { + throw error; + } } } From 38d370a52fd07f8b33b391ed17b4d0c6610b11f1 Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Wed, 8 Jan 2025 14:15:51 -0500 Subject: [PATCH 5/6] clarify comment --- src/cli.ts | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 2332174..409315f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,7 +26,7 @@ export async function cli(args: Args) { } } 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/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; } From 546b37ac328579a5ac93998149378621e71b50f1 Mon Sep 17 00:00:00 2001 From: "escottalexander@gmail.com" Date: Wed, 8 Jan 2025 14:29:36 -0500 Subject: [PATCH 6/6] changeset --- .changeset/heavy-apples-appear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/heavy-apples-appear.md 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