@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
JavaScript
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