Zum Inhalt springen

Entwickler-Workflows mit eigenen CLI-Tools automatisieren

Wie du wiederkehrende Aufgaben erkennst und eigene CLI-Tools dafür baust – Aufgabenanalyse, Script-Designmuster und der Zinseszinseffekt.

6 Min. Lesezeit
Terminalfenster mit eigenen Automatisierungs-Skripten für Entwickler in Aktion

Die Drei-Stunden-Regel für Automatisierung

Wenn du etwas mehr als dreimal manuell machst, automatisiere es. Wenn der manuelle Prozess länger als fünf Minuten dauert und du ihn wöchentlich ausführst, amortisiert sich die Automatisierung innerhalb eines Monats. Das sind keine präzisen Schwellenwerte – es sind Heuristiken, um die Trägheit zu überwinden, die Entwickler dazu bringt, repetitive Aufgaben von Hand zu erledigen.

Die Ironie der Softwareentwicklung ist, dass wir Automatisierung für alle anderen bauen, während wir selbst Umgebungsvariablen manuell zwischen Terminals kopieren, Datenbank-Seed-Befehle von Hand zusammenbasteln und diese Woche schon zum fünfzehnten Mal dieselbe Git-Beschwörungsformel tippen.

Diese Anleitung zeigt, wie man Automatisierungsmöglichkeiten erkennt, CLI-Tools entwirft, die sich durchsetzen, und ein persönliches Automatisierungs-Toolkit aufbaut, das mit der Zeit Zinseszins bringt.

Automatisierungskandidaten identifizieren

Nicht jede repetitive Aufgabe lohnt eine Automatisierung. Der Sweet Spot sind Aufgaben, die häufig vorkommen, fehleranfällig sind und klar definierte Schritte haben.

tstypescript
interface AutomationCandidate {
  task: string;
  frequencyPerWeek: number;
  minutesPerExecution: number;
  errorRate: string;
  automationEffort: string;
  weeklyTimeSaved: number;
  paybackWeeks: number;
}
 
function evaluateCandidates(
  candidates: AutomationCandidate[]
): AutomationCandidate[] {
  return candidates
    .map((c) => {
      const automationHours =
        c.automationEffort === "low"
          ? 1
          : c.automationEffort === "medium"
            ? 4
            : 12;
      const weeklySavedHours =
        (c.frequencyPerWeek * c.minutesPerExecution) / 60;
      const payback = automationHours / weeklySavedHours;
 
      return {
        ...c,
        weeklyTimeSaved: weeklySavedHours * 60,
        paybackWeeks: Math.ceil(payback),
      };
    })
    .sort((a, b) => a.paybackWeeks - b.paybackWeeks);
}
 
const candidates: AutomationCandidate[] = [
  {
    task: "Set up new feature branch with ticket reference",
    frequencyPerWeek: 8,
    minutesPerExecution: 3,
    errorRate: "low",
    automationEffort: "low",
    weeklyTimeSaved: 0,
    paybackWeeks: 0,
  },
  {
    task: "Seed local database with test data",
    frequencyPerWeek: 5,
    minutesPerExecution: 8,
    errorRate: "medium",
    automationEffort: "medium",
    weeklyTimeSaved: 0,
    paybackWeeks: 0,
  },
  {
    task: "Generate API client from OpenAPI spec",
    frequencyPerWeek: 2,
    minutesPerExecution: 15,
    errorRate: "high",
    automationEffort: "medium",
    weeklyTimeSaved: 0,
    paybackWeeks: 0,
  },
];

Die Amortisationsrechnung ist bewusst einfach gehalten. Zu viel ROI-Analyse bei der Automatisierung ist selbst eine Form von Prokrastination. Wenn die Amortisation unter vier Wochen liegt und die Aufgabe dich nervt, bau das Tool.

Dein erstes Automatisierungs-Skript bauen

Fang mit Shell-Skripten an, die in eine konsistente Schnittstelle verpackt sind. Kein Framework nötig – nur eine Datei im scripts/-Verzeichnis deines Projekts mit einem klaren Namen und einer Hilfeausgabe.

tstypescript
#!/usr/bin/env node
// scripts/new-feature.ts
import { execSync } from "child_process";
 
const args = process.argv.slice(2);
 
function printHelp(): void {
  console.log(`
Usage: ./scripts/new-feature.ts <ticket-id> [description]
 
Creates a new feature branch from latest main with:
- Branch name: feature/<ticket-id>-<description>
- Commits an empty .feature file with ticket metadata
 
Examples:
  ./scripts/new-feature.ts PROJ-123 add-payment-flow
  ./scripts/new-feature.ts PROJ-456 refactor-auth
`);
}
 
if (args.length < 1 || args[0] === "--help") {
  printHelp();
  process.exit(args[0] === "--help" ? 0 : 1);
}
 
const ticketId = args[0];
const description = args[1] || "feature";
const branchName = `feature/${ticketId}-${description}`.toLowerCase();
 
function run(cmd: string): string {
  return execSync(cmd, { encoding: "utf-8" }).trim();
}
 
try {
  // Ensure clean working directory
  const status = run("git status --porcelain");
  if (status) {
    console.error("Error: Working directory is not clean. Commit or stash changes first.");
    process.exit(1);
  }
 
  // Update main and create branch
  console.log("Updating main branch...");
  run("git checkout main");
  run("git pull origin main");
 
  console.log(`Creating branch: ${branchName}`);
  run(`git checkout -b ${branchName}`);
 
  console.log(`\nBranch '${branchName}' created and checked out.`);
  console.log(`Ticket: ${ticketId}`);
} catch (error) {
  console.error("Failed:", (error as Error).message);
  process.exit(1);
}

Das Skript validiert Eingaben, prüft Vorbedingungen und liefert klare Fehlermeldungen. Diese drei Eigenschaften entscheiden, ob ein Skript einmal benutzt und vergessen wird oder zum täglichen Workflow des Teams gehört.

Komponierbare Skript-Architektur

Wenn dein Automatisierungs-Toolkit wächst, sollten sich einzelne Skripte zu größeren Workflows zusammensetzen lassen. Eine gemeinsame Bibliothek mit Hilfsfunktionen verhindert Duplikation.

tstypescript
// scripts/lib/git.ts
import { execSync } from "child_process";
 
export function getCurrentBranch(): string {
  return execSync("git branch --show-current", {
    encoding: "utf-8",
  }).trim();
}
 
export function isClean(): boolean {
  const status = execSync("git status --porcelain", {
    encoding: "utf-8",
  }).trim();
  return status === "";
}
 
export function getLastTag(): string | null {
  try {
    return execSync("git describe --tags --abbrev=0", {
      encoding: "utf-8",
    }).trim();
  } catch {
    return null;
  }
}
 
export function getCommitsSince(ref: string): string[] {
  return execSync(`git log ${ref}..HEAD --oneline`, {
    encoding: "utf-8",
  })
    .trim()
    .split("\n")
    .filter(Boolean);
}
tstypescript
// scripts/lib/env.ts
import { readFileSync, existsSync } from "fs";
import path from "path";
 
export function loadEnvFile(envPath: string): Record<string, string> {
  if (!existsSync(envPath)) {
    throw new Error(`Environment file not found: ${envPath}`);
  }
 
  const content = readFileSync(envPath, "utf-8");
  const vars: Record<string, string> = {};
 
  for (const line of content.split("\n")) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith("#")) continue;
 
    const eqIndex = trimmed.indexOf("=");
    if (eqIndex === -1) continue;
 
    const key = trimmed.slice(0, eqIndex).trim();
    let value = trimmed.slice(eqIndex + 1).trim();
 
    // Remove surrounding quotes
    if (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.slice(1, -1);
    }
 
    vars[key] = value;
  }
 
  return vars;
}
 
export function requireEnvVars(vars: string[]): void {
  const missing = vars.filter((v) => !process.env[v]);
  if (missing.length > 0) {
    throw new Error(
      `Missing required environment variables: ${missing.join(", ")}`
    );
  }
}
tstypescript
// scripts/lib/prompt.ts
import readline from "readline";
 
export async function confirm(message: string): Promise<boolean> {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
 
  return new Promise((resolve) => {
    rl.question(`${message} (y/N): `, (answer) => {
      rl.close();
      resolve(answer.toLowerCase() === "y");
    });
  });
}
 
export async function select(
  message: string,
  options: string[]
): Promise<string> {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
 
  console.log(message);
  options.forEach((opt, i) => console.log(`  ${i + 1}. ${opt}`));
 
  return new Promise((resolve) => {
    rl.question("Choice: ", (answer) => {
      rl.close();
      const idx = parseInt(answer, 10) - 1;
      resolve(options[idx] || options[0]);
    });
  });
}

Diese Hilfsfunktionen – Git-Operationen, Umgebungsverwaltung, interaktive Prompts – bilden die Bausteine für jedes Automatisierungs-Skript. Importiere, was du brauchst; ignoriere den Rest.

Automatisierung von Datenbank-Seeding

Datenbank-Seeding ist eines der wertvollsten Automatisierungsziele. Es wird häufig ausgeführt, variiert je nach Kontext, und Fehler führen zu kaskadierenden Entwicklungsproblemen.

tstypescript
#!/usr/bin/env node
// scripts/seed.ts
import { confirm, select } from "./lib/prompt";
 
interface SeedProfile {
  name: string;
  description: string;
  users: number;
  projects: number;
  includeEdgeCases: boolean;
}
 
const profiles: SeedProfile[] = [
  {
    name: "minimal",
    description: "1 admin, 2 users, 1 project — fast startup",
    users: 3,
    projects: 1,
    includeEdgeCases: false,
  },
  {
    name: "development",
    description: "10 users, 5 projects, realistic data distribution",
    users: 10,
    projects: 5,
    includeEdgeCases: false,
  },
  {
    name: "stress-test",
    description: "1000 users, 50 projects, includes edge cases",
    users: 1000,
    projects: 50,
    includeEdgeCases: true,
  },
];
 
async function main(): Promise<void> {
  const profileName = await select(
    "Select seed profile:",
    profiles.map((p) => `${p.name} — ${p.description}`)
  );
 
  const profile = profiles.find((p) =>
    profileName.startsWith(p.name)
  );
 
  if (!profile) {
    console.error("Invalid profile selected");
    process.exit(1);
  }
 
  const shouldReset = await confirm(
    "Reset database before seeding?"
  );
 
  if (shouldReset) {
    console.log("Resetting database...");
    // Reset logic here
  }
 
  console.log(`Seeding with profile: ${profile.name}`);
  console.log(`  Users: ${profile.users}`);
  console.log(`  Projects: ${profile.projects}`);
 
  // Seed execution here
  console.log("Seeding complete.");
}
 
main().catch(console.error);

Der Automatisierungs-Index: Dein Toolkit im Blick behalten

Halte in deinem Skriptverzeichnis ein README, das dokumentiert, was jedes Tool tut, wann man es verwendet und wer es pflegt:

tstypescript
// scripts/README.ts — Generate automation index from script metadata
import { readdirSync, readFileSync } from "fs";
import path from "path";
 
interface ScriptMeta {
  name: string;
  description: string;
  usage: string;
  author: string;
  lastUpdated: string;
}
 
function extractMeta(filepath: string): ScriptMeta | null {
  const content = readFileSync(filepath, "utf-8");
  const lines = content.split("\n").slice(0, 20);
 
  const descLine = lines.find((l) => l.includes("@description"));
  const usageLine = lines.find((l) => l.includes("@usage"));
  const authorLine = lines.find((l) => l.includes("@author"));
 
  if (!descLine) return null;
 
  return {
    name: path.basename(filepath),
    description: descLine.replace(/.*@description\s*/, "").trim(),
    usage: usageLine?.replace(/.*@usage\s*/, "").trim() || "See --help",
    author: authorLine?.replace(/.*@author\s*/, "").trim() || "team",
    lastUpdated: "",
  };
}
 
function generateIndex(scriptsDir: string): string {
  const files = readdirSync(scriptsDir).filter(
    (f) => f.endsWith(".ts") || f.endsWith(".mjs")
  );
 
  const metas = files
    .map((f) => extractMeta(path.join(scriptsDir, f)))
    .filter(Boolean) as ScriptMeta[];
 
  let index = "# Developer Scripts\n\n";
  index += "| Script | Description | Usage |\n";
  index += "|--------|-------------|-------|\n";
 
  for (const meta of metas) {
    index += `| \`${meta.name}\` | ${meta.description} | \`${meta.usage}\` |\n`;
  }
 
  return index;
}

Ein auffindbares Toolkit wird genutzt. Ein unauffindbares wird vom nächsten Entwickler, der auf dasselbe Problem stößt, von Grund auf neu gebaut.

Skripte in package.json integrieren

jsonjson
{
  "scripts": {
    "new-feature": "tsx scripts/new-feature.ts",
    "seed": "tsx scripts/seed.ts",
    "seed:minimal": "tsx scripts/seed.ts --profile minimal --no-prompt",
    "db:reset": "tsx scripts/db-reset.ts",
    "release": "tsx scripts/release.ts",
    "check:deps": "tsx scripts/check-deps.ts",
    "scripts:index": "tsx scripts/generate-index.ts"
  }
}

Skripte über package.json bereitzustellen macht sie über die Tab-Vervollständigung von npm run auffindbar und dokumentiert sie neben den Standardbefehlen des Projekts. npm run seed wird mit höherer Wahrscheinlichkeit gefunden und genutzt als tsx scripts/seed.ts.

Die wichtigsten Erkenntnisse

Automatisierung ist die Tätigkeit mit der größten Hebelwirkung in der Softwareentwicklung. Jede Minute, die in den Bau eines zuverlässigen Skripts investiert wird, multipliziert sich mit jeder zukünftigen Ausführung. Der Zinseszinseffekt ist enorm – ein Skript, das täglich fünf Minuten spart, spart über 20 Stunden pro Jahr, und das nur für eine einzige Person im Team.

Fang klein an: Such dir die Aufgabe, die dich am meisten nervt, automatisiere sie auf die einfachste mögliche Weise und platziere das Skript dort, wo das Team es findet. Baue eine gemeinsame Bibliothek mit Hilfsfunktionen für Git-Operationen, Umgebungsverwaltung und interaktive Prompts. Dokumentiere deine Skripte in einem zentralen Index.

Das beste Entwickler-Tooling ist nicht das ausgefeilteste – es ist das zuverlässigste. Ein Skript, das jedes Mal funktioniert, Fehler sauber behandelt und in zehn Sekunden durchläuft, wird täglich genutzt. Ein schönes CLI-Framework, für dessen Bau man drei Tage braucht, vielleicht nicht.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX