use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = "pctx",
version,
author,
about = "Generate LLM-ready context from your codebase",
long_about = r#"Generate LLM-ready context from your codebase.
STRUCTURED OUTPUT:
Use --json for machine-readable output. All progress messages go to stderr,
only the result goes to stdout. Exit codes indicate success/failure type.
Note: In --json mode, errors are output to stdout as part of the JSON response
to maintain a consistent API contract for programmatic consumers.
RECURSION:
By default, pctx recursively scans all directories. Use --max-depth to limit:
--max-depth 1 Only immediate children (no recursion)
--max-depth 2 Children and grandchildren
--max-depth 0 Unlimited depth (default)
EXIT CODES:
0 Success
1 General failure
2 Usage error (bad arguments)
3 File/directory not found
4 Permission denied
5 Conflict (file exists)
6 No files matched filters
7 Partial success (some files failed)
EXAMPLES:
# Basic usage - current directory to stdout
pctx
# JSON output for programmatic use
pctx --json
# Copy to clipboard
pctx --clipboard
# Write to file (fails if exists, use --force to overwrite)
pctx --output context.md
pctx --output context.md --force
# Filter files
pctx --exclude "*.test.ts" --exclude "__tests__"
pctx --include "*.rs" --include "*.toml"
# Read file list from stdin
find . -name "*.rs" | pctx --stdin
pctx files list --quiet | pctx --stdin
# List files without generating context
pctx files list --json
# Quiet mode - just file paths, one per line (for piping)
pctx files list --quiet
# Adjust truncation thresholds
pctx --max-lines 1000 --head-lines 50 --tail-lines 20
# Disable all truncation
pctx --no-truncation
# Limit recursion depth
pctx --max-depth 2
# Dry run with full preview
pctx --dry-run --json
# Pipe-friendly: get paths of large files
pctx files list --json | jq -r '.data[] | select(.size_bytes > 10000) | .path'"#,
after_help = "For more information, visit: https://github.com/mc-marcocheng/pctx"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[command(flatten)]
pub global: GlobalArgs,
#[command(flatten)]
pub generate: GenerateArgs,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(subcommand)]
Files(FilesCommands),
#[command(subcommand)]
Config(ConfigCommands),
Completions {
#[arg(value_enum)]
shell: Shell,
},
}
#[derive(Subcommand, Debug)]
pub enum FilesCommands {
#[command(
long_about = "List all files that would be included in the context.\n\n\
Use --json for structured output, --quiet for bare paths (one per line).\n\n\
EXAMPLES:\n \
pctx files list # Human-readable list\n \
pctx files list --json # JSON array of file info\n \
pctx files list --quiet # Bare paths for piping\n \
pctx files list -q | xargs wc -l # Count lines in each file"
)]
List {
#[command(flatten)]
filter: FilterArgs,
#[arg(short, long)]
quiet: bool,
},
#[command(
long_about = "Show the tree structure of files that would be included.\n\n\
EXAMPLES:\n \
pctx files tree # Visual tree\n \
pctx files tree --json # JSON tree structure"
)]
Tree {
#[command(flatten)]
filter: FilterArgs,
},
}
#[derive(Subcommand, Debug)]
pub enum ConfigCommands {
#[command(
long_about = "Display the current configuration after merging defaults, \
config file, and command-line options."
)]
Show,
#[command(
long_about = "Initialize a new .pctx.toml config file in the current directory.\n\n\
EXAMPLES:\n \
pctx config init # Create config (fails if exists)\n \
pctx config init --force # Overwrite existing config"
)]
Init {
#[arg(long)]
force: bool,
},
#[command(long_about = "List all patterns that are excluded by default \
(node_modules, .git, etc.)")]
Defaults,
}
#[derive(Args, Debug, Clone)]
pub struct GlobalArgs {
#[arg(long, global = true)]
pub json: bool,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(short, long, global = true, conflicts_with = "verbose")]
pub quiet: bool,
#[arg(long, global = true, value_name = "FILE")]
pub config: Option<PathBuf>,
#[arg(long, global = true)]
pub no_color: bool,
}
#[derive(Args, Debug, Clone)]
pub struct GenerateArgs {
#[arg()]
pub paths: Vec<PathBuf>,
#[command(flatten)]
pub filter: FilterArgs,
#[command(flatten)]
pub output: OutputArgs,
#[command(flatten)]
pub truncation: TruncationArgs,
#[arg(long)]
pub dry_run: bool,
#[arg(long, value_name = "MODEL", default_value = "gpt-4")]
pub token_model: String,
#[arg(long)]
pub stdin: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct FilterArgs {
#[arg(short, long = "exclude", value_name = "PATTERN")]
pub exclude: Vec<String>,
#[arg(short, long = "include", value_name = "PATTERN")]
pub include: Vec<String>,
#[arg(long)]
pub hidden: bool,
#[arg(long)]
pub no_default_excludes: bool,
#[arg(long)]
pub no_gitignore: bool,
#[arg(long, default_value = "1024", value_name = "KB")]
pub max_size: u64,
#[arg(short = 'd', long, default_value = "0", value_name = "N")]
pub max_depth: usize,
}
#[derive(Args, Debug, Clone)]
pub struct OutputArgs {
#[arg(short, long)]
pub clipboard: bool,
#[arg(short, long, value_name = "FILE")]
pub output: Option<PathBuf>,
#[arg(long, requires = "output")]
pub force: bool,
#[arg(short, long, value_enum, default_value = "markdown")]
pub format: ContentFormat,
#[arg(short, long)]
pub tree: bool,
#[arg(short, long)]
pub stats: bool,
#[arg(long)]
pub absolute_paths: bool,
}
#[derive(Args, Debug, Clone)]
pub struct TruncationArgs {
#[arg(long, conflicts_with_all = ["max_lines", "max_line_length"])]
pub no_truncation: bool,
#[arg(long, value_name = "N")]
pub max_lines: Option<usize>,
#[arg(long, value_name = "N")]
pub head_lines: Option<usize>,
#[arg(long, value_name = "N")]
pub tail_lines: Option<usize>,
#[arg(long, value_name = "N")]
pub max_line_length: Option<usize>,
#[arg(long, value_name = "N")]
pub head_chars: Option<usize>,
#[arg(long, value_name = "N")]
pub tail_chars: Option<usize>,
}
#[derive(ValueEnum, Clone, Debug, Default, PartialEq)]
pub enum ContentFormat {
#[default]
Markdown,
Xml,
Plain,
}
impl ContentFormat {
pub fn as_str(&self) -> &'static str {
match self {
ContentFormat::Markdown => "markdown",
ContentFormat::Xml => "xml",
ContentFormat::Plain => "plain",
}
}
}
impl std::fmt::Display for ContentFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(ValueEnum, Clone, Debug)]
pub enum Shell {
Bash,
Zsh,
Fish,
PowerShell,
Elvish,
}