mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
- Convert 40 JS files to TypeScript (CLI, tools, core, MCP server) - Add Zod for runtime parameter validation - Add type definitions in src/types/ - Keep src/templates/ as JavaScript (dashboard frontend) - Update bin entries to use dist/ - Add tsconfig.json with strict mode - Add backward-compatible exports for tests - All 39 tests passing Breaking changes: None (backward compatible) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
350 lines
9.2 KiB
TypeScript
350 lines
9.2 KiB
TypeScript
/**
|
|
* Get Modules by Depth Tool
|
|
* Scan project structure and organize modules by directory depth (deepest first)
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
|
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
|
import { join, resolve, relative, extname } from 'path';
|
|
|
|
// System/cache directories to always exclude
|
|
const SYSTEM_EXCLUDES = [
|
|
// Version control and IDE
|
|
'.git', '.gitignore', '.gitmodules', '.gitattributes',
|
|
'.svn', '.hg', '.bzr',
|
|
'.history', '.vscode', '.idea', '.vs', '.vscode-test',
|
|
'.sublime-text', '.atom',
|
|
// Python
|
|
'__pycache__', '.pytest_cache', '.mypy_cache', '.tox',
|
|
'.coverage', 'htmlcov', '.nox', '.venv', 'venv', 'env',
|
|
'.egg-info', '.eggs', '.wheel',
|
|
'site-packages', '.python-version',
|
|
// Node.js/JavaScript
|
|
'node_modules', '.npm', '.yarn', '.pnpm', 'yarn-error.log',
|
|
'.nyc_output', 'coverage', '.next', '.nuxt',
|
|
'.cache', '.parcel-cache', '.vite', 'dist', 'build',
|
|
'.turbo', '.vercel', '.netlify',
|
|
// Build/compile outputs
|
|
'out', 'output', '_site', 'public',
|
|
'.output', '.generated', 'generated', 'gen',
|
|
'bin', 'obj', 'Debug', 'Release',
|
|
// Testing
|
|
'test-results', 'junit.xml', 'test_results',
|
|
'cypress', 'playwright-report', '.playwright',
|
|
// Logs and temp files
|
|
'logs', 'log', 'tmp', 'temp', '.tmp', '.temp',
|
|
// Documentation build outputs
|
|
'_book', 'docs/_build', 'site', 'gh-pages',
|
|
'.docusaurus', '.vuepress', '.gitbook',
|
|
// Cloud and deployment
|
|
'.serverless', '.terraform',
|
|
'.aws', '.azure', '.gcp',
|
|
// Mobile development
|
|
'.gradle', '.expo', '.metro',
|
|
'DerivedData',
|
|
// Game development
|
|
'Library', 'Temp', 'ProjectSettings',
|
|
'MemoryCaptures', 'UserSettings'
|
|
];
|
|
|
|
// Define Zod schema for validation
|
|
const ParamsSchema = z.object({
|
|
format: z.enum(['list', 'grouped', 'json']).default('list'),
|
|
path: z.string().default('.'),
|
|
});
|
|
|
|
type Params = z.infer<typeof ParamsSchema>;
|
|
|
|
interface ModuleInfo {
|
|
depth: number;
|
|
path: string;
|
|
files: number;
|
|
types: string[];
|
|
has_claude: boolean;
|
|
}
|
|
|
|
interface ToolOutput {
|
|
format: string;
|
|
total_modules: number;
|
|
max_depth: number;
|
|
output: string;
|
|
}
|
|
|
|
/**
|
|
* Parse .gitignore file and return patterns
|
|
*/
|
|
function parseGitignore(basePath: string): string[] {
|
|
const gitignorePath = join(basePath, '.gitignore');
|
|
const patterns: string[] = [];
|
|
|
|
if (existsSync(gitignorePath)) {
|
|
const content = readFileSync(gitignorePath, 'utf8');
|
|
content.split('\n').forEach(line => {
|
|
line = line.trim();
|
|
// Skip empty lines and comments
|
|
if (!line || line.startsWith('#')) return;
|
|
// Remove trailing slash
|
|
line = line.replace(/\/$/, '');
|
|
patterns.push(line);
|
|
});
|
|
}
|
|
|
|
return patterns;
|
|
}
|
|
|
|
/**
|
|
* Check if a path should be excluded
|
|
*/
|
|
function shouldExclude(name: string, gitignorePatterns: string[]): boolean {
|
|
// Check system excludes
|
|
if (SYSTEM_EXCLUDES.includes(name)) return true;
|
|
|
|
// Check gitignore patterns (simple matching)
|
|
for (const pattern of gitignorePatterns) {
|
|
if (name === pattern) return true;
|
|
// Simple wildcard matching
|
|
if (pattern.includes('*')) {
|
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
if (regex.test(name)) return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get file types in a directory
|
|
*/
|
|
function getFileTypes(dirPath: string): string[] {
|
|
const types = new Set<string>();
|
|
try {
|
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
entries.forEach(entry => {
|
|
if (entry.isFile()) {
|
|
const ext = extname(entry.name).slice(1);
|
|
if (ext) types.add(ext);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
// Ignore errors
|
|
}
|
|
return Array.from(types);
|
|
}
|
|
|
|
/**
|
|
* Count files in a directory (non-recursive)
|
|
*/
|
|
function countFiles(dirPath: string): number {
|
|
try {
|
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
return entries.filter(e => e.isFile()).length;
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively scan directories and collect info
|
|
*/
|
|
function scanDirectories(
|
|
basePath: string,
|
|
currentPath: string,
|
|
depth: number,
|
|
gitignorePatterns: string[],
|
|
results: ModuleInfo[]
|
|
): void {
|
|
try {
|
|
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) continue;
|
|
if (shouldExclude(entry.name, gitignorePatterns)) continue;
|
|
|
|
const fullPath = join(currentPath, entry.name);
|
|
const relPath = './' + relative(basePath, fullPath).replace(/\\/g, '/');
|
|
const fileCount = countFiles(fullPath);
|
|
|
|
// Only include directories with files
|
|
if (fileCount > 0) {
|
|
const types = getFileTypes(fullPath);
|
|
const hasClaude = existsSync(join(fullPath, 'CLAUDE.md'));
|
|
|
|
results.push({
|
|
depth: depth + 1,
|
|
path: relPath,
|
|
files: fileCount,
|
|
types,
|
|
has_claude: hasClaude
|
|
});
|
|
}
|
|
|
|
// Recurse into subdirectories
|
|
scanDirectories(basePath, fullPath, depth + 1, gitignorePatterns, results);
|
|
}
|
|
} catch (e) {
|
|
// Ignore permission errors, etc.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format output as list (default)
|
|
*/
|
|
function formatList(results: ModuleInfo[]): string {
|
|
// Sort by depth descending (deepest first)
|
|
results.sort((a, b) => b.depth - a.depth);
|
|
|
|
return results.map(r =>
|
|
`depth:${r.depth}|path:${r.path}|files:${r.files}|types:[${r.types.join(',')}]|has_claude:${r.has_claude ? 'yes' : 'no'}`
|
|
).join('\n');
|
|
}
|
|
|
|
/**
|
|
* Format output as grouped
|
|
*/
|
|
function formatGrouped(results: ModuleInfo[]): string {
|
|
// Sort by depth descending
|
|
results.sort((a, b) => b.depth - a.depth);
|
|
|
|
const maxDepth = results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0;
|
|
const lines = ['Modules by depth (deepest first):'];
|
|
|
|
for (let d = maxDepth; d >= 0; d--) {
|
|
const atDepth = results.filter(r => r.depth === d);
|
|
if (atDepth.length > 0) {
|
|
lines.push(` Depth ${d}:`);
|
|
atDepth.forEach(r => {
|
|
const claudeIndicator = r.has_claude ? ' [OK]' : '';
|
|
lines.push(` - ${r.path}${claudeIndicator}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Format output as JSON
|
|
*/
|
|
function formatJson(results: ModuleInfo[]): string {
|
|
// Sort by depth descending
|
|
results.sort((a, b) => b.depth - a.depth);
|
|
|
|
const maxDepth = results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0;
|
|
const modules: Record<number, { path: string; has_claude: boolean }[]> = {};
|
|
|
|
for (let d = maxDepth; d >= 0; d--) {
|
|
const atDepth = results.filter(r => r.depth === d);
|
|
if (atDepth.length > 0) {
|
|
modules[d] = atDepth.map(r => ({
|
|
path: r.path,
|
|
has_claude: r.has_claude
|
|
}));
|
|
}
|
|
}
|
|
|
|
return JSON.stringify({
|
|
max_depth: maxDepth,
|
|
modules
|
|
}, null, 2);
|
|
}
|
|
|
|
// Tool schema for MCP
|
|
export const schema: ToolSchema = {
|
|
name: 'get_modules_by_depth',
|
|
description: `Scan project structure and organize modules by directory depth (deepest first).
|
|
Respects .gitignore patterns and excludes common system directories.
|
|
Output formats: list (pipe-delimited), grouped (human-readable), json.`,
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
format: {
|
|
type: 'string',
|
|
enum: ['list', 'grouped', 'json'],
|
|
description: 'Output format (default: list)',
|
|
default: 'list'
|
|
},
|
|
path: {
|
|
type: 'string',
|
|
description: 'Target directory path (default: current directory)',
|
|
default: '.'
|
|
}
|
|
},
|
|
required: []
|
|
}
|
|
};
|
|
|
|
// Handler function
|
|
export async function handler(params: Record<string, unknown>): Promise<ToolResult<ToolOutput>> {
|
|
const parsed = ParamsSchema.safeParse(params);
|
|
if (!parsed.success) {
|
|
return { success: false, error: `Invalid params: ${parsed.error.message}` };
|
|
}
|
|
|
|
const { format, path: targetPath } = parsed.data;
|
|
|
|
try {
|
|
const basePath = resolve(process.cwd(), targetPath);
|
|
|
|
if (!existsSync(basePath)) {
|
|
return { success: false, error: `Directory not found: ${basePath}` };
|
|
}
|
|
|
|
const stat = statSync(basePath);
|
|
if (!stat.isDirectory()) {
|
|
return { success: false, error: `Not a directory: ${basePath}` };
|
|
}
|
|
|
|
// Parse gitignore
|
|
const gitignorePatterns = parseGitignore(basePath);
|
|
|
|
// Collect results
|
|
const results: ModuleInfo[] = [];
|
|
|
|
// Check root directory
|
|
const rootFileCount = countFiles(basePath);
|
|
if (rootFileCount > 0) {
|
|
results.push({
|
|
depth: 0,
|
|
path: '.',
|
|
files: rootFileCount,
|
|
types: getFileTypes(basePath),
|
|
has_claude: existsSync(join(basePath, 'CLAUDE.md'))
|
|
});
|
|
}
|
|
|
|
// Scan subdirectories
|
|
scanDirectories(basePath, basePath, 0, gitignorePatterns, results);
|
|
|
|
// Format output
|
|
let output: string;
|
|
switch (format) {
|
|
case 'grouped':
|
|
output = formatGrouped(results);
|
|
break;
|
|
case 'json':
|
|
output = formatJson(results);
|
|
break;
|
|
case 'list':
|
|
default:
|
|
output = formatList(results);
|
|
break;
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
result: {
|
|
format,
|
|
total_modules: results.length,
|
|
max_depth: results.length > 0 ? Math.max(...results.map(r => r.depth)) : 0,
|
|
output
|
|
}
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to scan modules: ${(error as Error).message}`
|
|
};
|
|
}
|
|
}
|