✨ Highlights

Diese Version ist ein Entwickler-Erfahrungs-Durchlauf für die CLI nach der Härtung in 0.8.8 und der Plugin-Loader-Arbeit in 0.8.9. Die für Anwender sichtbarste Änderung: Fehlermeldungen bei Workspace-Konfigurationen verweisen jetzt mit einem funktionierenden Beispiel auf das eigentliche Problem, statt nur einen einzelnen nackten Satz auszugeben. Mehrere seit Langem offene CLI-Lücken werden ebenfalls geschlossen: Die Flagge deploy --force wird tatsächlich beachtet (keine stillen Überschreibungen mehr), migrate --dry-run ist jetzt eine echte Option, migrate --upgrade deckt die vollständige Legacy-Schlüssel-Map ab, der Dev-Server beendet sich bei docmd stop sauber, und der Plugin-Installer gibt bei einem No-Op-Install keine Erfolgsmeldung mehr aus.

Keine Änderungen an der öffentlichen API. Keine brechenden Konfigurationsänderungen. Reine Entwickler-Erfahrung.

🧪 migrate --dry-run für jeden Migrationspfad

docmd migrate --dry-run ist jetzt eine echte Flagge für jede unterstützte Quelle (Docusaurus, MkDocs, VitePress, Starlight) sowie für den In-Place-Pfad --upgrade. Dry-Run gibt aus, was sich ändern würde, und beendet sich mit Exit-Code 0, ohne etwas zu schreiben:

┌─ Dry run: MkDocs migration
│  Would move      3 files → mkdocs-backup/
│                  docs
│                  index.md
│                  mkdocs.yml
│  Would write     docmd.config.js
│  Config          {"title":"My Test Site","src":"docs","out":"dist","theme":{"appearance":"system"}}
└──────────────────────────────────────────────────────────

⬢ No changes made. Re-run without --dry-run to apply.

Für --upgrade gibt der Dry-Run die vollständige aktualisierte Konfiguration aus, sodass Sie sie gegen die bestehende Datei diffen können, bevor Sie committen.

📦 migrate --upgrade deckt die vollständige Legacy-Schlüssel-Map ab

Der Pfad --upgrade behandelte bisher nur sechs Legacy-Schlüssel (projects, siteTitle, siteUrl, baseUrl, srcDir, outputDir, defaultLocale). Jetzt behandelt er zusätzlich:

Legacy-Schlüssel Wird zu
source src
outDir out
nav navigation
top-level search (boolean) plugins.search
top-level sidebar layout.sidebar
theme.defaultMode theme.appearance
theme.enableModeToggle optionsMenu.components.themeSwitch
theme.positionMode optionsMenu.position

Jedes Upgrade emittiert einen [ DONE ] Upgraded "X" to "Y"-Schritt, und eine Konfiguration ohne jegliche Legacy-Schlüssel gibt weiterhin Configuration is already up to date with the latest schema. aus.

🩺 Bessere Workspace-Fehlermeldungen

Wenn einer Workspace-Konfiguration das Wurzelprojekt fehlt (der häufigste Erstkonfigurations-Fehler), lautete die Fehlermeldung bisher nur ein einzelner nackter Satz: "Workspace configuration must have a root project with prefix "/"". Das gab dem Anwender weder ein Beispiel noch einen nächsten Schritt an die Hand.

Die Fehlermeldung lautet jetzt:

Workspace configuration must include a root project with prefix "/".
Each workspace needs one project that owns the site root, with the others mounted under their own prefixes.
Example:
  {
    "workspace": {
      "projects": [
        { "name": "main", "src": "./docs",     "prefix": "/" },
        { "name": "api",  "src": "./api-docs", "prefix": "/api/" }
      ]
    }
  }
See https://docs.docmd.io/guides/workspace for the full layout guide.

Dieselbe Form gilt für jeden Workspace-Validierungsfehler: Jeder endet mit der konkreten Konfiguration, die den Validator zufriedenstellen würde.

📦 deploy --force meint jetzt, was es sagt

Die Flagge --force für docmd deploy wurde bisher akzeptiert, vom Deployer aber nie gelesen. Der Aufruf von docmd deploy --docker in einem Projekt, das bereits eine Dockerfile enthielt, überschrieb diese stillschweigend ohne Warnung.

Das neue Verhalten:

  • Ohne --force wird eine bestehende Konfigurationsdatei erhalten, und die TUI-Zeile zeigt [ SKIP ] Dockerfile (already exists, skipped — use --force to overwrite).
  • Mit --force wird die bestehende Datei wie bisher überschrieben.

Dies gilt für jedes Deploy-Ziel: --docker, --nginx, --caddy, --github-pages, --vercel und --netlify.

🛰️ Netlify-Template macht keinen Soft-404 mehr auf jeder Route

Die generierte netlify.toml enthielt bisher einen [[redirects]]-Block, der jede URL mit HTTP 200 an /index.html weiterleitete:

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Das bedeutet, dass jede fehlende Route die Startseite mit Status 200 zurückgab — ein Soft-404, der Fehler versteckt, SEO schadet und eine URL-Enumeration-Angriffsfläche eröffnet. Der Block wurde entfernt: docmd generiert für jede Route eine echte HTML-Datei, sodass Netlify die richtige Datei ausliefert, wenn sie existiert, und ansonsten auf das gebündelte 404.html (mit 404-Status) zurückfällt.

Die Flagge docmd build --offline ließ im gerenderten HTML bisher absolute Pfade wie <a href="/destination/"> für Markdown-Links stehen. Diese funktionieren auf einem HTTP-Server, brechen aber, wenn die Anwender site/index.html aus dem Dateisystem öffnen — file:// kann absolute Pfade ohne Host nicht auflösen.

Der Fix verarbeitet das gerenderte Markdown-HTML im Offline-Modus nach und schreibt jedes interne <a href> und <img src> auf einen relativen .html-Pfad um, wobei die gleiche Logik wie beim Button-Container (:::) verwendet wird. Das Ergebnis:

Quelle build (HTTP) build --offline (file://)
Button-Container ::: "/destination" ./destination/ ./destination/index.html
Markdown-Link [text](/destination) /destination/ ./destination/index.html
Markdown-Bild ![alt](/assets/x.png) /assets/x.png ./assets/x.png
Externes https://example.com unverändert unverändert
Anker #section unverändert unverändert

Nicht-Offline-Builds bleiben vollständig unverändert (saubere URLs für SEO bleiben erhalten). Die Ausgabe funktioniert in jeder Hosting-Variante: file://, HTTP-Server, eigene Domains, Dev-Server, Live-Editor. Verschachtelte Seiten erhalten das korrekte ../-Präfix, um aus Unterordnern nach oben zu navigieren.

🔌 docmd-search Peer-Abhängigkeiten werden gemeinsam installiert

Wenn plugins.search.semantic: true gesetzt ist und entweder docmd-search selbst oder eine seiner Peers (@huggingface/transformers, onnxruntime-node) fehlt, tut das Plugin jetzt Folgendes:

  1. Es schränkt seinen Modul-Auflösungs-Scope auf das Projekt des Anwenders ein (nur process.cwd()/node_modules) — zuvor konnte es über __dirname bis zum Monorepo-Root aufwärts wandern und dort eine Kopie finden, was zu verwirrendem Verhalten führte.
  2. Es installiert nur die Peers automatisch nach, wenn nur diese fehlen (zuvor: warnen und aufgeben).
  3. Es gibt den tatsächlichen Grund in der Fallback-Meldung aus — zuvor lautete die Meldung immer “docmd-search not installed”, selbst wenn nur ein Peer fehlte.

🛰️ Open Knowledge Format ist standardmäßig aktiv

OKF wurde in 0.8.8 ausgeliefert und ist nun vollständig standardmäßig aktiv:

  • Wird bei jedem Build automatisch geladen, sofern nicht explizit über plugins.okf: false deaktiviert.
  • plugins.okf.typeField ist standardmäßig type (Frontmatter > Pfad > Fallback).
  • Fehlende type-Felder erzeugen EINE zusammengefasste TUI-Zeile plus einen Eintrag pro Seite in okf/_meta/lint-report.txt, damit keine Details verloren gehen.
  • Verwaiste Konzepte und defekte interne Links werden im selben Lint-Bericht aufgeführt.
  • Der Graph-Viewer ist Opt-in über plugins.okf.graph: true (eine graph/index.html, graph.js und graph.css werden unter okf/graph/ emittiert).
  • plugins.okf.i18n: true emittiert OKF-Bundles für jede konfigurierte Locale (andernfalls nur für die Standard-Locale).

Keine Änderungen an der öffentlichen API.

🛑 Sauberer Shutdown bei docmd stop

docmd stop schickte bisher SIGTERM und, falls das fehlschlug, sofort SIGKILL. Der SIGTERM-Handler des Dev-Servers war ein einzeiliger process.exit(0), der Watcher, HTTP-Server, WebSocket-Server und Worker-Pool-Cleanup übersprang. Nach einem Stop konnten Kindprozesse und offene Sockets hängen bleiben.

Der neue Ablauf:

  1. docmd stop sendet SIGTERM.
  2. Der Dev-Server führt denselben sauberen Shutdown aus wie bei Ctrl+C (Watcher schließen → wss schließen → http-Server schließen → Worker-Pool beenden → Exit 0).
  3. docmd stop prüft alle 100 ms bis zu 5 Sekunden lang, ob der Prozess von selbst beendet wird.
  4. Nur wenn der Prozess nach der Gnadenfrist noch lebt, eskaliert docmd stop zu SIGKILL.

Die TUI-Zeile zeigt Stopped ... PID gracefully bei sauberem Exit oder Killed ... PID (SIGKILL after grace), falls die Eskalation nötig war.

🐛 Fehlerbehebungen

  • Workspace-Validator: umsetzbare Fehlermeldung bei fehlendem Wurzelprojekt. Der einzelne Satz wird durch eine mehrzeilige Meldung ersetzt, die die Anforderung erklärt, ein gültiges JSON-Beispiel zeigt und auf den Workspace-Layout-Leitfaden verlinkt.
  • Plugin-Installer: korrekte Abschlussmeldung bei bereits installierten Plugins. docmd add <plugin> für ein bereits konfiguriertes Plugin gab bisher “Plugin successfully installed and activated” aus, obwohl keine Installation stattfand. Die TUI-Abschlusszeile spiegelt jetzt das tatsächliche Ergebnis wider: Neuinstallation zeigt die Erfolgsmeldung; bereits konfiguriert druckt ⬢ Plugin was already configured. Nothing changed.
  • Deployer: --force überschreibt bestehende Konfigurationen nicht mehr stillschweigend. Jedes Deploy-Ziel beachtet jetzt --force: Standardverhalten ist das Überspringen bestehender Dateien; --force aktiviert das Überschreiben.
  • Deployer: Netlify-[[redirects]]-Block zur Behebung des Soft-404 entfernt. Die generierte netlify.toml schreibt jede URL nicht mehr mit Status 200 auf /index.html um. Fehlende Routen liefern jetzt das gebündelte 404.html mit 404-Status aus.
  • Offline-Build: Markdown-Links und -Bilder werden jetzt auf relative .html-Pfade umgeschrieben. Zuvor war nur der Button-Container Offline-fähig; Markdown-[text](url) und ![alt](src) gaben absolute Pfade aus, die file:// nicht auflösen konnte. Sie durchlaufen jetzt dieselbe fixHtmlLinks-Umschreibung wie der Button-Container (#167).
  • Dev-Server: SIGTERM führt denselben sauberen Shutdown wie SIGINT aus. Der Dev-Server schließt jetzt Watcher, WebSocket-Server, HTTP-Server und Worker-Pool bei SIGTERM, bevor er beendet wird. docmd stop prüft, ob der Prozess beendet wird, und eskaliert erst nach einer Gnadenfrist von 5 Sekunden zu SIGKILL.
  • docmd-search: Peer-Abhängigkeiten werden bei Fehlen automatisch installiert. Wenn plugins.search.semantic: true gesetzt ist und die Peer-Pakete (@huggingface/transformers, onnxruntime-node) nicht installiert sind, installiert das Plugin sie, anstatt zu warnen und auf Stichwortsuche zurückzufallen (#163 tieferer Fix).
  • Versionen: config.versions.list wird als Alias für all akzeptiert. Anwender, die list (eine übliche Form für “Liste von Versionen”) verwendeten, erhielten bisher still 0-Seiten-Builds, weil das Schema nur all erkannte. Die Aliasierung listall stellt ihre Konfiguration wieder her, ohne den kanonischen Schlüssel zu ändern (M-6).
  • Versionen: Fehlendes Verzeichnis der aktuellen Version ist ein harter Fehler. Der Build bricht jetzt mit einem klaren Verweis auf den fehlenden Pfad und die Versions-ID ab. Zuvor erzeugte er still eine 0-Seiten-Site ohne umsetzbare Meldung. Alte (nicht aktuelle) Versionen bleiben Soft-Warnungen (T-Z6).
  • llms.txt / llms.json-Titel werden bereinigt. Frontmatter-Titel, die Markdown-Injection-Zeichen (`, [, ], Zeilenumbrüche) enthalten, brechen die Linkform nicht mehr und werden nicht mehr als rohes HTML gerendert; Titel, die mit =, +, - oder @ beginnen, erhalten ein vorangestelltes einfaches Anführungszeichen, damit das Öffnen in einer Tabellenkalkulation keine Formel ausführt. CSV-Formel-Neutralisierung (T-Z11) und Markdown-Injection-Verhinderung (T-Z10) in einem Helfer.
  • NO_COLOR und DOCMD_NO_BANNER unterdrücken das Build-Banner. Das Setzen von NO_COLOR=1 (oder des docmd-spezifischen DOCMD_NO_BANNER=1) blendet jetzt das ASCII-Art und die Versionszeile am Anfang der Build-Ausgabe aus. NO_COLOR ist der De-facto-Standard für CLIs; DOCMD_NO_BANNER ist eine docmd-spezifische Hintertür für Anwender, die Farbe, aber kein Banner möchten (N-13 + N-16).
  • Migrations-Feinschliff. moveFilesToBackup behält nun Lockfiles und package.json an Ort und Stelle, sodass eine Wiederherstellung nicht jede Abhängigkeit neu auflösen muss (N-10). Die Docusaurus- und MkDocs-Migratoren erhalten das ursprüngliche staticDir / site_dir (N-22), und MkDocs-nav:-Blöcke werden in docmds navigation-Format übersetzt, wobei mehrstufige Abschnitte über children erhalten bleiben (N-9).

Changelog

  1. Workspace-Validator: validateProjects in packages/core/src/engine/workspace.ts löst jetzt eine mehrzeilige Error-Ausnahme mit einem JSON-Beispiel und einem Docs-Link aus, wenn kein Projekt prefix: "/" hat (M-2).
  2. Plugin-Installer: addPlugin in packages/plugins/installer/src/index.ts verzweigt nach dem injected-Flag, das vom Config-Editor zurückgegeben wird, und emittiert TUI.info für den bereits konfigurierten Fall, anstatt der irreführenden TUI.success (M-14).
  3. Deployer: write() in packages/deployer/src/index.ts nimmt jetzt ein force-Argument und überspringt mit einer [ SKIP ]-TUI-Zeile, wenn die Zieldatei existiert und --force nicht übergeben wurde (N-2). Alle sechs Zielgeneratoren (docker, nginx, caddy, github-pages, vercel, netlify) reichen opts.force durch.
  4. Deployer (Netlify): generateNetlify in packages/deployer/src/providers/netlify.ts emittiert keinen [[redirects]] from = "/*" status = 200-Block mehr. docmd generiert echtes HTML pro Route, sodass der Catch-all sowohl überflüssig als auch die Quelle des Soft-404 war (T-Z9).
  5. Stop: stopServer in packages/core/src/commands/stop.ts exportiert einen neuen waitForExit(pid, timeoutMs)-Helfer und verwendet ihn, um nach SIGTERM zu prüfen, ob der Zielprozess beendet wird. Die Eskalation zu SIGKILL erfolgt erst nach Ablauf der 5-Sekunden-Gnadenfrist (M-11).
  6. Dev-Server: startDevServer in packages/core/src/commands/dev.ts extrahiert den SIGINT-Shutdown-Pfad in eine gracefulShutdown()-Funktion, die sowohl von den SIGINT- als auch von den SIGTERM-Handlern verwendet wird (M-11).
  7. Migrate: migrateProject in packages/core/src/commands/migrate.ts akzeptiert eine neue dryRun-Option (N-3). Für jeden Quellpfad (Docusaurus, MkDocs, VitePress, Starlight) druckt der Dry-Run die Datei-Verschiebeliste und die neue docmd.config.js, bevor irgendwelche Seiteneffekte auftreten; für --upgrade druckt er die aktualisierte Konfiguration. packages/core/src/bin/docmd.ts fügt --dry-run zur Migrate-CLI hinzu.
  8. Migrate: der --upgrade-Pfad in packages/core/src/commands/migrate.ts behandelt jetzt acht zusätzliche Legacy-Schlüssel: source, outDir, nav, top-level search, top-level sidebar, theme.defaultMode, theme.enableModeToggle und theme.positionMode (N-4).
  9. Tests: tests/cli-contracts/validate-workspace.test.js fügt 5 neue M-2-Assertions hinzu, die den Exit-Code und die vier Meldungsinhalt-Invarianten abdecken.
  10. Tests: tests/cli-contracts/plugin-add-remove.test.js fügt 3 neue M-14-Assertions hinzu, die die Meldung “already installed”, das Fehlen der falschen Erfolgsmeldung und den Regressionsschutz für Neuinstallationen abdecken.
  11. Tests: neue tests/cli-contracts/deploy.test.js (5 Assertions, registriert in tests/runner.js als deploy) decken die Pfade N-2 Überspringen / Überschreiben / Neu ab.
  12. Tests: neue tests/cli-contracts/stop.test.js (3 Assertions, registriert als stop) decken den M-11-Helfer waitForExit gegen laufende Kindprozesse ab.
  13. Tests: neue tests/cli-contracts/migrate.test.js (33 Assertions, registriert als migrate) decken N-3 Dry-Run sowohl für Quell-Migrationen als auch für Upgrade ab, sowie N-4-Abdeckung aller 13 Legacy-Upgrade-Pfade.