01 · Problem
Wofür braucht man das?
Eine CLI braucht einen deterministischen Parser, typisierte Wertkonvertierung, verständliche Fehler, Help-Ausgabe und einen klaren Übergang zum Command-Handler.
02 · Denkmodell
Das mentale Modell
Registry wählt den Command, Parser erzeugt einen validierten Context, Middleware umschließt den Handler und ein Renderer übersetzt Fehler in Exit-Code und Hilfe.
Cli ist leer; mehrere benötigte Interfaces und Basisklassen fehlen, HelloWorld widerspricht den vorgesehenen Typen und es existiert weder Export noch bin-Einstieg. Das Package kompiliert nicht.
03 · Kontrollfluss
Was passiert in welcher Reihenfolge?
- Erstes Token gegen eine Command-Registry auflösen.
- Restliche Tokens quote-aware in Positionals und Flags zerlegen.
- Rohwerte per Command-Schema konvertieren, Defaults setzen und validieren.
- Middleware als Onion-Kette ausführen und zuletzt den Command-Handler aufrufen.
- Fehler in stabile Exit-Codes und gezielte Hilfe übersetzen.
04 · Bauteile
Die entscheidenden Verträge
Cli (leer)
Die vorhandene exportlose Klasse enthält noch keine Implementierung.
CommandSpec (beabsichtigt)
Soll Name, Arguments, Flags, Validatoren und run-Handler verbinden.
HelloWorldCommand (unvollständig)
Skizziert eine Command-Definition, kann mit den fehlenden Basistypen aber nicht bauen.
05 · Build it yourself
Selbst implementieren
Halte Tokenisierung, Schemaauswertung und Ausführung getrennt – so lässt sich jede Schicht ohne Prozessstart testen.
- Definiere Commands als Datenobjekte mit Parsern für jeden Wert.
- Parse argv ohne Seiteneffekte in einen Context oder einen typisierten Fehler.
- Komponiere Middleware und injiziere stdout, stderr und Exit-Verhalten.
type Command = {
name: string;
parse(tokens: string[]): Record<string, unknown>;
run(context: Record<string, unknown>): Promise<void>;
};
async function runCli(argv: string[], commands: Map<string, Command>) {
const [name, ...tokens] = argv;
const command = commands.get(name);
if (!command) throw new Error("Unknown command: " + name);
const context = command.parse(tokens);
await command.run(context);
}06 · Verifizieren
Was du testen solltest
- Pflichtargumente, Defaults, lange und kurze Flags werden deterministisch geparst.
- Unbekannte Commands, Flags und zusätzliche Positionals liefern gezielte Fehler.
- Middleware-Reihenfolge, Async-Fehler und Exit-Code-Mapping werden mit injizierten Streams geprüft.
07 · Grenzen
Kompromisse und Stolperfallen
- Shell-Quoting ist meist bereits vom Betriebssystem verarbeitet; ein eigener Stringparser braucht andere Regeln.
- Automatische Typmagie erschwert Fehlermeldungen – explizite Value-Parser sind besser testbar.
Der aktuelle Repository-Code ist eine Entwurfsskizze, keine nutzbare CLI-Library.
08 · Weiterdenken