ctxpack 0.1.0

Configuration-driven source packing for LLM context, with a versioning protocol for incremental patch/replace
use anyhow::Result;
use clap::Parser;
use tracing::info;

mod apply;
mod cli;
mod config;
mod detection;
mod discovery;
mod encoding_layer;
mod extraction;
mod index;
mod migrate;
mod pack;
mod version;

use cli::{CacheSubcommands, Cli, Commands};

fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .init();

    let cli = Cli::parse();
    let config = config::load_config(cli.config.as_deref())?;
    let warnings = config::validate_config(&config);
    for w in &warnings {
        tracing::warn!("{}", w);
    }

    match cli.command {
        Commands::Init => {
            init::run_init()?;
        }
        Commands::Pack(args) => {
            pack::run_pack(&config, &args)?;
        }
        Commands::Apply(args) => {
            apply::run_apply(&config, &args)?;
        }
        Commands::Status(args) => {
            pack::run_status(&config, &args)?;
        }
        Commands::Tree(args) => {
            pack::run_tree(&config, &args)?;
        }
        Commands::Prompt(args) => {
            pack::run_prompt(&config, &args)?;
        }
        Commands::MigratePrefix(args) => {
            migrate::run_migrate_prefix(&config, &args)?;
        }
        Commands::Cache(args) => match args.subcommand {
            CacheSubcommands::Clean(clean_args) => {
                pack::run_cache_clean(&config, &clean_args)?;
            }
            CacheSubcommands::Info => {
                pack::run_cache_info(&config)?;
            }
        },
    }

    info!("done");
    Ok(())
}

mod init {
    use anyhow::Result;

    pub fn run_init() -> Result<()> {
        let content = r#"# ctx-pack configuration
# Run `ctx-pack pack` to generate context files

global:
  # XML tag prefix used in output file
  prefix: "ctx"
  # Anchor line interval (0 = disabled). Annotates every Nth line with line number.
  anchor_interval: 10
  # Replace leading spaces with [N] prefix to save tokens
  indent_encoding: true
  # Tab width in spaces
  tab_width: 4
  # How to handle binary files: skip | warn | abort
  binary_policy: skip
  # Auto-detect and convert non-UTF-8 files
  encoding_detection: true
  # Total output size warning threshold
  max_content_size: "500KB"
  # Per-file size warning threshold
  max_file_size: "100KB"
  # Size policy: warn | abort | ignore
  size_policy: warn
  # Index file path (tracks file versions)
  index_file: ".ctx-index.yaml"
  # Cache directory for snapshots
  cache_dir: ".ctx-cache"
  # Number of recent snapshots to retain per file
  cache_retention: 5
  # Generate manifest file alongside output
  manifest: true
  # Generate protocol explanation prompt
  prompt_generation: true

profiles:
  default:
    roots:
      - path: "."
        label: "project"
    discovery:
      use_gitignore: true
      include: []
      exclude:
        - "*.ctx"
        - "*.ctx.manifest"
        - "*.rej"
        - ".ctx-cache/**"
        - ".ctx-index.yaml"
        - "ctx-pack.yaml"
    extraction:
      default_mode: full
      rules: []
    versioning:
      auto_diff: true
      replace_threshold: 0.5
      max_patches_before_replace: 5
    output:
      file: "context.ctx"
      manifest: "context.ctx.manifest"
"#;

        let path = std::path::Path::new("ctx-pack.yaml");
        if path.exists() {
            anyhow::bail!("ctx-pack.yaml already exists");
        }
        std::fs::write(path, content)?;
        println!("Created ctx-pack.yaml");
        Ok(())
    }
}