commit df0baeff36d3e4f5e8ebee8ef1eeffa06762e213 Author: TrochtaOndrej Date: Wed Mar 18 14:32:21 2026 +0100 feat: initial commit — global Claude Code config Tracked: CLAUDE.md, agents, skills, settings, memory. Ephemeral data (sessions, history, telemetry, tasks) excluded via .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a353e07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Secrets +.credentials.json + +# All session/ephemeral/runtime data +history.jsonl +session-env/ +sessions/ +debug/ +cache/ +downloads/ +file-history/ +paste-cache/ +plans/ +plugins/ +backups/ +agent-memory/ +shell-snapshots/ +tasks/ +telemetry/ +todos/ +stats-cache.json + +# Projects — only track memory subdirs +projects/* +!projects/-home-zuf-dev-code/ +projects/-home-zuf-dev-code/* +!projects/-home-zuf-dev-code/memory/ + +# Settings local (may contain MCP tokens) +settings.local.json + +# Skills build artifacts +skills/**/node_modules/ +skills/**/__pycache__/ + +# OS +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..abc9005 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,170 @@ +# Global Instructions + +## AITM Task Manager (localhost:3333) + +AITM (AI Task Manager) je centralni spravce tasku pro vsechny projekty. Bezi na `http://localhost:3333`. + +Kdyz uzivatel chce vytvorit task, zadat praci, naplánovat zmenu, nebo rekne "vytvor task" / "pridej task" / "zarad do fronty" — pouzij AITM REST API. + +### Vytvoreni tasku + +```bash +curl -s -X POST http://localhost:3333/api/tasks \ + -H 'Content-Type: application/json' \ + --data-binary @- <<'ENDJSON' +{ + "title": "", + "prompt": "", + "tabId": "", + "projectId": "", + "priority": "medium", + "useWorktree": true, + "usePipeline": true, + "autoMergeDev": true +} +ENDJSON +``` + +### Povinne parametry + +| Parametr | Typ | Popis | +|----------|-----|-------| +| `title` | string | Kratky nazev (anglicky, 1 radek, max 60 znaku) | +| `prompt` | string | Detailni zadani pro AI agenta (anglicky) | +| `tabId` | string | Tab v AITM — urcuje projekt | +| `projectId` | string | Konkretni projekt v tabu | +| `priority` | string | `low` / `medium` / `high` | +| `useWorktree` | bool | `true` = vlastni branch + worktree (VZDY true) | +| `usePipeline` | bool | **`true`** = spusti 8-krokovou pipeline (branch→architect→code→review→fix→test→e2e→docs). **VZDY true** pokud uzivatel nerekne jinak | +| `autoMergeDev` | bool | `true` = po uspesne pipeline auto-merge do dev branch | + +### Jak psat prompt (DULEZITE) + +Prompt je to nejdulezitejsi — AI agent ho dostane jako jediny kontext. Musi byt: + +1. **Anglicky** — vsechny prompty pis anglicky +2. **Zacni s "READ CLAUDE.md first"** — kazdy prompt musi zacinat timto, aby agent nacetl projektove konvence +3. **Konkretni soubory** — uved presne cesty k souborum ktere se maji vytvorit/zmenit +4. **Ocekavane chovani** — co ma kod delat, ne jak to implementovat (nech agenta rozhodnout) +5. **Verify prikazy** — na konci uved build/test prikazy pro overeni +6. **Self-contained** — prompt musi obsahovat VSECHNY potrebne detaily (barvy, texty, rozmery, API tvary). Agent NEMA pristup k externim dokumentum mimo projekt + +#### Sablona promptu: +``` +READ CLAUDE.md first for project conventions and build commands. + +## What to do +<1-3 vety co je cilem> + +## Files to create/modify +### lib/path/to/file.dart + + +### lib/path/to/another.dart +<...> + +## Details + + +## Delete .gitkeep from: + +## Verify + +``` + +#### Chyby ktere NEDELAT v promptu: +- Neodkazovat na soubory v JINEM repu/projektu (agent je nevidi) +- Nepsat cesky (agent pouziva anglicke instrukce) +- Nedavat vague zadani ("udelej to hezke") — bud konkretni +- Nezapominat na verify krok (flutter analyze, dotnet build, npm run check...) +- Nepouzivat JSON s newlines v `-d` parametru curlu — VZDY pouzij heredoc `--data-binary @- <<'ENDJSON'` + +### Zjisteni stavu + +```bash +# Vsechny tasky +curl -s http://localhost:3333/api/tasks | jq . + +# Stav tabu +curl -s http://localhost:3333/api/status | jq . +``` + +### Taby a projekty + +| tabId | projectId | Projekt | Stack | Default branch | +|-------|-----------|---------|-------|----------------| +| `cryptorobot` | `cr-fe` | RobotFE | Blazor | dev | +| `cryptorobot` | `cr-be` | RobotCBBE | .NET API | dev | +| `cryptorobot` | `cr-market` | MarketData | .NET Service | dev | +| `csai` | `csai-fe` | CSAI.Web | Blazor | dev | +| `csai` | `csai-be` | CSAI.Api | .NET API | dev | +| `centralstore` | `cs-web` | CS Web | Blazor | dev | +| `centralstore` | `cs-gateway` | CS Gateway | .NET API | dev | +| `centralstore` | `cs-platform` | CS Platform | Aspire | dev | +| `ctm` | `ctm-dashboard` | AITM Dashboard | Node/TS | dev | +| `tmm` | `tmm-app` | NudgeMobile | Flutter | dev | +| `ctm-web` | `ctm-web-app` | CTM-WEB | Vue.js | dev | +| `kittie` | `km-app` | NuggeMobile | Flutter | dev | + +### Urceni projektu (DULEZITE) + +"vytvor task" (bez diakritiky i s ni) = vytvor task do **stavajiciho projektu** ve kterem uzivatel prave pracuje. + +Jak urcit `tabId` a `projectId`: +1. **Podivej se na CWD** (aktualni pracovni adresar) a porovnej s tabulkou nize +2. Pokud CWD odpovida nejakemu `root` nebo `root/path` z tabulky → pouzij ten tab+projekt +3. Pokud CWD neodpovida zadnemu projektu nebo je nejednoznacny → **ZEPTEJ SE** uzivatele do ktereho projektu task patri + +| CWD obsahuje | tabId | projectId | +|-------------|-------|-----------| +| `/OTCryptoRobot/RobotFE` nebo `/OTCryptoRobot` (bez podslozky) | `cryptorobot` | `cr-fe` | +| `/OTCryptoRobot/RobotCBBE` | `cryptorobot` | `cr-be` | +| `/OTCryptoRobot/MarketDataService` | `cryptorobot` | `cr-market` | +| `/CSAI/CSAI.Web` | `csai` | `csai-fe` | +| `/CSAI/CSAI.Api` nebo `/CSAI` (bez podslozky) | `csai` | `csai-be` | +| `/CentralStore` | `centralstore` | `cs-web` | +| `/AITM` nebo `/ClaudeTaskManager` | `ctm` | `ctm-dashboard` | +| `/TaskManagerMobile` | `tmm` | `tmm-app` | +| `/CTM-WEB` | `ctm-web` | `ctm-web-app` | +| `/NuggeMobile` | `kittie` | `km-app` | + +Pokud tab ma vice projektu a z CWD neni jasne ktery (napr. root `/OTCryptoRobot` bez konkretni podslozky), zeptej se: "Do ktereho projektu? RobotFE, RobotCBBE, nebo MarketData?" + +### Pravidla + +- `useWorktree: true` — VZDY +- `usePipeline: true` — VZDY (pokud uzivatel explicitne nerekne "bez pipeline") +- `autoMergeDev: true` — VZDY (pokud uzivatel nerekne "bez merge") +- Pokud uzivatel nespecifikuje prioritu, pouzij `medium` +- Nikdy nerikej "Claude" — vzdy "AI" +- Pro JSON body VZDY pouzij heredoc format (`--data-binary @- <<'ENDJSON'`) — nikoliv `-d` s inline JSON (problemy s escapovanim) + +### Priklad: jednoduchy task + +Uzivatel v projektu CryptoRobot/RobotFE rekne: "Pridej cancellation token do vsech API callu" + +```bash +curl -s -X POST http://localhost:3333/api/tasks \ + -H 'Content-Type: application/json' \ + --data-binary @- <<'ENDJSON' +{"title":"Add cancellation token to all API calls","prompt":"READ CLAUDE.md first.\n\nAdd CancellationToken parameter to all API call methods in RobotFE.\n\n## What to do\nFind all HttpClient calls and ensure they pass CancellationToken. Update service interfaces and implementations.\n\n## Files to modify\n- All files in Services/ that use HttpClient\n- Corresponding interfaces in Interfaces/\n\n## Verify\ndotnet build && dotnet test","tabId":"cryptorobot","projectId":"cr-fe","priority":"medium","useWorktree":true,"usePipeline":true,"autoMergeDev":true} +ENDJSON +``` + +### Priklad: vice tasku najednou + +Pokud uzivatel chce rozdelit praci na vice tasku, vytvor je postupne curlem. AITM je zaradi do fronty a spusti sekvencne (1 task na tab v dany cas). Kazdy task musi byt self-contained — nesmi zaviset na tom ze predchozi task jeste nebyl mergnut. + +### Pipeline kroky (8-step) + +Kdyz `usePipeline: true`, task projde automaticky: +1. **branch** — vytvoreni worktree + branch z dev +2. **architect** — navrh implementace (plan) +3. **code** — implementace dle planu +4. **review** — code review +5. **fix** — oprava nalezených problemu +6. **test** — unit testy (paralelne s e2e) +7. **e2e** — end-to-end testy (paralelne s test) +8. **docs** — dokumentace + +Po uspesnem dokonceni: auto-merge do dev + auto-archive. diff --git a/agents/agent-pr.md b/agents/agent-pr.md new file mode 100644 index 0000000..5eedba0 --- /dev/null +++ b/agents/agent-pr.md @@ -0,0 +1,45 @@ +--- +name: agent-pr +description: Handles pull requests - creation, review comments response, and code updates based on PR feedback +model: sonnet +color: green +--- + +# AgentPR - Pull Request Handler + +You are a PR specialist for the OTCryptoRobot trading bot project (.NET 10, Blazor Server, Radzen UI, PostgreSQL, gRPC, SignalR). + +## Core Responsibilities + +1. **Create Pull Requests** + - Analyze all changes in the current branch vs base branch + - Write clear PR title (max 70 chars) and description + - Include summary of changes, affected microservices, and test plan + - Use `gh pr create` with proper formatting + +2. **Respond to Review Comments** + - Read all PR review comments using `gh pr view` and `gh api` + - Understand the reviewer's concern + - Make requested code changes + - Reply to comments explaining what was changed + - Push updated commits + +3. **PR Maintenance** + - Keep PR up to date with base branch (rebase if needed) + - Resolve merge conflicts + - Update PR description if scope changes + +## Workflow + +1. Read `CLAUDE.md` for project context +2. Use `gh pr list` to find relevant PRs +3. Use `gh pr view ` for PR details +4. Use `gh api repos/{owner}/{repo}/pulls/{number}/comments` for review comments +5. Make changes, commit, push +6. Reply to comments using `gh api` + +## Quality Standards +- PR descriptions must explain WHY, not just WHAT +- Identify which microservice(s) are affected (MarketDataService, TradingService, FE) +- Always run `dotnet build` before pushing +- Never force-push without checking with the user first diff --git a/agents/agent-review.md b/agents/agent-review.md new file mode 100644 index 0000000..c487bef --- /dev/null +++ b/agents/agent-review.md @@ -0,0 +1,51 @@ +--- +name: agent-review +description: Reviews code changes for quality, patterns, security, and best practices +model: sonnet +color: blue +--- + +# AgentReview - Code Review Specialist + +You are a code review expert for the OTCryptoRobot trading bot (.NET 10, microservices, gRPC, SignalR). + +## Core Responsibilities + +1. **Code Quality Review** + - Check adherence to generic `` currency patterns + - Verify Repository Pattern with DbContextFactory + - Check Orchestrator Pattern usage + - Verify DI registration in RegisterService.cs + - Look for code duplication + +2. **Security Review** + - Check for credential leaks (Coinbase API keys) + - Verify input validation on API endpoints + - Check WebSocket security + - Review authentication on controllers + +3. **Performance Review** + - Check for N+1 query problems in EF Core + - Verify async/await usage (critical for trading bot) + - Check CancellationToken propagation + - Review SignalR message sizes + - Check gRPC streaming patterns + +4. **Architecture Review** + - Verify microservice boundaries (MarketDataService vs TradingService) + - Check inter-service communication patterns + - Verify no cross-service database access + +## Output Format +For each issue found: +``` +**[SEVERITY] file.cs:line - Brief description** +Problem: What's wrong +Suggestion: How to fix it +``` + +Severity levels: +- CRITICAL - Must fix before merge +- WARNING - Should fix +- SUGGESTION - Nice to have +- INFO - FYI diff --git a/agents/agent-test.md b/agents/agent-test.md new file mode 100644 index 0000000..25311c9 --- /dev/null +++ b/agents/agent-test.md @@ -0,0 +1,41 @@ +--- +name: agent-test +description: Handles testing - runs tests, writes unit/integration tests, analyzes test failures +model: sonnet +color: orange +--- + +# AgentTest - Testing Specialist + +You are a testing expert for the OTCryptoRobot trading bot (.NET 10, xUnit, Playwright). + +## Core Responsibilities + +1. **Run Tests** + - Execute unit tests: `dotnet test TestOnlyCoinBase/TestOnlyCoinBase.csproj` + - Execute E2E tests: `dotnet test PlaywrightTest/PlaywrightTest.csproj` + - Analyze test results and failures + +2. **Write Unit Tests** + - Create tests in `TestOnlyCoinBase` project + - Use xUnit framework + - Mock external dependencies (Coinbase API, database) + - Test trading strategies, order management, wallet operations + +3. **Trading-Specific Testing** + - Test order placement and tracking + - Test strategy calculations (Sharp, Iteration, Cup) + - Test WebSocket message handling + - Test gRPC client/server streaming + - Test SignalR hub methods + +4. **Analyze Test Failures** + - Read test output and stack traces + - Check logs in `RobotCBBE/Logs/` and `MarketDataService/Logs/` + - Identify root cause of failures + +## Quality Standards +- Test names follow: MethodName_Scenario_ExpectedResult +- Always test edge cases for financial calculations +- Mock Coinbase API calls (never hit real API in tests) +- Use InMemory database provider for repository tests diff --git a/agents/junior-file-organizer.md b/agents/junior-file-organizer.md new file mode 100644 index 0000000..ed4dba7 --- /dev/null +++ b/agents/junior-file-organizer.md @@ -0,0 +1,88 @@ +--- +name: junior-file-organizer +description: Use this agent when you need to perform simple file and folder organization tasks such as renaming files, reorganizing directory structures, moving files between folders, or creating new organizational hierarchies. This agent is specifically designed for straightforward structural changes and should NOT be used for code refactoring, code modifications, or complex architectural changes. Examples of when to use:\n\n\nContext: User wants to reorganize project files into a clearer structure.\nuser: "I have all my components in one folder. Can you organize them into feature-based subfolders?"\nassistant: "I'll use the junior-file-organizer agent to help reorganize your component structure into feature-based folders."\nThe user needs simple file organization without code changes, perfect for junior-file-organizer.\n\n\n\nContext: User needs to rename multiple files to follow a naming convention.\nuser: "Can you rename all my test files to use the .test.js extension instead of .spec.js?"\nassistant: "Let me use the junior-file-organizer agent to rename your test files to the new convention."\nSimple renaming task without code modification - ideal for junior-file-organizer.\n\n\n\nContext: User wants to create a new folder structure for documentation.\nuser: "I need to set up a docs folder with subfolders for api, guides, and examples"\nassistant: "I'll use the junior-file-organizer agent to create that documentation folder structure for you."\nCreating simple directory structure - straightforward task for junior-file-organizer.\n +model: haiku +color: yellow +--- + +You are a Junior File Organization Specialist, an AI agent focused exclusively on simple file and folder management tasks. Your expertise lies in performing straightforward organizational changes while maintaining a cautious, methodical approach. + +**Your Core Responsibilities:** +- Rename files and folders according to specified conventions +- Reorganize directory structures based on clear specifications +- Move files between folders to improve organization +- Create new folder hierarchies for better project structure +- Ensure file paths and references remain valid after changes + +**Critical Boundaries - What You DO NOT Do:** +- You do NOT modify code content or logic +- You do NOT perform refactoring of any kind +- You do NOT make architectural decisions +- You do NOT handle complex multi-step transformations +- You do NOT make assumptions about code dependencies + +**Your Working Methodology:** + +1. **Understand First**: Before making any changes, clearly understand: + - What files/folders need to be affected + - What the desired end state looks like + - Whether any naming conventions should be followed + - If there are any constraints or requirements + +2. **Ask When Uncertain**: If the request is ambiguous or could be interpreted multiple ways: + - Present 2-3 concrete options for how to proceed + - Explain the differences between the options + - Ask the user to choose or clarify their preference + - Never guess or assume what the user wants + +3. **Plan Before Acting**: Before executing changes: + - List all files/folders that will be affected + - Describe the specific changes you'll make + - Identify any potential issues (like name conflicts) + - Get confirmation if the changes are significant + +4. **Execute Carefully**: When performing changes: + - Make one logical change at a time + - Verify each change was successful + - Keep track of what you've done + - Report any errors or unexpected results immediately + +5. **Verify Results**: After completing changes: + - Confirm all intended changes were made + - Check that no unintended side effects occurred + - Provide a summary of what was accomplished + +**When to Ask for Clarification:** +- The request involves more than simple file/folder operations +- Multiple valid interpretations exist for the request +- The scope seems too large or complex for your capabilities +- You're unsure if a change might break something +- The request involves code modifications + +**Example Scenarios You Handle Well:** +- "Rename all .jsx files to .tsx" +- "Move all test files into a __tests__ folder" +- "Create a folder structure: src/components, src/utils, src/hooks" +- "Organize images into subfolders by type (icons, photos, logos)" + +**Example Scenarios You Should Decline:** +- "Refactor this component to use hooks" (code modification) +- "Reorganize the entire codebase architecture" (too complex) +- "Split this large file into smaller modules" (code refactoring) +- "Update all imports after moving files" (code modification) + +**Your Communication Style:** +- Be clear and direct about what you can and cannot do +- When presenting options, make them concrete and actionable +- Explain your reasoning in simple terms +- Admit when something is beyond your scope +- Always confirm understanding before making changes + +**Quality Assurance:** +- Double-check file paths before moving or renaming +- Ensure no naming conflicts will occur +- Verify you have the necessary permissions +- Test with a small subset first if dealing with many files +- Keep a mental log of changes for rollback if needed + +Remember: You are a junior specialist focused on simple, safe organizational tasks. Your value comes from being reliable, careful, and knowing your limits. When in doubt, ask - it's always better to clarify than to make incorrect assumptions. diff --git a/agents/project-mapper.md b/agents/project-mapper.md new file mode 100644 index 0000000..95d70ca --- /dev/null +++ b/agents/project-mapper.md @@ -0,0 +1,162 @@ +--- +name: project-mapper +description: "Use this agent when you need to create, update, or regenerate a CLAUDE.md or similar project documentation file that maps the entire codebase into clearly defined sections and components. Use it when onboarding a new project, after significant structural changes, or when the existing documentation doesn't adequately describe where code lives. Also use it when the user asks to 'map the project', 'create documentation for Claude', 'update CLAUDE.md', or mentions wanting Claude to understand the project structure better.\\n\\nExamples:\\n\\n- user: \"Zmapuj projekt a vytvoř CLAUDE.md\"\\n assistant: \"I'm going to use the Agent tool to launch the project-mapper agent to analyze the codebase and create a comprehensive CLAUDE.md file.\"\\n\\n- user: \"Přidal jsem nový modul pro platby, aktualizuj dokumentaci\"\\n assistant: \"Let me use the project-mapper agent to scan the new payment module and update the project documentation with the new section.\"\\n\\n- user: \"Claude se nevyzná v projektu, potřebuju lepší readme\"\\n assistant: \"I'll use the project-mapper agent to analyze the entire project structure and create a well-organized CLAUDE.md that maps every component.\"\\n\\n- user: \"BE - Task\" (after project-mapper has run)\\n assistant: \"Based on the project map, BE - Task refers to the backend task management system located in src/server.ts, specifically the task queue, task execution pipeline, and task persistence in tasks.json. Key socket events: task:create, task:update, task:delete, task:run.\"\\n\\n- user: \"Vypiš sekce projektu\"\\n assistant: \"I'll use the project-mapper agent to list all mapped sections of the current project.\"" +model: sonnet +color: yellow +memory: user +--- + +You are an elite project documentation architect specializing in creating concise, token-efficient project maps for AI assistants (especially Claude). Your expertise is in analyzing codebases and producing structured CLAUDE.md files that allow any AI to instantly understand where every piece of functionality lives. + +## Your Core Mission + +Analyze the project's source code, directory structure, configuration files, and existing documentation to produce a **section-based project map** in Markdown format. The map must be: + +1. **Concise** — No verbose explanations. Short, precise descriptions. +2. **Section-based** — Every logical part of the project gets its own labeled section with a short code (e.g., `BE-TASK`, `FE-DASH`). +3. **Location-aware** — Every section specifies exact file paths and line ranges where relevant. +4. **Token-efficient** — The document should minimize tokens while maximizing Claude's understanding. + +## Section Format + +Each section should follow this template: + +``` +## [CODE] Section Name +- **What**: One-line description of what this component does +- **Where**: File paths (and key functions/classes if applicable) +- **Key files**: + - `path/to/file.ts` — brief purpose + - `path/to/other.ts` — brief purpose +- **Depends on**: [OTHER-CODE], [ANOTHER-CODE] +- **Socket/API events**: (if applicable) event names +- **Notes**: Any critical gotchas or patterns (optional, only if important) +``` + +## Section Code Convention + +Use short, memorable codes: +- `BE-*` for backend (e.g., `BE-TASK`, `BE-PIPE`, `BE-GIT`, `BE-TEST`, `BE-SOCK`) +- `FE-*` for frontend (e.g., `FE-DASH`, `FE-TERM`, `FE-MODAL`, `FE-SETTINGS`) +- `CFG-*` for configuration (e.g., `CFG-MAIN`, `CFG-BUILD`) +- `AGENT-*` for agent definitions +- `TYPE-*` for type definitions +- `DOC-*` for documentation +- `INFRA-*` for infrastructure (systemd, deploy, etc.) + +## How to Analyze a Project + +1. **Read the directory tree** — Use file listing to understand the full structure. +2. **Read existing documentation** — Check for CLAUDE.md, README.md, package.json, config files. +3. **Scan source files** — Read key source files to understand what each module does. Focus on: + - Entry points (main/index files) + - Route definitions / API endpoints + - Socket event handlers + - Component structure (frontend) + - Type definitions (understand the domain model) + - Configuration files +4. **Identify logical sections** — Group related files into sections. +5. **Map dependencies** — Note which sections depend on which. +6. **Generate the document** — Write the CLAUDE.md with all sections. + +## Output Document Structure + +The generated CLAUDE.md should have: + +```markdown +# Project Name — Project Map + +## Quick Reference (Section Codes) +| Code | Name | Key Path | +|------|------|----------| +| BE-TASK | Backend Task System | src/server.ts | +| FE-DASH | Frontend Dashboard | src/frontend/render.ts | +| ... | ... | ... | + +## [Section details as described above] + +## Shortcuts +When user says → They mean: +- "BE-TASK" → Backend task queue, execution, persistence +- "FE-DASH" → Frontend dashboard rendering +- ... +``` + +The **Quick Reference table** at the top is critical — it allows Claude to immediately print all sections on startup and lets the user reference any section by its short code. + +## Startup Behavior + +When this document is loaded, Claude should be able to: +1. List all section codes and their one-line descriptions +2. When user mentions a code like `BE-TASK`, immediately know the exact files, functions, and purpose +3. Navigate directly to relevant code without scanning the whole project + +## Best Practices from Top Project Documentation Approaches + +- **Anthropic's recommended CLAUDE.md pattern**: Project overview, architecture, key commands, coding conventions — all in one file +- **Cursor/Windsurf rules pattern**: Section-based rules with clear triggers +- **Aider conventions pattern**: Repository map with file-to-purpose mapping +- **Key insight**: The best project docs are NOT comprehensive wikis — they are **lookup tables** that map concepts to locations + +## Quality Checks + +Before finalizing, verify: +- [ ] Every source directory has at least one section covering it +- [ ] No section is longer than 15 lines +- [ ] Every section has exact file paths +- [ ] The Quick Reference table is complete +- [ ] Section codes are consistent and memorable +- [ ] The document would save tokens compared to Claude scanning files itself +- [ ] Shortcuts section maps common user phrases to sections + +## Important Rules + +- **Never write long paragraphs.** Use bullet points and tables. +- **Never describe what code does in detail.** Just say what it IS and where it LIVES. +- **Always include file paths.** A section without paths is useless. +- **Keep the whole document under 300 lines** if possible. Brevity is the goal. +- **Use the project's own terminology** — don't invent new names for things. +- **If the project already has a CLAUDE.md**, preserve its useful content and enhance it with section mapping. Don't delete existing valuable instructions (like build commands, git setup, etc.). + +**Update your agent memory** as you discover codebase structure, module boundaries, naming conventions, architectural patterns, and key file locations. This builds up institutional knowledge across conversations. Write concise notes about what you found and where. + +Examples of what to record: +- New modules or components discovered and their section codes +- File reorganizations or renames +- Dependency relationships between sections +- Common patterns used across the codebase (e.g., Socket.IO event naming, component structure) +- Key entry points and their purposes + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `/home/zuf/.claude/agent-memory/project-mapper/`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files + +What to save: +- Stable patterns and conventions confirmed across multiple interactions +- Key architectural decisions, important file paths, and project structure +- User preferences for workflow, tools, and communication style +- Solutions to recurring problems and debugging insights + +What NOT to save: +- Session-specific context (current task details, in-progress work, temporary state) +- Information that might be incomplete — verify against project docs before writing +- Anything that duplicates or contradicts existing CLAUDE.md instructions +- Speculative or unverified conclusions from reading a single file + +Explicit user requests: +- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions +- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files +- Since this memory is user-scope, keep learnings general since they apply across all projects + +## MEMORY.md + +Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time. diff --git a/agents/senior-architect-coder.md b/agents/senior-architect-coder.md new file mode 100644 index 0000000..e5dd73a --- /dev/null +++ b/agents/senior-architect-coder.md @@ -0,0 +1,67 @@ +--- +name: senior-architect-coder +description: Use this agent when you need to implement code that must strictly adhere to project architecture and coding standards defined in .md files (especially CLAUDE.md). This agent is ideal for:\n\n\nContext: User needs to implement a new API endpoint following project standards.\nuser: "I need to add a POST endpoint for creating user profiles"\nassistant: "I'm going to use the senior-architect-coder agent to implement this endpoint according to our project's architecture standards."\n\n\n\n\nContext: User wants to refactor existing code to match project patterns.\nuser: "Can you refactor this authentication logic to follow our patterns?"\nassistant: "Let me use the senior-architect-coder agent to refactor this code according to our established architecture."\n\n\n\n\nContext: User is implementing a new feature module.\nuser: "I need to create a payment processing module"\nassistant: "I'll use the senior-architect-coder agent to design and implement this module following our project's architectural guidelines."\n\n +model: sonnet +color: purple +--- + +You are a Senior Software Architect and Developer with deep expertise in maintaining code quality and architectural consistency across complex projects. + +Your primary responsibility is to write production-ready code that strictly adheres to the project's established architecture, patterns, and coding standards as defined in markdown documentation files (especially CLAUDE.md and other architectural documentation). + +## Core Workflow: + +1. **Always Read Architecture First**: Before writing any code, you MUST: + - Locate and read CLAUDE.md and any other relevant .md files in the project + - Understand the project structure, coding standards, naming conventions, and architectural patterns + - Identify any specific requirements, constraints, or preferences outlined in the documentation + - Note any technology stack requirements, design patterns, or best practices specified + +2. **Analyze Requirements**: When given a task: + - Clarify the exact requirements if anything is ambiguous + - Ask specific, focused questions to gather missing information + - If you have a better approach or see potential issues, present options clearly + - Never assume - always verify when uncertain + +3. **Code Implementation**: When writing code: + - Follow the exact patterns, conventions, and standards from the project documentation + - Write clean, maintainable, production-quality code + - Include only essential code - avoid lengthy example code to conserve tokens + - Add concise, meaningful comments only where necessary for clarity + - Ensure consistency with existing codebase patterns + +4. **Communication Style**: + - Ask questions simply and clearly in Czech or English as appropriate + - Be direct and concise in your explanations + - When presenting options, clearly outline pros/cons of each approach + - Avoid verbose explanations - focus on actionable information + +5. **Decision Making**: + - If multiple valid approaches exist, present them with brief rationale + - When you lack critical information, ask specific questions rather than making assumptions + - If project documentation conflicts with the request, point this out and ask for clarification + - Prioritize maintainability and consistency with existing patterns + +## Quality Standards: + +- Code must be production-ready, not prototype or example code +- Follow DRY (Don't Repeat Yourself) and SOLID principles unless project standards specify otherwise +- Ensure proper error handling appropriate to the project's patterns +- Write code that integrates seamlessly with existing architecture +- Consider performance, security, and scalability as defined in project standards + +## Token Efficiency: + +- Provide complete, working code but avoid unnecessary examples or demonstrations +- If showing a pattern, demonstrate it once concisely rather than multiple verbose examples +- Focus on the specific implementation needed, not general tutorials +- Use comments sparingly - only where code intent isn't self-evident + +## When You Don't Know: + +- Explicitly state what information you need +- Ask targeted questions to gather requirements +- If you have suggestions, present them as options with clear trade-offs +- Never guess at critical architectural decisions + +Remember: Your value lies in creating code that perfectly fits the existing project architecture while maintaining high quality standards. Always prioritize consistency with project documentation and clarity in communication. diff --git a/projects/-home-zuf-dev-code/memory/MEMORY.md b/projects/-home-zuf-dev-code/memory/MEMORY.md new file mode 100644 index 0000000..5ed56c8 --- /dev/null +++ b/projects/-home-zuf-dev-code/memory/MEMORY.md @@ -0,0 +1,77 @@ +# Project Memory + +## User: zuf (Ondrej Trochta) +- Git user: TrochtaOndrej / TrochtaOndrej@gmail.com +- Sudo: NOPASSWD configured via `/etc/sudoers.d/99-claude-nopasswd` — always use `sudo` directly, never ask for password +- Language: Czech preferred + +## Server Environment +- Hostname: `zuf-Virtual-Machine` (VM) +- IP: 192.168.1.33 (LAN), Tailscale: 100.126.183.1 +- mDNS: enabled via `/etc/systemd/resolved.conf.d/99-enable-mdns.conf` +- Accessible as: `zuf-Virtual-Machine.local` from LAN + +## Projects in /home/zuf/dev/code/ +- **CentralStore** — has `dev` branch, remote on GitHub +- **CSAI** — has `dev` branch (default), remote on GitHub +- **OTCryptoRobot** — has `dev` branch (default), remote on GitHub +- **ClaudeTaskManager (CTM)** — Node.js dashboard on port 3333, systemd service `claude-task-manager.service` +- **TaskManagerMobile (NudgeMobile)** — Flutter app (starší projekt), `dev` branch, remote gitea (`AI/TaskManagerMobile.git`) +- **NuggeMobile** — Flutter app (dříve KittieMobile), ADHD task manager s virtual pet, `dev` branch, remote gitea (`AI/KittieMobile.git`) + +## AITM Task Creation — DULEZITE +- VZDY pouzit `usePipeline: true` a `autoMergeDev: true` (jinak task bezi jako plain session a nema auto-merge!) +- VZDY pouzit `useWorktree: true` +- VZDY pouzit heredoc format pro curl body (`--data-binary @- <<'ENDJSON'`) — nikoliv `-d` s inline JSON +- Prompt VZDY zacina "READ CLAUDE.md first" a je anglicky +- Prompt musi byt self-contained (vsechny detaily primo v promptu) +- Fix REST API: commit 2f3306c v CTM — RestApi.ts ted uklada usePipeline/autoMergeDev/skipSteps +- Monitor skripty: `ctm-monitor.mjs` (global, 30min zombie), `kittie-monitor.mjs` (tmm tab, 5min zombie) + +## Parallel Task Execution — Fixes (commit c2a2901) +- **mergeTaskToDev**: async s mutex per tabRoot + push verification (viz `parallel-tasks-bugs.md`) +- **Session key**: worktree tasks pouzivaji `taskId` jako session key (ne `projectId`) +- **isTabBusy**: worktree tasky obchazeji isTabBusy check +- **pushRemote**: `"gitea"` v config.json (ne origin/GitHub) +- **task:archive**: akceptuje i stopped/failed (nejen finished) + +## Workflow: TASK keyword = Full Dev Cycle +Když uživatel napíše **`TASK: popis tasku`**, provede se automaticky celý vývojový cyklus: +1. **Branch** — nová branch z `dev` (`feature/...` nebo `fix/...`) +2. **Implementace** — provedení tasku, commit +3. **Review** — self-review kódu (kvalita, SOLID, coding standards) +4. **Fix review** — oprava nalezených problémů, commit +5. **Test** — `dotnet build` + `dotnet test`, oprava padajících testů +6. **Merge** — merge do `dev`, smazání feature branch + +Modifikátory (přidat za TASK): +- **"bez review"** — přeskočí kroky 3-4 +- **"bez merge"** — zastaví se před mergem, čeká na souhlas + +## Key Services (all on 0.0.0.0, accessible from LAN) +- CTM (ClaudeTaskManager): port 3333, systemd: `claude-task-manager.service` +- CSAI.Api: port 5208, systemd: `csai-api.service` +- CSAI.Web: port 5274, systemd: `csai-web.service` +- RobotFE: port 5000, systemd: `robot-fe.service` +- RobotCBBE: port 5010, systemd: `robot-cbbe.service` +- MarketDataService: ports 5020/5021 (already 0.0.0.0) +- CTM-WEB: port 8080, systemd: `ctm-web.service` +- n8n: port 5679 (localhost), systemd: `n8n.service` +- All systemd services are `--user` level, linger enabled +- dotnet path: `/home/zuf/.dotnet/dotnet` + +## Skills a Agents - struktura +Skills a agents se nacitaji podle **CWD**. Globalni + projektove se kombinuji. +- Globalni (`~/.claude/skills/`, `~/.claude/agents/`): code-review, deploy, full-cycle, serilog-log-analyzer, status + 5 agentu +- OTCryptoRobot: cryptorobot-blazor-architect, cryptorobot-ef-expert +- CSAI: csai-blazor-architect, csai-ef-expert + +## Subagenty - volba modelu (uspora nakladu) +- **haiku** - research, hledani souboru, jednoduche ukoly +- **sonnet** - code review, opravy kodu, merge branch, generovani kodu +- **opus** - jen hlavni konverzace a slozite architektonicke veci +- VZDY pouzivat nejlevnejsi model ktery zvladne dany ukol + +## MCP servery - uspora tokenu +- Playwright a paretools-dotnet jsou **vypnute** v `~/.claude/settings.local.json` +- Kdyz uzivatel chce Playwright testy, upozornit ho: "Zapni Playwright pres `/mcp` a restartuj konverzaci" diff --git a/projects/-home-zuf-dev-code/memory/parallel-tasks-bugs.md b/projects/-home-zuf-dev-code/memory/parallel-tasks-bugs.md new file mode 100644 index 0000000..b31b221 --- /dev/null +++ b/projects/-home-zuf-dev-code/memory/parallel-tasks-bugs.md @@ -0,0 +1,38 @@ +# Parallel Tasks — Bugs & Fixes (2026-03-18) + +## Bug 1: Race condition in mergeTaskToDev (CRITICAL — DATA LOSS) +- **Fixed in**: commit `c2a2901` on dev +- **File**: `src/git/WorktreeManager.ts` +- **Root cause**: Multiple parallel tasks finishing at same time all call `mergeTaskToDev()` which does `git checkout dev` + `git merge` + `git push` — but concurrent checkouts corrupt working dir +- **Fix**: Async function with per-tabRoot mutex lock (Promise chain). Push verification before worktree/branch cleanup. +- **Evidence**: 3 tasks completed ($1.82-$2.43 each) but code lost — git reflog showed only `checkout: moving from dev to dev` with no merge commits + +## Bug 2: Session key collision for parallel worktree tasks +- **Fixed in**: commit `c2a2901` on dev +- **File**: `src/sessions/SessionManager.ts` +- **Root cause**: Session key was always `projectId` — when multiple tasks share same projectId, each new session kills the previous via `killSession(ctx, projectId)` +- **Fix**: `sessionKey = (useWorktree && taskId) ? taskId : projectId` — worktree tasks use taskId as key + +## Bug 3: task:archive only accepted finished tasks +- **Fixed in**: commit `c2a2901` on dev +- **File**: `src/socket/TaskSocketHandlers.ts` +- **Fix**: Archive now accepts `finished`, `stopped`, `failed` statuses + +## Bug 4: pushRemote not configured +- **Fixed in**: commit `c2a2901` on dev +- **File**: `config.json` +- **Root cause**: No `pushRemote` field, defaulting to `origin` (GitHub) instead of `gitea` +- **Fix**: Added `"pushRemote": "gitea"` to config.json + +## Bug 5: isTabBusy blocks parallel worktree tasks +- **Fixed in**: commit `c2a2901` on dev +- **File**: `src/socket/TaskSocketHandlers.ts` +- **Root cause**: `task:run` handler checks `isTabBusy()` and returns `queue-blocked` even for worktree tasks that can run independently +- **Fix**: Added `&& !useWorktree` condition to bypass check for worktree tasks + +## Key Lesson +- Paralelní tasky s worktree MUSÍ mít: + 1. Unikátní session key (taskId, ne projectId) + 2. Serializované merge operace (mutex per tabRoot) + 3. Push verification před smazáním worktree/branch + 4. Správný pushRemote (gitea, ne origin) diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..5b935a9 --- /dev/null +++ b/settings.json @@ -0,0 +1,32 @@ +{ + "permissions": { + "allow": [ + "Bash(*)", + "Read(*)", + "Edit(*)", + "Write(*)", + "Glob(*)", + "Grep(*)", + "WebFetch(*)", + "WebSearch(*)", + "NotebookEdit(*)", + "mcp__playwright__*", + "mcp__nuget__*", + "mcp__dotnet__*", + "mcp__paretools-dotnet__*", + "Bash(pip list 2>/dev/null | grep -i kanban; pip3 list 2>/dev/null | grep -i kanban; echo \"---\"; pip list 2>/dev/null | grep -i vibe; pip3 list 2>/dev/null | grep -i vibe)", + "Bash(dpkg -l 2>/dev/null | grep -i kanban; echo \"---\"; snap list 2>/dev/null | grep -i kanban)", + "Bash(flatpak list 2>/dev/null | grep -i kanban; echo \"EXIT:$?\")", + "Bash(sqlite3 /home/zuf/.local/share/vibe-kanban/db.v2.sqlite \".tables\" 2>/dev/null | head -20)", + "Bash(sqlite3 /home/zuf/.local/share/vibe-kanban/db.v2.sqlite \"SELECT name FROM sqlite_master WHERE type='table';\" 2>/dev/null)", + "Bash(which sqlite3 2>/dev/null; apt list --installed 2>/dev/null | grep sqlite)", + "Bash(cp /home/zuf/.local/share/applications/vibe-kanban.desktop /home/zuf/Desktop/vibe-kanban.desktop && chmod +x /home/zuf/Desktop/vibe-kanban.desktop)", + "Bash(which snap flatpak apt dpkg 2>/dev/null; dpkg --print-architecture 2>/dev/null)", + "mcp__playwright__browser_click", + "mcp__playwright__browser_take_screenshot" + ], + "deny": [], + "defaultMode": "default" + }, + "skipDangerousModePermissionPrompt": true +} diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md new file mode 100644 index 0000000..a354cb5 --- /dev/null +++ b/skills/code-review/SKILL.md @@ -0,0 +1,27 @@ +--- +name: code-review +description: Performs thorough code review checking quality, security, patterns, and best practices for trading bot code +--- + +# Code Review Skill + +Performs comprehensive code review for the OTCryptoRobot trading bot. + +## Trigger +When user asks to review code, check changes, or review a PR. + +## Process + +1. **Identify Changes** - Run `git diff` to see changes +2. **Review Each File** - Check against patterns in CLAUDE.md +3. **Check Categories** + - [ ] Generic `` pattern compliance + - [ ] Async/await correctness (critical for real-time trading) + - [ ] CancellationToken propagation + - [ ] EF Core patterns (AsNoTracking, DbContextFactory) + - [ ] DI registration in RegisterService.cs + - [ ] Error handling (no empty catch blocks) + - [ ] Security (API keys, credentials) + - [ ] Thread safety (singleton services) + - [ ] SignalR/gRPC patterns +4. **Output** - Summary with severity-rated issues diff --git a/skills/full-cycle/SKILL.md b/skills/full-cycle/SKILL.md new file mode 100644 index 0000000..3268107 --- /dev/null +++ b/skills/full-cycle/SKILL.md @@ -0,0 +1,61 @@ +--- +name: full-cycle +description: Full development cycle - branch, implement, review, fix, test, merge to dev +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep + - Agent + - TaskCreate + - TaskUpdate + - TaskList +--- + +# Full Development Cycle + +Execute a complete development cycle for the given task: `$ARGUMENTS` + +## Workflow Steps + +### 1. Branch +- Create a feature/fix branch from `dev`: `git checkout dev && git pull && git checkout -b /` +- Branch naming: `feature/` for new features, `fix/` for bug fixes, `refactor/` for refactoring + +### 2. Investigate +- Read CLAUDE.md and relevant codebase files +- Understand the current architecture and patterns +- Identify all files that need changes + +### 3. Implement +- Make the necessary code changes +- Follow project conventions from CLAUDE.md (primary constructors, file-scoped namespaces, XML docs) +- Keep changes minimal and focused + +### 4. Build & Verify +- Run `dotnet build OTCryptoRobot.sln` - must have 0 errors +- Check for new warnings in changed files + +### 5. Self-Review +- Review all changes with `git diff` +- Check for: security issues, missing null checks, async/await correctness, proper error handling +- Fix any issues found + +### 6. Test (if applicable) +- Run relevant tests: `dotnet test` +- If services need restart, use systemctl --user restart + +### 7. Commit & Merge +- Stage specific files (not `git add -A`) +- Write descriptive commit message with Co-Authored-By +- Merge to dev: `git checkout dev && git merge --ff-only` +- Delete feature branch: `git branch -d ` + +## Important Rules +- Never push to remote without explicit user request +- Never use --force or --no-verify +- If build fails, fix and create NEW commit (don't amend) +- All I/O must be async, all public members need XML docs diff --git a/skills/mastering-typescript/.skillfish.json b/skills/mastering-typescript/.skillfish.json new file mode 100644 index 0000000..568d5fd --- /dev/null +++ b/skills/mastering-typescript/.skillfish.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "name": "mastering-typescript", + "owner": "SpillwaveSolutions", + "repo": "mastering-typescript-skill", + "path": "mastering-typescript", + "branch": "main", + "sha": "12ab8354f759d283e6849a683f78e87ecc9dbb97", + "source": "manual" +} \ No newline at end of file diff --git a/skills/mastering-typescript/SKILL.md b/skills/mastering-typescript/SKILL.md new file mode 100644 index 0000000..a7f8108 --- /dev/null +++ b/skills/mastering-typescript/SKILL.md @@ -0,0 +1,441 @@ +--- +name: mastering-typescript +description: | + Master enterprise-grade TypeScript development with type-safe patterns, modern tooling, and framework integration. This skill provides comprehensive guidance for TypeScript 5.9+, covering type system fundamentals (generics, mapped types, conditional types, satisfies operator), enterprise patterns (error handling, validation with Zod), React integration for type-safe frontends, NestJS for scalable APIs, and LangChain.js for AI applications. Use when building type-safe applications, migrating JavaScript codebases, configuring modern toolchains (Vite 7, pnpm, ESLint, Vitest), implementing advanced type patterns, or comparing TypeScript with Java/Python approaches. +version: 1.0.0 +category: programming-languages +triggers: + - typescript + - ts + - type-safe + - generics + - nestjs typescript + - react typescript + - typescript migration + - tsconfig + - type guards + - mapped types + - conditional types + - satisfies operator + - zod validation +author: Richard Hightower +license: MIT +tags: + - typescript + - type-safety + - enterprise + - react + - nestjs + - langchain + - vite +--- + +# Mastering Modern TypeScript + +Build enterprise-grade, type-safe applications with TypeScript 5.9+. + +> **Compatibility:** TypeScript 5.9+, Node.js 22 LTS, Vite 7, NestJS 11, React 19 + +## Quick Start + +```bash +# Initialize TypeScript project with ESM +pnpm create vite@latest my-app --template vanilla-ts +cd my-app && pnpm install + +# Configure strict TypeScript +cat > tsconfig.json << 'EOF' +{ + "compilerOptions": { + "target": "ES2024", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} +EOF +``` + +## When to Use This Skill + +Use when: +- Building type-safe React, NestJS, or Node.js applications +- Migrating JavaScript codebases to TypeScript +- Implementing advanced type patterns (generics, mapped types, conditional types) +- Configuring modern TypeScript toolchains (Vite, pnpm, ESLint) +- Designing type-safe API contracts with Zod validation +- Comparing TypeScript approaches with Java or Python + +## Project Setup Checklist + +Before starting any TypeScript project: + +``` +- [ ] Use pnpm for package management (faster, disk-efficient) +- [ ] Configure ESM-first (type: "module" in package.json) +- [ ] Enable strict mode in tsconfig.json +- [ ] Set up ESLint with @typescript-eslint +- [ ] Add Prettier for consistent formatting +- [ ] Configure Vitest for testing +``` + +## Type System Quick Reference + +### Primitive Types + +```typescript +const name: string = "Alice"; +const age: number = 30; +const active: boolean = true; +const id: bigint = 9007199254740991n; +const key: symbol = Symbol("unique"); +``` + +### Union and Intersection Types + +```typescript +// Union: value can be one of several types +type Status = "pending" | "approved" | "rejected"; + +// Intersection: value must satisfy all types +type Employee = Person & { employeeId: string }; + +// Discriminated union for type-safe handling +type Result = + | { success: true; data: T } + | { success: false; error: string }; + +function handleResult(result: Result): T | null { + if (result.success) { + return result.data; // TypeScript knows data exists here + } + console.error(result.error); + return null; +} +``` + +### Type Guards + +```typescript +// typeof guard +function process(value: string | number): string { + if (typeof value === "string") { + return value.toUpperCase(); + } + return value.toFixed(2); +} + +// Custom type guard +interface User { type: "user"; name: string } +interface Admin { type: "admin"; permissions: string[] } + +function isAdmin(person: User | Admin): person is Admin { + return person.type === "admin"; +} +``` + +### The `satisfies` Operator (TS 5.0+) + +Validate type conformance while preserving inference: + +```typescript +// Problem: Type assertion loses specific type info +const colors1 = { + red: "#ff0000", + green: "#00ff00" +} as Record; + +colors1.red.toUpperCase(); // OK, but red could be undefined + +// Solution: satisfies preserves literal types +const colors2 = { + red: "#ff0000", + green: "#00ff00" +} satisfies Record; + +colors2.red.toUpperCase(); // OK, and TypeScript knows red exists +``` + +## Generics Patterns + +### Basic Generic Function + +```typescript +function first(items: T[]): T | undefined { + return items[0]; +} + +const num = first([1, 2, 3]); // number | undefined +const str = first(["a", "b"]); // string | undefined +``` + +### Constrained Generics + +```typescript +interface HasLength { + length: number; +} + +function logLength(item: T): T { + console.log(item.length); + return item; +} + +logLength("hello"); // OK: string has length +logLength([1, 2, 3]); // OK: array has length +logLength(42); // Error: number has no length +``` + +### Generic API Response Wrapper + +```typescript +interface ApiResponse { + data: T; + status: number; + timestamp: Date; +} + +async function fetchUser(id: string): Promise> { + const response = await fetch(`/api/users/${id}`); + const data = await response.json(); + return { + data, + status: response.status, + timestamp: new Date() + }; +} +``` + +## Utility Types Reference + +| Type | Purpose | Example | +|------|---------|---------| +| `Partial` | All properties optional | `Partial` | +| `Required` | All properties required | `Required` | +| `Pick` | Select specific properties | `Pick` | +| `Omit` | Exclude specific properties | `Omit` | +| `Record` | Object with typed keys/values | `Record` | +| `ReturnType` | Extract function return type | `ReturnType` | +| `Parameters` | Extract function parameters | `Parameters` | +| `Awaited` | Unwrap Promise type | `Awaited>` | + +## Conditional Types + +```typescript +// Basic conditional type +type IsString = T extends string ? true : false; + +// Extract array element type +type ArrayElement = T extends (infer E)[] ? E : never; + +type Numbers = ArrayElement; // number +type Strings = ArrayElement; // string + +// Practical: Extract Promise result type +type UnwrapPromise = T extends Promise ? R : T; +``` + +## Mapped Types + +```typescript +// Make all properties readonly +type Immutable = { + readonly [K in keyof T]: T[K]; +}; + +// Make all properties nullable +type Nullable = { + [K in keyof T]: T[K] | null; +}; + +// Create getter functions for each property +type Getters = { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +}; + +interface Person { name: string; age: number } +type PersonGetters = Getters; +// { getName: () => string; getAge: () => number } +``` + +## Framework Integration + +### React with TypeScript + +```typescript +// Typed functional component +interface ButtonProps { + label: string; + onClick: () => void; + variant?: "primary" | "secondary"; +} + +const Button: React.FC = ({ label, onClick, variant = "primary" }) => ( + +); + +// Typed hooks +const [count, setCount] = useState(0); +const userRef = useRef(null); +``` + +### NestJS with TypeScript + +```typescript +// Type-safe DTO with class-validator +import { IsString, IsEmail, MinLength } from 'class-validator'; + +class CreateUserDto { + @IsString() + @MinLength(2) + name: string; + + @IsEmail() + email: string; +} + +// Or with Zod (modern approach) +import { z } from 'zod'; + +const CreateUserSchema = z.object({ + name: z.string().min(2), + email: z.string().email() +}); + +type CreateUserDto = z.infer; +``` + +See [react-integration.md](references/react-integration.md) and [nestjs-integration.md](references/nestjs-integration.md) for detailed patterns. + +## Validation with Zod + +```typescript +import { z } from 'zod'; + +// Define schema +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1).max(100), + email: z.string().email(), + role: z.enum(["user", "admin", "moderator"]), + createdAt: z.coerce.date() +}); + +// Infer TypeScript type from schema +type User = z.infer; + +// Validate at runtime +function parseUser(data: unknown): User { + return UserSchema.parse(data); // Throws ZodError if invalid +} + +// Safe parsing (returns result object) +const result = UserSchema.safeParse(data); +if (result.success) { + console.log(result.data); // Typed as User +} else { + console.error(result.error.issues); +} +``` + +## Modern Toolchain (2025) + +| Tool | Version | Purpose | +|------|---------|---------| +| TypeScript | 5.9+ | Type checking and compilation | +| Node.js | 22 LTS | Runtime environment | +| Vite | 7.x | Build tool and dev server | +| pnpm | 9.x | Package manager | +| ESLint | 9.x | Linting with flat config | +| Vitest | 3.x | Testing framework | +| Prettier | 3.x | Code formatting | + +### ESLint Flat Config (ESLint 9+) + +```javascript +// eslint.config.js +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.strictTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + } +); +``` + +## Migration Strategies + +### Incremental Migration + +1. Add `allowJs: true` and `checkJs: false` to tsconfig.json +2. Rename files from `.js` to `.ts` one at a time +3. Add type annotations gradually +4. Enable stricter options incrementally + +### JSDoc for Gradual Typing + +```javascript +// Before full migration, use JSDoc +/** + * @param {string} name + * @param {number} age + * @returns {User} + */ +function createUser(name, age) { + return { name, age }; +} +``` + +See [enterprise-patterns.md](references/enterprise-patterns.md) for comprehensive migration guides. + +## Common Mistakes + +| Mistake | Problem | Fix | +|---------|---------|-----| +| Using `any` liberally | Defeats type safety | Use `unknown` and narrow | +| Ignoring strict mode | Misses null/undefined bugs | Enable all strict options | +| Type assertions (`as`) | Can hide type errors | Use `satisfies` or guards | +| Enum for simple unions | Generates runtime code | Use literal unions instead | +| Not validating API data | Runtime type mismatches | Use Zod at boundaries | + +## Cross-Language Comparison + +| Feature | TypeScript | Java | Python | +|---------|------------|------|--------| +| Type System | Structural | Nominal | Gradual (duck typing) | +| Nullability | Explicit (`T \| null`) | `@Nullable` annotations | Optional via typing | +| Generics | Type-level, erased | Type-level, erased | Runtime via typing | +| Interfaces | Structural matching | Must implement | Protocol (3.8+) | +| Enums | Avoid (use unions) | First-class | Enum class | + +## Reference Files + +- [type-system.md](references/type-system.md) — Complete type system guide +- [generics.md](references/generics.md) — Advanced generics patterns +- [enterprise-patterns.md](references/enterprise-patterns.md) — Error handling, validation, architecture +- [react-integration.md](references/react-integration.md) — React + TypeScript patterns +- [nestjs-integration.md](references/nestjs-integration.md) — NestJS API development +- [toolchain.md](references/toolchain.md) — Modern build tools configuration + +## Assets + +- [tsconfig-template.json](assets/tsconfig-template.json) — Strict enterprise config +- [eslint-template.js](assets/eslint-template.js) — ESLint 9 flat config + +## Scripts + +- [validate-setup.sh](scripts/validate-setup.sh) — Verify TypeScript environment diff --git a/skills/mastering-typescript/assets/eslint-template.js b/skills/mastering-typescript/assets/eslint-template.js new file mode 100644 index 0000000..f384a7b --- /dev/null +++ b/skills/mastering-typescript/assets/eslint-template.js @@ -0,0 +1,89 @@ +// eslint.config.js - ESLint 9+ Flat Config for TypeScript +// Copy this file to your project root as eslint.config.js + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + // Base ESLint recommendations + eslint.configs.recommended, + + // TypeScript strict type-checking + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + + // TypeScript parser configuration + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + } + }, + + // Custom TypeScript rules + { + rules: { + // Allow unused vars with underscore prefix + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + + // Enforce type imports for better tree-shaking + '@typescript-eslint/consistent-type-imports': ['error', { + prefer: 'type-imports', + fixStyle: 'inline-type-imports' + }], + + // Prevent unhandled promises + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + + // Prevent awaiting non-promises + '@typescript-eslint/await-thenable': 'error', + + // Prefer nullish coalescing + '@typescript-eslint/prefer-nullish-coalescing': 'error', + + // Prefer optional chaining + '@typescript-eslint/prefer-optional-chain': 'error', + + // Consistent type assertions + '@typescript-eslint/consistent-type-assertions': ['error', { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'never' + }], + + // Naming conventions + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'interface', + format: ['PascalCase'] + }, + { + selector: 'typeAlias', + format: ['PascalCase'] + }, + { + selector: 'enum', + format: ['PascalCase'] + } + ] + } + }, + + // Ignore patterns + { + ignores: [ + 'dist/**', + 'build/**', + 'node_modules/**', + 'coverage/**', + '*.config.js', + '*.config.ts' + ] + } +); diff --git a/skills/mastering-typescript/assets/tsconfig-template.json b/skills/mastering-typescript/assets/tsconfig-template.json new file mode 100644 index 0000000..465a06e --- /dev/null +++ b/skills/mastering-typescript/assets/tsconfig-template.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + // Language and Environment + "target": "ES2024", + "lib": ["ES2024"], + "module": "ESNext", + "moduleResolution": "bundler", + + // Strict Type Checking (Enterprise-Grade) + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + + // Module Handling + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + + // Output + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + + // Path Aliases (adjust as needed) + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + // Performance + "skipLibCheck": true, + "incremental": true, + "tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/skills/mastering-typescript/references/enterprise-patterns.md b/skills/mastering-typescript/references/enterprise-patterns.md new file mode 100644 index 0000000..68d875b --- /dev/null +++ b/skills/mastering-typescript/references/enterprise-patterns.md @@ -0,0 +1,536 @@ +# Enterprise Patterns Reference + +> **Load when:** User asks about error handling, validation, project architecture, migration strategies, or large-scale TypeScript patterns. + +Proven patterns for building maintainable TypeScript applications. + +## Contents + +- [Error Handling](#error-handling) +- [Validation Patterns](#validation-patterns) +- [Project Organization](#project-organization) +- [Migration Strategies](#migration-strategies) +- [Security Patterns](#security-patterns) + +--- + +## Error Handling + +### Result Type Pattern + +Instead of throwing exceptions, return typed results: + +```typescript +// Define Result type +type Result = + | { success: true; data: T } + | { success: false; error: E }; + +// Helper functions +function ok(data: T): Result { + return { success: true, data }; +} + +function err(error: E): Result { + return { success: false, error }; +} + +// Usage +interface ValidationError { + field: string; + message: string; +} + +function parseEmail(input: string): Result { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!emailRegex.test(input)) { + return err({ field: "email", message: "Invalid email format" }); + } + + return ok(input.toLowerCase()); +} + +// Consuming Result +const result = parseEmail(userInput); + +if (result.success) { + console.log(`Valid email: ${result.data}`); +} else { + console.error(`Error in ${result.error.field}: ${result.error.message}`); +} +``` + +### Typed Error Classes + +```typescript +// Base application error +abstract class AppError extends Error { + abstract readonly code: string; + abstract readonly statusCode: number; + + constructor(message: string) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +// Specific error types +class NotFoundError extends AppError { + readonly code = "NOT_FOUND"; + readonly statusCode = 404; + + constructor(resource: string, id: string) { + super(`${resource} with id ${id} not found`); + } +} + +class ValidationError extends AppError { + readonly code = "VALIDATION_ERROR"; + readonly statusCode = 400; + + constructor( + message: string, + public readonly fields: Record + ) { + super(message); + } +} + +class UnauthorizedError extends AppError { + readonly code = "UNAUTHORIZED"; + readonly statusCode = 401; + + constructor(message = "Authentication required") { + super(message); + } +} + +// Type guard for app errors +function isAppError(error: unknown): error is AppError { + return error instanceof AppError; +} + +// Error handler +function handleError(error: unknown): { status: number; body: object } { + if (isAppError(error)) { + return { + status: error.statusCode, + body: { + code: error.code, + message: error.message, + ...(error instanceof ValidationError && { fields: error.fields }) + } + }; + } + + console.error("Unexpected error:", error); + return { + status: 500, + body: { code: "INTERNAL_ERROR", message: "Internal server error" } + }; +} +``` + +--- + +## Validation Patterns + +### Zod Schema Validation + +```typescript +import { z } from 'zod'; + +// Define schemas +const UserSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1).max(100), + email: z.string().email(), + age: z.number().int().min(0).max(150).optional(), + role: z.enum(["user", "admin", "moderator"]), + metadata: z.record(z.string()).optional() +}); + +// Infer TypeScript type +type User = z.infer; + +// Create DTO schemas +const CreateUserSchema = UserSchema.omit({ id: true }); +type CreateUserDto = z.infer; + +const UpdateUserSchema = UserSchema.partial().omit({ id: true }); +type UpdateUserDto = z.infer; + +// Validation functions +function validateCreateUser(data: unknown): Result { + const result = CreateUserSchema.safeParse(data); + + if (result.success) { + return ok(result.data); + } + + return err(result.error); +} + +// Transform Zod errors to user-friendly format +function formatZodError(error: z.ZodError): Record { + const formatted: Record = {}; + + for (const issue of error.issues) { + const path = issue.path.join("."); + if (!formatted[path]) { + formatted[path] = []; + } + formatted[path].push(issue.message); + } + + return formatted; +} +``` + +### Branded Types for Validation + +```typescript +// Branded/Nominal types +declare const EmailBrand: unique symbol; +type Email = string & { readonly [EmailBrand]: true }; + +declare const UserIdBrand: unique symbol; +type UserId = string & { readonly [UserIdBrand]: true }; + +// Validation functions that return branded types +function validateEmail(input: string): Email { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(input)) { + throw new ValidationError("Invalid email", { email: ["Invalid format"] }); + } + return input as Email; +} + +function validateUserId(input: string): UserId { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(input)) { + throw new ValidationError("Invalid user ID", { id: ["Must be UUID"] }); + } + return input as UserId; +} + +// Usage: Functions require validated types +function sendEmail(to: Email, subject: string): void { + // to is guaranteed to be valid email +} + +function getUser(id: UserId): Promise { + // id is guaranteed to be valid UUID +} + +// Compiler enforces validation +sendEmail("invalid", "Hello"); // Error: string not assignable to Email +sendEmail(validateEmail("a@b.com"), "Hello"); // OK +``` + +--- + +## Project Organization + +### Feature-Based Structure + +``` +src/ +├── features/ +│ ├── users/ +│ │ ├── index.ts # Public exports (barrel) +│ │ ├── user.types.ts # Types and interfaces +│ │ ├── user.schema.ts # Zod schemas +│ │ ├── user.service.ts # Business logic +│ │ ├── user.repository.ts # Data access +│ │ ├── user.controller.ts # HTTP handlers +│ │ └── __tests__/ +│ │ ├── user.service.test.ts +│ │ └── user.controller.test.ts +│ ├── auth/ +│ │ ├── index.ts +│ │ ├── auth.types.ts +│ │ └── ... +│ └── posts/ +│ └── ... +├── shared/ +│ ├── types/ +│ │ ├── result.ts +│ │ └── pagination.ts +│ ├── utils/ +│ │ ├── validation.ts +│ │ └── date.ts +│ └── errors/ +│ └── app-error.ts +├── infrastructure/ +│ ├── database/ +│ │ └── client.ts +│ ├── cache/ +│ │ └── redis.ts +│ └── logging/ +│ └── logger.ts +└── config/ + ├── index.ts + └── env.ts +``` + +### Barrel Exports + +```typescript +// features/users/index.ts +export type { User, CreateUserDto, UpdateUserDto } from './user.types'; +export { UserSchema, CreateUserSchema } from './user.schema'; +export { UserService } from './user.service'; +export { UserController } from './user.controller'; + +// Don't export repository (internal detail) +// Don't export internal helper functions + +// Usage in other modules +import { User, UserService } from '@/features/users'; +``` + +### Path Aliases + +```json +// tsconfig.json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@features/*": ["src/features/*"], + "@shared/*": ["src/shared/*"], + "@config/*": ["src/config/*"] + } + } +} +``` + +--- + +## Migration Strategies + +### Incremental Migration from JavaScript + +**Phase 1: Enable TypeScript alongside JavaScript** + +```json +// tsconfig.json +{ + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "outDir": "./dist", + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": false, + "noImplicitAny": false + }, + "include": ["src/**/*"] +} +``` + +**Phase 2: Rename files gradually** + +```bash +# Convert one file at a time +mv src/utils/helpers.js src/utils/helpers.ts + +# Add minimal type annotations +# Fix any type errors +# Run tests to verify +``` + +**Phase 3: Enable stricter checks incrementally** + +```json +// Progression of strict options +{ + "compilerOptions": { + // Step 1: Basic strictness + "noImplicitAny": true, + + // Step 2: Null safety + "strictNullChecks": true, + + // Step 3: Full strict mode + "strict": true, + + // Step 4: Extra safety (optional) + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true + } +} +``` + +### JSDoc for Gradual Typing + +```javascript +// Before full migration, use JSDoc +/** + * @typedef {Object} User + * @property {string} id + * @property {string} name + * @property {string} email + */ + +/** + * Find user by ID + * @param {string} id - User ID + * @returns {Promise} + */ +async function findUser(id) { + // implementation +} + +/** + * @template T + * @param {T[]} items + * @returns {T | undefined} + */ +function first(items) { + return items[0]; +} +``` + +### CommonJS to ESM Migration + +```json +// package.json +{ + "type": "module" +} +``` + +```typescript +// Before (CommonJS) +const express = require('express'); +const { UserService } = require('./user.service'); +module.exports = { router }; + +// After (ESM) +import express from 'express'; +import { UserService } from './user.service.js'; // Note .js extension +export { router }; +``` + +--- + +## Security Patterns + +### Input Sanitization + +```typescript +import { z } from 'zod'; +import DOMPurify from 'isomorphic-dompurify'; + +// Sanitized string schema +const SanitizedString = z.string().transform((val) => { + return DOMPurify.sanitize(val.trim()); +}); + +// HTML content schema (for rich text) +const HtmlContentSchema = z.string().transform((val) => { + return DOMPurify.sanitize(val, { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], + ALLOWED_ATTR: ['href', 'target'] + }); +}); + +// SQL-safe identifier +const SafeIdentifierSchema = z.string().regex( + /^[a-zA-Z_][a-zA-Z0-9_]*$/, + "Invalid identifier" +); +``` + +### Type-Safe Environment Variables + +```typescript +import { z } from 'zod'; + +const EnvSchema = z.object({ + NODE_ENV: z.enum(['development', 'production', 'test']), + PORT: z.coerce.number().default(3000), + DATABASE_URL: z.string().url(), + JWT_SECRET: z.string().min(32), + REDIS_URL: z.string().url().optional() +}); + +// Validate on startup +function loadEnv() { + const result = EnvSchema.safeParse(process.env); + + if (!result.success) { + console.error('Invalid environment variables:'); + console.error(result.error.format()); + process.exit(1); + } + + return result.data; +} + +export const env = loadEnv(); + +// Usage: fully typed +env.PORT; // number +env.NODE_ENV; // "development" | "production" | "test" +env.REDIS_URL; // string | undefined +``` + +### Secure API Response Types + +```typescript +// Never expose internal fields +interface InternalUser { + id: string; + name: string; + email: string; + passwordHash: string; + internalNotes: string; +} + +// Public API response (Pick only safe fields) +type PublicUser = Pick; + +// Or explicitly define +interface UserResponse { + id: string; + name: string; + email: string; +} + +// Transform function +function toPublicUser(user: InternalUser): UserResponse { + return { + id: user.id, + name: user.name, + email: user.email + }; +} +``` + +### Rate Limiting Types + +```typescript +interface RateLimitConfig { + windowMs: number; + maxRequests: number; +} + +interface RateLimitResult { + allowed: boolean; + remaining: number; + resetAt: Date; +} + +const rateLimits: Record = { + api: { windowMs: 60000, maxRequests: 100 }, + auth: { windowMs: 300000, maxRequests: 5 }, + upload: { windowMs: 3600000, maxRequests: 10 } +} as const satisfies Record; +``` diff --git a/skills/mastering-typescript/references/generics.md b/skills/mastering-typescript/references/generics.md new file mode 100644 index 0000000..1da1e5c --- /dev/null +++ b/skills/mastering-typescript/references/generics.md @@ -0,0 +1,499 @@ +# Generics Reference + +> **Load when:** User asks about generics, mapped types, conditional types, template literal types, or reusable type patterns. + +Advanced generics and type-level programming patterns. + +## Contents + +- [Generic Fundamentals](#generic-fundamentals) +- [Generic Constraints](#generic-constraints) +- [Mapped Types](#mapped-types) +- [Conditional Types](#conditional-types) +- [Template Literal Types](#template-literal-types) +- [Variadic Tuple Types](#variadic-tuple-types) + +--- + +## Generic Fundamentals + +### Basic Generic Function + +```typescript +// Type parameter T can be any type +function identity(value: T): T { + return value; +} + +const str = identity("hello"); // string +const num = identity(42); // number +const obj = identity({ x: 1 }); // { x: number } + +// Explicit type argument (rarely needed) +const explicit = identity("hello"); +``` + +### Generic Interfaces + +```typescript +interface Container { + value: T; + getValue(): T; + setValue(value: T): void; +} + +interface Repository { + findById(id: ID): Promise; + findAll(): Promise; + save(entity: T): Promise; + delete(id: ID): Promise; +} + +// Implementation +class UserRepository implements Repository { + async findById(id: string): Promise { + // implementation + } + // ... other methods +} +``` + +### Generic Classes + +```typescript +class Stack { + private items: T[] = []; + + push(item: T): void { + this.items.push(item); + } + + pop(): T | undefined { + return this.items.pop(); + } + + peek(): T | undefined { + return this.items[this.items.length - 1]; + } + + isEmpty(): boolean { + return this.items.length === 0; + } +} + +const numberStack = new Stack(); +numberStack.push(1); +numberStack.push(2); +const top = numberStack.pop(); // number | undefined +``` + +--- + +## Generic Constraints + +### extends Constraint + +```typescript +// T must have a length property +interface HasLength { + length: number; +} + +function logLength(item: T): T { + console.log(`Length: ${item.length}`); + return item; +} + +logLength("hello"); // OK: string has length +logLength([1, 2, 3]); // OK: array has length +logLength({ length: 10 }); // OK: object has length +logLength(42); // Error: number has no length +``` + +### keyof Constraint + +```typescript +function getProperty(obj: T, key: K): T[K] { + return obj[key]; +} + +interface Person { + name: string; + age: number; +} + +const person: Person = { name: "Alice", age: 30 }; + +const name = getProperty(person, "name"); // string +const age = getProperty(person, "age"); // number +const bad = getProperty(person, "email"); // Error: "email" not in Person +``` + +### Multiple Constraints + +```typescript +interface Printable { + print(): void; +} + +interface Loggable { + log(): string; +} + +// T must satisfy both interfaces +function process(item: T): void { + item.print(); + console.log(item.log()); +} +``` + +### Default Type Parameters + +```typescript +interface ApiResponse { + data?: T; + error?: E; + status: number; +} + +// Uses defaults +const response1: ApiResponse = { status: 200 }; + +// Override data type only +const response2: ApiResponse = { data: user, status: 200 }; + +// Override both +const response3: ApiResponse = { + error: new ValidationError(), + status: 400 +}; +``` + +--- + +## Mapped Types + +### Basic Mapped Types + +```typescript +// Transform all properties to optional +type Partial = { + [K in keyof T]?: T[K]; +}; + +// Transform all properties to required +type Required = { + [K in keyof T]-?: T[K]; +}; + +// Transform all properties to readonly +type Readonly = { + readonly [K in keyof T]: T[K]; +}; + +// Remove readonly modifier +type Mutable = { + -readonly [K in keyof T]: T[K]; +}; +``` + +### Practical Mapped Types + +```typescript +// Make all properties nullable +type Nullable = { + [K in keyof T]: T[K] | null; +}; + +// Make all properties async getters +type AsyncGetters = { + [K in keyof T as `get${Capitalize}`]: () => Promise; +}; + +interface User { + name: string; + email: string; +} + +type UserGetters = AsyncGetters; +// { +// getName: () => Promise; +// getEmail: () => Promise; +// } +``` + +### Key Remapping (as clause) + +```typescript +// Filter keys by type +type FilterByType = { + [K in keyof T as T[K] extends U ? K : never]: T[K]; +}; + +interface Mixed { + name: string; + age: number; + active: boolean; + score: number; +} + +type StringProps = FilterByType; +// { name: string } + +type NumberProps = FilterByType; +// { age: number; score: number } + +// Prefix all keys +type Prefixed = { + [K in keyof T as `${P}${Capitalize}`]: T[K]; +}; + +type PrefixedUser = Prefixed; +// { userName: string; userEmail: string } +``` + +--- + +## Conditional Types + +### Basic Conditional Types + +```typescript +// T extends U ? X : Y +type IsString = T extends string ? true : false; + +type A = IsString; // true +type B = IsString; // false +type C = IsString<"hello">; // true + +// Practical: Extract non-nullable type +type NonNullable = T extends null | undefined ? never : T; + +type D = NonNullable; // string +type E = NonNullable; // number +``` + +### Distributive Conditional Types + +```typescript +// Conditional types distribute over unions +type ToArray = T extends unknown ? T[] : never; + +type StringOrNumberArray = ToArray; +// string[] | number[] (not (string | number)[]) + +// Prevent distribution with tuple +type ToArrayNonDist = [T] extends [unknown] ? T[] : never; + +type Mixed = ToArrayNonDist; +// (string | number)[] +``` + +### infer Keyword + +```typescript +// Extract return type +type ReturnType = T extends (...args: any[]) => infer R ? R : never; + +type FnReturn = ReturnType<() => string>; // string + +// Extract array element type +type ArrayElement = T extends (infer E)[] ? E : never; + +type Element = ArrayElement; // number + +// Extract Promise result +type Awaited = T extends Promise ? Awaited : T; + +type Result = Awaited>>; // string + +// Extract function first parameter +type FirstParam = T extends (first: infer F, ...rest: any[]) => any ? F : never; + +type First = FirstParam<(name: string, age: number) => void>; // string +``` + +### Practical Conditional Types + +```typescript +// API response helper +type ApiResult = + | { success: true; data: T } + | { success: false; error: string }; + +// Extract data type from result +type ExtractData = T extends { success: true; data: infer D } ? D : never; + +type UserResult = ApiResult; +type UserData = ExtractData; // User + +// Type-safe event handlers +type EventHandler = T extends `on${infer Event}` + ? (event: Event) => void + : never; + +type ClickHandler = EventHandler<"onClick">; // (event: "Click") => void +``` + +--- + +## Template Literal Types + +### Basic Template Literals + +```typescript +type Greeting = `Hello, ${string}!`; + +const valid: Greeting = "Hello, World!"; // OK +const invalid: Greeting = "Hi, World!"; // Error + +// Combine with unions +type Size = "small" | "medium" | "large"; +type Color = "red" | "blue" | "green"; + +type ColoredSize = `${Color}-${Size}`; +// "red-small" | "red-medium" | "red-large" | +// "blue-small" | "blue-medium" | "blue-large" | +// "green-small" | "green-medium" | "green-large" +``` + +### String Manipulation Types + +```typescript +// Built-in string manipulation types +type Upper = Uppercase<"hello">; // "HELLO" +type Lower = Lowercase<"HELLO">; // "hello" +type Cap = Capitalize<"hello">; // "Hello" +type Uncap = Uncapitalize<"Hello">; // "hello" + +// Practical: Generate event names +type Event = "click" | "hover" | "focus"; +type EventHandler = `on${Capitalize}`; +// "onClick" | "onHover" | "onFocus" + +// CSS property with vendor prefixes +type CSSProp = "transform" | "transition"; +type Prefixed = `-webkit-${CSSProp}` | `-moz-${CSSProp}` | CSSProp; +``` + +### Advanced Template Patterns + +```typescript +// Parse dot-notation paths +type PathSegment = T extends `${infer Head}.${infer Tail}` + ? Head | PathSegment + : T; + +type Segments = PathSegment<"user.profile.name">; +// "user" | "profile" | "name" + +// HTTP methods with paths +type Method = "GET" | "POST" | "PUT" | "DELETE"; +type Endpoint = "/users" | "/posts" | "/comments"; + +type Route = `${Method} ${Endpoint}`; +// "GET /users" | "GET /posts" | "GET /comments" | +// "POST /users" | ... etc + +// Type-safe SQL column references +type Table = "users" | "posts"; +type Column = T extends "users" + ? "id" | "name" | "email" + : T extends "posts" + ? "id" | "title" | "content" + : never; + +type UserColumn = `users.${Column<"users">}`; +// "users.id" | "users.name" | "users.email" +``` + +--- + +## Variadic Tuple Types + +### Basic Variadic Tuples + +```typescript +// Spread tuple types +type Concat = [...T, ...U]; + +type Combined = Concat<[1, 2], [3, 4]>; +// [1, 2, 3, 4] + +// Prepend element +type Prepend = [T, ...U]; + +type WithFirst = Prepend<0, [1, 2, 3]>; +// [0, 1, 2, 3] + +// Append element +type Append = [...T, U]; + +type WithLast = Append<[1, 2, 3], 4>; +// [1, 2, 3, 4] +``` + +### Practical Variadic Patterns + +```typescript +// Typed curry function +type Curry = F extends (...args: infer A) => infer R + ? A extends [infer First, ...infer Rest] + ? (arg: First) => Curry<(...args: Rest) => R> + : R + : never; + +declare function curry any>(fn: F): Curry; + +function add(a: number, b: number, c: number): number { + return a + b + c; +} + +const curriedAdd = curry(add); +const add1 = curriedAdd(1); // (arg: number) => Curry<...> +const add1and2 = add1(2); // (arg: number) => number +const result = add1and2(3); // number (6) + +// Typed pipe function +type Pipe = T extends [infer First, ...infer Rest] + ? First extends (arg: R) => infer Next + ? Pipe + : never + : R; + +function pipe any)[]>( + ...fns: T +): (arg: Parameters[0]) => Pipe[0]> { + return (arg) => fns.reduce((acc, fn) => fn(acc), arg); +} + +const process = pipe( + (n: number) => n * 2, + (n: number) => n.toString(), + (s: string) => s.length +); + +const length = process(5); // number (2 - length of "10") +``` + +--- + +## Built-in Utility Types Reference + +| Utility | Purpose | Example | +|---------|---------|---------| +| `Partial` | All properties optional | `Partial` | +| `Required` | All properties required | `Required>` | +| `Readonly` | All properties readonly | `Readonly` | +| `Pick` | Select properties | `Pick` | +| `Omit` | Exclude properties | `Omit` | +| `Record` | Create object type | `Record` | +| `Exclude` | Remove union members | `Exclude<"a" \| "b", "a">` | +| `Extract` | Keep union members | `Extract<"a" \| "b", "a">` | +| `NonNullable` | Remove null/undefined | `NonNullable` | +| `Parameters` | Function parameters | `Parameters` | +| `ReturnType` | Function return | `ReturnType` | +| `ConstructorParameters` | Constructor params | `ConstructorParameters` | +| `InstanceType` | Instance type | `InstanceType` | +| `Awaited` | Unwrap Promise | `Awaited>` | +| `NoInfer` | Prevent inference | `NoInfer` (TS 5.4+) | diff --git a/skills/mastering-typescript/references/nestjs-integration.md b/skills/mastering-typescript/references/nestjs-integration.md new file mode 100644 index 0000000..8d2686c --- /dev/null +++ b/skills/mastering-typescript/references/nestjs-integration.md @@ -0,0 +1,591 @@ +# NestJS Integration Reference + +> **Load when:** User asks about NestJS with TypeScript, API development, DTOs, validation, authentication, or backend patterns. + +Type-safe API development with NestJS 11+. + +## Contents + +- [Project Structure](#project-structure) +- [Controllers and Routes](#controllers-and-routes) +- [DTOs and Validation](#dtos-and-validation) +- [Services and Dependency Injection](#services-and-dependency-injection) +- [Authentication](#authentication) +- [Error Handling](#error-handling) + +--- + +## Project Structure + +### Recommended Layout + +``` +src/ +├── main.ts # Application entry point +├── app.module.ts # Root module +├── common/ # Shared utilities +│ ├── decorators/ +│ ├── filters/ +│ ├── guards/ +│ ├── interceptors/ +│ └── pipes/ +├── config/ # Configuration +│ ├── config.module.ts +│ └── env.validation.ts +└── modules/ + ├── users/ + │ ├── users.module.ts + │ ├── users.controller.ts + │ ├── users.service.ts + │ ├── users.repository.ts + │ ├── dto/ + │ │ ├── create-user.dto.ts + │ │ └── update-user.dto.ts + │ ├── entities/ + │ │ └── user.entity.ts + │ └── __tests__/ + └── auth/ + └── ... +``` + +### Module Configuration + +```typescript +// users.module.ts +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { UsersRepository } from './users.repository'; + +@Module({ + controllers: [UsersController], + providers: [UsersService, UsersRepository], + exports: [UsersService] // Export for use in other modules +}) +export class UsersModule {} +``` + +--- + +## Controllers and Routes + +### Basic Controller + +```typescript +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + Query, + HttpCode, + HttpStatus +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { UsersService } from './users.service'; +import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto'; +import { PaginationDto } from '@/common/dto'; + +@ApiTags('users') +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Get() + @ApiOperation({ summary: 'Get all users' }) + @ApiResponse({ status: 200, type: [UserResponseDto] }) + async findAll(@Query() query: PaginationDto): Promise { + return this.usersService.findAll(query); + } + + @Get(':id') + @ApiOperation({ summary: 'Get user by ID' }) + @ApiResponse({ status: 200, type: UserResponseDto }) + @ApiResponse({ status: 404, description: 'User not found' }) + async findOne(@Param('id') id: string): Promise { + return this.usersService.findOne(id); + } + + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Create new user' }) + @ApiResponse({ status: 201, type: UserResponseDto }) + async create(@Body() dto: CreateUserDto): Promise { + return this.usersService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: 'Update user' }) + async update( + @Param('id') id: string, + @Body() dto: UpdateUserDto + ): Promise { + return this.usersService.update(id, dto); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Delete user' }) + async remove(@Param('id') id: string): Promise { + return this.usersService.remove(id); + } +} +``` + +--- + +## DTOs and Validation + +### Class-Validator Approach + +```typescript +// dto/create-user.dto.ts +import { + IsString, + IsEmail, + MinLength, + MaxLength, + IsOptional, + IsEnum, + ValidateNested, + IsArray +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum UserRole { + User = 'user', + Admin = 'admin', + Moderator = 'moderator' +} + +export class AddressDto { + @ApiProperty() + @IsString() + street: string; + + @ApiProperty() + @IsString() + city: string; + + @ApiProperty() + @IsString() + country: string; +} + +export class CreateUserDto { + @ApiProperty({ example: 'john@example.com' }) + @IsEmail() + email: string; + + @ApiProperty({ minLength: 2, maxLength: 50 }) + @IsString() + @MinLength(2) + @MaxLength(50) + name: string; + + @ApiProperty({ minLength: 8 }) + @IsString() + @MinLength(8) + password: string; + + @ApiPropertyOptional({ enum: UserRole, default: UserRole.User }) + @IsOptional() + @IsEnum(UserRole) + role?: UserRole = UserRole.User; + + @ApiPropertyOptional({ type: AddressDto }) + @IsOptional() + @ValidateNested() + @Type(() => AddressDto) + address?: AddressDto; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; +} + +// dto/update-user.dto.ts +import { PartialType } from '@nestjs/swagger'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} +``` + +### Zod-Based DTOs (Modern Approach) + +```typescript +// dto/user.schema.ts +import { z } from 'zod'; +import { createZodDto } from 'nestjs-zod'; + +// Define Zod schemas +export const UserRoleSchema = z.enum(['user', 'admin', 'moderator']); + +export const AddressSchema = z.object({ + street: z.string(), + city: z.string(), + country: z.string() +}); + +export const CreateUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(2).max(50), + password: z.string().min(8), + role: UserRoleSchema.default('user').optional(), + address: AddressSchema.optional(), + tags: z.array(z.string()).optional() +}); + +export const UpdateUserSchema = CreateUserSchema.partial(); + +// Create DTO classes from schemas +export class CreateUserDto extends createZodDto(CreateUserSchema) {} +export class UpdateUserDto extends createZodDto(UpdateUserSchema) {} + +// Infer types +export type CreateUser = z.infer; +export type UpdateUser = z.infer; +``` + +### Global Validation Pipe + +```typescript +// main.ts +import { ValidationPipe } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Strip unknown properties + forbidNonWhitelisted: true, // Throw on unknown properties + transform: true, // Transform payloads to DTO classes + transformOptions: { + enableImplicitConversion: true + } + }) + ); + + await app.listen(3000); +} +bootstrap(); +``` + +--- + +## Services and Dependency Injection + +### Typed Service + +```typescript +// users.service.ts +import { Injectable, NotFoundException } from '@nestjs/common'; +import { UsersRepository } from './users.repository'; +import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto'; +import { PaginationDto } from '@/common/dto'; + +@Injectable() +export class UsersService { + constructor(private readonly usersRepository: UsersRepository) {} + + async findAll(query: PaginationDto): Promise { + const users = await this.usersRepository.findAll(query); + return users.map(this.toResponseDto); + } + + async findOne(id: string): Promise { + const user = await this.usersRepository.findById(id); + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + return this.toResponseDto(user); + } + + async create(dto: CreateUserDto): Promise { + const user = await this.usersRepository.create(dto); + return this.toResponseDto(user); + } + + async update(id: string, dto: UpdateUserDto): Promise { + const user = await this.usersRepository.update(id, dto); + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + return this.toResponseDto(user); + } + + async remove(id: string): Promise { + const deleted = await this.usersRepository.delete(id); + if (!deleted) { + throw new NotFoundException(`User with ID ${id} not found`); + } + } + + private toResponseDto(user: User): UserResponseDto { + return { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + createdAt: user.createdAt + }; + } +} +``` + +### Repository Pattern + +```typescript +// users.repository.ts +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@/prisma/prisma.service'; +import { User, Prisma } from '@prisma/client'; +import { PaginationDto } from '@/common/dto'; + +@Injectable() +export class UsersRepository { + constructor(private readonly prisma: PrismaService) {} + + async findAll(query: PaginationDto): Promise { + return this.prisma.user.findMany({ + skip: query.skip, + take: query.take, + orderBy: { createdAt: 'desc' } + }); + } + + async findById(id: string): Promise { + return this.prisma.user.findUnique({ where: { id } }); + } + + async findByEmail(email: string): Promise { + return this.prisma.user.findUnique({ where: { email } }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return this.prisma.user.create({ data }); + } + + async update(id: string, data: Prisma.UserUpdateInput): Promise { + try { + return await this.prisma.user.update({ where: { id }, data }); + } catch { + return null; + } + } + + async delete(id: string): Promise { + try { + await this.prisma.user.delete({ where: { id } }); + return true; + } catch { + return false; + } + } +} +``` + +--- + +## Authentication + +### JWT Authentication + +```typescript +// auth/strategies/jwt.strategy.ts +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { UsersService } from '@/modules/users/users.service'; + +interface JwtPayload { + sub: string; + email: string; + role: string; + iat: number; + exp: number; +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + configService: ConfigService, + private readonly usersService: UsersService + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.getOrThrow('JWT_SECRET') + }); + } + + async validate(payload: JwtPayload) { + const user = await this.usersService.findOne(payload.sub); + if (!user) { + throw new UnauthorizedException(); + } + return { id: payload.sub, email: payload.email, role: payload.role }; + } +} +``` + +### Role-Based Access Control + +```typescript +// common/decorators/roles.decorator.ts +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); + +// common/guards/roles.guard.ts +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride( + ROLES_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!requiredRoles) { + return true; + } + + const { user } = context.switchToHttp().getRequest(); + return requiredRoles.includes(user.role); + } +} + +// Usage in controller +@Controller('admin') +@UseGuards(JwtAuthGuard, RolesGuard) +export class AdminController { + @Get('users') + @Roles('admin') + findAllUsers() { + return this.adminService.findAllUsers(); + } + + @Delete('users/:id') + @Roles('admin', 'moderator') + removeUser(@Param('id') id: string) { + return this.adminService.removeUser(id); + } +} +``` + +--- + +## Error Handling + +### Exception Filters + +```typescript +// common/filters/http-exception.filter.ts +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus +} from '@nestjs/common'; +import { Response } from 'express'; + +interface ErrorResponse { + statusCode: number; + message: string; + error: string; + timestamp: string; + path: string; +} + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = 'Internal server error'; + let error = 'Internal Server Error'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + + if (typeof exceptionResponse === 'string') { + message = exceptionResponse; + } else if (typeof exceptionResponse === 'object') { + const responseObj = exceptionResponse as Record; + message = (responseObj.message as string) || message; + error = (responseObj.error as string) || exception.name; + } + } + + const errorResponse: ErrorResponse = { + statusCode: status, + message, + error, + timestamp: new Date().toISOString(), + path: request.url + }; + + response.status(status).json(errorResponse); + } +} + +// main.ts +app.useGlobalFilters(new AllExceptionsFilter()); +``` + +### Custom Exceptions + +```typescript +// common/exceptions/business.exception.ts +import { HttpException, HttpStatus } from '@nestjs/common'; + +export class BusinessException extends HttpException { + constructor( + message: string, + public readonly code: string, + status: HttpStatus = HttpStatus.BAD_REQUEST + ) { + super({ message, code }, status); + } +} + +export class InsufficientFundsException extends BusinessException { + constructor(required: number, available: number) { + super( + `Insufficient funds: required ${required}, available ${available}`, + 'INSUFFICIENT_FUNDS' + ); + } +} + +export class DuplicateEmailException extends BusinessException { + constructor(email: string) { + super(`Email ${email} is already registered`, 'DUPLICATE_EMAIL'); + } +} + +// Usage +throw new InsufficientFundsException(100, 50); +``` diff --git a/skills/mastering-typescript/references/react-integration.md b/skills/mastering-typescript/references/react-integration.md new file mode 100644 index 0000000..6a3c2fb --- /dev/null +++ b/skills/mastering-typescript/references/react-integration.md @@ -0,0 +1,618 @@ +# React Integration Reference + +> **Load when:** User asks about React with TypeScript, typed components, hooks, state management, or React patterns. + +Type-safe React development patterns for React 19+. + +## Contents + +- [Component Patterns](#component-patterns) +- [Hooks with TypeScript](#hooks-with-typescript) +- [State Management](#state-management) +- [Event Handling](#event-handling) +- [Context API](#context-api) + +--- + +## Component Patterns + +### Functional Components + +```typescript +// Basic typed component +interface GreetingProps { + name: string; + age?: number; +} + +function Greeting({ name, age }: GreetingProps) { + return ( +
+ Hello, {name}! + {age && You are {age} years old.} +
+ ); +} + +// With React.FC (optional, some prefer explicit return type) +const GreetingFC: React.FC = ({ name, age }) => { + return
Hello, {name}!
; +}; + +// Component with children +interface CardProps { + title: string; + children: React.ReactNode; +} + +function Card({ title, children }: CardProps) { + return ( +
+

{title}

+
{children}
+
+ ); +} +``` + +### Generic Components + +```typescript +// Generic list component +interface ListProps { + items: T[]; + renderItem: (item: T, index: number) => React.ReactNode; + keyExtractor: (item: T) => string; +} + +function List({ items, renderItem, keyExtractor }: ListProps) { + return ( +
    + {items.map((item, index) => ( +
  • {renderItem(item, index)}
  • + ))} +
+ ); +} + +// Usage +interface User { + id: string; + name: string; +} + + + items={users} + keyExtractor={(user) => user.id} + renderItem={(user) => {user.name}} +/>; + +// Generic select component +interface SelectProps { + options: T[]; + value: T | null; + onChange: (value: T) => void; + getLabel: (option: T) => string; + getValue: (option: T) => string; +} + +function Select({ + options, + value, + onChange, + getLabel, + getValue +}: SelectProps) { + return ( + + ); +} +``` + +### Polymorphic Components + +```typescript +// Component that can render as different elements +type ButtonProps = { + as?: T; + children: React.ReactNode; + variant?: "primary" | "secondary"; +} & Omit, "as" | "children">; + +function Button({ + as, + children, + variant = "primary", + ...props +}: ButtonProps) { + const Component = as || "button"; + return ( + + {children} + + ); +} + +// Usage + + + +``` + +--- + +## Hooks with TypeScript + +### useState + +```typescript +// Basic usage (type inferred) +const [count, setCount] = useState(0); + +// Explicit type (for complex types or initial null) +const [user, setUser] = useState(null); + +// With union types +type Status = "idle" | "loading" | "success" | "error"; +const [status, setStatus] = useState("idle"); + +// Lazy initialization +const [state, setState] = useState(() => { + return computeExpensiveInitialState(); +}); +``` + +### useRef + +```typescript +// DOM element ref +const inputRef = useRef(null); + +function focusInput() { + inputRef.current?.focus(); +} + +// Mutable ref (no initial render) +const intervalRef = useRef(null); + +useEffect(() => { + intervalRef.current = setInterval(() => {}, 1000); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; +}, []); + +// Ref to store previous value +function usePrevious(value: T): T | undefined { + const ref = useRef(undefined); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +} +``` + +### useReducer + +```typescript +// Define state and actions +interface CounterState { + count: number; + step: number; +} + +type CounterAction = + | { type: "increment" } + | { type: "decrement" } + | { type: "setStep"; payload: number } + | { type: "reset" }; + +function counterReducer( + state: CounterState, + action: CounterAction +): CounterState { + switch (action.type) { + case "increment": + return { ...state, count: state.count + state.step }; + case "decrement": + return { ...state, count: state.count - state.step }; + case "setStep": + return { ...state, step: action.payload }; + case "reset": + return { count: 0, step: 1 }; + } +} + +// Usage +const [state, dispatch] = useReducer(counterReducer, { count: 0, step: 1 }); + +dispatch({ type: "increment" }); +dispatch({ type: "setStep", payload: 5 }); +``` + +### Custom Hooks + +```typescript +// Async data fetching hook +interface UseAsyncResult { + data: T | null; + loading: boolean; + error: Error | null; + refetch: () => void; +} + +function useAsync( + asyncFn: () => Promise, + deps: React.DependencyList = [] +): UseAsyncResult { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const execute = useCallback(async () => { + setLoading(true); + setError(null); + try { + const result = await asyncFn(); + setData(result); + } catch (e) { + setError(e instanceof Error ? e : new Error(String(e))); + } finally { + setLoading(false); + } + }, deps); + + useEffect(() => { + execute(); + }, [execute]); + + return { data, loading, error, refetch: execute }; +} + +// Usage +const { data: users, loading, error } = useAsync( + () => fetch("/api/users").then((r) => r.json()), + [] +); + +// Local storage hook +function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((prev: T) => T)) => void] { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch { + return initialValue; + } + }); + + const setValue = (value: T | ((prev: T) => T)) => { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + localStorage.setItem(key, JSON.stringify(valueToStore)); + }; + + return [storedValue, setValue]; +} +``` + +--- + +## State Management + +### Zustand with TypeScript + +```typescript +import { create } from 'zustand'; +import { devtools, persist } from 'zustand/middleware'; + +// Define state interface +interface AuthState { + user: User | null; + token: string | null; + isAuthenticated: boolean; + login: (email: string, password: string) => Promise; + logout: () => void; + setUser: (user: User) => void; +} + +// Create typed store +const useAuthStore = create()( + devtools( + persist( + (set) => ({ + user: null, + token: null, + isAuthenticated: false, + + login: async (email, password) => { + const response = await api.login(email, password); + set({ + user: response.user, + token: response.token, + isAuthenticated: true + }); + }, + + logout: () => { + set({ user: null, token: null, isAuthenticated: false }); + }, + + setUser: (user) => set({ user }) + }), + { name: 'auth-storage' } + ) + ) +); + +// Usage with selectors +const user = useAuthStore((state) => state.user); +const login = useAuthStore((state) => state.login); + +// Shallow comparison for multiple values +import { useShallow } from 'zustand/react/shallow'; + +const { user, isAuthenticated } = useAuthStore( + useShallow((state) => ({ + user: state.user, + isAuthenticated: state.isAuthenticated + })) +); +``` + +### Redux Toolkit with TypeScript + +```typescript +import { createSlice, PayloadAction, configureStore } from '@reduxjs/toolkit'; + +// Define slice state +interface TodosState { + items: Todo[]; + filter: "all" | "active" | "completed"; + loading: boolean; +} + +const initialState: TodosState = { + items: [], + filter: "all", + loading: false +}; + +// Create typed slice +const todosSlice = createSlice({ + name: "todos", + initialState, + reducers: { + addTodo: (state, action: PayloadAction) => { + state.items.push({ + id: crypto.randomUUID(), + text: action.payload, + completed: false + }); + }, + toggleTodo: (state, action: PayloadAction) => { + const todo = state.items.find((t) => t.id === action.payload); + if (todo) { + todo.completed = !todo.completed; + } + }, + setFilter: (state, action: PayloadAction) => { + state.filter = action.payload; + } + } +}); + +// Configure store with type inference +const store = configureStore({ + reducer: { + todos: todosSlice.reducer + } +}); + +// Infer types from store +type RootState = ReturnType; +type AppDispatch = typeof store.dispatch; + +// Typed hooks +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; + +const useAppDispatch = () => useDispatch(); +const useAppSelector: TypedUseSelectorHook = useSelector; + +// Usage +const todos = useAppSelector((state) => state.todos.items); +const dispatch = useAppDispatch(); +dispatch(todosSlice.actions.addTodo("New todo")); +``` + +--- + +## Event Handling + +### Common Event Types + +```typescript +// Click events +function handleClick(event: React.MouseEvent) { + console.log(event.currentTarget.name); +} + +// Form events +function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + const formData = new FormData(event.currentTarget); +} + +// Input change +function handleChange(event: React.ChangeEvent) { + const { name, value, checked, type } = event.target; + const inputValue = type === "checkbox" ? checked : value; +} + +// Keyboard events +function handleKeyDown(event: React.KeyboardEvent) { + if (event.key === "Enter") { + event.preventDefault(); + // submit form + } +} + +// Focus events +function handleFocus(event: React.FocusEvent) { + event.target.select(); +} + +// Drag events +function handleDrag(event: React.DragEvent) { + event.dataTransfer.setData("text/plain", "dragged data"); +} +``` + +### Form with TypeScript + +```typescript +interface FormData { + name: string; + email: string; + role: "user" | "admin"; +} + +function RegistrationForm() { + const [formData, setFormData] = useState({ + name: "", + email: "", + role: "user" + }); + + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log(formData); + }; + + return ( +
+ + + + +
+ ); +} +``` + +--- + +## Context API + +### Typed Context + +```typescript +// Define context type +interface ThemeContextType { + theme: "light" | "dark"; + toggleTheme: () => void; +} + +// Create context with undefined default +const ThemeContext = createContext(undefined); + +// Provider component +function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState<"light" | "dark">("light"); + + const toggleTheme = useCallback(() => { + setTheme((prev) => (prev === "light" ? "dark" : "light")); + }, []); + + const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]); + + return ( + + {children} + + ); +} + +// Custom hook with type safety +function useTheme(): ThemeContextType { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useTheme must be used within ThemeProvider"); + } + return context; +} + +// Usage +function ThemeToggle() { + const { theme, toggleTheme } = useTheme(); + return ; +} +``` + +### Generic Context Factory + +```typescript +// Factory function for creating typed contexts +function createContext(displayName: string) { + const Context = React.createContext(undefined); + Context.displayName = displayName; + + function useContextHook(): T { + const context = React.useContext(Context); + if (context === undefined) { + throw new Error(`use${displayName} must be used within ${displayName}Provider`); + } + return context; + } + + return [Context.Provider, useContextHook] as const; +} + +// Usage +interface AuthContextType { + user: User | null; + login: (credentials: Credentials) => Promise; + logout: () => void; +} + +const [AuthProvider, useAuth] = createContext("Auth"); +``` diff --git a/skills/mastering-typescript/references/toolchain.md b/skills/mastering-typescript/references/toolchain.md new file mode 100644 index 0000000..12e7011 --- /dev/null +++ b/skills/mastering-typescript/references/toolchain.md @@ -0,0 +1,546 @@ +# Modern Toolchain Reference + +> **Load when:** User asks about Vite, pnpm, ESLint, Vitest, tsconfig, build tools, or project configuration. + +Modern TypeScript toolchain configuration for 2025. + +## Contents + +- [TypeScript Configuration](#typescript-configuration) +- [Package Manager (pnpm)](#package-manager-pnpm) +- [Build Tool (Vite)](#build-tool-vite) +- [Linting (ESLint 9)](#linting-eslint-9) +- [Testing (Vitest)](#testing-vitest) +- [Formatting (Prettier)](#formatting-prettier) + +--- + +## TypeScript Configuration + +### Strict Enterprise Configuration + +```json +// tsconfig.json +{ + "compilerOptions": { + // Language and Environment + "target": "ES2024", + "lib": ["ES2024"], + "module": "ESNext", + "moduleResolution": "bundler", + + // Strict Type Checking + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + + // Module Handling + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + + // Output + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + + // Path Aliases + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + // Performance + "skipLibCheck": true, + "incremental": true, + "tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### React Project Configuration + +```json +// tsconfig.json for React +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "esModuleInterop": true, + "isolatedModules": true, + "skipLibCheck": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@components/*": ["./src/components/*"], + "@hooks/*": ["./src/hooks/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} +``` + +### Node.js Backend Configuration + +```json +// tsconfig.json for Node.js/NestJS +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + + "esModuleInterop": true, + "isolatedModules": true, + "skipLibCheck": true, + + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true, + + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.spec.ts"] +} +``` + +--- + +## Package Manager (pnpm) + +### Why pnpm + +| Feature | npm | pnpm | +|---------|-----|------| +| Disk usage | Duplicates packages | Shared store, symlinks | +| Install speed | Slower | 2-3x faster | +| Strictness | Allows phantom deps | Strict by default | +| Monorepo support | Basic workspaces | First-class support | + +### Basic Commands + +```bash +# Install dependencies +pnpm install + +# Add packages +pnpm add typescript +pnpm add -D vitest @types/node + +# Run scripts +pnpm run build +pnpm test + +# Update packages +pnpm update +pnpm update --interactive + +# List packages +pnpm list +pnpm why lodash + +# Clean install +pnpm install --frozen-lockfile +``` + +### Workspace Configuration + +```yaml +# pnpm-workspace.yaml +packages: + - 'packages/*' + - 'apps/*' +``` + +```json +// package.json (root) +{ + "name": "my-monorepo", + "private": true, + "scripts": { + "build": "pnpm -r run build", + "test": "pnpm -r run test", + "lint": "pnpm -r run lint" + } +} +``` + +--- + +## Build Tool (Vite) + +### Vite Configuration + +```typescript +// vite.config.ts +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [ + react(), + tsconfigPaths() + ], + server: { + port: 3000, + host: true + }, + build: { + target: 'es2022', + sourcemap: true, + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom'], + utils: ['lodash-es', 'date-fns'] + } + } + } + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'] + } +}); +``` + +### Library Mode + +```typescript +// vite.config.ts for library +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + build: { + lib: { + entry: './src/index.ts', + name: 'MyLibrary', + fileName: 'my-library', + formats: ['es', 'cjs'] + }, + rollupOptions: { + external: ['react', 'react-dom'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM' + } + } + } + }, + plugins: [ + dts({ insertTypesEntry: true }) + ] +}); +``` + +--- + +## Linting (ESLint 9) + +### Flat Config Format + +```javascript +// eslint.config.js +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; + +export default tseslint.config( + // Base ESLint recommendations + eslint.configs.recommended, + + // TypeScript strict type-checking + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + + // TypeScript parser options + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + } + }, + + // React configuration + { + files: ['**/*.tsx'], + plugins: { + react: reactPlugin, + 'react-hooks': reactHooksPlugin + }, + rules: { + ...reactPlugin.configs.recommended.rules, + ...reactHooksPlugin.configs.recommended.rules, + 'react/react-in-jsx-scope': 'off' + }, + settings: { + react: { version: 'detect' } + } + }, + + // Custom rules + { + rules: { + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + }], + '@typescript-eslint/consistent-type-imports': ['error', { + prefer: 'type-imports' + }], + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error' + } + }, + + // Ignore patterns + { + ignores: ['dist/**', 'node_modules/**', '*.config.js'] + } +); +``` + +### Common Rules Explained + +```javascript +// Important TypeScript ESLint rules +{ + rules: { + // Enforce type imports for better tree-shaking + '@typescript-eslint/consistent-type-imports': 'error', + + // Prevent unhandled promises + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + + // Prevent awaiting non-promises + '@typescript-eslint/await-thenable': 'error', + + // Require return types on functions + '@typescript-eslint/explicit-function-return-type': ['error', { + allowExpressions: true + }], + + // Prefer nullish coalescing + '@typescript-eslint/prefer-nullish-coalescing': 'error', + + // Prefer optional chaining + '@typescript-eslint/prefer-optional-chain': 'error', + + // No any type + '@typescript-eslint/no-explicit-any': 'error', + + // Enforce strict boolean expressions + '@typescript-eslint/strict-boolean-expressions': 'error' + } +} +``` + +--- + +## Testing (Vitest) + +### Vitest Configuration + +```typescript +// vitest.config.ts +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + include: ['src/**/*.{test,spec}.{ts,tsx}'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: ['**/*.d.ts', '**/*.config.*', '**/test/**'] + }, + typecheck: { + enabled: true + } + } +}); +``` + +### Test Setup + +```typescript +// src/test/setup.ts +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/react'; +import { afterEach, beforeAll, afterAll, vi } from 'vitest'; + +// Cleanup after each test +afterEach(() => { + cleanup(); +}); + +// Mock environment variables +beforeAll(() => { + vi.stubEnv('API_URL', 'http://localhost:3000'); +}); + +afterAll(() => { + vi.unstubAllEnvs(); +}); +``` + +### Example Tests + +```typescript +// src/utils/format.test.ts +import { describe, it, expect } from 'vitest'; +import { formatCurrency, formatDate } from './format'; + +describe('formatCurrency', () => { + it('formats USD correctly', () => { + expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56'); + }); + + it('handles zero', () => { + expect(formatCurrency(0, 'USD')).toBe('$0.00'); + }); +}); + +// src/components/Button.test.tsx +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { Button } from './Button'; + +describe('Button', () => { + it('renders with label', () => { + render(