diff --git a/README.md b/README.md index a2b1085..670fb66 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 specific technologies (skip auto-detect) -h, --help Show help message ``` diff --git a/packages/autoskills/README.md b/packages/autoskills/README.md index 0cff108..8d9d980 100644 --- a/packages/autoskills/README.md +++ b/packages/autoskills/README.md @@ -36,6 +36,15 @@ npx autoskills -y npx autoskills --dry-run ``` +### Force specific technologies + +Skip auto-detection and install skills for the technologies you specify: + +```bash +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 +56,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 specific technologies (skip auto-detect) | | `-h`, `--help` | Show help message | ## Supported Technologies diff --git a/packages/autoskills/main.ts b/packages/autoskills/main.ts index 90306b9..522b74f 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,7 @@ interface CliArgs { help: boolean; clearCache: boolean; agents: string[]; + technologies: string[]; } function parseArgs(): CliArgs { @@ -69,6 +78,21 @@ function parseArgs(): CliArgs { agents.push(args[i]); } } + + const technologies: string[] = []; + for (let i = 0; i < args.length; i++) { + if (args[i] === "-t" || args[i] === "--tech") { + const val = args[i + 1]; + if (val && !val.startsWith("-")) { + for (const v of val.split(",")) { + const trimmed = v.trim(); + if (trimmed) technologies.push(trimmed); + } + i++; + } + } + } + return { autoYes: args.includes("-y") || args.includes("--yes"), dryRun: args.includes("--dry-run"), @@ -76,6 +100,7 @@ function parseArgs(): CliArgs { help: args.includes("--help") || args.includes("-h"), clearCache: args.includes("--clear-cache"), agents, + technologies, }; } @@ -89,6 +114,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,6 +122,7 @@ 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 specific technologies (skip auto-detect) -h, --help Show this help message `); } @@ -500,7 +527,7 @@ async function selectSkills(skills: SkillEntry[], autoYes: boolean): Promise { - const { autoYes, dryRun, verbose, help, clearCache, agents } = parseArgs(); + const { autoYes, dryRun, verbose, help, clearCache, agents, technologies } = parseArgs(); if (help) { showHelp(); @@ -522,9 +549,33 @@ 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 (technologies.length > 0) { + const validIds = new Set(SKILLS_MAP.map((t) => t.id)); + const forcedIds = new Set(); + for (const id of technologies) { + if (validIds.has(id)) { + forcedIds.add(id); + } else { + log(yellow(` ⚠ Unknown technology "${id}" — skipping.`)); + } + } + + detected = SKILLS_MAP.filter((t) => forcedIds.has(t.id)); + isFrontend = detected.some((t) => FRONTEND_PACKAGES.has(t.id)); + const detectedIds = detected.map((t) => t.id); + combos = detectCombos(detectedIds); + } 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.")); diff --git a/packages/autoskills/tests/cli.test.ts b/packages/autoskills/tests/cli.test.ts index 3c787cd..ad72e72 100644 --- a/packages/autoskills/tests/cli.test.ts +++ b/packages/autoskills/tests/cli.test.ts @@ -2,10 +2,23 @@ import { describe, it } from "node:test"; import { ok } from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { join, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; import { useTmpDir, writePackageJson, writeFile, writeJson, addWorkspace } from "./helpers.ts"; -const CLI_PATH = resolve(import.meta.dirname!, "..", "index.mjs"); +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function findCliPath(): string { + let dir = __dirname; + for (let i = 0; i < 3; i++) { + const candidate = resolve(dir, "index.mjs"); + if (existsSync(candidate)) return candidate; + dir = dirname(dir); + } + throw new Error("index.mjs not found"); +} + +const CLI_PATH = findCliPath(); function run(args: string[] = [], cwd: string = process.cwd()): string { return execFileSync(process.execPath, [CLI_PATH, ...args], { @@ -476,4 +489,42 @@ describe("CLI", () => { ok(!output.includes("universal")); }); }); + + describe("--tech", () => { + const tmp = useTmpDir(); + + it("forces specific technology and skips auto-detection", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "react"], tmp.path); + ok(output.includes("React")); + ok(!output.includes("No supported technologies")); + }); + + 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("warns about unknown technologies", () => { + writePackageJson(tmp.path); + const output = run(["--dry-run", "--tech", "unknown-tech"], tmp.path); + ok(output.includes("Unknown technology")); + }); + + 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")); + }); + }); });