import { lastCommandSegment } from "./splitCompositeCommand.ts"; export interface CommandDetectionResult { type: string; command: string | null; confidence: number; category: | "git" | "test" | "build" | "shell" | "docker" | "package" | "infra" | "cloud" | "generic"; matchedPatterns: string[]; } type Detector = { type: string; category: CommandDetectionResult["category"]; commandPatterns: RegExp[]; contentPatterns: RegExp[]; }; const COMMAND_PREFIXES = [ "git", "make", "gradle", "gradlew", "dotnet", "terraform", "tofu", "opentofu", "systemctl", "npm", "pnpm", "yarn", "vitest", "jest", "pytest", "python", "go", "cargo", "tsc", "eslint", "webpack", "vite", "biome", "prettier", "turbo", "nx", "playwright", "ruff", "mypy", "pip", "uv", "poetry", "golangci-lint", "bundle", "rubocop", "kubectl", "composer", "gh", "docker", "aws", "gcloud", "ssh", "rsync", "curl", "wget", "ls", "find", "grep", "rg", "ag", "ps", "df", "du", ]; const COMMAND_PREFIX_PATTERN = new RegExp(`^(?:${COMMAND_PREFIXES.join("|")})\\b`); const DETECTORS: Detector[] = [ { type: "git-status", category: "git", commandPatterns: [/^git\s+status\b/i], contentPatterns: [ /^On branch /m, /^Changes (?:not staged|to be committed)/m, /^Untracked files:/m, ], }, { type: "git-branch", category: "git", commandPatterns: [/^git\s+branch\b/i, /^git\s+checkout\b/i, /^git\s+switch\b/i], contentPatterns: [/^\*\s+\S+/m, /Switched to (?:a new )?branch/i, /Already on ['"][^'"]+['"]/i], }, { type: "git-diff", category: "git", commandPatterns: [/^git\s+diff\b/i, /^git\s+show\b/i], contentPatterns: [/^diff --git /m, /^@@\s+-\d+,\d+\s+\+\d+,\d+\s+@@/m], }, { type: "git-log", category: "git", commandPatterns: [/^git\s+log\b/i], contentPatterns: [/^commit [0-9a-f]{7,40}/m, /^Author: /m], }, { type: "make", category: "build", commandPatterns: [/^make\b/i], contentPatterns: [/^make\[\d+\]: (?:Entering|Leaving) directory/m, /make: \*\*\* /], }, { type: "gradle", category: "build", commandPatterns: [/^(?:gradle|gradlew|\.\/gradlew)\b/i], contentPatterns: [/^> Task :/m, /^BUILD (?:SUCCESSFUL|FAILED)\b/m], }, { type: "dotnet", category: "build", commandPatterns: [/^dotnet\s+(?:build|test|run|restore|publish|pack|msbuild)\b/i, /^dotnet\b/i], contentPatterns: [/^Build (?:succeeded|FAILED)\b/m, /\b(?:error|warning) CS\d+\b/m], }, { type: "terraform-plan", category: "infra", commandPatterns: [/^terraform\s+plan\b/i], contentPatterns: [/Terraform will perform the following actions:/, /Plan: \d+ to add/i], }, { type: "tofu-plan", category: "infra", commandPatterns: [/^(?:tofu|opentofu)\s+plan\b/i], contentPatterns: [/OpenTofu will perform the following actions:/, /Plan: \d+ to add/i], }, { type: "systemctl-status", category: "infra", commandPatterns: [/^systemctl\s+status\b/i], contentPatterns: [/^\s*Loaded:\s+/m, /^\s*Active:\s+/m, /^●\s+\S+\.service/m], }, { type: "test-vitest", category: "test", commandPatterns: [/^vitest\b/i, /^npm\s+(?:run\s+)?test:vitest\b/i], contentPatterns: [/\bvitest\b/i, /^ ✓ /m, /^ ❯ /m, /Test Files\s+\d+\s+(?:passed|failed)/i], }, { type: "test-jest", category: "test", commandPatterns: [/^jest\b/i, /^npm\s+(?:run\s+)?test\b/i], contentPatterns: [/Test Suites:\s+\d+/i, /Tests:\s+\d+/i, /^PASS\s+/m, /^FAIL\s+/m], }, { type: "test-pytest", category: "test", commandPatterns: [/^pytest\b/i, /^python\s+-m\s+pytest\b/i], contentPatterns: [/=+\s+(?:\d+\s+)?(?:passed|failed|errors?)/i, /^E\s+/m, /^FAILED /m], }, { type: "test-cargo", category: "test", commandPatterns: [/^cargo\s+test\b/i, /^cargo\s+nextest\b/i], contentPatterns: [ /^running \d+ tests?/m, /^test\s+[\w:.-]+\s+\.\.\.\s+(?:ok|FAILED|ignored)/m, /test result:\s+(?:ok|FAILED)/i, ], }, { type: "test-go", category: "test", commandPatterns: [/^go\s+test\b/i], contentPatterns: [/^(?:ok|FAIL)\s+[\w./-]+\s+[\d.]+s/m, /^--- FAIL: /m, /^panic: /m], }, { type: "build-typescript", category: "build", commandPatterns: [/^tsc\b/i, /^npm\s+run\s+typecheck\b/i], contentPatterns: [/TS\d{4}:/, /error TS\d{4}/i], }, { type: "build-eslint", category: "build", commandPatterns: [/^eslint\b/i, /^npm\s+run\s+lint\b/i], contentPatterns: [/\s+\d+:\d+\s+(?:error|warning)\s+/, /✖\s+\d+\s+problems?/], }, { type: "build-webpack", category: "build", commandPatterns: [/^webpack\b/i, /^npx\s+webpack\b/i, /^npm\s+run\s+build:webpack\b/i], contentPatterns: [ /webpack\s+\d/i, /compiled (?:successfully|with \d+ errors?)/i, /asset .+\.js/i, ], }, { type: "build-vite", category: "build", commandPatterns: [/^vite\s+build\b/i, /^npm\s+run\s+build\b/i, /^pnpm\s+build\b/i], contentPatterns: [/vite v[\d.]+/i, /✓ built in/i, /transforming \(\d+\)/i], }, { type: "biome", category: "build", commandPatterns: [/^biome\b/i, /^npx\s+biome\b/i], contentPatterns: [/lint\/[A-Za-z0-9/.-]+/, /Checked \d+ files? in/i], }, { type: "prettier", category: "build", commandPatterns: [/^prettier\b/i, /^npx\s+prettier\b/i], contentPatterns: [/^Checking formatting\.\.\./m, /Code style issues found/i], }, { type: "turbo", category: "build", commandPatterns: [/^turbo\b/i, /^npx\s+turbo\b/i], contentPatterns: [/^• Packages in scope:/m, /^Tasks:\s+\d+\s+successful/m], }, { type: "nx", category: "build", commandPatterns: [/^nx\b/i, /^npx\s+nx\b/i], contentPatterns: [/^NX\s+/m, /^> nx run /m], }, { type: "playwright", category: "test", commandPatterns: [/^playwright\s+test\b/i, /^npx\s+playwright\s+test\b/i], contentPatterns: [/Running \d+ tests? using \d+ workers?/i, /^\s+\d+ failed/m], }, { type: "npm-install", category: "package", commandPatterns: [/^(?:npm|pnpm|yarn)\s+(?:install|add|update)\b/i], contentPatterns: [ /added \d+ packages/i, /packages are looking for funding/i, /audited \d+ packages/i, ], }, { type: "npm-audit", category: "package", commandPatterns: [/^(?:npm|pnpm|yarn)\s+audit\b/i], contentPatterns: [/found \d+ vulnerabilities/i, /\b(?:low|moderate|high|critical)\b/i], }, { type: "ruff", category: "build", commandPatterns: [/^ruff\b/i, /^uv\s+run\s+ruff\b/i], contentPatterns: [/^[\w./-]+\.py:\d+:\d+:\s+[A-Z]\d+/m, /Found \d+ errors?\./i], }, { type: "mypy", category: "build", commandPatterns: [/^mypy\b/i, /^python\s+-m\s+mypy\b/i], contentPatterns: [/^[\w./-]+\.py:\d+:\s+error:/m, /Found \d+ errors? in \d+ files?/i], }, { type: "pip", category: "package", commandPatterns: [/^pip\s+(?:install|download|uninstall)\b/i, /^python\s+-m\s+pip\b/i], contentPatterns: [/^Collecting /m, /^Successfully installed /m], }, { type: "uv-sync", category: "package", commandPatterns: [/^uv\s+sync\b/i, /^uv\s+pip\s+install\b/i], contentPatterns: [/^Resolved \d+ packages?/m, /^Installed \d+ packages?/m], }, { type: "poetry-install", category: "package", commandPatterns: [/^poetry\s+install\b/i], contentPatterns: [/^Installing dependencies from lock file/m, /^Package operations:/m], }, { type: "golangci-lint", category: "build", commandPatterns: [/^golangci-lint\b/i], contentPatterns: [/^[\w./-]+\.go:\d+:\d+:/m, /^\d+ issues?:/m], }, { type: "bundle-install", category: "package", commandPatterns: [/^bundle\s+install\b/i], contentPatterns: [/^Fetching gem metadata from /m, /^Bundle complete!/m], }, { type: "rubocop", category: "build", commandPatterns: [/^rubocop\b/i, /^bundle\s+exec\s+rubocop\b/i], contentPatterns: [/^Inspecting \d+ files/m, /^[\w./-]+\.rb:\d+:\d+:\s+[A-Z]:/m], }, { type: "docker-ps", category: "docker", commandPatterns: [/^docker\s+ps\b/i], contentPatterns: [/^CONTAINER ID\s+IMAGE\s+COMMAND/m], }, { type: "docker-logs", category: "docker", commandPatterns: [/^docker\s+logs\b/i, /^docker\s+compose\s+logs\b/i], contentPatterns: [ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/m, /\b(?:ERROR|WARN|INFO)\b/, /^Attaching to /m, ], }, { type: "aws", category: "cloud", commandPatterns: [/^aws\b/i], contentPatterns: [/An error occurred \([A-Za-z0-9]+\) when calling/, /^(?:upload|download): /m], }, { type: "gcloud", category: "cloud", commandPatterns: [/^gcloud\b/i], contentPatterns: [/^ERROR: \(gcloud\./m, /^Updated property \[/m], }, { type: "ssh", category: "cloud", commandPatterns: [/^ssh\b/i], contentPatterns: [ /Permission denied \(/, /Host key verification failed/, /Connection timed out/, ], }, { type: "rsync", category: "cloud", commandPatterns: [/^rsync\b/i], contentPatterns: [/^sending incremental file list/m, /^rsync error:/m], }, { type: "curl", category: "cloud", commandPatterns: [/^curl\b/i], contentPatterns: [/curl: \(\d+\)/, /^HTTP\/\d(?:\.\d)? \d{3}/m], }, { type: "wget", category: "cloud", commandPatterns: [/^wget\b/i], contentPatterns: [/^--\d{4}-\d{2}-\d{2}/m, /^ERROR \d{3}:/m], }, { type: "json-output", category: "generic", commandPatterns: [/^jq\b/i, /^cat\s+.*\.json\b/i], contentPatterns: [/^\s*[\[{][\s\S]*[\]}]\s*$/], }, { type: "shell-ls", category: "shell", commandPatterns: [/^ls(?:\s+-[A-Za-z]+)?\b/i], contentPatterns: [/^total \d+/m, /^\S+\s+\S+\s+\d+\s+\w+\s+\d{1,2}\s+/m], }, { type: "shell-find", category: "shell", commandPatterns: [/^find\b/i], contentPatterns: [/^(?:\.{1,2}|\/|[\w.-]+\/).+/m], }, { type: "shell-grep", category: "shell", commandPatterns: [/^(?:grep|rg|ag)\b/i], contentPatterns: [ /^[\w./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|md|json|ya?ml|txt):\d*:/m, /^[\w./-]+\/[\w./-]+:\d*:/m, ], }, { type: "shell-ps", category: "shell", commandPatterns: [/^ps\b/i], contentPatterns: [/^(?:USER\s+PID|\s*PID\s+)/m], }, { type: "shell-df", category: "shell", commandPatterns: [/^df\b/i], contentPatterns: [/^Filesystem\s+.*Use%/m], }, { type: "shell-du", category: "shell", commandPatterns: [/^du\b/i], contentPatterns: [/^\d+(?:\.\d+)?[KMGTP]?\s+\S+/m], }, { type: "error-stacktrace", category: "generic", commandPatterns: [], contentPatterns: [ /Traceback \(most recent call last\):/, /^\s+at\s+\S+\s+\(.+:\d+:\d+\)/m, /^panic: /m, /^thread '[^']+' panicked at/m, ], }, { type: "generic-error", category: "generic", commandPatterns: [], contentPatterns: [/Error:/, /Exception:/, /Traceback \(most recent call last\):/], }, ]; export function detectCommandFromText(text: string): string | null { const firstLines = text.split(/\r?\n/).slice(0, 4); for (const line of firstLines) { const trimmed = line.trim().replace(/^\$\s+/, ""); if (!trimmed) continue; if (COMMAND_PREFIX_PATTERN.test(trimmed)) { return lastCommandSegment(trimmed); } } return null; } export function detectCommandType(text: string, command?: string | null): CommandDetectionResult { const detectedCommand = lastCommandSegment(command?.trim() || detectCommandFromText(text) || "") || null; let best: CommandDetectionResult | null = null; for (const detector of DETECTORS) { const matchedPatterns: string[] = []; const commandMatched = Boolean(detectedCommand) && detector.commandPatterns.some((pattern) => { const matched = pattern.test(detectedCommand ?? ""); if (matched) matchedPatterns.push(pattern.source); return matched; }); const contentMatches = detector.contentPatterns.filter((pattern) => { const matched = pattern.test(text); if (matched) matchedPatterns.push(pattern.source); return matched; }).length; if (!commandMatched && contentMatches === 0) continue; const confidence = Math.min(1, (commandMatched ? 0.55 : 0) + contentMatches * 0.25); if (!best || confidence > best.confidence) { best = { type: detector.type, command: detectedCommand, confidence, category: detector.category, matchedPatterns, }; } } return ( best ?? { type: "unknown", command: detectedCommand, confidence: detectedCommand ? 0.35 : 0.1, category: "generic", matchedPatterns: [], } ); } export const detectCommandOutput = detectCommandType;