developer-stack-skills
Advanced tools
| --- | ||
| description: Frontend — React, Angular, TypeScript, TanStack Query, Vitest, Playwright | ||
| globs: ["**/*.tsx","**/*.jsx","**/*.ts","**/*.js","**/*.vue","**/*.svelte"] | ||
| alwaysApply: false | ||
| --- | ||
| Load and follow this skill file: | ||
| - ./frontend/SKILL.md |
| --- | ||
| description: Java & Spring Boot — Spring Boot 3, JPA, REST APIs, JUnit 5, Maven/Gradle | ||
| globs: ["**/*.java","**/*.kt","**/pom.xml","**/build.gradle","**/build.gradle.kts"] | ||
| alwaysApply: false | ||
| --- | ||
| Load and follow this skill file: | ||
| - ./java-spring/SKILL.md |
| --- | ||
| description: Project conventions — Git, commits, PRs, ADRs, naming, environment config | ||
| alwaysApply: true | ||
| --- | ||
| Load and follow this skill file: | ||
| - ./project-conventions/SKILL.md |
| --- | ||
| description: Python backend — FastAPI, Django, SQLAlchemy, Pydantic, pytest | ||
| globs: ["**/*.py","**/requirements*.txt","**/pyproject.toml","**/setup.py","**/Pipfile"] | ||
| alwaysApply: false | ||
| --- | ||
| Load and follow this skill file: | ||
| - ./python-backend/SKILL.md |
| --- | ||
| description: Testing — JUnit 5, pytest, Vitest, Testing Library, Playwright, Testcontainers | ||
| globs: ["**/*.test.ts","**/*.test.tsx","**/*.test.js","**/*.test.jsx","**/*.spec.ts","**/*.spec.js","**/*.spec.jsx","**/test/**","**/tests/**","**/__tests__/**"] | ||
| alwaysApply: false | ||
| --- | ||
| Load and follow this skill file: | ||
| - ./testing/SKILL.md |
| --- | ||
| description: Add REST API endpoint following stack and REST conventions | ||
| argument-hint: [METHOD /path description] | ||
| allowed-tools: Read, Write, Edit, Bash, Grep, Glob | ||
| --- | ||
| Add endpoint: $ARGUMENTS | ||
| 1. Detect stack: | ||
| - `pom.xml` or `build.gradle` → Java/Spring Boot (@RestController pattern) | ||
| - `pyproject.toml` or `requirements.txt` → Python/FastAPI (APIRouter pattern) | ||
| - `package.json` without pom.xml/build.gradle → TypeScript/Node.js (Express/NestJS/Fastify — follow existing router pattern in project) | ||
| - If no stack detected, state so and ask user to specify framework before proceeding. | ||
| 2. Read existing endpoints in the project to understand current patterns, naming, and error handling before writing anything. | ||
| 3. Design the endpoint following REST conventions: | ||
| - Correct HTTP method: GET (read), POST (create → 201), PUT/PATCH (update → 200), DELETE (204) | ||
| - Request/response DTOs: Java records with `@Valid`, Pydantic models with field validators | ||
| - Input validated at controller/router layer — never in service layer | ||
| 4. Implement in layers — do not skip: | ||
| - Controller/Router: thin, delegates all logic to service | ||
| - Service: business logic, throws domain-specific exceptions (never generic ones) | ||
| - Repository: data access only (create if DB interaction needed) | ||
| 5. Write integration test covering: | ||
| - Happy path (correct status and response body) | ||
| - Validation failure (400 with meaningful error message) | ||
| - Not-found case (404) if endpoint accepts an ID | ||
| 6. Check if OpenAPI/Swagger docs are auto-generated or manual. Update if manual. | ||
| 7. Run existing tests to confirm no regressions. |
| --- | ||
| description: Audit dependencies for outdated versions and known vulnerabilities | ||
| allowed-tools: Bash, Read, Glob | ||
| --- | ||
| Audit all project dependencies for outdated versions and security vulnerabilities. | ||
| Detect package managers from project root files: | ||
| - `package.json` → npm / yarn / pnpm / bun | ||
| - `pyproject.toml` or `requirements*.txt` → pip / uv / poetry | ||
| - `pom.xml` → Maven | ||
| - `build.gradle` or `build.gradle.kts` → Gradle | ||
| For each detected package manager, run the appropriate audit command and check for: | ||
| 1. **Security vulnerabilities** — packages with known CVEs (highest priority) | ||
| 2. **Major version updates** — breaking changes available | ||
| 3. **Minor/patch updates** — non-breaking updates available | ||
| 4. **Deprecated packages** — no longer maintained, replacement needed | ||
| Audit commands by ecosystem: | ||
| - npm: `npm audit --json` | ||
| - yarn: `yarn audit --json` | ||
| - pnpm: `pnpm audit --json` | ||
| - pip/uv: `pip-audit` or `safety check` if available, otherwise `pip list --outdated` | ||
| - poetry: `poetry show --outdated` | ||
| - Maven: `mvn versions:display-dependency-updates` | ||
| - Gradle: `gradle dependencyUpdates` (if plugin present) or check `build.gradle` for version declarations | ||
| Report findings as a prioritized table: | ||
| | Package | Current | Latest | Severity | Action | | ||
| |---------|---------|--------|----------|--------| | ||
| Suggest the exact update command(s) for each package manager found. Flag any package where upgrading requires migration steps. Do not update anything without explicit confirmation. |
| --- | ||
| description: Implement feature following project stack conventions | ||
| argument-hint: [feature-description] | ||
| allowed-tools: Read, Write, Edit, Bash, Grep, Glob | ||
| --- | ||
| Implement: $ARGUMENTS | ||
| 1. Detect project stack by checking root files: | ||
| - `pom.xml` or `build.gradle` → Java/Spring Boot | ||
| - `pyproject.toml`, `requirements.txt`, or `setup.py` → Python/FastAPI | ||
| - `package.json` → TypeScript/JavaScript (React or Angular) | ||
| 2. Read existing code to understand current architecture, naming conventions, and patterns before writing anything. | ||
| 3. State assumptions and create a concise implementation plan. List files to create or modify. | ||
| 4. Implement following the skill conventions already loaded for this stack: | ||
| - Java: thin controllers, business logic in service layer, DTOs via records, constructor injection only | ||
| - Python: Pydantic models for I/O, async handlers, pydantic-settings for config | ||
| - TypeScript: functional components, TanStack Query for server state, no fetch inside components | ||
| 5. Write tests alongside implementation: | ||
| - Unit test for service/business logic | ||
| - Integration test for any new API endpoint | ||
| - Arrange-Act-Assert structure, one concept per test | ||
| 6. After implementing, list all files changed and confirm the implementation compiles or type-checks. |
| --- | ||
| description: Review current branch changes against project conventions | ||
| allowed-tools: Bash(git:*), Read, Grep | ||
| --- | ||
| Review changes in the current branch. | ||
| First, detect the repository's default branch by running: !`git remote show origin` | ||
| Then get the changed files and diff summary against that branch. Use `git diff <default-branch>...HEAD --name-only` and `git diff <default-branch>...HEAD --stat`. If the upstream tracking branch is set, prefer `git diff @{upstream}...HEAD`. | ||
| For each changed file review against: | ||
| **Code quality:** | ||
| - Follows stack conventions (layering, naming, injection patterns) | ||
| - No business logic leaked into wrong layer (controller, component) | ||
| - Error handling present and specific — no bare `catch (Exception e)` or bare `except:` | ||
| - No `any` in TypeScript, no mutable default args in Python | ||
| **Testing:** | ||
| - Tests added or updated for every behavior change | ||
| - At least one happy path and one failure path per change | ||
| - Tests assert behaviour, not implementation details | ||
| **project-conventions checklist:** | ||
| - No hardcoded secrets or credentials | ||
| - No `TODO` without a ticket ID | ||
| - No commented-out code | ||
| - PR is under 400 lines (warn if larger) | ||
| - Commit messages follow Conventional Commits format | ||
| Report findings grouped by: **Critical** (must fix) / **Warning** (should fix) / **Suggestion** (nice to have). Include file and line references. |
| --- | ||
| description: Write tests for a file or class following testing conventions | ||
| argument-hint: [file-or-class] | ||
| allowed-tools: Read, Write, Edit, Bash, Grep, Glob | ||
| --- | ||
| Write tests for: $ARGUMENTS | ||
| 1. Read the target file thoroughly to understand all public methods, edge cases, and error paths. | ||
| 2. Detect test framework from project: | ||
| - Java → JUnit 5 + Mockito (check pom.xml or build.gradle) | ||
| - Python → pytest (check pyproject.toml) | ||
| - TypeScript/React → Vitest + Testing Library | ||
| - Angular → Jasmine + Karma | ||
| 3. For each public method or exported function, write: | ||
| - At least one happy path test | ||
| - At least one failure or edge case test | ||
| - Test behaviour not implementation — assert on outputs and side effects, not internal calls | ||
| 4. Follow Arrange-Act-Assert structure. Separate each section with a blank line. | ||
| 5. Mock only external dependencies (DB sessions, HTTP clients, filesystem). Never mock your own code. | ||
| 6. Use descriptive test names: `method_WhenCondition_ExpectedResult` (Java) or `test_does_x_when_y` (Python) or `should do X when Y` (TypeScript). | ||
| 7. Place test file in the correct location per project structure. Run existing tests to confirm nothing broke. |
| # Developer Stack Skills | ||
| Load and follow these skill files before starting work: | ||
| - node_modules/developer-stack-skills/java-spring/SKILL.md | ||
| - node_modules/developer-stack-skills/testing/SKILL.md | ||
| - node_modules/developer-stack-skills/project-conventions/SKILL.md |
| #!/usr/bin/env node | ||
| // Matches package install commands across supported ecosystems | ||
| const INSTALL_PATTERN = /\b(?:pip3?\s+install|uv\s+(?:pip\s+install|add)|poetry\s+add|npm\s+(?:install|i)\b|yarn\s+add|pnpm\s+add|bun\s+add|npx\s+\S+@(?:latest|\d))/i; | ||
| const chunks = []; | ||
| process.stdin.on("data", (chunk) => chunks.push(chunk)); | ||
| process.stdin.on("end", () => { | ||
| let input = {}; | ||
| try { input = JSON.parse(Buffer.concat(chunks).toString()); } catch {} | ||
| const command = input.tool_input?.command || ""; | ||
| if (INSTALL_PATTERN.test(command)) { | ||
| process.stdout.write(JSON.stringify({ | ||
| continue: true, | ||
| systemMessage: "[developer-stack-skills] freshdeps: Verify this is the latest stable version before installing. Check for known vulnerabilities or deprecated releases.", | ||
| })); | ||
| } | ||
| process.exit(0); | ||
| }); |
| #!/usr/bin/env node | ||
| const path = require("path"); | ||
| const REMINDERS = { | ||
| java: "java-spring: Constructor injection only. DTOs via Java records. FetchType.LAZY for all JPA associations. @ControllerAdvice for exceptions. No business logic in controllers.", | ||
| kotlin: "java-spring (Kotlin): Constructor injection only. Data classes for DTOs. @ControllerAdvice for exceptions. No business logic in controllers.", | ||
| python: "python-backend: Pydantic models for all I/O. Async handlers in FastAPI. Never hardcode secrets — use pydantic-settings. pytest with fixtures, not unittest.", | ||
| angular: "frontend (Angular): Standalone components + OnPush. Signals for reactive state. Return Observable from services — never subscribe inside services.", | ||
| frontend: "frontend: Functional components. Never use `any` in TypeScript. TanStack Query for server state. No useEffect for data fetching. Never fetch directly in components.", | ||
| test: "testing: Arrange-Act-Assert. One concept per test. Test behaviour not implementation. Mock only external deps (DB, HTTP) — not your own code.", | ||
| config: "project-conventions: Never commit secrets. .env.example must list all required keys (values redacted). Use pydantic-settings or Spring @Value for env vars.", | ||
| sql: "project-conventions: SQL migrations → timestamp prefix: YYYYMMDD_description.sql. Never modify existing migrations — add a new one instead.", | ||
| }; | ||
| function getReminder(filePath) { | ||
| const name = path.basename(filePath); | ||
| // Test files — checked first so test files don't get source reminder | ||
| const isJavaTest = /Tests?\.java$/.test(name) || /IT\.java$/.test(name) || /ITest\.java$/.test(name); | ||
| const isPythonTest = /^test_/.test(name) || /_test\.py$/.test(name); | ||
| const isGenericTest = /\.(test|spec)\.(ts|tsx|js|jsx|py|java)$/.test(name); | ||
| if (isJavaTest || isPythonTest || isGenericTest) return REMINDERS.test; | ||
| // Source files by extension | ||
| if (/\.java$/.test(name)) return REMINDERS.java; | ||
| if (/\.kt$/.test(name)) return REMINDERS.kotlin; | ||
| if (/\.py$/.test(name)) return REMINDERS.python; | ||
| // Angular-specific TypeScript files before generic TS catch-all | ||
| if (/\.(component|service|module|guard|pipe|interceptor|directive|resolver)\.ts$/.test(name)) return REMINDERS.angular; | ||
| if (/\.(ts|tsx|js|jsx)$/.test(name)) return REMINDERS.frontend; | ||
| // Config / secrets — skip .env.example (safe template) | ||
| if (/^\.env(\.|$)/.test(name) && !name.endsWith(".example")) return REMINDERS.config; | ||
| // SQL migrations | ||
| if (/\.sql$/.test(name)) return REMINDERS.sql; | ||
| return null; | ||
| } | ||
| const chunks = []; | ||
| process.stdin.on("data", (chunk) => chunks.push(chunk)); | ||
| process.stdin.on("end", () => { | ||
| let input = {}; | ||
| try { input = JSON.parse(Buffer.concat(chunks).toString()); } catch {} | ||
| const filePath = input.tool_input?.file_path || ""; | ||
| const reminder = getReminder(filePath); | ||
| if (reminder) { | ||
| process.stdout.write(JSON.stringify({ | ||
| continue: true, | ||
| systemMessage: `[developer-stack-skills] ${reminder}`, | ||
| })); | ||
| } | ||
| process.exit(0); | ||
| }); |
| const path = require("path"); | ||
| const fsp = require("fs/promises"); | ||
| const PACKAGE_NAME = "developer-stack-skills"; | ||
| const SKILL_META = { | ||
| "java-spring": { | ||
| description: "Java & Spring Boot 3 — JPA, REST APIs, JUnit 5, Mockito, Maven/Gradle", | ||
| globs: ["**/*.java", "**/*.kt", "**/pom.xml", "**/build.gradle", "**/build.gradle.kts"], | ||
| }, | ||
| "python-backend": { | ||
| description: "Python backend — FastAPI, Django, SQLAlchemy 2.x, Pydantic v2, pytest", | ||
| globs: ["**/*.py", "**/requirements*.txt", "**/pyproject.toml", "**/setup.py", "**/Pipfile"], | ||
| }, | ||
| frontend: { | ||
| description: "Frontend — React 18+, Angular 17+, TypeScript, TanStack Query, Vitest, Playwright", | ||
| globs: ["**/*.tsx", "**/*.jsx", "**/*.ts", "**/*.js", "**/*.vue", "**/*.svelte", "**/package.json"], | ||
| }, | ||
| testing: { | ||
| description: "Testing — JUnit 5, pytest, Vitest, Testing Library, Playwright, Testcontainers", | ||
| globs: ["**/*.test.*", "**/*.spec.*", "**/test/**", "**/tests/**", "**/__tests__/**"], | ||
| }, | ||
| "project-conventions": { | ||
| description: "Project conventions — Git flow, Conventional Commits, PR process, ADRs, naming, env config", | ||
| globs: [], | ||
| }, | ||
| }; | ||
| const SKILL_NAMES = Object.keys(SKILL_META); | ||
| const TOOLS = [ | ||
| { | ||
| name: "list_available_skills", | ||
| description: "List all developer stack skills with descriptions and applicable file patterns.", | ||
| inputSchema: { type: "object", properties: {}, required: [] }, | ||
| annotations: { readOnlyHint: true }, | ||
| }, | ||
| { | ||
| name: "get_skill", | ||
| description: "Get full SKILL.md content for a technology stack. Load this before writing code in that stack.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| stack_name: { | ||
| type: "string", | ||
| enum: SKILL_NAMES, | ||
| description: "Stack to retrieve: java-spring, python-backend, frontend, testing, or project-conventions.", | ||
| }, | ||
| }, | ||
| required: ["stack_name"], | ||
| }, | ||
| annotations: { readOnlyHint: true }, | ||
| }, | ||
| { | ||
| name: "get_conventions", | ||
| description: "Get project-wide conventions: Git branching, Conventional Commits, PR process, naming rules, ADRs, env config.", | ||
| inputSchema: { type: "object", properties: {}, required: [] }, | ||
| annotations: { readOnlyHint: true }, | ||
| }, | ||
| { | ||
| name: "detect_stack", | ||
| description: "Detect the recommended skill to load from a file path. Returns the skill name and a ready-to-use get_skill call.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| file_path: { | ||
| type: "string", | ||
| description: "File path to analyze, e.g. src/UserService.java or app/routes/users.py.", | ||
| }, | ||
| }, | ||
| required: ["file_path"], | ||
| }, | ||
| annotations: { readOnlyHint: true }, | ||
| }, | ||
| ]; | ||
| function getPackageRoot() { | ||
| return path.resolve(__dirname, ".."); | ||
| } | ||
| function getVersion() { | ||
| return require(path.join(getPackageRoot(), "package.json")).version; | ||
| } | ||
| async function readSkillFile(skillName) { | ||
| const skillPath = path.join(getPackageRoot(), skillName, "SKILL.md"); | ||
| try { | ||
| return await fsp.readFile(skillPath, "utf8"); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function detectStack(filePath) { | ||
| const name = path.basename(filePath); | ||
| const isTestFile = | ||
| /\.(test|spec)\.(java|py|ts|tsx|js|jsx)$/.test(name) || | ||
| /Tests?\.java$/.test(name) || | ||
| /IT\.java$/.test(name) || | ||
| /^test_/.test(name) || | ||
| /_test\.py$/.test(name); | ||
| if (isTestFile) return "testing"; | ||
| if (/\.(java|kt)$/.test(name) || /^(pom\.xml|build\.gradle(\.kts)?)$/.test(name)) return "java-spring"; | ||
| if (/\.py$/.test(name) || /^(pyproject\.toml|requirements.*\.txt|setup\.py|Pipfile)$/.test(name)) return "python-backend"; | ||
| if (/\.(component|service|module|guard|pipe|directive|interceptor|resolver)\.ts$/.test(name)) return "frontend"; | ||
| if (/\.(tsx|jsx|ts|js|vue|svelte)$/.test(name) || /^package\.json$/.test(name)) return "frontend"; | ||
| return "project-conventions"; | ||
| } | ||
| async function handleTool(name, args) { | ||
| if (name === "list_available_skills") { | ||
| const skills = SKILL_NAMES.map((skillName) => ({ | ||
| name: skillName, | ||
| description: SKILL_META[skillName].description, | ||
| applies_to: SKILL_META[skillName].globs.slice(0, 3).join(", ") || "always", | ||
| })); | ||
| return { content: [{ type: "text", text: JSON.stringify(skills, null, 2) }] }; | ||
| } | ||
| if (name === "get_skill") { | ||
| const { stack_name } = args; | ||
| if (!SKILL_META[stack_name]) { | ||
| return { | ||
| content: [{ type: "text", text: `Unknown skill: ${stack_name}. Available: ${SKILL_NAMES.join(", ")}` }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| const content = await readSkillFile(stack_name); | ||
| if (!content) { | ||
| return { | ||
| content: [{ type: "text", text: `Skill file not found: ${stack_name}` }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| return { content: [{ type: "text", text: content }] }; | ||
| } | ||
| if (name === "get_conventions") { | ||
| const content = await readSkillFile("project-conventions"); | ||
| if (!content) { | ||
| return { | ||
| content: [{ type: "text", text: "project-conventions skill file not found." }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| return { content: [{ type: "text", text: content }] }; | ||
| } | ||
| if (name === "detect_stack") { | ||
| const { file_path } = args; | ||
| const stack = detectStack(file_path); | ||
| const meta = SKILL_META[stack]; | ||
| return { | ||
| content: [{ | ||
| type: "text", | ||
| text: JSON.stringify({ | ||
| file_path, | ||
| recommended_skill: stack, | ||
| description: meta.description, | ||
| next_step: `Call get_skill with stack_name: "${stack}"`, | ||
| }, null, 2), | ||
| }], | ||
| }; | ||
| } | ||
| return { | ||
| content: [{ type: "text", text: `Unknown tool: ${name}` }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| async function runMcpServer() { | ||
| // Lazy-load SDK so pure functions (detectStack, etc.) work without it installed | ||
| const { Server } = require("@modelcontextprotocol/sdk/server/index.js"); | ||
| const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js"); | ||
| const { ListToolsRequestSchema, CallToolRequestSchema } = require("@modelcontextprotocol/sdk/types.js"); | ||
| const server = new Server( | ||
| { name: PACKAGE_NAME, version: getVersion() }, | ||
| { | ||
| capabilities: { tools: {} }, | ||
| instructions: "Use list_available_skills first to discover what stacks are available. Use detect_stack to identify which skill applies to a specific file. Use get_skill to load full conventions before writing code.", | ||
| }, | ||
| ); | ||
| server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); | ||
| server.setRequestHandler(CallToolRequestSchema, async (request) => { | ||
| const { name, arguments: args = {} } = request.params; | ||
| return handleTool(name, args); | ||
| }); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| process.stderr.write(`[${PACKAGE_NAME}] MCP server started (stdio)\n`); | ||
| } | ||
| module.exports = { runMcpServer, detectStack, SKILL_META, SKILL_NAMES }; |
@@ -24,2 +24,8 @@ #!/usr/bin/env node | ||
| if (args.command === "serve") { | ||
| const { runMcpServer } = require("../lib/mcp-server"); | ||
| await runMcpServer(); | ||
| return; | ||
| } | ||
| if (["version", "--version", "-v"].includes(args.command)) { | ||
@@ -26,0 +32,0 @@ printVersion(); |
+30
-0
| # Changelog | ||
| ## 2.0.0 - 2026-05-16 | ||
| Added: | ||
| - MCP server (`developer-stack-skills serve`) with four tools: `list_available_skills`, `get_skill`, `get_conventions`, `detect_stack` | ||
| - `serve` command in CLI to start MCP server over stdio | ||
| - Claude Code slash commands (`commands/`): `implement-feature`, `write-tests`, `review-pr`, `check-deps`, `add-endpoint` | ||
| - Claude Code hooks (`hooks/`): `pre-write.js` injects per-stack reminders on file writes; `pre-bash.js` warns on package install commands | ||
| - `.claude/rules/` per-stack rule files for automatic skill loading in Claude Code | ||
| - Cursor rules split into per-stack `.mdc` files: `developer-stack-skills-frontend.mdc`, `developer-stack-skills-java-spring.mdc`, `developer-stack-skills-python-backend.mdc`, `developer-stack-skills-project-conventions.mdc`, `developer-stack-skills-testing.mdc` | ||
| - Roocode migrated from `.roo/config.yml` to `.roo/rules/developer-stack-skills.md` | ||
| - `.gitignore` added to package | ||
| Changed: | ||
| - `@modelcontextprotocol/sdk` promoted to runtime dependency (was implicit dev dep) | ||
| - `files` in `package.json` now includes `commands/` and `hooks/` | ||
| Removed: | ||
| - `.roo/config.yml` replaced by `.roo/rules/developer-stack-skills.md` | ||
| - `.cursor/rules/developer-stack-skills.mdc` (single file) replaced by per-stack `.mdc` files | ||
| Notes: | ||
| - MCP server exposes all five skills as tools — agents can call `detect_stack` with a file path and get back which skill to load | ||
| - Hooks require Claude Code; Cursor and Copilot integration remains config-file based | ||
| - All MCP tools are read-only (`readOnlyHint: true`) | ||
| ## 1.2.1 - 2026-05-15 | ||
@@ -15,2 +44,3 @@ | ||
| - README now documents explicit post-install configuration flow, `--foreground-scripts` requirement for visible npm lifecycle prompts, and updated command examples | ||
| - README now clarifies why global install still asks for project directory and how global vs local install differ | ||
@@ -17,0 +47,0 @@ Notes: |
@@ -0,1 +1,2 @@ | ||
| <!-- developer-stack-skills:start --> | ||
| Follow these skill files before producing code or process guidance: | ||
@@ -6,1 +7,2 @@ | ||
| - node_modules/developer-stack-skills/project-conventions/SKILL.md | ||
| <!-- developer-stack-skills:end --> |
+445
-48
@@ -12,2 +12,3 @@ const fsp = require("fs/promises"); | ||
| const MODES = ["copy", "symlink"]; | ||
| const HOOKS_DIR = "hooks"; | ||
@@ -22,2 +23,36 @@ const SKILLS = [ | ||
| const CONVENTIONS_RULE_CONFIG = { | ||
| skillName: "project-conventions", | ||
| description: "Project conventions — Git, commits, PRs, ADRs, naming, environment config", | ||
| globs: [], | ||
| alwaysApply: true, | ||
| }; | ||
| const RULE_CONFIGS = [ | ||
| { | ||
| skillName: "java-spring", | ||
| description: "Java & Spring Boot — Spring Boot 3, JPA, REST APIs, JUnit 5, Maven/Gradle", | ||
| globs: ["**/*.java", "**/*.kt", "**/pom.xml", "**/build.gradle", "**/build.gradle.kts"], | ||
| }, | ||
| { | ||
| skillName: "python-backend", | ||
| description: "Python backend — FastAPI, Django, SQLAlchemy, Pydantic, pytest", | ||
| globs: ["**/*.py", "**/requirements*.txt", "**/pyproject.toml", "**/setup.py", "**/Pipfile"], | ||
| }, | ||
| { | ||
| skillName: "frontend", | ||
| description: "Frontend — React, Angular, TypeScript, TanStack Query, Vitest, Playwright", | ||
| globs: ["**/*.tsx", "**/*.jsx", "**/*.ts", "**/*.js", "**/*.vue", "**/*.svelte"], | ||
| }, | ||
| { | ||
| skillName: "testing", | ||
| description: "Testing — JUnit 5, pytest, Vitest, Testing Library, Playwright, Testcontainers", | ||
| globs: [ | ||
| "**/*.test.ts", "**/*.test.tsx", "**/*.test.js", "**/*.test.jsx", | ||
| "**/*.spec.ts", "**/*.spec.js", "**/*.spec.jsx", | ||
| "**/test/**", "**/tests/**", "**/__tests__/**", | ||
| ], | ||
| }, | ||
| ]; | ||
| function detectPlatform() { | ||
@@ -130,2 +165,3 @@ switch (process.platform) { | ||
| console.log(" uninstall remove installed skills and agent config entries"); | ||
| console.log(" serve start MCP server on stdio"); | ||
| console.log(" version print package version"); | ||
@@ -274,2 +310,20 @@ console.log(" help print this help"); | ||
| function getHooksDestPath(installRoot) { | ||
| return path.join(installRoot, HOOKS_DIR); | ||
| } | ||
| function buildHookCommand(hooksDir, scriptName) { | ||
| return `node ${JSON.stringify(path.join(hooksDir, scriptName))}`; | ||
| } | ||
| function isOurHookEntry(entry) { | ||
| return (entry.hooks || []).some( | ||
| (h) => typeof h.command === "string" && h.command.includes(PACKAGE_NAME), | ||
| ); | ||
| } | ||
| function removeOurHookEntries(hookArray) { | ||
| return (hookArray || []).filter((entry) => !isOurHookEntry(entry)); | ||
| } | ||
| function quoteYamlString(value) { | ||
@@ -415,13 +469,27 @@ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; | ||
| async function configureClaude(projectDir, skillPaths, dryRun = false) { | ||
| async function configureClaude(projectDir, conventionsPath, context, dryRun = false) { | ||
| const filePath = path.join(projectDir, "CLAUDE.md"); | ||
| const current = await readIfExists(filePath); | ||
| const body = [ | ||
| "Load these skill files before starting work:", | ||
| const lines = []; | ||
| if (context && (context.description || context.testCmd || context.buildCmd)) { | ||
| lines.push("## Project context", ""); | ||
| if (context.description) lines.push(context.description, ""); | ||
| if (context.testCmd) lines.push(`- Test: \`${context.testCmd}\``); | ||
| if (context.buildCmd) lines.push(`- Build: \`${context.buildCmd}\``); | ||
| lines.push(""); | ||
| } | ||
| lines.push( | ||
| "Load this skill file before starting work:", | ||
| "", | ||
| ...skillPaths.map((skillPath) => `- ${skillPath}`), | ||
| `- ${conventionsPath}`, | ||
| "", | ||
| "Stack skills (java-spring, python-backend, frontend, testing) load contextually via `.claude/rules/`.", | ||
| "", | ||
| "After loading, create concise implementation plan, state assumptions, then implement requested changes.", | ||
| ].join("\n"); | ||
| ); | ||
| const body = lines.join("\n"); | ||
| const next = replaceManagedBlock(current, body, "html"); | ||
@@ -432,25 +500,244 @@ await writeFileWithDirs(filePath, next, dryRun); | ||
| async function configureCursor(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".cursor", "rules", "developer-stack-skills.mdc"); | ||
| const body = [ | ||
| "---", | ||
| "description: Load installed developer-stack-skills files before coding", | ||
| "globs: []", | ||
| "alwaysApply: false", | ||
| "---", | ||
| "", | ||
| "Read and follow these skill files before starting work:", | ||
| "", | ||
| ...skillPaths.map((skillPath) => `- ${skillPath}`), | ||
| ].join("\n"); | ||
| function buildRuleFileContent(skillPath, config) { | ||
| const alwaysApply = config.alwaysApply === true; | ||
| const lines = ["---", `description: ${config.description}`]; | ||
| if (!alwaysApply && config.globs && config.globs.length > 0) { | ||
| lines.push(`globs: ${JSON.stringify(config.globs)}`); | ||
| } | ||
| lines.push(`alwaysApply: ${alwaysApply}`, "---", "", "Load and follow this skill file:", "", `- ${skillPath}`, ""); | ||
| return lines.join("\n"); | ||
| } | ||
| await writeFileWithDirs(filePath, body, dryRun); | ||
| async function configureClaudeRules(projectDir, installRoot, dryRun = false) { | ||
| const rulesDir = path.join(projectDir, ".claude", "rules"); | ||
| const configured = []; | ||
| for (const config of RULE_CONFIGS) { | ||
| const skillPath = path.join(installRoot, config.skillName, "SKILL.md"); | ||
| const filePath = path.join(rulesDir, `developer-stack-skills-${config.skillName}.md`); | ||
| await writeFileWithDirs(filePath, buildRuleFileContent(skillPath, config), dryRun); | ||
| configured.push(filePath); | ||
| } | ||
| return configured; | ||
| } | ||
| async function configureClaudeCommands(projectDir, packageRoot, dryRun = false) { | ||
| const sourceDir = path.join(packageRoot, "commands"); | ||
| const destDir = path.join(projectDir, ".claude", "commands"); | ||
| const configured = []; | ||
| let files; | ||
| try { | ||
| files = await fsp.readdir(sourceDir); | ||
| } catch { | ||
| return configured; | ||
| } | ||
| await ensureDir(destDir, dryRun); | ||
| for (const file of files) { | ||
| if (!file.endsWith(".md")) continue; | ||
| const sourcePath = path.join(sourceDir, file); | ||
| const destPath = path.join(destDir, `developer-stack-skills-${file}`); | ||
| if (!dryRun) await fsp.copyFile(sourcePath, destPath); | ||
| configured.push(destPath); | ||
| } | ||
| return configured; | ||
| } | ||
| async function unconfigureClaudeCommands(projectDir, dryRun = false) { | ||
| const commandsDir = path.join(projectDir, ".claude", "commands"); | ||
| let files; | ||
| try { | ||
| files = await fsp.readdir(commandsDir); | ||
| } catch { | ||
| return []; | ||
| } | ||
| const removed = []; | ||
| for (const file of files) { | ||
| if (file.startsWith("developer-stack-skills-") && file.endsWith(".md")) { | ||
| const filePath = path.join(commandsDir, file); | ||
| await removePath(filePath, dryRun); | ||
| removed.push(filePath); | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| function buildMcpCommand(packageInstallType) { | ||
| return packageInstallType === "global" | ||
| ? { command: "developer-stack-skills", args: ["serve"] } | ||
| : { command: "npx", args: ["developer-stack-skills", "serve"] }; | ||
| } | ||
| async function configureMcp(projectDir, packageInstallType, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".claude", "mcp.json"); | ||
| const current = await readIfExists(filePath); | ||
| let config = {}; | ||
| if (current.trim()) { | ||
| try { | ||
| config = JSON.parse(current); | ||
| } catch { | ||
| process.stderr.write(`[${PACKAGE_NAME}] Warning: ${filePath} has invalid JSON — skipping MCP update to avoid data loss\n`); | ||
| return filePath; | ||
| } | ||
| } | ||
| if (!config.mcpServers) config.mcpServers = {}; | ||
| const { command, args } = buildMcpCommand(packageInstallType); | ||
| config.mcpServers[PACKAGE_NAME] = { command, args, type: "stdio" }; | ||
| await writeFileWithDirs(filePath, JSON.stringify(config, null, 2) + "\n", dryRun); | ||
| return filePath; | ||
| } | ||
| async function unconfigureMcp(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".claude", "mcp.json"); | ||
| const current = await readIfExists(filePath); | ||
| if (!current.trim()) return filePath; | ||
| let config; | ||
| try { config = JSON.parse(current); } catch { return filePath; } | ||
| if (config.mcpServers) { | ||
| delete config.mcpServers[PACKAGE_NAME]; | ||
| if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers; | ||
| } | ||
| if (Object.keys(config).length === 0) { | ||
| await removePath(filePath, dryRun); | ||
| } else { | ||
| await writeFileWithDirs(filePath, JSON.stringify(config, null, 2) + "\n", dryRun); | ||
| } | ||
| return filePath; | ||
| } | ||
| async function unconfigureClaudeRules(projectDir, dryRun = false) { | ||
| const rulesDir = path.join(projectDir, ".claude", "rules"); | ||
| let files; | ||
| try { | ||
| files = await fsp.readdir(rulesDir); | ||
| } catch { | ||
| return []; | ||
| } | ||
| const removed = []; | ||
| for (const file of files) { | ||
| if (file.startsWith("developer-stack-skills-") && file.endsWith(".md")) { | ||
| const filePath = path.join(rulesDir, file); | ||
| await removePath(filePath, dryRun); | ||
| removed.push(filePath); | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| async function installHooks({ packageRoot, installRoot, mode, platform, dryRun = false }) { | ||
| const sourcePath = path.join(packageRoot, HOOKS_DIR); | ||
| const destPath = getHooksDestPath(installRoot); | ||
| await removePath(destPath, dryRun); | ||
| if (mode === "copy") { | ||
| if (!dryRun) await fsp.cp(sourcePath, destPath, { recursive: true }); | ||
| } else { | ||
| const symlinkType = platform === "windows" ? "junction" : "dir"; | ||
| if (!dryRun) await fsp.symlink(sourcePath, destPath, symlinkType); | ||
| } | ||
| return { sourcePath, destPath }; | ||
| } | ||
| async function configureClaudeHooks(projectDir, hooksDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".claude", "settings.json"); | ||
| const current = await readIfExists(filePath); | ||
| let settings = {}; | ||
| if (current.trim()) { | ||
| try { | ||
| settings = JSON.parse(current); | ||
| } catch { | ||
| process.stderr.write(`[${PACKAGE_NAME}] Warning: ${filePath} has invalid JSON — skipping hooks update to avoid data loss\n`); | ||
| return filePath; | ||
| } | ||
| } | ||
| if (settings.PreToolUse) { | ||
| settings.PreToolUse = removeOurHookEntries(settings.PreToolUse); | ||
| if (settings.PreToolUse.length === 0) delete settings.PreToolUse; | ||
| } | ||
| if (!settings.PreToolUse) settings.PreToolUse = []; | ||
| settings.PreToolUse.push( | ||
| { | ||
| matcher: "Write|Edit", | ||
| hooks: [{ type: "command", command: buildHookCommand(hooksDir, "pre-write.js"), timeout: 10 }], | ||
| }, | ||
| { | ||
| matcher: "Bash", | ||
| hooks: [{ type: "command", command: buildHookCommand(hooksDir, "pre-bash.js"), timeout: 10 }], | ||
| }, | ||
| ); | ||
| await writeFileWithDirs(filePath, JSON.stringify(settings, null, 2) + "\n", dryRun); | ||
| return filePath; | ||
| } | ||
| async function unconfigureClaudeHooks(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".claude", "settings.json"); | ||
| const current = await readIfExists(filePath); | ||
| if (!current.trim()) return filePath; | ||
| let settings; | ||
| try { settings = JSON.parse(current); } catch { return filePath; } | ||
| if (settings.PreToolUse) { | ||
| settings.PreToolUse = removeOurHookEntries(settings.PreToolUse); | ||
| if (settings.PreToolUse.length === 0) delete settings.PreToolUse; | ||
| } | ||
| if (Object.keys(settings).length === 0) { | ||
| await removePath(filePath, dryRun); | ||
| } else { | ||
| await writeFileWithDirs(filePath, JSON.stringify(settings, null, 2) + "\n", dryRun); | ||
| } | ||
| return filePath; | ||
| } | ||
| async function configureCursor(projectDir, installRoot, dryRun = false) { | ||
| const rulesDir = path.join(projectDir, ".cursor", "rules"); | ||
| const configured = []; | ||
| for (const config of RULE_CONFIGS) { | ||
| const skillPath = path.join(installRoot, config.skillName, "SKILL.md"); | ||
| const filePath = path.join(rulesDir, `developer-stack-skills-${config.skillName}.mdc`); | ||
| await writeFileWithDirs(filePath, buildRuleFileContent(skillPath, config), dryRun); | ||
| configured.push(filePath); | ||
| } | ||
| const conventionsPath = path.join(installRoot, CONVENTIONS_RULE_CONFIG.skillName, "SKILL.md"); | ||
| const conventionsFilePath = path.join(rulesDir, `developer-stack-skills-${CONVENTIONS_RULE_CONFIG.skillName}.mdc`); | ||
| await writeFileWithDirs(conventionsFilePath, buildRuleFileContent(conventionsPath, CONVENTIONS_RULE_CONFIG), dryRun); | ||
| configured.push(conventionsFilePath); | ||
| return configured; | ||
| } | ||
| async function configureCline(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".clinerules"); | ||
| const current = await readIfExists(filePath); | ||
| const next = upsertSkillsSection(current, skillPaths, (skillPath) => ` - ${quoteYamlString(skillPath)}`); | ||
| const body = [ | ||
| "Read and follow these skill files before starting work:", | ||
| "", | ||
| ...skillPaths.map((skillPath) => `- ${skillPath}`), | ||
| ].join("\n"); | ||
| const next = replaceManagedBlock(current, body, "html"); | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
@@ -461,7 +748,13 @@ return filePath; | ||
| async function configureRoocode(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".roo", "config.yml"); | ||
| const current = await readIfExists(filePath); | ||
| const next = upsertSkillsSection(current, skillPaths, (skillPath) => ` - path: ${quoteYamlString(skillPath)}`); | ||
| const filePath = path.join(projectDir, ".roo", "rules", "developer-stack-skills.md"); | ||
| const body = [ | ||
| "# Developer Stack Skills", | ||
| "", | ||
| "Load and follow these skill files before starting work:", | ||
| "", | ||
| ...skillPaths.map((skillPath) => `- ${skillPath}`), | ||
| "", | ||
| ].join("\n"); | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| await writeFileWithDirs(filePath, body, dryRun); | ||
| return filePath; | ||
@@ -488,3 +781,3 @@ } | ||
| async function configureAgents(agent, projectDir, installRoot, dryRun = false) { | ||
| async function configureAgents({ agent, projectDir, installRoot, context, generateCommands, configureMcpServer, packageInstallType, dryRun = false }) { | ||
| const skillPaths = buildSkillPaths(installRoot); | ||
@@ -494,5 +787,22 @@ const targets = getAgentTargets(agent); | ||
| const hooksDir = getHooksDestPath(installRoot); | ||
| for (const target of targets) { | ||
| if (target === "claude") { | ||
| configured.push({ agent: target, filePath: await configureClaude(projectDir, skillPaths, dryRun) }); | ||
| const conventionsPath = path.join(installRoot, "project-conventions", "SKILL.md"); | ||
| configured.push({ agent: target, filePath: await configureClaude(projectDir, conventionsPath, context, dryRun) }); | ||
| const ruleFiles = await configureClaudeRules(projectDir, installRoot, dryRun); | ||
| for (const ruleFilePath of ruleFiles) { | ||
| configured.push({ agent: "claude-rules", filePath: ruleFilePath }); | ||
| } | ||
| configured.push({ agent: "claude-hooks", filePath: await configureClaudeHooks(projectDir, hooksDir, dryRun) }); | ||
| if (generateCommands) { | ||
| const commandFiles = await configureClaudeCommands(projectDir, getPackageRoot(), dryRun); | ||
| for (const filePath of commandFiles) { | ||
| configured.push({ agent: "claude-commands", filePath }); | ||
| } | ||
| } | ||
| if (configureMcpServer) { | ||
| configured.push({ agent: "claude-mcp", filePath: await configureMcp(projectDir, packageInstallType, dryRun) }); | ||
| } | ||
| continue; | ||
@@ -502,3 +812,6 @@ } | ||
| if (target === "cursor") { | ||
| configured.push({ agent: target, filePath: await configureCursor(projectDir, skillPaths, dryRun) }); | ||
| const cursorFiles = await configureCursor(projectDir, installRoot, dryRun); | ||
| for (const filePath of cursorFiles) { | ||
| configured.push({ agent: target, filePath }); | ||
| } | ||
| continue; | ||
@@ -539,11 +852,25 @@ } | ||
| async function unconfigureCursor(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".cursor", "rules", "developer-stack-skills.mdc"); | ||
| await removePath(filePath, dryRun); | ||
| return filePath; | ||
| const rulesDir = path.join(projectDir, ".cursor", "rules"); | ||
| let files; | ||
| try { | ||
| files = await fsp.readdir(rulesDir); | ||
| } catch { | ||
| return []; | ||
| } | ||
| const removed = []; | ||
| for (const file of files) { | ||
| if (file.startsWith("developer-stack-skills-") && file.endsWith(".mdc")) { | ||
| const filePath = path.join(rulesDir, file); | ||
| await removePath(filePath, dryRun); | ||
| removed.push(filePath); | ||
| } | ||
| } | ||
| return removed; | ||
| } | ||
| async function unconfigureCline(projectDir, skillPaths, dryRun = false) { | ||
| async function unconfigureCline(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".clinerules"); | ||
| const current = await readIfExists(filePath); | ||
| const next = removeSkillsSectionItems(current, skillPaths, (skillPath) => ` - ${quoteYamlString(skillPath)}`); | ||
| const next = removeManagedBlock(current, "html"); | ||
@@ -558,12 +885,5 @@ if (next.trim()) { | ||
| async function unconfigureRoocode(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".roo", "config.yml"); | ||
| const current = await readIfExists(filePath); | ||
| const next = removeSkillsSectionItems(current, skillPaths, (skillPath) => ` - path: ${quoteYamlString(skillPath)}`); | ||
| if (next.trim()) { | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| } else { | ||
| await removePath(filePath, dryRun); | ||
| } | ||
| async function unconfigureRoocode(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".roo", "rules", "developer-stack-skills.md"); | ||
| await removePath(filePath, dryRun); | ||
| return filePath; | ||
@@ -586,3 +906,2 @@ } | ||
| async function unconfigureAgents(agent, projectDir, installRoot, dryRun = false) { | ||
| const skillPaths = buildSkillPaths(installRoot); | ||
| const targets = getAgentTargets(agent); | ||
@@ -594,2 +913,12 @@ const configured = []; | ||
| configured.push({ agent: target, filePath: await unconfigureClaude(projectDir, dryRun) }); | ||
| const removedRules = await unconfigureClaudeRules(projectDir, dryRun); | ||
| for (const ruleFilePath of removedRules) { | ||
| configured.push({ agent: "claude-rules", filePath: ruleFilePath }); | ||
| } | ||
| configured.push({ agent: "claude-hooks", filePath: await unconfigureClaudeHooks(projectDir, dryRun) }); | ||
| const removedCommands = await unconfigureClaudeCommands(projectDir, dryRun); | ||
| for (const filePath of removedCommands) { | ||
| configured.push({ agent: "claude-commands", filePath }); | ||
| } | ||
| configured.push({ agent: "claude-mcp", filePath: await unconfigureMcp(projectDir, dryRun) }); | ||
| continue; | ||
@@ -599,3 +928,6 @@ } | ||
| if (target === "cursor") { | ||
| configured.push({ agent: target, filePath: await unconfigureCursor(projectDir, dryRun) }); | ||
| const removedCursor = await unconfigureCursor(projectDir, dryRun); | ||
| for (const filePath of removedCursor) { | ||
| configured.push({ agent: target, filePath }); | ||
| } | ||
| continue; | ||
@@ -605,3 +937,3 @@ } | ||
| if (target === "cline") { | ||
| configured.push({ agent: target, filePath: await unconfigureCline(projectDir, skillPaths, dryRun) }); | ||
| configured.push({ agent: target, filePath: await unconfigureCline(projectDir, dryRun) }); | ||
| continue; | ||
@@ -611,3 +943,3 @@ } | ||
| if (target === "roocode") { | ||
| configured.push({ agent: target, filePath: await unconfigureRoocode(projectDir, skillPaths, dryRun) }); | ||
| configured.push({ agent: target, filePath: await unconfigureRoocode(projectDir, dryRun) }); | ||
| continue; | ||
@@ -624,2 +956,16 @@ } | ||
| async function collectProjectContext(prompt) { | ||
| console.log("\n[developer-stack-skills] Optional: add project context to CLAUDE.md (Enter to skip each)"); | ||
| const description = await prompt.ask("Project description (1 line): "); | ||
| const testCmd = await prompt.ask("Test command (e.g. mvn test, pytest, npm test): "); | ||
| const buildCmd = await prompt.ask("Build/start command (e.g. mvn spring-boot:run, uvicorn main:app): "); | ||
| const context = {}; | ||
| if (description.trim()) context.description = description.trim(); | ||
| if (testCmd.trim()) context.testCmd = testCmd.trim(); | ||
| if (buildCmd.trim()) context.buildCmd = buildCmd.trim(); | ||
| return Object.keys(context).length ? context : null; | ||
| } | ||
| async function collectAnswers(args, defaults = {}) { | ||
@@ -657,2 +1003,15 @@ const prompt = createPrompt(); | ||
| const normalizedAgent = normalizeAgent(agent) || "all"; | ||
| const isClaudeTarget = normalizedAgent === "all" || normalizedAgent === "claude"; | ||
| const projectContext = isClaudeTarget ? await collectProjectContext(prompt) : null; | ||
| let generateCommands = false; | ||
| let configureMcpServer = false; | ||
| if (isClaudeTarget) { | ||
| const commandsInput = await prompt.ask("\nGenerate Claude Code slash commands? [yes/no] (default: yes): "); | ||
| generateCommands = commandsInput === "" || /^y(es)?$/i.test(commandsInput.trim()); | ||
| const mcpInput = await prompt.ask("Configure MCP server (skills on-demand via tools)? [yes/no] (default: yes): "); | ||
| configureMcpServer = mcpInput === "" || /^y(es)?$/i.test(mcpInput.trim()); | ||
| } | ||
| return { | ||
@@ -662,2 +1021,5 @@ agent, | ||
| projectDir, | ||
| projectContext, | ||
| generateCommands, | ||
| configureMcpServer, | ||
| }; | ||
@@ -685,2 +1047,5 @@ } finally { | ||
| projectDir: path.resolve(args.projectDir || defaults.projectDir || process.cwd()), | ||
| projectContext: null, | ||
| generateCommands: true, | ||
| configureMcpServer: true, | ||
| }; | ||
@@ -749,3 +1114,15 @@ } | ||
| const configured = await configureAgents(selected.agent, selected.projectDir, installRoot, rawArgs.dryRun); | ||
| const hooksResult = await installHooks({ packageRoot, installRoot, mode: selected.mode, platform, dryRun: rawArgs.dryRun }); | ||
| console.log(`[${PACKAGE_NAME}] hooks ${rawArgs.dryRun ? "would install" : "installed"}: ${hooksResult.destPath}`); | ||
| const configured = await configureAgents({ | ||
| agent: selected.agent, | ||
| projectDir: selected.projectDir, | ||
| installRoot, | ||
| context: selected.projectContext, | ||
| generateCommands: selected.generateCommands, | ||
| configureMcpServer: selected.configureMcpServer, | ||
| packageInstallType, | ||
| dryRun: rawArgs.dryRun, | ||
| }); | ||
| for (const item of configured) { | ||
@@ -795,2 +1172,6 @@ console.log(`[${PACKAGE_NAME}] ${item.agent} config ${rawArgs.dryRun ? "would update" : "updated"}: ${item.filePath}`); | ||
| const hooksPath = getHooksDestPath(installRoot); | ||
| await removePath(hooksPath, rawArgs.dryRun); | ||
| console.log(`[${PACKAGE_NAME}] hooks ${rawArgs.dryRun ? "would remove" : "removed"}: ${hooksPath}`); | ||
| console.log(`[${PACKAGE_NAME}] ${rawArgs.dryRun ? "uninstall dry run complete" : "uninstall complete"}`); | ||
@@ -838,6 +1219,19 @@ | ||
| AGENTS, | ||
| CONVENTIONS_RULE_CONFIG, | ||
| HOOKS_DIR, | ||
| MODES, | ||
| RULE_CONFIGS, | ||
| SKILLS, | ||
| buildMcpCommand, | ||
| buildRuleFileContent, | ||
| buildSkillPaths, | ||
| configureClaude, | ||
| configureAgents, | ||
| configureClaudeCommands, | ||
| configureClaudeHooks, | ||
| configureClaudeRules, | ||
| configureCline, | ||
| configureCursor, | ||
| configureMcp, | ||
| configureRoocode, | ||
| detectPackageInstallType, | ||
@@ -847,4 +1241,6 @@ detectPlatform, | ||
| getDefaultProjectDir, | ||
| getHooksDestPath, | ||
| getInstallRoot, | ||
| isInteractiveInstall, | ||
| isOurHookEntry, | ||
| parseArgs, | ||
@@ -854,2 +1250,3 @@ printHelp, | ||
| removeManagedBlock, | ||
| removeOurHookEntries, | ||
| removeSkillsSectionItems, | ||
@@ -856,0 +1253,0 @@ replaceManagedBlock, |
+10
-1
| { | ||
| "name": "developer-stack-skills", | ||
| "version": "1.2.1", | ||
| "version": "2.0.0", | ||
| "description": "AI agent SKILL.md files plus installer CLI for Java/Spring, Python/FastAPI, React/Angular, Testing, and Project Conventions. Compatible with Claude, Cline, Roocode, Copilot, and Cursor.", | ||
@@ -32,2 +32,5 @@ "keywords": [ | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| }, | ||
| "scripts": { | ||
@@ -37,5 +40,11 @@ "postinstall": "node bin/postinstall.js", | ||
| }, | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.6.0" | ||
| }, | ||
| "files": [ | ||
| "bin/**/*", | ||
| "commands/**/*", | ||
| "hooks/**/*", | ||
| "lib/**/*", | ||
| ".claude/rules/**/*", | ||
| "java-spring/SKILL.md", | ||
@@ -42,0 +51,0 @@ "python-backend/SKILL.md", |
+143
-2
@@ -21,3 +21,3 @@ # developer-stack-skills | ||
| Version in this README: `1.2.1` | ||
| Version in this README: `2.0.0` | ||
@@ -34,2 +34,11 @@ Interactive `npm install` can auto-run post-install configuration, but recent npm versions hide lifecycle script output by default. Treat configuration as explicit step after installation unless you install with `--foreground-scripts`. | ||
| Why global install still asks for project directory: | ||
| - Global install has two separate outputs: | ||
| 1. Skill files go into shared user-level folder: `~/.ai-skills/developer-stack-skills` | ||
| 2. Agent config files still get written into one specific project | ||
| - Installer asks for `Project directory` so it knows where to update `CLAUDE.md`, `.clinerules`, `.roo/config.yml`, `.cursor/rules/developer-stack-skills.mdc`, and `.github/copilot-instructions.md` | ||
| - Global package install does not mean "enable skills for every repo automatically" | ||
| - It means "store one shared copy of skills globally, then link chosen project to those skills" | ||
| Post-install configure command: | ||
@@ -73,2 +82,8 @@ | ||
| Start MCP server (stdio): | ||
| ```bash | ||
| developer-stack-skills serve | ||
| ``` | ||
| Or run from local package without global install: | ||
@@ -78,2 +93,3 @@ | ||
| npx developer-stack-skills configure | ||
| npx developer-stack-skills serve | ||
| ``` | ||
@@ -115,3 +131,3 @@ | ||
| ```text | ||
| [developer-stack-skills] installing version 1.2.1 | ||
| [developer-stack-skills] installing version 2.0.0 | ||
| [developer-stack-skills] package install type: global | ||
@@ -141,2 +157,3 @@ [developer-stack-skills] skill install scope: global | ||
| - `uninstall` | ||
| - `serve` | ||
| - `version` | ||
@@ -147,2 +164,76 @@ - `help` | ||
| ## MCP Server | ||
| Skills are exposed as MCP tools via stdio transport. | ||
| ```bash | ||
| developer-stack-skills serve | ||
| ``` | ||
| Add to MCP client config: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "developer-stack-skills": { | ||
| "command": "developer-stack-skills", | ||
| "args": ["serve"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Or without global install: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "developer-stack-skills": { | ||
| "command": "npx", | ||
| "args": ["developer-stack-skills", "serve"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Available tools: | ||
| | Tool | Description | | ||
| |---|---| | ||
| | `list_available_skills` | List all skills with descriptions and file patterns | | ||
| | `get_skill` | Load full SKILL.md for a stack (`java-spring`, `python-backend`, `frontend`, `testing`, `project-conventions`) | | ||
| | `get_conventions` | Load project-wide conventions (shortcut for `get_skill` with `project-conventions`) | | ||
| | `detect_stack` | Given a file path, return which skill applies and a ready-to-use `get_skill` call | | ||
| --- | ||
| ## Claude Code Commands | ||
| Five slash commands are installed into your project: | ||
| | Command | Description | | ||
| |---|---| | ||
| | `/implement-feature [description]` | Detect stack, plan, implement with tests | | ||
| | `/write-tests [target]` | Write tests following stack conventions | | ||
| | `/review-pr` | Review branch changes against conventions | | ||
| | `/check-deps` | Audit dependencies for outdated versions and vulnerabilities | | ||
| | `/add-endpoint [description]` | Add REST endpoint following stack and REST conventions | | ||
| --- | ||
| ## Hooks | ||
| Two Claude Code hooks fire automatically: | ||
| | Hook | File | Fires When | | ||
| |---|---|---| | ||
| | `pre-write` | `hooks/pre-write.js` | Before any file write — injects stack reminder based on file extension | | ||
| | `pre-bash` | `hooks/pre-bash.js` | Before bash commands — warns to verify latest stable version on package installs | | ||
| The `pre-write` hook covers: Java, Kotlin, Python, Angular TypeScript, generic TypeScript/JSX, `.env` files, and `.sql` migrations. | ||
| The `pre-bash` hook detects: `pip install`, `uv add`, `npm install`, `yarn add`, `pnpm add`, `bun add`, `poetry add`, and `npx pkg@latest`. | ||
| --- | ||
| ## Installed Files | ||
@@ -178,2 +269,52 @@ | ||
| ## FAQ | ||
| ### Why does `npm install -g developer-stack-skills` still ask for `Project directory`? | ||
| Because installer does two different things: | ||
| 1. It installs skill folders | ||
| 2. It updates agent config files for a specific project | ||
| With global package install, skill folders go to: | ||
| ```text | ||
| ~/.ai-skills/developer-stack-skills/ | ||
| ``` | ||
| But agent configs still must be written into a real project, for example: | ||
| ```text | ||
| D:\Projects\my-app\CLAUDE.md | ||
| D:\Projects\my-app\.clinerules | ||
| D:\Projects\my-app\.roo\config.yml | ||
| ``` | ||
| So `Project directory` prompt is still required. Global install means shared skill storage, not machine-wide auto-enable for every repository. | ||
| ### What is difference between `npm install -g developer-stack-skills` and `npm install developer-stack-skills`? | ||
| `npm install -g developer-stack-skills` | ||
| - Package installs into global npm directory | ||
| - CLI command `developer-stack-skills` works anywhere | ||
| - Skills install into `~/.ai-skills/developer-stack-skills/` | ||
| - Default install mode is `symlink` | ||
| - Best when you want one shared install reused across many projects | ||
| `npm install developer-stack-skills` | ||
| - Package installs into current project's `node_modules` | ||
| - CLI usually runs through `npx developer-stack-skills` or npm scripts | ||
| - Skills install into `<project>/.ai-skills/developer-stack-skills/` | ||
| - Default install mode is `copy` | ||
| - Best when each project should keep its own isolated skill copy | ||
| Same in both cases: | ||
| - Installer still updates agent config files per project | ||
| - Skills are not auto-enabled for every repository on machine | ||
| --- | ||
| ## Skills Included | ||
@@ -180,0 +321,0 @@ |
+81
-0
| # Release Notes | ||
| ## 2.0.0 - 2026-05-16 | ||
| This release adds MCP server support, Claude Code slash commands, and hooks — making skills available to AI agents at runtime without manual SKILL.md loading. | ||
| ### MCP Server | ||
| Skills are now exposed as MCP tools. Start the server with: | ||
| ```bash | ||
| developer-stack-skills serve | ||
| ``` | ||
| Add to your MCP client config (stdio transport): | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "developer-stack-skills": { | ||
| "command": "developer-stack-skills", | ||
| "args": ["serve"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Or with npx (no global install required): | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "developer-stack-skills": { | ||
| "command": "npx", | ||
| "args": ["developer-stack-skills", "serve"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Available tools: | ||
| | Tool | Description | | ||
| |---|---| | ||
| | `list_available_skills` | List all skills with descriptions and file patterns | | ||
| | `get_skill` | Load full SKILL.md for a stack before writing code | | ||
| | `get_conventions` | Load project-wide conventions | | ||
| | `detect_stack` | Detect which skill applies to a file path | | ||
| Typical agent workflow: | ||
| 1. Agent opens a file → calls `detect_stack` with the file path | ||
| 2. Server returns the recommended skill name | ||
| 3. Agent calls `get_skill` to load full conventions | ||
| 4. Agent writes code following those conventions | ||
| ### Claude Code Commands | ||
| Five slash commands are now included and installed into your project: | ||
| - `/implement-feature [description]` — detect stack, plan, implement with tests | ||
| - `/write-tests [target]` — write tests following stack conventions | ||
| - `/review-pr` — review branch changes against conventions | ||
| - `/check-deps` — audit dependencies for outdated versions and vulnerabilities | ||
| - `/add-endpoint [description]` — add REST endpoint following stack and REST conventions | ||
| ### Hooks | ||
| Two Claude Code hooks inject reminders automatically: | ||
| - `pre-write.js` — fires before any file write; injects a one-line stack reminder based on file extension (Java, Kotlin, Python, Angular, TypeScript, env files, SQL migrations) | ||
| - `pre-bash.js` — fires before bash commands; warns to verify latest stable version before any package install (`pip install`, `npm install`, `uv add`, etc.) | ||
| Hooks require Claude Code. They run automatically — no manual configuration beyond the installer wiring them up. | ||
| ### Agent Config Updates | ||
| - **Claude Code**: `.claude/rules/` now contains per-stack rule files that auto-load the right skill | ||
| - **Cursor**: Single `.cursor/rules/developer-stack-skills.mdc` replaced by five per-stack `.mdc` files for finer-grained activation | ||
| - **Roocode**: Migrated from `.roo/config.yml` to `.roo/rules/developer-stack-skills.md` | ||
| --- | ||
| ## 1.2.1 - 2026-05-15 | ||
@@ -4,0 +85,0 @@ |
| skills: | ||
| - path: node_modules/io.github.jabhijeet/java-spring/SKILL.md | ||
| - path: node_modules/io.github.jabhijeet/testing/SKILL.md | ||
| - path: node_modules/io.github.jabhijeet/project-conventions/SKILL.md |
Sorry, the diff of this file is not supported yet
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
143135
41.59%34
61.9%1332
79.03%337
71.94%1
Infinity%9
28.57%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added