UNPKG

@blundergoat/goat-flow

Version:

AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.

503 lines 18.4 kB
import { PROJECT_STACK_EXTRA_LANGUAGE_SIGNALS, PROJECT_STACK_JAVA_MANIFEST_PATHS, PROJECT_STACK_NODE_FRAMEWORKS, PROJECT_STACK_ROOT_PYTHON_FILES, PROJECT_STACK_SETUP_FRAMEWORK_MARKERS, PROJECT_STACK_SUBDIRECTORY_PYTHON_GLOBS, } from "./project-stack-data.js"; import { hasAnyGlob, hasAnyPath, readFirstExistingFile, } from "./project-stack-files.js"; import { countSourceFiles, detectProjectSignals, } from "./project-stack-signals.js"; /** Display labels for canonical stack language ids shown in the setup UI. */ const SETUP_LANGUAGE_LABELS = { javascript: "JavaScript", typescript: "TypeScript", php: "PHP", python: "Python", go: "Go", rust: "Rust", ruby: "Ruby", java: "Java", csharp: "C#", bash: "Bash", swift: "Swift", kotlin: "Kotlin", markdown: "Markdown", blade: "Blade", jinja: "Jinja", twig: "Twig", erb: "ERB", }; /** Framework labels that map directly from canonical stack language ids. */ const STACK_LANGUAGE_FRAMEWORK_LABELS = { react: "React", vue: "Vue", angular: "Angular", svelte: "Svelte", express: "Express", django: "Django", fastapi: "FastAPI", laravel: "Laravel", symfony: "Symfony", rails: "Rails", spring: "Spring", blazor: "Blazor", }; /** Check if an npm script command is a placeholder (npm init default) */ function isPlaceholderScript(cmd) { return (/^echo\s+"Error:/.test(cmd) || /^echo\s+"no\s+(test|build)/.test(cmd) || /^exit\s+1$/.test(cmd.trim()) || /^echo\s+.*&&\s*exit\s+1$/.test(cmd.trim())); } /** Extract commands from a package.json scripts block */ function extractNodeCommands(scripts) { /** Drop empty or intentionally placeholder script commands from detection output. */ /** Keep only real script commands that should survive stack detection. */ const filterPlaceholder = (cmd) => { if (!cmd || isPlaceholderScript(cmd)) return null; return cmd; }; // Test command: try exact match first, then scan for test-like script names let testCommand = filterPlaceholder(scripts.test); if (!testCommand) { const testPatterns = [ "e2e", "cypress", "spec", "test:unit", "test:e2e", "test:integration", ]; for (const pattern of testPatterns) { if (scripts[pattern] && !isPlaceholderScript(scripts[pattern])) { testCommand = `npm run ${pattern}`; break; } } // Fallback: first script name containing "test" if (!testCommand) { const testKey = Object.keys(scripts).find((k) => k.includes("test") && scripts[k] !== undefined && !isPlaceholderScript(scripts[k])); if (testKey) testCommand = `npm run ${testKey}`; } } return { buildCommand: filterPlaceholder(scripts.build), testCommand, lintCommand: filterPlaceholder(scripts.lint), formatCommand: filterPlaceholder(scripts.format ?? scripts["format:check"]), }; } /** Check if TypeScript is present in subdirectories (monorepo) */ function hasSubdirTypeScript(fs) { return fs.existsGlob("*/tsconfig.json") || fs.existsGlob("*/*/tsconfig.json"); } /** Add a language label once without disturbing existing detection order. */ function addLanguageIfMissing(languages, language) { if (languages.includes(language) === false) { languages.push(language); } } /** Add a setup display label once without disturbing existing order. */ function addSetupLabelIfMissing(labels, label) { if (labels.includes(label) === false) { labels.push(label); } } /** Detect whether any package in a candidate set is present in a dependency map. */ function hasAnyDependency(deps, packages) { return packages.some((pkg) => pkg in deps); } /** Detect common JavaScript or TypeScript source roots without package metadata. */ function hasNodeSourceFiles(fs) { return (fs.existsGlob("src/**/*.ts") || fs.existsGlob("src/**/*.js") || fs.existsGlob("lib/**/*.js")); } /** Infer JavaScript, TypeScript, and framework labels from Node manifests and files. */ function collectNodeLanguages(fs, runtimeDeps, deps) { const languages = []; const hasRuntimeDeps = runtimeDeps !== undefined && Object.keys(runtimeDeps).length > 0; const hasTypeScript = "typescript" in deps || fs.exists("tsconfig.json"); if (hasRuntimeDeps || hasNodeSourceFiles(fs) || hasTypeScript) { addLanguageIfMissing(languages, "javascript"); } if (hasTypeScript) { addLanguageIfMissing(languages, "typescript"); } for (const detector of PROJECT_STACK_NODE_FRAMEWORKS) { if (hasAnyDependency(deps, detector.packages)) { addLanguageIfMissing(languages, detector.language); } } return languages; } /** Detect root node stack. */ function detectRootNodeStack(fs, pkg) { const runtimeDeps = pkg.dependencies; const devDeps = pkg.devDependencies; const deps = { ...runtimeDeps, ...devDeps }; const scripts = pkg.scripts; const commands = scripts ? extractNodeCommands(scripts) : {}; return { languages: collectNodeLanguages(fs, runtimeDeps, deps), ...commands, }; } /** Detect subdirectory package manifests for monorepo-style Node projects. */ function hasSubdirNodePackage(fs) { return fs.existsGlob("*/package.json") || fs.existsGlob("*/*/package.json"); } /** Detect Node.js / TypeScript from package.json (root or subdirectory) */ function detectNodeStack(fs) { const pkg = fs.readJson("package.json"); if (pkg) { return detectRootNodeStack(fs, pkg); } // Monorepo: check subdirectory manifests if not detected at root if (hasSubdirNodePackage(fs)) { const languages = ["javascript"]; if (hasSubdirTypeScript(fs)) { languages.push("typescript"); } return { languages }; } return {}; } /** Detect Go from go.mod (root or subdirectory, up to 2 levels deep) */ function detectGoStack(fs) { if (fs.exists("go.mod") || fs.existsGlob("*/go.mod") || fs.existsGlob("*/*/go.mod")) { return { languages: ["go"], buildCommand: "go build ./...", testCommand: "go test ./...", lintCommand: "go vet ./...", formatCommand: "gofmt -l .", }; } return {}; } /** Detect Rust from Cargo.toml (root or subdirectory) */ function detectRustStack(fs) { if (fs.exists("Cargo.toml") || fs.existsGlob("*/Cargo.toml")) { return { languages: ["rust"], buildCommand: "cargo build", testCommand: "cargo test", lintCommand: "cargo clippy", formatCommand: "cargo fmt --check", }; } return {}; } /** Root Python manifests can name framework dependencies that plain file globs cannot. */ function detectPythonLanguages(fs) { const languages = ["python"]; const pyContent = fs.readFile("requirements.txt") ?? fs.readFile("pyproject.toml") ?? ""; if (/\bdjango\b/i.test(pyContent)) languages.push("django"); if (/\bfastapi\b/i.test(pyContent)) languages.push("fastapi"); return languages; } /** Detect Python projects from root or subdirectory manifests. */ function detectPythonStack(fs) { const hasRootPython = hasAnyPath(fs, PROJECT_STACK_ROOT_PYTHON_FILES); const hasSubdirPython = !hasRootPython && hasAnyGlob(fs, PROJECT_STACK_SUBDIRECTORY_PYTHON_GLOBS); if (!hasRootPython && !hasSubdirPython) { return {}; } const languages = detectPythonLanguages(fs); return hasRootPython ? { languages, testCommand: "pytest", lintCommand: "ruff check" } : { languages }; } /** Detect php languages. */ function detectPHPLanguages(fs, composer) { const languages = ["php"]; const require = composer.require; if (require && "laravel/framework" in require) addLanguageIfMissing(languages, "laravel"); if (require && "symfony/framework-bundle" in require) addLanguageIfMissing(languages, "symfony"); if (fs.exists("artisan")) addLanguageIfMissing(languages, "laravel"); if (fs.exists("symfony.lock")) addLanguageIfMissing(languages, "symfony"); return languages; } /** Return the first script value that exists in a composer scripts block. */ function firstDefinedScript(scripts, keys) { for (const key of keys) { if (scripts[key] !== undefined) { return scripts[key] ?? null; } } return null; } /** Extract php commands from scripts. */ function extractPHPCommandsFromScripts(scripts) { return { testCommand: scripts.test ?? null, lintCommand: firstDefinedScript(scripts, ["analyse", "lint"]), formatCommand: firstDefinedScript(scripts, ["cs:check", "cs:fix"]), }; } /** Extract php commands. */ function extractPHPCommands(composer) { const scripts = composer.scripts; return scripts ? extractPHPCommandsFromScripts(scripts) : { testCommand: null, lintCommand: null, formatCommand: null, }; } /** Detect PHP projects from root or subdirectory composer manifests. */ function detectPHPStack(fs) { const composer = fs.readJson("composer.json"); if (composer) { return { languages: detectPHPLanguages(fs, composer), ...extractPHPCommands(composer), }; } // Monorepo: check subdirectory manifests if not detected at root if (fs.existsGlob("*/composer.json")) { return { languages: ["php"] }; } return {}; } /** Detect Ruby from Gemfile */ function detectRubyStack(fs) { if (fs.exists("Gemfile") || fs.existsGlob("*/Gemfile")) { const languages = ["ruby"]; const gemfile = fs.readFile("Gemfile") ?? ""; if (/gem\s+['"]rails['"]/.test(gemfile) || fs.exists("bin/rails")) { languages.push("rails"); } return { languages, testCommand: "bundle exec rspec", lintCommand: "bundle exec rubocop", }; } return {}; } /** Java framework identity comes from manifest content because file names only reveal the build tool. */ function detectJavaLanguages(manifest) { const languages = ["java"]; if (/spring-boot/i.test(manifest)) { languages.push("spring"); } return languages; } /** Return java commands. */ function getJavaCommands(hasMaven) { return hasMaven ? { buildCommand: "mvn package", testCommand: "mvn test" } : { buildCommand: "gradle build", testCommand: "gradle test" }; } /** Detect Java from pom.xml or build.gradle */ function detectJavaStack(fs) { const hasMaven = fs.exists("pom.xml") || fs.existsGlob("*/pom.xml"); const hasGradle = fs.existsGlob("build.gradle*") || fs.existsGlob("*/build.gradle*"); if (!hasMaven && !hasGradle) { return {}; } const manifest = readFirstExistingFile(fs, PROJECT_STACK_JAVA_MANIFEST_PATHS) ?? ""; return { languages: detectJavaLanguages(manifest), ...getJavaCommands(hasMaven), }; } /** Detect .NET from *.csproj or *.sln */ function detectDotnetStack(fs) { if (fs.existsGlob("**/*.csproj") || fs.existsGlob("*.sln")) { return { languages: ["csharp"], buildCommand: "dotnet build", testCommand: "dotnet test", }; } return {}; } /** Treat shell as a language when scripts exist anywhere, even without a package manifest. */ function detectShellScripts(fs) { if (fs.existsGlob("**/*.sh")) { return { languages: ["bash"] }; } return {}; } /** Detect markdown-only (docs) project - only when no other languages found */ function detectMarkdownOnly(fs) { if (fs.glob("**/*.md").length > 5) { return { languages: ["markdown"] }; } return {}; } /** Merge detected language labels while preserving first-seen order. */ function mergeLanguages(target, languages) { for (const language of languages ?? []) { addLanguageIfMissing(target, language); } } /** Keep the first non-null command discovered across detector passes. */ function firstDetectedCommand(current, next) { return current ?? next ?? null; } /** Merge newly detected commands into the accumulated stack result. */ function mergeCommands(result, commands) { commands.buildCommand = firstDetectedCommand(commands.buildCommand, result.buildCommand); commands.testCommand = firstDetectedCommand(commands.testCommand, result.testCommand); commands.lintCommand = firstDetectedCommand(commands.lintCommand, result.lintCommand); commands.formatCommand = firstDetectedCommand(commands.formatCommand, result.formatCommand); } /** Combine detector outputs into the final stack info shape. */ function mergeDetectorResults(detectors) { const languages = []; const commands = { buildCommand: null, testCommand: null, lintCommand: null, formatCommand: null, }; for (const result of detectors) { mergeLanguages(languages, result.languages); mergeCommands(result, commands); } return { languages, ...commands }; } /** Detect template-only Jinja usage that would not show up as a normal manifest. */ function hasJinjaSignal(fs) { if (fs.existsGlob("**/*.jinja2")) return true; return fs .glob("**/*.html") .filter((file) => /templates\//.test(file)) .some((file) => { const content = fs.readFile(file); return content !== null && /\{[%{]/.test(content); }); } /** Apply data-table language signals after primary manifest detectors have run. */ function detectExtraLanguages(fs) { const languages = []; for (const signal of PROJECT_STACK_EXTRA_LANGUAGE_SIGNALS) { if (signal.language === "jinja") { if (hasJinjaSignal(fs)) languages.push(signal.language); continue; } if (!hasAnyPath(fs, signal.paths) && !hasAnyGlob(fs, signal.globs)) continue; languages.push(signal.language); } return languages; } /** Fall back to markdown-only classification when no code stack was detected. */ function applyMarkdownFallback(fs, languages) { if (languages.length > 0) return; const mdResult = detectMarkdownOnly(fs); for (const language of mdResult.languages ?? []) { addLanguageIfMissing(languages, language); } } /** * Detect languages and workflow commands from manifests and source files. * * Detector order intentionally preserves command priority: the first detector * that supplies a build/test/lint/format command wins, while language labels are * merged across all detectors. * * @param fs Read-only project filesystem abstraction. * @returns Canonical stack info, source count, and richer project signals. */ export function detectStack(fs) { // Order matters: first detector to provide a command wins (matches original priority) const detectorResults = [ detectNodeStack(fs), detectPHPStack(fs), detectRustStack(fs), detectGoStack(fs), detectPythonStack(fs), detectRubyStack(fs), detectJavaStack(fs), detectDotnetStack(fs), detectShellScripts(fs), ]; const stack = mergeDetectorResults(detectorResults); for (const language of detectExtraLanguages(fs)) { addLanguageIfMissing(stack.languages, language); } applyMarkdownFallback(fs, stack.languages); const signals = detectProjectSignals(fs, stack.languages, stack.formatCommand); const sourceFileCount = countSourceFiles(fs); return { ...stack, sourceFileCount, signals }; } /** Convert canonical stack command fields into setup-view command slots. */ function buildSetupCommands(stack) { return { test: stack.testCommand ?? "", lint: stack.lintCommand ?? "", build: stack.buildCommand ?? "", format: stack.formatCommand ?? "", }; } /** Convert canonical stack language ids into setup-view display labels. */ function buildSetupLanguages(stackLanguages) { const labels = []; for (const language of stackLanguages) { const display = SETUP_LANGUAGE_LABELS[language]; if (display) addSetupLabelIfMissing(labels, display); } return labels; } /** Check whether any file in a candidate list contains one of the given markers. */ function hasFrameworkMarker(fs, files, markers) { return files.some((file) => { const content = fs.readFile(file); const haystack = content ?? (() => { const json = fs.readJson(file); return json === null ? null : JSON.stringify(json); })(); if (haystack === null) return false; const normalized = haystack.toLowerCase(); return markers.some((marker) => normalized.includes(marker.toLowerCase())); }); } /** Build setup-view framework labels from canonical stack languages plus a few * extra framework markers not represented as distinct stack language ids. */ function buildSetupFrameworks(fs, stackLanguages) { const frameworks = []; for (const language of stackLanguages) { const display = STACK_LANGUAGE_FRAMEWORK_LABELS[language]; if (display) addSetupLabelIfMissing(frameworks, display); } for (const detector of PROJECT_STACK_SETUP_FRAMEWORK_MARKERS) { if (hasFrameworkMarker(fs, detector.files, detector.markers)) { addSetupLabelIfMissing(frameworks, detector.name); } } return frameworks; } /** * Build the setup-view stack summary from the canonical detector output. * * @param fs Read-only project filesystem abstraction. * @returns Dashboard-friendly labels and command slots derived from `detectStack`. */ export function detectSetupStack(fs) { const stack = detectStack(fs); return { languages: buildSetupLanguages(stack.languages), frameworks: buildSetupFrameworks(fs, stack.languages), commands: buildSetupCommands(stack), }; } //# sourceMappingURL=project-stack.js.map