diff --git a/README.md b/README.md index a2b1085..b168742 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ This keeps the package small while avoiding live downloads from third-party skil ``` -y, --yes Skip confirmation prompt --dry-run Show what would be installed without installing +-t, --tech Force technology ids (skip auto-detect) -h, --help Show help message ``` diff --git a/packages/autoskills/README.md b/packages/autoskills/README.md index 0cff108..5202087 100644 --- a/packages/autoskills/README.md +++ b/packages/autoskills/README.md @@ -36,6 +36,16 @@ npx autoskills -y npx autoskills --dry-run ``` +### Force specific technologies + +Skip auto-detection and install skills for the technology ids you specify: + +```bash +npx autoskills --tech react nextjs +npx autoskills --tech react --tech nextjs +npx autoskills --tech react,nextjs,tailwind +``` + ### Claude Code summary If `claude-code` is auto-detected or passed with `-a`, `autoskills` writes a `CLAUDE.md` file in your project root summarizing the markdown files installed under `.claude/skills`. @@ -47,6 +57,7 @@ If `claude-code` is auto-detected or passed with `-a`, `autoskills` writes a `CL | `-y`, `--yes` | Skip confirmation prompt, install all detected skills | | `--dry-run` | Show detected skills without installing anything | | `-v`, `--verbose` | Show install trace and error details | +| `-t`, `--tech` | Force technology ids (skip auto-detect) | | `-h`, `--help` | Show help message | ## Supported Technologies diff --git a/packages/autoskills/main.ts b/packages/autoskills/main.ts index 90306b9..057f64d 100644 --- a/packages/autoskills/main.ts +++ b/packages/autoskills/main.ts @@ -2,7 +2,15 @@ import { resolve, dirname, join } from "node:path"; import { existsSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { detectTechnologies, collectSkills, detectAgents, getInstalledSkillNames } from "./lib.ts"; +import { + detectTechnologies, + collectSkills, + detectAgents, + getInstalledSkillNames, + SKILLS_MAP, + FRONTEND_PACKAGES, + detectCombos, +} from "./lib.ts"; import type { SkillEntry, Technology, ComboSkill } from "./lib.ts"; import { log, @@ -57,6 +65,21 @@ interface CliArgs { help: boolean; clearCache: boolean; agents: string[]; + forceTechnologies: boolean; + technologies: string[]; +} + +interface ForcedTechnologiesResult { + detected: Technology[]; + isFrontend: boolean; + combos: ComboSkill[]; +} + +function appendCommaSeparatedValues(target: string[], value: string): void { + for (const part of value.split(",")) { + const trimmed = part.trim(); + if (trimmed) target.push(trimmed); + } } function parseArgs(): CliArgs { @@ -69,6 +92,33 @@ function parseArgs(): CliArgs { agents.push(args[i]); } } + + const technologies: string[] = []; + let forceTechnologies = false; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg.startsWith("--tech=")) { + forceTechnologies = true; + appendCommaSeparatedValues(technologies, arg.slice("--tech=".length)); + continue; + } + + if (arg.startsWith("-t=")) { + forceTechnologies = true; + appendCommaSeparatedValues(technologies, arg.slice("-t=".length)); + continue; + } + + if (arg === "-t" || arg === "--tech") { + forceTechnologies = true; + while (args[i + 1] && !args[i + 1].startsWith("-")) { + appendCommaSeparatedValues(technologies, args[i + 1]); + i++; + } + } + } + return { autoYes: args.includes("-y") || args.includes("--yes"), dryRun: args.includes("--dry-run"), @@ -76,6 +126,43 @@ function parseArgs(): CliArgs { help: args.includes("--help") || args.includes("-h"), clearCache: args.includes("--clear-cache"), agents, + forceTechnologies, + technologies, + }; +} + +function technologyImpliesFrontend(tech: Technology): boolean { + if (FRONTEND_PACKAGES.has(tech.id)) return true; + return (tech.detect.packages ?? []).some((pkg) => FRONTEND_PACKAGES.has(pkg)); +} + +function resolveForcedTechnologies(technologies: string[]): ForcedTechnologiesResult { + const knownIds = new Set(SKILLS_MAP.map((tech) => tech.id)); + const forcedIds = new Set(); + const unknownIds = new Set(); + + if (technologies.length === 0) { + log(yellow(" ⚠ No technology ids provided for --tech.")); + } + + for (const id of technologies) { + if (knownIds.has(id)) { + forcedIds.add(id); + } else { + unknownIds.add(id); + } + } + + for (const id of unknownIds) { + log(yellow(` ⚠ Unknown technology "${id}" — skipping.`)); + } + + const detected = SKILLS_MAP.filter((tech) => forcedIds.has(tech.id)); + const detectedIds = detected.map((tech) => tech.id); + return { + detected, + isFrontend: detected.some(technologyImpliesFrontend), + combos: detectCombos(detectedIds), }; } @@ -89,6 +176,7 @@ function showHelp(): void { npx autoskills ${dim("--dry-run")} Show what would be installed npx autoskills ${dim("--clear-cache")} Clear downloaded skills cache npx autoskills ${dim("-a cursor claude-code")} Install for specific IDEs only + npx autoskills ${dim("-t react nextjs")} Force specific technologies ${bold("Options:")} -y, --yes Skip confirmation prompt @@ -96,19 +184,25 @@ function showHelp(): void { --clear-cache Clear downloaded skills cache -v, --verbose Show install trace and error details -a, --agent Install for specific IDEs only (e.g. cursor, claude-code) + -t, --tech Force technology ids (skip auto-detect) -h, --help Show this help message `); } // ── Display ────────────────────────────────────────────────── -function printDetected(detected: Technology[], combos: ComboSkill[], isFrontend: boolean): void { +function printDetected( + detected: Technology[], + combos: ComboSkill[], + isFrontend: boolean, + { forced = false }: { forced?: boolean } = {}, +): void { if (detected.length > 0) { const withSkills = detected.filter((t) => t.skills.length > 0); const withoutSkills = detected.filter((t) => t.skills.length === 0); const allTech = [...withSkills, ...withoutSkills]; - log(cyan(" ◆ ") + bold("Detected technologies:")); + log(cyan(" ◆ ") + bold(forced ? "Selected technologies:" : "Detected technologies:")); log(); const COLS = 3; @@ -131,7 +225,7 @@ function printDetected(detected: Technology[], combos: ComboSkill[], isFrontend: if (combos.length > 0) { log(); - log(magenta(" ◆ ") + bold("Detected combos:")); + log(magenta(" ◆ ") + bold(forced ? "Selected combos:" : "Detected combos:")); log(); for (const combo of combos) { log(magenta(` ⚡ `) + combo.name); @@ -500,7 +594,8 @@ async function selectSkills(skills: SkillEntry[], autoYes: boolean): Promise { - const { autoYes, dryRun, verbose, help, clearCache, agents } = parseArgs(); + const { autoYes, dryRun, verbose, help, clearCache, agents, forceTechnologies, technologies } = + parseArgs(); if (help) { showHelp(); @@ -522,9 +617,23 @@ async function main(): Promise { const projectDir = resolve("."); - write(dim(" Scanning project...\r")); - const { detected, isFrontend, combos } = detectTechnologies(projectDir); - write("\x1b[K"); + let detected: Technology[]; + let isFrontend: boolean; + let combos: ComboSkill[]; + + if (forceTechnologies) { + const forced = resolveForcedTechnologies(technologies); + detected = forced.detected; + isFrontend = forced.isFrontend; + combos = forced.combos; + } else { + write(dim(" Scanning project...\r")); + const result = detectTechnologies(projectDir); + write("\x1b[K"); + detected = result.detected; + isFrontend = result.isFrontend; + combos = result.combos; + } if (detected.length === 0 && !isFrontend) { log(yellow(" ⚠ No supported technologies detected.")); @@ -533,7 +642,7 @@ async function main(): Promise { process.exit(0); } - printDetected(detected, combos, isFrontend); + printDetected(detected, combos, isFrontend, { forced: forceTechnologies }); const installedNames = getInstalledSkillNames(projectDir); const skills = collectSkills({ detected, isFrontend, combos, installedNames }); diff --git a/packages/autoskills/tests/cli.test.ts b/packages/autoskills/tests/cli.test.ts index 3c787cd..00466ba 100644 --- a/packages/autoskills/tests/cli.test.ts +++ b/packages/autoskills/tests/cli.test.ts @@ -26,6 +26,7 @@ describe("CLI", () => { ok(output.includes("--clear-cache")); ok(output.includes("--yes")); ok(output.includes("--agent")); + ok(output.includes("--tech")); }); it("shows help with -h", () => { @@ -476,4 +477,76 @@ describe("CLI", () => { ok(!output.includes("universal")); }); }); + + describe("--tech", () => { + const tmp = useTmpDir(); + + it("forces specific technology and skips auto-detection", () => { + writePackageJson(tmp.path, { dependencies: { express: "^5" } }); + const output = run(["--dry-run", "--tech", "react"], tmp.path); + ok(output.includes("Selected technologies")); + ok(output.includes("React")); + ok(!output.includes("Express")); + ok(!output.includes("No supported technologies")); + }); + + it("supports multiple values after one --tech flag", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "react", "nextjs"], tmp.path); + ok(output.includes("React")); + ok(output.includes("Next.js")); + }); + + it("supports multiple --tech flags", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "react", "--tech", "nextjs"], tmp.path); + ok(output.includes("React")); + ok(output.includes("Next.js")); + }); + + it("supports comma-separated technologies", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "react,nextjs"], tmp.path); + ok(output.includes("React")); + ok(output.includes("Next.js")); + }); + + it("supports equals-form comma-separated technologies", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech=react,nextjs"], tmp.path); + ok(output.includes("React")); + ok(output.includes("Next.js")); + }); + + it("adds web fundamentals for forced frontend technologies", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "nextjs"], tmp.path); + ok(output.includes("Next.js")); + ok(output.includes("frontend-design")); + ok(output.includes("accessibility")); + ok(output.includes("seo")); + }); + + it("warns about unknown technologies", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "unknown-tech"], tmp.path); + ok(output.includes("Unknown technology")); + ok(output.includes("No supported technologies")); + }); + + it("does not auto-detect when --tech is passed without values", () => { + writePackageJson(tmp.path, { dependencies: { react: "^19" } }); + const output = run(["--dry-run", "--tech"], tmp.path); + ok(output.includes("No technology ids provided")); + ok(output.includes("No supported technologies")); + ok(!output.includes("React")); + }); + + it("combines --tech with -y", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "react", "-y"], tmp.path); + ok(output.includes("React")); + ok(output.includes("Skills to install")); + }); + }); });