@zero2vibecode/terminal-sim
v0.1.5
Published
A deterministic terminal simulator with verifiable learning goals. No shell, no network, no filesystem — safe to put in front of a beginner.
Maintainers
Readme
@zero2vibecode/terminal-sim
A deterministic terminal simulator with verifiable goals. No shell, no network, no filesystem — which is exactly why it is safe to put in front of a beginner, and why an exercise built on it can be proven solvable in CI.
Type in it right now → — that page is this
package with a UI on top. mkdir a directory and ls will list it; pipe something into
grep; break it and read the error. Nothing is being faked for the demo.
npm i @zero2vibecode/terminal-sim
Recorded from that page, not staged — the same commands work in the box.
import { TerminalSession } from "@zero2vibecode/terminal-sim";
const session = new TerminalSession({
files: { "notes/todo.md": "buy milk" },
goals: [{ type: "fileExists", path: "notes/done.md" }],
});
session.run("ls notes"); // → "todo.md"
session.goalsMet; // → false
session.run("touch notes/done.md");
session.goalsMet; // → trueWhy this exists
Teaching the command line has an awkward gap. A real shell is honest but dangerous, and you cannot ship one in a browser. A screenshot is safe but dead. A fake that just prints canned responses teaches the learner to type the expected thing rather than to think.
This is the third option: a real interpreter over an in-memory filesystem. mkdir makes
a directory that ls then lists and rm then removes. Pipes, redirects, $VAR, aliases
and .sh execution work. git, npm and node are simulated well enough to teach the
shape of using them. What it cannot do is touch anything outside itself.
The goals engine is the part you cannot get from an emulator. An exercise declares what "done" means — a directory exists, a file contains something, a command was run, the output said a word — and the session answers whether the learner got there. That turns a toy terminal into a checkable exercise.
The solvability contract
If an exercise ships with a solution, the solution should be proven to satisfy the exercise's own goals. Not reviewed — executed:
import { verifySolution } from "@zero2vibecode/terminal-sim";
const { solved } = verifySolution(
["mkdir notes", "cd notes", "touch todo.md"], // the author's solution
[ // the exercise's goals
{ type: "dirExists", path: "notes" },
{ type: "fileExists", path: "notes/todo.md" },
],
);Run that over your whole course in CI and an unsolvable exercise fails the build instead of a learner. This is the discipline the package was extracted from: every interactive lesson on zero2vibecode.com is proven completable this way on every commit.
Goals
| Goal | Met when |
|---|---|
| { type: "dirExists", path } | a directory exists at path |
| { type: "fileExists", path } | a file exists at path |
| { type: "fileContains", path, text } | that file's contents include text |
| { type: "cwdIs", path } | the session's working directory is path |
| { type: "commandRun", matches } | any command entered matches this regex |
| { type: "outputContains", text } | any output printed includes text |
Paths resolve the way the learner's own would: relative to the home directory the session
starts in (/home/user), unless they begin with / or ~. A malformed commandRun
pattern compiles to a never-match rather than throwing — a typo in content must not crash
a lesson.
API
new TerminalSession({ files?, goals? })
| Member | What it gives you |
|---|---|
| run(line) | executes one line, returns { input, output, failed, lines } |
| runAll(lines) | executes several in order |
| goalsMet | true when every goal is satisfied |
| goalStatus | per-goal verdicts, in declaration order — render a checklist |
| transcript | every line executed so far |
| history | just the commands, as the up-arrow would walk them |
| files / cwd | the current filesystem and working directory |
| toLog() | the session as plain text ($ command then its output) |
verifySolution(solution, goals, files?) → { solved, session }
Output lines carry a kind (input / output / error / system) on line.lines, so
a UI can colour them; line.output is the same thing flattened to text.
Beyond the facade
The engine's own modules are exported as subpaths — @zero2vibecode/terminal-sim/vfs,
/commands, /goals, /interpreter, /complete, /suggest, /history, /prompt,
/env, /log, /types — for building your own UI on top: an immutable filesystem, tab
completion, "did you mean" suggestions, prompt rendering.
The split is deliberate. The main entry point is a contract and follows semver strictly. The subpaths are the workshop: useful, documented, and allowed to change in a minor release. If you build on them, pin a minor version.
Determinism
Same input, same output, every time. No Date.now(), no Math.random(), no I/O in the
engine. That is a teaching requirement, not an aesthetic one: a lesson that behaves
differently on the second run cannot be checked, and a hydration mismatch is a bug the
learner sees.
Not a real shell
Not POSIX-complete, not an xterm.js replacement, not a sandboxed VM. It is a teaching instrument. If you need to run actual programs, you need a container, not this.
Feedback
Bugs, missing commands and API friction: [email protected].
This package's source is not public today — it lives in the site's repository, which is private for now. That is a decision about timing rather than a policy, so it may change; until it does there is no issue tracker and no pull requests here. The trade is that the maintainer reads that inbox himself and a reproducible report gets a release. If a command you need is missing, say which one and what you expected it to print.
The project does keep a public repository — agents-compared, a comparison of AI coding agents written to the same discipline as the course.
License
MIT. Built for zero2vibecode.com, useful anywhere.
