diff --git a/packages/autoskills/lib.ts b/packages/autoskills/lib.ts index 535c74eb..d6d671c9 100644 --- a/packages/autoskills/lib.ts +++ b/packages/autoskills/lib.ts @@ -344,6 +344,37 @@ export function resolveWorkspaces(projectDir: string, preloaded?: PreloadedManif // ── Detection ───────────────────────────────────────────────── +export function existsDeep( + dir: string, + fileNames: string[], + maxDepth: number = 3, +): boolean { + const nameSet = new Set(fileNames); + + function scan(current: string, depth: number): boolean { + let entries: import("node:fs").Dirent[]; + try { + entries = readdirSync(current, { withFileTypes: true }); + } catch { + return false; + } + for (const entry of entries) { + if (entry.isFile() && nameSet.has(entry.name)) return true; + if ( + entry.isDirectory() && + depth < maxDepth && + !entry.name.startsWith(".") && + !SCAN_SKIP_DIRS.has(entry.name) + ) { + if (scan(join(current, entry.name), depth + 1)) return true; + } + } + return false; + } + + return scan(dir, 0); +} + export function readGemfile(dir: string): string[] { const gemfilePath = join(dir, "Gemfile"); if (!existsSync(gemfilePath)) return []; @@ -468,6 +499,9 @@ function detectTechnologiesInDir( if (!found && tech.detect.configFiles) { found = tech.detect.configFiles.some((f) => cachedExists(join(dir, f))); + if (!found) { + found = existsDeep(dir, tech.detect.configFiles); + } } if (!found && tech.detect.gems) { diff --git a/packages/autoskills/tests/detect.test.ts b/packages/autoskills/tests/detect.test.ts index b67be9b3..6d95fdad 100644 --- a/packages/autoskills/tests/detect.test.ts +++ b/packages/autoskills/tests/detect.test.ts @@ -9,6 +9,7 @@ import { detectTechnologies, detectCombos, parseSettingsGradleModules, + existsDeep, } from "../lib.ts"; import { useTmpDir, writePackageJson, writeJson, writeFile, addWorkspace } from "./helpers.ts"; @@ -1633,3 +1634,72 @@ describe("detectCombos", () => { ok(!detectCombos(["rails"]).some((c) => c.id === "rails-rspec")); }); }); + +// ── existsDeep ─────────────────────────────────────────────── + +describe("existsDeep", () => { + const tmp = useTmpDir(); + + it("finds a file at the root level", () => { + writeFile(tmp.path, "Package.swift", ""); + ok(existsDeep(tmp.path, ["Package.swift"])); + }); + + it("finds a file one level deep", () => { + writeFile(tmp.path, "Packages/DesignSystem/Package.swift", ""); + ok(existsDeep(tmp.path, ["Package.swift"])); + }); + + it("finds a file two levels deep", () => { + writeFile(tmp.path, "ios/App/Podfile", ""); + ok(existsDeep(tmp.path, ["Podfile"])); + }); + + it("returns false when file is absent", () => { + ok(!existsDeep(tmp.path, ["Package.swift", "Podfile"])); + }); + + it("returns false when file exceeds maxDepth", () => { + writeFile(tmp.path, "a/b/c/d/Package.swift", ""); + ok(!existsDeep(tmp.path, ["Package.swift"], 3)); + }); + + it("skips node_modules directories", () => { + writeFile(tmp.path, "node_modules/some-pkg/Package.swift", ""); + ok(!existsDeep(tmp.path, ["Package.swift"])); + }); + + it("matches any name from the provided list", () => { + writeFile(tmp.path, "ios/Podfile", ""); + ok(existsDeep(tmp.path, ["Package.swift", "Podfile"])); + }); +}); + +// ── detectTechnologies — configFiles deep scan (SwiftUI) ────── + +describe("detectTechnologies — configFiles deep scan (SwiftUI)", () => { + const tmp = useTmpDir(); + + it("detects swiftui when Package.swift is at root", () => { + writeFile(tmp.path, "Package.swift", ""); + const { detected } = detectTechnologies(tmp.path); + ok(detected.some((t) => t.id === "swiftui")); + }); + + it("detects swiftui when Package.swift is in a subdirectory (e.g. Packages/)", () => { + writeFile(tmp.path, "Packages/DesignSystem/Package.swift", ""); + const { detected } = detectTechnologies(tmp.path); + ok(detected.some((t) => t.id === "swiftui")); + }); + + it("detects swiftui when Podfile is nested under ios/", () => { + writeFile(tmp.path, "ios/Podfile", ""); + const { detected } = detectTechnologies(tmp.path); + ok(detected.some((t) => t.id === "swiftui")); + }); + + it("does not detect swiftui when neither Package.swift nor Podfile exist", () => { + const { detected } = detectTechnologies(tmp.path); + ok(!detected.some((t) => t.id === "swiftui")); + }); +});