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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
11 changes: 11 additions & 0 deletions packages/autoskills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand Down
127 changes: 118 additions & 9 deletions packages/autoskills/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -69,13 +92,77 @@ 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"),
verbose: args.includes("--verbose") || args.includes("-v"),
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<string>();
const unknownIds = new Set<string>();

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),
};
}

Expand All @@ -89,26 +176,33 @@ 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
--dry-run Show skills without installing
--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;
Expand All @@ -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);
Expand Down Expand Up @@ -500,7 +594,8 @@ async function selectSkills(skills: SkillEntry[], autoYes: boolean): Promise<Ski
// ── Main ─────────────────────────────────────────────────────

async function main(): Promise<void> {
const { autoYes, dryRun, verbose, help, clearCache, agents } = parseArgs();
const { autoYes, dryRun, verbose, help, clearCache, agents, forceTechnologies, technologies } =
parseArgs();

if (help) {
showHelp();
Expand All @@ -522,9 +617,23 @@ async function main(): Promise<void> {

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."));
Expand All @@ -533,7 +642,7 @@ async function main(): Promise<void> {
process.exit(0);
}

printDetected(detected, combos, isFrontend);
printDetected(detected, combos, isFrontend, { forced: forceTechnologies });

const installedNames = getInstalledSkillNames(projectDir);
const skills = collectSkills({ detected, isFrontend, combos, installedNames });
Expand Down
73 changes: 73 additions & 0 deletions packages/autoskills/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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"));
});
});
});