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
28 changes: 15 additions & 13 deletions packages/autoskills/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
COMBO_SKILLS_MAP,
FRONTEND_PACKAGES,
FRONTEND_BONUS_SKILLS,
BACKEND_ONLY_IDS,
WEB_FRONTEND_EXTENSIONS,
AGENT_FOLDER_MAP,
} from "./skills-map.ts";
Expand All @@ -20,6 +21,7 @@ import {
COMBO_SKILLS_MAP,
FRONTEND_PACKAGES,
FRONTEND_BONUS_SKILLS,
BACKEND_ONLY_IDS,
WEB_FRONTEND_EXTENSIONS,
AGENT_FOLDER_MAP,
} from "./skills-map.ts";
Expand Down Expand Up @@ -404,24 +406,18 @@ export function getAllPackageNames(pkg: Record<string, unknown> | null): string[
}

interface DetectInDirOptions {
skipFrontendFiles?: boolean;
pkg?: Record<string, unknown> | null;
denoJson?: Record<string, unknown> | null;
}

interface DetectInDirResult {
detected: Technology[];
isFrontendByPackages: boolean;
isFrontendByFiles: boolean;
}

function detectTechnologiesInDir(
dir: string,
{
skipFrontendFiles = false,
pkg: preloadedPkg,
denoJson: preloadedDeno,
}: DetectInDirOptions = {},
{ pkg: preloadedPkg, denoJson: preloadedDeno }: DetectInDirOptions = {},
): DetectInDirResult {
const pkg = preloadedPkg !== undefined ? preloadedPkg : readPackageJson(dir);
const allPackages = getAllPackageNames(pkg);
Expand Down Expand Up @@ -500,10 +496,8 @@ function detectTechnologiesInDir(
}

const isFrontendByPackages = allDepsArray.some((p) => FRONTEND_PACKAGES.has(p));
const isFrontendByFiles =
isFrontendByPackages || skipFrontendFiles ? false : hasWebFrontendFiles(dir);

return { detected, isFrontendByPackages, isFrontendByFiles };
return { detected, isFrontendByPackages };
}

export interface DetectResult {
Expand All @@ -517,25 +511,33 @@ export function detectTechnologies(projectDir: string): DetectResult {
const denoJson = readDenoJson(projectDir);
const root = detectTechnologiesInDir(projectDir, { pkg, denoJson });
const seenIds = new Map<string, Technology>(root.detected.map((t) => [t.id, t]));
let isFrontend = root.isFrontendByPackages || root.isFrontendByFiles;
let isFrontend = root.isFrontendByPackages;

const workspaceDirs = resolveWorkspaces(projectDir, { pkg, denoJson });
for (const wsDir of workspaceDirs) {
const ws = detectTechnologiesInDir(wsDir, { skipFrontendFiles: isFrontend });
const ws = detectTechnologiesInDir(wsDir);

for (const tech of ws.detected) {
if (!seenIds.has(tech.id)) {
seenIds.set(tech.id, tech);
}
}

if (ws.isFrontendByPackages || ws.isFrontendByFiles) {
if (ws.isFrontendByPackages) {
isFrontend = true;
}
}

const detected = [...seenIds.values()];
const detectedIds = detected.map((t) => t.id);

// Backend-only stacks (e.g. Python, Java) often contain .html templates
// or static .css files that should not trigger frontend classification.
if (!isFrontend && !detectedIds.some((id) => BACKEND_ONLY_IDS.has(id))) {
isFrontend =
hasWebFrontendFiles(projectDir) || workspaceDirs.some((dir) => hasWebFrontendFiles(dir));
}

const combos = detectCombos(detectedIds);

return { detected, isFrontend, combos };
Expand Down
33 changes: 24 additions & 9 deletions packages/autoskills/skills-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ export const SKILLS_MAP: Technology[] = [
skills: [
"github/awesome-copilot/dotnet-best-practices",
"github/awesome-copilot/dotnet-design-pattern-review",
"github/awesome-copilot/dotnet-upgrade"
"github/awesome-copilot/dotnet-upgrade",
],
},
{
Expand Down Expand Up @@ -819,10 +819,7 @@ export const SKILLS_MAP: Technology[] = [
patterns: ["Microsoft.NET.Sdk.Web"],
},
},
skills: [
"github/awesome-copilot/containerize-aspnetcore",
"openai/skills/aspnet-core",
],
skills: ["github/awesome-copilot/containerize-aspnetcore", "openai/skills/aspnet-core"],
},
{
id: "aspnet-blazor",
Expand All @@ -833,9 +830,7 @@ export const SKILLS_MAP: Technology[] = [
patterns: ["Microsoft.NET.Sdk.BlazorWebAssembly", "Microsoft.AspNetCore.Components"],
},
},
skills: [
"github/awesome-copilot/fluentui-blazor"
],
skills: ["github/awesome-copilot/fluentui-blazor"],
},
{
id: "aspnet-minimal-api",
Expand All @@ -849,7 +844,7 @@ export const SKILLS_MAP: Technology[] = [
},
skills: [
"github/awesome-copilot/aspnet-minimal-api-openapi",
"dotnet/skills/minimal-api-file-upload"
"dotnet/skills/minimal-api-file-upload",
],
},
{
Expand Down Expand Up @@ -1408,3 +1403,23 @@ export const WEB_FRONTEND_EXTENSIONS: Set<string> = new Set([
".pug",
".njk",
]);

/**
* Backend-only stacks whose .html/.css files are server-side templates or
* static assets, not a frontend application. Skipping frontend detection for
* these avoids misclassifying e.g. Flask or Django projects as frontend.
*
* Trade-off: causes a false negative when one of these backends is paired
* with a vanilla frontend directory (no package.json). Accepted because the
* inverse case is significantly more common. Extend this set when adding
* backend languages whose templates match WEB_FRONTEND_EXTENSIONS (e.g. Go,
* PHP, server-side Node).
*/
export const BACKEND_ONLY_IDS: Set<string> = new Set([
"python",
"java",
"springboot",
"django",
"flask",
"fastapi",
]);
20 changes: 20 additions & 0 deletions packages/autoskills/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,26 @@ describe("CLI", () => {
ok(output.includes("Spring Boot"));
});

it("does NOT detect web frontend for Python-only project with --dry-run", () => {
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
writeFile(tmp.path, "app/main.py", "from flask import Flask");
writeFile(tmp.path, "templates/index.html", "<html><body>Hello</body></html>");

const output = run(["--dry-run"], tmp.path);

ok(output.includes("Python"));
ok(!output.includes("Web frontend detected"));
ok(!output.includes("frontend-design"));
});

it("detects Python from requirements.txt with --dry-run", () => {
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");

const output = run(["--dry-run"], tmp.path);

ok(output.includes("Python"));
});

it("adds web fundamentals when npm frontend is detected too", () => {
writePackageJson(tmp.path, { dependencies: { react: "^19", next: "^15" } });
const output = run(["--dry-run"], tmp.path);
Expand Down
59 changes: 59 additions & 0 deletions packages/autoskills/tests/detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,13 @@ describe("detectTechnologies", () => {
strictEqual(isFrontend, false);
});

it("detects frontend from .html files when no backend stack is present", () => {
writeFile(tmp.path, "index.html", "<!DOCTYPE html><html></html>");
writeFile(tmp.path, "style.css", "body { margin: 0 }");
const { isFrontend } = detectTechnologies(tmp.path);
strictEqual(isFrontend, true);
});

it("detects combos when multiple technologies match", () => {
writePackageJson(tmp.path, { dependencies: { expo: "^52.0.0", tailwindcss: "^4.0.0" } });
const { combos } = detectTechnologies(tmp.path);
Expand Down Expand Up @@ -1482,6 +1489,58 @@ describe("detectTechnologies (monorepo)", () => {
});
});

// ── Python detection ─────────────────────────────────────────

describe("detectTechnologies (Python)", () => {
const tmp = useTmpDir();

it("detects Python from requirements.txt", () => {
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
const { detected } = detectTechnologies(tmp.path);
ok(detected.some((t) => t.id === "python"));
});

it("detects Python from pyproject.toml", () => {
writeFile(tmp.path, "pyproject.toml", "[project]\nname = 'myapp'");
const { detected } = detectTechnologies(tmp.path);
ok(detected.some((t) => t.id === "python"));
});

it("detects Python from setup.py", () => {
writeFile(tmp.path, "setup.py", "from setuptools import setup");
const { detected } = detectTechnologies(tmp.path);
ok(detected.some((t) => t.id === "python"));
});

it("detects Python from Pipfile", () => {
writeFile(tmp.path, "Pipfile", "[packages]\nflask = '*'");
const { detected } = detectTechnologies(tmp.path);
ok(detected.some((t) => t.id === "python"));
});

it("does NOT detect web frontend for Python project with .html templates", () => {
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
writeFile(tmp.path, "templates/index.html", "<html><body>Hello</body></html>");
const { isFrontend } = detectTechnologies(tmp.path);
strictEqual(isFrontend, false);
});

it("does NOT detect web frontend for Django project with templates", () => {
writeFile(tmp.path, "manage.py", "#!/usr/bin/env python");
writeFile(tmp.path, "templates/base.html", "{% block content %}{% endblock %}");
writeFile(tmp.path, "static/style.css", "body { margin: 0 }");
const { isFrontend } = detectTechnologies(tmp.path);
strictEqual(isFrontend, false);
});

it("detects frontend when both Python and frontend framework are present", () => {
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
writePackageJson(tmp.path, { dependencies: { react: "^19" } });
const { isFrontend } = detectTechnologies(tmp.path);
strictEqual(isFrontend, true);
});
});

// ── detectCombos ──────────────────────────────────────────────

describe("detectCombos", () => {
Expand Down
Loading