Skip to content

Commit d6a0e8c

Browse files
committed
fix: prevent Python/Java backend projects from being detected as web frontend (#48)
1 parent c6fef04 commit d6a0e8c

4 files changed

Lines changed: 123 additions & 12 deletions

File tree

packages/autoskills/lib.mjs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {
77
COMBO_SKILLS_MAP,
88
FRONTEND_PACKAGES,
99
FRONTEND_BONUS_SKILLS,
10+
BACKEND_ONLY_IDS,
1011
WEB_FRONTEND_EXTENSIONS,
1112
AGENT_FOLDER_MAP,
1213
} from "./skills-map.mjs";
@@ -16,6 +17,7 @@ import {
1617
COMBO_SKILLS_MAP,
1718
FRONTEND_PACKAGES,
1819
FRONTEND_BONUS_SKILLS,
20+
BACKEND_ONLY_IDS,
1921
WEB_FRONTEND_EXTENSIONS,
2022
AGENT_FOLDER_MAP,
2123
} from "./skills-map.mjs";
@@ -328,14 +330,12 @@ export function getAllPackageNames(pkg) {
328330
/**
329331
* Scans a single directory for known technologies by checking packages, package patterns,
330332
* config files, and config file content against the SKILLS_MAP.
331-
* Also determines whether the directory looks like a frontend project.
333+
* Also determines whether the directory has frontend packages.
332334
* @param {string} dir - Directory to scan.
333-
* @returns {{ detected: object[], isFrontendByPackages: boolean, isFrontendByFiles: boolean }}
335+
* @param {{ pkg?: object|null, denoJson?: object|null }} [opts] - Pre-read manifests to avoid duplicate I/O.
336+
* @returns {{ detected: object[], isFrontendByPackages: boolean }}
334337
*/
335-
function detectTechnologiesInDir(
336-
dir,
337-
{ skipFrontendFiles = false, pkg: preloadedPkg, denoJson: preloadedDeno } = {},
338-
) {
338+
function detectTechnologiesInDir(dir, { pkg: preloadedPkg, denoJson: preloadedDeno } = {}) {
339339
const pkg = preloadedPkg !== undefined ? preloadedPkg : readPackageJson(dir);
340340
const allPackages = getAllPackageNames(pkg);
341341
const deno = preloadedDeno !== undefined ? preloadedDeno : readDenoJson(dir);
@@ -402,10 +402,8 @@ function detectTechnologiesInDir(
402402
}
403403

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

408-
return { detected, isFrontendByPackages, isFrontendByFiles };
406+
return { detected, isFrontendByPackages };
409407
}
410408

411409
/**
@@ -419,25 +417,33 @@ export function detectTechnologies(projectDir) {
419417
const denoJson = readDenoJson(projectDir);
420418
const root = detectTechnologiesInDir(projectDir, { pkg, denoJson });
421419
const seenIds = new Map(root.detected.map((t) => [t.id, t]));
422-
let isFrontend = root.isFrontendByPackages || root.isFrontendByFiles;
420+
let isFrontend = root.isFrontendByPackages;
423421

424422
const workspaceDirs = resolveWorkspaces(projectDir, { pkg, denoJson });
425423
for (const wsDir of workspaceDirs) {
426-
const ws = detectTechnologiesInDir(wsDir, { skipFrontendFiles: isFrontend });
424+
const ws = detectTechnologiesInDir(wsDir);
427425

428426
for (const tech of ws.detected) {
429427
if (!seenIds.has(tech.id)) {
430428
seenIds.set(tech.id, tech);
431429
}
432430
}
433431

434-
if (ws.isFrontendByPackages || ws.isFrontendByFiles) {
432+
if (ws.isFrontendByPackages) {
435433
isFrontend = true;
436434
}
437435
}
438436

439437
const detected = [...seenIds.values()];
440438
const detectedIds = detected.map((t) => t.id);
439+
440+
// Backend-only stacks (e.g. Python, Java) often contain .html templates
441+
// or static .css files that should not trigger frontend classification.
442+
if (!isFrontend && !detectedIds.some((id) => BACKEND_ONLY_IDS.has(id))) {
443+
isFrontend = hasWebFrontendFiles(projectDir) ||
444+
workspaceDirs.some((dir) => hasWebFrontendFiles(dir));
445+
}
446+
441447
const combos = detectCombos(detectedIds);
442448

443449
return { detected, isFrontend, combos };

packages/autoskills/skills-map.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,23 @@ export const SKILLS_MAP = [
473473
},
474474
skills: [],
475475
},
476+
{
477+
id: "python",
478+
name: "Python",
479+
detect: {
480+
configFiles: [
481+
"requirements.txt",
482+
"requirements-prod.txt",
483+
"pyproject.toml",
484+
"setup.py",
485+
"setup.cfg",
486+
"Pipfile",
487+
"poetry.lock",
488+
"manage.py",
489+
],
490+
},
491+
skills: [],
492+
},
476493
{
477494
id: "deno",
478495
name: "Deno",
@@ -744,6 +761,8 @@ export const AGENT_FOLDER_MAP = {
744761
".kiro": "kiro-cli",
745762
};
746763

764+
export const BACKEND_ONLY_IDS = new Set(["python", "java", "springboot"]);
765+
747766
export const WEB_FRONTEND_EXTENSIONS = new Set([
748767
".html",
749768
".htm",

packages/autoskills/tests/cli.test.mjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,26 @@ describe("CLI", () => {
437437
ok(output.includes("Node.js"));
438438
});
439439

440+
it("does NOT detect web frontend for Python-only project with --dry-run", () => {
441+
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
442+
writeFile(tmp.path, "app/main.py", "from flask import Flask");
443+
writeFile(tmp.path, "templates/index.html", "<html><body>Hello</body></html>");
444+
445+
const output = run(["--dry-run"], tmp.path);
446+
447+
ok(output.includes("Python"));
448+
ok(!output.includes("Web frontend detected"));
449+
ok(!output.includes("frontend-design"));
450+
});
451+
452+
it("detects Python from requirements.txt with --dry-run", () => {
453+
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
454+
455+
const output = run(["--dry-run"], tmp.path);
456+
457+
ok(output.includes("Python"));
458+
});
459+
440460
it("adds web fundamentals when npm frontend is detected too", () => {
441461
writePackageJson(tmp.path, { dependencies: { react: "^19", next: "^15" } });
442462

packages/autoskills/tests/detect.test.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,72 @@ describe("detectTechnologies (monorepo)", () => {
684684
});
685685
});
686686

687+
// ── Python detection ─────────────────────────────────────────
688+
689+
describe("detectTechnologies (Python)", () => {
690+
const tmp = useTmpDir();
691+
692+
it("detects Python from requirements.txt", () => {
693+
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
694+
const { detected } = detectTechnologies(tmp.path);
695+
ok(detected.some((t) => t.id === "python"));
696+
});
697+
698+
it("detects Python from pyproject.toml", () => {
699+
writeFile(tmp.path, "pyproject.toml", "[project]\nname = 'myapp'");
700+
const { detected } = detectTechnologies(tmp.path);
701+
ok(detected.some((t) => t.id === "python"));
702+
});
703+
704+
it("detects Python from setup.py", () => {
705+
writeFile(tmp.path, "setup.py", "from setuptools import setup");
706+
const { detected } = detectTechnologies(tmp.path);
707+
ok(detected.some((t) => t.id === "python"));
708+
});
709+
710+
it("detects Python from Pipfile", () => {
711+
writeFile(tmp.path, "Pipfile", "[packages]\nflask = '*'");
712+
const { detected } = detectTechnologies(tmp.path);
713+
ok(detected.some((t) => t.id === "python"));
714+
});
715+
716+
it("detects Python from manage.py (Django)", () => {
717+
writeFile(tmp.path, "manage.py", "#!/usr/bin/env python");
718+
const { detected } = detectTechnologies(tmp.path);
719+
ok(detected.some((t) => t.id === "python"));
720+
});
721+
722+
it("does NOT detect web frontend for Python project with .html templates", () => {
723+
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
724+
writeFile(tmp.path, "templates/index.html", "<html><body>Hello</body></html>");
725+
const { isFrontend } = detectTechnologies(tmp.path);
726+
strictEqual(isFrontend, false);
727+
});
728+
729+
it("does NOT detect web frontend for Django project with templates", () => {
730+
writeFile(tmp.path, "manage.py", "#!/usr/bin/env python");
731+
writeFile(tmp.path, "templates/base.html", "{% block content %}{% endblock %}");
732+
writeFile(tmp.path, "static/style.css", "body { margin: 0 }");
733+
const { isFrontend } = detectTechnologies(tmp.path);
734+
strictEqual(isFrontend, false);
735+
});
736+
737+
it("detects frontend when both Python and frontend framework are present", () => {
738+
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
739+
writePackageJson(tmp.path, { dependencies: { react: "^19" } });
740+
const { isFrontend } = detectTechnologies(tmp.path);
741+
strictEqual(isFrontend, true);
742+
});
743+
744+
it("returns correct skills (empty) for Python detection", () => {
745+
writeFile(tmp.path, "requirements.txt", "flask==3.0.0");
746+
const { detected } = detectTechnologies(tmp.path);
747+
const python = detected.find((t) => t.id === "python");
748+
ok(python);
749+
deepStrictEqual(python.skills, []);
750+
});
751+
});
752+
687753
// ── detectCombos ──────────────────────────────────────────────
688754

689755
describe("detectCombos", () => {

0 commit comments

Comments
 (0)