Skip to content

Instantly share code, notes, and snippets.

@dudo
Last active August 24, 2026 20:37
Show Gist options
  • Select an option

  • Save dudo/203ee3ba65c0dc9851b91068b97a01ef to your computer and use it in GitHub Desktop.

Select an option

Save dudo/203ee3ba65c0dc9851b91068b97a01ef to your computer and use it in GitHub Desktop.
Node terminal bootstrap
import { ThingInventory } from "./domain.ts";
import { runTerminal } from "./terminal.ts";
const thingInventory = new ThingInventory();
await runTerminal(thingInventory);
import assert from "node:assert/strict";
import test from "node:test";
import { ThingInventory } from "./domain.ts";
test("creates thing and trims names", () => {
const inventory = new ThingInventory();
const thing = inventory.add({
name: " Projector ",
});
assert.equal(thing.id, "E001");
assert.equal(thing.name, "Projector");
});
test("things are added to the list in sorted order", () => {
const inventory = new ThingInventory();
inventory.add({ name: "Projector" });
inventory.add({ name: "Camera" });
inventory.add({ name: "Microphone" });
const list = inventory.list();
assert.equal(list.length, 3);
assert.equal(list[0].name, "Camera");
assert.equal(list[1].name, "Microphone");
assert.equal(list[2].name, "Projector");
});
test("rejects empty thing names", () => {
const inventory = new ThingInventory();
assert.throws(
() =>
inventory.add({
name: " ",
}),
/name is required/,
);
});
export type ThingInput = {
name: string;
};
export type Thing = {
id: string;
name: string;
}
export class ThingInventory {
private nextId = 1;
private thingList: Thing[] = [];
add(input: ThingInput): Thing {
const name = input.name.trim();
if (!name) throw new Error("Thing name is required.");
const thing: Thing = {
id: `E${String(this.nextId).padStart(3, "0")}`,
name,
};
this.nextId += 1;
this.thingList.push(thing);
return { ...thing };
}
list(): Thing[] {
return this.thingList
.toSorted((left, right) => left.name.localeCompare(right.name))
.map((thing) => ({ ...thing }));
}
}
{
"name": "node-terminal-bootstrap",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=26"
},
"scripts": {
"start": "node src/app.ts",
"dev": "node --watch src/app.ts",
"check": "node --check src/app.ts && node --check src/terminal.ts && node --check src/domain.ts && node --check src/domain.test.ts",
"test": "node --test",
"test:watch": "node --test --watch"
},
"devDependencies": {
"@types/node": "^26.2.0"
}
}
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";
import {
type Thing,
type ThingInventory,
} from "./domain.ts";
export async function runTerminal(
thingInventory: ThingInventory,
): Promise<void> {
const terminal = createInterface({ input, output });
console.log("Thing inventory management");
console.log("Type 'help' to see the available commands.\n");
try {
while (true) {
const command = (await terminal.question("inventory> "))
.trim()
.toLowerCase();
try {
switch (command) {
case "add": {
const name = await terminal.question("Thing name: ");
const thing = thingInventory.add({ name });
console.log(
`Created ${thing.id} for ${thing.name}.`,
);
break;
}
case "list":
printThing(thingInventory.list());
break;
case "help":
case "?":
printHelp();
break;
case "quit":
case "exit":
console.log("Goodbye.");
return;
case "":
break;
default:
console.log(`Unknown command: ${command}. Type 'help' for options.`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
}
}
} finally {
terminal.close();
}
}
function printThing(thingList: Thing[]): void {
if (thingList.length === 0) {
console.log("No things found.");
return;
}
const rows = thingList.map((thing) => [
thing.id,
thing.name,
]);
const headers = ["ID", "NAME"];
const widths = headers.map((header, column) =>
Math.max(header.length, ...rows.map((row) => row[column].length)),
);
const formatRow = (row: string[]) =>
row.map((cell, column) => cell.padEnd(widths[column])).join(" ");
console.log(formatRow(headers));
console.log(widths.map((width) => "-".repeat(width)).join(" "));
rows.forEach((row) => console.log(formatRow(row)));
}
function printHelp(): void {
console.log(`
Commands:
add Register thing to inventory
list List all things
help Show this help
quit Exit the application
`);
}
{
"compilerOptions": {
"lib": ["es2023"],
"skipLibCheck": true,
"types": ["node"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true
},
"include": ["src/**/*.ts"]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment