Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ Current detectors cover common JavaScript/TypeScript, Python, Ruby, PHP, C#, Jav

- package manager: npm, pnpm, yarn, bun, pip, bundler, composer, dotnet, maven, gradle, cargo, go
- commands: install, dev, start, build, test, lint, format, backend install, local services
- .NET tooling: ASP.NET Core, xUnit, NUnit, MSTest, Microsoft.NET.Test.Sdk
- docs: README, architecture docs, ADR directories, existing agent docs
- CI: GitHub Actions workflows
- monorepos: npm/pnpm workspaces, Turborepo, Nx, Lerna, Rush
Expand Down
4 changes: 3 additions & 1 deletion docs/detectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This page summarizes the current detector coverage. Claims here should stay alig
| Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `tests/` | FastAPI, Django, Flask, Pytest, Ruff | `python3 -m pip install ...`, `python3 -m pytest`, Ruff lint/format |
| Ruby | `Gemfile`, `config/application.rb`, `bin/rails` | Rails | `bundle install`, `bin/rails server`, `bin/rails test` |
| PHP | `composer.json`, `artisan`, `app/Http/Kernel.php` | Laravel | `composer install`, `php artisan serve`, `php artisan test` |
| C# / .NET | `*.csproj`, `*.sln`, `global.json`, `Program.cs` | .NET | `dotnet restore`, `dotnet build`, `dotnet test` |
| C# / .NET | `*.csproj`, `*.sln`, `global.json`, `Program.cs`, `Microsoft.NET.Sdk.Web`, common test package references | .NET, ASP.NET Core, xUnit, NUnit, MSTest, .NET Test SDK | `dotnet restore`, `dotnet build`, `dotnet test` |
| Java / Kotlin | `pom.xml`, `mvnw`, `build.gradle`, `build.gradle.kts`, `gradlew`, Spring Boot plugins/dependencies | Spring Boot | `./mvnw package`, `./mvnw test`, `./gradlew build`, `./gradlew test` |
| Rust | `Cargo.toml` | Actix, Axum, Rocket as Rust Web | `cargo build`, `cargo test`, `cargo clippy --all-targets --all-features`, `cargo fmt` |
| Go | `go.mod` | Gin | `go build ./...`, `go test ./...`, `go vet ./...`, `gofmt -w .` |
Expand All @@ -32,6 +32,8 @@ This page summarizes the current detector coverage. Claims here should stay alig
| Rails | `Gemfile` containing `rails`, `config/application.rb`, `bin/rails` |
| Laravel | `composer.json` with `laravel/framework`, `artisan`, `app/Http/Kernel.php` |
| .NET | `*.csproj`, `*.sln`, `global.json`, `Program.cs` |
| ASP.NET Core | `Microsoft.NET.Sdk.Web`, `WebApplication.CreateBuilder`, or minimal API route mapping in `Program.cs` |
| .NET test tooling | `PackageReference` entries for `Microsoft.NET.Test.Sdk`, xUnit, NUnit, or MSTest |
| Spring Boot | Spring Boot references in `pom.xml`, `build.gradle`, or `build.gradle.kts` |
| Docker Compose | Compose files named `docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml` |

Expand Down
33 changes: 31 additions & 2 deletions src/scanner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function scanRepo(root = process.cwd(), options = {}) {

const mergedGradleBuild = [gradleBuild, gradleBuildKts].filter(Boolean).join("\n");
const languageCounts = countLanguages(files);
const dotnetFiles = await readDotnetFiles(absoluteRoot, files);
const packageManager = await detectPackageManager(absoluteRoot, files);
const detectedCommands = await detectCommands({
root: absoluteRoot,
Expand Down Expand Up @@ -90,7 +91,7 @@ export async function scanRepo(root = process.cwd(), options = {}) {
name: detectName({ packageJson, pyproject, cargoToml, goMod, pomXml, composerJson, files, root: absoluteRoot }),
languages: languageCounts,
primaryLanguage: languageCounts[0]?.name || "Unknown",
frameworks: detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild: mergedGradleBuild, gemfile, composerJson, files }),
frameworks: detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild: mergedGradleBuild, gemfile, composerJson, files, dotnetFiles }),
monorepo: detectMonorepo({ packageJson, pnpmWorkspace, files }),
packageManager,
commands,
Expand Down Expand Up @@ -273,7 +274,7 @@ function detectName({ packageJson, pyproject, cargoToml, goMod, pomXml, composer
return path.basename(root);
}

function detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild, gemfile, composerJson, files }) {
function detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gradleBuild, gemfile, composerJson, files, dotnetFiles = [] }) {
const deps = {
...(packageJson?.dependencies || {}),
...(packageJson?.devDependencies || {}),
Expand Down Expand Up @@ -317,6 +318,8 @@ function detectFrameworks({ packageJson, pyproject, cargoToml, goMod, pomXml, gr
if (hasLaravelSignals({ composerJson, files })) names.push("Laravel");
if (hasSpringBootSignals({ pomXml, gradleBuild })) names.push("Spring Boot");
if (hasDotnetSignals(files)) names.push(".NET");
if (hasAspNetCoreSignals(dotnetFiles)) names.push("ASP.NET Core");
names.push(...detectDotnetTestTools(dotnetFiles));
if (/actix|axum|rocket/i.test(cargoToml)) names.push("Rust Web");
if (/github\.com\/gin-gonic\/gin/i.test(goMod)) names.push("Gin");
if (hasNextAppRouter) names.push("Next.js App Router");
Expand All @@ -334,6 +337,32 @@ function hasDotnetSignals(files) {
));
}

async function readDotnetFiles(root, files) {
const dotnetFiles = files.filter((file) => file.endsWith(".csproj") || /(^|\/)Program\.cs$/.test(file));
return Promise.all(dotnetFiles.map(async (file) => ({
file,
text: await readTextIfExists(path.join(root, file)),
})));
}

function hasAspNetCoreSignals(dotnetFiles) {
return dotnetFiles.some(({ file, text }) => (
file.endsWith(".csproj") && /<Project\s+Sdk=["']Microsoft\.NET\.Sdk\.Web["']/i.test(text)
) || (
/(^|\/)Program\.cs$/.test(file) && /\bWebApplication\.CreateBuilder\b|\bMap(Get|Post|Put|Delete|Patch)\b/.test(text)
));
}

function detectDotnetTestTools(dotnetFiles) {
const text = dotnetFiles.map((item) => item.text).join("\n");
const tools = [];
if (/<PackageReference\s+Include=["']xunit(?:\.[^"']+)?["']/i.test(text)) tools.push("xUnit");
if (/<PackageReference\s+Include=["']NUnit(?:\.[^"']+)?["']/i.test(text)) tools.push("NUnit");
if (/<PackageReference\s+Include=["']MSTest(?:\.[^"']+)?["']/i.test(text)) tools.push("MSTest");
if (/<PackageReference\s+Include=["']Microsoft\.NET\.Test\.Sdk["']/i.test(text)) tools.push(".NET Test SDK");
return tools;
}

function firstProjectStem(files, extension) {
const file = files.find((item) => item.endsWith(extension));
return file ? path.basename(file, extension) : "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@
<TargetFramework>net9.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.6.4" />
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions test/scanner.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ test("scan detects .NET and C# project signals", async () => {
assert.equal(profile.primaryLanguage, "C#");
assert.equal(profile.packageManager, "dotnet");
assert.ok(profile.frameworks.includes(".NET"));
assert.ok(profile.frameworks.includes("ASP.NET Core"));
assert.ok(profile.frameworks.includes(".NET Test SDK"));
assert.ok(profile.frameworks.includes("xUnit"));
assert.ok(profile.frameworks.includes("NUnit"));
assert.ok(profile.frameworks.includes("MSTest"));
assert.equal(profile.commands.install, "dotnet restore");
assert.equal(profile.commands.build, "dotnet build");
assert.equal(profile.commands.test, "dotnet test");
Expand Down