Skip to content
Merged
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
80 changes: 76 additions & 4 deletions packages/core/src/services/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ const PACKAGE_MANAGERS: Array<{ file: string; name: string }> = [
{ file: "pnpm-lock.yaml", name: "pnpm" },
{ file: "yarn.lock", name: "yarn" },
{ file: "package-lock.json", name: "npm" },
{ file: "bun.lockb", name: "bun" }
{ file: "bun.lockb", name: "bun" },
{ file: "packages.lock.json", name: "nuget" }
];

export async function analyzeRepo(repoPath: string): Promise<RepoAnalysis> {
Expand All @@ -82,9 +83,17 @@ export async function analyzeRepo(repoPath: string): Promise<RepoAnalysis> {
const hasRequirements = files.includes("requirements.txt");
const hasGoMod = files.includes("go.mod");
const hasCargo = files.includes("Cargo.toml");
const hasCsproj = files.some(
(f) => f.endsWith(".csproj") || f.endsWith(".sln") || f.endsWith(".slnx")
const hasDotnet = files.some(
(f) =>
f.endsWith(".csproj") ||
f.endsWith(".fsproj") ||
f.endsWith(".sln") ||
f.endsWith(".slnx") ||
f === "global.json" ||
f === "Directory.Build.props"
);
const hasCsproj = hasDotnet && files.some((f) => f.endsWith(".csproj"));
const hasFsproj = files.some((f) => f.endsWith(".fsproj"));
const hasPomXml = files.includes("pom.xml");
const hasBuildGradle = files.includes("build.gradle") || files.includes("build.gradle.kts");
const hasGemfile = files.includes("Gemfile");
Expand All @@ -100,7 +109,8 @@ export async function analyzeRepo(repoPath: string): Promise<RepoAnalysis> {
if (hasPyProject || hasRequirements) analysis.languages.push("Python");
if (hasGoMod) analysis.languages.push("Go");
if (hasCargo) analysis.languages.push("Rust");
if (hasCsproj) analysis.languages.push("C#");
if (hasCsproj || (hasDotnet && !hasFsproj)) analysis.languages.push("C#");
if (hasFsproj) analysis.languages.push("F#");
Comment on lines +112 to +113
if (hasPomXml || hasBuildGradle) analysis.languages.push("Java");
if (hasGemfile) analysis.languages.push("Ruby");
if (hasComposerJson) analysis.languages.push("PHP");
Expand All @@ -120,6 +130,11 @@ export async function analyzeRepo(repoPath: string): Promise<RepoAnalysis> {
analysis.frameworks.push(...detectFrameworks(deps, files));
}

if (hasDotnet) {
const dotnetFrameworks = await detectDotnetFrameworks(repoPath);
analysis.frameworks.push(...dotnetFrameworks);
}

const workspace = await detectWorkspace(repoPath, files, rootPackageJson);
if (workspace) {
analysis.workspaceType = workspace.type;
Expand Down Expand Up @@ -203,6 +218,63 @@ function detectFrameworks(deps: string[], files: string[]): string[] {
return frameworks;
}

async function detectDotnetFrameworks(repoPath: string): Promise<string[]> {
const projectFiles = await fg("**/*.{csproj,fsproj}", {
cwd: repoPath,
onlyFiles: true,
ignore: ["**/node_modules/**", "**/bin/**", "**/obj/**"]
});

const frameworks: string[] = [];
for (const projFile of projectFiles) {
try {
const content = await fs.readFile(path.join(repoPath, projFile), "utf8");
frameworks.push(...parseDotnetProject(content));
} catch {
// ignore read errors
}
}
return frameworks;
}

function parseDotnetProject(content: string): string[] {
const frameworks: string[] = [];
const hasPackage = (pkg: string): boolean => content.includes(`Include="${pkg}"`);

// SDK-based detection
if (content.includes('Sdk="Microsoft.NET.Sdk.Web"')) frameworks.push("ASP.NET Core");
if (content.includes('Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"'))
frameworks.push("Blazor WebAssembly");

// Package reference detection
if (hasPackage("Microsoft.AspNetCore") || hasPackage("Microsoft.AspNetCore.App"))
frameworks.push("ASP.NET Core");
if (hasPackage("Microsoft.AspNetCore.Components")) frameworks.push("Blazor");
if (hasPackage("Microsoft.EntityFrameworkCore")) frameworks.push("Entity Framework");
if (hasPackage("Microsoft.Maui.Controls")) frameworks.push(".NET MAUI");
if (hasPackage("Xamarin.Forms") || hasPackage("Xamarin.Essentials")) frameworks.push("Xamarin");

// Project property detection
if (content.includes("<UseWPF>true</UseWPF>")) frameworks.push("WPF");
if (content.includes("<UseWindowsForms>true</UseWindowsForms>")) frameworks.push("Windows Forms");

// Test framework detection
if (hasPackage("xunit") || hasPackage("xunit.core")) frameworks.push("xUnit");
if (hasPackage("NUnit") || hasPackage("nunit.framework")) frameworks.push("NUnit");
if (hasPackage("MSTest.TestFramework")) frameworks.push("MSTest");

// Console app fallback
if (
frameworks.length === 0 &&
content.includes("<OutputType>Exe</OutputType>") &&
content.includes('Sdk="Microsoft.NET.Sdk"')
) {
frameworks.push("Console");
}

return frameworks;
}

async function safeReadFile(filePath: string): Promise<string | undefined> {
try {
return await fs.readFile(filePath, "utf8");
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/services/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export async function generateCopilotInstructions(

Fan out multiple Explore subagents to map out the codebase in parallel:
1. Check for existing instruction files: glob for **/{.github/copilot-instructions.md,AGENT.md,CLAUDE.md,.cursorrules,README.md}
2. Identify the tech stack: look at package.json, tsconfig.json, pyproject.toml, Cargo.toml, go.mod, *.csproj, *.sln, build.gradle, pom.xml, etc.
2. Identify the tech stack: look at package.json, tsconfig.json, pyproject.toml, Cargo.toml, go.mod, *.csproj, *.fsproj, *.sln, global.json, build.gradle, pom.xml, etc.
3. Understand the structure: list key directories
4. Detect monorepo structures: check for workspace configs (npm/pnpm/yarn workspaces, Cargo.toml [workspace], go.work, .sln solution files, settings.gradle include directives, pom.xml modules)

Expand Down
78 changes: 78 additions & 0 deletions src/services/__tests__/analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,84 @@ describe("analyzeRepo", () => {
expect(result.packageManager).toBe("nuget");
});

it("detects F# language via .fsproj", async () => {
const repoPath = await makeTmpDir();
await fs.writeFile(
path.join(repoPath, "MyProject.fsproj"),
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType></PropertyGroup></Project>'
);

const result = await analyzeRepo(repoPath);
expect(result.languages).toContain("F#");
expect(result.frameworks).toContain("Console");
});

it("detects .NET via global.json", async () => {
const repoPath = await makeTmpDir();
await fs.writeFile(
path.join(repoPath, "global.json"),
JSON.stringify({ sdk: { version: "8.0.100" } })
);

const result = await analyzeRepo(repoPath);
expect(result.languages).toContain("C#");
});

it("detects ASP.NET Core framework from csproj", async () => {
const repoPath = await makeTmpDir();
await fs.writeFile(
path.join(repoPath, "WebApp.csproj"),
'<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>'
);

const result = await analyzeRepo(repoPath);
expect(result.languages).toContain("C#");
expect(result.frameworks).toContain("ASP.NET Core");
});

it("detects xUnit test framework from csproj", async () => {
const repoPath = await makeTmpDir();
await fs.writeFile(
path.join(repoPath, "Tests.csproj"),
[
'<Project Sdk="Microsoft.NET.Sdk">',
" <ItemGroup>",
' <PackageReference Include="xunit" Version="2.5.0" />',
' <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />',
" </ItemGroup>",
"</Project>"
].join("\n")
);

const result = await analyzeRepo(repoPath);
expect(result.frameworks).toContain("xUnit");
expect(result.frameworks).not.toContain("MSTest");
});

it("detects both C# and F# in mixed repo", async () => {
const repoPath = await makeTmpDir();
await fs.writeFile(
path.join(repoPath, "App.csproj"),
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType></PropertyGroup></Project>'
Comment on lines +313 to +317
);
await fs.writeFile(
path.join(repoPath, "Lib.fsproj"),
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>'
);

const result = await analyzeRepo(repoPath);
expect(result.languages).toContain("C#");
expect(result.languages).toContain("F#");
});

it("detects packages.lock.json as nuget", async () => {
const repoPath = await makeTmpDir();
await fs.writeFile(path.join(repoPath, "packages.lock.json"), "{}");

const result = await analyzeRepo(repoPath);
expect(result.packageManager).toBe("nuget");
});

it("detects Gradle multi-project", async () => {
const repoPath = await makeTmpDir();
await fs.writeFile(
Expand Down
Loading