High-performance implementation of plcbundle written in Rust

refactor CLI command structure

+3 -1
.gitignore
··· 1 1 target 2 - plcbundle-* 2 + plcbundle-go 3 + plcbundle-rs 4 + plcbundle 3 5 test_bundles 4 6 test_bundles* 5 7 .claude
+1 -1
Cargo.toml
··· 9 9 10 10 [[bin]] 11 11 name = "plcbundle-rs" 12 - path = "src/cli/plcbundle-rs.rs" 12 + path = "src/cli/mod.rs" 13 13 14 14 [lib] 15 15 crate-type = ["cdylib", "rlib"]
+9 -3
RULES.md
··· 46 46 Index::init(&dir, origin, force)?; 47 47 Index::rebuild_from_bundles(&dir, origin, callback)?; 48 48 49 - // ✅ CORRECT - Via BundleManager API 49 + // ❌ WRONG - Direct BundleManager::new (no helpful error messages) 50 + let manager = BundleManager::new(dir)?; 51 + 52 + // ✅ CORRECT - Via cli::utils::create_manager (with helpful errors) 53 + let manager = super::utils::create_manager(dir, verbose)?; 50 54 manager.load_bundle(num, options)?; 51 55 manager.get_operation_raw(bundle, pos)?; 52 56 manager.delete_bundle_files(&[num])?; 53 - manager.init_repository(origin, force)?; 54 - manager.rebuild_index(origin, callback)?; 57 + 58 + // For static operations (init/rebuild don't need existing repository) 59 + BundleManager::init_repository(origin, force)?; 60 + BundleManager::rebuild_index(origin, callback)?; 55 61 ``` 56 62 57 63 ## Architecture
+17 -3
src/cli/cmd_bench.rs
··· 3 3 use anyhow::Result; 4 4 use clap::Args; 5 5 use plcbundle::BundleManager; 6 - use std::io::{Read, Write}; 6 + use std::io::Read; 7 7 use std::path::PathBuf; 8 8 use std::time::Instant; 9 9 10 10 #[derive(Args)] 11 + #[command( 12 + about = "Benchmark bundle operations", 13 + after_help = "Examples:\n \ 14 + # Run all benchmarks with default iterations\n \ 15 + plcbundle bench\n\n \ 16 + # Benchmark specific operation\n \ 17 + plcbundle bench --op-read --iterations 1000\n\n \ 18 + # Benchmark DID lookup\n \ 19 + plcbundle bench --did-lookup -n 500\n\n \ 20 + # Run on specific bundle\n \ 21 + plcbundle bench --bundle 100\n\n \ 22 + # JSON output for analysis\n \ 23 + plcbundle bench --format json > benchmark.json" 24 + )] 11 25 pub struct BenchCommand { 12 26 /// Number of iterations for each benchmark 13 27 #[arg(short = 'n', long, default_value = "100")] ··· 107 121 } 108 122 109 123 pub fn run(cmd: BenchCommand, dir: PathBuf) -> Result<()> { 110 - let manager = BundleManager::new(dir.clone())?; 124 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 111 125 112 126 // Determine which benchmarks to run 113 127 let run_all = cmd.all ··· 331 345 }; 332 346 333 347 let mut processed = 0; 334 - for (i, &bundle_num) in bundles.iter().enumerate() { 348 + for (_i, &bundle_num) in bundles.iter().enumerate() { 335 349 let size = match bundle_compressed_size(manager, bundle_num)? { 336 350 Some(size) => size, 337 351 None => continue,
+113 -8
src/cli/cmd_did.rs
··· 1 1 // DID Resolution and Query commands 2 2 use anyhow::Result; 3 + use clap::{Args, Subcommand}; 3 4 use plcbundle::{BundleManager, DIDLookupStats, DIDLookupTimings}; 4 5 use std::path::PathBuf; 5 6 6 - // ============================================================================ 7 + #[derive(Args)] 8 + #[command( 9 + about = "DID operations and queries", 10 + long_about = "Query and analyze DIDs in the bundle repository. All commands\nrequire a DID index to be built for optimal performance.", 11 + after_help = "Examples:\n \ 12 + # Resolve DID to current document\n \ 13 + plcbundle did resolve did:plc:524tuhdhh3m7li5gycdn6boe\n\n \ 14 + # Show DID operation log\n \ 15 + plcbundle did log did:plc:524tuhdhh3m7li5gycdn6boe\n\n \ 16 + # Show complete audit log\n \ 17 + plcbundle did history did:plc:524tuhdhh3m7li5gycdn6boe" 18 + )] 19 + pub struct DidCommand { 20 + #[command(subcommand)] 21 + pub command: DIDCommands, 22 + } 23 + 24 + #[derive(Args)] 25 + #[command( 26 + about = "Resolve handle to DID", 27 + long_about = "Resolves an AT Protocol handle to its DID using the handle resolver." 28 + )] 29 + pub struct HandleCommand { 30 + /// Handle to resolve (e.g., tree.fail) 31 + pub handle: String, 32 + 33 + /// Handle resolver URL (defaults to quickdid.smokesignal.tools) 34 + #[arg(long)] 35 + pub handle_resolver: Option<String>, 36 + } 37 + 38 + #[derive(Subcommand)] 39 + pub enum DIDCommands { 40 + /// Resolve DID to current W3C DID document 41 + #[command(alias = "doc", alias = "document")] 42 + Resolve { 43 + /// DID or handle to resolve 44 + did: String, 45 + 46 + /// Handle resolver URL (e.g., https://quickdid.smokesignal.tools) 47 + #[arg(long)] 48 + handle_resolver: Option<String>, 49 + 50 + /// Verbose output 51 + #[arg(short, long)] 52 + verbose: bool, 53 + }, 54 + 55 + /// Show DID operation log 56 + #[command(alias = "lookup", alias = "find", alias = "get", alias = "history")] 57 + Log { 58 + /// DID to show log for 59 + did: String, 60 + 61 + /// Verbose output 62 + #[arg(short, long)] 63 + verbose: bool, 64 + 65 + /// Output as JSON 66 + #[arg(long)] 67 + json: bool, 68 + }, 69 + 70 + /// Process multiple DIDs from file or stdin (TODO) 71 + Batch { 72 + /// Action: lookup, resolve, export 73 + #[arg(long, default_value = "lookup")] 74 + action: String, 75 + 76 + /// Number of parallel workers 77 + #[arg(long, default_value = "4")] 78 + workers: usize, 79 + 80 + /// Output file 81 + #[arg(short, long)] 82 + output: Option<PathBuf>, 83 + 84 + /// Read from stdin 85 + #[arg(long)] 86 + stdin: bool, 87 + }, 88 + } 89 + 90 + pub fn run_did(cmd: DidCommand, dir: PathBuf) -> Result<()> { 91 + match cmd.command { 92 + DIDCommands::Resolve { 93 + did, 94 + handle_resolver, 95 + verbose, 96 + } => { 97 + cmd_did_resolve(dir, did, handle_resolver, verbose)?; 98 + } 99 + DIDCommands::Log { did, verbose, json } => { 100 + cmd_did_lookup(dir, did, verbose, json)?; 101 + } 102 + DIDCommands::Batch { 103 + action, 104 + workers, 105 + output, 106 + stdin, 107 + } => { 108 + cmd_did_batch(dir, action, workers, output, stdin)?; 109 + } 110 + } 111 + Ok(()) 112 + } 113 + 114 + pub fn run_handle(cmd: HandleCommand, dir: PathBuf) -> Result<()> { 115 + cmd_did_handle(dir, cmd.handle, cmd.handle_resolver)?; 116 + Ok(()) 117 + } 118 + 7 119 // DID RESOLVE - Convert DID to W3C DID Document 8 - // ============================================================================ 9 120 10 121 pub fn cmd_did_resolve( 11 122 dir: PathBuf, ··· 178 289 (hash % 256) as u8 179 290 } 180 291 181 - // ============================================================================ 182 292 // DID HANDLE - Resolve handle to DID 183 - // ============================================================================ 184 293 185 294 pub fn cmd_did_handle( 186 295 dir: PathBuf, ··· 209 318 Ok(()) 210 319 } 211 320 212 - // ============================================================================ 213 321 // DID LOOKUP - Find all operations for a DID 214 - // ============================================================================ 215 322 216 323 pub fn cmd_did_lookup(dir: PathBuf, input: String, verbose: bool, json: bool) -> Result<()> { 217 324 use plcbundle::constants; ··· 567 674 } 568 675 } 569 676 570 - // ============================================================================ 571 677 // DID BATCH - Process multiple DIDs (TODO) 572 - // ============================================================================ 573 678 574 679 pub fn cmd_did_batch( 575 680 _dir: PathBuf,
+2 -3
src/cli/cmd_diff.rs
··· 60 60 } 61 61 62 62 pub fn run(cmd: DiffCommand, dir: PathBuf) -> Result<()> { 63 - let manager = BundleManager::new(dir.clone())?; 63 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 64 64 65 65 let rt = Runtime::new()?; 66 66 ··· 249 249 }; 250 250 251 251 // Try to load bundle from target directory 252 - let target_manager = BundleManager::new(target_dir.to_path_buf()) 253 - .context("Failed to create manager for target directory")?; 252 + let target_manager = super::utils::create_manager(target_dir.to_path_buf(), false, false)?; 254 253 let target_result = target_manager 255 254 .load_bundle(bundle_num, plcbundle::LoadOptions::default()) 256 255 .context("Failed to load bundle from target directory")?;
+66 -21
src/cli/cmd_export.rs
··· 1 1 use anyhow::Result; 2 + use clap::{Args, ValueEnum}; 2 3 use indicatif::HumanDuration; 3 - use plcbundle::BundleManager; 4 4 use std::fs::File; 5 5 use std::io::{self, BufRead, BufReader, BufWriter, Write}; 6 6 use std::path::PathBuf; ··· 10 10 11 11 use super::progress::ProgressBar; 12 12 use super::utils; 13 - // ExportFormat is defined in plcbundle-rs.rs 14 - // Access it via the parent module 15 - use super::ExportFormat; 13 + 14 + #[derive(Args)] 15 + #[command(about = "Export operations to different formats")] 16 + pub struct ExportCommand { 17 + /// Bundle range (e.g., "1-100") 18 + #[arg(short, long)] 19 + pub range: Option<String>, 20 + 21 + /// Export all bundles 22 + #[arg(long)] 23 + pub all: bool, 24 + 25 + /// Bundle range (legacy, use --range instead) 26 + #[arg(long, hide = true)] 27 + pub bundles: Option<String>, 28 + 29 + /// Output format 30 + #[arg(short = 'f', long, default_value = "jsonl")] 31 + pub format: ExportFormat, 32 + 33 + /// Output file (default: stdout) 34 + #[arg(short, long)] 35 + pub output: Option<PathBuf>, 36 + 37 + /// Limit number of operations to export 38 + #[arg(long)] 39 + pub count: Option<usize>, 40 + 41 + /// Export operations after this timestamp (ISO 8601 format) 42 + #[arg(long)] 43 + pub after: Option<String>, 44 + 45 + /// Filter by DID 46 + #[arg(long)] 47 + pub did: Option<String>, 48 + 49 + /// Filter by operation type 50 + #[arg(long)] 51 + pub op_type: Option<String>, 52 + 53 + /// Compression 54 + #[arg(short = 'z', long)] 55 + pub compress: bool, 56 + } 16 57 17 - pub fn cmd_export( 18 - dir: PathBuf, 19 - range: Option<String>, 20 - all: bool, 21 - bundles: Option<String>, // Legacy flag 22 - format: ExportFormat, 23 - output: Option<PathBuf>, 24 - count: Option<usize>, 25 - after: Option<String>, 26 - did: Option<String>, 27 - op_type: Option<String>, 28 - _compress: bool, 29 - quiet: bool, 30 - verbose: bool, 31 - ) -> Result<()> { 58 + #[derive(Debug, Clone, ValueEnum)] 59 + pub enum ExportFormat { 60 + Jsonl, 61 + Json, 62 + Csv, 63 + Parquet, 64 + } 65 + 66 + pub fn run(cmd: ExportCommand, dir: PathBuf, quiet: bool, verbose: bool) -> Result<()> { 67 + let range = cmd.range; 68 + let all = cmd.all; 69 + let bundles = cmd.bundles; 70 + let format = cmd.format; 71 + let output = cmd.output; 72 + let count = cmd.count; 73 + let after = cmd.after; 74 + let did = cmd.did; 75 + let op_type = cmd.op_type; 76 + let _compress = cmd.compress; 32 77 // Create BundleManager (follows RULES.md - NO direct file access from CLI) 33 - let manager = BundleManager::new(dir.clone())?; 78 + let manager = super::utils::create_manager(dir.clone(), verbose, quiet)?; 34 79 let index = manager.get_index(); 35 80 let max_bundle = index.last_bundle; 36 81 ··· 130 175 let start = Instant::now(); 131 176 let mut exported_count = 0; 132 177 let mut bundles_processed = 0; 133 - let mut bytes_written = Arc::new(Mutex::new(0u64)); 178 + let bytes_written = Arc::new(Mutex::new(0u64)); 134 179 let mut output_buffer = String::with_capacity(1024 * 1024); // 1MB buffer 135 180 const BATCH_SIZE: usize = 10000; 136 181
+129 -7
src/cli/cmd_index.rs
··· 1 1 // DID Index CLI commands 2 2 use super::utils; 3 3 use anyhow::Result; 4 + use clap::{Args, Subcommand}; 4 5 use plcbundle::{BundleManager, constants}; 5 6 use std::path::PathBuf; 6 7 use std::time::Instant; 7 8 9 + #[derive(Args)] 10 + #[command( 11 + about = "DID index management", 12 + long_about = "Manage the DID position index which maps DIDs to their bundle locations.\nThis index enables fast O(1) DID lookups and is required for DID\nresolution and query operations.", 13 + after_help = "Examples:\n \ 14 + # Build DID position index\n \ 15 + plcbundle index build\n\n \ 16 + # Repair DID index (rebuild from bundles)\n \ 17 + plcbundle index repair\n\n \ 18 + # Show DID index statistics\n \ 19 + plcbundle index stats\n\n \ 20 + # Verify DID index integrity\n \ 21 + plcbundle index verify" 22 + )] 23 + pub struct IndexCommand { 24 + #[command(subcommand)] 25 + pub command: IndexCommands, 26 + } 27 + 28 + #[derive(Subcommand)] 29 + pub enum IndexCommands { 30 + /// Build DID position index 31 + #[command(after_help = "Examples:\n \ 32 + # Build index\n \ 33 + plcbundle index build\n\n \ 34 + # Force rebuild from scratch\n \ 35 + plcbundle index build --force")] 36 + Build { 37 + /// Rebuild even if index exists 38 + #[arg(long)] 39 + force: bool, 40 + }, 41 + 42 + /// Repair DID index 43 + #[command(alias = "rebuild")] 44 + Repair {}, 45 + 46 + /// Show DID index statistics 47 + #[command(alias = "info")] 48 + Stats { 49 + /// Output as JSON 50 + #[arg(long)] 51 + json: bool, 52 + }, 53 + 54 + /// Verify DID index integrity 55 + #[command(alias = "check")] 56 + Verify { 57 + /// Verbose output 58 + #[arg(short, long)] 59 + verbose: bool, 60 + }, 61 + 62 + /// Debug and inspect DID index internals 63 + #[command(alias = "inspect")] 64 + Debug { 65 + /// Show specific shard (0-255 or hex like 0xac) 66 + #[arg(short, long)] 67 + shard: Option<String>, 68 + 69 + /// Output as JSON 70 + #[arg(long)] 71 + json: bool, 72 + }, 73 + 74 + /// Compact delta segments in DID index 75 + #[command(after_help = "Examples:\n \ 76 + # Compact all shards\n \ 77 + plcbundle index compact\n\n \ 78 + # Compact specific shards\n \ 79 + plcbundle index compact --shards 0xac 0x12 0xff")] 80 + Compact { 81 + /// Specific shards to compact (0-255 or hex like 0xac) 82 + #[arg(short, long, value_delimiter = ' ')] 83 + shards: Option<Vec<String>>, 84 + }, 85 + } 86 + 87 + pub fn run(cmd: IndexCommand, dir: PathBuf) -> Result<()> { 88 + match cmd.command { 89 + IndexCommands::Build { force } => { 90 + cmd_index_build(dir, force)?; 91 + } 92 + IndexCommands::Repair {} => { 93 + cmd_index_repair(dir)?; 94 + } 95 + IndexCommands::Stats { json } => { 96 + cmd_index_stats(dir, json)?; 97 + } 98 + IndexCommands::Verify { verbose } => { 99 + cmd_index_verify(dir, verbose)?; 100 + } 101 + IndexCommands::Debug { shard, json } => { 102 + let shard_num = shard.map(|s| parse_shard(&s)).transpose()?; 103 + cmd_index_debug(dir, shard_num, json)?; 104 + } 105 + IndexCommands::Compact { shards } => { 106 + let shard_nums = shards 107 + .map(|shard_list| { 108 + shard_list 109 + .iter() 110 + .map(|s| parse_shard(s)) 111 + .collect::<Result<Vec<u8>>>() 112 + }) 113 + .transpose()?; 114 + cmd_index_compact(dir, shard_nums)?; 115 + } 116 + } 117 + Ok(()) 118 + } 119 + 120 + /// Parse shard number from string (supports hex 0xac or decimal) 121 + fn parse_shard(s: &str) -> Result<u8> { 122 + if s.starts_with("0x") || s.starts_with("0X") { 123 + u8::from_str_radix(&s[2..], 16) 124 + .map_err(|_| anyhow::anyhow!("Invalid shard number: {}", s)) 125 + } else { 126 + s.parse::<u8>() 127 + .map_err(|_| anyhow::anyhow!("Invalid shard number: {}", s)) 128 + } 129 + } 130 + 8 131 pub fn cmd_index_build(dir: PathBuf, force: bool) -> Result<()> { 9 - let manager = BundleManager::new(dir.clone())?; 132 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 10 133 11 134 // Check if index exists 12 135 let did_index = manager.get_did_index_stats(); ··· 63 186 } 64 187 65 188 pub fn cmd_index_repair(dir: PathBuf) -> Result<()> { 66 - let manager = BundleManager::new(dir.clone())?; 189 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 67 190 68 191 // Check if index config exists (even if corrupted) 69 192 let did_index = manager.get_did_index(); ··· 116 239 } 117 240 118 241 pub fn cmd_index_stats(dir: PathBuf, json: bool) -> Result<()> { 119 - let manager = BundleManager::new(dir.clone())?; 242 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 120 243 121 244 // Get raw stats from did_index 122 245 let did_index = manager.get_did_index(); ··· 472 595 } 473 596 474 597 pub fn cmd_index_verify(dir: PathBuf, verbose: bool) -> Result<()> { 475 - let manager = BundleManager::new(dir.clone())?; 598 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 476 599 477 600 let did_index = manager.get_did_index(); 478 601 let stats_map = did_index.read().unwrap().get_stats(); ··· 724 847 } 725 848 726 849 pub fn cmd_index_debug(dir: PathBuf, shard: Option<u8>, json: bool) -> Result<()> { 727 - let manager = BundleManager::new(dir.clone())?; 850 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 728 851 729 852 let did_index = manager.get_did_index(); 730 853 let stats_map = did_index.read().unwrap().get_stats(); ··· 952 1075 Ok(()) 953 1076 } 954 1077 955 - #[allow(dead_code)] 956 1078 pub fn cmd_index_compact(dir: PathBuf, shards: Option<Vec<u8>>) -> Result<()> { 957 - let manager = BundleManager::new(dir.clone())?; 1079 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 958 1080 959 1081 let did_index = manager.get_did_index(); 960 1082 let stats_map = did_index.read().unwrap().get_stats();
+13
src/cli/cmd_init.rs
··· 4 4 use std::path::PathBuf; 5 5 6 6 #[derive(Args)] 7 + #[command( 8 + about = "Initialize a new PLC bundle repository", 9 + long_about = "Creates a new repository with an empty index file. Similar to 'git init'.", 10 + after_help = "Examples:\n \ 11 + # Initialize in current directory\n \ 12 + plcbundle init\n\n \ 13 + # Initialize in specific directory\n \ 14 + plcbundle init /path/to/bundles\n\n \ 15 + # Set custom origin identifier\n \ 16 + plcbundle init --origin my-node\n\n \ 17 + # Force reinitialize existing repository\n \ 18 + plcbundle init --force" 19 + )] 7 20 pub struct InitCommand { 8 21 /// Directory to initialize (default: current directory) 9 22 #[arg(default_value = ".")]
+14 -2
src/cli/cmd_inspect.rs
··· 21 21 • Service endpoint analysis\n \ 22 22 • Temporal distribution\n \ 23 23 • Size analysis\n\n\ 24 - Can inspect either by bundle number (from repository) or direct file path." 24 + Can inspect either by bundle number (from repository) or direct file path.", 25 + after_help = "Examples:\n \ 26 + # Inspect from repository\n \ 27 + plcbundle inspect 42\n\n \ 28 + # Inspect specific file\n \ 29 + plcbundle inspect /path/to/000042.jsonl.zst\n \ 30 + plcbundle inspect 000042.jsonl.zst\n\n \ 31 + # Skip certain analysis sections\n \ 32 + plcbundle inspect 42 --skip-patterns\n\n \ 33 + # Show sample operations\n \ 34 + plcbundle inspect 42 --samples --sample-count 20\n\n \ 35 + # JSON output (for scripting)\n \ 36 + plcbundle inspect 42 --json" 25 37 )] 26 38 pub struct InspectCommand { 27 39 /// Bundle number or file path to inspect ··· 165 177 } 166 178 167 179 pub fn run(cmd: InspectCommand, dir: PathBuf) -> Result<()> { 168 - let manager = BundleManager::new(dir.clone())?; 180 + let manager = super::utils::create_manager(dir.clone(), false, false)?; 169 181 170 182 // Resolve target to bundle number or file path 171 183 let (bundle_num, file_path) = resolve_target(&cmd.target, &dir)?;
+22 -3
src/cli/cmd_ls.rs
··· 1 1 use anyhow::Result; 2 2 use clap::Args; 3 - use plcbundle::BundleManager; 4 3 use plcbundle::format::format_duration_compact; 5 4 use std::path::PathBuf; 6 5 7 6 #[derive(Args)] 7 + #[command( 8 + about = "List bundles (machine-readable)", 9 + after_help = "Examples:\n \ 10 + # List all bundles\n \ 11 + plcbundle ls\n\n \ 12 + # Last 10 bundles\n \ 13 + plcbundle ls -n 10\n\n \ 14 + # Oldest first\n \ 15 + plcbundle ls --reverse\n\n \ 16 + # Custom format\n \ 17 + plcbundle ls --format \"bundle,hash,date,size\"\n\n \ 18 + # CSV format\n \ 19 + plcbundle ls --separator \",\"\n\n \ 20 + # Scripting examples\n \ 21 + plcbundle ls | awk '{print $1}' # Just bundle numbers\n \ 22 + plcbundle ls | grep 000150 # Find specific bundle\n \ 23 + plcbundle ls -n 5 | cut -f1,4 # First and 4th columns\n \ 24 + plcbundle ls --format bundle,hash # Custom columns\n \ 25 + plcbundle ls --separator \",\" > bundles.csv # Export to CSV" 26 + )] 8 27 pub struct LsCommand { 9 28 /// Show only last N bundles (0 = all) 10 29 #[arg(short = 'n', long, default_value = "0")] ··· 27 46 pub separator: String, 28 47 } 29 48 30 - pub fn run(cmd: LsCommand, dir: PathBuf) -> Result<()> { 31 - let manager = BundleManager::new(dir)?; 49 + pub fn run(cmd: LsCommand, dir: PathBuf, verbose: bool, quiet: bool) -> Result<()> { 50 + let manager = super::utils::create_manager(dir, verbose, quiet)?; 32 51 33 52 // Get all bundle metadata from the index 34 53 let bundles = super::utils::get_all_bundle_metadata(&manager);
+23 -1
src/cli/cmd_mempool.rs
··· 1 1 // src/bin/commands/mempool.rs 2 2 use super::utils; 3 + use super::utils::HasGlobalFlags; 3 4 use anyhow::Result; 4 5 use clap::{Args, Subcommand}; 5 6 use plcbundle::format::format_number; ··· 8 9 use std::path::PathBuf; 9 10 10 11 #[derive(Args)] 12 + #[command( 13 + about = "Manage mempool operations", 14 + long_about = "The mempool stores operations waiting to be bundled. It maintains\nstrict chronological order and automatically validates consistency.", 15 + alias = "mp", 16 + after_help = "Examples:\n \ 17 + # Show mempool status\n \ 18 + plcbundle mempool\n \ 19 + plcbundle mempool status\n\n \ 20 + # Clear all operations\n \ 21 + plcbundle mempool clear\n\n \ 22 + # Export operations as JSONL\n \ 23 + plcbundle mempool dump\n \ 24 + plcbundle mempool dump > operations.jsonl\n\n \ 25 + # Using alias\n \ 26 + plcbundle mp status" 27 + )] 11 28 pub struct MempoolCommand { 12 29 #[command(subcommand)] 13 30 pub command: Option<MempoolSubcommand>, ··· 48 65 }, 49 66 } 50 67 68 + impl HasGlobalFlags for MempoolCommand { 69 + fn verbose(&self) -> bool { self.verbose } 70 + fn quiet(&self) -> bool { false } 71 + } 72 + 51 73 pub fn run(cmd: MempoolCommand) -> Result<()> { 52 - let manager = utils::create_manager(cmd.dir.clone(), cmd.verbose)?; 74 + let manager = utils::create_manager_from_cmd(cmd.dir.clone(), &cmd)?; 53 75 54 76 match cmd.command { 55 77 Some(MempoolSubcommand::Status { verbose }) => {
+14 -7
src/cli/cmd_migrate.rs
··· 1 1 // Migrate command - convert bundles to multi-frame format 2 2 use super::progress::ProgressBar; 3 - use super::utils::format_bytes; 3 + use super::utils::{format_bytes, HasGlobalFlags}; 4 4 use anyhow::{Result, bail}; 5 5 use clap::Args; 6 6 use plcbundle::BundleManager; ··· 57 57 pub verbose: bool, 58 58 } 59 59 60 - pub fn run(cmd: MigrateCommand, dir: PathBuf) -> Result<()> { 61 - let manager = super::utils::create_manager(dir.clone(), cmd.verbose)?; 60 + impl HasGlobalFlags for MigrateCommand { 61 + fn verbose(&self) -> bool { self.verbose } 62 + fn quiet(&self) -> bool { false } 63 + } 64 + 65 + pub fn run(mut cmd: MigrateCommand, dir: PathBuf, global_verbose: bool) -> Result<()> { 66 + // Merge global verbose flag with command's verbose flag 67 + cmd.verbose = cmd.verbose || global_verbose; 68 + let manager = super::utils::create_manager_from_cmd(dir.clone(), &cmd)?; 62 69 63 70 // Auto-detect number of workers if 0 64 - let workers = super::utils::get_num_workers(cmd.workers, 4); 71 + let workers = super::utils::get_worker_threads(cmd.workers, 4); 65 72 66 73 eprintln!("Scanning for legacy bundles in: {}\n", dir.display()); 67 74 ··· 204 211 205 212 // Only update progress bar periodically to reduce lock contention 206 213 if current_count % update_interval == 0 || current_count == 1 { 207 - let mut prog = progress_arc.lock().unwrap(); 214 + let prog = progress_arc.lock().unwrap(); 208 215 prog.set_with_bytes(current_count, total_bytes); 209 216 } 210 217 ··· 225 232 let total_bytes = bytes_atomic.fetch_add(info.old_size, Ordering::Relaxed) + info.old_size; 226 233 227 234 // Update every bundle in sequential mode (no contention) 228 - let mut prog = progress_arc.lock().unwrap(); 235 + let prog = progress_arc.lock().unwrap(); 229 236 prog.set_with_bytes(current_count, total_bytes); 230 237 231 238 (info, result) ··· 285 292 { 286 293 let final_count = count_atomic.load(Ordering::Relaxed); 287 294 let final_bytes = bytes_atomic.load(Ordering::Relaxed); 288 - let mut prog = progress_arc.lock().unwrap(); 295 + let prog = progress_arc.lock().unwrap(); 289 296 prog.set_with_bytes(final_count, final_bytes); 290 297 prog.finish(); 291 298 }
+106 -4
src/cli/cmd_op.rs
··· 1 1 use anyhow::Result; 2 - use plcbundle::{BundleManager, LoadOptions, constants}; 2 + use clap::{Args, Subcommand}; 3 + use plcbundle::{LoadOptions, constants}; 3 4 use std::path::PathBuf; 4 5 use std::time::Instant; 5 6 7 + #[derive(Args)] 8 + #[command( 9 + about = "Operation queries and inspection", 10 + long_about = "Direct access to individual operations within bundles using either:\n • Bundle number + position (e.g., 42 1337)\n • Global position (e.g., 420000)\n\nGlobal position format: (bundleNumber × 10,000) + position\nExample: 88410345 = bundle 8841, position 345", 11 + alias = "operation", 12 + alias = "record", 13 + after_help = "Examples:\n \ 14 + # Get operation as JSON\n \ 15 + plcbundle op get 42 1337\n \ 16 + plcbundle op get 420000\n\n \ 17 + # Show operation (formatted)\n \ 18 + plcbundle op show 42 1337\n \ 19 + plcbundle op show 88410345\n\n \ 20 + # Find by CID\n \ 21 + plcbundle op find bafyreig3..." 22 + )] 23 + pub struct OpCommand { 24 + #[command(subcommand)] 25 + pub command: OpCommands, 26 + } 27 + 28 + #[derive(Subcommand)] 29 + pub enum OpCommands { 30 + /// Get operation as JSON (machine-readable) 31 + /// 32 + /// Supports two input formats: 33 + /// 1. Bundle number + position: get 42 1337 34 + /// 2. Global position: get 420000 35 + /// 36 + /// Global position = (bundleNumber × 10,000) + position 37 + #[command(after_help = "Examples:\n \ 38 + # By bundle + position\n \ 39 + plcbundle op get 42 1337\n\n \ 40 + # By global position\n \ 41 + plcbundle op get 88410345\n\n \ 42 + # Pipe to jq\n \ 43 + plcbundle op get 42 1337 | jq .did")] 44 + Get { 45 + /// Bundle number (or global position if only one arg) 46 + bundle: u32, 47 + 48 + /// Operation position within bundle (optional if using global position) 49 + position: Option<usize>, 50 + }, 51 + 52 + /// Show operation with formatted output 53 + /// 54 + /// Displays operation in human-readable format with: 55 + /// • Bundle location and global position 56 + /// • DID and CID 57 + /// • Timestamp 58 + /// • Nullification status 59 + /// • Parsed operation details 60 + /// • Performance metrics (when not quiet) 61 + #[command(after_help = "Examples:\n \ 62 + # By bundle + position\n \ 63 + plcbundle op show 42 1337\n\n \ 64 + # By global position\n \ 65 + plcbundle op show 88410345\n\n \ 66 + # Quiet mode (minimal output)\n \ 67 + plcbundle op show 42 1337 -q")] 68 + Show { 69 + /// Bundle number (or global position if only one arg) 70 + bundle: u32, 71 + 72 + /// Operation position within bundle (optional if using global position) 73 + position: Option<usize>, 74 + }, 75 + 76 + /// Find operation by CID across all bundles 77 + /// 78 + /// Searches the entire repository for an operation with the given CID 79 + /// and returns its location (bundle + position). 80 + /// 81 + /// Note: This performs a full scan and can be slow on large repositories. 82 + #[command(after_help = "Examples:\n \ 83 + # Find by CID\n \ 84 + plcbundle op find bafyreig3tg4k...\n\n \ 85 + # Use with op get\n \ 86 + plcbundle op find bafyreig3... | awk '{print $3, $5}' | xargs plcbundle op get")] 87 + Find { 88 + /// CID to search for 89 + cid: String, 90 + }, 91 + } 92 + 93 + pub fn run(cmd: OpCommand, dir: PathBuf, quiet: bool) -> Result<()> { 94 + match cmd.command { 95 + OpCommands::Get { bundle, position } => { 96 + cmd_op_get(dir, bundle, position, quiet)?; 97 + } 98 + OpCommands::Show { bundle, position } => { 99 + cmd_op_show(dir, bundle, position, quiet)?; 100 + } 101 + OpCommands::Find { cid } => { 102 + cmd_op_find(dir, cid, quiet)?; 103 + } 104 + } 105 + Ok(()) 106 + } 107 + 6 108 /// Parse operation position - supports both global position and bundle + position 7 109 /// Mimics Go version: small numbers (< 10000) are treated as bundle 1, position N 8 110 pub fn parse_op_position(bundle: u32, position: Option<usize>) -> (u32, usize) { ··· 31 133 pub fn cmd_op_get(dir: PathBuf, bundle: u32, position: Option<usize>, quiet: bool) -> Result<()> { 32 134 let (bundle_num, op_index) = parse_op_position(bundle, position); 33 135 34 - let manager = BundleManager::new(dir)?; 136 + let manager = super::utils::create_manager(dir, false, quiet)?; 35 137 36 138 if quiet { 37 139 // Just output JSON - no stats ··· 61 163 pub fn cmd_op_show(dir: PathBuf, bundle: u32, position: Option<usize>, quiet: bool) -> Result<()> { 62 164 let (bundle_num, op_index) = parse_op_position(bundle, position); 63 165 64 - let manager = BundleManager::new(dir)?; 166 + let manager = super::utils::create_manager(dir, false, quiet)?; 65 167 66 168 // Use the new get_operation API instead of loading entire bundle 67 169 let load_start = Instant::now(); ··· 186 288 } 187 289 188 290 pub fn cmd_op_find(dir: PathBuf, cid: String, quiet: bool) -> Result<()> { 189 - let manager = BundleManager::new(dir)?; 291 + let manager = super::utils::create_manager(dir, false, quiet)?; 190 292 let last_bundle = manager.get_last_bundle(); 191 293 192 294 if !quiet {
+88 -34
src/cli/cmd_query.rs
··· 1 1 use anyhow::Result; 2 + use clap::{Args, ValueEnum}; 2 3 use plcbundle::*; 3 4 use std::io::Write; 4 5 use std::path::PathBuf; ··· 7 8 8 9 use super::progress::ProgressBar as CustomProgressBar; 9 10 use super::utils; 10 - // QueryModeArg and OutputFormat are defined in plcbundle-rs.rs 11 - // Access them via the parent module 12 - use super::{OutputFormat, QueryModeArg}; 11 + 12 + #[derive(Args)] 13 + #[command( 14 + about = "Query bundles with JMESPath or simple path", 15 + alias = "q" 16 + )] 17 + pub struct QueryCommand { 18 + /// Query expression (e.g., "did", "operation.type", etc.) 19 + pub expression: String, 20 + 21 + /// Bundle range (e.g., "1-10,15,20-25" or "latest:10" for last 10) 22 + #[arg(short, long)] 23 + pub bundles: Option<String>, 24 + 25 + /// Number of threads (0 = auto) 26 + #[arg(short = 'j', long, default_value = "0")] 27 + pub threads: usize, 28 + 29 + /// Query mode 30 + #[arg(short = 'm', long, default_value = "auto")] 31 + pub mode: QueryModeArg, 32 + 33 + /// Batch size for output 34 + #[arg(long, default_value = "2000")] 35 + pub batch_size: usize, 36 + 37 + /// Output format 38 + #[arg(short = 'f', long, default_value = "jsonl")] 39 + pub format: OutputFormat, 40 + 41 + /// Output file (default: stdout) 42 + #[arg(short, long)] 43 + pub output: Option<PathBuf>, 13 44 14 - pub struct StdoutHandler { 15 - lock: Mutex<()>, 16 - stats_only: bool, 45 + /// Show statistics only, don't output results 46 + #[arg(long)] 47 + pub stats_only: bool, 17 48 } 18 49 19 - impl StdoutHandler { 20 - pub fn new(stats_only: bool) -> Self { 21 - Self { 22 - lock: Mutex::new(()), 23 - stats_only, 24 - } 25 - } 50 + #[derive(Debug, Clone, ValueEnum)] 51 + pub enum QueryModeArg { 52 + /// Auto-detect based on query 53 + Auto, 54 + /// Simple path mode (faster) 55 + Simple, 56 + /// JMESPath mode (flexible) 57 + Jmespath, 26 58 } 27 59 28 - impl OutputHandler for StdoutHandler { 29 - fn write_batch(&self, batch: &str) -> Result<()> { 30 - if self.stats_only { 31 - return Ok(()); 32 - } 33 - let _lock = self.lock.lock().unwrap(); 34 - std::io::stdout().write_all(batch.as_bytes())?; 35 - Ok(()) 36 - } 60 + #[derive(Debug, Clone, ValueEnum)] 61 + pub enum OutputFormat { 62 + /// JSON Lines (one per line) 63 + Jsonl, 64 + /// Pretty JSON 65 + Json, 66 + /// CSV 67 + Csv, 68 + /// Plain text (values only) 69 + Plain, 37 70 } 38 71 39 - pub fn cmd_query( 40 - expression: String, 41 - dir: PathBuf, 42 - bundles_spec: Option<String>, 43 - threads: usize, 44 - mode: QueryModeArg, 45 - batch_size: usize, 46 - _format: OutputFormat, 47 - _output: Option<PathBuf>, 72 + pub struct StdoutHandler { 73 + lock: Mutex<()>, 48 74 stats_only: bool, 49 - quiet: bool, 50 - verbose: bool, 51 - ) -> Result<()> { 75 + } 76 + 77 + pub fn run(cmd: QueryCommand, dir: PathBuf, quiet: bool, verbose: bool) -> Result<()> { 78 + let expression = cmd.expression; 79 + let bundles_spec = cmd.bundles; 80 + let threads = cmd.threads; 81 + let mode = cmd.mode; 82 + let batch_size = cmd.batch_size; 83 + let _format = cmd.format; 84 + let _output = cmd.output; 85 + let stats_only = cmd.stats_only; 52 86 let num_threads = if threads == 0 { 53 87 std::thread::available_parallelism() 54 88 .map(|n| n.get()) ··· 185 219 186 220 Ok(()) 187 221 } 222 + 223 + impl StdoutHandler { 224 + pub fn new(stats_only: bool) -> Self { 225 + Self { 226 + lock: Mutex::new(()), 227 + stats_only, 228 + } 229 + } 230 + } 231 + 232 + impl OutputHandler for StdoutHandler { 233 + fn write_batch(&self, batch: &str) -> Result<()> { 234 + if self.stats_only { 235 + return Ok(()); 236 + } 237 + let _lock = self.lock.lock().unwrap(); 238 + std::io::stdout().write_all(batch.as_bytes())?; 239 + Ok(()) 240 + } 241 + }
+2 -3
src/cli/cmd_random.rs
··· 1 1 use anyhow::Result; 2 2 use clap::Args; 3 - use plcbundle::BundleManager; 4 3 use std::path::PathBuf; 5 4 6 - /// Sample random DIDs without touching bundle files. 7 5 #[derive(Args, Debug)] 6 + #[command(about = "Output random DIDs sampled from the index")] 8 7 pub struct RandomCommand { 9 8 /// Number of random DIDs to output 10 9 #[arg(short = 'n', long = "count", default_value = "10")] ··· 20 19 } 21 20 22 21 pub fn run(cmd: RandomCommand, dir: PathBuf) -> Result<()> { 23 - let manager = BundleManager::new(dir)?; 22 + let manager = super::utils::create_manager(dir, false, false)?; 24 23 let count = cmd.count.max(1); 25 24 let dids = manager.sample_random_dids(count, cmd.seed)?; 26 25
+9 -2
src/cli/cmd_rebuild.rs
··· 1 1 // Rebuild plc_bundles.json from existing bundle files 2 2 use super::progress::ProgressBar; 3 - use super::utils::format_bytes; 3 + use super::utils::{format_bytes, HasGlobalFlags}; 4 4 use anyhow::Result; 5 5 use clap::Args; 6 6 use plcbundle::BundleManager; ··· 48 48 pub verbose: bool, 49 49 } 50 50 51 - pub fn run(cmd: RebuildCommand, dir: PathBuf) -> Result<()> { 51 + impl HasGlobalFlags for RebuildCommand { 52 + fn verbose(&self) -> bool { self.verbose } 53 + fn quiet(&self) -> bool { false } 54 + } 55 + 56 + pub fn run(mut cmd: RebuildCommand, dir: PathBuf, global_verbose: bool) -> Result<()> { 57 + // Merge global verbose flag with command's verbose flag 58 + cmd.verbose = cmd.verbose || global_verbose; 52 59 eprintln!("Rebuilding bundle index from: {}\n", dir.display()); 53 60 54 61 let start = Instant::now();
+15 -1
src/cli/cmd_rollback.rs
··· 6 6 use std::path::PathBuf; 7 7 8 8 #[derive(Args)] 9 + #[command( 10 + about = "Rollback repository to earlier state", 11 + after_help = "Examples:\n \ 12 + # Rollback TO bundle 100 (keeps 1-100, removes 101+)\n \ 13 + plcbundle rollback --to 100\n\n \ 14 + # Remove last 5 bundles\n \ 15 + plcbundle rollback --last 5\n\n \ 16 + # Rollback without confirmation\n \ 17 + plcbundle rollback --to 50 --force\n\n \ 18 + # Rollback and rebuild DID index\n \ 19 + plcbundle rollback --to 100 --rebuild-did-index\n\n \ 20 + # Rollback but keep bundle files (index-only)\n \ 21 + plcbundle rollback --to 100 --keep-files" 22 + )] 9 23 pub struct RollbackCommand { 10 24 /// Rollback TO this bundle (keeps it) 11 25 #[arg(long)] ··· 46 60 47 61 pub fn run(cmd: RollbackCommand, dir: PathBuf) -> Result<()> { 48 62 // Step 1: Validate options and calculate plan 49 - let mut manager = BundleManager::new(dir.clone())?; 63 + let mut manager = super::utils::create_manager(dir.clone(), false, false)?; 50 64 let plan = calculate_rollback_plan(&manager, &cmd)?; 51 65 52 66 // Step 2: Display plan and get confirmation
-2
src/cli/cmd_server.rs
··· 7 7 #[cfg(feature = "server")] 8 8 use super::utils; 9 9 #[cfg(feature = "server")] 10 - use chrono::Utc; 11 - #[cfg(feature = "server")] 12 10 use plcbundle::BundleManager; 13 11 #[cfg(feature = "server")] 14 12 use plcbundle::server::{Server, ServerConfig};
+48 -3
src/cli/cmd_stats.rs
··· 1 - // Stats command implementation will be moved here 2 - // For now, this is handled in main.rs 3 - // TODO: Extract the full stats implementation 1 + use anyhow::Result; 2 + use clap::{Args, ValueEnum}; 3 + use std::path::PathBuf; 4 + 5 + #[derive(Args)] 6 + #[command(about = "Display statistics about bundles")] 7 + pub struct StatsCommand { 8 + /// Bundle range 9 + #[arg(short, long)] 10 + pub bundles: Option<String>, 11 + 12 + /// Statistics type 13 + #[arg(short = 't', long, default_value = "summary")] 14 + pub stat_type: StatType, 15 + 16 + /// Output format 17 + #[arg(short = 'f', long, default_value = "human")] 18 + pub format: InfoFormat, 19 + } 20 + 21 + #[derive(Debug, Clone, ValueEnum)] 22 + pub enum StatType { 23 + /// Summary statistics 24 + Summary, 25 + /// Operation type distribution 26 + Operations, 27 + /// DID statistics 28 + Dids, 29 + /// Timeline statistics 30 + Timeline, 31 + } 32 + 33 + #[derive(Debug, Clone, ValueEnum)] 34 + pub enum InfoFormat { 35 + /// Human-readable output 36 + Human, 37 + /// JSON output 38 + Json, 39 + /// YAML output 40 + Yaml, 41 + /// Table format 42 + Table, 43 + } 44 + 45 + pub fn run(_cmd: StatsCommand, _dir: PathBuf) -> Result<()> { 46 + println!("Stats not yet implemented"); 47 + Ok(()) 48 + }
+19 -4
src/cli/cmd_status.rs
··· 3 3 use plcbundle::*; 4 4 use std::path::PathBuf; 5 5 6 + use super::cmd_stats::InfoFormat; 6 7 use super::utils; 7 8 8 9 #[derive(Parser)] 10 + #[command( 11 + about = "Show comprehensive repository status", 12 + long_about = "Displays an overview of the repository including bundle statistics,\nDID index status, mempool state, and health recommendations.", 13 + alias = "info", 14 + after_help = "Examples:\n \ 15 + # Show repository status\n \ 16 + plcbundle status\n\n \ 17 + # Show detailed status with recent bundles\n \ 18 + plcbundle status --detailed\n\n \ 19 + # JSON output for scripting\n \ 20 + plcbundle status --format json\n\n \ 21 + # Using legacy 'info' alias\n \ 22 + plcbundle info" 23 + )] 9 24 pub struct StatusCommand { 10 25 /// Show detailed information 11 26 #[arg(short, long)] ··· 13 28 14 29 /// Output format 15 30 #[arg(short = 'f', long, default_value = "human")] 16 - pub format: super::InfoFormat, 31 + pub format: InfoFormat, 17 32 } 18 33 19 34 pub fn run(cmd: StatusCommand, dir: PathBuf) -> Result<()> { 20 - let manager = utils::create_manager(dir.clone(), false)?; 35 + let manager = utils::create_manager(dir.clone(), false, false)?; 21 36 let index = manager.get_index(); 22 37 23 38 match cmd.format { 24 - super::InfoFormat::Human => print_human_status(&manager, &index, &dir, cmd.detailed)?, 25 - super::InfoFormat::Json => print_json_status(&manager, &index, &dir)?, 39 + InfoFormat::Human => print_human_status(&manager, &index, &dir, cmd.detailed)?, 40 + InfoFormat::Json => print_json_status(&manager, &index, &dir)?, 26 41 _ => { 27 42 anyhow::bail!("Only 'human' and 'json' formats are supported"); 28 43 }
+23 -2
src/cli/cmd_sync.rs
··· 1 1 use super::utils; 2 + use super::utils::HasGlobalFlags; 2 3 use anyhow::Result; 3 4 use clap::Args; 4 5 use plcbundle::{ 5 - BundleManager, constants, 6 + constants, 6 7 sync::{CliLogger, PLCClient, ServerLogger, SyncConfig, SyncManager}, 7 8 }; 8 9 use std::path::PathBuf; ··· 10 11 use std::time::Duration; 11 12 12 13 #[derive(Args)] 14 + #[command( 15 + about = "Fetch new bundles from PLC directory", 16 + long_about = "Download new operations from the PLC directory and create bundles.\nSimilar to 'git fetch' - updates your local repository with new data.", 17 + alias = "fetch", 18 + after_help = "Examples:\n \ 19 + # Fetch new bundles once\n \ 20 + plcbundle sync\n\n \ 21 + # Run continuously (daemon mode)\n \ 22 + plcbundle sync --continuous\n\n \ 23 + # Custom sync interval\n \ 24 + plcbundle sync --continuous --interval 30s\n\n \ 25 + # Fetch maximum 10 bundles then stop\n \ 26 + plcbundle sync --max-bundles 10" 27 + )] 13 28 pub struct SyncCommand { 14 29 /// PLC directory URL 15 30 #[arg(long, default_value = constants::DEFAULT_PLC_DIRECTORY_URL)] ··· 40 55 pub quiet: bool, 41 56 } 42 57 58 + impl HasGlobalFlags for SyncCommand { 59 + fn verbose(&self) -> bool { self.verbose } 60 + fn quiet(&self) -> bool { self.quiet } 61 + } 62 + 43 63 fn parse_duration(s: &str) -> Result<Duration, String> { 44 64 if let Some(s) = s.strip_suffix('s') { 45 65 s.parse::<u64>() ··· 68 88 } 69 89 70 90 let client = PLCClient::new(&cmd.plc)?; 71 - let manager = Arc::new(BundleManager::new(cmd.dir)?.with_verbose(cmd.verbose)); 91 + let dir = cmd.dir.clone(); 92 + let manager = Arc::new(super::utils::create_manager_from_cmd(dir, &cmd)?); 72 93 73 94 let config = SyncConfig { 74 95 plc_url: cmd.plc.clone(),
+25 -3
src/cli/cmd_verify.rs
··· 1 1 use super::progress::ProgressBar; 2 - use super::utils::{format_bytes, format_number, parse_bundle_range_simple}; 2 + use super::utils::{format_bytes, format_number, parse_bundle_range_simple, HasGlobalFlags}; 3 3 use anyhow::{Result, bail}; 4 4 use clap::Args; 5 5 use plcbundle::{BundleManager, VerifySpec}; ··· 7 7 use std::time::Instant; 8 8 9 9 #[derive(Args)] 10 + #[command( 11 + about = "Verify bundle integrity and chain", 12 + after_help = "Examples:\n \ 13 + # Verify entire chain\n \ 14 + plcbundle verify\n \ 15 + plcbundle verify --chain\n\n \ 16 + # Verify specific bundle\n \ 17 + plcbundle verify --bundle 42\n\n \ 18 + # Verify range of bundles\n \ 19 + plcbundle verify --range 1-100\n\n \ 20 + # Verbose output\n \ 21 + plcbundle verify --chain -v\n\n \ 22 + # Parallel verification (faster for ranges)\n \ 23 + plcbundle verify --range 1-1000 --parallel --workers 8" 24 + )] 10 25 pub struct VerifyCommand { 11 26 /// Verify specific bundle number 12 27 #[arg(short, long)] ··· 45 60 pub threads: usize, 46 61 } 47 62 48 - pub fn run(cmd: VerifyCommand, dir: PathBuf) -> Result<()> { 49 - let manager = BundleManager::new(dir.clone())?; 63 + impl HasGlobalFlags for VerifyCommand { 64 + fn verbose(&self) -> bool { self.verbose } 65 + fn quiet(&self) -> bool { false } 66 + } 67 + 68 + pub fn run(mut cmd: VerifyCommand, dir: PathBuf, global_verbose: bool) -> Result<()> { 69 + // Merge global verbose flag with command's verbose flag 70 + cmd.verbose = cmd.verbose || global_verbose; 71 + let manager = super::utils::create_manager_from_cmd(dir.clone(), &cmd)?; 50 72 51 73 // Determine number of threads 52 74 let num_threads = if cmd.threads == 0 {
+121
src/cli/mod.rs
··· 1 + use anyhow::Result; 2 + use clap::{Parser, Subcommand}; 3 + use plcbundle::*; 4 + use std::path::PathBuf; 5 + 6 + // CLI Commands (cmd_ prefix) 7 + mod cmd_bench; 8 + mod cmd_did; 9 + mod cmd_diff; 10 + mod cmd_export; 11 + mod cmd_index; 12 + mod cmd_init; 13 + mod cmd_inspect; 14 + mod cmd_ls; 15 + mod cmd_mempool; 16 + mod cmd_migrate; 17 + mod cmd_op; 18 + mod cmd_query; 19 + mod cmd_random; 20 + mod cmd_rebuild; 21 + mod cmd_rollback; 22 + mod cmd_server; 23 + mod cmd_stats; 24 + mod cmd_status; 25 + mod cmd_sync; 26 + mod cmd_verify; 27 + 28 + // Helper modules (no cmd_ prefix) 29 + mod logger; 30 + mod progress; 31 + mod utils; 32 + 33 + const VERSION: &str = env!("CARGO_PKG_VERSION"); 34 + 35 + #[derive(Parser)] 36 + #[command(name = "plcbundle")] 37 + #[command(version = VERSION)] 38 + #[command(about = concat!("plcbundle v", env!("CARGO_PKG_VERSION"), " (rust) - DID PLC Bundle Management"))] 39 + #[command(long_about = concat!( 40 + "plcbundle v", env!("CARGO_PKG_VERSION"), " - DID PLC Bundle Management\n\n", 41 + "Tool for archiving AT Protocol's DID PLC Directory operations\n", 42 + "into immutable, cryptographically-chained bundles of 10,000\n", 43 + "operations each.\n\n", 44 + "Documentation: https://tangled.org/@atscan.net/plcbundle" 45 + ))] 46 + #[command(author)] 47 + #[command(propagate_version = true)] 48 + struct Cli { 49 + /// Repository directory 50 + #[arg(short = 'C', long = "dir", global = true, default_value = ".")] 51 + dir: PathBuf, 52 + 53 + /// Suppress progress output 54 + #[arg(short, long, global = true)] 55 + quiet: bool, 56 + 57 + /// Enable verbose output 58 + #[arg(short, long, global = true)] 59 + verbose: bool, 60 + 61 + #[command(subcommand)] 62 + command: Commands, 63 + } 64 + 65 + #[derive(Subcommand)] 66 + enum Commands { 67 + Query(cmd_query::QueryCommand), 68 + Init(cmd_init::InitCommand), 69 + Status(cmd_status::StatusCommand), 70 + Ls(cmd_ls::LsCommand), 71 + Verify(cmd_verify::VerifyCommand), 72 + Export(cmd_export::ExportCommand), 73 + Op(cmd_op::OpCommand), 74 + Stats(cmd_stats::StatsCommand), 75 + Did(cmd_did::DidCommand), 76 + Handle(cmd_did::HandleCommand), 77 + Index(cmd_index::IndexCommand), 78 + Mempool(cmd_mempool::MempoolCommand), 79 + Sync(cmd_sync::SyncCommand), 80 + Rollback(cmd_rollback::RollbackCommand), 81 + Diff(cmd_diff::DiffCommand), 82 + Inspect(cmd_inspect::InspectCommand), 83 + Server(cmd_server::ServerCommand), 84 + Migrate(cmd_migrate::MigrateCommand), 85 + Rebuild(cmd_rebuild::RebuildCommand), 86 + Bench(cmd_bench::BenchCommand), 87 + Random(cmd_random::RandomCommand), 88 + } 89 + 90 + fn main() -> Result<()> { 91 + let cli = Cli::parse(); 92 + 93 + // Initialize logger based on verbosity flags 94 + logger::init_logger(cli.verbose, cli.quiet); 95 + 96 + match cli.command { 97 + Commands::Query(cmd) => cmd_query::run(cmd, cli.dir, cli.quiet, cli.verbose)?, 98 + Commands::Init(cmd) => cmd_init::run(cmd)?, 99 + Commands::Status(cmd) => cmd_status::run(cmd, cli.dir)?, 100 + Commands::Ls(cmd) => cmd_ls::run(cmd, cli.dir, cli.verbose, cli.quiet)?, 101 + Commands::Verify(cmd) => cmd_verify::run(cmd, cli.dir, cli.verbose)?, 102 + Commands::Export(cmd) => cmd_export::run(cmd, cli.dir, cli.quiet, cli.verbose)?, 103 + Commands::Op(cmd) => cmd_op::run(cmd, cli.dir, cli.quiet)?, 104 + Commands::Stats(cmd) => cmd_stats::run(cmd, cli.dir)?, 105 + Commands::Handle(cmd) => cmd_did::run_handle(cmd, cli.dir)?, 106 + Commands::Did(cmd) => cmd_did::run_did(cmd, cli.dir)?, 107 + Commands::Index(cmd) => cmd_index::run(cmd, cli.dir)?, 108 + Commands::Mempool(cmd) => cmd_mempool::run(cmd)?, 109 + Commands::Sync(cmd) => cmd_sync::run(cmd)?, 110 + Commands::Rollback(cmd) => cmd_rollback::run(cmd, cli.dir)?, 111 + Commands::Diff(cmd) => cmd_diff::run(cmd, cli.dir)?, 112 + Commands::Inspect(cmd) => cmd_inspect::run(cmd, cli.dir)?, 113 + Commands::Server(cmd) => cmd_server::run(cmd, cli.dir)?, 114 + Commands::Migrate(cmd) => cmd_migrate::run(cmd, cli.dir, cli.verbose)?, 115 + Commands::Rebuild(cmd) => cmd_rebuild::run(cmd, cli.dir, cli.verbose)?, 116 + Commands::Bench(cmd) => cmd_bench::run(cmd, cli.dir)?, 117 + Commands::Random(cmd) => cmd_random::run(cmd, cli.dir)?, 118 + } 119 + 120 + Ok(()) 121 + }
+1 -41
src/cli/progress.rs
··· 7 7 pb: IndicatifProgressBar, 8 8 show_bytes: bool, 9 9 current_bytes: Arc<Mutex<u64>>, 10 - total_bytes: u64, 11 10 } 12 11 13 12 impl ProgressBar { ··· 25 24 pb, 26 25 show_bytes: false, 27 26 current_bytes: Arc::new(Mutex::new(0)), 28 - total_bytes: 0, 29 27 } 30 28 } 31 29 32 30 /// Create a progress bar with byte tracking 33 - pub fn with_bytes(total: usize, total_bytes: u64) -> Self { 31 + pub fn with_bytes(total: usize, _total_bytes: u64) -> Self { 34 32 let pb = IndicatifProgressBar::new(total as u64); 35 33 36 34 // Custom template that shows MB/s calculated from our tracked bytes ··· 46 44 pb, 47 45 show_bytes: true, 48 46 current_bytes: Arc::new(Mutex::new(0)), 49 - total_bytes, 50 47 } 51 48 } 52 49 ··· 89 86 }; 90 87 self.pb.set_message(new_msg); 91 88 } 92 - 93 - /// Add bytes to current progress 94 - #[allow(dead_code)] 95 - pub fn add_bytes(&self, increment: usize, bytes: u64) { 96 - self.pb.inc(increment as u64); 97 - let mut bytes_guard = self.current_bytes.lock().unwrap(); 98 - *bytes_guard += bytes; 99 - let current_msg = self.pb.message().to_string(); 100 - drop(bytes_guard); 101 - 102 - // Update message to include MB/s, preserving user message if present 103 - let elapsed = self.pb.elapsed().as_secs_f64(); 104 - let bytes_guard = self.current_bytes.lock().unwrap(); 105 - let bytes = *bytes_guard; 106 - drop(bytes_guard); 107 - 108 - let mb_per_sec = if elapsed > 0.0 { 109 - (bytes as f64 / 1_000_000.0) / elapsed 110 - } else { 111 - 0.0 112 - }; 113 - 114 - // Extract user message (everything after " | " if present) 115 - let user_msg = if let Some(pos) = current_msg.find(" | ") { 116 - &current_msg[pos + 3..] 117 - } else { 118 - "" 119 - }; 120 - 121 - let new_msg = if user_msg.is_empty() { 122 - format!("{:.1} MB/s", mb_per_sec) 123 - } else { 124 - format!("{:.1} MB/s | {}", mb_per_sec, user_msg) 125 - }; 126 - self.pb.set_message(new_msg); 127 - } 128 - 129 89 130 90 pub fn set_message<S: Into<String>>(&self, msg: S) { 131 91 let msg_str: String = msg.into();
+31 -14
src/cli/utils.rs
··· 6 6 7 7 pub use plcbundle::format::{format_bytes, format_number}; 8 8 9 + /// Trait for extracting global flags from command objects 10 + /// Commands that have verbose/quiet fields should implement this trait 11 + pub trait HasGlobalFlags { 12 + fn verbose(&self) -> bool; 13 + fn quiet(&self) -> bool; 14 + } 15 + 9 16 /// Parse bundle specification string into a vector of bundle numbers 10 17 pub fn parse_bundle_spec(spec: Option<String>, max_bundle: u32) -> Result<Vec<u32>> { 11 18 match spec { ··· 52 59 /// 53 60 /// # Returns 54 61 /// Number of worker threads to use 55 - pub fn get_num_workers(workers: usize, fallback: usize) -> usize { 62 + pub fn get_worker_threads(workers: usize, fallback: usize) -> usize { 56 63 if workers == 0 { 57 - match std::thread::available_parallelism() { 58 - Ok(n) => n.get(), 59 - Err(e) => { 60 - eprintln!( 61 - "Warning: Failed to detect CPU count: {}, using fallback: {}", 62 - e, fallback 63 - ); 64 - fallback 65 - } 66 - } 64 + std::thread::available_parallelism() 65 + .map(|n| n.get()) 66 + .unwrap_or(fallback) 67 67 } else { 68 68 workers 69 69 } ··· 74 74 manager.get_last_bundle() == 0 75 75 } 76 76 77 - /// Create BundleManager with optional verbose flag 78 - pub fn create_manager(dir: PathBuf, verbose: bool) -> Result<BundleManager> { 79 - Ok(BundleManager::new(dir)?.with_verbose(verbose)) 77 + /// Create BundleManager with verbose/quiet flags 78 + /// 79 + /// This is the standard way to create a BundleManager from CLI commands. 80 + /// It respects the verbose and quiet flags for logging. 81 + pub fn create_manager(dir: PathBuf, verbose: bool, _quiet: bool) -> Result<BundleManager> { 82 + let manager = BundleManager::new(dir)?; 83 + 84 + if verbose { 85 + Ok(manager.with_verbose(true)) 86 + } else { 87 + Ok(manager) 88 + } 89 + } 90 + 91 + /// Create BundleManager with global flags extracted from command 92 + /// 93 + /// Convenience function for commands that implement `HasGlobalFlags`. 94 + /// The global flags (verbose, quiet) are automatically extracted from the command. 95 + pub fn create_manager_from_cmd<C: HasGlobalFlags>(dir: PathBuf, cmd: &C) -> Result<BundleManager> { 96 + create_manager(dir, cmd.verbose(), cmd.quiet()) 80 97 } 81 98 82 99 /// Get all bundle metadata from the repository
+4 -21
src/did_index.rs
··· 162 162 // ============================================================================ 163 163 164 164 struct Shard { 165 - #[allow(dead_code)] 166 - shard_num: u8, 167 165 base: Option<Mmap>, 168 - #[allow(dead_code)] 169 - base_file: Option<File>, 170 166 segments: Vec<SegmentLayer>, 171 167 last_used: AtomicU64, 172 168 access_count: AtomicU64, 173 169 } 174 170 175 171 struct SegmentLayer { 176 - #[allow(dead_code)] 177 172 meta: DeltaSegmentMeta, 178 173 mmap: Mmap, 179 174 _file: File, ··· 186 181 } 187 182 188 183 impl Shard { 189 - fn new_empty(shard_num: u8) -> Self { 184 + fn new_empty(_shard_num: u8) -> Self { 190 185 Shard { 191 - shard_num, 192 186 base: None, 193 - base_file: None, 194 187 segments: Vec::new(), 195 188 last_used: AtomicU64::new(unix_timestamp()), 196 189 access_count: AtomicU64::new(0), 197 190 } 198 191 } 199 192 200 - fn load(shard_num: u8, shard_path: &Path) -> Result<Self> { 201 - let file = File::open(shard_path)?; 202 - let mmap = unsafe { MmapOptions::new().map(&file)? }; 193 + fn load(_shard_num: u8, shard_path: &Path) -> Result<Self> { 194 + let _file = File::open(shard_path)?; 195 + let mmap = unsafe { MmapOptions::new().map(&_file)? }; 203 196 204 197 Ok(Shard { 205 - shard_num, 206 198 base: Some(mmap), 207 - base_file: Some(file), 208 199 segments: Vec::new(), 209 200 last_used: AtomicU64::new(unix_timestamp()), 210 201 access_count: AtomicU64::new(1), ··· 2068 2059 } 2069 2060 2070 2061 Ok(()) 2071 - } 2072 - 2073 - #[allow(dead_code)] 2074 - fn update_config(&self, total_dids: i64, last_bundle: i32) -> Result<()> { 2075 - self.modify_config(|config| { 2076 - config.total_dids = total_dids; 2077 - config.last_bundle = last_bundle; 2078 - }) 2079 2062 } 2080 2063 2081 2064 fn modify_config<F>(&self, mutator: F) -> Result<()>
-9
src/manager.rs
··· 1953 1953 )) 1954 1954 } 1955 1955 1956 - /// Save bundle to disk with compression and index updates (backwards compatibility) 1957 - #[allow(dead_code)] 1958 - async fn save_bundle(&self, bundle_num: u32, operations: Vec<Operation>) -> Result<()> { 1959 - self.save_bundle_with_timing(bundle_num, operations).await?; 1960 - Ok(()) 1961 - } 1962 - 1963 1956 /// Migrate a bundle to multi-frame format 1964 1957 /// 1965 1958 /// This method loads a bundle and re-saves it with multi-frame compression ··· 2294 2287 2295 2288 /// Rollback repository to a specific bundle 2296 2289 pub fn rollback_to_bundle(&mut self, target_bundle: u32) -> Result<()> { 2297 - use anyhow::Context; 2298 - 2299 2290 let mut index = self.index.write().unwrap(); 2300 2291 2301 2292 // Keep only bundles up to target
-18
src/server/error.rs
··· 46 46 msg.contains("not found") || msg.contains("not in index") 47 47 } 48 48 49 - /// Convert a Result to an HTTP response, handling common error cases 50 - pub fn handle_result<T: IntoResponse>( 51 - result: Result<T, anyhow::Error>, 52 - ) -> impl IntoResponse { 53 - match result { 54 - Ok(response) => response.into_response(), 55 - Err(e) => { 56 - if e.to_string().contains("deactivated") { 57 - gone("DID has been deactivated").into_response() 58 - } else if is_not_found_error(&e) { 59 - not_found(&e.to_string()).into_response() 60 - } else { 61 - internal_error(&e.to_string()).into_response() 62 - } 63 - } 64 - } 65 - } 66 -