Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ Who needs a website when you have a terminal.
- chmod
- chown
- clear
- cowsay
- cp
- curl
- date
- df
- echo
- emacs
Expand Down Expand Up @@ -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!
Expand Down
74 changes: 55 additions & 19 deletions config/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pattern> 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();
Expand Down Expand Up @@ -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");
Expand All @@ -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
Expand Down Expand Up @@ -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/<user> to mimic a real shell.
pwd: function () {
term.stylePrint("/" + term.cwd.replaceAll("~", `home/${term.user}`));
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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(
Expand All @@ -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 ─────────────────────────────────────────────────────────────────
Expand Down
90 changes: 90 additions & 0 deletions tests/commands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Loading