Skip to content

Commit ec1ec0d

Browse files
Release (#93)
2 parents 5aec716 + ba5e2ed commit ec1ec0d

File tree

5 files changed

+66
-1020
lines changed

5 files changed

+66
-1020
lines changed

.changeset/fuzzy-hats-ring.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eth-tech-tree": minor
3+
---
4+
5+
using create-eth extensions for challenges, bug fixes with coloring of menus and view height

package.json

+2-9
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,13 @@
4343
"dependencies": {
4444
"@changesets/cli": "^2.26.2",
4545
"@inquirer/prompts": "^7.1.0",
46-
"@types/terminal-kit": "^2.5.6",
4746
"ansi-escapes": "^7.0.0",
4847
"arg": "5.0.2",
4948
"chalk": "^5.3.0",
50-
"create-eth": "^0.0.41",
49+
"create-eth": "latest",
5150
"dotenv": "^16.4.5",
5251
"execa": "7.1.1",
53-
"handlebars": "^4.7.7",
54-
"listr2": "^8.2.5",
55-
"merge-packages": "^0.1.6",
56-
"ncp": "^2.0.0",
57-
"pkg-install": "1.0.0",
58-
"semver": "^7.6.3",
59-
"terminal-kit": "^3.1.1"
52+
"semver": "^7.6.3"
6053
},
6154
"packageManager": "[email protected]"
6255
}

src/actions/setup-challenge.ts

+15-221
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,28 @@
1+
import chalk from "chalk";
2+
import fs from "fs";
13
import { execa } from "execa";
24
import semver, { Range } from 'semver';
3-
import ncp from "ncp";
4-
import path from "path";
5-
import fs from "fs";
6-
import { createFirstGitCommit } from "../tasks/create-first-git-commit";
7-
import { fetchChallenges } from "../modules/api";
8-
import { loadChallenges } from "../utils/state-manager";
9-
import { IChallenge } from "../types";
10-
import { BASE_REPO, BASE_BRANCH, BASE_COMMIT } from "../config";
11-
import { DefaultRenderer, Listr, ListrTaskWrapper, SimpleRenderer } from "listr2";
12-
import chalk from "chalk";
135

146
type RequiredDependency = "node" | "git" | "yarn" | "foundryup";
157

16-
// Sidestep for ncp issue https://github.com/AvianFlu/ncp/issues/127
17-
const copy = (source: string, destination: string, options?: ncp.Options) => new Promise((resolve, reject) => {
18-
ncp(source, destination, options || {}, (err) => {
19-
if (err) {
20-
reject(err);
21-
} else {
22-
setTimeout(resolve, 100);
23-
}
24-
});
25-
});
26-
27-
const filesToRemove = [
28-
"packages/foundry/contracts/YourContract.sol",
29-
"packages/foundry/script/DeployYourContract.s.sol",
30-
"packages/foundry/test/YourContract.t.sol"
31-
];
32-
338
export const setupChallenge = async (name: string, installLocation: string) => {
34-
let challengeRepo = loadChallenges().find(challenge => challenge.name === name)?.repo;
35-
if (!challengeRepo) {
36-
// Fetch challenges from server if not locally available
37-
const challenges = await fetchChallenges();
38-
challengeRepo = challenges.find((challenge: IChallenge) => challenge.name === name)?.repo;
39-
}
40-
41-
// Check if challenge repository was found
42-
if (!challengeRepo) {
43-
console.log("A challenge repository was not found with that name.");
44-
return;
45-
}
46-
47-
// Use environment variable as override if provided
48-
challengeRepo = process.env.CHALLENGE_REPO || challengeRepo;
9+
checkUserDependencies();
4910

50-
const targetDir = path.join(`${installLocation}/${name}`);
51-
// Make sure the install location exists
52-
if (!fs.existsSync(installLocation)) {
53-
fs.mkdirSync(installLocation);
54-
}
55-
// Check for existing repository
56-
const repoExists = fs.existsSync(targetDir);
57-
if (repoExists) {
58-
console.log("Repository already exists, skipping download.");
59-
return;
60-
}
11+
const challengeRepo = process.env.CHALLENGE_REPO || "BuidlGuidl/eth-tech-tree-challenges";
6112

62-
const tasks = new Listr([
63-
{
64-
title: 'Checking for required dependencies',
65-
task: () => checkUserDependencies()
66-
},
67-
{
68-
title: 'Setting up base repository',
69-
task: () => setupBaseRepo(targetDir)
70-
},
71-
{
72-
title: 'Merging challenge files',
73-
task: () => mergeChallenge(challengeRepo as string, name, targetDir)
74-
},
75-
{
76-
title: 'Installing dependencies',
77-
task: (_, task) => installPackages(targetDir, task),
78-
rendererOptions: {
79-
outputBar: 8,
80-
persistentOutput: false,
81-
},
82-
},
83-
{
84-
title: 'Initializing Git repository',
85-
task: () => createFirstGitCommit(targetDir)
86-
}
87-
]);
13+
// Create install location if it doesn't exist
14+
fs.mkdirSync(installLocation, { recursive: true });
8815

16+
const extensionName = `${challengeRepo}:${name}-extension`;
17+
const challengeDir = `${installLocation}/${name}`;
8918
try {
90-
await tasks.run();
91-
console.log(chalk.green("Challenge setup completed successfully."));
92-
console.log("");
93-
console.log(chalk.cyan(`Now open this repository in your favorite code editor and look at the readme for instructions: ${targetDir}`));
94-
} catch (error: any) {
95-
console.error(chalk.red("An error occurred during challenge setup:"), error.message);
96-
}
19+
await execa("create-eth", ["-e", extensionName, challengeDir], { stdio: "inherit" });
20+
console.clear();
21+
console.log(chalk.green("Challenge setup completed successfully.\n"));
22+
console.log(chalk.cyan(`Now open this repository in your favorite code editor and look at the readme for instructions:\n${challengeDir}`));
23+
} catch (e) {
24+
console.error(`Failed to create challenge: ${name}, \n${e}`);
25+
}
9726
}
9827

9928
const checkDependencyInstalled = async (name: RequiredDependency) => {
@@ -124,139 +53,4 @@ export const checkUserDependencies = async () => {
12453
])
12554
}
12655

127-
const setupBaseRepo = async (targetDir: string): Promise<void> => {
128-
await execa("git", ["clone", "--branch", BASE_BRANCH, "--single-branch", BASE_REPO, targetDir]);
129-
await execa("git", ["checkout", BASE_COMMIT], { cwd: targetDir });
130-
for (const file of filesToRemove) {
131-
await execa("rm", [path.join(targetDir, file)]);
132-
}
133-
}
134-
135-
const mergeChallenge = async (challengeRepo: string, name: string, targetDir: string): Promise<void> => {
136-
const tempDir = path.join("temp_" + Math.random().toString(36).substring(2));
137-
await execa("git", ["clone", "--branch", name, "--single-branch", challengeRepo, tempDir]);
138-
await copy(tempDir, targetDir);
139-
await execa("rm", ["-rf", tempDir]);
140-
const readmePath = path.join(targetDir, "README.md");
141-
const readmeContent = fs.readFileSync(readmePath, "utf8");
142-
const modifiedReadme = readmeContent
143-
.replace("@@TOP_CONTENT@@", README_CONTENT.TOP_CONTENT)
144-
.replace("@@BOTTOM_CONTENT@@", README_CONTENT.BOTTOM_CONTENT);
145-
fs.writeFileSync(readmePath, modifiedReadme);
146-
}
147-
148-
const installPackages = async (targetDir: string, task: ListrTaskWrapper<any, typeof DefaultRenderer, typeof SimpleRenderer>): Promise<void> => {
149-
const execute = execa("yarn", ["install"], { cwd: targetDir });
150-
let outputBuffer: string = "";
151-
152-
const chunkSize = 1024;
153-
execute?.stdout?.on("data", (data: Buffer) => {
154-
outputBuffer += data.toString();
155-
156-
if (outputBuffer.length > chunkSize) {
157-
outputBuffer = outputBuffer.slice(-1 * chunkSize);
158-
}
159-
160-
const visibleOutput =
161-
outputBuffer
162-
.match(new RegExp(`.{1,${chunkSize}}`, "g"))
163-
?.slice(-1)
164-
.map(chunk => chunk.trimEnd() + "\n")
165-
.join("") ?? outputBuffer;
166-
167-
task.output = visibleOutput;
168-
if (visibleOutput.includes("Link step")) {
169-
task.output = chalk.yellow(`starting link step, this might take a little time...`);
170-
}
171-
});
172-
173-
execute?.stderr?.on("data", (data: Buffer) => {
174-
outputBuffer += data.toString();
175-
176-
if (outputBuffer.length > chunkSize) {
177-
outputBuffer = outputBuffer.slice(-1 * chunkSize);
178-
}
179-
180-
const visibleOutput =
181-
outputBuffer
182-
.match(new RegExp(`.{1,${chunkSize}}`, "g"))
183-
?.slice(-1)
184-
.map(chunk => chunk.trimEnd() + "\n")
185-
.join("") ?? outputBuffer;
186-
187-
task.output = visibleOutput;
188-
});
189-
190-
await execute;
191-
};
192-
193-
const README_CONTENT = {
194-
TOP_CONTENT: `## Contents
195-
- [Requirements](#requirements)
196-
- [Start Here](#start-here)
197-
- [Challenge Description](#challenge-description)
198-
- [Testing Your Progress](#testing-your-progress)
199-
- [Solved! (Final Steps)](#solved-final-steps)
200-
201-
## Requirements
202-
Before you begin, you need to install the following tools:
203-
204-
- [Node (v18 LTS)](https://nodejs.org/en/download/)
205-
- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install))
206-
- [Git](https://git-scm.com/downloads)
207-
- [Foundryup](https://book.getfoundry.sh/getting-started/installation)
208-
209-
__For Windows users we highly recommend using [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) or Git Bash as your terminal app.__
210-
211-
## Start Here
212-
Run the following commands in your terminal:
213-
\`\`\`bash
214-
yarn install
215-
foundryup
216-
\`\`\``,
217-
BOTTOM_CONTENT: `## Testing Your Progress
218-
Use your skills to build out the above requirements in whatever way you choose. You are encouraged to run tests periodically to visualize your progress.
219-
220-
Run tests using \`yarn foundry:test\` to run a set of tests against the contract code. Initially you will see build errors but as you complete the requirements you will start to pass tests. If you struggle to understand why some tests are returning errors then you might find it useful to run the command with the extra logging verbosity flag \`-vvvv\` (\`yarn foundry:test -vvvv\`) as this will show you very detailed information about where tests are failing. Learn how to read the traces [here](https://book.getfoundry.sh/forge/traces). You can also use the \`--match-test "TestName"\` flag to only run a single test. Of course you can chain both to include a higher verbosity and only run a specific test by including both flags \`yarn foundry:test -vvvv --match-test "TestName"\`. You will also see we have included an import of \`console2.sol\` which allows you to use \`console.log()\` type functionality inside your contracts to know what a value is at a specific time of execution. You can read more about how to use that at [FoundryBook](https://book.getfoundry.sh/reference/forge-std/console-log).
221-
222-
For a more "hands on" approach you can try testing your contract with the provided front end interface by running the following:
223-
\`\`\`bash
224-
yarn chain
225-
\`\`\`
226-
in a second terminal deploy your contract:
227-
\`\`\`bash
228-
yarn deploy
229-
\`\`\`
230-
in a third terminal start the NextJS front end:
231-
\`\`\`bash
232-
yarn start
233-
\`\`\`
234-
235-
## Solved! (Final Steps)
236-
Once you have a working solution and all the tests are passing your next move is to deploy your lovely contract to the Sepolia testnet.
237-
First you will need to generate an account. **You can skip this step if you have already created a keystore on your machine. Keystores are located in \`~/.foundry/keystores\`**
238-
\`\`\`bash
239-
yarn account:generate
240-
\`\`\`
241-
You can optionally give your new account a name be passing it in like so: \`yarn account:generate NAME-FOR-ACCOUNT\`. The default is \`scaffold-eth-custom\`.
242-
243-
You will be prompted for a password to encrypt your newly created keystore. Make sure you choose a [good one](https://xkcd.com/936/) if you intend to use your new account for more than testnet funds.
244-
245-
Now you need to update \`packages/foundry/.env\` so that \`ETH_KEYSTORE_ACCOUNT\` = your new account name ("scaffold-eth-custom" if you didn't specify otherwise).
246-
247-
Now you are ready to send some testnet funds to your new account.
248-
Run the following to view your new address and balances across several networks.
249-
\`\`\`bash
250-
yarn account
251-
\`\`\`
252-
To fund your account with Sepolia ETH simply search for "Sepolia testnet faucet" on Google or ask around in onchain developer groups who are usually more than willing to share. Send the funds to your wallet address and run \`yarn account\` again to verify the funds show in your Sepolia balance.
253-
254-
Once you have confirmed your balance on Sepolia you can run this command to deploy your contract.
255-
\`\`\`bash
256-
yarn deploy:verify --network sepolia
257-
\`\`\`
258-
This command will deploy your contract and verify it with Sepolia Etherscan.
259-
Copy your deployed contract address from your console and paste it in at [sepolia.etherscan.io](https://sepolia.etherscan.io). You should see a green checkmark on the "Contract" tab showing that the source code has been verified.
26056

261-
Now you can return to the ETH Tech Tree CLI, navigate to this challenge in the tree and submit your deployed contract address. Congratulations!`
262-
}

src/index.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class TechTree {
5353
await this.navigate();
5454
}
5555

56-
async navigate(node?: TreeNode, selection?: string): Promise<void> {
56+
async navigate(node?: TreeNode, selection?: string, heightOffset = 3): Promise<void> {
5757
if (!node) {
5858
this.globalTree = this.buildTree();
5959
node = Object.assign({}, this.globalTree);
@@ -68,7 +68,7 @@ export class TechTree {
6868
choices,
6969
loop: false,
7070
default: selection,
71-
pageSize: this.getMaxViewHeight() - 3,
71+
pageSize: this.getMaxViewHeight() - heightOffset,
7272
theme: {
7373
helpMode: "always" as "never" | "always" | "auto" | undefined,
7474
prefix: ""
@@ -400,7 +400,7 @@ Open up the challenge in your favorite code editor and follow the instructions i
400400
const width = process.stdout.columns;
401401
const userInfo = `${chalk.green(user)} ${chalk.yellow(`(${points} points)`)}`;
402402
const topMenuText = chalk.bold(`${borderLeft}${currentViewName}${new Array(width - (stripAnsi(currentViewName).length + stripAnsi(userInfo).length + 4)).fill(border).join('')}${userInfo}${borderRight}`);
403-
const bottomMenuText = chalk.bold(`${borderLeft}${chalk.bgBlue(`<q>`)} to quit | ${chalk.bgBlue(`<Esc>`)} to go back | ${chalk.bgBlue(`<p>`)} view progress | ${chalk.bgBlue(`<l>`)} leaderboard${new Array(width - 72).fill(border).join('')}${borderRight}`);
403+
const bottomMenuText = chalk.bold(`${borderLeft}${chalk.bgBlueBright(`<q>`)} to quit | ${chalk.bgBlueBright(`<Esc>`)} to go back | ${chalk.bgBlueBright(`<p>`)} view progress | ${chalk.bgBlueBright(`<l>`)} leaderboard${new Array(width - 72).fill(border).join('')}${borderRight}`);
404404

405405
// Save cursor position
406406
process.stdout.write('\x1B7');
@@ -446,13 +446,13 @@ Open up the challenge in your favorite code editor and follow the instructions i
446446
async printProgress(): Promise<void> {
447447
const progressView = new ProgressView(this.userState, this.challenges);
448448
const progressTree = progressView.buildProgressTree();
449-
await this.navigate(progressTree);
449+
await this.navigate(progressTree, undefined, 6);
450450
}
451451

452452
async printLeaderboard(): Promise<void> {
453453
const leaderboardData = await fetchLeaderboard();
454454
const leaderboardView = new LeaderboardView(leaderboardData, this.userState.address);
455455
const leaderboardTree = leaderboardView.buildLeaderboardTree();
456-
await this.navigate(leaderboardTree);
456+
await this.navigate(leaderboardTree, undefined, 6);
457457
}
458458
}

0 commit comments

Comments
 (0)