Compare commits

..

14 Commits

Author SHA1 Message Date
d01d2ef3e4 fix: simplify repository opening logic in append_date_and_git_hash function
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Failing after 2m17s
2025-09-20 12:01:22 +02:00
e06752f625 refactor: update Config to support multiple directories and add relative path option
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Failing after 1m27s
2025-09-20 11:54:39 +02:00
935f700e7e refactor: remove Windows from CI matrix for streamlined builds
All checks were successful
Continuous Integration / Build and Test on ubuntu-latest (push) Successful in 2m6s
2025-08-24 15:24:07 +02:00
7c8d2e20c4 fix: update installation script URL from 'main' to 'master'
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Successful in 2m2s
Continuous Integration / Build and Test on windows-latest (push) Has been cancelled
2025-08-24 14:57:19 +02:00
57f9c4bc67 docs: update installation instructions and usage examples in README
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Successful in 2m6s
Continuous Integration / Build and Test on windows-latest (push) Has been cancelled
2025-08-24 14:53:03 +02:00
13791a445a refactor: streamline console format determination logic in main function
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Successful in 2m5s
Continuous Integration / Build and Test on windows-latest (push) Has been cancelled
2025-08-24 14:45:50 +02:00
725891d1b6 refactor: simplify output format determination logic in main function
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Failing after 1m16s
Continuous Integration / Build and Test on windows-latest (push) Has been cancelled
2025-08-24 14:40:38 +02:00
8745c2627e fix: update branch references from 'main' to 'master' in CI workflow
Some checks failed
Continuous Integration / Build and Test on ubuntu-latest (push) Failing after 1m52s
Continuous Integration / Build and Test on windows-latest (push) Has been cancelled
2025-08-24 14:26:21 +02:00
73f6f74a71 feat: enable manual triggering of CI workflow with workflow_dispatch event 2025-08-24 14:25:42 +02:00
c9d67c8cd9 feat: add GitHub Actions workflow for continuous integration with Rust 2025-08-24 14:22:59 +02:00
3c9f288715 fix: specify shell type for artifact path preparation step in GitHub Actions workflow 2025-08-24 14:19:18 +02:00
70524b4b34 feat: remove macOS build configuration from GitHub Actions workflow 2025-08-24 14:15:23 +02:00
dc61958151 fix: update clone URL in installation instructions in README.md 2025-08-24 14:11:43 +02:00
6d36f4b3aa feat: update GitHub Actions workflow for improved artifact handling and release process 2025-08-24 14:10:17 +02:00
8 changed files with 213 additions and 131 deletions

34
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Continuous Integration
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build_and_test:
name: Build and Test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Check formatting
run: cargo fmt -- --check
- name: Run Clippy
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test

View File

@@ -14,65 +14,54 @@ jobs:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
- os: macos-latest asset_name: codebase-to-prompt-linux-amd64
target: x86_64-apple-darwin
- os: windows-latest - os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
asset_name: codebase-to-prompt-windows-amd64.exe
steps: steps:
- uses: actions/checkout@v2 - name: Checkout code
- name: Build uses: actions/checkout@v4
run: cargo build --release --target ${{ matrix.target }}
- name: Upload Artifact - name: Install Rust toolchain
uses: actions/upload-artifact@v2 uses: dtolnay/rust-toolchain@stable
with: with:
name: codebase-to-prompt-${{ matrix.target }} targets: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/codebase-to-prompt*
- name: Build binary
run: cargo build --release --target ${{ matrix.target }}
- name: Prepare artifact path
id: artifact_path
shell: bash
run: |
if [ "${{ matrix.os }}" = "windows-latest" ]; then
echo "path=target/${{ matrix.target }}/release/codebase-to-prompt.exe" >> $GITHUB_OUTPUT
else
echo "path=target/${{ matrix.target }}/release/codebase-to-prompt" >> $GITHUB_OUTPUT
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset_name }}
path: ${{ steps.artifact_path.outputs.path }}
release: release:
name: Create Release name: Create GitHub Release
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write # This is required to create a release and upload assets
steps: steps:
- uses: actions/checkout@v2 - name: Download all artifacts
- name: Download Artifacts uses: actions/download-artifact@v4
uses: actions/download-artifact@v2
with: with:
path: artifacts path: artifacts/
- name: Create Release
id: create_release - name: Create Release and Upload Assets
uses: actions/create-release@v1 uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref }} files: |
release_name: Release ${{ github.ref }} artifacts/codebase-to-prompt-linux-amd64/*
draft: false artifacts/codebase-to-prompt-windows-amd64.exe/*
prerelease: false
- name: Upload Release Asset (Linux)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./artifacts/codebase-to-prompt-x86_64-unknown-linux-gnu/codebase-to-prompt
asset_name: codebase-to-prompt-x86_64-unknown-linux-gnu
asset_content_type: application/octet-stream
- name: Upload Release Asset (macOS)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./artifacts/codebase-to-prompt-x86_64-apple-darwin/codebase-to-prompt
asset_name: codebase-to-prompt-x86_64-apple-darwin
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./artifacts/codebase-to-prompt-x86_64-pc-windows-msvc/codebase-to-prompt.exe
asset_name: codebase-to-prompt-x86_64-pc-windows-msvc.exe
asset_content_type: application/octet-stream

2
Cargo.lock generated
View File

@@ -183,7 +183,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]] [[package]]
name = "codebase-to-prompt" name = "codebase-to-prompt"
version = "1.0.0" version = "1.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "codebase-to-prompt" name = "codebase-to-prompt"
version = "1.0.0" version = "1.0.1"
edition = "2024" edition = "2024"
authors = ["Gabriel Kaszewski <gabrielkaszewski@gmail.com>"] authors = ["Gabriel Kaszewski <gabrielkaszewski@gmail.com>"]
license = "MIT" license = "MIT"

View File

@@ -1,5 +1,8 @@
# Codebase to Prompt # Codebase to Prompt
[![CI](https://github.com/GKaszewski/codebase-to-prompt/actions/workflows/ci.yml/badge.svg)](https://github.com/GKaszewski/codebase-to-prompt/actions/workflows/ci.yml)
[![Release](https://github.com/GKaszewski/codebase-to-prompt/actions/workflows/release.yml/badge.svg)](https://github.com/GKaszewski/codebase-to-prompt/actions/workflows/release.yml)
`codebase-to-prompt` is a Rust-based CLI tool designed to bundle files from a directory into a single output file. It supports filtering files by extensions, respecting `.gitignore` rules, and formatting the output in Markdown, plain text, or console-friendly formats. `codebase-to-prompt` is a Rust-based CLI tool designed to bundle files from a directory into a single output file. It supports filtering files by extensions, respecting `.gitignore` rules, and formatting the output in Markdown, plain text, or console-friendly formats.
## Features ## Features
@@ -14,17 +17,41 @@
## Installation ## Installation
1. Ensure you have [Rust](https://www.rust-lang.org/) installed. There are multiple ways to install `codebase-to-prompt`.
2. Clone this repository:
```bash ### Option 1: Using `cargo install` (Recommended for Rust developers)
git clone <repository-url>
cd codebase-to-prompt If you have the Rust toolchain installed, you can easily install the latest version from [crates.io](https://crates.io/):
```
3. Build the project: ```bash
```bash cargo install codebase-to-prompt
cargo build --release ```
```
4. The binary will be available at `target/release/codebase-to-prompt`. ### Option 2: Using the Install Script (for Linux & macOS)
You can download and run the installation script, which will install the latest release binary for your system:
```bash
curl -fsSL https://raw.githubusercontent.com/GKaszewski/codebase-to-prompt/master/install.sh | sh
```
### Option 3: From GitHub Releases
You can download the pre-compiled binary for your operating system directly from the [Releases page](https://github.com/GKaszewski/codebase-to-prompt/releases).
### Option 4: Building from Source
1. Ensure you have [Rust](https://www.rust-lang.org/) installed.
2. Clone this repository:
```bash
git clone https://github.com/GKaszewski/codebase-to-prompt
cd codebase-to-prompt
```
3. Build the project:
```bash
cargo build --release
```
4. The binary will be available at `target/release/codebase-to-prompt`.
## Usage ## Usage
@@ -48,22 +75,23 @@ codebase-to-prompt [OPTIONS] [DIRECTORY]
### Examples ### Examples
1. Bundle all `.rs` files in the current directory into `output.md` in Markdown format: 1. Bundle all `.rs` files in the current directory into `output.md` in Markdown format:
```bash ```bash
codebase-to-prompt -o output.md -i rs --format markdown codebase-to-prompt -o output.md -i rs
``` ```
2. Bundle all files except `.log` files, appending the current date and Git hash to the output file name: 2. Bundle all files except `.log` files, appending the current date and Git hash to the output file name:
```bash ```bash
codebase-to-prompt -o output.txt -e log -d -g codebase-to-prompt -o output.txt -e log -d -g
``` ```
3. Output all files to the console, including line numbers: 3. Output all files to the console, including line numbers:
```bash
codebase-to-prompt -l ```bash
``` codebase-to-prompt -l
```
## Development ## Development
@@ -87,19 +115,6 @@ To run tests:
cargo test cargo test
``` ```
### Code Structure
- `src/lib.rs`: Core logic for file processing and bundling.
- `src/main.rs`: CLI entry point.
### Adding Dependencies
To add a new dependency, update `Cargo.toml` and run:
```bash
cargo build
```
### Linting and Formatting ### Linting and Formatting
To lint the code: To lint the code:
@@ -120,4 +135,4 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
## Contributing ## Contributing
Contributions are welcome! Feel free to open issues or submit pull requests. Contributions are welcome\! Feel free to open issues or submit pull requests.

View File

@@ -7,7 +7,7 @@ use chrono::Local;
use clap::ValueEnum; use clap::ValueEnum;
use git2::Repository; use git2::Repository;
use ignore::gitignore::Gitignore; use ignore::gitignore::Gitignore;
use tracing::{error, info, warn}; use tracing::{debug, error, info, warn};
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
/// Represents the output format for the bundled files. /// Represents the output format for the bundled files.
@@ -26,8 +26,8 @@ pub enum Format {
/// Configuration options for the file bundling process. /// Configuration options for the file bundling process.
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
/// The directory to process. /// The directories to process.
pub directory: PathBuf, pub directory: Vec<PathBuf>,
/// The optional output file path. If not provided, output is written to stdout. /// The optional output file path. If not provided, output is written to stdout.
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
/// File extensions to include in the output. /// File extensions to include in the output.
@@ -46,6 +46,8 @@ pub struct Config {
pub ignore_hidden: bool, pub ignore_hidden: bool,
/// Whether to respect `.gitignore` rules. /// Whether to respect `.gitignore` rules.
pub respect_gitignore: bool, pub respect_gitignore: bool,
/// Whether to use relative paths in the output.
pub relative_path: bool,
} }
/// Runs the file bundling process based on the provided configuration. /// Runs the file bundling process based on the provided configuration.
@@ -62,9 +64,20 @@ pub fn run(config: Config) -> Result<()> {
append_date_and_git_hash(&mut output_path, &config)?; append_date_and_git_hash(&mut output_path, &config)?;
} }
let writer = determine_output_writer(&output_path)?; let mut writer = determine_output_writer(&output_path)?;
process_directory(&config, writer) for dir in &config.directory {
if !dir.is_dir() {
return Err(anyhow::anyhow!(
"The specified path is not a directory: {}",
dir.display()
));
}
process_directory(&config, &mut writer, dir)?;
}
Ok(())
} }
/// Appends the current date and/or Git hash to the output file name if required. /// Appends the current date and/or Git hash to the output file name if required.
@@ -90,16 +103,18 @@ fn append_date_and_git_hash(output_path: &mut Option<PathBuf>, config: &Config)
} }
if config.append_git_hash { if config.append_git_hash {
match Repository::open(&config.directory) { for dir in &config.directory {
Ok(repo) => { match Repository::open(dir) {
let head = repo.head().context("Failed to get repository HEAD")?; Ok(repo) => {
if let Some(oid) = head.target() { let head = repo.head().context("Failed to get repository HEAD")?;
new_filename.push('_'); if let Some(oid) = head.target() {
new_filename.push_str(&oid.to_string()[..7]); new_filename.push('_');
info!("Appending git hash to filename."); new_filename.push_str(&oid.to_string()[..7]);
info!("Appending git hash to filename.");
}
} }
Err(_) => warn!("Not a git repository, cannot append git hash."),
} }
Err(_) => warn!("Not a git repository, cannot append git hash."),
} }
} }
@@ -139,23 +154,26 @@ fn determine_output_writer(output_path: &Option<PathBuf>) -> Result<Box<dyn Writ
/// ///
/// # Returns /// # Returns
/// * `Result<()>` - Returns `Ok(())` if successful, or an error if the process fails. /// * `Result<()>` - Returns `Ok(())` if successful, or an error if the process fails.
fn process_directory(config: &Config, mut writer: Box<dyn Write>) -> Result<()> { fn process_directory(config: &Config, writer: &mut dyn Write, dir: &PathBuf) -> Result<()> {
let (gitignore, _) = Gitignore::new(config.directory.join(".gitignore")); let (gitignore, _) = Gitignore::new(dir.join(".gitignore"));
let walker = WalkDir::new(&config.directory) let walker = WalkDir::new(dir)
.into_iter() .into_iter()
.filter_entry(|e| should_include_entry(e, &gitignore, config)); .filter_entry(|e| should_include_entry(e, &gitignore, config));
for result in walker { for result in walker {
let entry = match result { let entry = match result {
Ok(entry) => entry, Ok(entry) => {
debug!("Processing entry: {:?}", entry);
entry
}
Err(err) => { Err(err) => {
error!("Failed to access entry: {}", err); error!("Failed to access entry: {}", err);
continue; continue;
} }
}; };
if let Err(err) = process_file_entry(&entry, &mut writer, config) { if let Err(err) = process_file_entry(&entry, writer, config, dir) {
error!("{}", err); error!("{}", err);
} }
} }
@@ -186,26 +204,37 @@ fn should_include_entry(entry: &DirEntry, gitignore: &Gitignore, config: &Config
/// ///
/// # Returns /// # Returns
/// * `Result<()>` - Returns `Ok(())` if successful, or an error if the process fails. /// * `Result<()>` - Returns `Ok(())` if successful, or an error if the process fails.
fn process_file_entry(entry: &DirEntry, writer: &mut dyn Write, config: &Config) -> Result<()> { fn process_file_entry(
entry: &DirEntry,
writer: &mut dyn Write,
config: &Config,
dir: &PathBuf,
) -> Result<()> {
debug!("File process entry process for {:?}", entry.path());
let path = entry.path(); let path = entry.path();
if !path.is_file() { if !path.is_file() {
debug!("{:?} is not a file.", path);
return Ok(()); return Ok(());
} }
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or(""); let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
let apply_include_filter = debug!("Extension {:?} for {:?}", extension, path);
!(config.include.is_empty() || config.include.len() == 1 && config.include[0].is_empty());
let apply_include_filter = !(config.include.is_empty());
if apply_include_filter && !config.include.contains(&extension.to_string()) { if apply_include_filter && !config.include.contains(&extension.to_string()) {
return Ok(()); return Ok(());
} }
if config.exclude.contains(&extension.to_string()) { let apply_exclude_filter = !(config.exclude.is_empty());
if apply_exclude_filter && config.exclude.contains(&extension.to_string()) {
return Ok(()); return Ok(());
} }
let relative_path = path.strip_prefix(&config.directory).unwrap_or(path); let relative_path = path.strip_prefix(dir).unwrap_or(path);
let content = match fs::read_to_string(path) { let content = match fs::read_to_string(path) {
Ok(content) => content, Ok(content) => content,
Err(_) => { Err(_) => {
@@ -214,8 +243,17 @@ fn process_file_entry(entry: &DirEntry, writer: &mut dyn Write, config: &Config)
} }
}; };
write_file_content(writer, relative_path, &content, extension, config) match &config.relative_path {
.with_context(|| format!("Failed to write file content for {}", path.display())) true => write_file_content(writer, relative_path, &content, extension, config)
.with_context(|| {
format!(
"Failed to write file content for {}",
relative_path.display()
)
}),
false => write_file_content(writer, path, &content, extension, config)
.with_context(|| format!("Failed to write file content for {}", path.display())),
}
} }
/// Writes the content of a single file to the writer based on the specified format. /// Writes the content of a single file to the writer based on the specified format.

View File

@@ -8,16 +8,16 @@ use tracing_subscriber::FmtSubscriber;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Args { struct Args {
#[arg(default_value = ".")] #[arg(default_value = ".", value_delimiter = ' ', num_args=1..)]
directory: PathBuf, directory: Vec<PathBuf>,
#[arg(short, long)] #[arg(short, long)]
output: Option<PathBuf>, output: Option<PathBuf>,
#[arg(short, long, use_value_delimiter = true, default_value = "")] #[arg(short, long, value_delimiter = ',', num_args = 0..)]
include: Vec<String>, include: Vec<String>,
#[arg(short, long, use_value_delimiter = true, default_value = "")] #[arg(short, long, value_delimiter = ',', num_args = 0..)]
exclude: Vec<String>, exclude: Vec<String>,
#[arg(long, value_enum, default_value_t = Format::Console)] #[arg(long, value_enum, default_value_t = Format::Console)]
@@ -32,30 +32,32 @@ struct Args {
#[arg(short = 'l', long)] #[arg(short = 'l', long)]
line_numbers: bool, line_numbers: bool,
#[arg(short = 'H', long)] #[arg(short = 'H', long, default_value_t = true)]
ignore_hidden: bool, ignore_hidden: bool,
#[arg(short = 'R', long, default_value_t = true)] #[arg(short = 'R', long, default_value_t = true)]
respect_gitignore: bool, respect_gitignore: bool,
#[arg(long, default_value_t = false)]
relative_path: bool,
} }
fn main() -> Result<()> { fn main() -> Result<()> {
let subscriber = FmtSubscriber::builder() let subscriber = FmtSubscriber::builder()
.with_max_level(LevelFilter::INFO) .with_max_level(LevelFilter::DEBUG)
.finish(); .finish();
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
let args = Args::parse(); let args = Args::parse();
let mut format = args.format; let mut format = args.format;
if matches!(format, Format::Console) { if matches!(format, Format::Console)
if let Some(output_path) = &args.output { && let Some(output_path) = &args.output
if output_path.extension().and_then(|s| s.to_str()) == Some("md") { {
format = Format::Markdown; match output_path.extension().and_then(|s| s.to_str()) {
} Some("md") => format = Format::Markdown,
if output_path.extension().and_then(|s| s.to_str()) == Some("txt") { Some("txt") => format = Format::Text,
format = Format::Text; _ => {}
}
} }
} }
@@ -70,6 +72,7 @@ fn main() -> Result<()> {
line_numbers: args.line_numbers, line_numbers: args.line_numbers,
ignore_hidden: args.ignore_hidden, ignore_hidden: args.ignore_hidden,
respect_gitignore: args.respect_gitignore, respect_gitignore: args.respect_gitignore,
relative_path: args.relative_path,
}; };
debug!("Starting codebase to prompt with config: {:?}", config); debug!("Starting codebase to prompt with config: {:?}", config);

View File

@@ -8,7 +8,7 @@ fn test_run_with_markdown_format() {
let output_file = temp_dir.path().join("output.md"); let output_file = temp_dir.path().join("output.md");
let config = Config { let config = Config {
directory: PathBuf::from("tests/fixtures"), directory: vec![PathBuf::from("tests/fixtures")],
output: Some(output_file.clone()), output: Some(output_file.clone()),
include: vec!["rs".to_string()], include: vec!["rs".to_string()],
exclude: vec![], exclude: vec![],
@@ -18,6 +18,7 @@ fn test_run_with_markdown_format() {
line_numbers: false, line_numbers: false,
ignore_hidden: true, ignore_hidden: true,
respect_gitignore: true, respect_gitignore: true,
relative_path: false,
}; };
let result = run(config); let result = run(config);
@@ -33,7 +34,7 @@ fn test_run_with_text_format() {
let output_file = temp_dir.path().join("output.txt"); let output_file = temp_dir.path().join("output.txt");
let config = Config { let config = Config {
directory: PathBuf::from("tests/fixtures"), directory: vec![PathBuf::from("tests/fixtures")],
output: Some(output_file.clone()), output: Some(output_file.clone()),
include: vec!["txt".to_string()], include: vec!["txt".to_string()],
exclude: vec![], exclude: vec![],
@@ -43,6 +44,7 @@ fn test_run_with_text_format() {
line_numbers: true, line_numbers: true,
ignore_hidden: true, ignore_hidden: true,
respect_gitignore: true, respect_gitignore: true,
relative_path: false,
}; };
let result = run(config); let result = run(config);
@@ -58,7 +60,7 @@ fn test_run_with_git_hash_append() {
let output_file = temp_dir.path().join("output.txt"); let output_file = temp_dir.path().join("output.txt");
let config = Config { let config = Config {
directory: PathBuf::from("tests/fixtures"), directory: vec![PathBuf::from("tests/fixtures")],
output: Some(output_file.clone()), output: Some(output_file.clone()),
include: vec!["txt".to_string()], include: vec!["txt".to_string()],
exclude: vec![], exclude: vec![],
@@ -68,6 +70,7 @@ fn test_run_with_git_hash_append() {
line_numbers: false, line_numbers: false,
ignore_hidden: true, ignore_hidden: true,
respect_gitignore: true, respect_gitignore: true,
relative_path: false,
}; };
let result = run(config); let result = run(config);