Git MCP Server
by cyanheads
Verified
- scripts
#!/usr/bin/env node
/**
* Directory Tree Generation Operation
* ==================================
*
* A utility for generating visual tree representations of the project's directory
* structure with configurable depth control and gitignore integration.
*
* This operation creates a formatted markdown file containing a hierarchical
* representation of directories and files, respecting ignore patterns and
* applying configurable filtering.
*
* Features:
* - Respects .gitignore patterns and common exclusions
* - Configurable maximum depth traversal
* - Customizable output location
* - Sorting with directories first
* - Cross-platform compatibility
*
* @module utilities/generate.directory.tree.operation
*
* Usage examples:
* - Add to package.json: "tree": "ts-node scripts/tree.ts"
* - Run directly: npm run tree
* - Custom output: ts-node scripts/tree.ts ./documentation/structure.md
* - Limit depth: ts-node scripts/tree.ts --depth=3
* - Show help: ts-node scripts/tree.ts --help
*/
import fs from 'fs/promises';
import path from 'path';
// -----------------------------------
// Type Definitions
// -----------------------------------
/**
* Standardized error category classification
*/
const ErrorCategoryType = {
CATEGORY_VALIDATION: 'VALIDATION',
CATEGORY_FILESYSTEM: 'FILESYSTEM',
CATEGORY_SYSTEM: 'SYSTEM',
CATEGORY_UNKNOWN: 'UNKNOWN'
} as const;
type ErrorCategoryType = typeof ErrorCategoryType[keyof typeof ErrorCategoryType];
/**
* Error severity classification
*/
const ErrorSeverityLevel = {
SEVERITY_DEBUG: 0,
SEVERITY_INFO: 1,
SEVERITY_WARN: 2,
SEVERITY_ERROR: 3,
SEVERITY_FATAL: 4
} as const;
type ErrorSeverityLevel = typeof ErrorSeverityLevel[keyof typeof ErrorSeverityLevel];
/**
* Standardized error structure for consistent error handling
*/
interface StandardizedApplicationErrorObject {
errorMessage: string; // Human-readable description
errorCode: string; // Machine-readable identifier
errorCategory: ErrorCategoryType; // System area affected
errorSeverity: ErrorSeverityLevel; // How critical the error is
errorTimestamp: string; // When the error occurred
errorContext: Record<string, unknown>; // Additional relevant data
errorStack?: string; // Stack trace if available
}
/**
* Successful result from an operation
*/
interface OperationResultSuccess<DataType> {
resultSuccessful: true;
resultData: DataType;
}
/**
* Failed result from an operation
*/
interface OperationResultFailure<ErrorType> {
resultSuccessful: false;
resultError: ErrorType;
}
/**
* Combined result type for operations
*/
type OperationResult<DataType, ErrorType = StandardizedApplicationErrorObject> =
| OperationResultSuccess<DataType>
| OperationResultFailure<ErrorType>;
/**
* Configuration options for the tree generation operation
*/
interface TreeGenerationConfiguration {
treeOutputFilePath: string;
maximumDirectoryDepth: number;
showHelpText: boolean;
}
/**
* Definition of a gitignore pattern with parsing metadata
*/
interface GitignorePatternDefinition {
patternText: string;
isNegatedPattern: boolean;
regexPattern: string;
}
/**
* Result from the tree generation operation
*/
interface TreeGenerationResult {
projectName: string;
treeOutputFilePath: string;
treeContentLength: number;
maximumDepthApplied: number;
generationTimestamp: string;
}
// -----------------------------------
// Constants
// -----------------------------------
/**
* Default patterns to always ignore regardless of gitignore contents
*/
const DEFAULT_IGNORE_PATTERNS: string[] = [
'.git',
'node_modules',
'.DS_Store',
'dist',
'build'
];
/**
* Default output path for the generated tree
*/
const DEFAULT_OUTPUT_PATH = 'docs/tree.md';
/**
* Help text displayed when requested
*/
const HELP_TEXT = `
Directory Tree Generator - Project structure visualization tool
Usage:
node dist/utilities/generate.directory.tree.operation.js [output-path] [--depth=<number>] [--help]
Options:
output-path Custom file path for the tree output (default: docs/tree.md)
--depth=<number> Maximum directory depth to display (default: unlimited)
--help Show this help message
`;
// -----------------------------------
// Utility Functions
// -----------------------------------
/**
* Creates a standardized success result
*
* @param data - The data to include in the success result
* @returns A standardized success result object
*/
function createSuccessResult<DataType>(data: DataType): OperationResultSuccess<DataType> {
return { resultSuccessful: true, resultData: data };
}
/**
* Creates a standardized failure result
*
* @param error - The error to include in the failure result
* @returns A standardized failure result object
*/
function createFailureResult<ErrorType>(error: ErrorType): OperationResultFailure<ErrorType> {
return { resultSuccessful: false, resultError: error };
}
/**
* Creates a standardized error object
*
* @param message - Human-readable error message
* @param code - Machine-readable error code
* @param category - Error category classification
* @param severity - Error severity level
* @param context - Additional context data
* @returns A standardized error object
*/
function createStandardizedError(
message: string,
code: string,
category: ErrorCategoryType,
severity: ErrorSeverityLevel,
context: Record<string, unknown> = {}
): StandardizedApplicationErrorObject {
return {
errorMessage: message,
errorCode: code,
errorCategory: category,
errorSeverity: severity,
errorTimestamp: new Date().toISOString(),
errorContext: context
};
}
/**
* Converts an exception to a standardized error object
*
* @param exception - The caught exception
* @param defaultMessage - Fallback message if exception is not an Error object
* @returns A standardized error object
*/
function wrapExceptionAsStandardizedError(
exception: unknown,
defaultMessage: string
): StandardizedApplicationErrorObject {
const errorMessage = exception instanceof Error ? exception.message : defaultMessage;
const errorStack = exception instanceof Error ? exception.stack : undefined;
return {
errorMessage,
errorCode: 'UNEXPECTED_ERROR',
errorCategory: ErrorCategoryType.CATEGORY_UNKNOWN,
errorSeverity: ErrorSeverityLevel.SEVERITY_ERROR,
errorTimestamp: new Date().toISOString(),
errorContext: { originalException: exception },
errorStack
};
}
// -----------------------------------
// Implementation Functions
// -----------------------------------
/**
* Parses command line arguments to extract configuration options
*
* @param commandLineArguments - Array of arguments from process.argv
* @returns Configuration object for tree generation
*/
function parseCommandLineArguments(
commandLineArguments: string[]
): TreeGenerationConfiguration {
let treeOutputFilePath = DEFAULT_OUTPUT_PATH;
let maximumDirectoryDepth = Infinity;
let showHelpText = false;
for (const argumentValue of commandLineArguments) {
if (argumentValue === '--help') {
showHelpText = true;
} else if (argumentValue.startsWith('--depth=')) {
const depthValue = argumentValue.split('=')[1];
const parsedDepth = parseInt(depthValue, 10);
if (isNaN(parsedDepth) || parsedDepth < 1) {
console.error('Invalid depth value. Using unlimited depth.');
maximumDirectoryDepth = Infinity;
} else {
maximumDirectoryDepth = parsedDepth;
}
} else if (!argumentValue.startsWith('--')) {
// If it's not an option flag, assume it's the output path
treeOutputFilePath = argumentValue;
}
}
return {
treeOutputFilePath,
maximumDirectoryDepth,
showHelpText
};
}
/**
* Loads and parses patterns from the .gitignore file
*
* @returns Promise resolving to an array of parsed gitignore patterns
*/
async function loadGitignorePatternDefinitions(): Promise<OperationResult<GitignorePatternDefinition[]>> {
try {
const gitignoreContent = await fs.readFile('.gitignore', 'utf-8');
const patternDefinitions = gitignoreContent
.split('\n')
.map(line => line.trim())
// Remove comments, empty lines, and lines with just whitespace
.filter(line => line && !line.startsWith('#') && line.trim() !== '')
// Process each pattern
.map(pattern => ({
patternText: pattern.startsWith('!') ? pattern.slice(1) : pattern,
isNegatedPattern: pattern.startsWith('!'),
// Convert glob patterns to regex-compatible strings (simplified approach)
regexPattern: pattern
.replace(/\./g, '\\.') // Escape dots first
.replace(/\*/g, '.*') // Convert * to .*
.replace(/\?/g, '.') // Convert ? to .
.replace(/\/$/, '(/.*)?') // Handle directory indicators
}));
return createSuccessResult(patternDefinitions);
} catch (exceptionObject) {
console.warn('No .gitignore file found, using default patterns only');
return createSuccessResult([]);
}
}
/**
* Checks if a given file path should be ignored based on patterns
*
* @param entryPath - The relative path to check
* @param ignorePatternDefinitions - Array of parsed gitignore patterns
* @returns Boolean indicating if the path should be ignored
*/
function checkPathShouldBeIgnored(
entryPath: string,
ignorePatternDefinitions: GitignorePatternDefinition[]
): boolean {
// Always check default patterns first
if (DEFAULT_IGNORE_PATTERNS.some(pattern => entryPath.includes(pattern))) {
return true;
}
let shouldBeIgnored = false;
for (const { patternText, isNegatedPattern, regexPattern } of ignorePatternDefinitions) {
// Convert the pattern to a proper regex
const compiledRegexPattern = new RegExp(`^${regexPattern}$|/${regexPattern}$|/${regexPattern}/`);
if (compiledRegexPattern.test(entryPath)) {
// If it's a negation pattern (!pattern), this file should NOT be ignored
// Otherwise, it should be ignored
shouldBeIgnored = !isNegatedPattern;
}
}
return shouldBeIgnored;
}
/**
* Recursively generates a tree representation of the directory structure
*
* @param directoryPath - Path to the directory to process
* @param ignorePatternDefinitions - Array of gitignore pattern definitions
* @param prefixString - Prefix string for the current level (used for indentation)
* @param isLastEntry - Whether this is the last entry at the current level
* @param relativePathString - Relative path from the root directory
* @param currentDepthLevel - Current depth level in the traversal
* @returns Promise resolving to the string representation of the tree
*/
async function generateDirectoryTreeRepresentation(
directoryPath: string,
ignorePatternDefinitions: GitignorePatternDefinition[],
prefixString = '',
isLastEntry = true,
relativePathString = '',
currentDepthLevel = 0,
maximumDepthLevel = Infinity
): Promise<OperationResult<string>> {
try {
const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
let treeOutputContent = '';
// Filter and sort entries
const filteredEntries = directoryEntries
.filter(entry => {
const entryPath = path.join(relativePathString, entry.name);
return !checkPathShouldBeIgnored(entryPath, ignorePatternDefinitions);
})
.sort((a, b) => {
// Directories first, then files
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (let entryIndex = 0; entryIndex < filteredEntries.length; entryIndex++) {
const entryItem = filteredEntries[entryIndex];
const isLastItem = entryIndex === filteredEntries.length - 1;
const newPrefixString = prefixString + (isLastEntry ? ' ' : '│ ');
const newRelativePath = path.join(relativePathString, entryItem.name);
treeOutputContent += prefixString + (isLastItem ? '└── ' : '├── ') + entryItem.name + '\n';
// Only traverse deeper if we haven't reached maximumDepthLevel
if (entryItem.isDirectory() && currentDepthLevel < maximumDepthLevel) {
const subTreeResult = await generateDirectoryTreeRepresentation(
path.join(directoryPath, entryItem.name),
ignorePatternDefinitions,
newPrefixString,
isLastItem,
newRelativePath,
currentDepthLevel + 1,
maximumDepthLevel
);
if (subTreeResult.resultSuccessful) {
treeOutputContent += subTreeResult.resultData;
} else {
return subTreeResult; // Propagate error
}
}
}
return createSuccessResult(treeOutputContent);
} catch (exceptionObject) {
return createFailureResult(
wrapExceptionAsStandardizedError(
exceptionObject,
`Failed to generate tree for directory: ${directoryPath}`
)
);
}
}
/**
* Ensures the directory for the output file exists, creating it if needed
*
* @param directoryPath - Path to the directory to check/create
* @returns Promise resolving to operation result
*/
async function ensureDirectoryExists(
directoryPath: string
): Promise<OperationResult<boolean>> {
try {
await fs.access(directoryPath);
return createSuccessResult(true);
} catch {
try {
await fs.mkdir(directoryPath, { recursive: true });
console.log(`Creating directory: ${directoryPath}`);
return createSuccessResult(true);
} catch (exceptionObject) {
return createFailureResult(
wrapExceptionAsStandardizedError(
exceptionObject,
`Failed to create directory: ${directoryPath}`
)
);
}
}
}
/**
* Writes the generated tree content to a markdown file
*
* @param projectName - Name of the project
* @param treeContent - Generated tree content
* @param outputFilePath - Path where the output file should be written
* @param maximumDepthValue - Maximum depth value that was applied
* @returns Promise resolving to operation result
*/
async function writeTreeContentToFile(
projectName: string,
treeContent: string,
outputFilePath: string,
maximumDepthValue: number
): Promise<OperationResult<TreeGenerationResult>> {
try {
const rootDirectoryPath = process.cwd();
const outputDirectoryPath = path.dirname(path.resolve(rootDirectoryPath, outputFilePath));
// Ensure output directory exists
const directoryResult = await ensureDirectoryExists(outputDirectoryPath);
if (!directoryResult.resultSuccessful) {
return directoryResult;
}
// Format the timestamp
const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
// Format the markdown content
const markdownContent = `# ${projectName} - Directory Structure
Generated on: ${timestamp}
${maximumDepthValue !== Infinity ? `_Depth limited to ${maximumDepthValue} levels_\n\n` : ''}
\`\`\`
${projectName}
${treeContent}
\`\`\`
_Note: This tree excludes files and directories matched by .gitignore and common patterns like node_modules._
`;
// Write the content to the file
await fs.writeFile(
path.resolve(rootDirectoryPath, outputFilePath),
markdownContent
);
return createSuccessResult({
projectName,
treeOutputFilePath: outputFilePath,
treeContentLength: treeContent.length,
maximumDepthApplied: maximumDepthValue,
generationTimestamp: timestamp
});
} catch (exceptionObject) {
return createFailureResult(
wrapExceptionAsStandardizedError(
exceptionObject,
`Failed to write tree to file: ${outputFilePath}`
)
);
}
}
/**
* Main operation function that orchestrates the tree generation process
*
* @returns Promise that resolves when the operation completes
*/
async function generateProjectDirectoryTree(): Promise<void> {
try {
// Parse command line arguments
const commandLineArguments = process.argv.slice(2);
const configurationSettings = parseCommandLineArguments(commandLineArguments);
// Display help if requested
if (configurationSettings.showHelpText) {
console.log(HELP_TEXT);
process.exit(0);
}
const rootDirectoryPath = process.cwd();
const projectName = path.basename(rootDirectoryPath);
// Load gitignore patterns
const ignorePatternResult = await loadGitignorePatternDefinitions();
if (!ignorePatternResult.resultSuccessful) {
throw new Error(`Failed to load gitignore patterns: ${ignorePatternResult.resultError.errorMessage}`);
}
const ignorePatternDefinitions = ignorePatternResult.resultData;
console.log(`Generating directory tree for: ${projectName}`);
console.log(`Output path: ${configurationSettings.treeOutputFilePath}`);
if (configurationSettings.maximumDirectoryDepth !== Infinity) {
console.log(`Maximum depth: ${configurationSettings.maximumDirectoryDepth}`);
}
// Generate the tree structure
const treeGenerationResult = await generateDirectoryTreeRepresentation(
rootDirectoryPath,
ignorePatternDefinitions,
'',
true,
'',
0,
configurationSettings.maximumDirectoryDepth
);
if (!treeGenerationResult.resultSuccessful) {
throw new Error(`Failed to generate tree: ${treeGenerationResult.resultError.errorMessage}`);
}
// Write the tree to a file
const writeResult = await writeTreeContentToFile(
projectName,
treeGenerationResult.resultData,
configurationSettings.treeOutputFilePath,
configurationSettings.maximumDirectoryDepth
);
if (!writeResult.resultSuccessful) {
throw new Error(`Failed to write tree: ${writeResult.resultError.errorMessage}`);
}
console.log(`✓ Successfully generated tree structure in ${configurationSettings.treeOutputFilePath}`);
} catch (exceptionObject) {
const standardizedError = wrapExceptionAsStandardizedError(
exceptionObject,
'Unhandled error during tree generation'
);
console.error(`× Error generating tree: ${standardizedError.errorMessage}`);
process.exit(1);
}
}
// -----------------------------------
// Script Execution
// -----------------------------------
// Execute the main operation function
generateProjectDirectoryTree();