From 03c059a2f595f113937984d8a54fbc83d7d40c33 Mon Sep 17 00:00:00 2001 From: Lee Edwards Date: Wed, 12 Aug 2026 18:52:32 -0700 Subject: [PATCH 1/2] Land three stalled command contributions, and finish the RickRoll move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four PRs sat open for between one and four years. The ideas were good; the patches had rotted against a file that moved underneath them. Reapplied here with credit. - cowsay (#106) — prints the classic bubble and cow. Uses writeln rather than stylePrint because it is alignment-sensitive art that stylePrint would wrap and recolor. - date and wget (#72) — date prints the real thing; wget points at curl, which already has the joke response. - The RickRoll (#84, #85). Both PRs wanted the joke to play in the terminal instead of punting the user to an imgur GIF in a new tab. That landed for `test` at some point, but `cat id_rsa`, `grep id_rsa` and `open test.htm` were left behind still opening the GIF. All four now go through one _rickRoll() helper, so they cannot drift apart again. Verified in a browser against a built dist/: cowsay renders correctly, date and wget print as expected, and all four RickRoll routes fire the in-terminal animation with zero external URLs opened. Co-Authored-By: mantra-1 Co-Authored-By: nkulkarni Co-Authored-By: ophiuchus44 Co-Authored-By: jandobrx Co-Authored-By: Claude Opus 5 --- README.md | 3 ++ config/commands.js | 74 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 416fe5b5..ef517896 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,10 @@ Who needs a website when you have a terminal. - chmod - chown - clear + - cowsay - cp - curl + - date - df - echo - emacs @@ -71,6 +73,7 @@ Who needs a website when you have a terminal. - uname - vi - vim + - wget - zsh Missing a favorite one? Make a PR! diff --git a/config/commands.js b/config/commands.js index 847e4d93..92cc1667 100644 --- a/config/commands.js +++ b/config/commands.js @@ -23,6 +23,25 @@ let killed = false; // Emits a series of styled strings that the RickRoll animation in rickroll.js // interprets as frame pointers. Triggered by `cat id_rsa` or `test`. +// Plays the RickRoll inside the terminal. Every route into the joke goes +// through here — `test`, `cat id_rsa`, `grep id_rsa`, `open test.htm` +// — so none of them can drift back to punting the user out to an external GIF. +function _rickRoll() { + SpawnRickRollPointers(); + if (typeof window.ensureRickRollLoaded !== "function") { + return; + } + window.ensureRickRollLoaded().then(() => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + if (typeof RickRoll === "function") { + RickRoll(); + } + }); + }); + }); +} + function SpawnRickRollPointers() { function padNumber(num, length) { let str = num.toString(); @@ -152,10 +171,6 @@ const commands = { term.displayURL("https://github.com/rootvc"); }, - swag: function () { - term.openURL("https://rootvc.creator-spring.com"); - }, - twitter: function () { term.displayURL("https://twitter.com/rootvc"); term.displayURL("https://twitter.com/machinepix"); @@ -169,6 +184,8 @@ const commands = { term.openURL(`mailto:${firm.email}`); }, + // Points at the static, crawlable mirror of this terminal generated by + // scripts/build-pages.js. Same data, plain HTML, real URLs. www: function () { // There used to be a static mirror at /about/, /portfolio/ and friends. // Those URLs now redirect back into this terminal, so pointing at them @@ -198,6 +215,24 @@ const commands = { term.stylePrint(`(Robot voice): ${message}`); }, + // writeln rather than stylePrint: the cow is alignment-sensitive art, and + // stylePrint would wrap and re-colorize it. + cowsay: function (args) { + const message = args.join(" ") || "moo"; + const bar = " " + "_".repeat(message.length + 2); + const lines = [ + bar, + `< ${message} >`, + " " + "-".repeat(message.length + 2), + " \\ ^__^", + " \\ (oo)\\_______", + " (__)\\ )\\/\\", + " ||----w |", + " || ||", + ]; + lines.forEach((line) => term.writeln(line)); + }, + // Expands ~ to /home/ to mimic a real shell. pwd: function () { term.stylePrint("/" + term.cwd.replaceAll("~", `home/${term.user}`)); @@ -207,6 +242,10 @@ const commands = { term.stylePrint(_filesHere().join(" ")); }, + date: function () { + term.stylePrint(new Date().toString()); + }, + // Simulates a minimal Unix directory tree: // / // ├── bin/ (contains zsh) @@ -332,13 +371,13 @@ const commands = { } }, - // Grepping id_rsa redirects to an appropriate reaction GIF instead. + // Grepping id_rsa gets you an appropriate reaction instead. grep: function (args) { const q = args[0]; const filename = args[1]; if (filename == "id_rsa") { - term.openURL("https://i.imgur.com/Q2Unw.gif"); + _rickRoll(); } if (!q || !filename) { @@ -367,7 +406,7 @@ const commands = { args[0].split(".")[0] == "test" && args[0].split(".")[1] == "htm" ) { - term.openURL("https://i.imgur.com/Q2Unw.gif"); + _rickRoll(); } else if (args[0].split(".")[1] == "htm") { term.openURL(`./${args[0]}`, false); } else if (args.join(" ") == "the pod bay doors") { @@ -619,6 +658,14 @@ const commands = { ); }, + wget: function (args) { + if (!args[0]) { + term.stylePrint("wget: missing URL"); + return; + } + term.stylePrint(`%wget% not installed. Try: %curl% ${args[0]}`); + }, + // scp gets a more redacted treatment than curl. scp: function (args) { term.stylePrint( @@ -638,18 +685,7 @@ const commands = { }, test: function () { - SpawnRickRollPointers(); - if (typeof window.ensureRickRollLoaded === "function") { - window.ensureRickRollLoaded().then(() => { - window.requestAnimationFrame(() => { - window.requestAnimationFrame(() => { - if (typeof RickRoll === "function") { - RickRoll(); - } - }); - }); - }); - } + _rickRoll(); }, // ── Upgrade ───────────────────────────────────────────────────────────────── From 4b3e714ff7fc647b2785fa2bc820fde34af9a900 Mon Sep 17 00:00:00 2001 From: Lee Edwards Date: Wed, 12 Aug 2026 18:52:32 -0700 Subject: [PATCH 2/2] Add cd test coverage, ported from #51 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cd is the one command with real branching — a switch over ~, .., /home, /bin and team member names, several branches of which recurse through term.command — and it drives term.cwd, which the prompt renders on every keystroke. It had no tests. The table comes from #51 (@astonm, November 2021), which never landed. Its harness is long gone, but every case it asserted still describes the intended behaviour, so the table is ported onto the current vitest setup and extended with the permission-denied and unknown-directory paths. Co-Authored-By: astonm Co-Authored-By: Claude Opus 5 --- tests/commands.test.js | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/commands.test.js b/tests/commands.test.js index c7ce7e1b..b0003c88 100644 --- a/tests/commands.test.js +++ b/tests/commands.test.js @@ -110,3 +110,93 @@ describe("apply", () => { } }); }); + +// Loads the full command set with a fake terminal. `cd` is the one command with +// real branching logic — a switch over ~, .., /home, /bin and team member names +// — and it drives term.cwd, which the prompt renders on every keystroke. +function loadCommands({ cwd = "~", user = "guest", team = { avidan: {} } } = {}) { + const term = { + cwd, + user, + stylePrint: vi.fn(), + writeln: vi.fn(), + printArt: vi.fn(), + openURL: vi.fn(), + displayURL: vi.fn(), + cols: 100, + }; + const context = vm.createContext({ + term, + jobs: productionJobs, + firm: { blurb: "", email: "hello@example.com" }, + team, + help: {}, + portfolio: {}, + colorText: (text) => text, + window: {}, + }); + vm.runInContext(commandSource, context); + const commands = vm.runInContext("commands", context); + // cd recurses through term.command for the paths that resolve via another cd. + term.command = (line) => { + const [name, ...args] = line.split(" "); + return commands[name](args); + }; + return { commands, term }; +} + +describe("cd", () => { + // Table ported from #51 (@astonm, 2021), which never landed. The cases still + // describe the intended behaviour; only the harness has changed. + it.each([ + ["anywhere", "/", "/"], + ["anywhere", "~", "~"], + ["anywhere", "~/", "~"], + ["~", "..", "home"], + ["~", "../", "home"], + ["anywhere", "../../", "/"], + ["anywhere", "../..", "/"], + ["anywhere", "../../../", "/"], + ["anywhere", "../../../../", "/"], + ["/", "home", "home"], + ["anywhere", "/home", "home"], + ["/", "bin", "bin"], + ["anywhere", ".", "anywhere"], + ["anywhere", "./", "anywhere"], + ["anywhere", "", "~"], + ["anywhere", "/bin", "bin"], + ])("from %s, cd %s -> %s", (cwd, arg, expected) => { + const { commands, term } = loadCommands({ cwd }); + commands.cd(arg === "" ? [] : [arg]); + expect(term.cwd).toBe(expected); + }); + + it("refuses a team member's home directory without moving", () => { + const { commands, term } = loadCommands({ cwd: "home" }); + commands.cd(["avidan"]); + expect(term.cwd).toBe("home"); + expect(term.stylePrint).toHaveBeenCalledWith( + "You do not have permission to access this directory" + ); + }); + + it("lets a user into their own home but not someone else's", () => { + const mine = loadCommands({ cwd: "home", user: "guest" }); + mine.commands.cd(["guest"]); + expect(mine.term.cwd).toBe("~"); + + const theirs = loadCommands({ cwd: "home", user: "guest" }); + theirs.commands.cd(["root"]); + expect(theirs.term.cwd).toBe("home"); + expect(theirs.term.stylePrint).toHaveBeenCalledWith( + "You do not have permission to access this directory" + ); + }); + + it("reports unknown directories without moving", () => { + const { commands, term } = loadCommands({ cwd: "~" }); + commands.cd(["nope"]); + expect(term.cwd).toBe("~"); + expect(term.stylePrint).toHaveBeenCalledWith("No such directory: nope"); + }); +});