Inspiration
Everyone bounces off the terminal the same way, blinking cursor, zero feedback, type one wrong thing, get a wall of red text, close it. I wanted to build the thing I wish I had when I was learning: a terminal that's actually safe to break.
What it does
Terminal Zero is a fake linux terminal in your browser with 7 levels that teach real commands. Each level gives you one objective, find a hidden file, redirect text into a file, grep for a word, and you solve it by actually typing commands. Get it right and you get a flag and a short explanation. Get stuck and you can reveal hints one at a time, the last one just gives you the answer, or skip the level entirely. The sidebar mirrors your real filesystem state live. Hit + for a sandbox tab with no objectives.
How I built it
The filesystem is just a nested JS object, { type: "dir", children: {...} } or { type: "file", content }. Every command clones it, mutates the clone, hands back new state. I hand wrote parsers for ls, cd, cat, mkdir, touch, mv, grep, echo with >/>>, and a few more, no shell library, just tokenizing and a switch statement. You can even chain commands with && like a real shell. Each level is a config with a description, 3 hints, and a validate() function that checks what you ran against the resulting filesystem. Progress saves to localStorage.
Challenges I ran into
The worst bug wasn't the filesystem, it was React. I had level completion scheduling a setTimeout inside a functional setState updater. React Strict Mode calls those updaters twice in dev to catch impure code, mine wasn't pure, so two timers fired and the second one silently skipped a level. Took forever to find because it looked like a race condition, not what it actually was. Fix was pulling every side effect out of setState and running it once in the event handler.
Second one was pacing hints without just handing people the answer. Ended up writing mistake detection that reads the actual error, wrong file name vs. wrong command vs. right command but not done yet, and gives a different nudge for each.
What I learned
How much invisible work real shells do. Glob matching, relative paths with .., redirection, all trivial to use and genuinely annoying to implement from scratch. Also learned the hard way that "the function you pass to setState must be pure" isn't a style suggestion, it's load bearing.
Built With
- framer-motion
- lucide-react
- nextjs
- react
- tailwindcss
- typescript
Log in or sign up for Devpost to join the conversation.