diff --git a/Configuration/AppConfig.cs b/Configuration/AppConfig.cs
new file mode 100644
index 0000000..eecabc6
--- /dev/null
+++ b/Configuration/AppConfig.cs
@@ -0,0 +1,13 @@
+namespace CodeContext.Configuration;
+
+///
+/// Application configuration loaded from config.json.
+///
+public record AppConfig
+{
+ public string DefaultInputPath { get; init; } = ".";
+ public string DefaultOutputFileName { get; init; } = "context.txt";
+ public string OutputFormat { get; init; } = "text";
+ public bool IncludeStructure { get; init; } = true;
+ public bool IncludeContents { get; init; } = true;
+}
diff --git a/FileChecker.cs b/FileChecker.cs
deleted file mode 100644
index 2056388..0000000
--- a/FileChecker.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using CodeContext.Configuration;
-using CodeContext.Services;
-
-namespace CodeContext;
-
-///
-/// Legacy compatibility wrapper for FileFilterService.
-/// Use FileFilterService directly for new code.
-///
-[Obsolete("Use FileFilterService instead for better testability and maintainability.")]
-public class FileChecker
-{
- private static readonly Lazy _instance = new(() =>
- new FileFilterService(new FilterConfiguration()));
-
- ///
- /// Determines if a file or directory should be skipped during processing.
- ///
- /// The file or directory information.
- /// The root path of the project being scanned.
- /// True if the file/directory should be skipped; otherwise, false.
- public static bool ShouldSkip(FileSystemInfo info, string rootPath)
- {
- return _instance.Value.ShouldSkip(info, rootPath);
- }
-}
\ No newline at end of file
diff --git a/FileUtils.cs b/FileUtils.cs
deleted file mode 100644
index c19af74..0000000
--- a/FileUtils.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using CodeContext.Utils;
-
-namespace CodeContext;
-
-///
-/// Legacy compatibility wrapper for FileUtilities.
-/// Use FileUtilities instead for new code.
-///
-[Obsolete("Use FileUtilities in CodeContext.Utils namespace instead.")]
-public static class FileUtils
-{
- ///
- /// Determines if a file is binary based on its content.
- ///
- /// Path to the file to check.
- /// True if the file appears to be binary; otherwise, false.
- public static bool IsBinaryFile(string filePath)
- {
- return FileUtilities.IsBinaryFile(filePath);
- }
-}
\ No newline at end of file
diff --git a/MyAppsContext.cs b/MyAppsContext.cs
deleted file mode 100644
index 6b6a40b..0000000
--- a/MyAppsContext.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using CodeContext.Configuration;
-using CodeContext.Services;
-
-namespace CodeContext;
-
-///
-/// Legacy compatibility wrapper for ProjectScanner.
-/// Use ProjectScanner directly for new code.
-///
-[Obsolete("Use ProjectScanner instead for better testability and maintainability.")]
-public class MyAppsContext
-{
- private static readonly Lazy _instance = new(() =>
- {
- var config = new FilterConfiguration();
- var fileChecker = new FileFilterService(config);
- var console = new ConsoleWriter();
- return new ProjectScanner(fileChecker, console);
- });
-
- ///
- /// Gets the git repository root path.
- ///
- public static string? GitRepoRoot => _instance.Value.GitRepoRoot;
-
- ///
- /// Gets user input with a prompt.
- ///
- /// The prompt to display.
- /// The user's input.
- public static string GetUserInput(string prompt)
- {
- return _instance.Value.GetUserInput(prompt);
- }
-
- ///
- /// Generates a hierarchical structure representation of the project directory.
- ///
- /// The directory path to scan.
- /// Current indentation level (used for recursion).
- /// A string representation of the directory structure.
- public static string GetProjectStructure(string path, int indent = 0)
- {
- return _instance.Value.GetProjectStructure(path, indent);
- }
-
- ///
- /// Retrieves the contents of all non-filtered files in the directory tree.
- ///
- /// The directory path to scan.
- /// A string containing all file contents with separators.
- public static string GetFileContents(string path)
- {
- return _instance.Value.GetFileContents(path);
- }
-}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index b5862bc..5187b5c 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,27 +1,48 @@
using System.Diagnostics;
using System.Text;
-using System.Text.Json;
-using CodeContext;
+using CodeContext.Configuration;
+using CodeContext.Services;
Console.OutputEncoding = Encoding.UTF8;
try
{
- var config = LoadConfig();
- var path = GetValidPath(args.FirstOrDefault() ?? config.DefaultInputPath);
-
- var inputFolderName = GetInputFolderName(path);
- var prefixedDefaultFileName = $"{inputFolderName}_{config.DefaultOutputFileName}";
- var defaultFullOutputPath = Path.Combine(path, prefixedDefaultFileName);
- var outputTarget = GetValidOutputPath(args.ElementAtOrDefault(1), defaultFullOutputPath);
-
- var sw = Stopwatch.StartNew();
- var content = BuildContent(path, config);
- var stats = CalculateStats(path, content, sw.Elapsed);
-
- string actualOutputPath = WriteOutput(outputTarget, content, config.OutputFormat, prefixedDefaultFileName);
- Console.WriteLine($"\n✅ Output written to {actualOutputPath}");
- Console.WriteLine(stats);
+ // Initialize dependencies
+ var console = new ConsoleWriter();
+ var configLoader = new ConfigLoader(console);
+ var pathResolver = new PathResolver(console);
+ var filterConfig = new FilterConfiguration();
+ var fileChecker = new FileFilterService(filterConfig);
+ var scanner = new ProjectScanner(fileChecker, console);
+ var contentBuilder = new ContentBuilder(scanner);
+ var outputFormatter = new OutputFormatter(console);
+ var statsCalculator = new StatsCalculator();
+
+ // Load configuration
+ var config = configLoader.Load();
+
+ // Get and validate input path
+ var defaultInputPath = args.FirstOrDefault() ?? config.DefaultInputPath;
+ console.Write($"Enter the path to index (default: {defaultInputPath}): ");
+ var projectPath = pathResolver.GetInputPath(defaultInputPath);
+
+ // Determine output path
+ var folderName = PathResolver.GetFolderName(projectPath);
+ var defaultFileName = $"{folderName}_{config.DefaultOutputFileName}";
+ var defaultOutputPath = Path.Combine(projectPath, defaultFileName);
+ var outputArg = args.ElementAtOrDefault(1);
+ console.Write($"Enter output file/directory (default: {defaultOutputPath}): ");
+ var outputPath = pathResolver.GetOutputPath(outputArg, defaultOutputPath);
+
+ // Build content
+ var stopwatch = Stopwatch.StartNew();
+ var content = contentBuilder.Build(projectPath, config);
+ var stats = statsCalculator.Calculate(projectPath, content, stopwatch.Elapsed);
+
+ // Write output
+ var actualOutputPath = outputFormatter.WriteToFile(outputPath, content, config.OutputFormat, defaultFileName);
+ console.WriteLine($"\n✅ Output written to {actualOutputPath}");
+ console.WriteLine(stats);
}
catch (DirectoryNotFoundException ex)
{
@@ -47,222 +68,3 @@
}
Environment.Exit(4);
}
-
-///
-/// Loads configuration from config.json file if it exists, otherwise returns default configuration.
-///
-/// The loaded or default configuration.
-static Config LoadConfig()
-{
- try
- {
- var configJson = File.Exists("config.json") ? File.ReadAllText("config.json") : "{}";
- return JsonSerializer.Deserialize(configJson) ?? new Config();
- }
- catch (JsonException ex)
- {
- Console.WriteLine($"⚠️ Warning: Invalid config.json format ({ex.Message}). Using defaults.");
- return new Config();
- }
-}
-
-///
-/// Gets and validates the directory path to be indexed.
-///
-/// The default path to use if user doesn't provide one.
-/// The validated full path.
-/// Thrown when the specified directory doesn't exist.
-static string GetValidPath(string defaultPath)
-{
- var path = MyAppsContext.GetUserInput($"Enter the path to index (default: {defaultPath}): ");
- var finalPath = string.IsNullOrWhiteSpace(path) ? defaultPath : path;
- var fullPath = Path.GetFullPath(finalPath);
-
- if (!Directory.Exists(fullPath))
- {
- throw new DirectoryNotFoundException($"Directory not found: {fullPath}");
- }
-
- return fullPath;
-}
-
-///
-/// Gets and validates the output path for the generated context file.
-///
-/// Optional output path from command-line arguments.
-/// Default output path if none provided.
-/// The validated full output path.
-static string GetValidOutputPath(string? outputArgFromUser, string defaultFullOutputPathIfNoArgAndNoInput)
-{
- if (!string.IsNullOrWhiteSpace(outputArgFromUser))
- {
- return Path.GetFullPath(outputArgFromUser);
- }
-
- var userInput = MyAppsContext.GetUserInput($"Enter output file/directory (default: {defaultFullOutputPathIfNoArgAndNoInput}): ");
- return string.IsNullOrWhiteSpace(userInput)
- ? defaultFullOutputPathIfNoArgAndNoInput
- : Path.GetFullPath(userInput);
-}
-
-///
-/// Extracts a clean folder name from the input path for output file naming.
-///
-/// The input path.
-/// A sanitized folder name.
-static string GetInputFolderName(string path)
-{
- var inputFolderName = new DirectoryInfo(path).Name;
-
- if (string.IsNullOrEmpty(inputFolderName) || inputFolderName == ".")
- {
- inputFolderName = new DirectoryInfo(Environment.CurrentDirectory).Name;
-
- if (path.EndsWith(Path.DirectorySeparatorChar.ToString()) ||
- path.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
- {
- var root = Path.GetPathRoot(Path.GetFullPath(path));
- if (!string.IsNullOrEmpty(root))
- {
- inputFolderName = root
- .Replace(Path.DirectorySeparatorChar.ToString(), "")
- .Replace(Path.AltDirectorySeparatorChar.ToString(), "")
- .Replace(":", "");
-
- if (string.IsNullOrEmpty(inputFolderName))
- {
- inputFolderName = "root";
- }
- }
- }
- }
-
- return inputFolderName;
-}
-
-///
-/// Builds the complete content output including structure and file contents based on configuration.
-///
-/// The directory path to process.
-/// The configuration specifying what to include.
-/// The complete output content.
-/// Thrown when an error occurs during processing.
-static string BuildContent(string path, Config config)
-{
- try
- {
- var sb = new StringBuilder();
-
- if (config.IncludeStructure)
- {
- sb.AppendLine("Project Structure:")
- .AppendLine(MyAppsContext.GetProjectStructure(path));
- }
-
- if (config.IncludeContents)
- {
- sb.AppendLine("\nFile Contents:")
- .AppendLine(MyAppsContext.GetFileContents(path));
- }
-
- return sb.ToString();
- }
- catch (Exception ex)
- {
- throw new InvalidOperationException($"Error processing project at {path}", ex);
- }
-}
-
-///
-/// Calculates and formats statistics about the processing operation.
-///
-/// The directory that was processed.
-/// The generated content.
-/// Time elapsed during processing.
-/// Formatted statistics string.
-static string CalculateStats(string path, string content, TimeSpan timeTaken)
-{
- try
- {
- var fileCount = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Length;
- var lineCount = content.Count(c => c == '\n');
-
- return $"""
-
- 📊 Stats:
- 📁 Files processed: {fileCount}
- 📝 Total lines: {lineCount}
- ⏱️ Time taken: {timeTaken.TotalSeconds:F2}s
- 💾 Output size: {content.Length} characters
- """;
- }
- catch (Exception)
- {
- return "\n📊 Stats: Unable to calculate statistics";
- }
-}
-
-///
-/// Writes the generated content to the specified output location.
-///
-/// Target path (file or directory).
-/// Content to write.
-/// Output format (text or json).
-/// Filename to use if outputTarget is a directory.
-/// The actual path where the file was written.
-/// Thrown when an error occurs during file writing.
-static string WriteOutput(string outputTarget, string content, string format, string effectiveOutputFileName)
-{
- Console.WriteLine("\n💾 Writing output...");
- string resolvedFilePath;
-
- try
- {
- if (Directory.Exists(outputTarget))
- {
- resolvedFilePath = Path.Combine(outputTarget, effectiveOutputFileName);
- }
- else
- {
- resolvedFilePath = outputTarget;
- var outputDirectory = Path.GetDirectoryName(resolvedFilePath);
-
- if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
- {
- Directory.CreateDirectory(outputDirectory);
- }
- }
-
- var formattedContent = format.ToLower() == "json"
- ? JsonSerializer.Serialize(new { content, timestamp = DateTime.Now }, new JsonSerializerOptions { WriteIndented = true })
- : content;
-
- File.WriteAllText(resolvedFilePath, formattedContent);
- return resolvedFilePath;
- }
- catch (UnauthorizedAccessException ex)
- {
- throw new IOException($"Access denied writing to {outputTarget}", ex);
- }
- catch (Exception ex)
- {
- throw new IOException($"Failed to write output to {outputTarget}", ex);
- }
-}
-
-///
-/// Application configuration record.
-///
-/// Default directory path to scan.
-/// Default output file name.
-/// Output format (text or json).
-/// Whether to include directory structure in output.
-/// Whether to include file contents in output.
-record Config
-{
- public string DefaultInputPath { get; init; } = ".";
- public string DefaultOutputFileName { get; init; } = "context.txt";
- public string OutputFormat { get; init; } = "text";
- public bool IncludeStructure { get; init; } = true;
- public bool IncludeContents { get; init; } = true;
-}
diff --git a/Services/ConfigLoader.cs b/Services/ConfigLoader.cs
new file mode 100644
index 0000000..e7b58e0
--- /dev/null
+++ b/Services/ConfigLoader.cs
@@ -0,0 +1,34 @@
+using System.Text.Json;
+using CodeContext.Configuration;
+
+namespace CodeContext.Services;
+
+///
+/// Loads application configuration from config.json.
+///
+public class ConfigLoader
+{
+ private readonly IConsoleWriter _console;
+
+ public ConfigLoader(IConsoleWriter console)
+ {
+ _console = console;
+ }
+
+ ///
+ /// Loads configuration from config.json if it exists, otherwise returns default configuration.
+ ///
+ public AppConfig Load()
+ {
+ try
+ {
+ var configJson = File.Exists("config.json") ? File.ReadAllText("config.json") : "{}";
+ return JsonSerializer.Deserialize(configJson) ?? new AppConfig();
+ }
+ catch (JsonException ex)
+ {
+ _console.WriteLine($"⚠️ Warning: Invalid config.json format ({ex.Message}). Using defaults.");
+ return new AppConfig();
+ }
+ }
+}
diff --git a/Services/ContentBuilder.cs b/Services/ContentBuilder.cs
new file mode 100644
index 0000000..a6da355
--- /dev/null
+++ b/Services/ContentBuilder.cs
@@ -0,0 +1,42 @@
+using System.Text;
+using CodeContext.Configuration;
+
+namespace CodeContext.Services;
+
+///
+/// Builds project context content based on configuration.
+///
+public class ContentBuilder
+{
+ private readonly ProjectScanner _scanner;
+
+ public ContentBuilder(ProjectScanner scanner)
+ {
+ _scanner = scanner;
+ }
+
+ ///
+ /// Builds the complete content output including structure and file contents.
+ ///
+ /// The directory path to process.
+ /// The configuration specifying what to include.
+ /// The complete output content.
+ public string Build(string projectPath, AppConfig config)
+ {
+ var content = new StringBuilder();
+
+ if (config.IncludeStructure)
+ {
+ content.AppendLine("Project Structure:")
+ .AppendLine(_scanner.GetProjectStructure(projectPath));
+ }
+
+ if (config.IncludeContents)
+ {
+ content.AppendLine("\nFile Contents:")
+ .AppendLine(_scanner.GetFileContents(projectPath));
+ }
+
+ return content.ToString();
+ }
+}
diff --git a/Services/FileFilterService.cs b/Services/FileFilterService.cs
index 76304c6..f37ea6b 100644
--- a/Services/FileFilterService.cs
+++ b/Services/FileFilterService.cs
@@ -13,28 +13,16 @@ public class FileFilterService : IFileChecker
private readonly GitIgnoreParser _gitIgnoreParser;
private bool _gitIgnoreLoaded;
- ///
- /// Initializes a new instance of the FileFilterService class.
- ///
- /// The filter configuration to use.
public FileFilterService(FilterConfiguration config)
{
- _config = config ?? throw new ArgumentNullException(nameof(config));
+ _config = Guard.NotNull(config, nameof(config));
_gitIgnoreParser = new GitIgnoreParser();
}
- ///
public bool ShouldSkip(FileSystemInfo info, string rootPath)
{
- if (info == null)
- {
- throw new ArgumentNullException(nameof(info));
- }
-
- if (string.IsNullOrEmpty(rootPath))
- {
- throw new ArgumentException("Root path cannot be null or empty.", nameof(rootPath));
- }
+ Guard.NotNull(info, nameof(info));
+ Guard.NotNullOrEmpty(rootPath, nameof(rootPath));
// Check if any parent directory is in the ignored list
var relativePath = Path.GetRelativePath(rootPath, info.FullName);
@@ -117,14 +105,15 @@ private bool ShouldSkipByExtension(string fileName)
private bool ShouldSkipByGitIgnore(string filePath, string rootPath)
{
- if (!IsInGitRepository(rootPath))
+ if (!GitHelper.IsInRepository(rootPath))
{
return false;
}
if (!_gitIgnoreLoaded)
{
- var gitIgnorePath = Path.Combine(FindGitRepoRoot(rootPath) ?? rootPath, ".gitignore");
+ var gitRoot = GitHelper.FindRepositoryRoot(rootPath) ?? rootPath;
+ var gitIgnorePath = Path.Combine(gitRoot, ".gitignore");
_gitIgnoreParser.LoadFromFile(gitIgnorePath);
_gitIgnoreLoaded = true;
}
@@ -134,8 +123,8 @@ private bool ShouldSkipByGitIgnore(string filePath, string rootPath)
return false;
}
- var gitRoot = FindGitRepoRoot(rootPath) ?? rootPath;
- var relativePath = Path.GetRelativePath(gitRoot, filePath);
+ var repositoryRoot = GitHelper.FindRepositoryRoot(rootPath) ?? rootPath;
+ var relativePath = Path.GetRelativePath(repositoryRoot, filePath);
return _gitIgnoreParser.IsIgnored(relativePath);
}
@@ -146,30 +135,9 @@ private bool IsGeneratedCode(string filePath)
var lines = File.ReadLines(filePath).Take(_config.GeneratedCodeLinesToCheck);
return lines.Any(line => line.Contains(""));
}
- catch (Exception)
+ catch
{
- // If we can't read the file, assume it's not generated code
return false;
}
}
-
- private static bool IsInGitRepository(string path)
- {
- return FindGitRepoRoot(path) != null;
- }
-
- private static string? FindGitRepoRoot(string path)
- {
- var currentPath = path;
- while (!string.IsNullOrEmpty(currentPath))
- {
- if (Directory.Exists(Path.Combine(currentPath, ".git")))
- {
- return currentPath;
- }
- currentPath = Path.GetDirectoryName(currentPath);
- }
-
- return null;
- }
}
diff --git a/Services/OutputFormatter.cs b/Services/OutputFormatter.cs
new file mode 100644
index 0000000..9bb3238
--- /dev/null
+++ b/Services/OutputFormatter.cs
@@ -0,0 +1,63 @@
+using System.Text.Json;
+using CodeContext.Interfaces;
+
+namespace CodeContext.Services;
+
+///
+/// Formats and writes output to files in various formats.
+///
+public class OutputFormatter
+{
+ private readonly IConsoleWriter _console;
+
+ public OutputFormatter(IConsoleWriter console)
+ {
+ _console = console;
+ }
+
+ ///
+ /// Writes content to the specified output location.
+ ///
+ /// Target path (file or directory).
+ /// Content to write.
+ /// Output format (text or json).
+ /// Filename to use if outputTarget is a directory.
+ /// The actual path where the file was written.
+ public string WriteToFile(string outputTarget, string content, string format, string defaultFileName)
+ {
+ _console.WriteLine("\n💾 Writing output...");
+
+ var resolvedPath = ResolveOutputPath(outputTarget, defaultFileName);
+ EnsureDirectoryExists(resolvedPath);
+
+ var formattedContent = FormatContent(content, format);
+ File.WriteAllText(resolvedPath, formattedContent);
+
+ return resolvedPath;
+ }
+
+ private static string ResolveOutputPath(string outputTarget, string defaultFileName)
+ {
+ return Directory.Exists(outputTarget)
+ ? Path.Combine(outputTarget, defaultFileName)
+ : outputTarget;
+ }
+
+ private static void EnsureDirectoryExists(string filePath)
+ {
+ var directory = Path.GetDirectoryName(filePath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+ }
+
+ private static string FormatContent(string content, string format)
+ {
+ return format.ToLower() == "json"
+ ? JsonSerializer.Serialize(
+ new { content, timestamp = DateTime.Now },
+ new JsonSerializerOptions { WriteIndented = true })
+ : content;
+ }
+}
diff --git a/Services/PathResolver.cs b/Services/PathResolver.cs
new file mode 100644
index 0000000..32555e4
--- /dev/null
+++ b/Services/PathResolver.cs
@@ -0,0 +1,96 @@
+using CodeContext.Interfaces;
+using CodeContext.Utils;
+
+namespace CodeContext.Services;
+
+///
+/// Resolves and validates input and output paths with user interaction.
+///
+public class PathResolver
+{
+ private readonly IConsoleWriter _console;
+
+ public PathResolver(IConsoleWriter console)
+ {
+ _console = console;
+ }
+
+ ///
+ /// Gets and validates the input directory path.
+ ///
+ /// The default path to use if user doesn't provide one.
+ /// The validated full path to the input directory.
+ public string GetInputPath(string defaultPath)
+ {
+ var userPath = _console.ReadLine() ?? string.Empty;
+ var selectedPath = string.IsNullOrWhiteSpace(userPath) ? defaultPath : userPath;
+ var fullPath = Path.GetFullPath(selectedPath);
+
+ return Guard.DirectoryExists(fullPath, nameof(fullPath));
+ }
+
+ ///
+ /// Gets and validates the output file path.
+ ///
+ /// Optional output path from command-line arguments.
+ /// Default output path if none provided.
+ /// The validated full output path.
+ public string GetOutputPath(string? commandLineArg, string defaultPath)
+ {
+ if (!string.IsNullOrWhiteSpace(commandLineArg))
+ {
+ return Path.GetFullPath(commandLineArg);
+ }
+
+ var userInput = _console.ReadLine() ?? string.Empty;
+ return string.IsNullOrWhiteSpace(userInput)
+ ? defaultPath
+ : Path.GetFullPath(userInput);
+ }
+
+ ///
+ /// Extracts a clean folder name from the input path for output file naming.
+ ///
+ /// The input path.
+ /// A sanitized folder name.
+ public static string GetFolderName(string path)
+ {
+ var folderName = new DirectoryInfo(path).Name;
+
+ if (!string.IsNullOrEmpty(folderName) && folderName != ".")
+ {
+ return folderName;
+ }
+
+ folderName = new DirectoryInfo(Environment.CurrentDirectory).Name;
+
+ if (IsPathSeparatorTerminated(path))
+ {
+ return ExtractRootFolderName(path) ?? "root";
+ }
+
+ return folderName;
+ }
+
+ private static bool IsPathSeparatorTerminated(string path)
+ {
+ return path.EndsWith(Path.DirectorySeparatorChar.ToString()) ||
+ path.EndsWith(Path.AltDirectorySeparatorChar.ToString());
+ }
+
+ private static string? ExtractRootFolderName(string path)
+ {
+ var root = Path.GetPathRoot(Path.GetFullPath(path));
+ if (string.IsNullOrEmpty(root))
+ {
+ return null;
+ }
+
+ var cleanedRoot = root
+ .Replace(Path.DirectorySeparatorChar.ToString(), "")
+ .Replace(Path.AltDirectorySeparatorChar.ToString(), "")
+ .Replace(":", "");
+
+ return string.IsNullOrEmpty(cleanedRoot) ? null : cleanedRoot;
+ }
+}
diff --git a/Services/ProjectScanner.cs b/Services/ProjectScanner.cs
index 8041162..bf4d931 100644
--- a/Services/ProjectScanner.cs
+++ b/Services/ProjectScanner.cs
@@ -1,5 +1,6 @@
using System.Text;
using CodeContext.Interfaces;
+using CodeContext.Utils;
namespace CodeContext.Services;
@@ -12,15 +13,10 @@ public class ProjectScanner
private readonly IConsoleWriter _console;
private string? _gitRepoRoot;
- ///
- /// Initializes a new instance of the ProjectScanner class.
- ///
- /// The file checker to use for filtering.
- /// The console writer for output.
public ProjectScanner(IFileChecker fileChecker, IConsoleWriter console)
{
- _fileChecker = fileChecker ?? throw new ArgumentNullException(nameof(fileChecker));
- _console = console ?? throw new ArgumentNullException(nameof(console));
+ _fileChecker = Guard.NotNull(fileChecker, nameof(fileChecker));
+ _console = Guard.NotNull(console, nameof(console));
}
///
@@ -37,35 +33,26 @@ public string GetUserInput(string prompt)
///
/// Generates a hierarchical structure representation of the project directory.
///
- /// The directory path to scan.
+ /// The directory path to scan.
/// Current indentation level (used for recursion).
/// A string representation of the directory structure.
- public string GetProjectStructure(string path, int indent = 0)
+ public string GetProjectStructure(string projectPath, int indent = 0)
{
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentException("Path cannot be null or empty.", nameof(path));
- }
-
- if (!Directory.Exists(path))
- {
- throw new DirectoryNotFoundException($"Directory not found: {path}");
- }
-
- _gitRepoRoot ??= FindGitRepoRoot(path);
+ Guard.DirectoryExists(projectPath, nameof(projectPath));
+ _gitRepoRoot ??= GitHelper.FindRepositoryRoot(projectPath);
if (indent == 0)
{
_console.WriteLine("📁 Analyzing directory structure...");
}
- var rootPath = _gitRepoRoot ?? path;
- var entries = Directory.EnumerateFileSystemEntries(path)
+ var rootPath = _gitRepoRoot ?? projectPath;
+ var entries = Directory.EnumerateFileSystemEntries(projectPath)
.OrderBy(e => e)
.Where(e => !_fileChecker.ShouldSkip(new FileInfo(e), rootPath))
.ToList();
- var sb = new StringBuilder();
+ var structure = new StringBuilder();
for (int i = 0; i < entries.Count; i++)
{
@@ -75,45 +62,36 @@ public string GetProjectStructure(string path, int indent = 0)
if (Directory.Exists(entry))
{
var dir = new DirectoryInfo(entry);
- sb.AppendLine($"{new string(' ', indent * 2)}[{dir.Name}/]");
- sb.Append(GetProjectStructure(entry, indent + 1));
+ structure.AppendLine($"{new string(' ', indent * 2)}[{dir.Name}/]");
+ structure.Append(GetProjectStructure(entry, indent + 1));
}
else
{
var file = new FileInfo(entry);
- sb.AppendLine($"{new string(' ', indent * 2)}[{file.Extension}] {file.Name}");
+ structure.AppendLine($"{new string(' ', indent * 2)}[{file.Extension}] {file.Name}");
}
}
- return sb.ToString();
+ return structure.ToString();
}
///
/// Retrieves the contents of all non-filtered files in the directory tree.
///
- /// The directory path to scan.
+ /// The directory path to scan.
/// A string containing all file contents with separators.
- public string GetFileContents(string path)
+ public string GetFileContents(string projectPath)
{
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentException("Path cannot be null or empty.", nameof(path));
- }
-
- if (!Directory.Exists(path))
- {
- throw new DirectoryNotFoundException($"Directory not found: {path}");
- }
-
- _gitRepoRoot ??= FindGitRepoRoot(path);
+ Guard.DirectoryExists(projectPath, nameof(projectPath));
+ _gitRepoRoot ??= GitHelper.FindRepositoryRoot(projectPath);
_console.WriteLine("\n📄 Processing files...");
- var rootPath = _gitRepoRoot ?? path;
- var files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
+ var rootPath = _gitRepoRoot ?? projectPath;
+ var files = Directory.EnumerateFiles(projectPath, "*", SearchOption.AllDirectories)
.Where(f => !_fileChecker.ShouldSkip(new FileInfo(f), rootPath))
.ToList();
- var results = new List();
+ var fileContents = new List();
for (int i = 0; i < files.Count; i++)
{
WriteProgress(i + 1, files.Count);
@@ -122,7 +100,7 @@ public string GetFileContents(string path)
try
{
var content = File.ReadAllText(file);
- results.Add($"{file}\n{new string('-', 100)}\n{content}");
+ fileContents.Add($"{file}\n{new string('-', 100)}\n{content}");
}
catch (Exception ex)
{
@@ -130,34 +108,14 @@ public string GetFileContents(string path)
}
}
- return string.Join("\n\n", results);
+ return string.Join("\n\n", fileContents);
}
///
- /// Gets the root path of the git repository containing the specified path.
+ /// Gets the root path of the git repository containing the scanned path.
///
public string? GitRepoRoot => _gitRepoRoot;
- private string? FindGitRepoRoot(string path)
- {
- if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
- {
- return null;
- }
-
- var currentPath = path;
- while (!string.IsNullOrEmpty(currentPath))
- {
- if (Directory.Exists(Path.Combine(currentPath, ".git")))
- {
- return currentPath;
- }
- currentPath = Path.GetDirectoryName(currentPath);
- }
-
- return null;
- }
-
private void WriteProgress(int current, int total)
{
var percent = (int)((current / (double)total) * 100);
diff --git a/Services/StatsCalculator.cs b/Services/StatsCalculator.cs
new file mode 100644
index 0000000..49408f8
--- /dev/null
+++ b/Services/StatsCalculator.cs
@@ -0,0 +1,36 @@
+namespace CodeContext.Services;
+
+///
+/// Calculates statistics about processed projects.
+///
+public class StatsCalculator
+{
+ ///
+ /// Calculates and formats statistics about the processing operation.
+ ///
+ /// The directory that was processed.
+ /// The generated content.
+ /// Time elapsed during processing.
+ /// Formatted statistics string.
+ public string Calculate(string projectPath, string content, TimeSpan elapsed)
+ {
+ try
+ {
+ var fileCount = Directory.GetFiles(projectPath, "*", SearchOption.AllDirectories).Length;
+ var lineCount = content.Count(c => c == '\n');
+
+ return $"""
+
+ 📊 Stats:
+ 📁 Files processed: {fileCount}
+ 📝 Total lines: {lineCount}
+ ⏱️ Time taken: {elapsed.TotalSeconds:F2}s
+ 💾 Output size: {content.Length} characters
+ """;
+ }
+ catch
+ {
+ return "\n📊 Stats: Unable to calculate statistics";
+ }
+ }
+}
diff --git a/Utils/FileUtilities.cs b/Utils/FileUtilities.cs
index d4f0671..8beb376 100644
--- a/Utils/FileUtilities.cs
+++ b/Utils/FileUtilities.cs
@@ -14,10 +14,7 @@ public static class FileUtilities
/// True if the file appears to be binary; otherwise, false.
public static bool IsBinaryFile(string filePath, int chunkSize = 4096, double binaryThreshold = 0.3)
{
- if (string.IsNullOrEmpty(filePath))
- {
- throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
- }
+ Guard.NotNullOrEmpty(filePath, nameof(filePath));
if (!File.Exists(filePath))
{
@@ -33,7 +30,6 @@ public static bool IsBinaryFile(string filePath, int chunkSize = 4096, double bi
return false;
}
- // Check for UTF-8 BOM
if (HasUtf8Bom(stream))
{
return false;
@@ -41,9 +37,8 @@ public static bool IsBinaryFile(string filePath, int chunkSize = 4096, double bi
return CheckBinaryContent(stream, chunkSize, binaryThreshold);
}
- catch (Exception)
+ catch
{
- // If we can't read the file (permissions, etc.), assume it's not binary
return false;
}
}
diff --git a/Utils/GitHelper.cs b/Utils/GitHelper.cs
new file mode 100644
index 0000000..9452de9
--- /dev/null
+++ b/Utils/GitHelper.cs
@@ -0,0 +1,42 @@
+namespace CodeContext.Utils;
+
+///
+/// Provides Git repository utilities.
+///
+public static class GitHelper
+{
+ ///
+ /// Finds the root directory of the Git repository containing the specified path.
+ ///
+ /// The path to start searching from.
+ /// The Git repository root path, or null if not in a Git repository.
+ public static string? FindRepositoryRoot(string? startPath)
+ {
+ if (string.IsNullOrEmpty(startPath) || !Directory.Exists(startPath))
+ {
+ return null;
+ }
+
+ var currentPath = startPath;
+ while (!string.IsNullOrEmpty(currentPath))
+ {
+ if (Directory.Exists(Path.Combine(currentPath, ".git")))
+ {
+ return currentPath;
+ }
+ currentPath = Path.GetDirectoryName(currentPath);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Determines if the specified path is within a Git repository.
+ ///
+ /// The path to check.
+ /// True if the path is within a Git repository; otherwise, false.
+ public static bool IsInRepository(string? path)
+ {
+ return FindRepositoryRoot(path) != null;
+ }
+}
diff --git a/Utils/Guard.cs b/Utils/Guard.cs
new file mode 100644
index 0000000..db47bdc
--- /dev/null
+++ b/Utils/Guard.cs
@@ -0,0 +1,76 @@
+namespace CodeContext.Utils;
+
+///
+/// Provides guard clauses for parameter validation to reduce boilerplate.
+///
+public static class Guard
+{
+ ///
+ /// Ensures that a reference is not null.
+ ///
+ /// The type of the parameter.
+ /// The value to check.
+ /// The name of the parameter.
+ /// The value if not null.
+ /// Thrown when value is null.
+ public static T NotNull(T? value, string paramName) where T : class
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(paramName);
+ }
+ return value;
+ }
+
+ ///
+ /// Ensures that a string is not null or empty.
+ ///
+ /// The value to check.
+ /// The name of the parameter.
+ /// The value if not null or empty.
+ /// Thrown when value is null or empty.
+ public static string NotNullOrEmpty(string? value, string paramName)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException($"{paramName} cannot be null or empty.", paramName);
+ }
+ return value;
+ }
+
+ ///
+ /// Ensures that a directory exists.
+ ///
+ /// The directory path to check.
+ /// The name of the parameter.
+ /// The path if the directory exists.
+ /// Thrown when the directory doesn't exist.
+ public static string DirectoryExists(string path, string paramName)
+ {
+ NotNullOrEmpty(path, paramName);
+
+ if (!Directory.Exists(path))
+ {
+ throw new DirectoryNotFoundException($"Directory not found: {path}");
+ }
+ return path;
+ }
+
+ ///
+ /// Ensures that a file exists.
+ ///
+ /// The file path to check.
+ /// The name of the parameter.
+ /// The path if the file exists.
+ /// Thrown when the file doesn't exist.
+ public static string FileExists(string path, string paramName)
+ {
+ NotNullOrEmpty(path, paramName);
+
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"File not found: {path}");
+ }
+ return path;
+ }
+}