use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::{Args, Subcommand};
use schemars::JsonSchema;
use serde::Serialize;
use crate::cli::GlobalArgs;
use crate::output::NdjsonWriter;
use crate::path_safety::validate_path;
use crate::signal::ShutdownSignal;
#[derive(Args, Debug)]
pub struct SparseArgs {
#[command(subcommand)]
pub action: SparseAction,
}
#[derive(Subcommand, Debug)]
pub enum SparseAction {
List(SparseListArgs),
Read(SparseReadArgs),
Outline(SparseOutlineArgs),
}
#[derive(Args, Debug)]
pub struct SparseListArgs {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, default_value_t = 100)]
pub max_files: u64,
#[arg(long, default_value_t = 1_048_576)]
pub max_bytes: u64,
#[arg(long, action = clap::ArgAction::Append)]
pub include: Vec<String>,
#[arg(long, action = clap::ArgAction::Append)]
pub exclude: Vec<String>,
}
#[derive(Args, Debug)]
pub struct SparseReadArgs {
#[arg(long)]
pub paths_file: PathBuf,
#[arg(long, default_value_t = 50)]
pub head: u64,
#[arg(long, default_value_t = 20)]
pub max_files: u64,
}
#[derive(Args, Debug)]
pub struct SparseOutlineArgs {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, default_value_t = 50)]
pub max_files: u64,
#[arg(long, action = clap::ArgAction::Append)]
pub include: Vec<String>,
#[arg(long, action = clap::ArgAction::Append)]
pub exclude: Vec<String>,
}
#[derive(Serialize, JsonSchema)]
struct SparseEntry {
r#type: &'static str,
path: String,
size: u64,
}
#[derive(Serialize, JsonSchema)]
struct SparseSummary {
r#type: &'static str,
emitted: u64,
truncated: bool,
elapsed_ms: u64,
}
pub fn cmd_sparse(
args: &SparseArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
_defaults: &crate::config::DefaultsSection,
) -> Result<()> {
match &args.action {
SparseAction::List(a) => sparse_list(a, global, writer, shutdown),
SparseAction::Read(a) => sparse_read(a, global, writer, shutdown),
SparseAction::Outline(a) => sparse_outline(a, global, writer, shutdown),
}
}
fn sparse_list(
args: &SparseListArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
) -> Result<()> {
let start = std::time::Instant::now();
let workspace = global.resolve_workspace()?;
let root = validate_path(&args.path, &workspace)?;
let mut builder = ignore::WalkBuilder::new(&root);
builder.hidden(false);
builder.git_ignore(true);
for g in &args.exclude {
builder.add_ignore(g);
}
let mut emitted = 0u64;
let mut bytes = 0u64;
let mut truncated = false;
for entry in builder.build() {
if shutdown.is_shutdown() {
break;
}
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
if !entry.file_type().is_some_and(|t| t.is_file()) {
continue;
}
if emitted >= args.max_files {
truncated = true;
break;
}
let path = entry.path().display().to_string();
bytes += path.len() as u64;
if bytes > args.max_bytes {
truncated = true;
break;
}
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
writer.write_event(&SparseEntry {
r#type: "sparse_entry",
path,
size,
})?;
emitted += 1;
}
writer.write_event(&SparseSummary {
r#type: "sparse_summary",
emitted,
truncated,
elapsed_ms: start.elapsed().as_millis() as u64,
})?;
Ok(())
}
fn sparse_read(
args: &SparseReadArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
) -> Result<()> {
let start = std::time::Instant::now();
let workspace = global.resolve_workspace()?;
let list_path = validate_path(&args.paths_file, &workspace)?;
let list = std::fs::read_to_string(&list_path)?;
let mut emitted = 0u64;
let mut truncated = false;
for line in list.lines() {
if shutdown.is_shutdown() {
break;
}
let line = line.trim();
if line.is_empty() {
continue;
}
if emitted >= args.max_files {
truncated = true;
break;
}
let path = validate_path(std::path::Path::new(line), &workspace)?;
let content = crate::file_io::read_file_string(&path, global.effective_max_filesize())?;
let head: String = content
.lines()
.take(args.head as usize)
.collect::<Vec<_>>()
.join("\n");
writer.write_event(&serde_json::json!({
"type": "sparse_read",
"path": path.display().to_string(),
"head": head,
"lines": args.head,
}))?;
emitted += 1;
}
writer.write_event(&SparseSummary {
r#type: "sparse_summary",
emitted,
truncated,
elapsed_ms: start.elapsed().as_millis() as u64,
})?;
Ok(())
}
fn sparse_outline(
args: &SparseOutlineArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
) -> Result<()> {
use std::time::Instant;
let start = Instant::now();
let workspace = global.resolve_workspace()?;
let root = validate_path(&args.path, &workspace)?;
let mut walker = ignore::WalkBuilder::new(&root);
walker.standard_filters(true);
for g in &args.exclude {
walker.add_ignore(g);
}
let mut files_seen = 0u64;
let mut items_emitted = 0u64;
let mut truncated = false;
for entry in walker.build() {
if shutdown.is_shutdown() {
break;
}
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
if !entry.file_type().is_some_and(|ft| ft.is_file()) {
continue;
}
if files_seen >= args.max_files {
truncated = true;
break;
}
let path = entry.path();
if !args.include.is_empty() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let ok = args.include.iter().any(|pat| {
if let Some(suf) = pat.strip_prefix("*.") {
name.ends_with(suf) || path.extension().and_then(|e| e.to_str()) == Some(suf)
} else {
name.contains(pat) || path.to_string_lossy().contains(pat)
}
});
if !ok {
continue;
}
}
files_seen += 1;
let max = global.effective_max_filesize();
let content = match crate::file_io::read_file_bytes(path, max) {
Ok(c) => c,
Err(_) => match std::fs::read(path) {
Ok(c) if (c.len() as u64) <= max => c,
_ => continue,
},
};
match crate::commands::outline::emit_outline_for_path(path, &content, None, writer) {
Ok(n) => items_emitted += n as u64,
Err(_) => continue,
}
}
writer.write_event(&SparseSummary {
r#type: "sparse_summary",
emitted: items_emitted.max(files_seen),
truncated,
elapsed_ms: start.elapsed().as_millis() as u64,
})?;
writer.write_event(&serde_json::json!({
"type": "sparse_outline_budget",
"files_seen": files_seen,
"items": items_emitted,
"truncated": truncated,
"max_files": args.max_files,
}))?;
Ok(())
}