diff --git a/AGENTS.md b/AGENTS.md index 958852d..a5be5e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ src/ ├── cli.ts # CLI entry point with parseArgs ├── config.ts # skills.json config management +├── detect.ts # Project stack detection ├── skills.ts # Skills CLI execution └── utils/ ├── colors.ts # ANSI color codes for terminal output @@ -31,6 +32,10 @@ test/index.test.ts # Tests using vitest - `installSkills(options?)` — Spawns `skills add` for each source with progress logging; options: `{ cwd?, agents?, global?, yes? }` - `installSkillSource(entry, options)` — Installs a single skill source; options: `{ cwd?, agents?, global?, yes?, prefix? }` +### Detect (src/detect.ts) + +- `detectSkills(options?)` — Detects project stack using `skills-detector` and saves to `skills.json`; options: `{ cwd? }` + ### CLI Entry (src/cli.ts) - `main(argv?)` — CLI entry point using Node.js `parseArgs` @@ -71,6 +76,7 @@ interface SkillSource { skillman # Install skills (default) skillman install, i [--global] [--agent ...] # Install skills from skills.json skillman add ... [--agent ...] # Add skill source(s) to skills.json +skillman detect # Detect project stack and save to skills.json ``` ### Source Format diff --git a/README.md b/README.md index decdb92..dd52ddc 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,23 @@ This creates a `skills.json` file: } ``` +**Auto detect skills for current project:** + +```bash +npx skillman detect +``` + +

+ Install preview +

+ ## CLI Usage ```sh npx skillman # Install skills from skills.json (default) npx skillman install, i # Same as above npx skillman add ... # Add skill source(s) to skills.json +npx skillman detect # Detect project stack and save to skills.json ``` ### Commands @@ -75,6 +86,14 @@ npx skillman add ... [options] | `--agent ` | Target agent (default: `claude-code`, repeatable) | | `-h, --help` | Show help | +#### `detect` + +Detects project stack (frameworks, languages, tools) and saves the information to `skills.json`. Uses [`skills-detector`](https://github.com/vercel-labs/skills-detector) under the hood. + +```sh +npx skillman detect +``` + ### Source Formats Sources can be specified in multiple formats: @@ -115,6 +134,9 @@ npx skillman install --global # Install for multiple agents npx skillman install --agent claude-code --agent cursor + +# Detect project stack +npx skillman detect ``` ## Development diff --git a/assets/detect.svg b/assets/detect.svg new file mode 100644 index 0000000..206ede8 --- /dev/null +++ b/assets/detect.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + +npx skillman detect + + + + + + + + + + + + +🔍Detecting skills in current project + + +Detected: + + +packageManager: pnpm + + +frameworks: - + + +languages: - + + +tools: - + + +testing: - + + +searchTerms: - + + +Updated skills.json with detected information. + + +Searching for suggested skills... + + +Suggested Skills: + + +antfu/skills: pnpm + + +Use command below to install all detected skills: + + +npx skillman add antfu/skills:pnpm + + + + + + + + + \ No newline at end of file diff --git a/assets/gen.sh b/assets/gen.sh index 1b8c9bd..fcb9ec6 100755 --- a/assets/gen.sh +++ b/assets/gen.sh @@ -9,9 +9,13 @@ rm -rf "$TMP_DIR" && mkdir -p "$TMP_DIR" && cd "$TMP_DIR" cleanup() { rm -rf "$TMP_DIR"; } trap cleanup EXIT +SKILLMAN="node ../../src/cli.ts" + # https://github.com/pamburus/termframe -termframe -o "$SCRIPT_DIR/add.svg" --padding 2 -H auto --title "npx skillman add" -- pnpx skillman add skills.sh/vercel-labs/skills/find-skills anthropics/skills:skill-creator +termframe -o "$SCRIPT_DIR/add.svg" --padding 2 -H auto --title "npx skillman add" -- $SKILLMAN add skills.sh/vercel-labs/skills/find-skills anthropics/skills:skill-creator + +termframe -o "$SCRIPT_DIR/install.svg" --padding 2 -H auto --title "npx skillman" -- $SKILLMAN -termframe -o "$SCRIPT_DIR/install.svg" --padding 2 -H auto --title "npx skillman" -- pnpx skillman +termframe -o "$SCRIPT_DIR/detect.svg" --padding 2 -H auto --title "npx skillman detect" -- $SKILLMAN detect diff --git a/package.json b/package.json index cb9274e..a0b7b15 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "typecheck": "tsgo --noEmit --skipLibCheck" }, "dependencies": { - "skills": "^1.3.1" + "skills": "^1.3.1", + "skills-detector": "link:../skills-detector" }, "devDependencies": { "@types/node": "latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6050e44..1ca407d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: skills: specifier: ^1.3.1 version: 1.3.1 + skills-detector: + specifier: link:../skills-detector + version: link:../skills-detector devDependencies: '@types/node': specifier: latest diff --git a/skills.json b/skills.json index ea4f7c0..2ba0427 100644 --- a/skills.json +++ b/skills.json @@ -13,5 +13,26 @@ "skill-creator" ] } - ] + ], + "detected": { + "packageManager": "pnpm", + "frameworks": [], + "languages": [ + "typescript", + "javascript" + ], + "tools": [], + "testing": [ + "vitest", + "pytest", + "minitest" + ], + "searchTerms": [ + "javascript", + "minitest", + "pytest", + "typescript", + "vitest" + ] + } } diff --git a/skills_schema.json b/skills_schema.json index 609834e..98979d2 100644 --- a/skills_schema.json +++ b/skills_schema.json @@ -30,6 +30,51 @@ } } } + }, + "detected": { + "type": "object", + "description": "Auto-detected project environment information", + "properties": { + "packageManager": { + "type": ["string", "null"], + "description": "Detected package manager (npm, yarn, pnpm, bun, deno)" + }, + "frameworks": { + "type": "array", + "description": "Detected frameworks (e.g., nextjs, remix, rails, django)", + "items": { + "type": "string" + } + }, + "languages": { + "type": "array", + "description": "Detected programming languages (e.g., typescript, python, go)", + "items": { + "type": "string" + } + }, + "tools": { + "type": "array", + "description": "Detected tools and libraries (e.g., prisma, tailwind, docker)", + "items": { + "type": "string" + } + }, + "testing": { + "type": "array", + "description": "Detected testing frameworks (e.g., vitest, jest, playwright)", + "items": { + "type": "string" + } + }, + "searchTerms": { + "type": "array", + "description": "Combined search terms for skill discovery", + "items": { + "type": "string" + } + } + } } } } diff --git a/src/cli.ts b/src/cli.ts index d741e67..5b1050d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,6 +4,7 @@ import { parseArgs } from "node:util"; import { c } from "./utils/colors.ts"; import { addSkill, findSkillsConfig } from "./config.ts"; +import { detectSkills } from "./detect.ts"; import { installSkillSource, installSkills } from "./skills.ts"; const name = "skillman"; @@ -113,6 +114,11 @@ ${c.dim}$${c.reset} npx ${name} add ${c.cyan}vercel-labs/skills${c.reset} return; } + if (command === "detect") { + await detectSkills(); + return; + } + throw new Error(`Unknown command: ${command}`); } @@ -154,6 +160,18 @@ ${c.bold}Examples:${c.reset} return; } + if (command === "detect") { + console.log(` +${c.bold}Usage:${c.reset} ${c.cyan}${name} detect${c.reset} + +Detect project stack and save to skills.json. + +${c.bold}Examples:${c.reset} + ${c.dim}$${c.reset} npx ${name} detect +`); + return; + } + console.log(` ${c.bold}${name}${c.reset} ${c.dim}v${version}${c.reset} @@ -164,6 +182,7 @@ ${c.bold}Usage:${c.reset} ${c.cyan}${name}${c.reset} [options] ${c.bold}Commands:${c.reset} ${c.cyan}install, i${c.reset} Install skills from skills.json ${c.dim}(default)${c.reset} ${c.cyan}add${c.reset} Add a skill source to skills.json + ${c.cyan}detect${c.reset} Detect project stack and save to skills.json ${c.bold}Options:${c.reset} ${c.cyan}-h, --help${c.reset} Show help for a command diff --git a/src/config.ts b/src/config.ts index e5cad0e..04ed1fe 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,11 @@ import { existsSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; +import type { DetectionResult } from "skills-detector"; export interface SkillsConfig { $schema?: string; + detected?: DetectionResult; skills: SkillSource[]; } diff --git a/src/detect.ts b/src/detect.ts new file mode 100644 index 0000000..d49e432 --- /dev/null +++ b/src/detect.ts @@ -0,0 +1,113 @@ +import { c } from "./utils/colors.ts"; +import { updateSkillsConfig } from "./config.ts"; + +export interface DetectOptions { + cwd?: string; +} + +export async function detectSkills(options: DetectOptions = {}): Promise { + const cwd = options.cwd ?? process.cwd(); + + console.log(`🔍 Detecting skills in current project`); + + // https://github.com/vercel-labs/skills-detector + const { detect } = await import("skills-detector"); + const detected = await detect({ cwd }); + + const keywords: Set = new Set(); + + console.log(`\n${c.bold}Detected:${c.reset}`); + for (const [key, value] of Object.entries(detected)) { + const items = Array.isArray(value) ? (value.length > 0 ? value.join(", ") : "-") : value || "-"; + if (Array.isArray(value)) { + for (const item of value) { + keywords.add(item); + } + } else if (typeof value === "string" && value) { + keywords.add(value); + } + console.log(` ${c.cyan}${key}${c.reset}: ${items}`); + } + console.log(); + + await updateSkillsConfig( + (config) => { + config.detected = detected; + }, + { cwd, createIfNotExists: true }, + ); + + console.log(`${c.green}✔${c.reset} Updated skills.json with detected information.`); + + console.log(`\n${c.bold}Searching for suggested skills...${c.reset}`); + const suggestedSkills = await Promise.all( + Array.from(keywords).map(async (keyword) => { + const skills = await searchSkillsAPI(keyword); + return skills; + }), + ).then((r) => r.flat()); + + // Normalize: unique by source, group skills[] per source + const skillsBySource = new Map(); + for (const skill of suggestedSkills) { + if (!skill.source) continue; + const existing = skillsBySource.get(skill.source) || []; + if (!existing.includes(skill.slug)) { + existing.push(skill.slug); + } + skillsBySource.set(skill.source, existing); + } + + if (skillsBySource.size > 0) { + console.log(`\n${c.bold}Suggested Skills:${c.reset}`); + for (const [source, skills] of skillsBySource) { + console.log(` ${c.cyan}${source}${c.reset}: ${skills.join(", ")}`); + } + const sources = Array.from(skillsBySource.entries()) + .map(([source, skills]) => `${source}:${skills.join(",")}`) + .join(" "); + console.log(`\n${c.dim}Use command below to install all detected skills:${c.reset}\n`); + console.log(`npx skillman add ${sources}\n`); + } +} + +// Search Skills via API + +// Source: https://github.com/vercel-labs/skills/blob/main/src/find.ts#L27 + +const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh"; + +export interface SearchSkill { + name: string; + slug: string; + source: string; + installs: number; +} + +// Search via API +export async function searchSkillsAPI(query: string): Promise { + try { + const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=1`; + const res = await fetch(url); + + if (!res.ok) return []; + + const data = (await res.json()) as { + skills: Array<{ + id: string; + name: string; + installs: number; + topSource: string | null; + }>; + }; + + return data.skills.map((skill) => ({ + name: skill.name, + slug: skill.id, + source: skill.topSource || "", + installs: skill.installs, + })); + } catch { + return []; + } +}