Skip to content

Commit 7e1b771

Browse files
Add new release (#79)
2 parents cdfe806 + 6214dd9 commit 7e1b771

7 files changed

+85
-31
lines changed

.changeset/heavy-apples-appear.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eth-tech-tree": patch
3+
---
4+
5+
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

src/actions/submit-challenge.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { loadUserState } from "../utils/state-manager";
22
import { submitChallengeToServer } from "../modules/api";
33
import chalk from "chalk";
44
import { input } from "@inquirer/prompts";
5+
import { isValidAddress } from "../utils/helpers";
56

67
export async function submitChallenge(name: string, contractAddress?: string) {
78
const { address: userAddress } = loadUserState();
89
if (!contractAddress) {
910
// Prompt the user for the contract address
1011
const question = {
11-
message: "Completed challenge contract address on Sepolia:",
12-
validate: (value: string) => /^0x[a-fA-F0-9]{40}$/.test(value),
12+
message: "What is the contract address of your completed challenge?:",
13+
validate: (value: string) => isValidAddress(value) ? true : "Please enter a valid contract address",
1314
};
1415
const answer = await input(question);
1516
contractAddress = answer;

src/cli.ts

+19-11
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,26 @@ import { TechTree } from ".";
1010

1111

1212
export async function cli(args: Args) {
13-
const commands = await parseCommandArgumentsAndOptions(args);
14-
const userState = loadUserState();
15-
if (commands.command || commands.help) {
16-
const parsedCommands = await promptForMissingCommandArgs(commands, userState);
17-
await handleCommand(parsedCommands);
18-
} else {
19-
await renderIntroMessage();
20-
await init(userState);
21-
// Navigate tree
22-
const techTree = new TechTree();
13+
try {
14+
const commands = await parseCommandArgumentsAndOptions(args);
15+
const userState = loadUserState();
16+
if (commands.command || commands.help) {
17+
const parsedCommands = await promptForMissingCommandArgs(commands, userState);
18+
await handleCommand(parsedCommands);
19+
} else {
20+
await renderIntroMessage();
21+
await init(userState);
22+
// Navigate tree
23+
const techTree = new TechTree();
2324

24-
await techTree.start();
25+
await techTree.start();
26+
}
27+
} catch (error) {
28+
if (error instanceof Error && error.name === 'ExitPromptError') {
29+
// Because canceling the promise (e.g. ctrl+c) can cause the inquirer prompt to throw we need to silence this error
30+
} else {
31+
throw error;
32+
}
2533
}
2634
}
2735

src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class TechTree {
9191
}
9292
} catch (error) {
9393
if (error instanceof Error && error.name === 'ExitPromptError') {
94-
// Because canceling the promise can cause the inquirer prompt to throw we need to silence this error
94+
// Because canceling the promise (e.g. ctrl+c) can cause the inquirer prompt to throw we need to silence this error
9595
} else {
9696
throw error;
9797
}

src/tasks/parse-command-arguments-and-options.ts

+39-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import arg from "arg";
22
import { IUser } from "../types";
33
import fs from "fs";
4-
import { select, input } from "@inquirer/prompts";
5-
import { isValidAddress } from "../utils/helpers";
4+
import { search, input } from "@inquirer/prompts";
5+
import { isValidAddress, searchChallenges } from "../utils/helpers";
66
import { promptForMissingUserState } from "./prompt-for-missing-user-state";
77

88
type Commands = {
@@ -27,6 +27,28 @@ type SubmitCommand = {
2727

2828
export type CommandOptions = BaseOptions & { command: string | null } & SetupCommand & SubmitCommand;
2929

30+
export type Choice<Value> = {
31+
value: Value;
32+
name?: string;
33+
description?: string;
34+
short?: string;
35+
disabled?: boolean | string;
36+
};
37+
38+
type SearchOptions = {
39+
type: "search";
40+
name: string;
41+
message: string;
42+
source: (term: string | undefined) => Promise<Choice<string>[]>;
43+
}
44+
45+
type InputOptions = {
46+
type: "input";
47+
name: string;
48+
message: string;
49+
validate: (value: string) => string | true;
50+
}
51+
3052
const commandArguments = {
3153
setup: {
3254
1: "challenge",
@@ -76,9 +98,6 @@ export async function parseCommandArgumentsAndOptions(
7698
}
7799

78100
export async function promptForMissingCommandArgs(commands: CommandOptions, userState: IUser): Promise<CommandOptions> {
79-
const cliAnswers = Object.fromEntries(
80-
Object.entries(commands).filter(([key, value]) => value !== null)
81-
);
82101
const questions = [];
83102

84103
const { command, challenge, contractAddress } = commands;
@@ -90,9 +109,10 @@ export async function promptForMissingCommandArgs(commands: CommandOptions, user
90109
if (command === "setup") {
91110
if (!challenge) {
92111
questions.push({
93-
type: "input",
112+
type: "search",
94113
name: "challenge",
95114
message: "Which challenge would you like to setup?",
115+
source: searchChallenges
96116
});
97117
}
98118
if (!installLocation) {
@@ -108,28 +128,34 @@ export async function promptForMissingCommandArgs(commands: CommandOptions, user
108128

109129
if (command === "submit") {
110130
// Need user state so direct to promptForMissingUserState
111-
await promptForMissingUserState(userState);
131+
await promptForMissingUserState(userState, true);
112132

113133
if (!challenge) {
114134
questions.push({
115-
type: "input",
135+
type: "search",
116136
name: "challenge",
117137
message: "Which challenge would you like to submit?",
138+
source: searchChallenges
118139
});
119140
}
120141
if (!contractAddress) {
121142
questions.push({
122143
type: "input",
123144
name: "contractAddress",
124-
message: "What is the deployed contract address?",
125-
validate: isValidAddress,
145+
message: "What is the contract address of your completed challenge?",
146+
validate: (value: string) => isValidAddress(value) ? true : "Please enter a valid contract address",
126147
});
127148
}
128149
}
129-
const answers = [];
150+
const answers: Record<string, string> = {};
130151
for (const question of questions) {
131-
const answer = await input(question);
132-
answers.push(answer);
152+
if (question.type === "search") {
153+
const answer = await search(question as unknown as SearchOptions);
154+
answers[question.name] = answer;
155+
} else if (question.type === "input") {
156+
const answer = await input(question as InputOptions);
157+
answers[question.name] = answer;
158+
}
133159
}
134160

135161
return {

src/tasks/prompt-for-missing-user-state.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const defaultOptions: Partial<IUser> = {
1010
};
1111

1212
export async function promptForMissingUserState(
13-
userState: IUser
13+
userState: IUser,
14+
skipInstallLocation: boolean = false
1415
): Promise<IUser> {
1516
const userDevice = getDevice();
1617
let identifier = userState.address;
@@ -39,7 +40,7 @@ export async function promptForMissingUserState(
3940
}
4041

4142
// Prompt for install location if it doesn't exist on device
42-
if (!existingInstallLocation) {
43+
if (!existingInstallLocation && !skipInstallLocation) {
4344
const answer = await input({
4445
message: "Where would you like to download the challenges?",
4546
default: defaultOptions.installLocation,
@@ -53,8 +54,8 @@ export async function promptForMissingUserState(
5354
}
5455

5556
const { address, ens, installLocations, challenges, creationTimestamp } = user;
56-
const thisDeviceLocation = installLocations.find((loc: {location: string, device: string}) => loc.device === userDevice);
57-
const newState = { address, ens, installLocation: thisDeviceLocation.location, challenges, creationTimestamp };
57+
const thisDeviceLocation = installLocations?.find((loc: {location: string, device: string}) => loc.device === userDevice);
58+
const newState = { address, ens, installLocation: thisDeviceLocation?.location, challenges, creationTimestamp };
5859
if (JSON.stringify(userState) !== JSON.stringify(newState)) {
5960
// Save the new state locally
6061
await saveUserState(newState);

src/utils/helpers.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import os from "os";
22
import fs from "fs";
33
import { IChallenge } from "../types";
4+
import { loadChallenges } from "./state-manager";
5+
import { fetchChallenges } from "../modules/api";
6+
import { Choice } from "../tasks/parse-command-arguments-and-options";
47

58
export function wait(ms: number) {
69
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -53,4 +56,14 @@ export const calculatePoints = (completedChallenges: Array<{ challenge: IChallen
5356
const points = pointsPerLevel[challenge!.level - 1] || 100;
5457
return total + points;
5558
}, 0);
59+
}
60+
61+
export const searchChallenges = async (term: string = "") => {
62+
const challenges = (await fetchChallenges()).filter((challenge: IChallenge) => challenge.enabled);
63+
const choices = challenges.map((challenge: IChallenge) => ({
64+
value: challenge.name,
65+
name: challenge.label,
66+
description: ""
67+
}));
68+
return choices.filter((choice: Choice<string>) => choice.name?.toLowerCase().includes(term.toLowerCase()));
5669
}

0 commit comments

Comments
 (0)