From 202b3b2caa24ee3514d75994dff8768a36423f67 Mon Sep 17 00:00:00 2001 From: Jordan Coin Jackson Date: Tue, 9 Dec 2025 21:37:52 -0500 Subject: [PATCH] Add --only and --exclude file filtering flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New flags for focusing on what matters: - --only : Show only files with specific extensions (e.g., --only swift,go) - --exclude : Exclude files matching patterns (e.g., --exclude .xcassets,Fonts,.png) Smart pattern matching - no quotes or wildcards needed: - .png or png โ†’ matches any .png file - Fonts โ†’ matches any /Fonts/ directory - *Test* โ†’ explicit glob pattern when needed Also: - Reorganized README (405 โ†’ 139 lines) - Moved HOOKS.md to docs/ - Created docs/MCP.md for MCP server documentation ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 319 ++++++-------------------------------- HOOKS.md => docs/HOOKS.md | 0 docs/MCP.md | 67 ++++++++ main.go | 27 +++- mcp/main.go | 8 +- scanner/filegraph.go | 2 +- scanner/types.go | 4 +- scanner/walker.go | 87 ++++++++++- scanner/walker_test.go | 16 +- watch/daemon.go | 2 +- 10 files changed, 245 insertions(+), 287 deletions(-) rename HOOKS.md => docs/HOOKS.md (100%) create mode 100644 docs/MCP.md diff --git a/README.md b/README.md index bfe0e3e..93e1310 100644 --- a/README.md +++ b/README.md @@ -8,172 +8,77 @@ ![codemap screenshot](assets/codemap.png) -## Table of Contents - -- [Why codemap exists](#why-codemap-exists) -- [Features](#features) -- [How It Works](#%EF%B8%8F-how-it-works) -- [Performance](#-performance) -- [Installation](#installation) -- [Usage](#usage) -- [Diff Mode](#diff-mode) -- [Dependency Flow Mode](#dependency-flow-mode) -- [Skyline Mode](#skyline-mode) -- [Supported Languages](#supported-languages) -- [Claude Integrations](#claude-integrations) -- [Roadmap](#roadmap) -- [Contributing](#contributing) -- [License](#license) - -## Why codemap exists - -Modern LLMs are powerful, but blind. They can write code โ€” but only after you ask them to burn tokens searching or manually explain your entire project structure. - -That means: -* ๐Ÿ”ฅ **Burning thousands of tokens** -* ๐Ÿ” **Repeating context** -* ๐Ÿ“‹ **Pasting directory trees** -* โ“ **Answering โ€œwhere is X defined?โ€** - -**codemap fixes that.** - -One command โ†’ a compact, structured โ€œbrain mapโ€ of your codebase that LLMs can instantly understand. - -## Features - -- ๐Ÿง  **Brain Map Output**: Visualizes your codebase structure in a single, pasteable block. -- ๐Ÿ“‰ **Token Efficient**: Clusters files and simplifies names to save vertical space. -- โญ๏ธ **Smart Highlighting**: Automatically flags the top 5 largest source code files. -- ๐Ÿ“‚ **Smart Flattening**: Merges empty intermediate directories (e.g., `src/main/java`). -- ๐ŸŽจ **Rich Context**: Color-coded by language for easy scanning. -- ๐Ÿšซ **Noise Reduction**: Automatically ignores `.git`, `node_modules`, and assets (images, binaries). - -## โš™๏ธ How It Works - -**codemap** is a fast Go binary with minimal dependencies: -1. **Scanner**: Instantly traverses your directory, respecting `.gitignore` and ignoring junk. -2. **Analyzer**: Uses [ast-grep](https://ast-grep.github.io/) to parse imports/functions across 18 languages. -3. **Renderer**: Outputs a clean, dense "brain map" that is both human-readable and LLM-optimized. - -## โšก Performance - -**codemap** runs instantly even on large repos (hundreds or thousands of files). This makes it ideal for LLM workflows โ€” no lag, no multi-tool dance. - -## Installation - -### Homebrew (macOS/Linux) +## Install ```bash -brew tap JordanCoin/tap -brew install codemap -``` - -### Scoop (Windows) +# macOS/Linux +brew tap JordanCoin/tap && brew install codemap -```powershell +# Windows scoop bucket add codemap https://github.com/JordanCoin/scoop-codemap scoop install codemap ``` -### Winget (Windows) - -```powershell -winget install JordanCoin.codemap -``` - -> **Note:** Winget availability depends on Microsoft approval. Use Scoop if not yet available. - -### Download Binary - -Pre-built binaries are available for all platforms on the [Releases page](https://github.com/JordanCoin/codemap/releases): - -- **macOS**: `codemap-darwin-amd64.tar.gz` (Intel) or `codemap-darwin-arm64.tar.gz` (Apple Silicon) -- **Linux**: `codemap-linux-amd64.tar.gz` or `codemap-linux-arm64.tar.gz` -- **Windows**: `codemap-windows-amd64.zip` - -```bash -# Example: download and install on Linux/macOS -curl -L https://github.com/JordanCoin/codemap/releases/latest/download/codemap-linux-amd64.tar.gz | tar xz -sudo mv codemap-linux-amd64/codemap /usr/local/bin/ -``` - -> **Note:** The `--deps` feature requires [ast-grep](https://ast-grep.github.io/). Install via `brew install ast-grep`, `pip install ast-grep-cli`, or `cargo install ast-grep`. - -### From source - -```bash -git clone https://github.com/JordanCoin/codemap.git -cd codemap -go build -o codemap . -``` - -## Usage +> Other options: [Releases](https://github.com/JordanCoin/codemap/releases) | `go install` | Build from source -Run `codemap` in any directory: +## Quick Start ```bash -codemap +codemap . # Project tree +codemap --only swift . # Just Swift files +codemap --exclude .xcassets,Fonts,.png . # Hide assets +codemap --depth 2 . # Limit depth +codemap --diff # What changed vs main +codemap --deps . # Dependency flow ``` -Or specify a path: +## Options -```bash -codemap /path/to/my/project -``` - -### AI Usage Example - -**The Killer Use Case:** +| Flag | Description | +|------|-------------| +| `--depth, -d ` | Limit tree depth (0 = unlimited) | +| `--only ` | Only show files with these extensions | +| `--exclude ` | Exclude files matching patterns | +| `--diff` | Show files changed vs main branch | +| `--ref ` | Branch to compare against (with --diff) | +| `--deps` | Dependency flow mode | +| `--importers ` | Check who imports a file | +| `--skyline` | City skyline visualization | +| `--json` | Output JSON | -1. Run codemap and copy the output: - ```bash - codemap . | pbcopy - ``` +**Smart pattern matching** โ€” no quotes needed: +- `.png` โ†’ any `.png` file +- `Fonts` โ†’ any `/Fonts/` directory +- `*Test*` โ†’ glob pattern -2. Or simply tell Claude, Codex, or Cursor: - > "Use codemap to understand my project structure." +## Modes -## Diff Mode +### Diff Mode -See what you're working on with `--diff`: +See what you're working on: ```bash codemap --diff +codemap --diff --ref develop ``` ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ myproject โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ Changed: 4 files | +156 -23 lines vs main โ”‚ -โ”‚ Top Extensions: .go (3), .tsx (1) โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ -myproject โ”œโ”€โ”€ api/ โ”‚ โ””โ”€โ”€ (new) auth.go โœŽ handlers.go (+45 -12) -โ”œโ”€โ”€ web/ -โ”‚ โ””โ”€โ”€ โœŽ Dashboard.tsx (+82 -8) โ””โ”€โ”€ โœŽ main.go (+29 -3) โš  handlers.go is used by 3 other files -โš  api is used by 2 other files ``` -**What it shows:** -- ๐Ÿ“Š **Change summary**: Total files and lines changed vs main branch -- โœจ **New vs modified**: `(new)` for untracked files, `โœŽ` for modified -- ๐Ÿ“ˆ **Line counts**: `(+45 -12)` shows additions and deletions per file -- โš ๏ธ **Impact analysis**: Which changed files are imported by others - -Compare against a different branch: -```bash -codemap --diff --ref develop -``` +### Dependency Flow -## Dependency Flow Mode - -See how your code connects with `--deps`: +See how your code connects: ```bash -codemap --deps /path/to/project +codemap --deps . ``` ``` @@ -181,30 +86,16 @@ codemap --deps /path/to/project โ”‚ MyApp - Dependency Flow โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Go: chi, zap, testify โ”‚ -โ”‚ Py: fastapi, pydantic, httpx โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ Backend โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• server โ”€โ”€โ”€โ–ถ validate โ”€โ”€โ”€โ–ถ rules, config api โ”€โ”€โ”€โ–ถ handlers, middleware -Frontend โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - App โ”€โ”€โ”ฌโ”€โ”€โ–ถ Dashboard - โ”œโ”€โ”€โ–ถ Settings - โ””โ”€โ”€โ–ถ api - HUBS: config (12โ†), api (8โ†), utils (5โ†) -45 files ยท 312 functions ยท 89 deps ``` -**What it shows:** -- ๐Ÿ“ฆ **External dependencies** grouped by language (from go.mod, requirements.txt, package.json, etc.) -- ๐Ÿ”— **Internal dependency chains** showing how files import each other -- ๐ŸŽฏ **Hub files** โ€” the most-imported files in your codebase - -## Skyline Mode - -Want something more visual? Run `codemap --skyline` for a cityscape visualization of your codebase: +### Skyline Mode ```bash codemap --skyline --animate @@ -212,146 +103,36 @@ codemap --skyline --animate ![codemap skyline](assets/skyline-animated.gif) -Each building represents a language in your project โ€” taller buildings mean more code. Add `--animate` for rising buildings, twinkling stars, and shooting stars. - ## Supported Languages -codemap supports **18 languages** for dependency analysis (powered by [ast-grep](https://ast-grep.github.io/)): - -| Language | Extensions | Import Detection | -|----------|------------|------------------| -| Go | .go | import statements | -| Python | .py | import, from...import | -| JavaScript | .js, .jsx, .mjs | import, require | -| TypeScript | .ts, .tsx | import, require | -| Rust | .rs | use, mod | -| Ruby | .rb | require, require_relative | -| C | .c, .h | #include | -| C++ | .cpp, .hpp, .cc | #include | -| Java | .java | import | -| Swift | .swift | import | -| Kotlin | .kt, .kts | import | -| C# | .cs | using | -| PHP | .php | use, require, include | -| Bash | .sh, .bash | source, . | -| Lua | .lua | require, dofile | -| Scala | .scala, .sc | import | -| Elixir | .ex, .exs | import, alias, use, require | -| Solidity | .sol | import | - -## Claude Integrations - -codemap provides four ways to integrate with Claude: - -### Claude Code Hooks (Recommended) - -The most seamless integration. Hooks run automatically at the right moments โ€” no commands to remember. +18 languages for dependency analysis: Go, Python, JavaScript, TypeScript, Rust, Ruby, C, C++, Java, Swift, Kotlin, C#, PHP, Bash, Lua, Scala, Elixir, Solidity -``` -Session starts โ†’ Claude sees project structure + hub files -Before editing โ†’ Claude is warned if the file is high-impact -After editing โ†’ Claude sees what depends on the changed file -Before compact โ†’ Hub state is saved so Claude remembers what matters -Session ends โ†’ Summary of all changes and their impact -``` +> Powered by [ast-grep](https://ast-grep.github.io/). Install via `brew install ast-grep` for `--deps` mode. -**Quick setup:** Tell Claude to `@HOOKS.md` and "add these hooks to my settings". Or see [HOOKS.md](HOOKS.md) for the full setup. +## Claude Integration -### CLAUDE.md +**Hooks (Recommended)** โ€” Automatic context at session start, before/after edits, and more. +โ†’ See [docs/HOOKS.md](docs/HOOKS.md) -Add the included `CLAUDE.md` to your project root. Claude Code reads it and knows when to run codemap: +**MCP Server** โ€” Deep integration with 7 tools for codebase analysis. +โ†’ See [docs/MCP.md](docs/MCP.md) +**CLAUDE.md** โ€” Add to your project root to teach Claude when to run codemap: ```bash cp /path/to/codemap/CLAUDE.md your-project/ ``` -This teaches Claude to: -- Run `codemap .` before starting tasks -- Run `codemap --deps` when refactoring -- Run `codemap --diff` when reviewing changes - -### Claude Code Skill - -For automatic invocation, install the codemap skill: - -```bash -# Copy to your project -cp -r /path/to/codemap/.claude/skills/codemap your-project/.claude/skills/ - -# Or install globally -cp -r /path/to/codemap/.claude/skills/codemap ~/.claude/skills/ -``` - -Skills are model-invoked โ€” Claude automatically decides when to use codemap based on your questions, no explicit commands needed. - -### MCP Server - -For the deepest integration, run codemap as an MCP server: - -```bash -# Build the MCP server -make build-mcp - -# Add to Claude Code -claude mcp add --transport stdio codemap -- /path/to/codemap-mcp -``` - -Or add to your project's `.mcp.json`: - -```json -{ - "mcpServers": { - "codemap": { - "command": "/path/to/codemap-mcp", - "args": [] - } - } -} -``` - -**Claude Desktop:** - -> โš ๏ธ Claude Desktop cannot see your local files by default. This MCP server runs on your machine and gives Claude that ability. - -Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "codemap": { - "command": "/path/to/codemap-mcp" - } - } -} -``` - -**MCP Tools:** -| Tool | Description | -|------|-------------| -| `status` | Verify MCP connection and local filesystem access | -| `list_projects` | Discover projects in a parent directory (with optional filter) | -| `get_structure` | Project tree view with file sizes and language detection | -| `get_dependencies` | Dependency flow with imports, functions, and hub files | -| `get_diff` | Changed files with line counts and impact analysis | -| `find_file` | Find files by name pattern | -| `get_importers` | Find all files that import a specific file | - ## Roadmap -- [x] **Diff Mode** (`codemap --diff`) โ€” show changed files with impact analysis -- [x] **Skyline Mode** (`codemap --skyline`) โ€” ASCII cityscape visualization -- [x] **Dependency Flow** (`codemap --deps`) โ€” function/import analysis with 14 language support -- [x] **Claude Code Skill** โ€” automatic invocation based on user questions -- [x] **MCP Server** โ€” deep integration with 7 tools for codebase analysis -- [ ] **Enhanced Analysis** โ€” entry points, key types, exported function counts for richer LLM context +- [x] Diff mode, Skyline mode, Dependency flow +- [x] Tree depth limiting (`--depth`) +- [x] File filtering (`--only`, `--exclude`) +- [x] Claude Code hooks & MCP server +- [ ] Enhanced analysis (entry points, key types) ## Contributing -We love contributions! -1. Fork the repo. -2. Create a branch (`git checkout -b feature/my-feature`). -3. Commit your changes. -4. Push and open a Pull Request. +1. Fork โ†’ 2. Branch โ†’ 3. Commit โ†’ 4. PR ## License diff --git a/HOOKS.md b/docs/HOOKS.md similarity index 100% rename from HOOKS.md rename to docs/HOOKS.md diff --git a/docs/MCP.md b/docs/MCP.md new file mode 100644 index 0000000..e629726 --- /dev/null +++ b/docs/MCP.md @@ -0,0 +1,67 @@ +# Codemap MCP Server + +Run codemap as an MCP (Model Context Protocol) server for deep Claude integration. + +## Setup + +### Build + +```bash +make build-mcp +``` + +### Claude Code + +```bash +claude mcp add --transport stdio codemap -- /path/to/codemap-mcp +``` + +Or add to your project's `.mcp.json`: + +```json +{ + "mcpServers": { + "codemap": { + "command": "/path/to/codemap-mcp", + "args": [] + } + } +} +``` + +### Claude Desktop + +> Claude Desktop cannot see your local files by default. This MCP server runs on your machine and gives Claude that ability. + +Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "codemap": { + "command": "/path/to/codemap-mcp" + } + } +} +``` + +## Available Tools + +| Tool | Description | +|------|-------------| +| `status` | Verify MCP connection and local filesystem access | +| `list_projects` | Discover projects in a parent directory (with optional filter) | +| `get_structure` | Project tree view with file sizes and language detection | +| `get_dependencies` | Dependency flow with imports, functions, and hub files | +| `get_diff` | Changed files with line counts and impact analysis | +| `find_file` | Find files by name pattern | +| `get_importers` | Find all files that import a specific file | + +## Usage + +Once configured, Claude can use these tools automatically. Try asking: + +- "What's the structure of this project?" +- "Show me the dependency flow" +- "What files import utils.go?" +- "What changed since the last commit?" diff --git a/main.go b/main.go index 631acd3..aed252b 100644 --- a/main.go +++ b/main.go @@ -57,6 +57,8 @@ func main() { diffMode := flag.Bool("diff", false, "Only show files changed vs main (or use --ref to specify branch)") diffRef := flag.String("ref", "main", "Branch/ref to compare against (use with --diff)") depthLimit := flag.Int("depth", 0, "Limit tree depth (0 = unlimited)") + onlyExts := flag.String("only", "", "Only show files with these extensions (comma-separated, e.g., 'swift,go')") + excludePatterns := flag.String("exclude", "", "Exclude files matching patterns (comma-separated, e.g., '.xcassets,Fonts')") jsonMode := flag.Bool("json", false, "Output JSON (for Python renderer compatibility)") debugMode := flag.Bool("debug", false, "Show debug info (gitignore loading, paths, etc.)") watchMode := flag.Bool("watch", false, "Live file watcher daemon (experimental)") @@ -79,6 +81,8 @@ func main() { fmt.Println(" --diff Only show files changed vs main") fmt.Println(" --ref Branch to compare against (default: main)") fmt.Println(" --depth, -d Limit tree depth (0 = unlimited)") + fmt.Println(" --only Only show files with these extensions (e.g., 'swift,go')") + fmt.Println(" --exclude Exclude paths matching patterns (e.g., '.xcassets,Fonts')") fmt.Println(" --importers Check file impact (who imports it, hub status)") fmt.Println() fmt.Println("Examples:") @@ -89,6 +93,8 @@ func main() { fmt.Println(" codemap --diff # Files changed vs main") fmt.Println(" codemap --diff --ref develop # Files changed vs develop") fmt.Println(" codemap --depth 3 . # Show only 3 levels deep") + fmt.Println(" codemap --only swift . # Just Swift files") + fmt.Println(" codemap --exclude .xcassets,Fonts,.png # Hide assets") fmt.Println(" codemap --importers scanner/types.go # Check file impact") fmt.Println() fmt.Println("Hooks (for Claude Code integration):") @@ -115,6 +121,23 @@ func main() { // Initialize gitignore cache (supports nested .gitignore files) gitCache := scanner.NewGitIgnoreCache(root) + // Parse --only and --exclude flags + var only, exclude []string + if *onlyExts != "" { + for _, ext := range strings.Split(*onlyExts, ",") { + if trimmed := strings.TrimSpace(ext); trimmed != "" { + only = append(only, trimmed) + } + } + } + if *excludePatterns != "" { + for _, pattern := range strings.Split(*excludePatterns, ",") { + if trimmed := strings.TrimSpace(pattern); trimmed != "" { + exclude = append(exclude, trimmed) + } + } + } + if *debugMode { fmt.Fprintf(os.Stderr, "[debug] Root path: %s\n", root) fmt.Fprintf(os.Stderr, "[debug] Absolute path: %s\n", absRoot) @@ -165,7 +188,7 @@ func main() { } // Scan files - files, err := scanner.ScanFiles(root, gitCache) + files, err := scanner.ScanFiles(root, gitCache, only, exclude) if err != nil { fmt.Fprintf(os.Stderr, "Error walking tree: %v\n", err) os.Exit(1) @@ -188,6 +211,8 @@ func main() { DiffRef: activeDiffRef, Impact: impact, Depth: *depthLimit, + Only: only, + Exclude: exclude, } // Render or output JSON diff --git a/mcp/main.go b/mcp/main.go index 0429c89..ab5ea1b 100644 --- a/mcp/main.go +++ b/mcp/main.go @@ -173,7 +173,7 @@ func handleGetStructure(ctx context.Context, req *mcp.CallToolRequest, input Pat } gitCache := scanner.NewGitIgnoreCache(input.Path) - files, err := scanner.ScanFiles(input.Path, gitCache) + files, err := scanner.ScanFiles(input.Path, gitCache, nil, nil) if err != nil { return errorResult("Scan error: " + err.Error()), nil, nil } @@ -257,7 +257,7 @@ func handleGetDiff(ctx context.Context, req *mcp.CallToolRequest, input DiffInpu } gitCache := scanner.NewGitIgnoreCache(input.Path) - files, err := scanner.ScanFiles(input.Path, gitCache) + files, err := scanner.ScanFiles(input.Path, gitCache, nil, nil) if err != nil { return errorResult("Scan error: " + err.Error()), nil, nil } @@ -282,7 +282,7 @@ func handleGetDiff(ctx context.Context, req *mcp.CallToolRequest, input DiffInpu func handleFindFile(ctx context.Context, req *mcp.CallToolRequest, input FindInput) (*mcp.CallToolResult, any, error) { gitCache := scanner.NewGitIgnoreCache(input.Path) - files, err := scanner.ScanFiles(input.Path, gitCache) + files, err := scanner.ScanFiles(input.Path, gitCache, nil, nil) if err != nil { return errorResult("Scan error: " + err.Error()), nil, nil } @@ -408,7 +408,7 @@ func handleListProjects(ctx context.Context, req *mcp.CallToolRequest, input Lis // Uses the same scanner logic as the main codemap command (respects nested .gitignore files) func getProjectStats(path string) string { gitCache := scanner.NewGitIgnoreCache(path) - files, err := scanner.ScanFiles(path, gitCache) + files, err := scanner.ScanFiles(path, gitCache, nil, nil) if err != nil { return "(error scanning)" } diff --git a/scanner/filegraph.go b/scanner/filegraph.go index 0c4e576..10ef275 100644 --- a/scanner/filegraph.go +++ b/scanner/filegraph.go @@ -44,7 +44,7 @@ func BuildFileGraph(root string) (*FileGraph, error) { // Scan all files gitCache := NewGitIgnoreCache(root) - files, err := ScanFiles(root, gitCache) + files, err := ScanFiles(root, gitCache, nil, nil) if err != nil { return nil, err } diff --git a/scanner/types.go b/scanner/types.go index 859da5d..ad31b89 100644 --- a/scanner/types.go +++ b/scanner/types.go @@ -23,7 +23,9 @@ type Project struct { Files []FileInfo `json:"files"` DiffRef string `json:"diff_ref,omitempty"` Impact []ImpactInfo `json:"impact,omitempty"` - Depth int `json:"depth,omitempty"` // Max tree depth (0 = unlimited) + Depth int `json:"depth,omitempty"` // Max tree depth (0 = unlimited) + Only []string `json:"only,omitempty"` // Extension filter (e.g., ["swift", "go"]) + Exclude []string `json:"exclude,omitempty"` // Exclusion patterns (e.g., [".xcassets", "Fonts"]) } // FileAnalysis holds extracted info about a single file for deps mode. diff --git a/scanner/walker.go b/scanner/walker.go index 888ef98..004ae9a 100644 --- a/scanner/walker.go +++ b/scanner/walker.go @@ -129,6 +129,70 @@ var IgnoredDirs = map[string]bool{ "grammars": true, } +// matchesPattern does smart pattern matching: +// - ".png" or "png" โ†’ extension match (case-insensitive) +// - "Fonts" โ†’ directory/component match (contains /Fonts/ or ends with /Fonts) +// - "*test*" โ†’ glob pattern (only if contains * or ?) +func matchesPattern(relPath string, pattern string) bool { + // If pattern contains glob characters, use glob matching + if strings.ContainsAny(pattern, "*?") { + // Match against filename + if matched, _ := filepath.Match(pattern, filepath.Base(relPath)); matched { + return true + } + // Match against full relative path + if matched, _ := filepath.Match(pattern, relPath); matched { + return true + } + return false + } + + // Extension match: .png, .xcassets, png, xcassets + ext := strings.TrimPrefix(pattern, ".") + if strings.HasSuffix(strings.ToLower(relPath), "."+strings.ToLower(ext)) { + return true + } + + // Directory component match: Fonts โ†’ matches path/Fonts/file or path/Fonts + if strings.Contains(relPath, "/"+pattern+"/") || + strings.HasSuffix(relPath, "/"+pattern) || + strings.HasPrefix(relPath, pattern+"/") || + relPath == pattern { + return true + } + + return false +} + +// shouldIncludeFile checks if a file passes the only/exclude filters +func shouldIncludeFile(relPath string, ext string, only []string, exclude []string) bool { + // If --only specified, file extension must be in the list + if len(only) > 0 { + extNoDot := strings.TrimPrefix(ext, ".") + found := false + for _, o := range only { + o = strings.TrimPrefix(strings.TrimSpace(o), ".") + if strings.EqualFold(extNoDot, o) { + found = true + break + } + } + if !found { + return false + } + } + + // If --exclude specified, check against each pattern + for _, pattern := range exclude { + pattern = strings.TrimSpace(pattern) + if pattern != "" && matchesPattern(relPath, pattern) { + return false + } + } + + return true +} + // LoadGitignore loads .gitignore from root if it exists // Deprecated: Use NewGitIgnoreCache for nested gitignore support func LoadGitignore(root string) *ignore.GitIgnore { @@ -145,7 +209,9 @@ func LoadGitignore(root string) *ignore.GitIgnore { // ScanFiles walks the directory tree and returns all files. // Supports nested .gitignore files via GitIgnoreCache. -func ScanFiles(root string, cache *GitIgnoreCache) ([]FileInfo, error) { +// only: list of extensions to include (empty = all) +// exclude: list of patterns to exclude +func ScanFiles(root string, cache *GitIgnoreCache, only []string, exclude []string) ([]FileInfo, error) { var files []FileInfo absRoot, _ := filepath.Abs(root) @@ -175,6 +241,16 @@ func ScanFiles(root string, cache *GitIgnoreCache) ([]FileInfo, error) { return filepath.SkipDir } } + // Check if directory matches any exclude pattern + relPath, _ := filepath.Rel(absRoot, absPath) + if relPath != "." { + for _, pattern := range exclude { + pattern = strings.TrimSpace(pattern) + if pattern != "" && matchesPattern(relPath, pattern) { + return filepath.SkipDir + } + } + } return nil } @@ -184,10 +260,17 @@ func ScanFiles(root string, cache *GitIgnoreCache) ([]FileInfo, error) { } relPath, _ := filepath.Rel(absRoot, absPath) + ext := filepath.Ext(path) + + // Apply user filters (--only and --exclude) + if !shouldIncludeFile(relPath, ext, only, exclude) { + return nil + } + files = append(files, FileInfo{ Path: relPath, Size: info.Size(), - Ext: filepath.Ext(path), + Ext: ext, }) return nil diff --git a/scanner/walker_test.go b/scanner/walker_test.go index 30802eb..b9bc454 100644 --- a/scanner/walker_test.go +++ b/scanner/walker_test.go @@ -55,7 +55,7 @@ func TestScanFiles(t *testing.T) { } // Scan the directory - result, err := ScanFiles(tmpDir, nil) + result, err := ScanFiles(tmpDir, nil, nil, nil) if err != nil { t.Fatalf("ScanFiles failed: %v", err) } @@ -86,7 +86,7 @@ func TestScanFilesIgnoresDirs(t *testing.T) { t.Fatal(err) } - result, err := ScanFiles(tmpDir, nil) + result, err := ScanFiles(tmpDir, nil, nil, nil) if err != nil { t.Fatalf("ScanFiles failed: %v", err) } @@ -120,7 +120,7 @@ func TestScanFilesExtensions(t *testing.T) { } } - result, err := ScanFiles(tmpDir, nil) + result, err := ScanFiles(tmpDir, nil, nil, nil) if err != nil { t.Fatal(err) } @@ -291,7 +291,7 @@ func TestNestedGitignore(t *testing.T) { // Scan with GitIgnoreCache cache := NewGitIgnoreCache(tmpDir) - files, err := ScanFiles(tmpDir, cache) + files, err := ScanFiles(tmpDir, cache, nil, nil) if err != nil { t.Fatalf("ScanFiles failed: %v", err) } @@ -334,7 +334,7 @@ func TestGitIgnoreCacheNil(t *testing.T) { } // Scan without gitignore cache - files, err := ScanFiles(tmpDir, nil) + files, err := ScanFiles(tmpDir, nil, nil, nil) if err != nil { t.Fatalf("ScanFiles failed: %v", err) } @@ -432,7 +432,7 @@ func TestNestedGitignoreMonorepo(t *testing.T) { // Scan with GitIgnoreCache cache := NewGitIgnoreCache(tmpDir) - files, err := ScanFiles(tmpDir, cache) + files, err := ScanFiles(tmpDir, cache, nil, nil) if err != nil { t.Fatalf("ScanFiles failed: %v", err) } @@ -479,7 +479,7 @@ func TestNestedGitignoreUnignore(t *testing.T) { os.WriteFile(filepath.Join(tmpDir, "sub", "other.log"), []byte("other"), 0644) cache := NewGitIgnoreCache(tmpDir) - files, err := ScanFiles(tmpDir, cache) + files, err := ScanFiles(tmpDir, cache, nil, nil) if err != nil { t.Fatalf("ScanFiles failed: %v", err) } @@ -536,7 +536,7 @@ func TestNestedGitignoreDirectoryIgnore(t *testing.T) { } cache := NewGitIgnoreCache(tmpDir) - files, err := ScanFiles(tmpDir, cache) + files, err := ScanFiles(tmpDir, cache, nil, nil) if err != nil { t.Fatalf("ScanFiles failed: %v", err) } diff --git a/watch/daemon.go b/watch/daemon.go index 69e937e..4708b03 100644 --- a/watch/daemon.go +++ b/watch/daemon.go @@ -137,7 +137,7 @@ func (d *Daemon) WriteInitialState() { func (d *Daemon) fullScan() error { start := time.Now() - files, err := scanner.ScanFiles(d.root, d.gitCache) + files, err := scanner.ScanFiles(d.root, d.gitCache, nil, nil) if err != nil { return err }