diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2702bd1
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,38 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.2.0] - 2025-11-01
+
+### Added
+- **TOON Output Format**: New token-efficient output format using the `rtoon` library
+ - Reduces token count by 2-5% for code repositories compared to JSON
+ - Up to 30-60% savings for structured data
+ - Use with `--format toon` option
+- Comprehensive test suite with 34 tests covering CLI, config, scanning, formatting, and security
+- Integration testing with real repositories (serde crate) validating TOON efficiency
+
+### Changed
+- Updated dependencies: Added `rtoon = "0.1.3"`
+- Made formatter functions public for testing
+- Enhanced CLI argument validation for output formats
+
+### Fixed
+- Improved scanner logic for include pattern matching when using subdirectories
+
+## [0.1.0] - 2025-10-XX
+
+### Added
+- Initial release of Remix - Rust implementation of repomix
+- Repository packing for AI tools
+- Multiple output formats: Markdown, JSON, Text
+- Remote repository support
+- Intelligent file filtering with include/exclude patterns
+- Multi-layered ignore system (.gitignore, .mixignore, custom patterns)
+- Security checks for sensitive information
+- Comment removal feature
+- Flexible JSON configuration
+- High-performance parallel processing
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 7322083..f10fb43 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1313,7 +1313,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "remix"
-version = "0.1.0"
+version = "0.2.0"
dependencies = [
"anyhow",
"assert_fs",
@@ -1332,11 +1332,12 @@ dependencies = [
"rayon",
"regex",
"reqwest",
+ "rtoon",
"serde",
"serde_json",
"tempfile",
"test-case",
- "thiserror",
+ "thiserror 1.0.69",
"tokio",
"tree_magic_mini",
"walkdir",
@@ -1382,6 +1383,17 @@ dependencies = [
"winreg",
]
+[[package]]
+name = "rtoon"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ee2f1565c9114bf1294b90ce7c5d1f38881fc97e52971452ac9c0bfcb475ed"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror 2.0.17",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@@ -1471,18 +1483,28 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -1491,14 +1513,16 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.140"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
+ "indexmap",
"itoa",
"memchr",
"ryu",
"serde",
+ "serde_core",
]
[[package]]
@@ -1682,7 +1706,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl 2.0.17",
]
[[package]]
@@ -1696,6 +1729,17 @@ dependencies = [
"syn",
]
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tinystr"
version = "0.7.6"
diff --git a/Cargo.toml b/Cargo.toml
index 65be544..f83ff28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "remix"
-version = "0.1.0"
+version = "0.2.0"
edition = "2021"
authors = ["Trenton Sousa tdsousa1993@gmail.com"]
description = "A Rust implementation of repomix - pack repositories for AI tools"
@@ -31,6 +31,7 @@ crossbeam = "0.8"
rayon = "1.8"
tree_magic_mini = "3.0"
glob-match = "0.2"
+rtoon = "0.1.3"
[dev-dependencies]
tempfile = "3.9"
diff --git a/README.md b/README.md
index dd8cd9c..53e59d2 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ flowchart TD
| ๐ฏ **Intelligent Filtering** | Include/exclude files using glob patterns |
| ๐ก๏ธ **Multi-layered Ignore System** | Uses `.gitignore`, `.mixignore`, and custom ignore patterns |
| ๐ **Security Checks** | Automatically detect and warn about sensitive information |
-| ๐ **Multiple Output Formats** | Markdown, JSON, and plain text support |
+| ๐ **Multiple Output Formats** | Markdown, JSON, plain text, and TOON support |
| ๐งน **Comment Removal** | Optionally strip comments from source code to reduce token count |
| โ๏ธ **Flexible Configuration** | JSON-based config files with CLI overrides |
| ๐จ **AI Tool Optimized** | Formatted output designed for LLM consumption |
@@ -211,8 +211,8 @@ remix --init
# Specify output path
remix --output ./my-repo.md
-# Change output format (md, json, txt)
-remix --format json
+# Change output format (md, json, txt, toon)
+remix --format toon
# Open output file after generation
remix --open
@@ -327,6 +327,14 @@ Structured JSON output containing:
- File information (path, size, type)
- Configuration used
+### TOON
+
+Token-efficient output using the TOON format:
+- Compact representation optimized for LLM prompts
+- 30-60% fewer tokens than JSON for structured data
+- Human-readable with indentation-based structure
+- Ideal for AI analysis workflows
+
### Text
Plain text output with:
diff --git a/remix-output.md b/remix-output.md
new file mode 100644
index 0000000..bd1329d
--- /dev/null
+++ b/remix-output.md
@@ -0,0 +1,3130 @@
+# User Instruction
+
+Please analyze this Rust codebase for performance optimizations and security vulnerabilities. Focus on the core logic in src/ and ignore test files unless they demonstrate important patterns.
+
+# Repository Summary
+
+- **Files:** 17
+- **Directories:** 3
+- **Total Size:** 87.76 KB
+- **Binary Files:** 0
+- **Extensions:** md, rs, toml
+
+## Security Check Results
+
+โ ๏ธ **6 suspicious file(s) detected that may contain sensitive information:**
+
+1. `tests/security_tests.rs`
+2. `src/config.rs`
+3. `tests/common/mod.rs`
+4. `src/security.rs`
+5. `tests/config_tests.rs`
+6. `remix.config.json`
+
+> **Note:** Please review these files before sharing this output.
+
+# Files
+
+## /
+
+### Cargo.toml
+
+- **Path:** Cargo.toml
+- **Size:** 966 bytes
+- **Type:** toml
+
+```toml
+[package]
+name = "remix"
+version = "0.1.0"
+edition = "2021"
+authors = ["Trenton Sousa tdsousa1993@gmail.com"]
+description = "A Rust implementation of repomix - pack repositories for AI tools"
+license = "MIT"
+readme = "README.md"
+repository = "https://github.com/CodingInCarhartts/remix"
+keywords = ["ai", "repository", "packing", "llm"]
+
+[dependencies]
+clap = { version = "4.4", features = ["derive"] }
+glob = "0.3"
+walkdir = "2.4"
+ignore = "0.4"
+regex = "1.10"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tokio = { version = "1.36", features = ["full"] }
+reqwest = { version = "0.11", features = ["json"] }
+anyhow = "1.0"
+thiserror = "1.0"
+log = "0.4"
+env_logger = "0.10"
+indicatif = "0.17"
+console = "0.15"
+tempfile = "3.9"
+git2 = "0.18"
+crossbeam = "0.8"
+rayon = "1.8"
+tree_magic_mini = "3.0"
+glob-match = "0.2"
+rtoon = "0.1.3"
+
+[dev-dependencies]
+tempfile = "3.9"
+test-case = "3.3"
+mockall = "0.12"
+assert_fs = "1.1"
+predicates = "3.0"
+
+```
+
+### README.md
+
+- **Path:** README.md
+- **Size:** 10.35 KB
+- **Type:** md
+
+```md
+
+
+# ๐ Remix
+
+[](https://crates.io/crates/remix)
+[](https://opensource.org/licenses/MIT)
+[](https://www.rust-lang.org/)
+
+**A blazing-fast Rust implementation of [repomix](https://github.com/yamadashy/repomix)**
+
+*Pack your entire repository into a single, AI-friendly file*
+
+[Installation](#-installation) โข
+[Usage](#-usage) โข
+[Configuration](#-configuration) โข
+[Features](#-features) โข
+[Examples](#-examples) โข
+[License](#-license)
+
+
+
+---
+
+## ๐ Overview
+
+Remix prepares your codebase for AI analysis by packing it into a single file. Perfect for feeding your code to Large Language Models (LLMs) like Claude, ChatGPT, DeepSeek, Perplexity, Gemini, and more.
+
+
+ ```mermaid
+flowchart TD
+ A[CLI Execution] --> B[Parse Command Line Arguments]
+ B --> C[Load Configuration]
+ C --> D{Remote Repository?}
+
+ D -->|Yes| E[Parse Remote URL]
+ E --> F[Clone Repository to Temp Directory]
+ F --> G[Checkout Target Branch/Commit]
+
+ D -->|No| H[Use Local Directory Path]
+
+ G --> I[Scan Repository]
+ H --> I
+
+ I --> J[Apply Include/Ignore Filters]
+ J --> K[Multi-layer Filtering]
+ K --> L[File Size Check]
+ L --> M[Binary File Detection]
+
+ M --> N[Parallel File Processing]
+ N --> O[Read File Content]
+ O --> P{Security Check Enabled?}
+ P -->|Yes| Q[Check for Sensitive Content]
+ P -->|No| R[Skip Security Check]
+
+ Q --> S[Filter Sensitive Files]
+ R --> T[Process File Content]
+ S --> T
+
+ T --> U{Compression Mode?}
+ U -->|Yes| V[Compress Content]
+ U -->|No| W{Comment Removal?}
+
+ V --> X[Format Output]
+ W -->|Yes| Y[Remove Comments]
+ W -->|No| X
+
+ Y --> X
+
+ X --> Z[Generate Repository Summary]
+ Z --> AA[Format Output File]
+ AA --> BB{Markdown Format}
+ AA --> CC{JSON Format}
+ AA --> DD{Text Format}
+
+ BB --> EE[Write Markdown File]
+ CC --> FF[Write JSON File]
+ DD --> GG[Write Text File]
+
+ EE --> HH{Open File?}
+ FF --> HH
+ GG --> HH
+
+ HH -->|Yes| II[Open Output File]
+ HH -->|No| JJ[Workflow Complete]
+ II --> JJ
+```
+
+
A visualization of how Remix processes your repository
+
+
+## โจ Features
+
+| Feature | Description |
+|---------|-------------|
+| โก **High Performance** | Built in Rust for maximum speed and efficiency |
+| ๐ **Repository Packing** | Combine your entire codebase into a single file |
+| ๐ **Remote Repository Support** | Process GitHub repositories directly with branch/tag/commit support |
+| ๐ฏ **Intelligent Filtering** | Include/exclude files using glob patterns |
+| ๐ก๏ธ **Multi-layered Ignore System** | Uses `.gitignore`, `.mixignore`, and custom ignore patterns |
+| ๐ **Security Checks** | Automatically detect and warn about sensitive information |
+| ๐ **Multiple Output Formats** | Markdown, JSON, plain text, and TOON support |
+| ๐งน **Comment Removal** | Optionally strip comments from source code to reduce token count |
+| โ๏ธ **Flexible Configuration** | JSON-based config files with CLI overrides |
+| ๐จ **AI Tool Optimized** | Formatted output designed for LLM consumption |
+
+## ๐ฆ Installation
+
+**From Crates.io:**
+```bash
+cargo install remix
+```
+
+**From Source:**
+```bash
+git clone https://github.com/CodingInCarhartts/remix.git
+cd remix
+cargo build --release
+# Binary will be available at target/release/remix
+```
+
+**As a Cargo Subcommand:**
+```bash
+cargo install --features cargo-subcommand remix
+# Then use as: cargo mix [options]
+```
+
+## ๐ง Usage
+
+### Basic Commands
+
+Pack your entire repository:
+```bash
+remix
+```
+
+Pack a specific directory:
+```bash
+remix /path/to/your/project
+```
+
+Pack a remote repository:
+```bash
+remix --remote https://github.com/username/repo
+```
+
+### Advanced Options
+
+
+๐ File Selection
+
+```bash
+# Include specific files or directories using glob patterns
+remix --include "*.rs,*.toml"
+remix --include "src/**/*.rs,tests/**/*.rs"
+
+# Exclude specific files or directories
+remix --ignore "*.log,*.tmp"
+remix --ignore "node_modules/**,target/**"
+
+# Set maximum file size (in bytes)
+remix --max-file-size 50000
+```
+
+
+
+๐ Remote Repositories
+
+```bash
+# Pack a remote repository (full URL)
+remix --remote https://github.com/microsoft/vscode
+
+# Pack a specific branch
+remix --remote https://github.com/user/repo --remote-branch develop
+
+# Pack a specific tag
+remix --remote https://github.com/user/repo --remote-branch v1.2.3
+
+# Using GitLab
+remix --remote https://gitlab.com/example/project
+```
+
+
+
+โ๏ธ Processing Options
+
+```bash
+# Remove comments from code
+remix --remove-comments
+
+# Skip security checks (use with caution)
+remix --skip-sensitive-check
+
+# Compress the output
+remix --compress
+
+# Add custom instructions for AI
+remix --instruction "Please analyze this Rust codebase"
+
+# Use instruction file
+remix --instruction-file ./context.txt
+
+# Initialize a new configuration file
+remix --init
+```
+
+
+
+๐ Output Options
+
+```bash
+# Specify output path
+remix --output ./my-repo.md
+
+# Change output format (md, json, txt, toon)
+remix --format toon
+
+# Open output file after generation
+remix --open
+```
+
+
+## ๐ Configuration
+
+Create a `remix.config.json` file in your project root for custom configurations:
+
+```json
+{
+ "include": [],
+ "ignore": {
+ "use_gitignore": true,
+ "use_default_patterns": true,
+ "use_mixignore": true,
+ "custom_patterns": []
+ },
+ "max_file_size": 100000,
+ "compress": false,
+ "security": {
+ "enable_security_check": true
+ },
+ "output": {
+ "format": "md",
+ "open_file": false,
+ "path": "./remix-output.md",
+ "instruction_file_path": null,
+ "remove_comments": false
+ },
+ "instruction": null
+}
+```
+
+### Configuration Examples
+
+
+Basic configuration with custom includes
+
+```json
+{
+ "include": ["src/**/*.rs", "Cargo.toml", "README.md"],
+ "output": {
+ "path": "./my-project.md"
+ }
+}
+```
+
+
+
+Configuration for a Node.js project
+
+```json
+{
+ "include": ["src/**/*.js", "package.json", "README.md"],
+ "ignore": {
+ "custom_patterns": ["node_modules/**", "*.log"]
+ },
+ "output": {
+ "format": "md",
+ "remove_comments": true
+ }
+}
+```
+
+
+
+Configuration for AI analysis
+
+```json
+{
+ "include": ["src/**/*.py", "requirements.txt", "docs/**"],
+ "max_file_size": 50000,
+ "output": {
+ "format": "md",
+ "instruction_file_path": "./ai-instructions.txt"
+ },
+ "instruction": "Please analyze this Python codebase for security vulnerabilities"
+}
+```
+
+
+## ๐ซ Ignore Files
+
+Remix uses a multi-layered ignore system:
+
+1. **`.gitignore`** - Standard Git ignore patterns are respected
+2. **`.mixignore`** - Project-specific ignore patterns for Remix
+3. **Command line `--ignore` patterns** or configuration file settings
+4. **Default patterns** - Common files and directories (node_modules, target, etc.)
+
+You can disable any of these layers using command-line options:
+- `--no-gitignore` - Don't use .gitignore patterns
+- `--no-default-patterns` - Don't use default ignore patterns
+
+## ๐ Output Formats
+
+### Markdown (Default)
+
+Generates a well-formatted markdown file with:
+- File tree structure
+- Syntax-highlighted code blocks
+- File metadata (size, path)
+- Instructions and context at the top
+
+### JSON
+
+Structured JSON output containing:
+- Repository metadata
+- File contents as base64-encoded strings
+- File information (path, size, type)
+- Configuration used
+
+### TOON
+
+Token-efficient output using the TOON format:
+- Compact representation optimized for LLM prompts
+- 30-60% fewer tokens than JSON for structured data
+- Human-readable with indentation-based structure
+- Ideal for AI analysis workflows
+
+### Text
+
+Plain text output with:
+- Simple file headers
+- Raw code content
+- Minimal formatting
+
+### Basic Usage Examples
+
+**Pack the current directory:**
+```bash
+remix
+```
+
+**Pack a specific project:**
+```bash
+remix ~/projects/my-app
+```
+
+**Include only Rust files:**
+```bash
+remix --include "*.rs"
+```
+
+### AI Tool Preparation
+
+**Add instructions for AI analysis:**
+```bash
+remix --instruction "Please review this codebase for security issues" --output ./security-review.md
+```
+
+**Use instruction file:**
+```bash
+echo "Analyze the following Rust code for performance optimizations:" > instructions.txt
+remix --instruction-file ./instructions.txt --include "*.rs"
+```
+
+### Advanced Filtering
+
+**Process only small files:**
+```bash
+remix --max-file-size 10000
+```
+
+**Exclude build artifacts:**
+```bash
+remix --ignore "target/**,*.log"
+```
+
+## ๐ง Troubleshooting
+
+### Common Issues
+
+**"No files found" error:**
+- Check your include patterns
+- Verify the path exists and contains files
+- Try `--no-default-patterns` if files are being excluded
+
+**Large output files:**
+- Use `--max-file-size` to limit file sizes
+- Add more ignore patterns with `--ignore`
+- Use `--compress` to reduce output size
+
+**Permission denied:**
+- Ensure you have read access to the target directory
+- For remote repos, ensure the repository is public or you have access
+
+**Configuration not loading:**
+- Verify the JSON syntax is valid
+- Check the config file path
+- Use `--init` to create a new config file
+
+### Performance Tips
+
+- Use specific include patterns instead of processing entire directories
+- Exclude large directories like `node_modules`, `target`, etc.
+- Use `--compress` for smaller output files
+- For large repositories, consider using `--max-file-size`
+
+## ๐ค Contributing
+
+Contributions are welcome! Feel free to open issues or submit pull requests.
+
+1. Fork the repository
+2. Create your feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
+
+## ๐ License
+
+[MIT License](https://opensource.org/licenses/MIT) - See the LICENSE file for details.
+
+---
+
+
+
+```
+
+## src/
+
+### cli.rs
+
+- **Path:** src/cli.rs
+- **Size:** 2.67 KB
+- **Type:** rs
+
+```rs
+use clap::Parser;
+use std::path::PathBuf;
+
+#[derive(Parser, Debug)]
+#[command(
+ name = "remix",
+ about = "Pack your repository into a single file for AI tools",
+ version,
+ author
+)]
+pub struct Cli {
+
+ #[arg(index = 1)]
+ pub path: Option,
+
+
+ #[arg(short, long)]
+ pub config: Option,
+
+
+ #[arg(long)]
+ pub init: bool,
+
+
+ #[arg(long)]
+ pub include: Option,
+
+
+ #[arg(long)]
+ pub ignore: Option,
+
+
+ #[arg(long)]
+ pub max_file_size: Option,
+
+
+ #[arg(short, long)]
+ pub output: Option,
+
+
+ #[arg(long, value_parser = ["md", "markdown", "json", "txt", "text", "toon"])]
+ pub format: Option,
+
+
+ #[arg(long)]
+ pub compress: bool,
+
+
+ #[arg(long)]
+ pub skip_sensitive_check: bool,
+
+
+ #[arg(long)]
+ pub remote: Option,
+
+
+ #[arg(long)]
+ pub remote_branch: Option,
+
+
+ #[arg(long)]
+ pub open: bool,
+
+
+ #[arg(long)]
+ pub instruction: Option,
+
+
+ #[arg(long)]
+ pub instruction_file: Option,
+
+
+ #[arg(long)]
+ pub remove_comments: bool,
+
+
+ #[arg(long)]
+ pub no_gitignore: bool,
+
+
+ #[arg(long)]
+ pub no_default_patterns: bool,
+}
+
+impl Cli {
+
+ pub fn parse_comma_separated(&self, input: &Option) -> Option> {
+ input.as_ref().map(|s| {
+ s.split(',')
+ .map(|part| part.trim().to_string())
+ .filter(|part| !part.is_empty())
+ .collect()
+ })
+ }
+
+
+ pub fn include_patterns(&self) -> Option> {
+ self.parse_comma_separated(&self.include)
+ }
+
+
+ pub fn ignore_patterns(&self) -> Option> {
+ self.parse_comma_separated(&self.ignore)
+ }
+}
+
+```
+
+### comments.rs
+
+- **Path:** src/comments.rs
+- **Size:** 12.19 KB
+- **Type:** rs
+
+```rs
+
+pub fn remove_comments(content: &str, file_extension: &str) -> String {
+ match file_extension {
+ "rs" => remove_rust_comments(content),
+ "js" | "ts" | "jsx" | "tsx" => remove_js_comments(content),
+ "py" => remove_python_comments(content),
+ "c" | "cpp" | "h" | "hpp" | "cs" | "java" | "go" | "swift" | "kt" => {
+ remove_c_style_comments(content)
+ }
+ "rb" => remove_ruby_comments(content),
+ "php" => remove_php_comments(content),
+ "html" | "xml" | "svg" => remove_html_comments(content),
+ "css" | "scss" | "sass" | "less" => remove_css_comments(content),
+ "sh" | "bash" => remove_shell_comments(content),
+ "yaml" | "yml" => remove_yaml_comments(content),
+ _ => content.to_string(),
+ }
+}
+
+
+pub fn is_comment_removal_supported(extension: &str) -> bool {
+ matches!(
+ extension,
+ "rs" | "js"
+ | "ts"
+ | "jsx"
+ | "tsx"
+ | "py"
+ | "c"
+ | "cpp"
+ | "h"
+ | "hpp"
+ | "cs"
+ | "java"
+ | "go"
+ | "swift"
+ | "kt"
+ | "rb"
+ | "php"
+ | "html"
+ | "xml"
+ | "svg"
+ | "css"
+ | "scss"
+ | "sass"
+ | "less"
+ | "sh"
+ | "bash"
+ | "yaml"
+ | "yml"
+ )
+}
+
+
+fn remove_rust_comments(content: &str) -> String {
+ remove_c_style_comments(content)
+}
+
+
+fn remove_js_comments(content: &str) -> String {
+ remove_c_style_comments(content)
+}
+
+
+fn remove_python_comments(content: &str) -> String {
+ let mut result = String::new();
+ let mut in_multiline_string = false;
+ let mut multiline_quotes = "";
+
+ let lines = content.lines();
+ for line in lines {
+ let line_trim = line.trim();
+
+
+ if in_multiline_string {
+ result.push_str(line);
+ result.push('\n');
+
+ if line.contains(multiline_quotes) {
+
+ let parts: Vec<&str> = line.rsplitn(2, multiline_quotes).collect();
+ if parts.len() > 1 && !parts[0].ends_with('\\') {
+ in_multiline_string = false;
+ }
+ }
+ continue;
+ }
+
+
+ if (line_trim.contains("'''") || line_trim.contains("\"\"\""))
+ && !(line_trim.starts_with("#"))
+ {
+ if line_trim.contains("'''") {
+ multiline_quotes = "'''";
+ } else {
+ multiline_quotes = "\"\"\"";
+ }
+
+
+ let count = line_trim.matches(multiline_quotes).count();
+ if count % 2 == 1 {
+ in_multiline_string = true;
+ }
+
+ result.push_str(line);
+ result.push('\n');
+ continue;
+ }
+
+
+ if let Some(comment_pos) = line.find('#') {
+ let preceding = &line[0..comment_pos];
+
+ let in_string = preceding.chars().filter(|&c| c == '"' || c == '\'').count() % 2 == 1;
+
+ if in_string {
+ result.push_str(line);
+ } else {
+ result.push_str(preceding);
+ }
+ result.push('\n');
+ } else {
+ result.push_str(line);
+ result.push('\n');
+ }
+ }
+
+ result
+}
+
+
+fn remove_c_style_comments(content: &str) -> String {
+ let mut result = String::new();
+ let mut chars = content.chars().peekable();
+ let mut in_string = false;
+ let mut in_char = false;
+ let mut in_single_line_comment = false;
+ let mut in_multi_line_comment = false;
+ let mut string_quote = '"';
+
+ while let Some(c) = chars.next() {
+ match c {
+
+ '"' | '\'' => {
+ if !in_single_line_comment && !in_multi_line_comment {
+ if c == '"' && !in_char {
+ if !in_string {
+ string_quote = '"';
+ in_string = true;
+ } else if string_quote == '"' {
+ in_string = false;
+ }
+ result.push(c);
+ } else if c == '\'' && !in_string {
+ in_char = !in_char;
+ result.push(c);
+ } else {
+ result.push(c);
+ }
+ }
+ }
+
+ '\\' => {
+ if !in_single_line_comment && !in_multi_line_comment && (in_string || in_char) {
+ result.push(c);
+ if let Some(next) = chars.next() {
+ result.push(next);
+ }
+ } else if !in_single_line_comment && !in_multi_line_comment {
+ result.push(c);
+ }
+ }
+
+ '/' => {
+ if !in_string && !in_char && !in_single_line_comment && !in_multi_line_comment {
+ if let Some(&next) = chars.peek() {
+ if next == '/' {
+ in_single_line_comment = true;
+ chars.next();
+ } else if next == '*' {
+ in_multi_line_comment = true;
+ chars.next();
+ } else {
+ result.push(c);
+ }
+ } else {
+ result.push(c);
+ }
+ } else if in_multi_line_comment {
+ if let Some(prev) = result.chars().last() {
+ if prev == '*' {
+ result.pop();
+ in_multi_line_comment = false;
+ }
+ }
+ } else if !in_single_line_comment && !in_multi_line_comment {
+ result.push(c);
+ }
+ }
+
+ '\n' => {
+ if in_single_line_comment {
+ in_single_line_comment = false;
+ }
+ result.push(c);
+ }
+
+ '*' => {
+ if !in_string && !in_char && !in_single_line_comment && in_multi_line_comment {
+ if let Some(&next) = chars.peek() {
+ if next == '/' {
+ in_multi_line_comment = false;
+ chars.next();
+ }
+ }
+ } else if !in_single_line_comment && !in_multi_line_comment {
+ result.push(c);
+ }
+ }
+
+ _ => {
+ if !in_single_line_comment && !in_multi_line_comment {
+ result.push(c);
+ }
+ }
+ }
+ }
+
+ result
+}
+
+
+fn remove_ruby_comments(content: &str) -> String {
+
+ remove_python_comments(content)
+}
+
+
+fn remove_php_comments(content: &str) -> String {
+
+ let content = remove_c_style_comments(content);
+
+
+ let mut result = String::new();
+ for line in content.lines() {
+ if let Some(comment_pos) = line.find('#') {
+ let preceding = &line[0..comment_pos];
+ result.push_str(preceding);
+ result.push('\n');
+ } else {
+ result.push_str(line);
+ result.push('\n');
+ }
+ }
+
+ result
+}
+
+
+fn remove_html_comments(content: &str) -> String {
+
+ let mut result = String::with_capacity(content.len());
+ let mut state = 0;
+
+ let mut chars = content.chars().peekable();
+ while let Some(c) = chars.next() {
+ match state {
+ 0 => {
+ if c == '<' {
+ state = 1;
+ result.push(c);
+ } else {
+ result.push(c);
+ }
+ }
+ 1 => {
+ if c == '!' {
+ state = 2;
+ result.push(c);
+ } else {
+ state = 0;
+ result.push(c);
+ }
+ }
+ 2 => {
+ if c == '-' {
+ state = 3;
+ result.push(c);
+ } else {
+ state = 0;
+ result.push(c);
+ }
+ }
+ 3 => {
+ if c == '-' {
+
+ state = 4;
+
+ result.truncate(result.len() - 3);
+ } else {
+ state = 0;
+ result.push(c);
+ }
+ }
+ 4 => {
+
+ if c == '-' {
+
+ if chars.peek() == Some(&'-') {
+ chars.next();
+ if chars.peek() == Some(&'>') {
+ chars.next();
+ state = 0;
+ }
+ }
+ }
+
+ }
+ _ => unreachable!(),
+ }
+ }
+
+ result
+}
+
+
+fn remove_css_comments(content: &str) -> String {
+ let mut result = String::new();
+ let mut i = 0;
+
+ while i < content.len() {
+ if i + 1 < content.len() && &content[i..i + 2] == "/*" {
+ let comment_end = content[i + 2..].find("*/");
+ if let Some(end) = comment_end {
+ i = i + 2 + end + 2;
+ } else {
+ result.push_str(&content[i..]);
+ break;
+ }
+ } else {
+ result.push(content.chars().nth(i).unwrap());
+ i += 1;
+ }
+ }
+
+ result
+}
+
+
+fn remove_shell_comments(content: &str) -> String {
+ let mut result = String::new();
+
+ for line in content.lines() {
+ if let Some(comment_pos) = line.find('#') {
+
+ let preceding = &line[0..comment_pos];
+ if preceding.contains("echo") || preceding.contains("printf") {
+ result.push_str(line);
+ } else {
+ result.push_str(preceding);
+ }
+ result.push('\n');
+ } else {
+ result.push_str(line);
+ result.push('\n');
+ }
+ }
+
+ result
+}
+
+
+fn remove_yaml_comments(content: &str) -> String {
+ let mut result = String::new();
+
+ for line in content.lines() {
+ if let Some(comment_pos) = line.find('#') {
+ let preceding = &line[0..comment_pos];
+ result.push_str(preceding);
+ result.push('\n');
+ } else {
+ result.push_str(line);
+ result.push('\n');
+ }
+ }
+
+ result
+}
+
+```
+
+### config.rs
+
+- **Path:** src/config.rs
+- **Size:** 9.29 KB
+- **Type:** rs
+
+```rs
+use crate::cli::Cli;
+use anyhow::{Context, Result};
+use log::{info, warn};
+use serde::{Deserialize, Serialize};
+use std::fs;
+use std::io::Write;
+use std::path::Path;
+
+const CONFIG_FILENAME: &str = "remix.config.json";
+const DEFAULT_MAX_FILE_SIZE: u64 = 100_000;
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct OutputConfig {
+
+ #[serde(default = "default_format")]
+ pub format: String,
+
+
+ #[serde(default)]
+ pub open_file: bool,
+
+
+ #[serde(default = "default_output_path")]
+ pub path: String,
+
+
+ pub instruction_file_path: Option,
+
+
+ #[serde(default)]
+ pub remove_comments: bool,
+}
+
+fn default_format() -> String {
+ "md".to_string()
+}
+
+fn default_output_path() -> String {
+ "./remix-output.md".to_string()
+}
+
+impl Default for OutputConfig {
+ fn default() -> Self {
+ Self {
+ format: default_format(),
+ open_file: false,
+ path: default_output_path(),
+ instruction_file_path: None,
+ remove_comments: false,
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct IgnoreConfig {
+
+ #[serde(default = "default_use_gitignore")]
+ pub use_gitignore: bool,
+
+
+ #[serde(default = "default_use_default_patterns")]
+ pub use_default_patterns: bool,
+
+
+ #[serde(default = "default_use_mixignore")]
+ pub use_mixignore: bool,
+
+
+ #[serde(default)]
+ pub custom_patterns: Vec,
+}
+
+fn default_use_gitignore() -> bool {
+ true
+}
+
+fn default_use_default_patterns() -> bool {
+ true
+}
+
+fn default_use_mixignore() -> bool {
+ true
+}
+
+impl Default for IgnoreConfig {
+ fn default() -> Self {
+ Self {
+ use_gitignore: default_use_gitignore(),
+ use_default_patterns: default_use_default_patterns(),
+ use_mixignore: default_use_mixignore(),
+ custom_patterns: Vec::new(),
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct SecurityConfig {
+
+ #[serde(default = "default_enable_security_check")]
+ pub enable_security_check: bool,
+}
+
+fn default_enable_security_check() -> bool {
+ true
+}
+
+impl Default for SecurityConfig {
+ fn default() -> Self {
+ Self {
+ enable_security_check: default_enable_security_check(),
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Config {
+
+ #[serde(default)]
+ pub include: Vec,
+
+
+ #[serde(default)]
+ pub ignore: IgnoreConfig,
+
+
+ #[serde(default = "default_max_file_size")]
+ pub max_file_size: u64,
+
+
+ #[serde(default)]
+ pub compress: bool,
+
+
+ #[serde(default)]
+ pub security: SecurityConfig,
+
+
+ #[serde(default)]
+ pub output: OutputConfig,
+
+
+ pub instruction: Option,
+}
+
+fn default_max_file_size() -> u64 {
+ DEFAULT_MAX_FILE_SIZE
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ include: Vec::new(),
+ ignore: IgnoreConfig {
+ use_gitignore: default_use_gitignore(),
+ use_default_patterns: default_use_default_patterns(),
+ use_mixignore: default_use_mixignore(),
+ custom_patterns: vec![
+ "node_modules/".to_string(),
+ "package-lock.json".to_string(),
+ "**/package-lock.json".to_string(),
+ "**/node_modules/".to_string(),
+ "**/bun.lockb".to_string(),
+ "bun.lockb".to_string(),
+ "bun.lock".to_string(),
+ ".conda/".to_string(),
+ "**/.conda/".to_string(),
+ ".venv/".to_string(),
+ "**/.venv/".to_string(),
+ ".mamba/".to_string(),
+ "**/.mamba/".to_string(),
+ ".pyenv/".to_string(),
+ "**/.pyenv/".to_string(),
+ ".git/".to_string(),
+ "**/.git/".to_string(),
+ ".gitignore".to_string(),
+ "**/.gitignore".to_string(),
+ ".gitattributes".to_string(),
+ "**/.gitattributes".to_string(),
+ ".github/".to_string(),
+ "**/.github/".to_string(),
+ ".gitmodules".to_string(),
+ "**/.gitmodules".to_string(),
+ ".gitkeep".to_string(),
+ "**/.gitkeep".to_string(),
+ "target/".to_string(),
+ "**/target/".to_string(),
+ "dist/".to_string(),
+ "**/dist/".to_string(),
+ "build/".to_string(),
+ "**/build/".to_string(),
+ "**/*.log".to_string(),
+ "**/Cargo.lock".to_string(),
+ "**/.env".to_string(),
+ "**/*.exe".to_string(),
+ "**/*.o".to_string(),
+ "**/*.so".to_string(),
+ "**/*.dylib".to_string(),
+ "**/*.dll".to_string(),
+ "**/*.lib".to_string(),
+ "**/*.a".to_string(),
+ "**/*.lib".to_string(),
+ ],
+ },
+ max_file_size: default_max_file_size(),
+ compress: false,
+ security: SecurityConfig::default(),
+ output: OutputConfig::default(),
+ instruction: None,
+ }
+ }
+}
+
+impl Config {
+
+ pub fn merge_with_cli(&self, cli: &Cli) -> Self {
+ let mut config = self.clone();
+
+
+ if let Some(patterns) = cli.include_patterns() {
+ config.include = patterns;
+ }
+
+ if let Some(patterns) = cli.ignore_patterns() {
+ config.ignore.custom_patterns = patterns;
+ }
+
+ if let Some(max_size) = cli.max_file_size {
+ config.max_file_size = max_size;
+ }
+
+ if cli.compress {
+ config.compress = true;
+ }
+
+ if cli.skip_sensitive_check {
+ config.security.enable_security_check = false;
+ }
+
+ if cli.no_gitignore {
+ config.ignore.use_gitignore = false;
+ }
+
+ if cli.no_default_patterns {
+ config.ignore.use_default_patterns = false;
+ }
+
+ if let Some(format) = &cli.format {
+ config.output.format = format.clone();
+ }
+
+ if let Some(output_path) = &cli.output {
+ config.output.path = output_path.to_string_lossy().to_string();
+ }
+
+ if cli.open {
+ config.output.open_file = true;
+ }
+
+ if cli.remove_comments {
+ config.output.remove_comments = true;
+ }
+
+ if let Some(instruction) = &cli.instruction {
+ config.instruction = Some(instruction.clone());
+ }
+
+ if let Some(instruction_file) = &cli.instruction_file {
+ config.output.instruction_file_path =
+ Some(instruction_file.to_string_lossy().to_string());
+ }
+
+ config
+ }
+}
+
+
+pub fn find_and_load_config() -> Result {
+ let current_dir = std::env::current_dir()?;
+ let config_path = current_dir.join(CONFIG_FILENAME);
+
+ if config_path.exists() {
+ info!("Found configuration file: {}", config_path.display());
+ load_config(&config_path)
+ } else {
+ warn!("No configuration file found, using defaults");
+ Ok(Config::default())
+ }
+}
+
+
+pub fn load_config(path: &Path) -> Result {
+ let content = fs::read_to_string(path)
+ .context(format!("Failed to read config file: {}", path.display()))?;
+
+ let config: Config = serde_json::from_str(&content)
+ .context(format!("Failed to parse config file: {}", path.display()))?;
+
+ Ok(config)
+}
+
+
+pub fn init_config() -> Result<()> {
+ let config = Config::default();
+ let current_dir = std::env::current_dir()?;
+ let config_path = current_dir.join(CONFIG_FILENAME);
+
+ if config_path.exists() {
+ warn!(
+ "Configuration file already exists: {}",
+ config_path.display()
+ );
+ return Ok(());
+ }
+
+ let json =
+ serde_json::to_string_pretty(&config).context("Failed to serialize configuration")?;
+
+ let mut file = fs::File::create(&config_path).context(format!(
+ "Failed to create config file: {}",
+ config_path.display()
+ ))?;
+
+ file.write_all(json.as_bytes()).context(format!(
+ "Failed to write to config file: {}",
+ config_path.display()
+ ))?;
+
+ info!("Created configuration file: {}", config_path.display());
+
+ Ok(())
+}
+
+```
+
+### formatter.rs
+
+- **Path:** src/formatter.rs
+- **Size:** 9.42 KB
+- **Type:** rs
+
+```rs
+use crate::config::OutputConfig;
+use crate::packer::PackedRepository;
+use crate::utils::{format_size, open_file};
+use anyhow::{Context, Result};
+use log::{info, warn};
+use std::fs;
+use std::io::Write;
+use std::path::Path;
+use crate::security::SecurityCheckStatus;
+use rtoon;
+
+pub fn output_result(repo: &PackedRepository, config: &OutputConfig) -> Result<()> {
+ let output_path = &config.path;
+ let format = &config.format;
+
+ info!("Generating output in {} format to {}", format, output_path);
+
+ let content = match format.as_str() {
+ "md" | "markdown" => format_markdown(repo),
+ "json" => format_json(repo)?,
+ "txt" | "text" => format_text(repo),
+ "toon" => format_toon(repo)?,
+ _ => {
+ warn!("Unknown format '{}', defaulting to markdown", format);
+ format_markdown(repo)
+ }
+ };
+
+
+ let mut file = fs::File::create(output_path)
+ .context(format!("Failed to create output file: {}", output_path))?;
+
+ file.write_all(content.as_bytes())
+ .context(format!("Failed to write to output file: {}", output_path))?;
+
+ info!("Output written to {}", output_path);
+
+
+ if config.open_file {
+ info!("Opening output file");
+ open_file(output_path).context(format!("Failed to open output file: {}", output_path))?;
+ }
+
+ Ok(())
+}
+
+fn format_markdown(repo: &PackedRepository) -> String {
+ let mut output = String::new();
+
+
+ if let Some(instruction) = &repo.instruction {
+ output.push_str("# User Instruction\n\n");
+ output.push_str(instruction);
+ output.push_str("\n\n");
+ }
+
+
+ output.push_str("# Repository Summary\n\n");
+ output.push_str(&format!("- **Files:** {}\n", repo.summary.file_count));
+ output.push_str(&format!(
+ "- **Directories:** {}\n",
+ repo.summary.directory_count
+ ));
+ output.push_str(&format!(
+ "- **Total Size:** {}\n",
+ format_size(repo.summary.total_size)
+ ));
+ output.push_str(&format!(
+ "- **Binary Files:** {}\n",
+ repo.summary.binary_file_count
+ ));
+
+ if !repo.summary.extensions.is_empty() {
+ output.push_str(&format!(
+ "- **Extensions:** {}\n",
+ repo.summary.extensions.join(", ")
+ ));
+ }
+
+
+ match &repo.security_check_status {
+ SecurityCheckStatus::Disabled => {
+ output.push_str("## Security Check\n\n");
+ output.push_str("๐ **Security check was disabled**\n");
+ }
+ SecurityCheckStatus::CompletedNoFindings => {
+ output.push_str("## Security Check\n\n");
+ output.push_str("โ
**Security check completed - no suspicious files found**\n");
+ }
+ SecurityCheckStatus::CompletedWithFindings => {
+ output.push_str("\n## Security Check Results\n\n");
+ output.push_str(&format!(
+ "โ ๏ธ **{} suspicious file(s) detected that may contain sensitive information:**\n\n",
+ repo.suspicious_files.as_ref().map_or(0, |v| v.len())
+ ));
+
+ for (i, file) in repo.suspicious_files.as_ref().unwrap_or(&vec![]).iter().enumerate() {
+ output.push_str(&format!("{}. `{}`\n", i + 1, file));
+ }
+
+ output
+ .push_str("\n> **Note:** Please review these files before sharing this output.\n");
+ }
+ SecurityCheckStatus::Failed(error) => {
+ output.push_str("## Security Check\n\n");
+ output.push_str(&format!("โ **Security check failed**: {}\n", error));
+ }
+ }
+
+
+ if let Some(binary_files) = &repo.binary_files {
+ if !binary_files.is_empty() {
+ output.push_str("\n## Binary Files\n\n");
+ output.push_str(
+ "The following binary files were detected but not included in the content:\n\n",
+ );
+
+ for (i, file) in binary_files.iter().enumerate() {
+ output.push_str(&format!("{}. `{}`\n", i + 1, file));
+ }
+ }
+ }
+
+ output.push_str("\n# Files\n\n");
+
+
+ let mut files_by_dir: std::collections::BTreeMap> =
+ std::collections::BTreeMap::new();
+
+ for file in &repo.files {
+ let path = Path::new(&file.relative_path);
+ let parent = path
+ .parent()
+ .map(|p| p.to_string_lossy().to_string())
+ .unwrap_or_else(|| ".".to_string());
+
+ files_by_dir.entry(parent).or_default().push(file);
+ }
+
+
+ for (dir, files) in &files_by_dir {
+ if dir != "." {
+ output.push_str(&format!("## {}/\n\n", dir));
+ } else {
+ output.push_str("## Root Directory\n\n");
+ }
+
+ for file in files {
+ let filename = Path::new(&file.relative_path)
+ .file_name()
+ .map(|f| f.to_string_lossy().to_string())
+ .unwrap_or_else(|| file.relative_path.clone());
+
+ output.push_str(&format!("### {}\n\n", filename));
+
+
+ output.push_str(&format!("- **Path:** {}\n", file.relative_path));
+ output.push_str(&format!("- **Size:** {}\n", format_size(file.size)));
+
+ if !file.extension.is_empty() {
+ output.push_str(&format!("- **Type:** {}\n", file.extension));
+ }
+
+ output.push_str("\n```");
+
+
+ if !file.extension.is_empty() {
+ output.push_str(&file.extension);
+ }
+
+ output.push('\n');
+ output.push_str(&file.content);
+ output.push('\n');
+ output.push_str("```\n\n");
+ }
+ }
+
+ output
+}
+
+fn format_json(repo: &PackedRepository) -> Result {
+ serde_json::to_string_pretty(repo).context("Failed to serialize repository to JSON")
+}
+
+pub fn format_toon(repo: &PackedRepository) -> Result {
+ let value = serde_json::to_value(repo).context("Failed to convert repository to JSON value")?;
+ rtoon::encode_default(&value).context("Failed to encode repository to TOON")
+}
+
+fn format_text(repo: &PackedRepository) -> String {
+ let mut output = String::new();
+
+
+ if let Some(instruction) = &repo.instruction {
+ output.push_str("USER INSTRUCTION:\n\n");
+ output.push_str(instruction);
+ output.push_str("\n\n");
+ }
+
+
+ output.push_str("REPOSITORY SUMMARY:\n\n");
+ output.push_str(&format!("Files: {}\n", repo.summary.file_count));
+ output.push_str(&format!("Directories: {}\n", repo.summary.directory_count));
+ output.push_str(&format!(
+ "Total Size: {}\n",
+ format_size(repo.summary.total_size)
+ ));
+ output.push_str(&format!(
+ "Binary Files: {}\n",
+ repo.summary.binary_file_count
+ ));
+
+ if !repo.summary.extensions.is_empty() {
+ output.push_str(&format!(
+ "Extensions: {}\n",
+ repo.summary.extensions.join(", ")
+ ));
+ }
+
+
+ match &repo.security_check_status {
+ SecurityCheckStatus::Disabled => {
+ output.push_str("SECURITY CHECK:\n\n");
+ output.push_str("Security check was disabled.\n\n");
+ }
+ SecurityCheckStatus::CompletedNoFindings => {
+ output.push_str("SECURITY CHECK:\n\n");
+ output.push_str("Security check completed - no suspicious files found.\n\n");
+ }
+ SecurityCheckStatus::CompletedWithFindings => {
+ output.push_str("SECURITY CHECK:\n\n");
+ output.push_str("WARNING: ");
+ output.push_str(&format!("{} suspicious file(s) detected that may contain sensitive information:\n\n", repo.suspicious_files.as_ref().map_or(0, |v| v.len())));
+
+ for (i, file) in repo.suspicious_files.as_ref().unwrap_or(&vec![]).iter().enumerate() {
+ output.push_str(&format!("{}. {}\n", i + 1, file));
+ }
+
+ output.push_str("\nPlease review these files before sharing this output.\n");
+ }
+ SecurityCheckStatus::Failed(error) => {
+ output.push_str("SECURITY CHECK:\n\n");
+ output.push_str(&format!("Security check failed: {}\n", error));
+ }
+ }
+
+
+ if let Some(binary_files) = &repo.binary_files {
+ if !binary_files.is_empty() {
+ output.push_str("\nBINARY FILES:\n\n");
+ output.push_str(
+ "The following binary files were detected but not included in the content:\n\n",
+ );
+
+ for (i, file) in binary_files.iter().enumerate() {
+ output.push_str(&format!("{}. {}\n", i + 1, file));
+ }
+ }
+ }
+
+ output.push_str("\nFILES:\n\n");
+
+
+ for file in &repo.files {
+ output.push_str(&format!("FILE: {}\n", file.relative_path));
+ output.push_str(&format!("SIZE: {}\n", format_size(file.size)));
+
+ if !file.extension.is_empty() {
+ output.push_str(&format!("TYPE: {}\n", file.extension));
+ }
+
+ output.push_str("\nCONTENT:\n");
+ output.push_str(&file.content);
+ output.push_str("\n\n");
+ output.push_str("--------------------------------\n\n");
+ }
+
+ output
+}
+
+```
+
+### lib.rs
+
+- **Path:** src/lib.rs
+- **Size:** 148 bytes
+- **Type:** rs
+
+```rs
+pub mod cli;
+pub mod comments;
+pub mod config;
+pub mod formatter;
+pub mod packer;
+pub mod remote;
+pub mod scanner;
+pub mod security;
+pub mod utils;
+
+```
+
+### main.rs
+
+- **Path:** src/main.rs
+- **Size:** 3.98 KB
+- **Type:** rs
+
+```rs
+mod cli;
+mod comments;
+mod config;
+mod formatter;
+mod packer;
+mod remote;
+mod scanner;
+mod security;
+mod utils;
+
+use anyhow::{Context, Result};
+use clap::Parser;
+use cli::Cli;
+use console::style;
+use env_logger::Env;
+use indicatif::{ProgressBar, ProgressStyle};
+use log::info;
+use std::path::PathBuf;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+
+ env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
+
+
+ let args: Vec = std::env::args().collect();
+ let cli = if args.len() > 1 && args[1] == "mix" {
+
+ let filtered_args: Vec = args
+ .into_iter()
+ .enumerate()
+ .filter(|(i, arg)| *i != 1 || arg != "mix")
+ .map(|(_, arg)| arg)
+ .collect();
+ Cli::parse_from(filtered_args)
+ } else {
+ Cli::parse()
+ };
+
+
+ let main_spinner = ProgressBar::new_spinner();
+ main_spinner.set_style(
+ ProgressStyle::default_spinner()
+ .template("{spinner:.cyan} {wide_msg}")
+ .unwrap(),
+ );
+
+ main_spinner.set_message(format!("๐ {} starting...", style("remix").bold().green()));
+ main_spinner.tick();
+
+
+ main_spinner.set_message("Loading configuration...");
+ let config = if let Some(ref config_path) = cli.config {
+ config::load_config(config_path).context(format!(
+ "Failed to load config from {}",
+ config_path.display()
+ ))?
+ } else {
+ config::find_and_load_config().unwrap_or_default()
+ };
+
+
+ if cli.init {
+ main_spinner.set_message("Initializing configuration...");
+ config::init_config()?;
+ main_spinner.finish_with_message(format!(
+ "{} Configuration initialized successfully",
+ style("โ").bold().green()
+ ));
+ return Ok(());
+ }
+
+
+ let target_path = if let Some(path) = &cli.path {
+ PathBuf::from(path)
+ } else {
+ std::env::current_dir()?
+ };
+
+ main_spinner.set_message(format!(
+ "Starting remix on {}",
+ style(target_path.display()).cyan()
+ ));
+
+ info!("Starting remix on {}", target_path.display());
+
+
+ if let Some(remote_url) = &cli.remote {
+ let branch = cli
+ .remote_branch
+ .as_ref()
+ .map_or_else(|| "main".to_string(), |s| s.clone());
+ main_spinner.set_message(format!(
+ "Processing remote repository: {} ({})",
+ style(remote_url).cyan(),
+ style(&branch).cyan()
+ ));
+ info!("Processing remote repository: {} ({})", remote_url, branch);
+
+ let temp_dir = remote::clone_repository(remote_url, &branch)
+ .context("Failed to clone remote repository")?;
+
+ main_spinner.set_message("Processing repository...");
+ let result = packer::pack_repository(&temp_dir, &config.merge_with_cli(&cli)).await?;
+
+ main_spinner.set_message("Formatting output...");
+ formatter::output_result(&result, &config.output)?;
+ } else {
+
+ main_spinner.set_message(format!(
+ "Processing local repository: {}",
+ style(target_path.display()).cyan()
+ ));
+ info!("Processing local repository: {}", target_path.display());
+
+ main_spinner.set_message("Processing repository...");
+ let result = packer::pack_repository(&target_path, &config.merge_with_cli(&cli)).await?;
+
+ main_spinner.set_message("Formatting output...");
+ formatter::output_result(&result, &config.output)?;
+ }
+
+ main_spinner.finish_with_message(format!(
+ "{} Repository packing completed successfully",
+ style("โ").bold().green()
+ ));
+ info!("Repository packing completed successfully");
+ Ok(())
+}
+
+```
+
+### packer.rs
+
+- **Path:** src/packer.rs
+- **Size:** 12.19 KB
+- **Type:** rs
+
+```rs
+use crate::comments;
+use crate::config::Config;
+use crate::scanner::{scan_repository, FileInfo};
+use crate::security;
+use anyhow::{Context, Result};
+use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
+use log::{debug, info, warn};
+use rayon::prelude::*;
+use serde::Serialize;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, Mutex};
+
+#[derive(Debug, Serialize, Clone)]
+pub struct FileContent {
+ pub relative_path: String,
+ pub extension: String,
+ pub content: String,
+ pub size: u64,
+ pub is_binary: bool,
+}
+
+#[derive(Debug, Serialize, Clone)]
+pub struct PackedRepository {
+ pub files: Vec,
+ pub summary: RepositorySummary,
+ pub instruction: Option,
+ pub suspicious_files: Option>,
+ pub security_check_status: security::SecurityCheckStatus,
+ pub binary_files: Option>,
+}
+
+#[derive(Debug, Serialize, Clone)]
+pub struct RepositorySummary {
+ pub file_count: usize,
+ pub total_size: u64,
+ pub directory_count: usize,
+ pub extensions: Vec,
+ pub binary_file_count: usize,
+}
+
+pub async fn pack_repository(path: &Path, config: &Config) -> Result {
+ info!("Packing repository at {}", path.display());
+
+
+ let multi_progress = MultiProgress::new();
+
+
+ let scan_progress = multi_progress.add(ProgressBar::new_spinner());
+ scan_progress.set_style(
+ ProgressStyle::default_spinner()
+ .template("{spinner:.blue} {prefix:.bold.dim} {msg}")
+ .unwrap(),
+ );
+ scan_progress.set_prefix("[Scan]");
+ scan_progress.set_message("Scanning repository...");
+
+ let files = scan_repository(path, config)?;
+ scan_progress.finish_with_message(format!("Found {} files", files.len()));
+
+ debug!("Found {} files to process", files.len());
+
+
+ let binary_files: Vec = files
+ .iter()
+ .filter(|file| file.is_binary)
+ .map(|file| file.relative_path.to_string_lossy().to_string())
+ .collect();
+
+
+ let process_progress = multi_progress.add(ProgressBar::new(files.len() as u64));
+ process_progress.set_style(
+ ProgressStyle::default_bar()
+ .template(
+ "{spinner:.green} {prefix:.bold.dim} [{bar:40.cyan/blue}] {pos}/{len} files {msg}",
+ )
+ .unwrap()
+ .progress_chars("=> "),
+ );
+ process_progress.set_prefix("[Process]");
+ process_progress.set_message("Processing files...");
+
+
+ let progress = Arc::new(Mutex::new(process_progress));
+
+
+ let file_contents: Vec = files
+ .par_iter()
+ .filter_map(|file| {
+ let result = match read_file_content(file, config) {
+ Ok(Some(content)) => Some(content),
+ Ok(none) => none,
+ Err(e) => {
+ warn!("Error reading file {}: {}", file.path.display(), e);
+ None
+ }
+ };
+
+
+ if let Ok(pb) = progress.lock() {
+ pb.inc(1);
+ if let Some(content) = &result {
+ pb.set_message(format!("Processed {}", content.relative_path));
+ }
+ }
+
+ result
+ })
+ .collect();
+
+
+ if let Ok(pb) = progress.lock() {
+ pb.finish_with_message(format!("Processed {} files", file_contents.len()));
+ }
+
+ info!("Processed {} files", file_contents.len());
+
+
+ let security_progress = multi_progress.add(ProgressBar::new_spinner());
+ security_progress.set_style(
+ ProgressStyle::default_spinner()
+ .template("{spinner:.yellow} {prefix:.bold.dim} {msg}")
+ .unwrap(),
+ );
+ security_progress.set_prefix("[Security]");
+
+ let (suspicious_files, security_status) = if !config.security.enable_security_check {
+ security_progress.finish_with_message("Security check disabled");
+ (None, security::SecurityCheckStatus::Disabled)
+ } else {
+ security_progress.set_message("Performing security check...");
+ match security::perform_security_check(path) {
+ Ok(files) => {
+ if !files.is_empty() {
+ security_progress
+ .finish_with_message(format!("Found {} suspicious files", files.len()));
+ info!(
+ "Found {} suspicious files that may contain sensitive information",
+ files.len()
+ );
+ (Some(files), security::SecurityCheckStatus::CompletedWithFindings)
+ } else {
+ security_progress.finish_with_message("No suspicious files found");
+ (None, security::SecurityCheckStatus::CompletedNoFindings)
+ }
+ }
+ Err(e) => {
+ security_progress.finish_with_message(format!("Security check failed: {}", e));
+ warn!("Security check failed: {}", e);
+ (None, security::SecurityCheckStatus::Failed(e.to_string()))
+ }
+ }
+ };
+
+
+ let summary = generate_summary(&file_contents, binary_files.len());
+
+
+ let instruction = match &config.output.instruction_file_path {
+ Some(instruction_file) => {
+ let path = Path::new(instruction_file);
+ if path.exists() {
+ match fs::read_to_string(path) {
+ Ok(content) => {
+ debug!("Read instruction file: {}", instruction_file);
+ Some(content)
+ }
+ Err(e) => {
+ warn!(
+ "Failed to read instruction file {}: {}",
+ instruction_file, e
+ );
+ config.instruction.clone()
+ }
+ }
+ } else {
+ warn!("Instruction file not found: {}", instruction_file);
+ config.instruction.clone()
+ }
+ }
+ _none => config.instruction.clone(),
+ };
+
+ Ok(PackedRepository {
+ files: file_contents,
+ summary,
+ instruction,
+ suspicious_files,
+ security_check_status: security_status,
+ binary_files: Some(binary_files),
+ })
+}
+
+fn read_file_content(file: &FileInfo, config: &Config) -> Result> {
+
+ if file.is_binary {
+ debug!("Skipping binary file: {}", file.path.display());
+ return Ok(None);
+ }
+
+
+ let content = fs::read_to_string(&file.path)
+ .context(format!("Failed to read file: {}", file.path.display()))?;
+
+
+ if config.security.enable_security_check && security::check_sensitive_content(&content) {
+ warn!(
+ "Skipping file with sensitive content: {}",
+ file.path.display()
+ );
+ return Ok(None);
+ }
+
+
+ let extension = file
+ .path
+ .extension()
+ .and_then(|ext| ext.to_str())
+ .unwrap_or("")
+ .to_lowercase();
+
+
+ let processed_content = if config.compress {
+ compress_content(&content, &extension)
+ } else if config.output.remove_comments && comments::is_comment_removal_supported(&extension) {
+ comments::remove_comments(&content, &extension)
+ } else {
+ content
+ };
+
+ Ok(Some(FileContent {
+ relative_path: file.relative_path.to_string_lossy().to_string(),
+ extension,
+ content: processed_content,
+ size: file.size,
+ is_binary: file.is_binary,
+ }))
+}
+
+fn compress_content(content: &str, _extension: &str) -> String {
+
+
+
+
+
+ let lines: Vec<&str> = content.lines().collect();
+
+
+ if lines.len() < 10 {
+ return content.to_string();
+ }
+
+
+ let mut compressed = Vec::new();
+ let mut in_comment_block = false;
+ let mut consecutive_empty_lines = 0;
+
+ for line in lines {
+ let trimmed = line.trim();
+
+
+ if trimmed.starts_with("/*") || trimmed.starts_with("/**") {
+ in_comment_block = true;
+ compressed.push(line);
+ continue;
+ }
+
+ if in_comment_block {
+ if trimmed.ends_with("*/") {
+ in_comment_block = false;
+ compressed.push(line);
+ }
+ continue;
+ }
+
+
+ if trimmed.is_empty() {
+ consecutive_empty_lines += 1;
+ if consecutive_empty_lines <= 1 {
+ compressed.push(line);
+ }
+ continue;
+ }
+ consecutive_empty_lines = 0;
+
+
+ if trimmed.starts_with("//") {
+ continue;
+ }
+
+
+ if trimmed.starts_with("fn ")
+ || trimmed.starts_with("pub fn ")
+ || trimmed.starts_with("class ")
+ || trimmed.starts_with("interface ")
+ || trimmed.starts_with("trait ")
+ || trimmed.starts_with("struct ")
+ || trimmed.starts_with("enum ")
+ || trimmed.starts_with("type ")
+ || trimmed.starts_with("pub struct ")
+ || trimmed.starts_with("pub enum ")
+ || trimmed.starts_with("export ")
+ || trimmed.starts_with("import ")
+ || trimmed.starts_with("use ")
+ || trimmed.starts_with("const ")
+ || trimmed.starts_with("let ")
+ || trimmed.starts_with("var ")
+ || trimmed.starts_with("function ")
+ {
+ compressed.push(line);
+ continue;
+ }
+
+
+ if trimmed == "{" || trimmed == "}" {
+ compressed.push(line);
+ continue;
+ }
+
+
+ if trimmed.contains("impl") || trimmed.contains(" for ") {
+ compressed.push(line);
+ continue;
+ }
+
+
+ if !trimmed.contains("(") && !trimmed.contains(")") {
+ continue;
+ }
+
+ compressed.push(line);
+ }
+
+ compressed.join("\n")
+}
+
+fn generate_summary(files: &[FileContent], binary_file_count: usize) -> RepositorySummary {
+ let file_count = files.len();
+ let total_size: u64 = files.iter().map(|f| f.size).sum();
+
+
+ let mut directories = std::collections::HashSet::new();
+
+ for file in files {
+ let path = PathBuf::from(&file.relative_path);
+ if let Some(parent) = path.parent() {
+ directories.insert(parent.to_path_buf());
+ }
+ }
+
+ let directory_count = directories.len();
+
+
+ let mut extension_counts = std::collections::HashMap::new();
+
+ for file in files {
+ if !file.extension.is_empty() {
+ *extension_counts.entry(file.extension.clone()).or_insert(0) += 1;
+ }
+ }
+
+ let mut extensions: Vec = extension_counts.keys().cloned().collect();
+ extensions.sort();
+
+ RepositorySummary {
+ file_count,
+ total_size,
+ directory_count,
+ extensions,
+ binary_file_count,
+ }
+}
+
+```
+
+### remote.rs
+
+- **Path:** src/remote.rs
+- **Size:** 7.35 KB
+- **Type:** rs
+
+```rs
+use anyhow::{Context, Result};
+use git2::{BranchType, Oid, Repository};
+use indicatif::{ProgressBar, ProgressStyle};
+use log::{debug, info};
+use regex::Regex;
+use std::path::PathBuf;
+use tempfile::TempDir;
+
+
+
+
+
+
+
+
+pub fn clone_repository(url: &str, branch: &str) -> Result {
+ info!("Cloning repository: {}", url);
+
+
+ let progress = ProgressBar::new_spinner();
+ progress.set_style(
+ ProgressStyle::default_spinner()
+ .template("{spinner:.green} {prefix:.bold.dim} {msg}")
+ .unwrap(),
+ );
+ progress.set_prefix("[Clone]");
+ progress.set_message(format!("Parsing URL: {}", url));
+
+
+ let (repo_url, target_ref) = parse_git_url(url, branch)?;
+
+
+ progress.set_message("Creating temporary directory...");
+ let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
+ let temp_path = temp_dir.path().to_path_buf();
+
+ debug!("Cloning to temporary directory: {}", temp_path.display());
+
+
+ progress.set_message(format!("Cloning repository: {}...", repo_url));
+
+
+
+ let clone_ticker = std::thread::spawn(move || {
+ let pb = progress;
+ let messages = [
+ "Downloading objects...",
+ "Resolving deltas...",
+ "Checking out files...",
+ "Indexing repository...",
+ ];
+ let mut idx = 0;
+ loop {
+ pb.set_message(format!("Cloning: {}", messages[idx % messages.len()]));
+ idx += 1;
+ std::thread::sleep(std::time::Duration::from_millis(500));
+ }
+ });
+
+
+ let clone_result = match Repository::clone(&repo_url, &temp_path) {
+ Ok(repo) => {
+
+ if !target_ref.is_empty() && target_ref != "main" && target_ref != "master" {
+ checkout_target(&repo, &target_ref)?;
+ }
+ Ok(())
+ }
+ Err(e) => Err(anyhow::anyhow!(
+ "Failed to clone repository: {}: {}",
+ repo_url,
+ e
+ )),
+ };
+
+
+ clone_ticker.thread().unpark();
+
+
+ match clone_result {
+ Ok(_) => {
+
+
+ std::mem::forget(temp_dir);
+
+ info!("Repository cloned successfully to {}", temp_path.display());
+ Ok(temp_path)
+ }
+ Err(e) => {
+
+ Err(e)
+ }
+ }
+}
+
+
+fn checkout_target(repo: &Repository, target_ref: &str) -> Result<()> {
+
+ if let Ok(oid) = Oid::from_str(target_ref) {
+ debug!("Checking out commit: {}", target_ref);
+ let commit = repo
+ .find_commit(oid)
+ .context(format!("Failed to find commit: {}", target_ref))?;
+
+
+ repo.set_head_detached(commit.id())
+ .context(format!("Failed to set HEAD to commit: {}", target_ref))?;
+
+
+ let obj = repo
+ .find_object(commit.id(), None)
+ .context(format!("Failed to find object for commit: {}", target_ref))?;
+
+ repo.reset(&obj, git2::ResetType::Hard, None)
+ .context(format!("Failed to reset to commit: {}", target_ref))?;
+ } else {
+
+ debug!("Checking out branch: {}", target_ref);
+
+
+ if let Ok(branch) = repo.find_branch(target_ref, BranchType::Local) {
+ repo.set_head(branch.get().name().unwrap_or(""))
+ .context(format!("Failed to set HEAD to branch: {}", target_ref))?;
+ } else {
+
+ let remote_branch_name = format!("origin/{}", target_ref);
+ if let Ok(branch) = repo.find_branch(&remote_branch_name, BranchType::Remote) {
+
+ let _branch_ref = branch.get().name().unwrap_or("");
+ repo.branch(
+ target_ref,
+ &repo.find_commit(branch.get().peel_to_commit()?.id())?,
+ false,
+ )
+ .context(format!(
+ "Failed to create local branch from remote: {}",
+ target_ref
+ ))?;
+
+
+ repo.set_head(&format!("refs/heads/{}", target_ref))
+ .context(format!("Failed to set HEAD to branch: {}", target_ref))?;
+ } else {
+
+ return Err(anyhow::anyhow!(
+ "Could not find branch {} in repository",
+ target_ref
+ ));
+ }
+
+
+ let obj = repo
+ .revparse_single(&format!("refs/heads/{}", target_ref))
+ .context(format!(
+ "Failed to find reference for branch: {}",
+ target_ref
+ ))?;
+
+ repo.reset(&obj, git2::ResetType::Hard, None)
+ .context(format!("Failed to reset to branch: {}", target_ref))?;
+ }
+ }
+
+ Ok(())
+}
+
+
+fn parse_git_url(url: &str, branch: &str) -> Result<(String, String)> {
+
+ if !url.contains("://") && url.contains('/') {
+ let repo_url = format!("https://github.com/{}", url);
+ return Ok((repo_url, branch.to_string()));
+ }
+
+
+ let branch_regex = Regex::new(r"(?:https?://[^/]+/[^/]+/[^/]+)/tree/([^/]+)").unwrap();
+ if let Some(captures) = branch_regex.captures(url) {
+ let base_url = branch_regex.replace(url, "$1").to_string();
+ let branch_name = captures.get(1).unwrap().as_str();
+ return Ok((base_url, branch_name.to_string()));
+ }
+
+
+ let commit_regex = Regex::new(r"(?:https?://[^/]+/[^/]+/[^/]+)/commit/([^/]+)").unwrap();
+ if let Some(captures) = commit_regex.captures(url) {
+ let base_url = commit_regex.replace(url, "$1").to_string();
+ let commit_hash = captures.get(1).unwrap().as_str();
+ return Ok((base_url, commit_hash.to_string()));
+ }
+
+
+ Ok((url.to_string(), branch.to_string()))
+}
+
+```
+
+### scanner.rs
+
+- **Path:** src/scanner.rs
+- **Size:** 11.08 KB
+- **Type:** rs
+
+```rs
+use crate::config::Config;
+use anyhow::{Context, Result};
+use glob::{glob_with, MatchOptions};
+use ignore::{
+ gitignore::{Gitignore, GitignoreBuilder},
+ WalkBuilder,
+};
+use indicatif::{ProgressBar, ProgressStyle};
+use log::{debug, info, warn};
+use rayon::prelude::*;
+use std::collections::HashSet;
+use std::fs;
+use std::path::{Path, PathBuf};
+use tree_magic_mini as tree_magic;
+
+#[derive(Debug, Clone)]
+pub struct FileInfo {
+ pub path: PathBuf,
+ pub relative_path: PathBuf,
+ pub size: u64,
+ pub mime_type: String,
+ pub is_binary: bool,
+}
+
+impl FileInfo {
+ pub fn new(path: PathBuf, base_path: &Path) -> Result {
+ let metadata = fs::metadata(&path)
+ .context(format!("Failed to get metadata for {}", path.display()))?;
+
+ let size = metadata.len();
+
+
+ let mime_type = tree_magic::from_filepath(&path).unwrap_or("application/octet-stream");
+
+
+ let is_binary = mime_type.starts_with("application/")
+ && !mime_type.contains("json")
+ && !mime_type.contains("xml")
+ && !mime_type.contains("javascript")
+ && !mime_type.contains("typescript")
+ || mime_type.starts_with("image/")
+ || mime_type.starts_with("audio/")
+ || mime_type.starts_with("video/");
+
+ let relative_path = path.strip_prefix(base_path).unwrap_or(&path).to_path_buf();
+
+ Ok(Self {
+ path,
+ relative_path,
+ size,
+ mime_type: mime_type.to_string(),
+ is_binary,
+ })
+ }
+}
+
+
+fn read_mixignore(base_path: &Path) -> Option {
+ let mixignore_path = base_path.join(".remixignore");
+ if mixignore_path.exists() {
+ debug!("Found .remixignore file");
+ let mut builder = GitignoreBuilder::new(base_path);
+ if builder
+ .add_line(
+ None,
+ &fs::read_to_string(mixignore_path).unwrap_or_default(),
+ )
+ .is_ok()
+ {
+ return builder.build().ok();
+ }
+ }
+ None
+}
+
+
+fn get_default_ignore_patterns() -> Vec<&'static str> {
+ vec![
+ "node_modules/",
+ ".git/",
+ ".gitignore",
+ ".gitattributes",
+ ".github/",
+ ".gitmodules",
+ ".gitkeep",
+ "target/",
+ "dist/",
+ "build/",
+ "**/*.log",
+ "**/Cargo.lock",
+ "**/.env",
+ "**/*.exe",
+ "**/*.o",
+ "**/*.so",
+ "**/*.dll",
+ "**/*.dylib",
+ "**/*.zip",
+ "**/*.tar",
+ "**/*.gz",
+ "**/*.rar",
+ "**/*.7z",
+ "**/*.jar",
+ "**/*.class",
+ "**/*.pyc",
+ "**/__pycache__/",
+ "**/.idea/",
+ "**/.vscode/",
+ "**/node_modules/",
+ "**/vendor/",
+ "**/bin/",
+ "**/obj/",
+ "**/build/",
+ ]
+}
+
+/// Normalize a path string for glob matching
+fn normalize_path(path: &str) -> String {
+ // Convert Windows backslashes to forward slashes for consistent glob matching
+ path.replace('\\', "/")
+}
+
+/// Check if a path should be ignored based on common patterns
+/// Used both by scanner and security check
+pub fn should_ignore_common(path: &Path) -> bool {
+ let path_str = normalize_path(&path.to_string_lossy());
+
+ // Common directories that should always be ignored
+ if path_str.contains("/target/")
+ || path_str.starts_with("target/")
+ || path_str.contains("/.git/")
+ || path_str.starts_with(".git/")
+ || path_str.contains("/node_modules/")
+ || path_str.starts_with("node_modules/")
+ || path_str.contains("/dist/")
+ || path_str.starts_with("dist/")
+ || path_str.contains("/build/")
+ || path_str.starts_with("build/")
+ {
+ return true;
+ }
+
+ // Common files that should always be ignored
+ if path_str.ends_with(".exe")
+ || path_str.ends_with(".o")
+ || path_str.ends_with(".obj")
+ || path_str.ends_with(".dll")
+ || path_str.ends_with(".so")
+ || path_str.ends_with(".dylib")
+ || path_str.ends_with(".class")
+ || path_str.ends_with(".jar")
+ || path_str.ends_with(".war")
+ || path_str.ends_with(".zip")
+ || path_str.ends_with(".tar")
+ || path_str.ends_with(".gz")
+ || path_str.ends_with(".rar")
+ || path_str.ends_with(".7z")
+ || path_str.ends_with(".pyc")
+ {
+ return true;
+ }
+
+ false
+}
+
+pub fn scan_repository(base_path: &Path, config: &Config) -> Result> {
+ info!("Scanning repository at {}", base_path.display());
+
+ // Create a progress bar for scanning
+ let spinner = ProgressBar::new_spinner();
+ spinner.set_style(
+ ProgressStyle::default_spinner()
+ .template("{spinner:.green} {msg}")
+ .unwrap(),
+ );
+ spinner.set_message("Scanning repository files...");
+
+ // Initialize the ignore system with the appropriate layers
+ let should_ignore = |path: &Path| -> bool {
+ // First check common patterns that should always be ignored
+ if should_ignore_common(path) {
+ return true;
+ }
+
+ let relative_path = path.strip_prefix(base_path).unwrap_or(path);
+ let path_str = normalize_path(&relative_path.to_string_lossy());
+
+ // Layer 1 (highest priority): Custom ignore patterns
+ if !config.ignore.custom_patterns.is_empty() {
+ for pattern in &config.ignore.custom_patterns {
+ // Normalize the pattern too
+ let pattern = normalize_path(pattern);
+
+ if glob_match::glob_match(&pattern, &path_str) {
+ debug!(
+ "Ignoring '{}' due to custom pattern '{}'",
+ path_str, pattern
+ );
+ return true;
+ }
+ }
+ }
+
+ // Layer 2: .remixignore file
+ if config.ignore.use_mixignore {
+ if let Some(ref mixignore) = read_mixignore(base_path) {
+ let result = mixignore.matched_path_or_any_parents(relative_path, false);
+ if result.is_ignore() {
+ debug!("Ignoring '{}' due to .remixignore", path_str);
+ return true;
+ }
+ }
+ }
+
+ // Layer 3: Default ignore patterns if enabled
+ if config.ignore.use_default_patterns {
+ for pattern in get_default_ignore_patterns() {
+ if glob_match::glob_match(pattern, &path_str) {
+ debug!(
+ "Ignoring '{}' due to default pattern '{}'",
+ path_str, pattern
+ );
+ return true;
+ }
+ }
+ }
+
+ false
+ };
+
+ // Use the ignore crate to build a walker that respects .gitignore if enabled
+ let mut walker = WalkBuilder::new(base_path);
+ walker.hidden(false); // Include hidden files/directories
+
+ // Add standard directories to always ignore
+ walker.filter_entry(|entry| {
+ let path = entry.path();
+
+ // Skip target/ and .git/ directories completely
+ if (path.ends_with("target") || path.ends_with(".git"))
+ && entry.file_type().is_some_and(|ft| ft.is_dir())
+ {
+ debug!("Ignoring directory: {}", path.display());
+ return false;
+ }
+
+ true
+ });
+
+ if config.ignore.use_gitignore {
+ walker.git_ignore(true);
+ walker.git_global(true);
+ walker.git_exclude(true);
+ } else {
+ walker.git_ignore(false);
+ walker.git_global(false);
+ walker.git_exclude(false);
+ }
+
+ // Filter files using our multi-layered ignore system
+ spinner.set_message("Collecting files...");
+ let mut files: Vec = walker
+ .build()
+ .filter_map(|entry| {
+ spinner.tick();
+ entry.ok()
+ })
+ .filter(|entry| {
+ let path = entry.path();
+
+ // Skip if not a regular file
+ if !entry.file_type().is_some_and(|ft| ft.is_file()) {
+ return false;
+ }
+
+ // Use our custom ignore function
+ !should_ignore(path)
+ })
+ .map(|entry| entry.into_path())
+ .collect();
+
+ // Apply include patterns if specified
+ if !config.include.is_empty() {
+ spinner.set_message(format!("Applying include patterns: {:?}", config.include));
+
+ let mut included_files = HashSet::new();
+ let options = MatchOptions {
+ case_sensitive: false,
+ require_literal_separator: false,
+ require_literal_leading_dot: false,
+ };
+
+ for pattern in &config.include {
+ // Normalize pattern for consistent matching
+ let normalized_pattern = normalize_path(pattern);
+ let full_pattern = base_path.join(&normalized_pattern);
+ let pattern_str = full_pattern.to_string_lossy().to_string();
+
+ match glob_with(&pattern_str, options) {
+ Ok(entries) => {
+ for entry in entries.filter_map(Result::ok) {
+ included_files.insert(entry);
+ }
+ }
+ Err(e) => {
+ warn!("Invalid include pattern '{}': {}", pattern, e);
+ }
+ }
+ }
+
+ // Only keep files that match the include patterns
+ if !included_files.is_empty() {
+ files.retain(|path| included_files.contains(path));
+ }
+ }
+
+ spinner.set_message("Processing file information...");
+
+ // Process file information in parallel
+ let file_infos: Vec = files
+ .par_iter()
+ .filter_map(|path| {
+ match FileInfo::new(path.clone(), base_path) {
+ Ok(info) => {
+ // Filter out files larger than the max size
+ if info.size > config.max_file_size {
+ debug!(
+ "Skipping large file: {} ({} bytes)",
+ info.path.display(),
+ info.size
+ );
+ None
+ } else if info.is_binary
+ && !config
+ .include
+ .iter()
+ .any(|p| p.contains("*.bin") || p.contains("binary"))
+ {
+ debug!(
+ "Skipping binary file: {} ({})",
+ info.path.display(),
+ info.mime_type
+ );
+ None
+ } else {
+ Some(info)
+ }
+ }
+ Err(e) => {
+ warn!("Error processing file {}: {}", path.display(), e);
+ None
+ }
+ }
+ })
+ .collect();
+
+ spinner.finish_with_message(format!("Found {} files to process", file_infos.len()));
+ info!("Found {} files to process", file_infos.len());
+
+ Ok(file_infos)
+}
+
+```
+
+### utils.rs
+
+- **Path:** src/utils.rs
+- **Size:** 1019 bytes
+- **Type:** rs
+
+```rs
+
+pub fn format_size(size: u64) -> String {
+ const KB: u64 = 1024;
+ const MB: u64 = KB * 1024;
+ const GB: u64 = MB * 1024;
+
+ if size >= GB {
+ format!("{:.2} GB", size as f64 / GB as f64)
+ } else if size >= MB {
+ format!("{:.2} MB", size as f64 / MB as f64)
+ } else if size >= KB {
+ format!("{:.2} KB", size as f64 / KB as f64)
+ } else {
+ format!("{} bytes", size)
+ }
+}
+
+
+pub fn open_file(path: &str) -> anyhow::Result<()> {
+ #[cfg(target_os = "windows")]
+ {
+ use std::process::Command;
+ Command::new("cmd")
+ .args(["/C", "start", "", path])
+ .spawn()?;
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ use std::process::Command;
+ Command::new("open").arg(path).spawn()?;
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ use std::process::Command;
+ Command::new("xdg-open").arg(path).spawn()?;
+ }
+
+ Ok(())
+}
+
+```
+
+## tests/
+
+### cli_tests.rs
+
+- **Path:** tests/cli_tests.rs
+- **Size:** 1.40 KB
+- **Type:** rs
+
+```rs
+use remix::cli::Cli;
+use clap::Parser;
+
+#[test]
+fn test_cli_parsing() {
+
+ let cli = Cli::parse_from(["remix", "path/to/repo"]);
+ assert_eq!(cli.path, Some("path/to/repo".to_string()));
+ assert!(cli.config.is_none());
+ assert!(!cli.init);
+ assert!(cli.include.is_none());
+ assert!(cli.ignore.is_none());
+ assert!(!cli.compress);
+ assert!(!cli.skip_sensitive_check);
+ assert!(!cli.remove_comments);
+}
+
+#[test]
+fn test_cli_include_ignore_patterns() {
+
+ let cli = Cli::parse_from([
+ "remix",
+ "--include",
+ "*.rs,*.md",
+ "--ignore",
+ "target/,node_modules/",
+ ]);
+
+ let include_patterns = cli.include_patterns().unwrap();
+ let ignore_patterns = cli.ignore_patterns().unwrap();
+
+ assert_eq!(include_patterns.len(), 2);
+ assert_eq!(include_patterns[0], "*.rs");
+ assert_eq!(include_patterns[1], "*.md");
+
+ assert_eq!(ignore_patterns.len(), 2);
+ assert_eq!(ignore_patterns[0], "target/");
+ assert_eq!(ignore_patterns[1], "node_modules/");
+}
+
+#[test]
+fn test_cli_remote_repo() {
+
+ let cli = Cli::parse_from([
+ "remix",
+ "--remote",
+ "username/repo",
+ "--remote-branch",
+ "main",
+ ]);
+
+ assert_eq!(cli.remote, Some("username/repo".to_string()));
+ assert_eq!(cli.remote_branch, Some("main".to_string()));
+}
+
+```
+
+### config_tests.rs
+
+- **Path:** tests/config_tests.rs
+- **Size:** 804 bytes
+- **Type:** rs
+
+```rs
+use remix::config::{Config, OutputConfig};
+
+#[test]
+fn test_default_config() {
+ let config = Config::default();
+
+
+ assert!(config.include.is_empty());
+ assert_eq!(config.max_file_size, 100_000);
+ assert!(!config.compress);
+ assert!(config.security.enable_security_check);
+ assert_eq!(config.output.format, "md");
+ assert!(!config.output.open_file);
+ assert_eq!(config.output.path, "./remix-output.md");
+}
+
+#[test]
+fn test_output_config_default() {
+ let output_config = OutputConfig::default();
+
+ assert_eq!(output_config.format, "md");
+ assert!(!output_config.open_file);
+ assert_eq!(output_config.path, "./remix-output.md");
+ assert!(output_config.instruction_file_path.is_none());
+ assert!(!output_config.remove_comments);
+}
+
+```
+
+### packer_tests.rs
+
+- **Path:** tests/packer_tests.rs
+- **Size:** 2.01 KB
+- **Type:** rs
+
+```rs
+use remix::config::Config;
+use remix::packer::pack_repository;
+
+
+mod common;
+
+#[tokio::test]
+async fn test_pack_repository_basic() {
+
+ let test_dir = common::create_test_repo();
+
+
+ let config = Config::default();
+
+
+ let result = pack_repository(test_dir.path(), &config)
+ .await
+ .expect("Failed to pack repository");
+
+
+ assert!(!result.files.is_empty(), "No files were packed");
+ assert!(result.summary.file_count > 0, "File count should be > 0");
+ assert!(result.summary.total_size > 0, "Total size should be > 0");
+
+
+ println!("File count: {}", result.summary.file_count);
+ println!("Directory count: {}", result.summary.directory_count);
+ println!(
+ "Files found: {:?}",
+ result
+ .files
+ .iter()
+ .map(|f| &f.relative_path)
+ .collect::>()
+ );
+
+
+ let file_paths: Vec = result
+ .files
+ .iter()
+ .map(|f| f.relative_path.replace('\\', "/"))
+ .collect();
+
+
+ let has_readme = file_paths.iter().any(|path| path.contains("README.md"));
+ let has_src_file = file_paths.iter().any(|path| path.contains("src/"));
+
+ assert!(
+ has_readme,
+ "README.md should be included in the packed files"
+ );
+ assert!(
+ has_src_file,
+ "At least one file from src/ should be included"
+ );
+
+
+ let has_node_modules = file_paths.iter().any(|path| path.contains("node_modules"));
+ assert!(!has_node_modules, "node_modules should be ignored");
+
+
+ if let Some(suspicious) = &result.suspicious_files {
+ println!("Suspicious files: {:?}", suspicious);
+ }
+}
+
+```
+
+### utils_tests.rs
+
+- **Path:** tests/utils_tests.rs
+- **Size:** 1.00 KB
+- **Type:** rs
+
+```rs
+use remix::utils;
+
+#[test]
+fn test_format_size_bytes() {
+ assert_eq!(utils::format_size(0), "0 bytes");
+ assert_eq!(utils::format_size(1), "1 bytes");
+ assert_eq!(utils::format_size(512), "512 bytes");
+ assert_eq!(utils::format_size(1023), "1023 bytes");
+}
+
+#[test]
+fn test_format_size_kilobytes() {
+ assert_eq!(utils::format_size(1024), "1.00 KB");
+ assert_eq!(utils::format_size(1536), "1.50 KB");
+ assert_eq!(utils::format_size(2048), "2.00 KB");
+ assert_eq!(utils::format_size(1024 * 1024 - 1), "1024.00 KB");
+}
+
+#[test]
+fn test_format_size_megabytes() {
+ assert_eq!(utils::format_size(1024 * 1024), "1.00 MB");
+ assert_eq!(utils::format_size(1536 * 1024), "1.50 MB");
+ assert_eq!(utils::format_size(1024 * 1024 * 1024 - 1), "1024.00 MB");
+}
+
+#[test]
+fn test_format_size_gigabytes() {
+ assert_eq!(utils::format_size(1024 * 1024 * 1024), "1.00 GB");
+ assert_eq!(utils::format_size(1536 * 1024 * 1024), "1.50 GB");
+ assert_eq!(utils::format_size(2048 * 1024 * 1024), "2.00 GB");
+}
+
+```
+
+### formatter_tests.rs
+
+- **Path:** tests/formatter_tests.rs
+- **Size:** 1.96 KB
+- **Type:** rs
+
+```rs
+use anyhow::Result;
+use remix::formatter::format_toon;
+use remix::packer::{PackedRepository, RepositorySummary, FileContent};
+
+#[test]
+fn test_format_toon_basic() -> Result<()> {
+ let repo = PackedRepository {
+ summary: RepositorySummary {
+ file_count: 2,
+ directory_count: 1,
+ total_size: 100,
+ extensions: vec!["txt".to_string(), "rs".to_string()],
+ binary_file_count: 0,
+ },
+ files: vec![
+ FileContent {
+ relative_path: "file1.txt".to_string(),
+ content: "Hello".to_string(),
+ size: 5,
+ extension: "txt".to_string(),
+ is_binary: false,
+ },
+ FileContent {
+ relative_path: "file2.rs".to_string(),
+ content: "fn main() {}".to_string(),
+ size: 11,
+ extension: "rs".to_string(),
+ is_binary: false,
+ },
+ ],
+ instruction: Some("Test instruction".to_string()),
+ suspicious_files: None,
+ security_check_status: remix::security::SecurityCheckStatus::CompletedNoFindings,
+ binary_files: None,
+ };
+
+ let result = format_toon(&repo)?;
+ assert!(!result.is_empty());
+ assert!(result.contains("files[2]"));
+ assert!(result.contains("file_count: 2"));
+ Ok(())
+}
+
+#[test]
+fn test_format_toon_empty_repo() -> Result<()> {
+ let repo = PackedRepository {
+ summary: RepositorySummary {
+ file_count: 0,
+ directory_count: 0,
+ total_size: 0,
+ extensions: vec![],
+ binary_file_count: 0,
+ },
+ files: vec![],
+ instruction: None,
+ suspicious_files: None,
+ security_check_status: remix::security::SecurityCheckStatus::CompletedNoFindings,
+ binary_files: None,
+ };
+
+ let result = format_toon(&repo)?;
+ assert!(!result.is_empty());
+ assert!(result.contains("file_count: 0"));
+ Ok(())
+}
+```
+
diff --git a/src/cli.rs b/src/cli.rs
index 8d9fb6a..d07423c 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -37,8 +37,8 @@ pub struct Cli {
#[arg(short, long)]
pub output: Option,
- /// Output format (md, json, txt)
- #[arg(long, value_parser = ["md", "markdown", "json", "txt", "text"])]
+ /// Output format (md, json, txt, toon)
+ #[arg(long, value_parser = ["md", "markdown", "json", "txt", "text", "toon"])]
pub format: Option,
/// Compress the code output
diff --git a/src/config.rs b/src/config.rs
index d2e2740..25ed8e1 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -11,7 +11,7 @@ const DEFAULT_MAX_FILE_SIZE: u64 = 100_000; // 100KB
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OutputConfig {
- /// Output format (md, json, txt)
+ /// Output format (md, json, txt, toon)
#[serde(default = "default_format")]
pub format: String,
diff --git a/src/formatter.rs b/src/formatter.rs
index 98aec0a..7dfae6f 100644
--- a/src/formatter.rs
+++ b/src/formatter.rs
@@ -18,6 +18,7 @@ pub fn output_result(repo: &PackedRepository, config: &OutputConfig) -> Result<(
"md" | "markdown" => format_markdown(repo),
"json" => format_json(repo)?,
"txt" | "text" => format_text(repo),
+ "toon" => format_toon(repo)?,
_ => {
warn!("Unknown format '{}', defaulting to markdown", format);
format_markdown(repo)
@@ -42,7 +43,7 @@ pub fn output_result(repo: &PackedRepository, config: &OutputConfig) -> Result<(
Ok(())
}
-fn format_markdown(repo: &PackedRepository) -> String {
+pub fn format_markdown(repo: &PackedRepository) -> String {
let mut output = String::new();
// Add user instruction if provided
@@ -176,11 +177,16 @@ fn format_markdown(repo: &PackedRepository) -> String {
output
}
-fn format_json(repo: &PackedRepository) -> Result {
+pub fn format_json(repo: &PackedRepository) -> Result {
serde_json::to_string_pretty(repo).context("Failed to serialize repository to JSON")
}
-fn format_text(repo: &PackedRepository) -> String {
+pub fn format_toon(repo: &PackedRepository) -> Result {
+ let value = serde_json::to_value(repo).context("Failed to convert repository to JSON value")?;
+ rtoon::encode_default(&value).context("Failed to encode repository to TOON")
+}
+
+pub fn format_text(repo: &PackedRepository) -> String {
let mut output = String::new();
// Add user instruction if provided
diff --git a/src/main.rs b/src/main.rs
index c407780..9019f17 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -101,10 +101,11 @@ async fn main() -> Result<()> {
.context("Failed to clone remote repository")?;
main_spinner.set_message("Processing repository...");
- let result = packer::pack_repository(&temp_dir, &config.merge_with_cli(&cli)).await?;
+ let merged_config = config.merge_with_cli(&cli);
+ let result = packer::pack_repository(&temp_dir, &merged_config).await?;
main_spinner.set_message("Formatting output...");
- formatter::output_result(&result, &config.output)?;
+ formatter::output_result(&result, &merged_config.output)?;
} else {
// Process local repository
main_spinner.set_message(format!(
@@ -114,10 +115,11 @@ async fn main() -> Result<()> {
info!("Processing local repository: {}", target_path.display());
main_spinner.set_message("Processing repository...");
- let result = packer::pack_repository(&target_path, &config.merge_with_cli(&cli)).await?;
+ let merged_config = config.merge_with_cli(&cli);
+ let result = packer::pack_repository(&target_path, &merged_config).await?;
main_spinner.set_message("Formatting output...");
- formatter::output_result(&result, &config.output)?;
+ formatter::output_result(&result, &merged_config.output)?;
}
main_spinner.finish_with_message(format!(
diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs
index cc27e57..d16be42 100644
--- a/tests/cli_tests.rs
+++ b/tests/cli_tests.rs
@@ -52,3 +52,96 @@ fn test_cli_remote_repo() {
assert_eq!(cli.remote, Some("username/repo".to_string()));
assert_eq!(cli.remote_branch, Some("main".to_string()));
}
+
+#[test]
+fn test_cli_format_validation() {
+ // Test valid formats
+ for format in ["md", "markdown", "json", "txt", "text", "toon"] {
+ let cli = Cli::parse_from(["remix", "--format", format]);
+ assert_eq!(cli.format, Some(format.to_string()));
+ }
+}
+
+
+
+#[test]
+fn test_cli_edge_cases() {
+ // Test empty path
+ let cli = Cli::parse_from(["remix", ""]);
+ assert_eq!(cli.path, Some("".to_string()));
+
+ // Test path with special characters
+ let cli = Cli::parse_from(["remix", "/path/with spaces/and@symbols#"]);
+ assert_eq!(cli.path, Some("/path/with spaces/and@symbols#".to_string()));
+
+ // Test very long path
+ let long_path = "a".repeat(1000);
+ let cli = Cli::parse_from(["remix", &long_path]);
+ assert_eq!(cli.path, Some(long_path));
+}
+
+#[test]
+fn test_cli_all_options() {
+ // Test all options together
+ let cli = Cli::parse_from([
+ "remix",
+ "/test/path",
+ "--config", "config.json",
+ "--init",
+ "--include", "*.rs,*.toml",
+ "--ignore", "target/**,*.log",
+ "--max-file-size", "50000",
+ "--output", "output.md",
+ "--format", "json",
+ "--compress",
+ "--skip-sensitive-check",
+ "--remote", "https://github.com/user/repo",
+ "--remote-branch", "develop",
+ "--open",
+ "--instruction", "Test instruction",
+ "--instruction-file", "instructions.txt",
+ "--remove-comments",
+ "--no-gitignore",
+ "--no-default-patterns",
+ ]);
+
+ assert_eq!(cli.path, Some("/test/path".to_string()));
+ assert_eq!(cli.config, Some("config.json".into()));
+ assert!(cli.init);
+ assert_eq!(cli.include, Some("*.rs,*.toml".to_string()));
+ assert_eq!(cli.ignore, Some("target/**,*.log".to_string()));
+ assert_eq!(cli.max_file_size, Some(50000));
+ assert_eq!(cli.output, Some("output.md".into()));
+ assert_eq!(cli.format, Some("json".to_string()));
+ assert!(cli.compress);
+ assert!(cli.skip_sensitive_check);
+ assert_eq!(cli.remote, Some("https://github.com/user/repo".to_string()));
+ assert_eq!(cli.remote_branch, Some("develop".to_string()));
+ assert!(cli.open);
+ assert_eq!(cli.instruction, Some("Test instruction".to_string()));
+ assert_eq!(cli.instruction_file, Some("instructions.txt".into()));
+ assert!(cli.remove_comments);
+ assert!(cli.no_gitignore);
+ assert!(cli.no_default_patterns);
+}
+
+#[test]
+fn test_cli_pattern_parsing_edge_cases() {
+ // Test empty patterns
+ let cli = Cli::parse_from(["remix", "--include", "", "--ignore", ""]);
+ assert!(cli.include_patterns().unwrap().is_empty());
+ assert!(cli.ignore_patterns().unwrap().is_empty());
+
+ // Test patterns with spaces
+ let cli = Cli::parse_from(["remix", "--include", " *.rs , *.md "]);
+ let patterns = cli.include_patterns().unwrap();
+ assert_eq!(patterns.len(), 2);
+ assert_eq!(patterns[0], "*.rs");
+ assert_eq!(patterns[1], "*.md");
+
+ // Test single pattern
+ let cli = Cli::parse_from(["remix", "--include", "*.rs"]);
+ let patterns = cli.include_patterns().unwrap();
+ assert_eq!(patterns.len(), 1);
+ assert_eq!(patterns[0], "*.rs");
+}
diff --git a/tests/comments_tests.rs b/tests/comments_tests.rs
new file mode 100644
index 0000000..912889f
--- /dev/null
+++ b/tests/comments_tests.rs
@@ -0,0 +1,112 @@
+use remix::comments::remove_comments;
+
+#[test]
+fn test_remove_comments_rust() {
+ let code = r#"
+// This is a comment
+fn main() {
+ println!("Hello"); // inline comment
+ /* block comment */
+}
+"#;
+
+ let expected = "\n\nfn main() {\n println!(\"Hello\"); \n \n}\n";
+
+ assert_eq!(remove_comments(code, "rs"), expected);
+}
+
+#[test]
+fn test_remove_comments_python() {
+ let code = r#"
+# This is a comment
+def main():
+ print("Hello") # inline comment
+ """
+ Multi-line string (not comment)
+ """
+ pass
+"#;
+
+ let expected = "\n\ndef main():\n print(\"Hello\") \n \"\"\"\n Multi-line string (not comment)\n \"\"\"\n pass\n";
+
+ assert_eq!(remove_comments(code, "py"), expected);
+}
+
+#[test]
+fn test_remove_comments_javascript() {
+ let code = r#"
+// Single line comment
+function test() {
+ console.log("Hello"); // inline
+ /* block comment */
+}
+/* another block */
+"#;
+
+ let expected = "\n\nfunction test() {\n console.log(\"Hello\"); \n \n}\n\n";
+
+ assert_eq!(remove_comments(code, "js"), expected);
+}
+
+#[test]
+fn test_remove_comments_c_style() {
+ let code = r#"
+// Comment
+int main() {
+ printf("Hello"); // inline
+ /* block */
+}
+/* end */
+"#;
+
+ let expected = "\n\nint main() {\n printf(\"Hello\"); \n \n}\n\n";
+
+ assert_eq!(remove_comments(code, "c"), expected);
+}
+
+#[test]
+fn test_remove_comments_html() {
+ let html = r#"
+
+Hello
+
+"#;
+
+ let expected = "\n\nHello
\n\n";
+
+ assert_eq!(remove_comments(html, "html"), expected);
+}
+
+#[test]
+fn test_remove_comments_css() {
+ let css = r#"
+/* comment */
+.class {
+ color: red; /* inline */
+}
+/* another */
+"#;
+
+ let expected = "\n\n.class {\n color: red; \n}\n\n";
+
+ assert_eq!(remove_comments(css, "css"), expected);
+}
+
+#[test]
+fn test_remove_comments_unsupported() {
+ let code = "// comment\ncode";
+ assert_eq!(remove_comments(code, "txt"), "// comment\ncode");
+}
+
+#[test]
+fn test_remove_comments_edge_cases() {
+ // Empty input
+ assert_eq!(remove_comments("", "rs"), "");
+
+ // No comments
+ let code = "fn main() {}\n";
+ assert_eq!(remove_comments(code, "rs"), code);
+
+ // Only comments
+ assert_eq!(remove_comments("// comment\n/* block */", "rs"), "\n");
+}
\ No newline at end of file
diff --git a/tests/config_tests.rs b/tests/config_tests.rs
index 83835aa..12bb189 100644
--- a/tests/config_tests.rs
+++ b/tests/config_tests.rs
@@ -1,4 +1,8 @@
-use remix::config::{Config, OutputConfig};
+use remix::config::{Config, OutputConfig, load_config};
+use remix::cli::Cli;
+use clap::Parser;
+use std::fs;
+use tempfile::TempDir;
#[test]
fn test_default_config() {
@@ -24,3 +28,109 @@ fn test_output_config_default() {
assert!(output_config.instruction_file_path.is_none());
assert!(!output_config.remove_comments);
}
+
+#[test]
+fn test_load_config_valid() {
+ let temp_dir = TempDir::new().unwrap();
+ let config_path = temp_dir.path().join("test_config.json");
+
+ let config_content = r#"
+ {
+ "include": ["*.rs", "*.toml"],
+ "ignore": {
+ "custom_patterns": ["target/**"]
+ },
+ "max_file_size": 50000,
+ "compress": true,
+ "security": {
+ "enable_security_check": false
+ },
+ "output": {
+ "format": "json",
+ "open_file": true,
+ "path": "custom_output.json",
+ "remove_comments": true
+ }
+ }
+ "#;
+
+ fs::write(&config_path, config_content).unwrap();
+
+ let config = load_config(&config_path).unwrap();
+
+ assert_eq!(config.include, vec!["*.rs".to_string(), "*.toml".to_string()]);
+ assert_eq!(config.ignore.custom_patterns, vec!["target/**".to_string()]);
+ assert_eq!(config.max_file_size, 50000);
+ assert!(config.compress);
+ assert!(!config.security.enable_security_check);
+ assert_eq!(config.output.format, "json");
+ assert!(config.output.open_file);
+ assert_eq!(config.output.path, "custom_output.json");
+ assert!(config.output.remove_comments);
+}
+
+#[test]
+fn test_load_config_invalid_json() {
+ let temp_dir = TempDir::new().unwrap();
+ let config_path = temp_dir.path().join("invalid_config.json");
+
+ fs::write(&config_path, "{ invalid json }").unwrap();
+
+ let result = load_config(&config_path);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_load_config_missing_file() {
+ let temp_dir = TempDir::new().unwrap();
+ let config_path = temp_dir.path().join("missing.json");
+
+ let result = load_config(&config_path);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_config_merge_with_cli() {
+ let config = Config::default();
+ let cli = Cli::parse_from([
+ "remix",
+ "--include", "*.rs",
+ "--format", "toon",
+ "--output", "output.toon",
+ "--compress",
+ "--remove-comments"
+ ]);
+
+ let merged = config.merge_with_cli(&cli);
+
+ assert_eq!(merged.include, vec!["*.rs".to_string()]);
+ assert_eq!(merged.output.format, "toon");
+ assert_eq!(merged.output.path, "output.toon");
+ assert!(merged.compress);
+ assert!(merged.output.remove_comments);
+}
+
+#[test]
+fn test_config_merge_overrides() {
+ let temp_dir = TempDir::new().unwrap();
+ let config_path = temp_dir.path().join("config.json");
+
+ let config_content = r#"
+ {
+ "output": {
+ "format": "md",
+ "path": "default.md"
+ }
+ }
+ "#;
+
+ fs::write(&config_path, config_content).unwrap();
+
+ let config = load_config(&config_path).unwrap();
+ let cli = Cli::parse_from(["remix", "--format", "json", "--output", "cli.json"]);
+
+ let merged = config.merge_with_cli(&cli);
+
+ assert_eq!(merged.output.format, "json");
+ assert_eq!(merged.output.path, "cli.json");
+}
diff --git a/tests/formatter_tests.rs b/tests/formatter_tests.rs
new file mode 100644
index 0000000..c4725d5
--- /dev/null
+++ b/tests/formatter_tests.rs
@@ -0,0 +1,169 @@
+use anyhow::Result;
+use remix::formatter::{format_toon, format_markdown, format_json, format_text};
+use remix::packer::{PackedRepository, RepositorySummary, FileContent};
+use remix::security::SecurityCheckStatus;
+
+#[test]
+fn test_format_toon_basic() -> Result<()> {
+ let repo = PackedRepository {
+ summary: RepositorySummary {
+ file_count: 2,
+ directory_count: 1,
+ total_size: 100,
+ extensions: vec!["txt".to_string(), "rs".to_string()],
+ binary_file_count: 0,
+ },
+ files: vec![
+ FileContent {
+ relative_path: "file1.txt".to_string(),
+ content: "Hello".to_string(),
+ size: 5,
+ extension: "txt".to_string(),
+ is_binary: false,
+ },
+ FileContent {
+ relative_path: "file2.rs".to_string(),
+ content: "fn main() {}".to_string(),
+ size: 11,
+ extension: "rs".to_string(),
+ is_binary: false,
+ },
+ ],
+ instruction: Some("Test instruction".to_string()),
+ suspicious_files: None,
+ security_check_status: remix::security::SecurityCheckStatus::CompletedNoFindings,
+ binary_files: None,
+ };
+
+ let result = format_toon(&repo)?;
+ assert!(!result.is_empty());
+ assert!(result.contains("files[2]"));
+ assert!(result.contains("file_count: 2"));
+ Ok(())
+}
+
+#[test]
+fn test_format_markdown() -> Result<()> {
+ let repo = PackedRepository {
+ summary: RepositorySummary {
+ file_count: 1,
+ directory_count: 0,
+ total_size: 10,
+ extensions: vec!["md".to_string()],
+ binary_file_count: 0,
+ },
+ files: vec![
+ FileContent {
+ relative_path: "README.md".to_string(),
+ content: "# Hello\nWorld".to_string(),
+ size: 10,
+ extension: "md".to_string(),
+ is_binary: false,
+ },
+ ],
+ instruction: Some("Test instruction".to_string()),
+ suspicious_files: None,
+ security_check_status: SecurityCheckStatus::CompletedNoFindings,
+ binary_files: None,
+ };
+
+ let result = format_markdown(&repo);
+ assert!(result.contains("# User Instruction"));
+ assert!(result.contains("Test instruction"));
+ assert!(result.contains("- **Files:** 1"));
+ assert!(result.contains("### README.md"));
+ assert!(result.contains("# Hello"));
+ Ok(())
+}
+
+#[test]
+fn test_format_json() -> Result<()> {
+ let repo = PackedRepository {
+ summary: RepositorySummary {
+ file_count: 1,
+ directory_count: 0,
+ total_size: 5,
+ extensions: vec!["txt".to_string()],
+ binary_file_count: 0,
+ },
+ files: vec![
+ FileContent {
+ relative_path: "test.txt".to_string(),
+ content: "Hello".to_string(),
+ size: 5,
+ extension: "txt".to_string(),
+ is_binary: false,
+ },
+ ],
+ instruction: None,
+ suspicious_files: None,
+ security_check_status: SecurityCheckStatus::CompletedNoFindings,
+ binary_files: None,
+ };
+
+ let result = format_json(&repo)?;
+ println!("JSON result: {}", result);
+ // Should be valid JSON
+ let json: serde_json::Value = serde_json::from_str(&result)?;
+ assert_eq!(json["summary"]["file_count"], 1);
+ assert_eq!(json["files"][0]["relative_path"], "test.txt");
+ assert_eq!(json["files"][0]["content"], "Hello");
+ Ok(())
+}
+
+#[test]
+fn test_format_text() -> Result<()> {
+ let repo = PackedRepository {
+ summary: RepositorySummary {
+ file_count: 1,
+ directory_count: 0,
+ total_size: 5,
+ extensions: vec!["txt".to_string()],
+ binary_file_count: 0,
+ },
+ files: vec![
+ FileContent {
+ relative_path: "test.txt".to_string(),
+ content: "Hello".to_string(),
+ size: 5,
+ extension: "txt".to_string(),
+ is_binary: false,
+ },
+ ],
+ instruction: Some("Instruction".to_string()),
+ suspicious_files: None,
+ security_check_status: SecurityCheckStatus::CompletedNoFindings,
+ binary_files: None,
+ };
+
+ let result = format_text(&repo);
+ assert!(result.contains("USER INSTRUCTION:"));
+ assert!(result.contains("Instruction"));
+ assert!(result.contains("Files: 1"));
+ assert!(result.contains("FILE: test.txt"));
+ assert!(result.contains("Hello"));
+ Ok(())
+}
+
+#[test]
+fn test_format_toon_empty_repo() -> Result<()> {
+ let repo = PackedRepository {
+ summary: RepositorySummary {
+ file_count: 0,
+ directory_count: 0,
+ total_size: 0,
+ extensions: vec![],
+ binary_file_count: 0,
+ },
+ files: vec![],
+ instruction: None,
+ suspicious_files: None,
+ security_check_status: remix::security::SecurityCheckStatus::CompletedNoFindings,
+ binary_files: None,
+ };
+
+ let result = format_toon(&repo)?;
+ assert!(!result.is_empty());
+ assert!(result.contains("file_count: 0"));
+ Ok(())
+}
\ No newline at end of file
diff --git a/tests/packer_tests.rs b/tests/packer_tests.rs
index 0a59db5..9d53c96 100644
--- a/tests/packer_tests.rs
+++ b/tests/packer_tests.rs
@@ -63,3 +63,148 @@ async fn test_pack_repository_basic() {
println!("Suspicious files: {:?}", suspicious);
}
}
+
+#[tokio::test]
+async fn test_pack_repository_with_include_patterns() {
+ let test_dir = common::create_test_repo();
+
+ let config = Config {
+ include: vec!["**/*.rs".to_string(), "**/*.md".to_string()],
+ ..Default::default()
+ };
+
+ let result = pack_repository(test_dir.path(), &config).await.unwrap();
+
+ let file_paths: Vec = result
+ .files
+ .iter()
+ .map(|f| f.relative_path.replace('\\', "/"))
+ .collect();
+
+ // Should only include .rs and .md files
+ assert!(file_paths.iter().all(|path| path.ends_with(".rs") || path.ends_with(".md")));
+ assert!(file_paths.iter().any(|path| path.contains("main.rs")));
+ assert!(file_paths.iter().any(|path| path.contains("README.md")));
+}
+
+#[tokio::test]
+async fn test_pack_repository_with_ignore_patterns() {
+ let test_dir = common::create_test_repo();
+
+ let mut config = Config::default();
+ config.ignore.custom_patterns = vec!["*.md".to_string()];
+
+ let result = pack_repository(test_dir.path(), &config).await.unwrap();
+
+ let file_paths: Vec = result
+ .files
+ .iter()
+ .map(|f| f.relative_path.replace('\\', "/"))
+ .collect();
+
+ // Should not include .md files
+ assert!(!file_paths.iter().any(|path| path.ends_with(".md")));
+ assert!(file_paths.iter().any(|path| path.contains("main.rs")));
+}
+
+#[tokio::test]
+async fn test_pack_repository_file_size_limit() {
+ let test_dir = common::create_test_repo();
+
+ // Create a large file
+ let large_content = "x".repeat(2000); // 2000 bytes
+ std::fs::write(test_dir.path().join("large.txt"), large_content).unwrap();
+
+ let config = Config {
+ max_file_size: 1000, // 1000 bytes limit
+ ..Default::default()
+ };
+
+ let result = pack_repository(test_dir.path(), &config).await.unwrap();
+
+ let file_paths: Vec = result
+ .files
+ .iter()
+ .map(|f| f.relative_path.replace('\\', "/"))
+ .collect();
+
+ // Large file should be excluded
+ assert!(!file_paths.iter().any(|path| path.contains("large.txt")));
+ // Small files should be included
+ assert!(file_paths.iter().any(|path| path.contains("main.rs")));
+}
+
+#[tokio::test]
+async fn test_pack_repository_binary_detection() {
+ let test_dir = common::create_test_repo();
+
+ // Create a binary-like file (null bytes)
+ let binary_content = vec![0u8; 100];
+ std::fs::write(test_dir.path().join("binary.dat"), binary_content).unwrap();
+
+ let config = Config::default();
+ let result = pack_repository(test_dir.path(), &config).await.unwrap();
+
+ let file_paths: Vec = result
+ .files
+ .iter()
+ .map(|f| f.relative_path.replace('\\', "/"))
+ .collect();
+
+ // Binary file should be excluded
+ assert!(!file_paths.iter().any(|path| path.contains("binary.dat")));
+}
+
+#[tokio::test]
+async fn test_pack_repository_gitignore_handling() {
+ let test_dir = common::create_test_repo();
+
+ // Create a file that should be ignored by gitignore
+ std::fs::write(test_dir.path().join("debug.log"), "log content").unwrap();
+
+ let config = Config::default();
+ let result = pack_repository(test_dir.path(), &config).await.unwrap();
+
+ let file_paths: Vec = result
+ .files
+ .iter()
+ .map(|f| f.relative_path.replace('\\', "/"))
+ .collect();
+
+ // Log file should be ignored
+ assert!(!file_paths.iter().any(|path| path.contains("debug.log")));
+}
+
+#[tokio::test]
+async fn test_pack_repository_directory_traversal() {
+ let test_dir = common::create_test_repo();
+
+ // Create deep nested directory
+ std::fs::create_dir_all(test_dir.path().join("deep/nested/folder")).unwrap();
+ std::fs::write(test_dir.path().join("deep/nested/folder/file.txt"), "nested content").unwrap();
+
+ let config = Config::default();
+ let result = pack_repository(test_dir.path(), &config).await.unwrap();
+
+ let file_paths: Vec = result
+ .files
+ .iter()
+ .map(|f| f.relative_path.replace('\\', "/"))
+ .collect();
+
+ // Deep nested file should be included
+ assert!(file_paths.iter().any(|path| path.contains("deep/nested/folder/file.txt")));
+}
+
+
+
+#[tokio::test]
+async fn test_pack_repository_empty_directory() {
+ let temp_dir = tempfile::TempDir::new().unwrap();
+
+ let config = Config::default();
+ let result = pack_repository(temp_dir.path(), &config).await.unwrap();
+
+ assert_eq!(result.summary.file_count, 0);
+ assert!(result.files.is_empty());
+}
diff --git a/tests/security_tests.rs b/tests/security_tests.rs
index 929bafd..77dd5ca 100644
--- a/tests/security_tests.rs
+++ b/tests/security_tests.rs
@@ -82,3 +82,66 @@ fn test_perform_security_check() {
"Security check should detect at least one suspicious file"
);
}
+
+#[test]
+fn test_check_sensitive_content_edge_cases() {
+ // Test various formats and edge cases
+ let sensitive_cases = vec![
+ "const API_KEY = 'abc123'", // api_key keyword
+ "secret_key: mySecret", // secret_key keyword
+ "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC\n-----END PRIVATE KEY-----", // private key
+ "password= mySecretPass123!", // password=
+ "mongodb://username:password@localhost:27017", // mongodb://
+ ];
+
+ let safe_cases = vec![
+ "const version = '1.2.3'", // Version string
+ "log.debug('API called with key: test_key')", // Test key in log
+ "const example = 'sk_test_example_from_docs'", // Example from docs
+ "README.md contains sk_test_123", // In documentation
+ ];
+
+ for case in sensitive_cases {
+ assert!(
+ security::check_sensitive_content(case),
+ "Should detect sensitive content: {}",
+ case
+ );
+ }
+
+ for case in safe_cases {
+ assert!(
+ !security::check_sensitive_content(case),
+ "Should not flag safe content: {}",
+ case
+ );
+ }
+}
+
+#[test]
+fn test_perform_security_check_empty_directory() {
+ let dir = tempdir().expect("Failed to create temporary directory");
+
+ let result = security::perform_security_check(dir.path());
+ assert!(result.is_ok());
+
+ let suspicious_files = result.unwrap();
+ assert!(suspicious_files.is_empty(), "Empty directory should have no suspicious files");
+}
+
+#[test]
+fn test_perform_security_check_binary_file() {
+ let dir = tempdir().expect("Failed to create temporary directory");
+
+ // Create a binary file with sensitive-looking content
+ let binary_path = dir.path().join("binary.dat");
+ let binary_content = b"\x00\x01\x02sk_test_1234567890\x03\x04\x05";
+ fs::write(&binary_path, binary_content).expect("Failed to write binary file");
+
+ let result = security::perform_security_check(dir.path());
+ assert!(result.is_ok());
+
+ let suspicious_files = result.unwrap();
+ // Binary files are not checked for sensitive content
+ assert!(suspicious_files.is_empty(), "Binary files should not be checked for sensitive content");
+}